语法糖

语法糖(Syntactic sugar)的概念是由英国计算机科学家彼得·兰丁提出的,用于表示编程语言中的某种类型的语法,这些语法不会影响功能,但使用起来却很方便。 语法糖,也称糖语法,这些语法不仅不会影响功能,编译后的结果跟不使用语法糖也一样。

简短变量声明 :=

  1. 多变量赋值可能会重新声明(这并没有引入新的变量,只是把变量的值改变了。
  2. 作用域问题

可变参数(…parameters)

开发时候遇到如下场景,某个方法原来的入参是切片,并在内部遍历了切片,现在需要添加一个方法,入参是一个元素,处理方式除了遍历完全相同,洁癖犯了之后不想额外再添加一个相似度极高的方法,想到Go的某些内置函数似乎有类似的场景,参数不固定的情况下,例如append,既可以append切片也可以append一个元素,点进去看了下源码

1func append(slice []Type, elems ...Type) []Type

append 就使用了可变参数

1result := []int{1, 3}
2data := []int{5, 7}
3result = append(result, data...) // 和 result = append(result, data[0], data[1]) 等价
4// 最终 result == []int{1, 3, 5, 7}
  1. 可变参数使用name ...Type的形式声明在函数的参数列表中,而且需要是参数列表的最后一个参数(只能包含一个可变参数);
  2. 可变参数在函数内部是作为切片来解析的;
  3. 可变参数可以不填,不填时函数内部当成 nil 切片处理;
  4. 可变参数可以填入切片;(函数内部使用的切片与传入的切片共享相同的存储空间。说得再直白一点就是,如果函数内部修改了切片,可能会影响外部调用的函数)
  5. 可变参数必须是相同类型的(如果需要是不同类型的可以定义为 interface{}类型);

func init()

  1. 每个包可以有多个 init() 函数

    • 在一个包中,可以定义多个 init() 函数,它们可以分布在不同的文件中。
    • 每个 init() 函数都会被自动调用,且调用顺序是按照文件名的字母顺序进行的。
  2. 每个文件可以有多个 init() 函数

    • 在一个文件中,可以定义多个 init() 函数。
    • 这些 init() 函数会按照它们在文件中出现的顺序依次执行。
  3. init() 函数只会执行一次

    • 无论包被导入多少次,init() 函数只会执行一次。
    • 即使包被多个文件导入,init() 函数也只会执行一次。
  4. init() 函数的执行时机

    • init() 函数会在包的变量初始化之后、main() 函数执行之前自动调用。
    • 如果一个包被多个包导入,它的 init() 函数会在所有导入它的包的 init() 函数执行之前调用。
  5. 从其他Package中读取 init() 函数

    • main()函数要单独读取package内的init()而不读取额外的函数,这时候需要透过**_**来读取。

      1import (
      2    _ "xxxx"
      3)
      
    • 如果不加上,当编译的时候会报错,因为main()没有用到任何非init()之外的东西。

    • 如果有多个packageinit()需要同时引入 ,这边也是会依照import的顺序来读取。

slices

Delete

在 Go 1.22 及以上版本中,可以使用 slices 包中的 Delete 函数来简化删除切片元素的操作。

1// 删除下标为 i 的元素
2infos = append(infos[:i}, infos[i+1]...) // 传统写法
3infos = slices.Delete(infos, i, i + 1)   // 更推荐的写法
  1. 函数签名:func Delete[S ~[]E, E any](s S, i, j int) S
  2. 参数:
    • s:要操作的切片
    • i:删除的起始索引
    • j:删除的结束索引(不含)
  3. 返回值:删除元素后的新切片

for range

for loop can be modernized using range over int

for 循环可以通过 Go 1.22 引入的 range over int 特性来现代化或简化。在 Go 1.22 之前,range 只能用于遍历切片、数组、映射等集合类型。如果要遍历一个整数范围(例如从 0n-1),必须使用传统的 for 循环,像这样:

1for i := 0; i < n; i++ {
2    // 循环体
3}

Go 1.22 开始,Go 语言支持 range over int,即可以直接用 range 遍历一个整数范围。例如:

1for i := range n {
2    // 循环体
3}

循环变量重用问题(闭包陷阱)

现象:在循环中启动协程或闭包时,所有闭包可能共享同一个循环变量的最终值。

原因for-range 的循环变量(如 iv)在每次迭代中会被 重用,而非重新声明。

示例

1values := []int{1, 2, 3}
2for _, v := range values {
3    go func() { fmt.Println(v) }()  // 可能输出 3,3,3
4}

解决

  • 传递参数:将循环变量作为参数传入闭包。
  • 局部变量拷贝:在循环内部声明局部变量保存当前值。
1for _, v := range values {
2    v := v  // 创建局部变量
3    go func() { fmt.Println(v) }()
4}
5// 或
6for i := range values {
7    go func(v int) { fmt.Println(v) }(values[i])
8}

底层机制:循环变量的内存地址在迭代中被复用,闭包捕获的是地址而非值 。

Go 1.22 优化了循环变量作用域,每次迭代重新声明变量,减少闭包陷阱。

遍历时修改原数据无效

现象:直接通过 for-rangev 修改元素无法影响原数组/切片。

原因v 是元素的 值拷贝,而非引用。

示例

1users := []User{{Name: "A"}, {Name: "B"}}
2for _, u := range users {
3    u.Name = "C"  // 修改无效,users 未变
4}

解决:通过 索引 直接修改原数据:

1for i := range users {
2    users[i].Name = "C"  // 正确修改
3}

但是如果 v 也是

参考:

  1. Golang常用语法糖
  2. 了解 golang 的可变参数(… parameters),这一篇就够了
  3. 可变参数