Go函数学习

76 阅读15分钟

Go函数

普通函数

  • 与Java相比,Go中的函数在声明时可以声明多个返回值(如果有多个,就用括号包裹,并用逗号分隔)

    func 函数名(参数)(返回值){
        函数体
    }
    
    • 函数的参数与返回值都是可选的

函数调用

  • Go中的函数调用栈是大小是动态的,因此不用担心函数的递归调用深度,不会产生因大规模递归导致的内存溢出

  • 函数调用时的右边的小括号可以换行写,此时为了防止编译器在行尾自动插入分号而导致的编译错误,可以在末尾的参数变量后面插入逗号

    for t := 0.0; t < cycles*2*math.Pi; t += res {
        x := math.Sin(t)
        y := math.Sin(t*freq + phase)
        img.SetColorIndex(
            size+int(x*size+0.5), size+int(y*size+0.5),
            blackIndex, // 最后插入的逗号不会导致编译错误,这是Go编译器的一个特性
        )               // 小括弧另起一行缩进,和大括弧的风格保存一致
    }
    

参数

类型省略

  • 函数的参数中如果相邻变量的类型相同,则可以省略前边的参数的类型

    // a b c三个参数的类型都是int
    func functionDemo(a, b , c int, d string) {
    }
    

可变参数

  • 与Java的类似,也是使用...(Java是类型 ...,Go是...类型)表示函数的可变参数,可变参数都要放在参数表中的最后一个(由解析逻辑决定,放在前边会出现解析的歧义)

    func main() {
    	functionDemo(1,2,3,4,5,6)
    }
    
    func functionDemo(a, b int, c ...int) {
    	for _,v := range c{
    		fmt.Println(v)
    	}
    }
    
    • 可变参数按照切片处理,Java中是按照数组来处理
  • 如果可变参数函数的传入参数是切片类型,可以在调用函数时使用...展开切片

    // Sum 测试可变参数函数,参数求和
    func Sum(c ...int) int {
        var result int
        for _, v := range c {
            result += v
        }
        return result
    }
    
    func main() {
        println(function.Sum(1, 2, 3))
        s := []int{2, 3, 4}
        // 切片展开
        println(function.Sum(s...))
    }
    
    • 不要混淆:可变参数函数内部将可变参数按照切片处理,不代表切片类型与可变参数兼容,因此上述代码中的Sum函数不会接收一个切片类型的参数,只接受其展开
  • 可变参数函数的最常用的场景就是格式化输出,比如func Printf(format string, a ...interface{}) (n int, err error) func Sprintf(format string, a ...interface{}) string,函数名的后缀f是一种通用的命名规范,代表该可变参数函数可以接收Printf风格的格式化字符串

注意事项

  • 参数名可以设置为_表示该参数在函数体内并未使用,仅起占位符的作用

  • 参数名可以全部设置为空,表示函数内部不使用参数

    func Test(int, int) int64 {
    	return 12
    }
    

返回值

多返回值

func functionDemo(a, b int, c ...int) (int, int) {
	return 1, 2
}
func main() {
	a,b := functionDemo(1, 2, 3, 4, 5, 6)
	fmt.Println(a,b)
}
  • 多返回值的特性常常用在错误处理中,即指定一个返回值为Error类型,以便于在函数内部出错时将详细错误返回给函数调用者,参考函数错误处理章节

bare return

func functionDemo1(a, b int, c ...int) (res, res1 int) {
	res = 1
	res1 = 2
	return
}
func main() {
	c,d := functionDemo1(1, 2, 3, 4, 5, 6)
	fmt.Println(c,d)
}
  • 在函数声明的返回值处直接声明返回值变量(同样满足函数参数中的简写原则),在函数体中可以直接使用,并且在返回值变量得到赋值后,直接一个return语句即可将返回值返回

  • 注意在bare return模式下,即便是仅有一个返回值,在函数声明时也要用括号包裹

  • bare return模式下,如果函数体内部有明确的无法返回的逻辑,则return可以不写,比如遇到死循环,或者是panic

  • 当函数中有很多return时,使用bare return可以减少代码量,但是降低代码的可读性

