15 个你可能不知道的 Go 技巧

学习新事物最好的方法之一就是定期记录所学到的知识。过去一年,我坚持记录 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 关键字“降级”为基本循环。具体实现方式取决于被降级的对象,例如 mapsliceiter 包中的 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 方法会判断 tu 是否代表同一时间点。即使两个时间点位于不同的时区,它们也可能相等。”

 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 条。

本文阅读量 次, 总访问量 ,总访客数
Built with Hugo .   Theme Stack designed by Jimmy