学习新事物最好的方法之一就是定期记录所学到的知识。过去一年,我坚持记录 Go 编程语言的相关知识。以下是我最喜欢的一些关于 Go 语言的鲜为人知的技巧。
遍历整数
从 Go 1.22 开始,你可以对整数进行遍历:
1
2
3
|
for i := range 10 {
fmt.Println(i + 1) // 1, 2, 3 ... 10
}
|
约束泛型函数签名
你可以使用 ~ 运算符来约束泛型类型签名。例如,对于类型化常量,您可以这样做:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
package main
import (
"fmt"
)
type someConstantType string
const someConstant someConstantType = "foo" // Underlying type is a string
func main() {
msg := buildMessage(someConstant)
fmt.Println(msg)
}
func buildMessage[T ~string](value T) string { // This accepts any value whose underlying type is a string
return fmt.Sprintf("The underlying string value is: '%s'", value)
}
|
基于索引的字符串插值
1
2
3
4
5
6
7
8
9
|
package main
import (
"fmt"
)
func main() {
fmt.Printf("%[1]s %[1]s %[2]s %[2]s %[3]s", "one", "two", "three") // yields "one one two two three"
}
|
time.After 函数
time.After 函数会创建一个通道,该通道会在 x 秒后发送一条消息。当与 SELECT 语句结合使用时,它可以轻松地为另一个例程设置截止时间。
不建议在 for 中使用!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan string, 1)
go func() {
time.Sleep(2 * time.Second)
ch <- "result"
}()
select {
case res := <-ch:
fmt.Println("Received:", res)
case <-time.After(1 * time.Second):
fmt.Println("Timeout: did not receive a result in time")
}
}
|
embed 包
“embed”包允许你将非 Go 文件直接嵌入到 Go 二进制文件中。运行时无需从磁盘读取这些文件。
可以嵌入 HTML、JS 甚至图片。将资源直接编译到二进制文件中可以显著简化部署过程。
使用 len() 函数处理字符串以及 UTF-8 编码的注意事项
Go 语言内置的 len() 函数并不返回字符串中的字符数,而是返回字节数,因为我们不能假设字符串字面量每个字符包含一个字节(因此,需要使用 runes)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
package main
import (
"fmt"
)
func main() {
s := "Hello 世界"
fmt.Println(len(s)) // Prints 11!
for i := 0; i < len(s); i++ {
fmt.Printf("index %d: value %c\n", i, s[i]) // Iterates over bytes. This will not work as expected....
/*
index 0: value H
index 1: value e
index 2: value l
index 3: value l
index 4: value o
index 5: value
index 6: value ä
index 7: value ¸
index 8: value <96>
index 9: value ç
index 10: value <95>
index 11: value <8c>
*/
}
for i, r := range s { // The range keyword iterates through runes.
fmt.Printf("byte %d: %s\n", i, string(r))
/*
byte 0: H
byte 1: e
byte 2: l
byte 3: l
byte 4: o
byte 5:
byte 6: 世
byte 9: 界
*/
}
}
|
Rune 对应于 Go 语言中的代码点,每个代码点长度介于 1 到 4 个字节之间。更复杂的是,虽然字符串字面量采用 UTF-8 编码,但它们本质上只是任意的字节集合,这意味着理论上字符串中可能包含无效数据。在这种情况下,Go 会将无效的 UTF-8 数据替换为"替换字符"。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
package main
import (
"fmt"
)
func main() {
invalidBytes := []byte{0x48, 0x65, 0x6C, 0x6C, 0x6F, 0xFF} // "Hello" + invalid byte
s := string(invalidBytes)
for _, r := range s {
fmt.Printf("%c ", r) // Prints: H e l l o �
}
}
|
Nil Interfaces
你认为这会打印什么?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
package main
import "fmt"
type Animal interface {
Speak()
}
type Dog struct{}
func (d *Dog) Speak() {}
func main() {
var d *Dog = nil
var a Animal = d
fmt.Println(a == nil)
}
|
答案: false
即使值为 nil,变量的类型也是非 nil 接口 。
Go 会将该值“封装”到一个接口中,而该接口的值不为 nil。如果从函数返回接口,即使该值是 nil, nil 检查将不再按预期工作。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
package main
import "fmt"
type Car interface {
Honk()
}
type Honda struct{}
func (h *Honda) Honk() {
fmt.Println("Beep!")
}
func giveCar() Car {
var h *Honda // h is nil
return h // nil *Honda wrapped in Car interface
}
func main() {
c := giveCar()
if c == nil {
fmt.Println("This will never print!")
}
}
|
对空值调用方法
实际上可以对一个 nil 结构体调用方法。这在 Go 语言中是有效的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
package main
import "fmt"
type Foo struct {
Val string
}
func (f *Foo) Hello() {
fmt.Println("hi from nil pointer receiver")
}
func main() {
var f *Foo = nil
f.Hello() // This is fine!
fmt.Println(f.Val) // This is not!
}
|
当然,试图访问其属性还是会 panic。
遍历 map 时扩容
在循环中更新 map 时,不能保证更新会在该次迭代期间完成。
唯一能保证的是,循环结束时,map 中包含了你的更新。当然,你可能永远不会想这样做(这是糟糕的代码),但了解这一点仍然很有意思。
1
2
3
4
5
6
7
8
9
10
11
12
|
func main() {
m := map[int]int{1: 1, 2: 2, 3: 3}
for key, value := range m {
fmt.Printf("%d = %d\n", key, value)
if key == 1 {
for i := 10; i < 20; i++ {
m[i] = i * 10 // Add many entries
}
}
}
}
|
例如,在上面的代码中,你可能看不到循环内添加的值被打印出来。
这是由于 Go 语言内部对象管理方式所致。在 Go 语言中,当你添加一个新的键/值对时,语言会对该键进行哈希处理,并将其放入一个存储桶中。如果 Go 的迭代已经“查看”过该对象中的存储桶,那么新条目就不会在循环中被访问。
空结构体
你经常会看到 Go 开发者发送空的 Go 结构体。为什么是结构体,而不是布尔值之类的呢?
在 Go 语言中,空结构体占用零字节。Go 运行时会处理所有零字节大小的内存分配,包括空结构体,它会返回一个不占用任何空间的特殊内存地址。
这就是为什么它们常用于在信道上传递信号,而无需实际发送任何数据。相比之下,布尔值仍然需要占用一定的空间。
Go 编译器和 range 关键字
Go 编译器会在进一步编译 Go 代码之前,将 range 关键字“降级”为基本循环。具体实现方式取决于被降级的对象,例如 map、slice 或 iter 包中的 sequence。
有趣的是,对于 iter 包,它实际上会将 range 内的 break 调用转换为 yield 函数通常返回的“false”以停止迭代。
匿名嵌套结构体
例如,假设你将 time.Time 结构体嵌入到 JSON 响应字段中,并尝试对该父级进行序列化。
嵌入结构体时,也会隐式地提升其包含的所有方法。由于 time.Time方法有一个MarshalJSON()` 方法,编译器会优先执行该方法,而不是常规的序列化行为。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
package main
import (
"encoding/json"
"fmt"
"time"
)
type Event struct {
Name string `json:"name"`
time.Time `json:"timestamp"`
}
func main() {
event := Event{
Name: "Launch",
Time: time.Date(2023, time.November, 10, 23, 0, 0, 0, time.UTC),
}
jsonData, _ := json.Marshal(event)
fmt.Println(string(jsonData)) // "2023-11-10T23:00:00Z" weird right?
}
|
在这个例子中, Event 结构体嵌入了一个 time.Time 字段。当把 Event 结构体序列化为 JSON 时, time.Time 类型的 MarshalJSON() 方法会自动被调用来格式化整个结果 ,最终导致输出的内容与预期不符。
其他方法也存在同样的问题,可能会导致一些奇怪且难以追踪的错误。使用结构体匿名嵌入时务必小心!
比较时间
在 Go 语言中将时间转换为字符串时,字符串生成器会自动添加时区信息,因此字符串比较将无法正常工作。请改用 .Equal() 方法,该方法用于比较时间:“.Equal 方法会判断 t 和 u 是否代表同一时间点。即使两个时间点位于不同的时区,它们也可能相等。”
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
package main
import (
"fmt"
"time"
)
func main() {
t1 := time.Date(2024, 1, 1, 15, 0, 0, 0, time.UTC)
t2 := t1.In(time.FixedZone("EST", -5*3600)) // Adds timezone info
fmt.Println(t1.String() == t2.String()) // prints false
fmt.Println(t1.Equal(t2)) // prints true!
}
|
wg.Go 函数
Go 1.25 引入了 waitgroup.Go 函数,可以更轻松地向等待组中添加 Go 例程。它取代了使用 go 关键字,如下所示:
1
2
3
|
wg.Go(func() {
// your goroutine code here
})
|
实现方式只是对以下代码的封装:
1
2
3
4
5
6
7
|
func (wg *WaitGroup) Go(f func()) {
wg.Add(1)
go func() {
defer wg.Done()
f()
}()
}
|
go fix
go fix ./...
这个命令会帮助开发者保持代码整洁,现代,高效! 会将旧代码迁移到新的方式(上面提到的其他技巧),尤其是一些前几年的库还用着弃用的函数。
参考
15-go-sublteties-you-may-not-already-know
我删除了部分,也增加了部分,最终还是 15 条。