# 函数

# 基本概念

函数(function)是结构化编程的最小模块单元。

将复杂算法过程分解成若干较小任务,隐藏细节,使得程序结构更加清晰,易于维护。函数被设计成相对独立,通过接收输入参数完成一段算法指令,输出或存储相关结果。因此,函数还是代码复用和测试的基本单元。

关键字 func 用于定义函数。有些不方便的限制,但也借鉴了动态语言的优点。

无需前置声明。
支持不定长变参。
支持多返回值。
支持命名返回值。
支持匿名函数和闭包。

不支持命名嵌套定义(nested)。
不支持同名函数重载(overload)。
不支持默认参数。

函数是第一类对象。
只能判断是否为 nil,不支持比较操作。

func main() {
    // 不支持命名函数嵌套,得改用匿名。
    func add(x, y int) int {  // syntax error: unexpected add
        return x + y
    }
}
func a() {}
func b() {}
func main() {
	println(a == nil)
	// println(a == b) // ~ invalid: func can only be compared to nil
}

具备相同签名(参数及返回值列表,不包括参数名)的视作同一类型。

func exec(f func()) {
	f()
}
func main() {
	var f func() = func() { println("hello, world!" )}
	exec(f)
}

基于阅读和维护角度,使用命名类型更简洁。

type FormatFunc func(string, ...any) string
// 如不使用命名类型,这个参数签名会长到没法看。
func toStringV2(f FormatFunc, s string, a ...any) string {
    return f(s, a...)
}
func toStringV1(f func(string, ...any) string, s string, a ...any) string {
    return f(s, a...)
}
func main() {
	println(toStringV2(fmt.Sprintf, "%d", 100))
}

安全返回局部变量指针。

编译器通过逃逸分析(escape analysis)来决定,是否在堆上分配内存。
但优化后(内联),最终生成的代码未必如此。总之,为了减少垃圾回收压力,编译器竭尽全力在栈分配内存。

func test() *int {
	a := 0x100
	return &a
}
func main() {
	var a *int = test()
	println(a, *a)
}
/*
$ go build -gcflags "-m"
  moved to heap: a
*/

不支持尾递归优化。

尾递归:若函数在尾位置调用自身(或是一个尾调用本身的其他函数等等),则称这种情况为尾递归

func factaux (n, ret int) int {
    if n < 2 { return ret }
    return factaux(n - 1, ret * n);
}
func main() {
    println(factaux(3, 1))
}
/*
$ go build
$ go tool objdump -s "main\.factaux" ./test
TEXT main.factaux(SB)
     CALL main.factaux(SB)			
  
*/

# 函数命名规则

在避免冲突的前提下,函数命名本着 精简短小、望文知意 的原则。

●避免只能通过大小写区分的同名函数。
●避免与内置函数同名,这会导致误用。
●避免使用数字,除非特定专有名词。

函数和方法的命名规则稍有不同。 方法通过选择符调用,且具备状态上下文,可使用更简短的动词命名。

# 函数参数

对参数的处理偏向保守。

●按签名顺序传递相同数量和类型的实参。
●不支持有默认值的可选参数。
●不支持命名实参。
●不能忽略 _ 命名的参数。

●参数列表中,相邻同类型参数声明可合并。
●参数可视作函数局部变量。

形参(parameter)是函数定义中的参数,实参(argument)则是函数调用时所传递参数。 形参同函数局部变量,而实参是函数外部对象。

func test(x, y int, s string, _ bool) *int {
	// var x string // ~  x redeclared in this block
	return nil
}
func main() {
	// test(1, 2, "abc")
	//      ~~~~~~~~~~~ not enough arguments in call to test
	//                      have (number, number, string) 
	//                      want (int, int, string, bool)
}

不管是指针、引用类型,还是其他类型,参数总是 值拷贝传递(pass by value)。区别无非是复制完整目标对象,还是仅复制头部或指针而已。

