写在前面:
0x01 — 函数
函数是Golang中使用最频繁的结构之一,在Golang中,函数需要注意的特点:
1. 可以返回多个结果
2. 参数和返回值类型需要明确
3. 如果定义了返回值,可以不进行return默认会返回该值
4. 指针类型参数
5. 函数作为参数
看下示例:
package main
import "testing"
// 加法函数
func funcAdd(a,b int) int { // 此处等效(a int, b int)
return a + b
}
func TestFunc(t *testing.T) {
c, d := 1, 2
var e int = funcAdd(c, d) // 调用函数
t.Log("e的值:", e)
}
输出:
=== RUN TestFunc
func_test.go:13: e的值: 3
--- PASS: TestFunc (0.00s)
PASS
以上是最简单的标准函数的调用,函数参数如果类型相同可以批量定义类型,继续看下去:
package main
import (
"sort"
"testing"
)
// 排序函数
func SortFunc(l []int) []int {
if len(l) < 2 {
return l
} else{
sort.Ints(l) // 对l进行排序
return l
}
}
// 排序函数
func SortFuncPtr(l *[]int) *[]int {
if len(*l) < 2 {
return l
} else{
sort.Ints(*l) // 对l进行排序
return l
}
}
func TestFunc1(t *testing.T) {
// 定义两个slice
var a = []int{2,1,3}
var b = []int{5,4,6}
// 调用传输变量函数
var c []int = SortFunc(a)
// 调用传输指针的函数
var d *[]int = SortFuncPtr(&b)
//通过打印c,d的值发现都可以正常排序
t.Log("c的值:", c)
t.Log("d的值:", *d) // 因为d是指针,需要取得d的值通过取值符*
// 我们打印下各自的指针数据,就可以看出区别了
t.Logf("a的指针:%p", &a)
t.Logf("b的指针:%p", &b)
t.Logf("c的指针:%p", &c)
t.Logf("d的指针:%p", d)
}
输出:
=== RUN TestFunc1
func_test.go:36: c的值: [1 2 3]
func_test.go:37: d的值: [4 5 6]
func_test.go:39: a的指针:0xc00000c048
func_test.go:40: b的指针:0xc00000c060
func_test.go:41: c的指针:0xc00000c078
func_test.go:42: d的指针:0xc00000c060
--- PASS: TestFunc1 (0.00s)
PASS
以上示例主要是介绍指针作为参数的区别,可以看到b的指针和d是样的,也就是在使用指针作为参数传递时,我们可以在不增加内存空间的基础上进行操作,这个含义对于内存控制很有帮助。如果传递的是普通变量,则在函数内获取的是这个变量内存的copy,增加一份新的内存,对于如何使用,建议根据环境视情况而定。
对于函数返回值,可以看下面的示例:
// 排序函数
func SortFuncPtr(l *[]int) (k *[]int) {
k = l // k被返回值定义了类型,表示已经声明了,所以此处可以直接赋值
if len(*k) < 2 {
} else{
sort.Ints(*k) // 对l进行排序
}
return // 由于我们在返回值中定义了返回值的名称,所以可以省略return的返回内容
// return &[]int{4,45, 66} //这个返回也是不会报错的
}
上面的示例和上一个SortFuncPtr函数效果是一样的,不同之处在于我们在函数返回是定义了一个变量名k,Golang中可以对返回类型进行命名,表示返回的是这个变量,可以称为具名返回值, 具名返回参数不管个数多少个,都需要使用圆括号 ( ) 将所有的具名返回参数(包括一个)与对应的数据类型包括起来 ,同时return可以省略返回值内容,但是,如果返回其他非定义的变量的值,只要类型相同也是可以的
Golang中函数可以被作为参数进行传递:
package main
import (
"fmt"
"testing"
)
// 函数参数
func execFunc(f func(int) int, x int) int{ // 此处注意,f的类型是func(int) int, func(int) int是一个整体,表示函数和返回值
fmt.Println("执行函数!")
return f(x)
}
func TestArgs(t *testing.T) {
t.Log("函数参数测试")
// 定义一个函数fa, 传一个int参数并返回参数
fa := func(b int) int{
fmt.Println("我是函数fa")
return b
}
fe := execFunc(fa, 12)
t.Log("fe的值:", fe)
}
输出:
=== RUN TestArgs
func_test.go:15: 函数参数测试
执行函数!
我是函数fa
func_test.go:22: fe的值: 12
--- PASS: TestArgs (0.00s)
PASS
函数作为参数要注意是函数本身作为参数,不是函数的执行结果,同时要明确整个函数的结构,如参数的类型,还有返回值的类型,但是我们发现一个函数作为类型会很长,这样写感觉很不友好,也不符合Go简洁的原则,所以我们可以将函数类型提取出来:
// 定义一个函数类型,来表示此函数
type testFuncT func(int) int
// 函数参数
func execFuncNew(f testFuncT, x int) int{ // 将原来函数类型替换成定义的类型,这样就简洁多了
fmt.Println("执行函数!")
return f(x)
}
可以看到通过定义一个新的函数类型可以简化很大一部分内容。
如果使用结构体作为参数,看如下示例:
package main
import (
"fmt"
"testing"
)
// 学生
type student struct {
name string
id int
class string
}
func readStudent(s student, q *student) {
fmt.Println("学生s学号:",s.id)
fmt.Println("学生s姓名:",s.name)
fmt.Println("学生s班级:",s.class)
fmt.Println("学生q学号:",s.id)
fmt.Println("学生q姓名:",s.name)
fmt.Println("学生q班级:",s.class)
}
// 测试结构体指针
func TestStructPtr(t *testing.T) {
s1 := student{"小米",20210902,"02-1班"}
s2 := &student{"小陈",20210901,"02-2班"}
readStudent(s1, s2)
t.Log("输出结束")
}
输出:
=== RUN TestStructPtr
学生s学号: 20210902
学生s姓名: 小米
学生s班级: 02-1班
学生q学号: 20210902
学生q姓名: 小米
学生q班级: 02-1班
func_test.go:28: 输出结束
--- PASS: TestStructPtr (0.00s)
PASS
可以发现,s和q参数一个是变量一个是指针,但是使用方式没区别,这是Golang中的规则, 在调用结构体时,不管是变量还是指针,都可以通过链式调用其成员的值 。
0x02 — defer
在开发中,我们经常会使用打开文件,建立连接等操作,这些操作都有个关键点,就是打开或者创建后,在使用完毕时都要手动关闭下,假如我们在java等语言中,这个关闭操作可能会在finaly语句中进行关闭。在Golang中,这个操作将变得非常简单,因为在Golang中有个deferred机制,通过defer关键字触发,具体含义是,在声明defer后,当执行到defer语句后,其后紧跟的函数和参数表达式得到计算,但直到包含该defer语句的函数执行完毕时,defer后的函数才会被执行。我们可以在一个函数中执行多条defer语句,它们的执行顺序与声明顺序相反(栈)。defer语句经常被用于处理成对的操作,如关闭、断开连接、释放锁。通过defer机制,不论函数逻辑多复杂,都能保证在任何执行路径下,资源被释放,即使由于panic导致异常结束,defer也会在结束前执行。
package main
import (
"fmt"
"io/ioutil"
"os"
"testing"
)
func TestDefer(t *testing.T) {
//通过打开读取文件进行验证defer
// 操作文件可以通过os库
f1, err := os.Open("./text.txt")
if err != nil {
t.Log("文件打开失败")
}
t.Logf("文件句柄:%v", f1)
// 定义一个defer,将close包含在函数里中,是因为可以获取关闭成功或失败的输出
// 同时函数可以固化当前状态,即时后续将f1重新赋值了,也不影响文件关闭
defer func(f1 *os.File) {
err := f1.Close()
if err != nil {
t.Log("文件关闭失败")
} else {
t.Log("文件关闭成功")
}
}(f1)
// 如果我们明确知道f1关闭没问题,可以简化defer
//defer f1.Close()
text, err := ioutil.ReadAll(f1) //ioutil是对io的部分接口的封装,不然读取文件需要遍历操作
if err != nil {
t.Log("文件读取失败")
} else {
t.Logf("text类型:%T, 文件内容:%s", text, string(text))
}
}
输出:
=== RUN TestDefer
method_test.go:17: 文件句柄:&{0xc0000ac240}
method_test.go:35: text类型:[]uint8, 文件内容:I am text, balabala
method_test.go:25: 文件关闭成功
--- PASS: TestDefer (0.00s)
PASS
可以看到输出,文件关闭是在最后输出的,同时我们读取出来的内容类型是[]uint8(等价[]byte)。
0x03 — 总结
函数多返回值中 ,通过_ 来接收不需要使用的返回值,这种约定一定要熟悉, 开发中常常会遇见函数返回多个值。会经常使用 _ 丢弃不想要的返回值。具名返回值并不常见。但是迟早都会遇到,所以了解他们是很重要的。
函数递归,匿名函数,可变参数, recover和panic等高级用法会在后面找时间专门说明。