go换行和分号

为什么强制花括号位置?

Go编译器会在特定情况下自动插入分号,这导致代码格式必须严格遵循规则:

// ❌ 错误写法
func main() 
{
    fmt.Println("hello")
}

// 实际被解析为:
func main();
{
    fmt.Println("hello");
}
// main后面多了分号,编译失败

自动插入分号的规则

当一行以以下某些标记结尾时,编译器会自动在末尾插入分号:

类别 具体内容
标识符 变量名、函数名等
字面量 整数、浮点数、字符串、字符等
特定关键字 break、continue、fallthrough、return
运算符/分隔符 ++、--、)、]、}

关键示例对比

不会插入分号的情况(以逗号结尾)

add(i,     // 逗号结尾,不会插入分号
    j)     // 可以正常换行

会插入分号的情况(以标识符结尾)

add(i      // i是标识符,会自动加; 导致编译错误
    ,j)

C/C++ vs Go 对比

特性 C/C++ Go
分号处理 程序员手动添加 编译器自动插入
代码灵活性 较高(分号可省略) 较低(必须遵守格式)
花括号位置 可放在下一行 必须放在同一行

核心要点

  1. 统一规范 :Go在语言层面强制代码格式,避免团队争论
  2. 简洁性 :程序员不需要手动写分号
  3. 规则明确 :记住哪些标记会触发分号插入
  4. 常见陷阱 :
  • 左花括号必须在上⼀行
  • 函数调用的参数换行时,逗号必须在上一行

正确的花括号使用

func main() {    // 花括号在同一行
    {
        x := 123
        fmt.Printf("type is %T\n", x)
    }
  
    {
        x := "123"
        fmt.Printf("type is %T\n", x)
    }
}

总结 :Go通过自动分号插入规则,强制统一的代码风格(如花括号位置),虽然从C/C++转过来会觉得别扭,但习惯后能带来更一致的代码规范和更简洁的语法。

函数

sort函数

sort.Slice

sort.Slice 是 Go 语言标准库 sort 包中一个非常实用的 切片排序函数 。它允许你通过自定义的比较规则来对任意类型的切片进行排序,而无需让切片元素类型实现 sort.Interface 接口。

func Slice(slice any, less func(i, j int) bool)
  • slice :要排序的切片,类型为 any(空接口),可以传入任意类型的切片。
  • less :比较函数,用于判断索引 i 的元素是否应该排在索引 j 的元素前面。
核心特点
  1. 无需实现接口 :不需要为类型实现 Len()、Less()、Swap() 方法。
  2. 灵活的比较逻辑 :可以定义任意的排序规则(升序、降序、按字段排序等)。
  3. 原地排序 :直接修改原始切片,不返回新切片。
  4. 不稳定排序 :从 Go 1.8 开始,sort.Slice 使用的是 不稳定排序算法 。如果需要稳定排序,可以使用 sort.SliceStable。
使用示例

对整数切片排序

package main

import (
    "fmt"
    "sort"
)

func main() {
    numbers := []int{5, 2, 8, 1, 9}
  
    // 升序排序
    sort.Slice(numbers, func(i, j int) bool {
        return numbers[i] < numbers[j]
    })
    fmt.Println(numbers) // 输出: [1 2 5 8 9]
  
    // 降序排序
    sort.Slice(numbers, func(i, j int) bool {
        return numbers[i] > numbers[j]
    })
    fmt.Println(numbers) // 输出: [9 8 5 2 1]
}

对结构体切片排序

type Person struct {
    Name string
    Age  int
}

func main() {
    people := []Person{
        {"Alice", 30},
        {"Bob", 25},
        {"Charlie", 35},
    }
  
    // 按年龄升序排序
    sort.Slice(people, func(i, j int) bool {
        return people[i].Age < people[j].Age
    })
    fmt.Println(people) 
    // 输出: [{Bob 25} {Alice 30} {Charlie 35}]
  
    // 按名字字典序排序
    sort.Slice(people, func(i, j int) bool {
        return people[i].Name < people[j].Name
    })
    fmt.Println(people)
    // 输出: [{Alice 30} {Bob 25} {Charlie 35}]
}

多字段排序

type Product struct {
    Name  string
    Price float64
    Stock int
}

func main() {
    products := []Product{
        {"Laptop", 999.99, 10},
        {"Mouse", 29.99, 50},
        {"Keyboard", 79.99, 30},
        {"Monitor", 299.99, 15},
    }
  
    // 先按价格升序,价格相同按库存降序
    sort.Slice(products, func(i, j int) bool {
        if products[i].Price != products[j].Price {
            return products[i].Price < products[j].Price
        }
        return products[i].Stock > products[j].Stock
    })
  
    for _, p := range products {
        fmt.Printf("%s: $%.2f (Stock: %d)\n", p.Name, p.Price, p.Stock)
    }
}
注意事项
  1. 切片不能为 nil :传入的切片不能为 nil,否则会 panic。
  2. 比较函数的正确性 :less 函数必须返回 严格的偏序关系 (非自反、非对称、传递),否则排序结果可能不正确。
  3. 性能考虑 :对于大型切片,确保 less 函数的执行效率,因为它会被频繁调用。
  4. 稳定排序 :如果需要保持相等元素的原始顺序,使用 sort.SliceStable 代替。
与 sort.SliceStable 的区别
特性 sort.Slice sort.SliceStable
稳定性 不稳定 稳定
性能 通常更快 稍慢(但差异不大)
使用场景 不关心相等元素顺序 需要保持原始顺序

sort.Slice 是 Go 中处理切片排序最常用、最灵活的方式,几乎可以应对所有常见的排序需求。

range函数

在 Go 语言中, range 并不是一个函数,而是一个关键字 ,用于在 for 循环中迭代各种数据结构(数组、切片、字符串、映射、通道)的元素。

基本语法

for index, value := range collection {
    // 循环体
}
// 只获取索引
for index := range collection {
    // 循环体
}
// 只获取值(使用下划线忽略索引)
for _, value := range collection {
    // 循环体
}

在不同数据结构的使用

  1. 数组和切片
package main

import "fmt"

func main() {
    // 数组
    arr := [3]string{"Go", "Python", "Java"}
  
    // 遍历数组(索引和值)
    for i, v := range arr {
        fmt.Printf("索引: %d, 值: %s\n", i, v)
    }
  
    // 切片
    slice := []int{10, 20, 30, 40}
  
    // 只遍历值
    for _, value := range slice {
        fmt.Println(value)
    }
  
    // 只遍历索引
    for i := range slice {
        fmt.Printf("索引: %d\n", i)
    }
}
  1. 字符串

    重要特性 :range 遍历字符串时,是按 Unicode 码点(rune) 迭代的,而不是按字节。

func main() {
    str := "Hello 世界"
  
    // 方式1: range 遍历(按字符)
    for i, ch := range str {
        fmt.Printf("索引: %d, 字符: %c, Unicode: %U\n", i, ch, ch)
    }
    // 输出:
    // 索引: 0, 字符: H, Unicode: U+0048
    // 索引: 1, 字符: e, Unicode: U+0065
    // 索引: 2, 字符: l, Unicode: U+006C
    // 索引: 3, 字符: l, Unicode: U+006C
    // 索引: 4, 字符: o, Unicode: U+006F
    // 索引: 5, 字符: 世, Unicode: U+4E16
    // 索引: 8, 字符: 界, Unicode: U+754C
  
    // 方式2: 按字节遍历(对比)
    for i := 0; i < len(str); i++ {
        fmt.Printf("字节索引: %d, 字节: %x\n", i, str[i])
    }
}

关键点 :

  • 中文等非 ASCII 字符占用多个字节,但 range 会将其作为一个字符处理
  • 索引是字符的起始字节位置,不一定连续
  1. 映射
func main() {
    scores := map[string]int{
        "Alice":   95,
        "Bob":     87,
        "Charlie": 92,
    }
  
    // 遍历 map(顺序是随机的)
    for name, score := range scores {
        fmt.Printf("%s 的成绩是 %d 分\n", name, score)
    }
  
    // 只遍历键
    for name := range scores {
        fmt.Println(name)
    }
  
    // 遍历时删除元素(安全,但可能影响遍历结果)
    for name, score := range scores {
        if score < 90 {
            delete(scores, name)
        }
    }
}

重要特性 :

  • Map 的迭代顺序是 不确定的 (Go 故意设计的随机性)
  • 在迭代过程中添加或删除 map 元素是安全的,但可能不会遍历到新添加的元素
  1. 通道
func main() {
    ch := make(chan int)
  
    // 在 goroutine 中发送数据
    go func() {
        for i := 1; i <= 5; i++ {
            ch <- i
        }
        close(ch) // 必须关闭通道,否则 range 会死锁
    }()
  
    // 从通道接收数据直到关闭
    for value := range ch {
        fmt.Println("收到:", value)
    }
}

关键点 :

  • 只有当通道被关闭时,range 循环才会结束
  • 如果通道没有关闭且没有数据,range 会阻塞

常见陷阱和注意事项

  1. 值拷贝问题
type Person struct {
    Name string
    Age  int
}

func main() {
    people := []Person{
        {"Alice", 30},
        {"Bob", 25},
    }
  
    // 错误示例:尝试修改值,但不会生效
    for _, p := range people {
        p.Age = 40 // 修改的是副本,不影响原切片
    }
    fmt.Println(people) // 输出: [{Alice 30} {Bob 25}]
  
    // 正确做法1:使用索引直接修改
    for i := range people {
        people[i].Age = 40
    }
    fmt.Println(people) // 输出: [{Alice 40} {Bob 40}]
  
    // 正确做法2:使用指针切片
    peoplePtr := []*Person{
        {"Alice", 30},
        {"Bob", 25},
    }
    for _, p := range peoplePtr {
        p.Age = 40 // 通过指针修改,生效
    }
}
  1. 迭代变量重用
func main() {
    numbers := []int{1, 2, 3}
  
    // 常见错误:在 goroutine 中使用 range 变量
    for _, num := range numbers {
        go func() {
            fmt.Println(num) // 可能都输出 3
        }()
    }
    time.Sleep(time.Second)
  
    // 正确做法:复制变量或使用参数传递
    for _, num := range numbers {
        n := num // 复制一份
        go func() {
            fmt.Println(n)
        }()
    }
    time.Sleep(time.Second)
}
  1. 遍历时修改切片
func main() {
    nums := []int{1, 2, 3, 4, 5}
  
    // 在遍历时删除元素
    for i := 0; i < len(nums); i++ {
        if nums[i]%2 == 0 {
            nums = append(nums[:i], nums[i+1:]...)
            i-- // 调整索引
        }
    }
    fmt.Println(nums) // 输出: [1 3 5]
  
    // 使用 range 时不能直接删除,因为索引会混乱
    // 建议使用传统的 for 循环或收集要删除的索引
}
  1. 性能考虑
// 如果只需要索引,使用单变量形式更高效
for i := range largeSlice {
    // 只使用索引,不产生值拷贝
}

// 如果需要值且元素类型较大,使用索引访问
for i := range largeSlice {
    value := &largeSlice[i] // 获取引用,避免拷贝
}

总结对比

数据结构 返回的第一个值 返回的第二个值 特殊行为
数组/切片 索引(int) 元素值(拷贝) -
字符串 字节索引(int) rune 字符 按 Unicode 码点迭代
映射 键(key) 值(value) 顺序随机
通道 元素值(value) 无 阻塞直到通道关闭

range 是 Go 语言中非常优雅和强大的迭代工具,理解其在不同数据结构上的行为特性,能够帮助你编写更高效、更正确的 Go 代码。