上卷 程序设计
中卷 标准库
- bufio 1.18
- bytes 1.18
- io 1.18
- container 1.18
- encoding 1.18
- crypto 1.18
- hash 1.18
- index 1.18
- sort 1.18
- context 1.18
- database 1.18
- connection
- query
- queryrow
- exec
- prepare
- transaction
- scan & null
- context
- tcp
- udp
- http
- server
- handler
- client
- h2、tls
- url
- rpc
- exec
- signal
- embed 1.18
- plugin 1.18
- reflect 1.18
- runtime 1.18
- KeepAlived
- ReadMemStats
- SetFinalizer
- Stack
- sync 1.18
- atomic
- mutex
- rwmutex
- waitgroup
- cond
- once
- map
- pool
- copycheck
- nocopy
- unsafe 1.18
- fmt 1.18
- log 1.18
- math 1.18
- time 1.18
- timer
下卷 运行时
源码剖析
附录
4. 函数
函数(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 toString(f FormatFunc, s string, a ...any) string { return f(s, a...) } func main() { println(toString(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)
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论