函数定义

函数定义

在 Go 语言中,函数是第一类对象,我们可以将函数保持到变量中。函数主要有具名和匿名之分,包级函数一般都是具名函数,具名函数是匿名函数的一种特例。当然,Go 语言中每个类型还可以有自己的方法,方法其实也是函数的一种。

// 具名函数
func Add(a, b int) int {
	return a+b
}

// 匿名函数
var Add = func(a, b int) int {
	return a+b
}

参数

不定参数

Go 语言中的函数可以有多个参数和多个返回值,参数和返回值都是以传值的方式和被调用者交换数据。在语法上,函数还支持可变数量的参数,可变数量的参数必须是最后出现的参数,可变数量的参数其实是一个切片类型的参数。

// 多个参数和多个返回值
func Swap(a, b int) (int, int) {
	return b, a
}

// 可变数量的参数
// more 对应 []int 切片类型
func Sum(a int, more ...int) int {
	for _, v := range more {
		a += v
	}
	return a
}

当可变参数是一个空接口类型时,调用者是否解包可变参数会导致不同的结果:

func main() {
	var a = []interface{}{123, "abc"}

	Print(a...) // 123 abc
	Print(a)    // [123 abc]
}

func Print(a ...interface{}) {
	fmt.Println(a...)
}

切片传递

Go 语言中,如果以切片为参数调用函数时,有时候会给人一种参数采用了传引用的方式的假象:因为在被调用函数内部可以修改传入的切片的元素。其实,任何可以通过函数参数修改调用参数的情形,都是因为函数参数中显式或隐式传入了指针参数。函数参数传值的规范更准确说是只针对数据结构中固定的部分传值,例如字符串或切片对应结构体中的指针和字符串长度结构体传值,但是并不包含指针间接指向的内容。将切片类型的参数替换为类似 reflect.SliceHeader 结构体就很好理解切片传值的含义了:

func twice(x []int) {
	for i := range x {
		x[i] *= 2
	}
}

type IntSliceHeader struct {
	Data []int
	Len  int
	Cap  int
}

func twice(x IntSliceHeader) {
	for i := 0; i < x.Len; i++ {
		x.Data[i] *= 2
	}
}

因为切片中的底层数组部分是通过隐式指针传递(指针本身依然是传值的,但是指针指向的却是同一份的数据),所以被调用函数是可以通过指针修改掉调用参数切片中的数据。除了数据之外,切片结构还包含了切片长度和切片容量信息,这 2 个信息也是传值的。如果被调用函数中修改了 Len 或 Cap 信息的话,就无法反映到调用参数的切片中,这时候我们一般会通过返回修改后的切片来更新之前的切片。这也是为何内置的 append 必须要返回一个切片的原因。

返回值

多值返回

Go 与众不同的特性之一就是函数和方法可返回多个值,将错误值返回(例如用 -1 表示 EOF)和修改通过地址传入的实参。在 os 包中,file 的 Write 方法签名如下:

func (file *File) Write(b []byte) (n int, err error)

正如文档所述,它返回写入的字节数,并在 n != len(b) 时返回一个非 nil 的 error 错误值。这是一种常见的编码风格,更多示例见错误处理一节。

多值返回的一个常用场景,就是在会引发错误的方法上,往往最后一个出参的类型为 error。

可命名结果形参

Go 函数的返回值或结果“形参”可被命名,并作为常规变量使用,就像传入的形参一样。命名后,一旦该函数开始执行,它们就会被初始化为与其类型相应的零值;若该函数执行了一条不带实参的 return 语句,则结果形参的当前值将被返回。

func Find(m map[int]int, key int) (value int, ok bool) {
	value, ok = m[key]
	return
}

如果返回值命名了,可以通过名字来修改返回值,也可以通过 defer 语句在 return 语句之后修改返回值:

func Inc() (v int) {
	defer func(){ v++ } ()
	return 42
}

当返回值得类型不同时,Naked Returns 优于 Named Result Parameters,例如:

func (n *Node) Parent1() *Node
func (n *Node) Parent2() (*Node, error)

// 优于

func (n *Node) Parent1() (node *Node)
func (n *Node) Parent2() (node *Node, err error)

当返回值中包含相同类型的出参时,命名返回值可以提高代码的可读性,例如:

// Location returns f's latitude and longitude.
// Negative values mean south and west, respectively.
func (f *Foo) Location() (lat, long float64, err error)

当方法行数较少时,可以使用 Naked return(未命名返回)。当方法达到中等长度时,就可以考虑使用命名出参了。但是,很多时候写文档注释,其实更为重要。最后,命名结果参数,能够 defer 中修改返回值。

上一页
下一页