[TOC]
描述: Go 语言中的指针区别于C/C++中的指针,Go语言中的指针不能进行偏移和运算是安全指针。
Go 语言中三个重要概念: 指针地址、指针类型以及指针取值。
简单回顾: 任何程序数据载入内存后,在内存都有他们的地址这就是指针。而为了保存一个数据在内存中的地址,我们就需要指针变量。
比如,“永远不要高估自己”这句话是我的座右铭,我想把它写入程序中,程序一启动这句话是要加载到内存(假设内存地址0x123456),我在程序中把这段话赋值给变量A,把内存地址赋值给变量B。这时候变量B就是一个指针变量, 通过变量A和变量B都能找到我的座右铭。
Go语言中的指针操作非常简单,我们只需要记住两个符号:&(取地址) 和 *(根据地址取值)。
描述: 每个变量在运行时都拥有一个地址,该地址代表变量在内存中的位置。
Go语言中使用&字符放在变量前面对变量进行“取地址”操作。
取变量指针(地址)的语法如下:
ptr := &v // v的类型为T // # 参数 // v:代表被取地址的变量,类型为T // ptr:用于接收地址的变量,ptr的类型就为*T,称做T的指针类型。*代表指针。
描述: Go语言中的值类型(int、float、bool、string、array、struct)都有对应的指针类型,如:*int、*int64、*string等。
简单示例:
func main() { a := 10 b := &a fmt.Printf("a:%d ptr:%p ", a, &a) // a:10 ptr:0xc00001a078 (指针地址) fmt.Printf("*b:%d ptr:%p type:%T ",*b, b, b) // b:10 ptr:0xc00001a078 type:*int (指针类型) fmt.Printf("&b ptr:%p ",&b) // &b ptr:0xc00000e018 }
为了更好的理解指针地址,我们来看一下b := &a的图示:
描述: 在对普通变量使用&操作符取地址后会获得这个变量的指针,然后可以对指针使用*操作,也就是指针取值,代码如下。
func main() { //指针取值 a := 10 b := &a // 取变量a的地址,将指针保存到b中 fmt.Printf("type of b:%T ", b) c := *b // 指针取值(根据指针去内存取值) fmt.Printf("type of c:%T ", c) fmt.Printf("value of c:%v ", c) }
输出结果:
type of b:*int type of c:int value of c:10
描述: 通过上面的指标变量、类型、取值的学习,我们了解到取地址操作符&和取值操作符*是一对互补操作符,其中&取出地址,*根据地址取出地址指向的值。
Tips : 变量、指针地址、指针变量、取地址、取值的相互关系和特性如下:
1.对变量进行取地址(&)操作,可以获得这个变量的指针变量(指针地址)。2.对指针变量进行取值(*)操作,可以获得指针变量指向的原变量的值。例如:我们可以在局部函数中修改全局变量的值采用指针传值:
func modify1(x int) { x = 100 } func modify2(x *int) { *x = 100 } func main() { a := 10 modify1(a) fmt.Println(a) // 10 modify2(&a) fmt.Println(a) // 100 }
描述: 在Go语言中对于引用类型的变量,我们在使用的时候不仅要声明它,还要为它分配内存空间,否则我们的值就没办法存储。而对于值类型的声明不需要分配内存空间,是因为它们在声明的时候已经默认分配好了内存空间。 Tips :Go语言中new和make是内建的两个函数,他主要用来分配内存。
例如:执行下述例子中的代码会引发panic错误
func main() { // 声明 var a *int // 定义 *a = 100 fmt.Println(*a) var b map[string]int b["沙河娜扎"] = 100 fmt.Println(b) }
New 函数 描述: new是Go语言的一置的函数它的函数签名如下:
func new(Type) *Type
其中,
Type 表示类型,new 函数只接受一个参数,这个参数是一个类型*Type 表示类型指针,new 函数返回一个指向该类型内存地址的指针。Tips :New 函数不太常用但由它可以得到一个类型的指针,并且该指针对应的值应该为该类型的零值。
func main() { // 只是声明了一个指针变量a但是没有初始化 a := new(int) b := new(bool) fmt.Printf("%T ", a) // *int fmt.Printf("%T ", b) // *bool fmt.Println(*a) // 0 fmt.Println(*b) // false }
Tips : 指针作为引用类型需要初始化后才会拥有内存空间才可以给它赋值,所以需要按照下述方式使用内置的new函数对a进行初始化之后就可正常对其赋值了。
func main() { var a *int a = new(int) *a = 10 fmt.Println(*a) }
make 函数 描述: make也是用于内存分配的,区别于new,它只用于slice、map以及chan的内存创建,而且它返回的类型就是这三个类型本身,而不是他们的指针类型,因为这三种类型就是引用类型,所以就没有必要返回他们的指针了。
函数签名如下:
func make(t Type, size ...IntegerType) Type
Tips : Type 主要是 slice、map 以及channel类型,并且必须使用make进行初始化后,才能对它进行操作。
例如:
func main() { // 只是声明变量b是一个map类型的变量 var b map[string]int //使用make函数进行初始化操作之后 b = make(map[string]int, 10) //才能对其进行键值对赋值: b["WeiyiGeek"] = 100 fmt.Println(b) }
总结:new 函数与 make函数的区别
二者都是用来做内存分配的。make只用于slice、map以及channel的初始化,返回的还是这三个引用类型本身;new用于类型的内存分配,并且内存对应的值为类型零值,返回的是指向类型的指针。示例演示:
// 转入int类型的参数 func normal(x int) { x = 65535 fmt.Printf("Func Param &x ptr : %p ", &x) } // 传入的参数为指针类型 func pointer(x *int) { *x = 65535 fmt.Printf("Func Param x ptr : %p ", x) } func demo1() { // 1.2获得变量a的内存地址 a := 1024 b := &a fmt.Printf("a : %d , a ptr: %p, b ptr : %v , *b = %d ", a, &a, b, *b) fmt.Printf("b type: %T, &b ptr : %p ", b, &b) fmt.Println() // 2.针对变量a的内存地址进行重赋值(此时会覆盖变量a的原值) *b = 2048 fmt.Printf("Change -> a : %d , a ptr: %p, b ptr : %v , *b = %d ", a, &a, b, *b) fmt.Printf("b type: %T, &b ptr : %p ", b, &b) // 3.指针传值 c := 4096 normal(c) fmt.Println("After Normal Function c : ", c) pointer(&c) fmt.Printf("After Pointer Function c : %v, c ptr: %p ", c, &c) // 4.new 内存地址申请 var a4 *int //*a4 = 100 // 此行会报 _panic 错误,因为未分配内存空间 fmt.Println("a4 ptr : ", a4) // 空指针 (<nil>)还没有内存地址 d := new(int) // 申请一块内存空间 (内存地址) fmt.Printf("%T ,%p, %v ", d, d, *d) // 其指针类型默认值为 0 与其类型相关联。 *d = 8192 // 对该内存地址赋值 fmt.Printf("%T ,%p, %v ", d, d, *d) // 5.make 内存地址申请 var b5 map[string]string //b5["Name"] = "WeiyGeek" //此行会报 _panic 错误,因为未分配内存空间 fmt.Printf("%T , %p , %v ", b5, &b5, *&b5) b5 = make(map[string]string, 10) // 申请一块内存空间 (内存地址) b5["Name"] = "WeiyGeek" // 此时便可对该Map类型进行赋值了 b5["Address"] = "ChongQIng China" fmt.Printf("%T , %p , %v ", b5, &b5, b5) }
执行结果:
a : 1024 , a ptr: 0xc00001a0d8, b ptr : 0xc00001a0d8 , *b = 1024 b type: *int, &b ptr : 0xc00000e028 Change -> a : 2048 , a ptr: 0xc00001a0d8, b ptr : 0xc00001a0d8 , *b = 2048 b type: *int, &b ptr : 0xc00000e028 Func Param &x ptr : 0xc00013a048 After Normal Function c : 4096 Func Param x ptr : 0xc00013a040 After Pointer Function c : 65535, c ptr: 0xc00013a040 a4 ptr : <nil> *int ,0xc00001a130, 0 *int ,0xc00001a130, 8192 map[string]string , 0xc00000e038 , map[] map[string]string , 0xc00000e038 , map[Address:ChongQIng China Name:WeiyGeek]
描述: 其实在前面的示例中我们都已经接触到了函数,例如Go语言的内置函数或者是您自己编写的函数,在本章之中我们将详细讲解Go语言函数的使用。
Why , 为啥各个编程语言都要要引入函数?
答: 函数是组织好的、可重复使用的、用于执行指定任务的代码块。 通用得说即减少代码量、增强可读性、代码复用、提高开发效率、节约资源等特点。
描述: Go语言中支持函数、匿名函数和闭包,并且函数在Go语言中属于一等公民。
Go语言中定义函数使用func关键字,具体格式如下:
func 函数名(参数)(返回值){ 函数体 }
其中:
函数名:由字母、数字、下划线组成。但函数名的第一个字母不能是数字。注意在同一个包内函数名也称不能重名(包的概念详见后文)。参数:参数由参数变量和参数变量的类型组成,多个参数之间使用,分隔。返回值:返回值由返回值变量和其变量类型组成,也可以只写返回值的类型,多个返回值必须用()包裹,并用,分隔。函数体:实现指定功能的代码块。示例1
// 方式1 func sayHello() { fmt.Println("Hello World, Let's Go") }
描述: 定义了函数之后,我们可以通过函数名()的方式调用函数。
例如.我们调用上面定义的函数代码如下:
func main() { fmt.Println("Start") sayHello() fmt.Println("End") }
描述: 通常我们需要为函数传递参数进行相应的处理以达到我们最终需要的产物。
函数参数类型
固定参数可变参数常规参数类型 针对固定函数的参数我们需要制定其类型,例如
func intSum(x int, y int) { fmt.Println("x + y =",x+y) }
参数类型简写 函数的参数中如果相邻变量的类型相同,则可以省略类型,例如:
func intSum(x , y int) { fmt.Println("x + y =",x+y) }
Tips : 上面的代码中,intSum函数有两个参数,这两个参数的类型均为int,因此可以省略x的类型,因为y后面有类型说明,x参数也是该类型。
描述: 可变参数是指函数的参数数量不固定。Go语言中的可变参数通过在参数名后加 ... 来标识(是否似曾相识,我们在数组那章节时使用过它,表示自动判断数组中元素个数进行初始化操作)。
示例1:
func intSum2(x ...int) int { fmt.Println(x) //x是一个切片 sum := 0 for _, v := range x { sum = sum + v } return sum }
调用上面的函数:
ret1 := intSum2() ret2 := intSum2(10) ret3 := intSum2(10, 20) ret4 := intSum2(10, 20, 30) fmt.Println(ret1, ret2, ret3, ret4) //0 10 30 60
注意:可变参数通常要作为函数的最后一个参数。
固定参数搭配可变参数使用时,可变参数要放在固定参数的后面,
示例2
func intSum3(x int, y ...int) int { fmt.Println(x, y) sum := x for _, v := range y { sum = sum + v } return sum }
调用上述函数:
ret5 := intSum3(100) ret6 := intSum3(100, 10) ret7 := intSum3(100, 10, 20) ret8 := intSum3(100, 10, 20, 30) fmt.Println(ret5, ret6, ret7, ret8) //100 110 130 160
Tips : 本质上,函数的可变参数是通过切片来实现的。
描述: 与其他编程语言一样,Go语言中通过return关键字向外输出返回值。
描述: Go语言中常规函数返回值。
举个例子:
func sum(x, y int)(res int) { return x + y }
描述: Go语言中函数支持多返回值,函数如果有多个返回值时必须用()将所有返回值包裹起来。
举个例子:
func calc(x, y int) (int, int) { sum := x + y sub := x - y return sum, sub }
函数调用并接收返回值:
sum,sub := calc(5, 3) // 8 , 2
描述: 函数定义时可以给返回值命名,并在函数体中直接使用这些变量,最后通过return关键字返回。
举个例子:
func calc(x, y int) (sum, sub int) { sum = x + y sub = x - y return }
函数调用并接收返回值:
sum,sub := calc(5, 3) // 8 , 2
Tips :如果使用返回值命令时,只要其中一个返回值命名则另外一个返回值也必须命名。
描述: 当我们的一个函数返回值类型为slice时,nil可以看做是一个有效的slice,没必要显示返回一个长度为0的切片。
func someFunc(x string) []int { if x == "" { return nil // 没必要返回[]int{} } ... }
描述: 变量作用域分为全局变量作用域和局部变量作用域以及代码块作用域
描述: 全局变量是定义在函数外部的变量,它在程序整个运行周期内都有效, 在函数中可以访问到全局变量。
package main import "fmt" //定义全局变量num var num int64 = 10 func testGlobalVar() { fmt.Printf("num=%d ", num) //函数中可以访问全局变量num } func main() { testGlobalVar() // 10 fmt.Printf("num=%d ", num) // 10 }
描述: 局部变量由分为两类一种是在函数内部定义的局部变量,另外一种则是在函数内部代码块中定义的局部变量
类1.在函数内定义的变量无法在该函数外使用。
例如: 下面的示例代码main函数中无法使用testLocalVar函数中定义的变量x
func testLocalVar() { //定义一个函数局部变量x,仅在该函数内生效 var x int64 = 100 fmt.Printf("x=%d ", x) } func main() { testLocalVar() fmt.Println(x) // 此时无法使用变量x,并且此时会报错undefine x。 }
类2.在函数内的语句块定义的变量 描述: 通常我们会在if条件判断、for循环、switch语句上使用这种定义变量的方式。
// if 代码块 func testLocalVar2(x, y int) { fmt.Println(x, y) //函数的参数也是只在本函数中生效 if x > 0 { z := 100 //变量z只在if语句块生效 fmt.Println(z) } //fmt.Println(z) //此处无法使用变量z } // for 代码块 func testLocalVar3() { for i := 0; i < 10; i++ { fmt.Println(i) //变量i只在当前for语句块中生效 } // fmt.Println(i) //此处无法使用变量i }
Tips : 如果局部变量和全局变量重名,则优先访问局部变量。
Tips : 函数中查找变量的顺序步骤,(1) 现在函数内部查找,(2) 在函数上层或者外层查找, (3) 最后在全局中查找(此时如果找不到则会报错)
描述: 我们可以使用type关键字来定义一个函数类型,具体格式如下:type calculation func(int, int) int
上面语句定义了一个calculation类型,它是一种函数类型,这种函数接收两个int类型的参数并且返回一个int类型的返回值。
简单来说,凡是满足这个条件的函数都是calculation类型的函数,例如下面的add和sub都是calculation类型的函数。
示例:
type calculation func(int, int) int func add(x, y int) int { return x + y } func sub(x, y int) int { return x - y } // add和sub都能赋值给calculation类型的变量。 var c calculation c = add fmt.Println(c(1,2)) // 函数类型变量传递
描述: 我们可以声明函数类型的变量并且为该变量赋值:
func main() { var c calculation // 声明一个calculation类型的变量c c = add // 把add赋值给calculation类型的变量c fmt.Printf("type of c:%T ", c) // type of c:main.calculation (区别点) fmt.Println(c(1, 2)) // 像调用add一样调用c 1 + 2 = 3 f := sub // 将函数sub赋值给变量f1 fmt.Printf("type of f:%T ", f) // type of f:func(int, int) int (区别点) 非函数类型的变量 fmt.Println(f(30, 20)) // 像调用add一样调用f 30 - 20 = 10 }
描述: 高阶函数分为函数作为参数和函数作为返回值两部分。
函数可以作为参数示例:
func add(x, y int) int { return x + y } func calc(x, y int, op func(int, int) int) int { return op(x, y) } func main() { ret2 := calc(10, 20, add) fmt.Println(ret2) //30 }
函数也可以作为返回值示例(此种方式非常值得学习):
func do(s string) (func(int, int) int, error) { switch s { case "+": return add, nil case "-": return sub, nil default: err := errors.New("无法识别的操作符") return nil, err } }
Q: 什么是递归(Recursion)函数?
答: 递归,就是在运行的过程中函数调用自身。 但是值得注意的是我们在使用递归时,开发者需要设置退出条件,否则递归将陷入无限循环中(一定一定要有退出条件)。
语法格式:
func recursion() { recursion() /* 函数调用自身 */ } func main() { recursion() }
Tips : 递归函数对于解决数学上的问题是非常有用的,就像计算阶乘,生成斐波那契数列等。
示例1.n的阶乘计算
func factorial(n uint64) (ret uint64) { if n <= 1 { return 1 } return n * factorial(n-1) } func demo1() { fmt.Println("5 的阶乘 : ", factorial(5)) }
执行结果:
5 的阶乘 : 120
示例2.利用递归求斐波那契数列
// 方式1 func Fibonacci(count uint64) (ret uint64) { if count == 0 { return 0 } if count == 1 || count == 2 { return 1 } ret = Fibonacci(count-1) + Fibonacci(count-2) return } func demo3() { count := 10 fmt.Printf("%v 个斐波那契数列:", count) for i := 1; i < count; i++ { fmt.Printf("%v ", Fibonacci(uint64(i))) } } // 方式2.值得学习 // fib returns a function that returns successive Fibonacci numbers. func fib() func() int { a, b := 0, 1 return func() int { a, b = b, a+b return a } } func main() { f := fib() fmt.Println(f(), f(), f(), f(), f()) }
执行结果:
10 个斐波那契数列:1 1 2 3 5 8 13 21 34
描述: 函数当然还可以作为返回值,但是在Go语言中函数内部不能再像之前那样定义函数了,只能定义匿名函数。
Q: 什么是匿名函数?
答: 匿名函数就是没有函数名的函数,在很多编程语言中都有这样的特性。 匿名函数多用于实现回调函数和闭包。
Tips : 匿名函数因为没有函数名,所以没办法像普通函数那样调用,所以匿名函数需要保存到某个变量或者作为立即执行函数:
func main() { // 方式1.将匿名函数保存到变量 add := func(x, y int) { fmt.Println(x + y) } add(10, 20) // 通过变量调用匿名函数 //方式2.自执行函数:匿名函数定义完加()直接执行 func(x, y int) { fmt.Println(x + y) }(10, 20) }
描述: 闭包指的是一个函数和与其相关的引用环境组合而成的实体。简单来说,闭包=函数+外遍变量的引用, 例如在第三方包里只能传递一个不带参数的函数,此时我们可以通过闭包的方式创建一个带参数处理的流程,并返回一个不带参数的函数。
Tips : 非常注意引用的外部外部变量在其生命周期内都是存在的(即下次调用还能使用该变量值)。
闭包基础示例1:
func adder() func(int) int { var x int // 在f的生命周期内,变量x也一直有效 return func(y int) int { x += y return x } } func main() { var f = adder() fmt.Println(f(10)) //x=0,y=10 -> x = 10 fmt.Println(f(20)) //x=10,y=20 -> x = 30 fmt.Println(f(30)) //x=30,y=30 -> x = 60 f1 := adder() fmt.Println(f1(40)) //40 fmt.Println(f1(50)) //90 }
闭包基础示例2:
package main import ( "fmt" "math" ) // 1.假设这是个第三方包 func f1(f func()) { fmt.Printf("# This is f1 func , Param is f func() : %T ", f) f() // 调用传入的函数 } // 2.自己实现的函数 func f2(x, y int) { fmt.Printf("# This is f2 func , Param is x,y: %v %v ", x, y) fmt.Printf("x ^ y = %v ", math.Pow(float64(x), float64(y))) } // 要求 f1(f2) 可以执行,此时由于f1 中的传递的函数参数并无参数,所以默认调用执行一定会报错。 // 此时我们需要一个中间商利用闭包和匿名函数来实现,返回一个不带参数的函数。 func f3(f func(int, int), x, y int) func() { tmp := func() { f(x, y) // 此处实际为了执行f2函数 } return tmp // 返回一个不带参数的函数,为返回给f1函数 } func main() { ret := f3(f2, 2, 10) // 此时函数并为执行只是将匿名函数进行返回。先执行 f3(fun,x,y int) f1(ret) // 当传入f1中时ret()函数便会进行执行。再执行 f1() ,最后执行 f2(x,y int) }
执行结果:
# This is f1 func , Param is f func() : func() # This is f2 func , Param is x,y: 2 10 x ^ y = 1024
Tips : 变量f是一个函数并且它引用了其外部作用域中的x变量,此时f就是一个闭包。并且在f的生命周期内,变量x也一直有效。
闭包进阶示例1:相比较于上面这种方式该种是将x变量放入函数参数之中,在进行函数调用时赋值。
func adder2(x int) func(int) int { return func(y int) int { x += y return x } } func main() { var f = adder2(10) // `在f的生命周期内,变量x也一直有效。` fmt.Println(f(10)) //20 fmt.Println(f(20)) //40 fmt.Println(f(30)) //70 f1 := adder2(20) fmt.Println(f1(40)) //60 fmt.Println(f1(50)) //110 }
闭包进阶示例2:判断文件名称是否以指定的后缀结尾,是则返回原文件名称,否则返回文件名称+指定后缀的文件。
func makeSuffixFunc(suffix string) func(string) string { return func(name string) string { // 判断name变量中的字符串是否已suffix结尾 if !strings.HasSuffix(name, suffix) { return name + suffix } return name } } func main() { jpgFunc := makeSuffixFunc(".jpg") txtFunc := makeSuffixFunc(".txt") fmt.Println(jpgFunc("test")) //test.jpg fmt.Println(txtFunc("test")) //test.txt }
闭包进阶示例3:该示例中函数同时返回add,sub两个函数.
func calc(base int) (func(int) int, func(int) int) { add := func(i int) int { base += i return base } sub := func(i int) int { base -= i return base } return add, sub } func main() { f1, f2 := calc(10) fmt.Println(f1(1), f2(2)) //11 9 fmt.Println(f1(3), f2(4)) //12 8 fmt.Println(f1(5), f2(6)) //13 7 }
Important : 闭包其实并不复杂,只要牢记闭包=函数+引用环境。
描述: Go语言中的defer语句会将其后面跟随的语句进行延迟处理。在defer归属的函数即将返回时,将延迟处理的语句按defer定义的逆序进行执行(压栈-后进先出),也就是说,先被defer的语句最后被执行,最后被defer的语句,最先被执行。
(1) defer 执行时机 描述: 在Go语言的函数中return语句在底层并不是原子操作,它分为给返回值赋值和RET指令两步。而defer语句执行的时机就在返回值赋值操作后,RET指令执行前。
具体如下图所示:
脑叶公司EGO防具怎么排名?-脑叶公司EGO防具排名教程攻略
脑叶公司EGO防具怎么排名?-脑叶公司EGO防具排名教程攻略