[golang] 函数

Go 程序是从 main 函数开始的,

func main(){
// ...
}

上述 main 函数没有入参,也没有返回。函数的一般形式会包含一个入参列表以及返回类型,e.g.:

func div(numerator int, denominator int) int {
	if denominator == 0 {
		return 0
	}
	return numerator / denominator
}

当入参类型相同时,可省略类型只保留参数列表中最后一个入参的类型:

func div(numerator, denominator int) int {

命名参数与可选参数

Go 中函数是不支持 named and optional 参数的,但可通过结构体来变相实现。

type Param struct {
	foo int
	bar string
	baz int
}

func test(param Param) int {
	return 0
}

func main() {
	test(Param{foo: 1})
	test(Param{foo: 1, baz: 2})
	test(Param{foo: 1, baz: 2, bar: "hello"})
}

变参函数

但 Go 支持 Variadic parameters。可变的入参部分位于参数列表最后,在类型前面加 ... 表示。常见的就是用来打印输出的 fmt.Println()

func Println(a ...interface{}) (n int, err error)

变参部分在接收到之后,其实是个 slice,因此,可以像操作 slice 一样操作它。同时,在传递时,也可以传递展开后的 slice。

func main() {
	add(3)
	add(3, 1)
	add(3, 1, 2)
	s := []int{1, 2, 3}
	add(3, s...)
	add(3, []int{1, 2, 3}...)
}

// 输出结果:
// []
// [4]
// [4 5]
// [4 5 6]
// [4 5 6]

函数的返回值

Go 函数支持返回多个值,返回类型中用逗号将各个返回类型分隔。

func divAndRemainder(numerator int, denominator int) (int, int, error) {
	if denominator == 0 {
		return 0, 0, errors.New("cannot divide by zero")
	}
	return numerator / denominator, numerator % denominator, nil
}

func main() {
	result, remainder, err := divAndRemainder(5, 2)
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
	fmt.Println(result, remainder)
}

约定俗成地,最后一个返回值为错误信息。

忽略返回值

如果不需要某个返回值,可使用 _ 来忽略,但占位必需有。

_, _, err := divAndRemainder(5, 2)
_, remainder, err2 := divAndRemainder(5, 2)
result, remainder2, _ := divAndRemainder(5, 2)

注意与 for-range 语句进行区分,如果我们不需要循环返回的值,除了使用 _ 来忽略,还可直接不书写这第二个变量的:

for i, _ := range weeks {
# 
for i := range weeks {
  fmt.Println(i)
}

命名返回

除了可返回多个值,还可为这些值指定名称。

func divAndRemainder(numerator int, denominator int) (result int, remainder int, err error) {
	if denominator == 0 {
		return result, remainder, errors.New("cannot divide by zero")
	}
	result, remainder = numerator/denominator, numerator%denominator
	return result, remainder, err
}

声明返回值名称本质上是提前在函数作用域中声明了对应名称的变量,可在函数体中正常使用这些变量。

如果只想命名部分变量,其他返回可使用 _ 来代替。

需要注意,返回值的名称只约束函数,不约束使用的地方。即,调用后仍然可赋值给任意名称的变量:

x, y, z := divAndRemainder(5, 2)

另外,声明了返回变量,并不代表一定返回它,实际返回值仍以 return 语句为准,即,return 5 返回 5 而不是声明的返回变量。

空返回

使用命名返回的情况下,return 时可省略变量直接返回,因为 return 后没跟任何东西,所以叫空返回。


func foo(a int, b int) (c int, d int, err error) {
	if b == 0 {
		err = errors.New("xxx")
		return
	}
	c, d = 1, 2
	return
}

func main() {
	fmt.Println(foo(1, 0)) // 0 0 xxx
	fmt.Println(foo(1, 2)) // 1 2 <nil>
}

因为空返回不够直观,不建议使用。

package main

import (
	"errors"
	"fmt"
)

func add(i int,j int) int{
	return i+j
}
func sub(i int,j int) int{
	return i-j
}
func mul(i int,j int) int{
	return i*j
}
func div(i int,j int) int{
	return i/j
}

func main() {
	m:=map[string]func(int,int)int{
		"+":add,
		"-":sub,
		"*":mul,
		"/":div,
	}
}

作为值传递的函数

用过 JavaScript 肯定对于将函数作为普通值一样使用不陌生,可以用来赋值,可以用来传参。

Go 中的函数亦然。

type Op func(int, int) int

func calculator(a int, b int, op Op) int {
	return op(a, b)
}

func add(a int, b int) int {
	return a + b
}

func sub(a int, b int) int {
	return a - b
}

func main() {
	result := calculator(1, 2, add)
	result2 := calculator(1, 2, sub)
	fmt.Println(result, result2) // 3 -1
}

匿名函数

接上面的例子,在传递函数时,可以不事先声明一个正式的函数,而是在用到的时候临时创建一个不带名字的匿名函数:

result3 := calculator(1, 2, func(a int, b int) int {
	return a * b
})

// 甚至直接调用匿名函数
result4 := func(a int, b int) int {
	return a / b
}(1, 2)

fmt.Println(result3, result4) // 2 0

闭包

同 JavaScript,函数可以访问其定义处外层的变量,这些变量在外层作用域销毁时因为被当前函数保持,形成闭包,所以没销毁。典型的场景就是将函数作为回调进行传递时。

type Student struct {
	name string
	age  int
}

func main() {
	a := []Student{
		{"foo", 1},
		{"bar", 2},
	}

	sort.Slice(a, func(i int, j int) bool {
		return a[i].age > a[j].age
	})
	fmt.Println(a)
}

此时我们说 a slice 被排序回调形成的闭包所持有。

输出结果:

[{bar 2} {foo 1}]

defer

defer 标识的语句在函数 return 作用域结束后执行,用于释放资源,做清理工作。

func createFile(p string) *os.File {
	f, err := os.Create(p)
	if err != nil {
		panic(err)
	}
	return f
}

func writeFile(f *os.File) {
	fmt.Fprintln(f, "hell world!")
}

func closeFile(f *os.File) {
	err := f.Close()
	if err != nil {
		fmt.Println("error:\n", err)
		os.Exit(1)
	}

}

func main() {
	f := createFile("./foo.txt")
	defer closeFile(f)
	writeFile(f)
}

一般,在创建了资源比如网络/文件 IO,应及时提供 defer 释放语句再开始其他业务。因为不管程序执行结果如何,defer 都会被执行,资源会得到正确的释放。

多个 defer 的语句执行顺序是先后进先出,可以理解为,每次 defer 将需要执行的语句压入任务栈,释放时从栈顶开始释放。

func main() {
	defer func() {
		fmt.Println("defer 1")
	}()
	defer func() {
		fmt.Println("defer 2")
	}()
	defer func() {
		fmt.Println("defer 3")
	}()
}

// defer 3
// defer 2
// defer 1

Go 中函数是值传递

Go 中的函数都是值传递,意味着参数会被复制一份在函数体中被操作,原数据不受影响:

type Person struct {
	name string
	age  int
}

func modify(i int, s string, p Person) {
	i += 1
	s += "!"
	p.age += 1
}

func main() {
	i, s, p := 1, "hello", Person{
		name: "foo",
		age:  1,
	}
	modify(i, s, p)
	fmt.Println(i, s, p) // 1 hello {foo 1}
}

但在处理 slice 和 map 时稍有不同:

func modMap(m map[int]string) {
	m[0] = "hello"
	m[1] = "world"
	delete(m, 2)
}

func modSlice(s []int) {
	for i, v := range s {
		s[i] = v * 2
	}
	s = append(s, 10)
}

func main() {
	m := map[int]string{
		1: "foo",
		2: "bar",
		3: "baz",
	}
	modMap(m)

	s := []int{
		1, 2, 3,
	}
	modSlice(s)

	fmt.Println(m, s) // map[0:hello 1:world 3:baz] [2 4 6]
}

对于 map,函数体中所做的变更全部体现到了原数据上,而 slice,除了无法 append ,对其中元素的操作也是生效的。原因是 map 及 slice 本质上是指针,当通过这个指针操作其中元素时,操作的就是原数据。