cover

在编程中经常会调用相同或者类似的操作,这些相同或者类似的操作由同一段代码完成,函数的出现,可以避免重复编写这些代码。函数的作用就是把相对独立的某个功能抽象出来,使之成为一个独立的实体

例如,开发一个支持人与人之间进行对话的社交网站,对话这个功能比较复杂,可以将它封装为一个函数,每次调用该函数就可以发起对话;大型网站都有日志功能,对所有重要操作都会记录日志,而日志处理需要由多行Go文件操作相关代码组成,将这些代码组装为函数,则每次写日志时调用此函数即可。

Go语言函数支持的特性包括:

  • 参数数量不固定(可变参数)。
  • 匿名函数及其闭包。
  • 函数本身作为值传递。
  • 函数的延迟执行。
  • 把函数作为接口调用。

声明函数

函数的声明以关键字func为标识,具体格式如下:

func 函数名(参数列表) (返回参数列表){
    函数体
}
  • 函数名:函数名由字母、数字和下划线构成,但是函数名不能以数字开头;在同一个包内,函数名不可重复。 注意:可暂时简单地将一个包理解为一个文件夹。
  • 参数列表:参数列表中的每个参数都由参数名称和参数类型两部分组成,参数变量为函数的局部变量。如果函数的参数数量不固定,Go语言函数还支持可变参数。
  • 返回参数列表:返回参数列表中的每个参数由返回的参数名称和参数类型组成,也可简写为返回值类型列表。
  • 函数体:函数体指函数的主体代码逻辑,若函数有返回参数列表,则函数体中必须有return语句返回值列表。

使用标准格式定义一个名为add的函数,其功能是进行两个整型数字的加法,并返回结果。

func add(x int,y int) (sum int) {
	sum = x + y
	return sum
}

函数参数简写 在参数列表中,如果相邻的变量为同类型,则不必重复写出类型。

func add(x,y int) (sum int) {
	sum = x + y
	return sum
}

函数返回值简写 如果函数的返回值都是同一类型,在返回值列表中可将返回参数省略。

func returnValue() (int, int) {
	return 0,1
}

//可能会降低代码的可读性,无法区分每个返回值的实际意义。

带有变量名的函数返回值 使用带有变量名的返回值时,返回默认值为类型的默认值,函数结束处直接调用return即可。

func defaultValue() (a int,b string,c bool) {
	return
}

//调用后会返回:0、空字符串和false。

如果return后跟返回值列表也是允许的:

func defaultValue() (a int,b string,c bool) {
	return 1,"a",true
}

使用函数

定义函数后,可通过对函数的调用使用函数,函数体内的代码逻辑执行完毕后,程序将继续执行被调用函数后的代码。

package main

import "fmt"

func addSub(x,y int) (add int,sub int) {
	add = x + y
	sub = x - y
	return
}


func main() {
	i:=10
	q:=7
	add,sub := addSub(i,q)
	fmt.Println(i,"+",q,"=",add)
	fmt.Println(i,"-",q,"=",sub)
}

// 10 + 7 = 17
// 10 - 7 = 3

以上程序中,我们一般将传入函数的变量i和q称为实参,将函数中的x和y称为形参。变量i和q通过值传递的方式将值赋给形参x和y。 注意:

  • addSub函数中的形参x和y作用域仅限于函数体内。
  • main函数中定义的变量add和sub与addSub函数中定义的局部变量sum和sub完全无关,函数体内定义的变量作用域仅限于函数体内。
  • 若不想接收函数的某个返回值,可用匿名变量“_”,但是不能所有返回值都用匿名变量代替。

函数变量

函数也是一种类型,我们可以将其保存在变量中。 函数变量的声明格式如下:

var 变量名称 func()

函数变量的声明和初始化:

package main

import "fmt"

func addSub(x,y int) (add int,sub int) {
	add = x + y
	sub = x - y
	return
}


func main() {
	i:=10
	q:=7
	var f1 func(x int,y int) (add int,sub int)
	f1 = addSub
	add,sub := f1(i,q)
	fmt.Println(i,"+",q,"=",add)
	fmt.Println(i,"-",q,"=",sub)
}

// 10 + 7 = 17
// 10 - 7 = 3

