go

Go 系列 语法糖

Posted by lichao modified on August 14, 2022

规则

规则一: 多变量赋值可能会重新声明:

我们知道使用 := 一次可以声明多个变量,像下面这样:

1
field1, offset := nextField(str, 0)

上面代码定义了两个变量,并用函数返回值进行赋值。

如果这两个变量中的一个再次出现在 := 左侧就会重新声明。像下面这样:

1
2
field1, offset := nextField(str, 0) 
field2, offset := nextField(str, offset)

offset被重新声明。

重新声明并没有什么问题,它并没有引入新的变量,只是把变量的值改变了,但要明白,这是Go提供的一个语法糖。

  • 当 := 左侧存在新变量时(如field2),那么已声明的变量(如offset)则会被重新声明,不会有其他额外副作用。
  • 当 := 左侧没有新变量是不允许的,编译会提示 no new variable on left side of := 。

我们所说的重新声明不会引入问题要满足一个前提,变量声明要在同一个作用域中出现。如果出现在不同的作用域, 那很可能就创建了新的同名变量,同一函数不同作用域的同名变量往往不是预期做法,很容易引入缺陷。关于作用域 的这个问题,我们在本节后面介绍。

规则二:不能用于函数外部:

简短变量场景只能用于函数中,使用 := 来声明和初始化全局变量是行不通的。

比如,像下面这样:

1
2
3
4
package sugar

import fmt
rule := "Short variable declarations" // syntax error: non-declaration statement outside function body

这里的编译错误提示 syntax error: non-declaration statement outside function body ,表示非声明语句不能出现在函数外部。可以理解成 := 实际上会拆分成两个语句,即声明和赋值。赋值语句不能出现在函数外部的。

变量作用域问题

几乎所有的工程师都了解变量作用域,但是由于 := 使用过于频繁的话,还是有可能掉进陷阱里。 下面代码源自真实项目,但为了描述方便,也为了避免信息安全风险,简化如下:

1
2
3
4
5
6
7
8
9
10
func Redeclare() { 
  field, err:= nextField() 
  // 1号err 
  if field == 1{ 
    field, err:= nextField() 
    // 2号err 
    newField, err := nextField() 
    // 3号err 
  } 
}

注意上面声明的三个err变量。2号err与1号err不属于同一个作用域, := 声明了新的变量,所以2号err与1号 err属于两个变量。2号err与3号err属于同一个作用域, := 重新声明了err但没创建新的变量,所以2号err与3 号err是同一个变量。 如果误把2号err与1号err混淆,就很容易产生意想不到的错误。

可变参

可变参函数是指函数的某个参数可有可无,即这个参数个数可以是0个或多个。声明可变参数函数的方式是在参数类型前加上 … 前缀。

比如 fmt 包中的 Println :

1
func Println(a ...interface{})

函数特征

我们先写一个可变参函数:

1
2
3
4
5
6
7
8
9
func Greeting(prefix string, who ...string) { 
  if who == nil { 
    fmt.Printf("Nobody to say hi.") 
    return 
  } 
  for _, people := range who{ 
    fmt.Printf("%s %s\n", prefix, people) 
  } 
}

Greeting 函数负责给指定的人打招呼,其参数 who 为可变参数。

这个函数几乎把可变参函数的特征全部表现出来了:

  • 可变参数必须在函数参数列表的尾部,即最后一个(如放前面会引起编译时歧义);
  • 可变参数在函数内部是作为切片来解析的;
  • 可变参数可以不填,不填时函数内部当成 nil 切片处理;
  • 可变参数必须是相同类型的(如果需要是不同类型的可以定义为interface{}类型);

使用举例

我们使用 testing 包中的Example函数来说明上面 Greeting 函数(函数位于sugar包中)用法。

不传值:

调用可变参函数时,可变参部分是可以不传值的,例如:

1
2
3
4
5
func ExampleGreetingWithoutParameter() {
  sugar.Greeting("nobody")
  // OutPut:
  // Nobody to say hi.
}

这里没有传递第二个参数。可变参数不传递的话,默认为nil。

传递多个参数:

调用可变参函数时,可变参数部分可以传递多个值,例如:

1
2
3
4
5
6
7
func ExampleGreetingWithParameter() { 
  sugar.Greeting("hello:", "Joe", "Anna", "Eileen") 
  // OutPut: 
  // hello: Joe 
  // hello: Anna 
  // hello: Eileen 
}

可变参数可以有多个。多个参数将会生成一个切片传入,函数内部按照切片来处理。

传递切片:

调用可变参函数时,可变参数部分可以直接传递一个切片。参数部分需要使用 slice... 来表示切片。例如:

1
2
3
4
5
6
7
8
func ExampleGreetingWithSlice() { 
  guest := []string{"Joe", "Anna", "Eileen"} 
  sugar.Greeting("hello:", guest...) 
  // OutPut: 
  // hello: Joe 
  // hello: Anna 
  // hello: Eileen 
}

此时需要注意的一点是,切片传入时不会生成新的切片,也就是说函数内部使用的切片与传入的切片共享相同的存储 空间。说得再直白一点就是,如果函数内部修改了切片,可能会影响外部调用的函数。

总结

  • 可变参数必须要位于函数列表尾部;
  • 可变参数是被当作切片来处理的;
  • 函数调用时,可变参数可以不填;
  • 函数调用时,可变参数可以填入切片;