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 |
|---|---|---|
| 分号处理 | 程序员手动添加 | 编译器自动插入 |
| 代码灵活性 | 较高(分号可省略) | 较低(必须遵守格式) |
| 花括号位置 | 可放在下一行 | 必须放在同一行 |
核心要点
- 统一规范 :Go在语言层面强制代码格式,避免团队争论
- 简洁性 :程序员不需要手动写分号
- 规则明确 :记住哪些标记会触发分号插入
- 常见陷阱 :
- 左花括号必须在上⼀行
- 函数调用的参数换行时,逗号必须在上一行
正确的花括号使用
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的元素前面。
核心特点
- 无需实现接口 :不需要为类型实现
Len()、Less()、Swap()方法。 - 灵活的比较逻辑 :可以定义任意的排序规则(升序、降序、按字段排序等)。
- 原地排序 :直接修改原始切片,不返回新切片。
- 不稳定排序 :从 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)
}
}
注意事项
- 切片不能为 nil :传入的切片不能为
nil,否则会 panic。 - 比较函数的正确性 :
less函数必须返回 严格的偏序关系 (非自反、非对称、传递),否则排序结果可能不正确。 - 性能考虑 :对于大型切片,确保
less函数的执行效率,因为它会被频繁调用。 - 稳定排序 :如果需要保持相等元素的原始顺序,使用
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 {
// 循环体
}
在不同数据结构的使用
- 数组和切片
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)
}
}
-
字符串
重要特性 :
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会将其作为一个字符处理 - 索引是字符的起始字节位置,不一定连续
- 映射
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 元素是安全的,但可能不会遍历到新添加的元素
- 通道
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会阻塞
常见陷阱和注意事项
- 值拷贝问题
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 // 通过指针修改,生效
}
}
- 迭代变量重用
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)
}
- 遍历时修改切片
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 循环或收集要删除的索引
}
- 性能考虑
// 如果只需要索引,使用单变量形式更高效
for i := range largeSlice {
// 只使用索引,不产生值拷贝
}
// 如果需要值且元素类型较大,使用索引访问
for i := range largeSlice {
value := &largeSlice[i] // 获取引用,避免拷贝
}
总结对比
| 数据结构 | 返回的第一个值 | 返回的第二个值 | 特殊行为 |
|---|---|---|---|
| 数组/切片 | 索引(int) | 元素值(拷贝) | - |
| 字符串 | 字节索引(int) | rune 字符 | 按 Unicode 码点迭代 |
| 映射 | 键(key) | 值(value) | 顺序随机 |
| 通道 | 元素值(value) | 无 | 阻塞直到通道关闭 |
range 是 Go 语言中非常优雅和强大的迭代工具,理解其在不同数据结构上的行为特性,能够帮助你编写更高效、更正确的 Go 代码。