函数错误处理

  • 关于Go中的函数错误处理与Go异常的区别,参考Go异常处理

  • 对于可能出现运行失败的函数,除了本来的函数处理结果对应的返回值之外,一般会返回一个额外的返回值,通常是最后一个返回值,用来表示函数处理是否失败

    • 如果导致失败的原因只有一个则返回一个bool类型值,一般被命名为ok

    • 如果导致失败的原因不止一个,比如IO操作,则需要返回error类型

  • Go中常用的函数错误处理策略有以下五种

    • 直接传播错误,即直接把错误返回给函数调用方,或者使用fmt.Errorf构造新的Error对象返回给函数调用方

      • 需要注意的是,构造Error对象时,一般会在原始Error对象上加上本阶段特征的前缀,这样,最终在main函数中展示的错误对象应该包含整个调用流程的链条,例如:

        genesis: crashed: no parachute: G-switch failed: bad relay orientation
        
      • 编写错误信息时,要确保错误信息对问题细节的描述是详尽的。尤其是要注意错误信息表达的一致性,即相同的函数或同包内的同一组函数返回的错误在构成和处理方式上是相似的

      • 一般而言,被调用函数会将调用信息和参数信息作为发生错误时的上下文放在错误信息中返回给调用者,调用者需要添加一些错误信息中不包含的信息

    • 如果错误的发生是偶然性的,或由不可预知的问题导致的。一个明智的选择是重新尝试失败的操作。在重试时,需要限制重试的时间间隔或重试的次数,防止无限制的重试

      // WaitForSerer
      /**
      错误处理函数,在出现偶然网络故障时使用此函数进行网络访问重试,
      而不是直接报函数错误,限制重试的时间间隔或重试的次数,防止无限制的重试
      */
      func WaitForSerer(url string) error {
      	// 重试的时间是1分钟
      	const timeout = 1 * time.Minute
      	// 计算重试截止时间
      	deadline := time.Now().Add(timeout)
      
      	for tries := 0; time.Now().Before(deadline); tries++ {
      		_, err := http.Head(url)
      		if err == nil {
      			return nil // 重试成功
      		}
      		log.Printf("server not responding (%s); retrying..", err)
      		// 使用指数补偿计算重试间隔
      		time.Sleep(time.Second << uint(tries))
      	}
      	// 重试失败
      	return fmt.Errorf("server %s failed to response after %s", url, timeout)
      }
      
    • 如果遇到错误后,无法再运行,需要输出错误信息并结束程序,这种策略更多的在main中使用而不是库函数中,库函数的函数错误应该尽量上报,除非是遇到了预料之外的bug

      // main function
      if err := WaitForSerer(url); err != nil {
        fmt.Errorf("Site %s is down: %v\n", url, err)
        os.Exit(1)
      }
      
      if err := WaitForSerer(url); err != nil {
        log.Fatalf("Site %s is down: %v\n", url, err)
      }
      
      • log.Fataf等价于fmt.Fprintf + os.Exit(1)
    • 遇到错误后,只需要输出错误信息,而不需要终止程序

    • 直接忽略错误,如果函数的错误不会导致程序逻辑受到影响可以直接忽略,但是必须使用注释记录忽略的原因

外部实现函数

  • 类似于Java中的native函数,只在编程语言层面定义,而用汇编的方式执行函数的定义,或者再细致的说是在编译过程中用汇编语言实现函数体,比如append函数
  • 可参考Stack Overflow的问题:Where is the implementation of func append in Go

变量作用域

  • 变量的生命周期不由其作用域决定,可参考下边的匿名函数中的闭包案例

全局变量

  • 全局变量是定义在函数外部的变量,它在程序整个运行周期内都有效。在函数中可以访问到全局变量
  • 若全局变量的变量名的首字母定义为大写,则该全局变量提升为包作用域,即可以在其他包内访问到此变量

局部变量

函数内部定义变量

  • 函数内定义的变量无法在该函数外使用

  • 如果局部变量和全局变量重名,优先访问局部变量

块作用域变量

  • if、for、switch等流程控制代码块内或者是一个自定义的代码块内定义的变量仅在代码块内部生效

包作用域

  • 在同一个包内,函数名不能重复,每一个包都是一个命名空间
  • 同一个包内,不同源文件内的全局变量、函数、类、接口都是可见的
  • 包内定义的首字母大写的全局变量、函数、类、接口,对于其他包可见
    • 类(结构体)内的属性如果是小写的话,也对包外不可见(包内仍是可见)
    • 关于Go中的类与方法参考Go方法学习Go面向对象

函数式编程

  • 所谓函数式编程,在形式上即可以将函数作为一种特殊类型,可以将函数作为一个整体进行赋值、作为参数或返回值

函数类型与函数类型的变量

func main() {
	// 定义一个函数类型
	type calculation func(int, int) int
	// 使用函数赋值
	var f calculation = sum
	// 类型推测同样可用
	f1 := sub
	// 3 1
	fmt.Println(f(1, 2), f1(3, 2))
  f1 = prod // compile error
}

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

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

func prod(a,b int)string{
  // ...
}
  • 函数类型使用type关键字定义,type关键字不仅仅可以定义函数类型,还可以定义其他任意类型,其实就是给复杂类型起一个别名,后边的Go的面向对象编程中会再次用到这个关键字
  • type关键字可以在函数内部使用,如果定义全局的类型的话,就在函数外部定义即可
  • 函数类型的零值是nil,并且函数类型是不可比较的,只能与nil进行比较,因此也不能作为map的key使用

函数作为参数与返回值

  • 以函数类型作为参数类型或者返回值类型的函数,称为高阶函数
type cal func(int, int) int

func main() {
	// 3
	fmt.Println(calc(1, 2, sum))
	// -1
	fmt.Println(calc(1, 2, sub))

  f, err := do("+")
	if(err == nil) {
		fmt.Println(f(1,2)) // 3
	}
}

/*
 * 函数作为参数
 */

// func calc(x, y int, op func(int, int) int) int {
// 	return op(x, y)
// }

func calc(x, y int, op cal) int {
	return op(x, y)
}

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

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


/*
 * 函数作为返回值
 */

func do(op string) (func(int, int) int, error) {
	switch op {
	case "+":
		return sum, nil
	case "-":
		return sub, nil
	default:
		err := errors.New("无法识别的操作符")
		return nil, err
	}
}

匿名函数

  • 前边的将函数作为参数或者返回值,本质上是函数作为特殊类型的值进行传递,但是函数都是在包级语法块中定义的,如果要在任一表达式中表示函数值就要使用匿名函数,而不能使用一般的函数定义形式:匿名函数就是普通的函数定义形式不包括函数名部分而已

    type cal func(int, int) int
    
    func main() {
    
    	add := func(a, b int) int {
    		return a + b
    	}
    	fmt.Println(add(1, 2))
    
    	var sub cal = func(i1, i2 int) int {
    		return i1 - i2
    	}
    	fmt.Println(sub(3, 2))
      
      // 匿名函数的自执行
    	c := func (a, b int) int {
    		return a * b
    	}(2,3)
    
    	fmt.Println(c)
    }
    

