返回介绍

上卷 程序设计

中卷 标准库

下卷 运行时

源码剖析

附录

4. 函数

发布于 2024-10-12 19:15:47 字数 7225 浏览 0 评论 0 收藏 0

函数(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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文