//go:noline
func test(x *int, s []int) {
	println(*x, len(s))
}
func main() {
	x := 100
	s := []int{1, 2, 3}
	test(&x, s)
}
/*
$ go build
$ go tool objdump -S -s "main\.main" ./test
func main() {
	x := 100
  0x462c94		MOVQ $0x64, 0x20(SP)
  
	s := []int{1, 2, 3}
  0x462ca9		MOVQ $0x1, 0x28(SP)	
  0x462cb2		MOVQ $0x2, 0x30(SP)	
  0x462cbb		MOVQ $0x3, 0x38(SP)	
  
	test(&x, s)
  0x462cc4		LEAQ 0x20(SP), AX	; &x
  0x462cc9		LEAQ 0x28(SP), BX	; s.ptr
  0x462cce		MOVL $0x3, CX		; s.len
  0x462cd3		MOVQ CX, DI		
  0x462cd6		CALL main.test(SB)	
}
*/

何时用指针类型参数:

●实参对象复制成本过高。
●需要修改目标对象。
●用二级指针实现传出(out)参数。

//go:noinline
func test(p **int) {
	x := 100
	*p = &x
}
func main() {
	var p *int
	test(&p)
	println(*p)
}

参数命名为 _,表示忽略。
比如,实现特定类型函数,忽略掉无用参数,以避免内部污染。

func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
    ...
}

出现如下情形,建议以复合结构代替多个形参。

●参数过多,不便阅读。
●有默认值的可选参数。
●后续调整,新增或重新排列。

type Option struct {
	addr    string
	port    int
	path    string
	timeout time.Duration
	log     *log.Logger
}
// 创建默认参数。
func newOption() *Option {
	return &Option{
		addr:    "0.0.0.0",
		port:    8080,
		path:    "/var/test",
		timeout: time.Second * 5,
		log:     nil,
	}
}
func server(option *Option) {
	fmt.Println(option)
}
func main() {
	opt := newOption()
	opt.port = 8085     // 修改默认设置。
	server(opt)
}

# 函数变参

变参本质上就是切片。只能接收一到多个同类型参数,且必须放在列表尾部。

func test(s string, a ...int) {
	fmt.Printf("%T, %v\n", a, a)
}
func main() {
	test("abc", 1, 2, 3, 4)
}
// []int, [1 2 3 4]

切片作为变参时,须进行展开操作。

func test(a ...int) {
	fmt.Println(a)
}
func main() {
	a := [3]int{10, 20, 30}
    
    // 转换为切片后展开。
	test(a[:]...) 
}

既然变参是切片,那么参数复制的仅是切片自身。正因如此,就有机会修改实参。

//go:noinline
func test(a ...int) {
	for i := range a {
		a[i] += 100
	}
}
func main() {
	a := []int{1, 2, 3}
	test(a...)
	
	println(a[1])
}
/*
$ go build
$ go tool objdump -S -s "main\.main" ./test
func main() {
	a := []int{1, 2, 3}
  0x462c20		MOVQ $0x1, 0x20(SP)	
  0x462c29		MOVQ $0x2, 0x28(SP)	
  0x462c32		MOVQ $0x3, 0x30(SP)	
  
	test(a...)
  0x462c3b		LEAQ 0x20(SP), AX	; a.ptr
  0x462c40		MOVL $0x3, BX		; a.len
  0x462c45		MOVQ BX, CX		
  0x462c48		CALL main.test(SB)
*/

# 参数生命周期

参数和其他局部变量的生命周期,未必能坚持到函数调用结束。 垃圾回收非常积极,对后续不再使用的对象,可能会提前清理。

//go:noinline
func test(x []byte) {
	println(len(x))
    // 模拟垃圾回收触发。
	runtime.SetFinalizer(&x, func(*[]byte){ println("drop!") })
	runtime.GC()
	println("exit.")
    
    // 确保目标活着。
	// runtime.KeepAlive(&x)
}
func main() {
	test(make([]byte, 10<<20))
}
// 10485760
// drop!
// exit.

# 函数返回值

借鉴动态语言的多返回值模式,让函数得以返回更多状态。

func div(x, y int) (int, error) {
	if y == 0 {
		return 0, errors.New("division by zero")
	}
    
	return x / y, nil
}
func main() {
	z, err := div(6, 2)
	if err != nil {
		log.Fatalln(err)
	}
	println(z)
}

●用 _ 忽略不想要的返回值。
●多返回值可用作调用实参,或当结果直接返回。

func log(x int, err error) {
	fmt.Println(x, err)
}
func test() (int, error) {
	return div(5, 0)        // 直接返回。
}
func main() {
	log(test())             // 直接当作多个实参。
}

●有返回值的函数,所有逻辑分支必须有明确 return 语句。
●除非 panic,或无 break 的死循环。

func test(x int) int {
	if x > 0 {
		return 1
	} else if x < 0 {
		return -1
	}
	// missing return
}
func test(x int) int {
	for {
		break
	}
	// missing return
}

# 返回值命名

对返回值命名,使其像参数一样当作局部变量使用。

●函数声明更加清晰、可读。
●更好的代码编辑器提示。

func paging(sql string, index int) (count int, pages int, err error) {
}

●可由 return 隐式返回。
●如被同名遮蔽,须显式返回。

func add(x, y int) (z int) {
	z = x + y
	return
}
func add(x, y int) (z int) {
	// 作为 “局部变量”,不能同级重复定义。
	{
		z := x + y
		// return // ~ result parameter z not in scope at return
		return z
	}
}

要么不命名,要么全部命名,否则编译器会搞不清状况。

func test() (int, s string, e error) {
	// return 0, "", nil // ~ cannot use 0 as string value in return statement
	
}

如返回值类型能明确表明含义,就尽量不要对其命名。

func NewUser() (*User, error)

# 匿名函数

匿名函数是指没有名字符号的函数。除此之外,和普通函数类似。

●可在函数内定义匿名函数,形成嵌套。
●可随定义直接传参调用,并返回结果。
●可保存到变量、结构字段,或作为参数、返回值传递。

