目录
前言
在编程中经常会调用相同或者类似的操作,这些相同或者类似的操作由同一段代码完成,函 数的出现,可以避免重复编写这些代码。函数的作用就是把相对独立的某个功能抽象出来,使之 成为一个独立的实体。
例如,开发一个支持人与人之间进行对话的社交网站,对话这个功能比较复杂,可以将它封装为一个函数,每次调用该函数就可以发起对话;大型网站都有日志功能,对所有重要操作都会记录日志,而日志处理需要由多行Go文件操作相关代码组成,将这些代码组装为函数,则每次写日志时调用此函数即可
1. 基本函数定义
Go语言中,函数的声明以关键字func为标识,具体格式如下
func 函数名(参数列表) (返回参数列表){
函数体
}
- 使用
func
关键字定义函数。 - 函数名:函数名由字母、数字和下划线构成,但是函数名不能以数字开头;在同一个包内,函数名不可重复。(可简单地先将一个包理解为一个文件夹)
- 参数列表中指定参数名称和类型。
- 返回参数列表:返回类型中的每个参数由返回的(参数名称,可选)和参数类型组成,也可称,返回类型
- 函数体:函数体指函数的主体代码逻辑,若函数有返回参数列表,则函数体中必须有return 语句返回值列表。
// 1. 基本函数定义
func add(x int, y int) (sum int) {
//sum := x + y //重复定义
sum = x + y
return sum
}
在参数列表中,如果相邻的变量为同类型,则不必重复写出类型。
多返回值:Go 函数可以返回多个值,这在错误处理中特别有用。
// 2. 多返回值
func divAndRemainder(x, y int) (int, int) {
return x / y, x % y
}
命名返回值:可以在函数签名中命名返回值。允许使用"裸"return 语句,会自动返回命名的返回值。
// 3. 命名返回值
func rectangleProps(length, width float64) (area, perimeter float64) {
area = length * width
perimeter = 2 * (length + width)
return // 裸返回
}
2. 使用函数
在定义函数后,可通过对函数的调用使用函数,函数体内的代码逻辑执行完毕后,程序将继 续执行被调用函数后的代码。
我们以add函数为例,函数的实参是1,2,函数的形参是x,y。add函数的形参x和y作用域仅限于函数体内。函数体内定义的变量作用域仅限于函数体内。
若不想接收函数的某个返回值,可用匿名变量“_”,但是不能所有返回值都用匿名变量代替
package main
import "fmt"
// 1. 基本函数定义
func add(x int, y int) (sum int) {
//sum := x + y //重复定义
sum = x + y
return sum
}
// 2. 多返回值
func divAndRemainder(x, y int) (int, int) {
return x / y, x % y
}
// 3. 命名返回值
func rectangleProps(length, width float64) (area, perimeter float64) {
area = length * width
perimeter = 2 * (length + width)
return // 裸返回
}
func main() {
// 基本函数调用
fmt.Println("1 + 2 =", add(1, 2))
// 多返回值
quotient, remainder := divAndRemainder(10, 3)
fmt.Printf("10 / 3 = %d remainder %d\n", quotient, remainder)
// 命名返回值
area, perimeter := rectangleProps(5, 3)
fmt.Printf("Rectangle area: %.2f, perimeter: %.2f\n", area, perimeter)
}
//result
1 + 2 = 3
10 / 3 = 3 remainder 1
Rectangle area: 15.00, perimeter: 16.00
3. 函数变量
Go语言中,函数也是一种类型,我们可以将其保存在变量中。
函数声明格式:var 变量名称 func()
var add func(x int, y int) (sum int)
package main
import "fmt"
func addSub(x int, y int) (sum int, sub int) {
sum = x + y
sub = x - y
return sum, sub
}
func main() {
a := 1
b := 2
var add func(x int, y int) (sum int, sub int)
add = addSub
sum, sub := add(a, b)
fmt.Println(a, "+", b, "=", sum)
fmt.Println(a, "-", b, "=", sub)
}
//result
1 + 2 = 3
1 - 2 = -1
函数变量也可用短变量格式进行声明和初始化:
package main
import "fmt"
func addSub(x int, y int) (sum int, sub int) {
sum = x + y
sub = x - y
return sum, sub
}
func main() {
a := 1
b := 2
//var add func(x int, y int) (sum int, sub int)
add := addSub
sum, sub := add(a, b)
fmt.Println(a, "+", b, "=", sum)
fmt.Println(a, "-", b, "=", sub)
}
//result
1 + 2 = 3
1 - 2 = -1
4. 可变参数
可变参数函数:使用 ...
语法允许函数接受任意数量的参数。
func 函数名 (固定参数列表,arg ...int ) (返回参数列表) {
函数体
}
注意:固定参数列表可省,
arg ...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+...+9+10=", add(1, 2, 3, 4, 5, 6, 7, 8, 9, 10))
}
//result
1+2+...+9+10= 55
可变参数与内置函数
Go语言中许多内置函数的参数都用了可变参数,比如最常用的fmt包中的Println函数和Printf 函数。 Println函数所有的参数都为可变参数:
// Println formats using the default formats for its operands and writes to standard output.
// Spaces are always added between operands and a newline is appended.
// It returns the number of bytes written and any write error encountered.
func Println(a ...any) (n int, err error) {
return Fprintln(os.Stdout, a...)
}
Printf函数源码如下,第一个参数指定了需要打印的格式:
// Printf formats according to a format specifier and writes to standard output.
// It returns the number of bytes written and any write error encountered.
func Printf(format string, a ...any) (n int, err error) {
return Fprintf(os.Stdout, format, a...)
}
可变参数的传递
可变参数本质上是一个切片,如果要在多个函数中传递可变参数,可在传递时添加“...”。
package main
import "fmt"
func addall(slice ...int) {
sum := 0
for _, value := range slice {
sum = sum + value
}
fmt.Println(sum)
}
func add(num ...int) {
addall(num...)
}
func main() {
add(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
}
//result
55
5. 匿名函数和闭包
匿名函数: 匿名函数是没有名字的函数。它们可以在声明的同时被调用,也可以赋值给变量,或者作为参数传递给其他函数。
匿名函数即在需要函数时定义函数,匿名函数能以变量方式传递,它常常被用于实现闭包。 首先,我们来了解什么是匿名函数以及它的调用方式。
func (参数列表) (返回参数列表) {
函数体
}
1. 定义并同时调用匿名函数,可以在匿名函数后添加“()”直接传入实参:
package main
import "fmt"
func main() {
func(data string) {
fmt.Println("Hello " + data)
}("world!")
}
//result
Hello world!
2. 将匿名函数赋值给变量,之后再进行调用:
package main
import "fmt"
func main() {
f := func(data string) {
fmt.Println("Hello, " + data)
}
f("World!")
}
//result
Hello, World!
闭包: 闭包是一个函数值,它引用了其外部作用域中的变量。闭包可以访问并操作其外部函数中的变量,即使外部函数已经返回。
闭包就如同有“记忆力”一般,可对作用域内的变量的引用进行修改。
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)
}()
}
//result
0xc00000a0b8
2
0xc00000a0b8
3
0xc00000a0b8
以上程序中的匿名函数由于在函数体内部引用了外部的自由变量num而形成了闭包。闭包每次 对num变量的加1操作都是对变量num引用的修改。
package main
import "fmt"
func AddOne(i int) func() int {
return func() int {
i++
return i
}
}
func main() {
a1 := AddOne(0) //这创建了一个闭包 a1,其中 i 初始化为 0。
fmt.Println(a1()) //调用闭包 a1,它将 i 从 0 增加到 1 并打印 1。
fmt.Println(a1()) //再次调用闭包 a1,它将 i 从 1 增加到 2 并打印 2。
a2 := AddOne(10) //创建另一个闭包 a2,其中 i 初始化为 10。
fmt.Println(a2()) //调用闭包 a2,它将 i 从 10 增加到 11 并打印 11。
fmt.Print("a1闭包的地址为:")
fmt.Printf("%p\n", &a1)
fmt.Print("a2闭包的地址为:")
fmt.Printf("%p\n", &a2)
}
//result
1
2
11
a1闭包的地址为:0xc00006c028
a2闭包的地址为:0xc00006c038
addOne函数返回了一个闭包函数,通过定义a1和a2变量,创建了两个闭包的实例(引用环境 不同导致)。
每次调用闭包实例,i的值都会在原有的基础上加1。从打印的结果可以看到,两个闭包实例的 地址完全不同,两个闭包的调用结果互不影响
6. 延迟执行语句
Go语言中存在一种延迟执行的语句,由defer关键字标识,使用 defer
关键字可以延迟函数的执行直到周围的函数返回。格式如下:
defer 任意语句
多个 defer 语句按照 LIFO(后进先出)顺序执行。defer后的语句不会被马上执行,在defer所属的函数即将返回时,函数体中的所有defer语句将 会按出现的顺序被逆序执行,即函数体中的最后一个defer语句最先被执行
package main
import "fmt"
func main() {
fmt.Println("start")
defer fmt.Println("这是第一句defer语句")
defer fmt.Println("这是第二句defer语句")
defer fmt.Println("这是第三句defer语句")
fmt.Println("end")
}
//result
start
end
这是第三句defer语句
这是第二句defer语句
这是第一句defer语句
由于defer语句是在当前函数即将返回时被调用,所以defer常常被用来释放资源。
package main
import (
"fmt"
"net"
)
// tcpSend establishes a TCP connection to www.baidu.com:80 and prints the remote address.
func tcpSend() {
conn, err := net.Dial("tcp", "www.baidu.com:80")
if err == nil {
defer conn.Close()
fmt.Println("remote address:", conn.RemoteAddr())
}
fmt.Println("error:", err)
}
// main function calls tcpSend to initiate the TCP connection.
func main() {
tcpSend()
}
//result
remote address: 44.0.2.250:80
error: <nil>
7. 函数参数传递的本质
在讲述参数传递前,我们首先要了解两个基本概念:值传递和引用传递。
- 值传递的本质:Go 中所有的参数传递都是通过值传递。这意味着函数接收的是参数的副本,而不是原始值的引用。
- 引用传递:将变量的内存地址传递给函数,函数中操作变量时会找到保存在该地址的变量, 对其进行操作,会改变原变量的值。
不同类型的行为:
- 基本类型(如 int, float, string):传递的是值的完整副本。修改不会影响原始值。
- 复合类型(如 slice, map, channel):传递的是包含指针的结构体的副本,该副本指向了原变量地址,因此对该副本的操作会影响原变量,从而达到了其他编程语言中类似于引用传递的效果。在Go中,切片、映射、通道、指针和接口默认使用这种效果。
package main
import "fmt"
func PassByValue(numPara int) { //值传递函数
fmt.Printf("PassByValue函数中变量numPara地址为: %p\n", &numPara)
numPara = 100
}
func PassByReference(numPara *int) { //引用传递函数
fmt.Printf("PassByReference函数中指针变量numPara地址为: %p\n", &numPara)
fmt.Printf("PassByReference函数中指针变量numPara指向的地址为: %p\n", numPara)
*numPara = 100
}
func main() {
num := 1
fmt.Printf("main函数中变量num地址为: %p\n", &num)
PassByValue(num)
fmt.Printf("num变量值为: %d\n", num)
PassByReference(&num)
fmt.Printf("num变量值为: %d\n", num)
}
//result
main函数中变量num地址为: 0xc00000a0b8
passByValue函数中变量numPara地址为: 0xc00000a0e0
num变量值为: 1
passByReference函数中指针变量numPara地址为: 0xc00006c030
passByReference函数中指针变量numPara指向的地址为: 0xc00000a0b8
num变量值为: 100
- 值传递 (PassByValue函数):
- 当把
num
传递给passByValue
函数时,会创建一个num
的副本。 - 函数内部对
numPara
的修改不会影响main()
中的原始num
变量。 - 函数调用后,
main()
中的num
值保持不变(仍为1)。
- 当把
- 引用传递 (PassByReference函数):
- 当把
&num
传递给passByReference
函数时,传递的是num
的内存地址。 - 函数内部通过指针
*numPara
可以直接访问和修改原始的num
变量。 - 函数调用后,
main()
中的num
值会被改变(变为100)。
- 当把
来一个更全面的比较
package main
import "fmt"
type Person struct {
Name string
Age int
}
func main() {
// 基本类型
x := 5
modifyInt(x)
fmt.Println("After modifyInt:", x) // 输出: 5
// 切片
slice := []int{1, 2, 3}
modifySlice(slice)
fmt.Println("After modifySlice:", slice) // 输出: [100 2 3]
// 映射
m := map[string]int{"a": 1, "b": 2}
modifyMap(m)
fmt.Println("After modifyMap:", m) // 输出: map[a:1 b:2 c:3]
// 结构体
p := Person{"Alice", 30}
modifyPerson(p)
fmt.Println("After modifyPerson:", p) // 输出: {Alice 30}
// 指针
modifyPersonPtr(&p)
fmt.Println("After modifyPersonPtr:", p) // 输出: {Bob 31}
}
func modifyInt(n int) {
n = 10
}
func modifySlice(s []int) {
s[0] = 100
}
func modifyMap(m map[string]int) {
m["c"] = 3
}
func modifyPerson(p Person) {
p.Name = "Charlie"
p.Age = 35
}
func modifyPersonPtr(p *Person) {
p.Name = "Bob"
p.Age = 31
}
8. 内置函数
append -- 用来追加元素到数组、slice中,返回修改后的数组、slice
close -- 主要用来关闭channel
delete -- 从map中删除key对应的value
panic -- 停止常规的goroutine (panic和recover:用来做错误处理)
recover -- 允许程序定义goroutine的panic动作
imag -- 返回complex的实部 (complex、real imag:用于创建和操作复数)
real -- 返回complex的虚部
make -- 用来分配内存,返回Type本身(只能应用于slice, map, channel)
new -- 用来分配内存,主要用来分配值类型,比如int、struct。返回指向Type的指针
cap -- capacity是容量的意思,用于返回某个类型的最大容量(只能用于切片和 map)
copy -- 用于复制和连接slice,返回复制的数目
len -- 来求长度,比如string、array、slice、map、channel ,返回长度
print、println -- 底层打印函数,在部署环境中建议使用 fmt 包
package main
import "fmt"
func main() {
c1 := complex(1, 2)
fmt.Println(c1, "实部为:", real(c1))
fmt.Println(c1, "虚部为:", imag(c1))
}
//result
(1+2i) 实部为: 1
(1+2i) 虚部为: 2