文章目录
- [1. 函数基础](#1. 函数基础)
-
- [1.1 函数基本语法](#1.1 函数基本语法)
- [1.2 函数参数和返回值](#1.2 函数参数和返回值)
-
- [1.2.1 无参无返回值](#1.2.1 无参无返回值)
- [1.2.2 有参数无返回值](#1.2.2 有参数无返回值)
- [1.2.3 无参数有返回值](#1.2.3 无参数有返回值)
- [1.2.4 有参有返回值(一个)](#1.2.4 有参有返回值(一个))
- [1.2.5 有参有返回值(多个)](#1.2.5 有参有返回值(多个))
- [1.2.6 返回值命名](#1.2.6 返回值命名)
- [1.3 值传递](#1.3 值传递)
- [1.4 引用传递](#1.4 引用传递)
- [1.5 参数简写](#1.5 参数简写)
- [2. 函数进阶用法](#2. 函数进阶用法)
-
- [2.1 可变参数](#2.1 可变参数)
- [2.2 函数作为类型](#2.2 函数作为类型)
- [2.3 匿名函数](#2.3 匿名函数)
- [2.4 ⭐ 闭包](#2.4 ⭐ 闭包)
-
- [2.4.1 基本理解](#2.4.1 基本理解)
- [2.4.2 函数选项模式](#2.4.2 函数选项模式)
- [2.4.3 ⚠️ 闭包陷阱 (1) - 循环变量捕获](#2.4.3 ⚠️ 闭包陷阱 (1) - 循环变量捕获)
- [2.4.4 ⚠️ 闭包陷阱 (2) - 内存泄漏问题](#2.4.4 ⚠️ 闭包陷阱 (2) - 内存泄漏问题)
- [2.4.5 🚀 综合示例](#2.4.5 🚀 综合示例)
- [3. 小结](#3. 小结)
本系列文章:
- Java 转 go 学习 - 项目管理
- Java 转 go 学习 - 基本语法
- Java 转 go 学习 - 类型转换
- Java 转 go 学习 - 流程控制结构
- Java 转 go 学习 - 数组和切片
- Java 转 go 学习 - map
上一篇文章我们学习了 go 中的 map,这篇文章就来看下 go 中的函数,go 语言的函数语法和 java 还是有很大区别的。
1. 函数基础
1.1 函数基本语法
首先来看下 go 中函数的基本语法。
go
func 函数名(参数名 参数类型) 返回值类型 {
// 函数体
return 返回值
}
其中参数和返回值都可以返回多个,比如多个返回值的格式如下,这一点是和 Java 不一样的,在 Java 中如果需要返回多个值,得把这些值封装到一个对象里面。
go
func 函数名(参数名 参数类型) 返回值类型1, 返回值类型2, 返回值类型3 {
// 函数体
return 返回值1, 返回值2, 返回值3
}
下面是一个简单的函数例子。
go
// 定义一个函数
func sayHello(name string) string {
return "Hello, " + name
}
func test1() {
// 调用函数
result := sayHello("张三")
fmt.Println(result) // Hello, 张三
}
1.2 函数参数和返回值
通过上面的基本语法可以得知 函数的参数数量和返回值可以是没有、一个或者多个,下面就分别来看下这些情况。
- 📝 无参数无返回值
- 📝 有参数无返回值
- 📝 无参数有返回值
- 📝 有参数有返回值
- 📝 多个返回值(Go 特色)
- 📝 命名返回值
1.2.1 无参无返回值
go
func test1() {
fmt.Println("Hello!")
}
// 调用
test1()
1.2.2 有参数无返回值
go
func test2(name string, age int) {
fmt.Printf("姓名: %s, 年龄: %d\n", name, age)
}
func main() {
test2("李四", 20) // 姓名: 李四, 年龄: 20
}
1.2.3 无参数有返回值
go
func test3() string {
return "no parameters"
}
func main() {
fmt.Println(test3()) // no parameters
}
1.2.4 有参有返回值(一个)
go
func test4(a int, b int) int {
return a + b
}
func main() {
fmt.Println(test4(1, 1)) // 2
}
1.2.5 有参有返回值(多个)
这个是 go 语言的特色,可以返回多个结果,一般来说结合 err 使用,外层调用通常会先判断 err != nil,满足这种情况就说明函数异常,外层调用做异常处理。
go
func test5(a, b int) (int, error) {
if b == 0 {
return 0, fmt.Errorf("除数不能为0")
}
return a / b, nil
}
返回值用 , 分割,外层调用的时候如果想要忽略某个返回值可以用 _,下面直接看例子:
go
func test5(a, b int) (int, error) {
if b == 0 {
return 0, fmt.Errorf("除数不能为0")
}
return a / b, nil
}
func main() {
// 调用
result, err := test5(10, 2)
if err != nil {
fmt.Println("error:", err)
} else {
fmt.Println("result:", result)
// result: 5
}
// 如果想忽略某个返回值,用 _
result2, _ := test5(10, 2) // 忽略错误
_, err2 := test5(10, 0) // 忽略结果
fmt.Println(result2)
fmt.Println(err2)
}
1.2.6 返回值命名
这里的意思是给返回值起名,然后在函数体里面直接使用,return 就可以不带参数。
go
func test6() (name string, age int) {
name = "张三"
age = 25
return // 自动返回 name 和 age, 不需要指定
}
func main() {
name, age := test6()
fmt.Println(name, age) // 张三 25
}
也可以结合 defer 使用,defer 就类似 Java 的 finally。
go
func test7() (result int) {
defer func() {
result = result + 1 // defer 可以修改命名返回值
}()
return 5 // 实际返回 6
}
⚠️ 注意: 一般来说这种方式不怎么用,主要是过度使用代码可读性会变得很低。
1.3 值传递
一般来说基本类型和结构体都是值拷贝,前面我们也说了值传递意思就是将值拷贝一份传进去,也就是说 在函数里面修改参数对原来的参数没有影响。
go
type Student struct {
Name string
Age int
}
func test8(s Student) {
s.Name = "修改后的名字"
s.Age = 99
}
func main() {
stu := Student{Name: "张三", Age: 18}
test8(stu)
fmt.Println(stu) // {张三 18} 不改变原值
}
如果要对值类型去修改,可以通过 & 将指针传进去,实际上将指针传进去也是值传递,只不过这里的值是将指针地址拷贝一份传进去,函数内的指针指向的地址的值不会被复制。
go
func test8(s *Student) {
fmt.Printf("%p\n", s)
// 0xc00009a090
s.Name = "修改后的名字"
s.Age = 99
}
func main() {
stu := Student{Name: "张三", Age: 18}
test8(&stu)
fmt.Printf("%p\n", &stu)
// 0xc00009a090
fmt.Println(stu) // {修改后的名字 99} 修改原值
}
可以看到上面的输出方法内的结构体和传入的结构体地址是一样的。
1.4 引用传递
指针、切片、map、channel 这些类型传递的是引用,函数内修改会影响原值,下面直接看下示例。
go
// 指针传递
func modifyPtr(s *Student) {
fmt.Printf("%p\n", s) // 0xc0000080a8
// 0xc0000080a8
s.Name = "修改后的名字"
s.Age = 99
}
// 切片传递(切片本身是引用类型)
func modifySlice(nums []int) {
fmt.Printf("%p\n", nums) // 0xc0000241e0
nums[0] = 100
}
// map 传递(map 是引用类型)
func modifyMap(m map[string]int) {
fmt.Printf("%p\n", m) // 0xc00001c0d8
m["age"] = 99
}
func test9() {
stu := Student{Name: "张三", Age: 18}
modifyPtr(&stu)
fmt.Printf("%p\n", &stu) // 0xc0000080a8
fmt.Println(stu) // {修改后的名字 99}
person := map[string]int{"age": 18}
fmt.Printf("%p\n", person) // 0xc0000241e0
modifyMap(person)
fmt.Println(person) // map[age:99]
arr := []int{1, 2, 3}
fmt.Printf("%p\n", arr) // 0xc00001c0d8
modifySlice(arr)
fmt.Println(arr) // [100 2 3] 原切片被修改!
}
可以看到对于引用类型,person 和 stu 存储的就是变量的地址,参数和上层传参的地址是一样的,所以在函数内修改可以影响到函数外的原始参数。
不过要注意:指针类型也是一个变量类型,有自己的地址和值,只是指针比较特殊,指针的值指向一个变量的地址,通过指针可以访问变量的地址,然后对变量进行修改,因此按引用传递也是按值传递的一种,只不过按引用传递是指将地址复制一遍传进去。
1.5 参数简写
对于同类型的参数是可以合并的,比如我们在参数传两个 int,那么可以简写传参。
go
// 写法1
func add(a int, b int) int {
return a + b
}
// 写法2, 简写 int
func add(a, b int) int {
return a + b
}
同样的,我们也可以在简写的参数后面加上其他类型的参数。
go
// 写法3, 简写 int + 其他类型的
func add3(a, b int, add bool) int {
if add {
return a + b
} else {
return a - b
}
}
func main() {
fmt.Println(add3(1, 2, true)) // 3
fmt.Println(add3(1, 2, false)) // -1
}
对于返回值也可以简写,下面还是来看个简单的例子。
go
func add3(a, b int, add bool) (result, isAdd int) {
if add {
result = a + b
return result, 1
} else {
result = a - b
return result, 2
}
}
2. 函数进阶用法
2.1 可变参数
和 Java 一样,Go 函数同样可以接收不定数量的参数,也就是可变参数,可以用 ... 来表示,Go 的 Println 穿的就是可变参数,下面是基础语法。
go
func 函数名(参数名 ...类型) 返回值类型 {
// 函数体
}
来看一个例子,函数接收不定数量的 int 来求和。
go
// 接收任意数量的 int 参数求和·1
func sum(nums ...int) int {
total := 0
for _, num := range nums {
total += num
}
return total
}
func test10() {
fmt.Println(sum()) // 0
fmt.Println(sum(1)) // 1
fmt.Println(sum(1, 2, 3)) // 6
fmt.Println(sum(1, 2, 3, 4, 5)) // 15
}
⚠️ 要注意可变参数只能作为函数的最后一个参数
如果已经存在切片,想要把切片传给可变参函数,需要用 ... 展开,不能直接把切片传进去
go
// 接收任意数量的 int 参数
func sum(nums ...int) int {
total := 0
for _, num := range nums {
total += num
}
return total
}
func test11() {
nums := []int{1, 2, 3, 4, 5}
fmt.Println(sum(nums...))
// result := sum(nums) // 编译错误, Cannot use 'nums' (type []int) as the type int
}
2.2 函数作为类型
在 Go 中函数也是一种类型,既然作为一种类型,就可以用于传参,和 Java 不一样,Java 是不能直接将函数作为参数传递的,需要用到函数式编程,下面来看下例子,我们定义一个函数,然后初始化。
go
// 定义一个函数类型
type MathFunc func(int, int, string) (int, error)
// 初始化函数
var f MathFunc = func(x int, y int, operate string) (int, error) {
if operate == "+" {
return x + y, nil
}
if operate == "-" {
return x - y, nil
}
if operate == "*" {
return x * y, nil
}
if operate == "/" {
if y == 0 {
return 0, fmt.Errorf("y = 0")
}
}
return 0, fmt.Errorf("unknown operate")
}
func main() {
res1, err := f(1, 2, "+")
fmt.Println(res1, err) // 3 <nil>
res2, err := f(1, 0, "/")
fmt.Println(res2, err) // 0 y = 0
}
然后再来看下函数作为参数来传递,这种可以用作回调函数。
go
// 定义一个函数类型
type Operation func(int, int) int
// 接收函数作为参数
func calculate(a, b int, op Operation) int {
return op(a, b)
}
func add(x, y int) int {
return x + y
}
func subtract(x, y int) int {
return x - y
}
func multiply(x, y int) int {
return x * y
}
上面的代码中,我们定义了一个函数类型 Operation,这个函数类型接收两个 int 的参数,返回结果是 int,然后定义了 calculate 函数,这个函数需要将 Operation 作为参数传进去,然后在 calculate 内部去调用 Operation。
下面的 add、subtract、multiply 类型和 Operation 一样,所以我们可以将这三个函数传进去,如果不想用这三个命名函数,同样的 Go 和 Java 也支持匿名函数的写法,来看下调用例子。
go
func test12() {
fmt.Println(calculate(10, 5, add)) // 15
fmt.Println(calculate(10, 5, subtract)) // 5
fmt.Println(calculate(10, 5, multiply)) // 50
// 也可以传匿名函数, 就是 Java 中 lambda 的写法
result := calculate(10, 5, func(x int, y int) int {
return x / y
})
fmt.Println(result) // 2
}
最后我们来看下函数作为返回值,下面直接看例子把,我们定义一个 getOperation 函数,然后根据传参 op 来决定返回什么函数,比如 op == +,我们就返回一个加法函数,这个函数接收两个 int 类型的参数,返回结果也是 int 类型。
go
// 根据操作符返回不同的函数
func getOperation(op string) func(int, int) int {
switch op {
case "+":
return func(a, b int) int { return a + b }
case "-":
return func(a, b int) int { return a - b }
case "*":
return func(a, b int) int { return a * b }
case "/":
return func(a, b int) int { return a / b }
default:
return func(a, b int) int { return 0 }
}
}
func test13() {
fmt.Println(getOperation("+")(10, 5)) // 15
fmt.Println(getOperation("-")(10, 5)) // 5
fmt.Println(getOperation("*")(10, 5)) // 20
fmt.Println(getOperation("/")(10, 5)) // 2
fmt.Println(getOperation("?")(10, 5)) // 0
}
2.3 匿名函数
匿名函数在上面已经出现果了,也就是没有名字的函数,适合用于 一次性使用 的场景,基本语法如下:
go
func(参数列表) 返回值类型 {
// 函数体
}
下面来看下例子,我们声明完匿名函数之后跟上 () 可以直接调用。
go
func test14() {
func(name string) {
fmt.Println(name)
}("匿名函数立刻调用")
// 匿名函数立刻调用
}
也可以将匿名函数赋值之后再调用。
go
func test15() {
// 赋值给变量后调用
f := func(x, y int) int {
return x + y
}
fmt.Println(f(3, 5)) // 8
}
同样的函数既然作为变量,也可以存到数据结构中,比如下面把函数存到 map 中去调用。
go
func test16() {
operations := map[string]func(int, int) int{
"add": func(a, b int) int { return a + b },
"sub": func(a, b int) int { return a - b },
"mul": func(a, b int) int { return a * b },
}
fmt.Println(operations["add"](1, 2)) // 3
fmt.Println(operations["sub"](1, 2)) // -1
}
2.4 ⭐ 闭包
2.4.1 基本理解
闭包 = 函数 + 引用到的外部变量 ,通俗理解就是一个函数将所在的环境中的变量(外部函数的变量 )一起打包带走,也就是说就算外部函数指向完了,内部函数一样能访问这些变量,因为变量没有被销毁。正式一点的说法就是:一个持有外层函数作用域中的变量(自由变量)的匿名函数,在外层函数返回之后依然可以使用这些不会被垃圾回收的变量。
下面先来看一个最经典的用法,生成一个累加器。
go
// 生成一个累加器
func counter() func() int {
// 自由变量
count := 0
// 返回一个匿名函数, 引用自由变量
return func() int {
// 修改 count
count++
return count
}
}
可以看到这个累加器引用了自由变量 count,下面是核心逻辑:
- 调用 counter() 时,会创建 count 变量(初始值 0),然后返回一个 匿名函数。
- counter() 执行完毕后,count 变量不会被销毁(因为闭包还在引用)。
- 每次调用 c1(),都会修改这个 count 变量。
- c1 和 c2 是两个独立的闭包,各自持有自己的 count 变量。
既然闭包能修改自由变量,就意味着闭包对自由变量的操作是 引用而不是拷贝,下面再来看一个例子,闭包修改结构体的。
go
type StudentInfo struct {
Name string
Age int
}
func modifyStudent(stu StudentInfo) func(int) {
// 自由变量:stu是外层函数的参数
return func(age int) {
stu.Age = age
fmt.Println("闭包内的stu: ", stu)
}
}
func test2_2() {
stu := StudentInfo{Name: "张三", Age: 18}
// 生成闭包
closure := modifyStudent(stu)
// 在闭包中修改变量
closure(99) // 闭包内的stu: {张三 99}
closure(88) // 闭包内的stu: {张三 88}
}
不过要注意因为调用 modifyStudent 没有传递指针,所以闭包修改的结构体和外层初始化的 stu 没有关系,stu 不会被修改,如果想要在闭包中同步修改外层的结构体,一样的道理,传入结构体指针就行了。
2.4.2 函数选项模式
下面就来看下闭包的应用,首先就是函数选项模式,在 Java 中叫 builder 建造者设计模式,也是链式编程,假设现在有一个结构体 Server,这个结构体里面有三个变量 Host、Port、Timeout。
go
type Server struct {
Host string
Port int
Timeout int
}
现在我们需要创建一个结构体,然后给里面的属性赋值,最简单的可以用下面的方式去初始化。
go
server := &Server{
Host: "127.0.0.1",
}
但是这样有一个坏处就是初始化必须给 Host 赋值,也就是初始化的属性是固定的,那有没有一种办法能够在创建 Server 的时候选择不固定的参数去初始化,函数选项模式就是为了解决这个问题。
我们先来看下如何做,首先要声明一个函数类型,这个函数接收 *Service 类型的指针。
go
type Option func(*Server)
接下来我们用闭包来封装变量的设置,下面三个函数接收属性传参,然后在闭包里面去给结构体指针赋值。
go
// 各种选项函数
func WithHost(host string) Option {
return func(s *Server) {
s.Host = host
}
}
func WithPort(port int) Option {
return func(s *Server) {
s.Port = port
}
}
func WithTimeout(timeout int) Option {
return func(s *Server) {
s.Timeout = timeout
}
}
接下来写初始化方法,这个方法参数是 opts ...Option,在这个方法中去创建结构体,然后应用所有闭包来初始化对应的属性值。
go
// 使用闭包创建服务器
func NewServer(options ...Option) *Server {
// 默认值
server := &Server{
Host: "127.0.0.1",
Port: 8080,
Timeout: 10,
}
// 调用闭包赋值
for _, option := range options {
option(server)
}
return server
}
最终测试如下:
go
func test2_3() {
// 使用函数选项模式创建服务器
s := NewServer(
WithHost("192.168.1.1"),
WithPort(9090),
WithTimeout(60),
)
fmt.Printf("%+v\n", s)
// &{Host:192.168.1.1 Port:9090 Timeout:60}
}
可以看到,调用 NewServer 的时候将我们想要传递的参数通过闭包函数添加进去就能给对应属性赋值了,这种写法比简单的通过 set 等设置属性的方法绑定结构体要简单,因为可以在创建结构体的时候就完成对某些属性的赋值。
2.4.3 ⚠️ 闭包陷阱 (1) - 循环变量捕获
go
package main
import "fmt"
func main() {
// 所有闭包都捕获同一个 i, 闭包内引用的是 i 的指针, 所以最终结果是 3 3 3
var funcs []func()
for i := 0; i < 3; i++ {
funcs = append(funcs, func() {
fmt.Println(i)
})
}
// 调用所有函数
for _, f := range funcs {
f()
}
// 输出: 3 3 3 (而不是 0 1 2)
// 原因:所有闭包都引用了同一个变量 i, 循环结束时 i=3
}
解决方法就是对于这些循环变量需要重新赋值给一个新的局部变量,又或者通过参数传进去,反正核心思想就是闭包内要引用这个变量的副本,而不是变量本身。
go
// 1. 重新赋值
func main() {
var funcs []func()
for i := 0; i < 3; i++ {
i := i
funcs = append(funcs, func() {
fmt.Println(i)
})
}
// 调用所有函数
for _, f := range funcs {
f()
}
}
// 2. 通过参数传递
func main() {
var funcs []func()
for i := 0; i < 3; i++ {
funcs = append(funcs, func(i int) func() {
return func() {
fmt.Println(i)
}
}(i))
}
// 调用所有函数
for _, f := range funcs {
f()
}
}
第二种有点绕,上面定义了一个 func(i int) func() 的匿名函数,这个函数需要接收一个 int 类型的传参,然后返回 func() 类型的闭包。
接下来我们通过 (i) 触发函数立刻执行,也就是匿名函数的立刻执行的写法。
go
func(i int) func() {
...
}(i)
立刻执行后会将 i 通过值传递传到函数中,然后在函数内部通过 return 返回的闭包里面将这个 i 输出,这样每个 i 就是原始 i 的副本了。
go
return func() {
fmt.Println(i)
}
📌 注意:Go 1.22+ 已经修复了这个问题,循环变量在每次迭代时会自动创建新副本,但是为了兼容旧版本,建议还是显式创建局部变量。
2.4.4 ⚠️ 闭包陷阱 (2) - 内存泄漏问题
如果闭包被长期持有(比如全局变量),它引用的自由变量也会一直存在,不会被垃圾回收,可能导致内存泄漏。
go
var globalClosure func() // 全局闭包
func init() {
bigArr := make([]int, 1000000)
globalClosure = func() {
// 在闭包中引用 bigArr
fmt.Println(len(bigArr))
}
}
init 函数会在 main 函数之前执行,但是由于 bigArr 被全局闭包引用,这种情况下就算 init 执行完了数组也不会被回收,所以如果有大对象用不到的要及时设置为 nil。
2.4.5 🚀 综合示例
最后我们来实现一个过滤器,通过闭包进行过滤。
go
package main
import "fmt"
// FilterFunc 数字过滤器, 判断一个数是不是符合要求的
type FilterFunc func(int) bool
func filterNumbers(nums []int, filter ...FilterFunc) []int {
var result []int
for _, num := range nums {
include := true
for _, f := range filter {
if !f(num) {
include = false
break
}
}
if include {
result = append(result, num)
}
}
return result
}
func main() {
numbers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
// 1. 创建闭包
isEven := func(num int) bool {
return num&1 == 0
}
isGreater5 := func(num int) bool {
return num > 5
}
result := filterNumbers(numbers, isEven, isGreater5)
fmt.Println(result) // [6 8 10]
// 2. 使用匿名函数
result2 := filterNumbers(numbers, func(num int) bool {
return num&1 == 0
}, func(num int) bool {
return num > 5
})
fmt.Println(result2) // [6 8 10]
}
3. 小结
这篇文章学习了 go 的函数基础以及一些函数的进阶用法,下一篇文章继续学习函数的特性,如 defer、递归 等。
如有错误,欢迎指出!!!!