●匿名函数可引用环境变量,形成闭包。
●不曾使用的匿名函数被当作错误。
●不能使用编译指令。(//go:noinline)

func main() {
    // 定义时调用。
	_ = func(s string) string {
		return "[" + s + "]"
	}("abc")
	// --------------
    
    // 变量。
	add := func(x, y int) int {
		return x + y
	}
	_ = add(1, 2)
}
func wrap(f func(string)string) func() {
	return func() {
		println(f("abc"))
	}
}
func main() {
	wrap(func(s string) string {
		return "[" + s + "]"
	})()
}
func main() {
	// func(s string) { println(s) }
	// ~~~~ func is not used
}

编译器为匿名函数自动生成名字。

func main() {
	func() {
		println("abc")
	}()
}
/*
$ go build -gcflags "-l"
$ go tool objdump -S -s "main\.main" ./test
	CALL main.main.func1(SB)
*/

普通函数和匿名函数都可作为结构体字段,或经通道传递。

func main() {
	calc := struct {
		add func(int, int) int
		mul func(int, int) int
	}{
		add: func(x, y int) int { return x + y },
		mul: func(x, y int) int { return x * y },
	}
	_ = calc.add(1, 2)
}
func main() {
	fn := make(chan func())
	go func() {
		defer close(fn)
		f := <- fn
		f()
	}()
	fn <- func(){ println("hi!") }
	<- fn
}

匿名函数是常见的重构手段:将大段代码分解成多个相对独立的逻辑单元。

●不提升代码作用域的前提下,分离流程和细节。
●依赖函数接口而非具体代码,逻辑层次更清晰。
●无状态逻辑与有状态变量分离,更利于测试。
●作用域隔离,修改代码时不会引发外部污染。

# 闭包

闭包(closure)是匿名函数和其引用的外部环境变量组合体。

//go:noinline
func test() func() {
	x := 100
	println(&x, x)
	return func() {      // 返回闭包,而非函数。
		x++
		println(&x, x)   // 引用环境变量 x 。
	}
}
func main() {
	test()()
}
/*
0xc000018060 100
0xc000018060 101
*/

从上例输出结果看,闭包会延长环境变量生命周期。 所谓闭包,实质上是由匿名函数和(一到多个)环境变量指针构成的结构体。

编译器优化可能改变闭包返回和执行方式,以实际输出为准。

/*
$ go build -gcflags "-m -S"
    moved to heap: x
---- func test ---------------------
    TEXT    "".test (SB), ABIInternal
x := 100
    LEAQ    type.int (SB), AX
    CALL    runtime.newobject (SB)
    MOVQ    AX, "".&x+16 (SP)
    MOVQ    $100, (AX)
return func (){...}
    LEAQ    type.noalg.struct { F uintptr; "".x *int }(SB), AX
    CALL    runtime.newobject (SB)
    LEAQ    "".test.func1 (SB), CX  ; 匿名函数。
    MOVQ    CX, (AX)
    MOVQ    "".&x+16 (SP), CX       ; 环境变量 x 地址。
    MOVQ    CX, 8 (AX)
    
    RET
*/

接收到的闭包,经专用寄存器(DX),将环境变量地址传递给匿名函数。

/*
--- func main ----------------------
    TEXT    "".main (SB), ABIInternal
    CALL    "".test (SB)   ; 返回闭包。
    MOVQ    (AX), CX      ; 匿名函数。
    MOVQ    AX, DX        ; 传递给匿名函数。
    CALL    CX            ; 调用匿名函数。
--- func test.func1 ----------------------
    TEXT    "".test.func1 (SB), NEEDCTXT|ABIInternal
    MOVQ    8 (DX), AX         ; 从闭包中获取环境变量。
    MOVQ    AX, "".&x+8 (SP)
    CALL    runtime.printlock (SB)
    MOVQ    "".&x+8 (SP), AX
    CALL    runtime.printpointer (SB)
    CALL    runtime.printnl (SB)
    CALL    runtime.printunlock (SB)
    RET
*/

从实现机制看,拿到手的闭包除非执行,否则只是存有两个指针的结构体。 这会导致 “延迟求值”,一个初学者时常出错的问题。

func test() (s []func()) {
	for i := 0; i < 2; i++ {
        // 循环使用的 i 始终是同一变量。
        // 闭包(存储 i 指针)被添加到切片内。
		s = append(s, func() {
			println(&i, i)
		})
	}
    
	return
}
func main() {
	for _, f := range test() {
        // 执行闭包中的匿名函数。
        // 它以指针读取环境变量 i 的值。
        // 自然是最后一次循环时 i = 2。
		f()
	}
}
// 0xc000018060 2
// 0xc000018060 2
func test() (s []func()) {
	for i := 0; i < 2; i++ {
		// 用不同的环境变量。
		x := i
		s = append(s, func() {
			println(&x, x)
		})
	}
	return
}
func main() {
	for _, f := range test() {
		f()
	}
}
// 0xc000082000 0
// 0xc000082008 1

闭包的应用

静态局部变量。

func test() func() {
	n := 0
	return func() {
		n++
		println("call:", n)
	}
}
func main() {
	f := test()
	f()
	f()
	f()
}
/*
call: 1
call: 2
call: 3
*/

模拟方法,绑定状态。

func handle(db Database) func(http.ResponseWriter, *http.Request) {
	return func(w http.ResponseWriter, r *http.Request) {
		url := db.Get()
		io.WriteString(w, url)
	}
}
func main() {
	db := NewDatabase("localhost:5432")
    
	http.HandleFunc("/url", handle(db))
	http.ListenAndServe(":3000", nil)
}

包装函数,改变其签名或增加额外功能。

package main
import (
	"fmt"
	"time"
)
// 修改签名。
func partial(f func(int), x int) func() {
	return func() {
		f(x)
	}
}
// 增加功能。
func proxy(f func()) func() {
	return func() {
		n := time.Now()
		defer func() {
			fmt.Println(time.Now().Sub(n))
		}()
		f()
	}
}
func main() {
	test := func(x int) {
		println(x)
	}
	var f func() = partial(test, 100)
	f()
	proxy(f)()
}
// 100
// 100
// 16.1µs

# 延迟调用

语句 defer 注册 稍后执行的函数调用。这些调用被称作 延迟调用,因为它们直到当前函数结束前(RET)才被执行。常用于资源释放、锁定解除,以及错误处理等操作。

●注册非 nil 函数,复制执行所需参数。
●多个延迟调用按 FILO 次序执行。
●运行时确保延迟调用总被执行。( os.Exit 除外)

func main() {
	f, err := os.Open("./main.go")
	if err != nil {
		log.Fatalln(err)
	}
	defer f.Close()
	
	b, err := io.ReadAll(f)
	if err != nil {
		log.Fatalln(err)
	}
	println(string(b))
}
func main() {
	defer println(1)   // 此处是注册,而非执行。
	defer println(2)   // 注册需提供执行所需参数。
	defer println(3)   // 按 FILO 次序执行。
	println("main")    
}                      // 退出前执行 defer。
/*
main
3
2
1
*/
var f func()
	// defer f() // ~ panic: invalid memory address or nil pointer dereference
}