函数变量f1声明后其值初始化为nil,在将addSub函数赋值给f1后,所有对f1的调用即为对addSub函数的调用。

函数变量也可用短变量格式进行声明和初始化:

package main

import "fmt"

func addSub(x,y int) (add int,sub int) {
	add = x + y
	sub = x - y
	return
}


func main() {
	i:=10
	q:=7
	f1 := addSub
	add,sub := f1(i,q)
	fmt.Println(i,"+",q,"=",add)
	fmt.Println(i,"-",q,"=",sub)
}

// 10 + 7 = 17
// 10 - 7 = 3

可变参数

Go语言支持可变参数的特性,即函数声明时可以没有固定数量的参数。 可变参数的函数格式如下:

func 函数名 (固定参数列表,v ...T ) (返回参数列表) {
    函数体
}
  • 可变参数一般放在函数参数列表的末尾,也可不存在固定参数列表。
  • “v ...T”代表的其实就是变量v为T类型的切片,v和T之间为三个“.”。

可变参数函数的使用

add函数只能对两个int类型的数进行加法计算,现在我们可以使用可变参数实现任意个int类型数的加法运算,最后返回加法结果。

package main

import "fmt"

func add(slice ... int) (int) {
	sum := 0
	for _,value:= range slice {
		sum = sum + value
	}
	return sum
}

func main() {
	fmt.Println("1+2+3...+9+10=",add(1,2,3,4,5,6,7,8,9,10))
}

// 1+2+3...+9+10= 55

使用可变参数的函数体中,常常会使用for循环来对切片中的项进行操作。

可变参数与内置函数

Go语言中许多内置函数的参数都用了可变参数,比如最常用的fmt包中的Println函数和Printf 函数。 fmt包中的Println函数源码如下:

func Println(a ...interface{}) (n int, err error) {
	return Fprintln(os.Stdout, a...)
}

Printf函数源码如下,第一个参数指定了需要打印的格式:

func Printf(format string, a ...interface{}) (n int, err error) {
	return Fprintf(os.Stdout, format, a...)
}

可变参数的传递

可变参数本质上是一个切片,如果要在多个函数中传递可变参数,可在传递时添加“...”。

package main

import "fmt"

func addall(slice ... int) (int) {
	sum := 0
	for _,value:= range slice {
		sum = sum + value
	}
	return sum
}

func add(num ... int)(int){
	return addall(num...)
}

func main() {
	fmt.Println("1+2+3...+9+10=",add(1,2,3,4,5,6,7,8,9,10))
}

// 1+2+3...+9+10= 55

如果想传递可变参数本身,可将addall函数的可变参数改为切片:

package main

import "fmt"

func addall(slice []int) (int) {
	sum := 0
	for _,value:= range slice {
		sum = sum + value
	}
	return sum
}

func add(num ... int)(int){
	return addall(num)
}

func main() {
	fmt.Println("1+2+3...+9+10=",add(1,2,3,4,5,6,7,8,9,10))
}

// 1+2+3...+9+10= 55

匿名函数和闭包

匿名函数即在需要函数时定义函数,匿名函数能以变量方式传递,它常常被用于实现闭包。 匿名函数的格式如下:

func (参数列表) (返回参数列表) {
    函数体
}

匿名函数的调用有两种方式:

  • 定义并同时调用匿名函数。
  • 将匿名函数赋值给变量。

定义并同时调用匿名函数

可以在匿名函数后添加“()”直接传入实参:

package main

import "fmt"

func main() {
	func (data string) {
		fmt.Println("Hello "+data)
	}("World!")
}

// Hello World!

将匿名函数赋值给变量

将匿名函数赋值给一个变量,之后再进行调用:

package main

import "fmt"

func main() {
	f1 := func(data string){
		fmt.Println("Hello "+data)
	}
	f1("World!")
}

// Hello World!

闭包

闭包就是包含了自由变量的匿名函数,其中的自由变量即使已经脱离了原有的自由变量环境也不会被删除,在闭包的作用域内可继续使用这个自由变量,同一个匿名函数和不同的引用环境组成了不同的闭包。 闭包就如同有“记忆力”一般,可对作用域内的变量的引用进行修改。

闭包的“记忆力”

# 例1
package main

import "fmt"