匿名函数与闭包

  • 除了定义形式上的特点之外,匿名函数的最大特征在于其可以记录完整的上下文环境,这也就是闭包的由来:Go使用闭包技术实现函数类型的值

    func main() {
    	f := squares()
    	fmt.Println(f()) // 1
    	fmt.Println(f()) // 4
    	fmt.Println(f()) // 9
    	fmt.Println(f()) // 16
    	fmt.Println(f()) // 25
    }
    
    func squares() func() int {
    	var x int
    	return func() int {
    		x++
    		return x * x
    	}
    }
    
    • squares函数返回的函数值可以记录以及更新squares函数中的变量x的状态
    • 变量的生命周期不由其作用域决定main函数已经不是变量x的作用域但是该变量仍隐形的存在于函数值中

匿名函数与递归调用

  • 匿名函数的递归调用也有其特点,即必须先声明函数类型变量后再执行递归调用

    // 如果要递归调用匿名函数,需要首先声明一个函数类型变量
    var visitAll func(items []string)
    // 定义匿名函数
    visitAll = func(items []string) {
      // ...
      visitAll(m[item])
      // ...
    }
    
    visitAll := func(items []string) {
        // ...
        visitAll(m[item]) // compile error: undefined: visitAll
        // ...
    }
    

闭包

  • 对于闭包的理解其实就是闭包 = 函数 + 调用环境,其出现的场景一般就是匿名函数作为返回值,导致该函数的定义环境与调用环境不同,此时分析函数的作用机制,要放到其定义的环境中分析

  • 通过下边的4个例子理解闭包

    func main() {
    	var f = adder()
    	// x = 10
    	fmt.Println(f(10)) //10
    	// x = 10 + 20
    	fmt.Println(f(20)) //30
    	// x = 30 + 30
    	fmt.Println(f(30)) //60
    
    	// 新的函数生命周期
    	f1 := adder()
    	// x = 40
    	fmt.Println(f1(40)) //40
    	// x = 40 + 50
    	fmt.Println(f1(50)) //90
    }
    
    func adder() func(int) int {
    	var x int
    
    	// 匿名函数内部使用了域外的变量
    	return func(i int) int {
    		x += i
    		return x
    	}
    }
    
    func adder2(x int) func(int) int {
    	return func(y int) int {
    		x += y
    		return x
    	}
    }
    func main() {
    	var f = adder2(10)
      
      // x = 10 + 10
    	fmt.Println(f(10)) //20
      // x = 20 + 20
    	fmt.Println(f(20)) //40
      // x = 40 + 30
    	fmt.Println(f(30)) //70
    	
      // 新的函数生命周期
    	f1 := adder2(20)
    	fmt.Println(f1(40)) //60
    	fmt.Println(f1(50)) //110
    }
    
    func makeSuffixFunc(suffix string) func(string) string {
    	return func(name string) string {
    		if !strings.HasSuffix(name, suffix) {
    			return name + suffix
    		}
    		return name
    	}
    }
    
    func main() {
    	jpgFunc := makeSuffixFunc(".jpg")
    	txtFunc := makeSuffixFunc(".txt")
    	fmt.Println(jpgFunc("test")) //test.jpg
    	fmt.Println(txtFunc("test")) //test.txt
    }
    
    func calc(base int) (func(int) int, func(int) int) {
    	add := func(i int) int {
    		base += i
    		return base
    	}
    
    	sub := func(i int) int {
    		base -= i
    		return base
    	}
    	return add, sub
    }
    
    func main() {
    	f1, f2 := calc(10)
      // base = 10 + 1 base = 11 - 2
    	fmt.Println(f1(1), f2(2)) //11 9
      // base = 9 + 3 base = 12 - 4
    	fmt.Println(f1(3), f2(4)) //12 8
      // base = 8 + 5 base = 13 - 6
    	fmt.Println(f1(5), f2(6)) //13 7
    }
    

闭包的使用场景

  • 通过闭包实现一个生成器,实现一定程度的数据隔离,看下边的例子

    package main
    
    import "fmt"
    
    // 定义一个玩家生成器,它的返回类型为 func() (string, int),输入名称,返回新的玩家数据
    func genPlayer(name string) func() (string, int)  {
    	// 定义玩家血量
    	hp := 1000
    	// 返回闭包
    	return func() (string, int) {
    		// 引用了外部的 hp 变量与name变量, 形成了闭包
    		return name, hp
    	}
    }
    
    func main()  {
    	// 创建一个玩家生成器
    	generator := genPlayer("犬小哈")
    
    	// 返回新创建玩家的姓名, 血量
    	name, hp := generator()
    
    	// 打印
    	fmt.Println(name, hp)
    }
    
    • 闭包具有面向对象语言的特性 —— 封装性,变量 hp 无法从外部直接访问和修改
  • sync.Once的任务函数需要传递函数参数,Do函数本身只接受无参函数,此时使用闭包可以从函数定义的位置向函数传递参数

    func (o *Once) Do(f func()) {}
    
    import (
    	"fmt"
    	"sync"
    )
    
    var once sync.Once
    
    func OnceTask(i int) func() {
    	return func() {
    		fmt.Println(i)
    	}
    }
    
    func main() {
    	once.Do(OnceTask(12))
    }
    
  • defer语句指定的函数有error抛出时,无法在单个defer语句中完成error的处理

    func process(conn net.Conn){
      // defer conn.Close()
      defer func(conn net.Conn) {
    		err := conn.Close()
    		if err != nil {
    			fmt.Println("Something error when close conn")
    		}
    	}(conn) // 关闭连接
    }
    
    // 或者是下边的形式
    func process(conn net.Conn) {
    	// defer conn.Close()
    	defer func() {
    		err := conn.Close()
    		if err != nil {
    			fmt.Println("Something error when close conn")
    		}
    	}() // 关闭连接
    }
    

注意事项

  • 循环迭代场景中使用Go函数值闭包时,常常会出现下边的错误:

    • 以这样一种场景为例:要求创建一批临时目录,最后在使用完临时目录后,执行临时目录的删除,其中删除操作使用函数值完成

      var rmdirs []func() // 定义一组删除函数值
      for _, d := range tempDirs() {
          dir := d // 重要!!!
          os.MkdirAll(dir, 0755) // 创建临时目录
        	// 借助闭包绑定临时目录的删除操作
          rmdirs = append(rmdirs, func() {
              os.RemoveAll(dir)
          })
      }
      // 使用临时目录
      // ...
      // 删除临时目录
      for _, rmdir := range rmdirs {
          rmdir() // clean up
      }
      
      • 如果在函数闭包中直接使用迭代变量将会导致错误,因为函数值记录的状态是迭代变量的地址,但是在整个迭代过程中产生的所有函数值记录的都是同一个迭代变量,换句话说所有删除操作中的迭代变量是共享的,因此随着迭代的进行,该变量的值最终就是最后一个临时文件夹,这意外后续执行删除操作时实际上删除的都是同一个临时文件

        • 上述代码中引入的临时变量dir的生命周期局限在每一次迭代中,因此不会被迭代更新
      • 不仅仅是在for-range循环中,在所有类似的迭代中,都可能出类似的错误

        var rmdirs []func()
        dirs := tempDirs()
        for i := 0; i < len(dirs); i++ {
            os.MkdirAll(dirs[i], 0755)
            rmdirs = append(rmdirs, func() {
                os.RemoveAll(dirs[i]) // 错误
            })
        }
        
        var rmdirs []func()
        dirs := tempDirs()
        for i := 0; i < len(dirs); i++ {
            os.MkdirAll(dirs[i], 0755)
            index := i
            rmdirs = append(rmdirs, func() {
                os.RemoveAll(dirs[index]) // 正确
            })
        }
        
    • 在使用go关键字创建协程并发和使用defer语句时也会出现该错误,这不是go或defer本身导致的,而是因为它们都会等待循环结束后,再执行函数值,有延迟执行的特点

defer语句

Go中的异常处理

参考