正常退出,或以 panic 中断,运行时都会确保延迟函数被执行。

func main() {
	defer println("defer!")
	panic("panic!")          // 换成 os.Exit,会立即终止进程,
}                            // 肯定不执行延迟调用。
/*
defer!
panic: panic!
*/

注意参数复制行为,必要时以指针或引用类型代替。

func main() {
	x := 100
	defer println("defer:", x)
	x++
	println("main:", x)
}
/*
 main: 101
 defer: 100
*/

延迟调用以闭包形式修改命名返回值,但须注意执行次序。

func test() (z int) {
	defer func() {
		z += 200
	}()
    
    return 100   // return = (ret_val_z = 100, call defer, ret)
}
func main() {
	println(test())  // 300
}

如果不是命名返回值,那么结果截然不同。

func test() int {
	z := 0
	defer func() {
		z += 200    // 本地变量,与返回值无关。
	}()
    
    z = 100
    return z        // return = (ret_val = 100, call defer, ret)
}
func main() {
	println(test())  // 100
}

# 错误处理

没有结构化异常,以返回值标记错误状态。

// builtin
type error interface {
	Error() string
}
// io
func ReadAll(r Reader) ([]byte, error)

●标志性错误:一种明确状态,比如 io.EOF
●提示性错误:返回可读信息,提示错误原因。

必须检查所有返回错误,这也导致代码不太美观。

func main() {
	file, err := os.Open("./main.go")
	if err != nil {
		log.Fatalln(err)
	}
	defer file.Close()
	content, err := io.ReadAll(file)
	if err != nil {
		log.Fatalln(err)
	}
	fmt.Println(string(content))
}

标志性错误,通常以 全局变量(指针、接口)方式定义。而提示性错误,则直接返回 临时对象。

// io
var EOF = errors.New("EOF")
// src/errors/errors.go
package errors
func New(text string) error {
    return &errorString{text}   // pointer & interface
}
type errorString struct {
    s string
}
func (e *errorString) Error() string {
    return e.s
}

●是否有错: err != nil
●具体错误: err == ErrVar
●错误匹配: case ErrVal
●类型转换: e, ok := err.(T)

自定义承载更多上下文的错误类型。

type TestError struct {
	x int
}
func (e *TestError) Error() string {
	return fmt.Sprintf("test: %d", e.x)
}
var ErrZero = &TestError{ 0 }  // 指针!(以便判断是否同一对象)
// -----------------------------
func main() {
	var e error = ErrZero
	fmt.Println(e == ErrZero)
	if t, ok := e.(*TestError); ok {
		fmt.Println(t.x)
	}
}

函数返回时,须注意 error 是否真等于 nil,以免错不成错。

func test() error {    
	var err *TestError
	// 结构体指针。
	println(err == nil)   // true
    
	// 接口只有类型和值都为 nil 时才等 于 nil。err 显然有类型信息。正确做法是直接 return nil。
    
	// 转型为接口。
	return err
}
func main() {
	err := test()
	println(err == nil)   // false
}

标准库:

errors.New : 创建包含文本信息的错误对象。
errors.Join : 打包一到多个错误对象,构建树状结构。
fmt.Errorf : 以 % w 包装一到多个错误对象。

包装对象:

errors.Unwrap : 返回被包装错误对象(或列表)。
errors.Is : 递归查找是否有指定错误对象。
errors.As : 递归查找并获取类型匹配的错误对象。

创建错误树,返回完整的错误信息。

