1、函数的声明
语法:func 函数名(参数列表)(返回参数列表){函数体}
- 函数名遵循标识符的命名规则,首字母的大小写决定该函数在其他包的可见性:大写时其他包可见,小写时只有相同的包可以访问;
- 函数的参数和返回值需要使用“()”,如果只有一个返回值,而且使用的是非命名的参数,则返回参数的“()”可以省略。
- 函数体使用“{}”,并且“{”必须位于函数返回值同行的行尾。
func main() {sum := add(1, 2)fmt.Println(sum)
}func add(a, b int) int {return a + b
}
声明一个在外部定义的函数只需给出函数名与函数签名即可,不需要写出完整的函数体,例如:
func hello(str,num int) //外部实现
函数同样可以通过声明的方式作为一个函数类型被使用,例如:type addNum func (int, int)int
- 此处不需要函数体“{}”,因为函数在Go语言中属于一等值(frst-class value)
- 函数也可以赋值给变量,如add := addNum,由于变量指向了addNum函数的签名,所以,不能再给它赋一个具有不同签名的函数值。
函数的特点:
- 函数可以没有输入参数,也可以没有返回值(默认为0)
- 多个相邻的相同类型的参数可以使用简写模式
- 支持有名的返回值,参数名相当于函数体内最外层的局部变量,命名返回值变量会被初始化为类型零值,最后的return可以不带参数名直接返回。
- 不支持默认值参数。
- 不支持函数重载。
- 不支持函数嵌套,严格来说是不支持命名函数的嵌套,但支持嵌套匿名函数。
func add(a int,b int) int {return a+b
}// 简写为func add(a,b int) int {return a+b
}
2、函数的调用
调用前的函数局部变量都会被保存起来不会丢失,被调用的函数运行结束后,恢复到调用函数的下一行继续执行代码,之前的局部变量也能继续访问。函数内的局部变量只能在函数体中使用,函数调用结束后,这些局部变量都会被释放并且失效。
语法:返回值变量列表 = 函数名(参数列表)
func main() {var num1 int = 200var num2 int = 100var maxNum intmaxNum = max(num1, num2)fmt.Printf("最大值是:%d\n", maxNum)
}/* 函数返回两个数的最大值 */
func max(num1, num2 int) int {var result intif num1 > num2 {result = num1} else {result = num2}return result
}
3、函数的参数
函数可以有一个或多个参数,每个参数后面都带有类型,通过“,”符号分隔。如果参数列表中若干个相邻参数的类型相同,则可以在参数列表中省略前面变量的类型声明:
func add(a,b int) (ret int, err error) {函数体
}
如果返回值列表中多个返回值的类型相同,也可以使用同样的方式合并。如果函数只有一个返回值,可以写成如下形式:
func add(a,b int) int {
}
函数如果使用参数,该变量可称为函数的形参。形参就像定义在函数体内的局部变量。
调用函数,可以通过两种方式来传递参数:值传递、引用传递。
a. 值传递
值传递是指在调用函数时将实际参数复制传递到函数中,这样在函数中如果对参数进行修改,将不会影响实际参数。默认情况下,Go语言使用的是值传递,即在调用过程中不会影响实际参数。
func main() {/* 定义局部变量 */var a int = 100var b int = 200fmt.Printf("交换前 a 的值为:%d\n", a)fmt.Printf("交换前 b 的值为:%d\n", b)/* 通过调用函数来交换值 */swapNum(a, b)fmt.Printf("交换后 a 的值为:%d\n", a)fmt.Printf("交换后 b 的值为:%d\n", b)
}/* 定义相互交换值的函数 */
func swapNum(x, y int) int {var temp inttemp = xx = yy = tempreturn temp
}
b. 引用传递
引用传递是指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响实际参数。引用传递指针参数传递到函数内,例如,交换函数swap()使用引用传递:
func main() {/* 定义局部变量 */var a int = 100var b int = 200fmt.Printf("交换前 a 的值为:%d\n", a)fmt.Printf("交换前 b 的值为:%d\n", b)/** &a 指向 a指针,a 变量的地址* &b 指向 b指针,b 变量的地址*/swapRef(&a, &b)fmt.Printf("交换后 a 的值为:%d\n", a)fmt.Printf("交换后 b 的值为:%d\n", b)
}func swapRef(x, y *int) {var temp inttemp = *x*x = *y*y = temp
}
4、函数的返回值
Go语言既支持安全指针,也支持多返回值,因此在使用函数进行逻辑编写时更为方便。
Go语言支持多返回值,多返回值能方便地获得函数执行后的多个返回参数。Go语言经常使用多返回值中的最后一个返回参数返回函数执行中可能发生的错误:
conn, err := connectToNetwork()
connectToNetwork返回两个参数,conn表示连接对象,err表示返回的错误信息。
如果返回值有多个,则用括号将多个返回值类型括起来,用逗号分隔每个返回值的类型。使用return语句返回时,值列表的顺序需要与函数声明的返回值类型一致。
func multiValues() (int, int, string) {return 1, 22, "tom"
}func main() {sex, age, name := multiValues()fmt.Printf("我叫%s,今年%d岁,我是%d", name, age, sex) // 我叫tom,今年22岁,我是1
}
- 以上属于纯类型的返回值,这种方式对于代码可读性不是很友好,特别是在同类型的返回值出现时,无法区分每个返回参数的意义。
Go语言支持对返回值进行命名,这样返回值就和参数一样拥有参数变量名和类型。命名的返回值变量的默认值为类型的默认值,即数值为0,字符串为空字符串,布尔值为false,指针为nil。
func multiValues() (sex int , age int, name string) {return 1, 22, "tom"
}func main() {sex, age, name := multiValues()fmt.Printf("我叫%s,今年%d岁,我是%d", name, age, sex)
}
- 同类型的返回值可以只写一个:
(sex int , age int, name string)
->(sex , age int, name string)
注意:如果只有一个返回值且不声明返回值变量,那么可以省略返回值(包括返回值的括号都可以不写)。如果没有返回值,那么就直接省略最后的返回信息,什么都不用写;如果有返回值,那么必须在函数的最后添加return语句。
5、函数类型和匿名函数
Go语言支持匿名函数,即在需要使用函数时再定义函数,匿名函数没有函数名,只有函数体,函数可以作为一种类型被赋值给函数类型的变量。匿名函数也往往以变量的方式被传递。
a. 函数类型
函数类型和map()、slice()一样,实际函数类型变量和函数名都可以当作指针变量,该指针指向函数代码的开始位置。通常说函数类型变量是一种引用类型,未初始化的函数类型的变量的默认值是nil。
func main() {sum := do(add, 1, 3)fmt.Println("1 + 3 = ", sum)sub := do(sub, 1, 3)fmt.Println("1 - 3 = ", sub)}type Op func(int, int) int // 定义一个函数类型,输入的是两个int类型,返回值是一个int类型func do(f Op, a, b int) int {return f(a, b) // 函数类型变量可以直接用来进行函数调用
}func add(a, b int) int {return a + b
}func sub(a, b int) int {return a - b
}
Go语言中有名函数的函数名可以看作函数类型的常量,可以直接使用函数名调用函数,也可以直接赋值给函数类型变量,后续通过该变量来调用该函数:
func main() {fmt.Println(add(1, 2)) //直接调用f := add // 直接赋给函数类型变量fmt.Println(f(3, 5))}func add(a, b int) int {return a + b
}
b. 匿名函数
匿名函数是指不需要定义函数名的一种函数实现方式,由一个不带函数名的函数声明和函数体组成。
① 在定义时调用匿名函数。匿名函数可以在声明后调用
func main() {func(name string) {fmt.Println("hello", name)}("Lebron")
}
② 将匿名函数赋值给变量。匿名函数可以被赋值。匿名函数本身就是一种值,可以方便地保存在各种容器中,实现回调函数和操作的封装。
func main() {// 将匿名函数保存在f()中f := func(name string) {fmt.Println("hello", name)}f("James")
}
③ 匿名函数用作回调函数。例如:实现对切片的遍历操作。
func main() {// 使用匿名函数打印切片内容visit([]int{1, 2, 3, 4}, func(v int) {fmt.Printf("%d\t", v) // 1 2 3 4})
}// 遍历切片的每个元素,通过给定函数进行元素访问
func visit(list []int, f func(int)) {for _, v := range list {f(v)}
}
④ 使用匿名函数实现操作封装。例如:将匿名函数作为map的键值,通过命令行参数动态调用匿名函数。
var skillParam = flag.String("skill", "", "skill to perform") // 6func main() {flag.Parse()var skill = map[string]func(){"fire": func() {fmt.Println("chicken fire")},"run": func() {fmt.Println("soldier run")},"fly": func() {fmt.Println("angel fly")},}if f, ok := skill[*skillParam]; ok {f()} else {fmt.Println("skill not found")}}
- 第1行,定义命令行参数skill,从命令行输入--skill,可以将=后的字符串传入skillParam指针变量。
- 第4行,解析命令行参数,解析完成后,skillParam指针变量将指向命令行传入的值。
- 第5行,定义一个从字符串映射到func()的map,然后填充这个map。
- 第6~14行,初始化map的键值对,值为匿名函数。
- 第16行,skillParam是一个*string类型的指针变量,使用*skillParam获取命令行传过来的值,并在map中查找对应命令行参数指定的字符串的函数。
- 如果在map定义中存在这个参数就调用,否则打印“skill not found”。
⑤ 匿名函数可以看作函数字面量,所有直接使用函数类型变量的地方都可以由匿名函数代替。匿名函数可以直接赋值给函数变量,可以当作实参,也可以作为返回值,还可以直接被调用:
// 匿名函数被直接复制函数变量
var sum1 = func(a, b int) int {return a + b
}// 匿名参数作为参数
func doinput(f func(int, int) int, a, b int) int {return f(a, b)
}// 匿名函数作为返回值
func wrap(op string) func(int, int) int {switch op {case "add":return func(a, b int) int {return a + b}case "sub":return func(a, b int) int {return a + b}default:return nil}
}func main() {// 匿名函数直接被调用defer func() {if err := recover(); err != nil {fmt.Println(err)}}()fmt.Println(sum1(1, 2)) // 3// 匿名参数作为实参result := doinput(func(x, y int) int {return x + y}, 1, 2)fmt.Println(result) // 3opFunc := wrap("add")re := opFunc(2, 3) // 5fmt.Printf("%d\n", re)
}
6、函数类型实现接口
函数和其他类型一样,其他类型能够实现接口,函数也可以实现接口。
a. 结构体实现接口
结构体实现Invoker接口。
// 调用器接口
type Invoker interface {// 需要实现一个Call方法Call(interface{})
}// 结构体类型
type Struct struct{}// 实现Invoker的Call
func (s *Struct) Call(p interface{}) {fmt.Println("from struct", p)
}func main() {// 声明接口变量var invoker Invoker// 实例化结构体s := new(Struct)// 将实例化的结构体复制到接口invoker = s// 使用接口调用实例化结构体的方法Struct.Callinvoker.Call("hello")
}
- s类型为*Struct,已经实现了Invoker接口类型,因此赋值给invoker时是成功的。
- 通过接口的Call()方法,传入hello,此时将调用Struct结构体的Call()方法。
b. 函数体实现接口
函数的声明不能直接实现接口,需要将函数定义为类型后,使用类型实现结构体,当类型方法被调用时,还需要调用函数本体
// 调用器接口
type Invoker interface {// 实现Call方法Call(interface{})
}// 函数定义为类型
type FuncCaller func(interface{})// 实现Invoker的Call
func (f FuncCaller) Call(p interface{}) {// 调用f()函数本体f(p)
}func main() {// 声明接口变量var invoker Invoker// 将匿名函数转为FuncCaller类型,再赋值给接口invoker = FuncCaller(func(v interface{}) {fmt.Println("from function", v)})// 使用接口调用FuncCaller.Call,内部会调用函数本体invoker.Call("hello")
}
- 将func(v interface{}){}匿名函数转换为FuncCaller类型,此时FuncCaller类型实现了Invoker的Call()方法,赋值给invoker接口是成功的。
7、defer
a. 用途
在进行I/O操作时,如果遇到错误,需要提前返回,而返回之前应该关闭相应的资源,否则容易造成资源泄露等问题,这时就需要使用defer语句来解决这些问题。
在defer后指定的函数会在函数退出前调用。如果多次调用defer,那么defer采用后进先出的模式:
func main() {for i := 0; i < 5; i++ {defer fmt.Printf("%d\t", i) // 4 3 2 1 0}
}
defer的使用可以总结为以下几点:
(1)defer后面必须是函数或方法的调用,不能是语句,否则编译器会提示expression in defer mustbe function call
错误。
(2)defer函数的实参在注册时通过值复制传递。
(3)defer语句必须先注册后才能执行,如果defer位于return之后,则defer因为没有注册,不会执行。
(4)在主动调用os.Exit(int)退出进程时,defer将不再被执行。
(5)defer的好处是可以在一定程度上避免资源泄露,特别是在有很多return语句,有多个资源需要关闭的场景中,很容易漏掉资源的关闭操作。
(6)使用defer改写后,在打开资源无报错后直接调用defer关闭资源,养成这样的编程习惯后,就很难忘记资源的释放。
(7)defer语句的位置不当有可能导致panic,一般defer语句放在错误检查语句之后。
(8)defer也有明显的副作用:defer会推迟资源的释放,defer尽量不要放到循环语句中,而应将大函数内部的defer语句单独拆分成一个小函数。
(9)defer中最好不要对有名的返回值参数进行操作,否则也会出现错误。
b. 执行顺序
多个defer语句的执行顺序为“逆序”,defer、return、返回值三者的执行逻辑如下:
- defer最先执行一些收尾工作;
- 然后return执行,return负责将结果写入返回值中;
- 最后函数携带当前返回值退出。
也就是说,先被defer的语句最后被执行,最后被defer的语句,最先被执行
func main() {fmt.Println("defer begin...")//将defer 放入延迟调用栈defer fmt.Println(0)defer fmt.Println(1)//最后一个放入 , 位于栈顶, 最先调用defer fmt.Println(2)fmt.Println("defer end!!!")
}
- 代码的延迟顺序与最终的执行顺序是反向的。
- 延迟调用在defer所在的函数结束时进行,函数结束可以是正常返回时,也可以是发生宕机时。
8、闭包
闭包 = 函数 + 引用环境
闭包是由函数及其相关引用环境组合而成的实体,一般通过在匿名函数中引用外部函数的局部变量或包全局变量构成。
闭包对闭包外的环境引入是直接引用,编译器检测到闭包,会将闭包引用的外部变量分配到堆上。
如果函数返回的闭包引用了该函数的局部变量(参数或函数内部变量):
- 多次调用该函数,返回的多个闭包所引用的外部变量是多个副本,原因是每次调用函数都会为局部变量分配内存。
- 用一个闭包函数多次,如果该闭包修改了其引用的外部变量,则每一次调用该闭包对该外部变量都有影响,因为闭包函数共享外部引用。
闭包最初的目的是减少全局变量,在函数调用的过程中隐式地传递共享变量,有其有用的一面;但是这种隐秘的共享变量的方式带来的坏处是不够直接,不够清晰,除非是非常有价值的地方,否则一般不建议使用闭包。
对象是附有行为的数据,而闭包是附有数据的行为,类在定义时已经显式地集中定义了行为,但是闭包中的数据没有显式地集中声明的地方,这种数据和行为耦合的模型不是一种推荐的编程模型,闭包仅仅是锦上添花的东西,不是不可缺少的。
闭包对它作用域上部的变量可以进行修改,修改引用的变量会对变量进行实际修改:
func main() {// 准备一个字符串str := "Apple"// 创建一个匿名函数foo := func() {// 匿名函数中访问strstr = "Orange"}// 调用匿名函数foo()fmt.Println(str) // Orange
}
- 在匿名函数中并没有定义str,str的定义在匿名函数之前,此时,str就被引用到了匿名函数中,形成了闭包。
- 执行闭包,此时str发生修改,变为Orange。
注意:如果函数返回的闭包引用的是全局变量,则多次调用该函数返回的多个闭包引用的都是同一个全局变量。同理,调用一个闭包多次引用的也是同一个全局变量。此时如果闭包中修改了全局变量值的逻辑,则每次闭包调用都会影响全局变量的值。使用闭包是为了减少全局变量,所以,闭包引用全局变量不是好的编程方式。