func main() {
	num:=1
	fmt.Printf("%p\n",&num)
	func() {
		num++
		fmt.Println(num)
		fmt.Printf("%p\n",&num)
	}()
	func() {
		num++
		fmt.Println(num)
		fmt.Printf("%p\n",&num)
	}()
}

//0xc00000a0b0
//2
//0xc00000a0b0
//3
//0xc00000a0b0

匿名函数由于在函数体内部引用了外部的自由变量num而形成了闭包。闭包每次对num变量的加1操作都是对变量num引用的修改。

# 例2
package main

import "fmt"

func addOne(i int) func() int {
	return func() int {
		i++
		return i
	}
}

func main() {
	a1 := addOne(0)
	fmt.Println(a1())  //0+1=1
	fmt.Println(a1())  //1+1=2
	a2 := addOne(10)
	fmt.Println(a2())
	fmt.Print("a1闭包的地址为:")
	fmt.Printf("%p\n",&a1)
	fmt.Print("a2闭包的地址为:")
	fmt.Printf("%p\n",&a2)
}

//1
//2
//11
//a1闭包的地址为:0xc000006028
//a2闭包的地址为:0xc000006038

addOne函数返回了一个闭包函数,通过定义a1和a2变量,创建了两个闭包的实例(引用环境不同导致)。

每次调用闭包实例,i的值都会在原有的基础上加1。从打印的结果可以看到,两个闭包实例的地址完全不同,两个闭包的调用结果互不影响。

延迟执行语句

Go语言中存在一种延迟执行的语句,由defer关键字标识,格式如下:

defer 任意语句

defer后的语句不会被马上执行,在defer所属的函数即将返回时,函数体中的所有defer语句将会按出现的顺序被逆序执行,即函数体中的最后一个defer语句最先被执行。

package main

import "fmt"

func main(){
	fmt.Println("start now")
	defer fmt.Println("这是第一句defer语句")
	defer fmt.Println("这是第二句defer语句")
	defer fmt.Println("这是第三句defer语句")
	fmt.Println("end")
}

//start now
//end
//这是第三句defer语句
//这是第二句defer语句
//这是第一句defer语句

由于defer语句是在当前函数即将返回时被调用,所以defer常常被用来释放资源。

package main

import (
	"fmt"
	"net"
)

func tcpSend() {
	conn,err := net.Dial("tcp","127.0.0.1:80")
	if err == nil {
		defer conn.Close()
		fmt.Println("remote address:",conn.RemoteAddr())
	}
	fmt.Println("error:", err)
}

func main() {
	tcpSend()
}


//remote address: 127.0.0.1:80
//error: <nil>

由于127.0.0.1对应的远端IP的80端口处于开启状态,在本机与该地址建立连接后和函数退出前,需要对打开的文件描述符conn进行关闭。

小结

  • Go语言中,函数的声明以关键字func为标识。
  • Go语言支持可变参数的特性,即函数声明时可以没有固定数量的参数。
  • 匿名函数即在需要函数时定义函数,匿名函数能以变量方式传递,它常常被用于实现闭包。
  • 闭包就是包含了自由变量的匿名函数,其中的自由变量即使已经脱离了原有的自由变量环境也不会被删除,在闭包的作用域内可继续使用这个自由变量。
  • defer语句是在当前函数即将返回时被调用,所以defer常常被用来释放资源。

函数参数传递的本质(知识拓展)

两个基本概念: 值传递引用传递

  • 值传递:将变量的一个副本传递给函数,函数中不管如何操作该变量副本,都不会改变原变量的值。
  • 引用传递:将变量的内存地址传递给函数,函数中操作变量时会找到保存在该地址的变量,对其进行操作,会改变原变量的值。

Go语言函数传入参数时使用的始终是值传递,对于值传递,Go语言主要分为以下两种情况:

  1. 对于int、string和bool等值类型变量,传递的是原变量的副本,对副本的操作不会影响原变量。
  2. 对于指针、切片、map和channel(通道)引用类型变量,传递的是原变量指针的一份副本,该副本指向了原变量地址,因此对该副本的操作会影响原变量,从而达到了其他编程语言中类似于引用传递的效果。
Golang-函数
转载前请阅读本站 版权协议,文章著作权归 饼铛 所有,转载请注明出处。

目录