func database() error {
	return errors.New("data")
}
func cache() error {
	if err := database(); err != nil {
		return fmt.Errorf("cache miss: %w", err)
	}
	return nil
}
func handle() error {
	return cache()
}
func main() {
	fmt.Println(handle())
}
// cache miss: data

从错误树获取信息。

type TestError struct {}
func (*TestError) Error() string { return "" }
func main() {
    
    // 打包。
	a := errors.New("a")
	b := fmt.Errorf("b, %w", a)
	c := fmt.Errorf("c, %w", b)
    
    fmt.Println(c)                      // c, b, a
    fmt.Println(errors.Unwrap(c) == b)  // true
    // 递归检查。
    fmt.Println(errors.Is(c, a))        // true
	// -------------------------------
	x := &TestError{}
	y := fmt.Errorf("y, %w", x)
	z := fmt.Errorf("z, %w", y)
 
    // 提取(二级指针)类型匹配的错误对象。
	var x2 *TestError
	if errors.As(z, &x2) {
        fmt.Println(x == x2)           // true
	}
}

# panic

与返回 error 相比, panic / recover 的使用方式,很像结构化异常。

func panic(v any)
func recover() any

恐慌( panic )立即中断当前流程,执行延迟调用。 在延迟调用中,恢复( recover ) 捕获并返回恐慌数据。

●沿调用堆栈向外传递,直至被捕获,或进程崩溃。
●连续引发 panic ,仅最后一次可被捕获。

●先捕获 panic ,恢复执行,然后才返回数据。
●恢复之后,可再度引发 panic ,可再次捕获。

●无论恢复与否,延迟调用总会执行。
●延迟调用中引发 panic ,不影响后续延迟调用执行。

func main() {
	defer func() {
        // 拦截 panic,返回数据。
        // 数据未必是 error,也可能是 nil。
        // 无法回到 panic 后续位置继续执行。
        
		if r := recover(); r != nil {
			log.Fatalln(r)
		}
        
	}()
    
    func() {
		panic("p1")   // 终止当前函数,执行 defer。
	}()
	
	println("exit.")  // 即便 recover,也不再执行。
}
// p1
func main() {
	defer func() {
        // 只有最后一次被捕获。
		if err := recover(); err != nil {
			log.Fatalln(err)
		}
        
	}()
    
    func() {
    	defer func(){
    		panic("p2")  // 第二次。
    	}()
		panic("p1")      // 第一次。
	}()
	println("exit.")
}
// p2
func main() {
	defer func() {
		log.Println(recover())      // 捕获第二次:p2
	}()
    
    func() {
    	defer func(){
    		panic("p2")             // 第二次!
    	}()
    	defer func() {
    		log.Println(recover())  // 捕获第一次:p1
    	}()
		panic("p1")                 // 第一次!
	}()
	println("exit.")
}
// p1
// p2

恢复函数 recover 只能在延迟调用( topmost )内正确执行。 直接注册为延迟调用,或被延迟函数间接调用都无法捕获恐慌。

func catch() {
	recover()
}
func main() {
	defer catch()                   // 有效!在延迟函数内直接调用。
	//defer log.Println (recover ()) // 无效!作为参数立即执行。
	//defer recover ()              // 无效!被直接注册为延迟调用。  
	panic("p!")
}

*PanicNilError 替代 nil ,使其有具体含义。

在 1.21 之前,panic (nil) 和没有发生恐慌,recover 都返回 nil,无法区分这两种状态。

func nopanic() {
	defer func() {
		e := recover()
		println(e == nil)   // true
	}()
}
func panicnil() {
	defer func() {
		e := recover()
		println(e == nil)   // false
		_, ok := e.(*runtime.PanicNilError)
		println(ok)         // true
	}()
	panic(nil)
}

适用场景:

●在调用堆栈任意位置中断,跳转到合适位置(恢复,参数为依据)。
runtime.Goexit :不能用于 main goroutine,进程崩溃。
os.Exit :进程立即终止,延迟调用不会执行。
●无法恢复的故障,输出调用堆栈现场。
○文件系统损坏。
○数据库无法连接。
○……

更新于
-->