Golang_4_函数

四、函数

4.1 定义

使用 func 定义函数,特性如下:

  • 无须前置声明
  • 不支持同名函数重载
  • 不支持命名嵌套 nested – ?
  • 不支持默认参数
  • 不支持定长变参
  • ==支持多返回值==
  • 支持命名返回值
  • 支持匿名函数和闭包

函数属于第一类对象,具备相同签名(参数和返回值一样)的视为同一类型

第一类对象:可在运行期创建,可作函数参数或返回值,可存入变量的实体。最常见的就是匿名函数

func hello()  {
	fmt.Println("hello world!")
}

// exec函数,参数值为函数名称
func exec(f func())  {
	f()
}

func main(){
	f := hello
	exec(f)
}

函数只能判断是否 ==nil,其余操作不支持

建议命名

  • 动词介词加名词 scanWords
  • 避免不必要的缩写 printError优于 printErr
  • 避免使用类型关键字 buildUserStruct看上去很别扭
  • 避免歧义
  • 避免只能通过大小写来区分的同名函数
  • 避免使用内置函数名
  • 避免使用数字,除非是UTF8这种专有名词
  • 避免添加作用域提示前缀
  • 统一使用 camel/pascal case 拼写风格 (驼峰)
  • 使用相同术语保持一致性
  • 使用习惯语,比如 init初始化,is/has 返回布尔值
  • 使用反义词组命名行为相反的函数,get/setmin/max

4.2 参数

  • 形参:函数定义中的参数
    • 类似局部变量
  • 实参:函数调用时所传递的参数
    • 函数外部对象,可以是常量、变量、表达式或函数等

==Golang是值传递==,也称作拷贝传递(pass-by-value),函数调用前,会为形参和返回值分配内存空间,并将实参拷贝到形参内存

func test(x *int){
   fmt.Printf("point: %p, target: %v\n", &x, x)
}

func main() {
   a := 0x100
   p := &a

   fmt.Printf("point: %p, target: %v\n", &p , p)
   // p 的地址和 p 的 地址
   //point: 0xc42008a018, target: 0xc420082008
   test(p)
   // 形参的地址 和 传入的值
   //point: 0xc42008a028, target: 0xc420082008
}

尽管实参和形参都指向同一目标,但传递指针时依然被赋值

使用指针更好,在栈上复制小对象只须很少的指令即可完成,远比运行时进行堆内存分配要快很多,并发编程也提倡尽可能使用不可变对象(只读或复制),可以消除数据同步p64==

参数的传出有两种方式,通常建议使用返回值,也可以使用二级指针

func test(p **int) {
   x := 100
   *p = &x // 地址操作
}

func main() {
   var p *int // 定义指针变量
   test(&p) // 调用函数,使用 &p
   fmt.Println(*p) // 还原指针对应的值,输出100
}

变参

变参实质上就是切片。只能收到一个或多个同类型参数,必须放在列表尾,写法为...

func test(s string, a ...int) {
   fmt.Printf("%T, %v\n", s, s)
   fmt.Printf("%T, %v\n", a, a)
}

func main() {
   test("abc", 1, 2, 3, 4)
}
// string, abc
// []int, [1 2 3 4]

用切片调用变参输入函数时,需要进行展开操作。若是数组,需要转化为切片。

func test(a ...int) {
   fmt.Printf("%T, %v\n", a, a)
}

func main() {
   a := [3]int{1, 2, 3}
   test(a[:]...) // 数组转化为切片类型
   // []int, [1 2 3]
}

变参复制的是切片自身,不包括底层数组,可以修改数据

func test(a ...int) {
   for i := range a{
      a[i] += 100
   }
}

func main() {
   a := []int{1, 2, 3} // 切片类型
   test(a...) // 切片类型传参
   fmt.Println(a)
}

4.3 返回值

有返回值的函数必须得有明确的return终止语句

func test(x int) int{
   if x > 0 {
      return 1
   } else if x < 0 {
      return -1
   }
}// 错误:Missing return at end of function

除非有panic或者无break的死循环,则无须return终止语句

func test(x int) int{
   for{
      x++
   }
}
// 死循环 without break,不报错
// 若循环内加上 break,则会报错

由于可以返回多值,error模式得以返回多状态

import "errors"

func div(x, y int) (int, error){// 多返回值需要使用括号
   if y == 0 {
      return 0, errors.New("Division by zero")
   }
   return x / y, nil
}

func log(x int, err error) {
	fmt.Println(x, err)
}

// 多返回值直接作 return 的结果
func test() (int, error) {
	return div(5, 0)
}

func main() {
    // 多返回值作实参
	log(test())
}

命名返回值

func paging(sql string, index int) (count int, pages int, err error) {
   return // 隐式返回,相当于 return count, pages, err
}

会有两个问题

  • 遮蔽

    命名返回值和参数一样,相当于函数的局部变量,最后由return隐式返回,这种 局部变量 会被不同级的同名变量遮蔽,需要显式得返回

    func add(x, y int) (z int) {
       {
          z := x + y
          return z // 必须显式返回,否则报错
       }
       return 
    }
    
  • 所有返回值都要命名(如有命名)

    所有返回值都要命名,否则编译器会搞不清情况,编译器会跳过未命名的返回值,无法准确匹配

    如果返回值类型可以明确表明含义,就尽量不要命名

    func test(str string) (int, s string, e error) {
       return 0, str, errors.New("123")// 编译器忽略了 int 返回值,所以第一个是 string ,不能为 int
       return str, errors.New("123") // 但是写两个返回值,又会报错 “Not enough argument to return”
    }
    

4.4 匿名函数

除了没有名字外,匿名函数和普通函数完全相同。

最大的区别在于:可以在函数内部定义匿名函数,形成嵌套效果。

匿名函数可以直接调用、保存到变量、作为参数或返回值

  • 直接执行

    func main(){
       func(s string){
          println(s)
       }("hello world")
    }
    
  • 赋值给变量

    func main(){
       add := func(x, y int) int{
          return x + y
       }
       fmt.Println(add(1, 2))
    }
    
  • 作为参数

    // 函数类型参数
    func test(f func())  {
       f()
    }
      
    func main(){
       test(func() {
          fmt.Println("hello world")
       })
    }
    
  • 作为返回值

    // 函数类型参数
    func test() func(int, int) int {
       return func(x, y int) int {
          return x + y
       }
    }
      
    func main(){
       add := test()
       fmt.Println(add(1, 2))
    }
    

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

未使用的匿名函数会被编译器当作错误

匿名函数也是一种常见的重构手段 ==p71==

闭包

闭包 = 函数 + 引用环境

// 定义一个函数,它的返回值是一个函数
func test(x string) func() {
   name := "小王子"
   return func() {
      fmt.Println(x, name)
   }
}

// 闭包 = 函数 + 外层变量的引用
func main() {
   f := test("hello") // f 就是一个闭包
   f() // 相当于执行了test函数内部的匿名函数
}

test 返回的是匿名函数,这个匿名函数包含它自己上下文环境变量 x 和 name 的引用。

当该函数在main中执行时,它依然可以正确读取x 和 name 的值,这种现象称为闭包

匿名函数被返回后,还能读取环境变量的值

例一:

// 1. 声明一个 makeSuffixFunc 的函数,参数为 suffix(string)
// 2. 返回值是一个函数类型:这个函数类型接收一个string参数,返回一个string参数
func makeSuffixFunc(suffix string) func(string) string {
    // 3. 返回一个匿名函数,和上层函数的返回值相同
    return func(name string) string {
        // HasSuffix:返回 name 是否以 suffix 结尾
        // 4. name 是匿名函数的变量,suffix 不是匿名函数的变量,而是外层函数的变量
        // 5. 定义的不是一个普通的匿名函数,还包含外层变量的引用,返回的结果也是一个闭包
        if !strings.HasSuffix(name, suffix){
            return name + suffix
        }
        return name
    }
}

// 闭包 = 函数 + 外层变量的引用
func main() {
    // 传入 suffix 变量为 .txt
    r := makeSuffixFunc(".txt")
    // 返回的匿名函数
    ret := r("哈哈哈")
    fmt.Println(ret)
}

例二:

// 1. calc 为计算函数,接收一个参数 base
// 2. 返回值有两个,都是函数类型
// 3. 内部定义了 add 和 sub 两个函数,最后return
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
	// 判断返回是不是闭包
	// 就是判断返回函数的内部是否包含外层变量的引用
	// 这里两个函数的 base 是外层变量引用,所以是闭包
}

func main() {
	// 闭包 = 函数 + 外层变量的引用
	// 100 就是外层的 base,100是内层的 i
	x, y := calc(100)
	ret1 := x(200) // base = 100 + 200
	fmt.Println(ret1)
	ret2 := y(200) // base = 300 - 200
	fmt.Println(ret2)
}

4.5 延迟调用

当前函数执行结束前才被执行,常用于资源释放、锁定解除以及错误处理等。

func main() {
   x, y := 1, 2
   defer func(a int) {
      fmt.Println("defer x , y = ", a, y) // y 是闭包
   }(x) // 注册时复制参数,为1

   x += 100
   y += 200
   fmt.Println(x, y)
}
// 101 202
// defer x , y =  1 202 // 延迟调用,最后执行

多个延迟注册按照FILO次序执行(先进后出)

性能要求高且压力大的算法,应该避免使用延迟调用

4.6 错误处理

官方推荐的做法是返回error状态

func Scanlan(a ...interface{})(n int, err error)
var errDivByZero = errors.New("division by zero")

func div(x, y int) (int, error) {
   if y == 0 {
      return 0, errDivByZero
   }
   return x / y, nil
}

func main() {
   z, err := div(5, 0)
   if err == errDivByZero {
      log.Fatalln(err) // 输出 2019/09/24 20:14:49 division by zero
   }

   fmt.Println(z)
}

panic,recover

和error相比,panic/recover在使用上更接近 try/catch 结构化异常

https://www.bilibili.com/video/av54285508

4.6 内置函数

内置函数 介绍
close 用来关闭channel
len 求长度,string、array、slice、map、channel
new 分配内存,主要用来分配值类型,int、struct,返回的是指针
make 分配内存,主要用来分配引用类型 chan、map、slice
append 追加元素到数组、slice中
panic和recover 错误处理

分享: