channel

  1. 如果channel关闭后,再操作会怎么样?
查看解析

当一个 channel 被关闭后,再从中取值会有特定的行为。下面是关闭 channel 后的行为总结:


1、关闭后的读取行为

  • 从已关闭的 channel 读取数据

    • 如果 channel 中还有未取出的数据,可以继续读取这些数据。
    • 一旦 channel 中的数据被取完,再次读取时,将返回该类型的零值,并且读取操作不会阻塞

    例如,如果 channel 类型是 int,则会返回 0;如果是 string,则返回 ""(空字符串)。

  • 检测 channel 是否已关闭

    • 在接收值时,可以通过第二个返回值来判断 channel 是否已关闭。当 channel 被关闭并且所有数据已经取完时,第二个返回值为 false
    1
    v, ok := <-ch
    • v 是接收到的值。
    • ok 是一个布尔值,当 ok == false 时,表示 channel 已经关闭且没有更多的数据可接收。

2、关闭后的发送行为

  • 向已关闭的 channel 发送数据

    • 如果你尝试向一个已关闭的 channel 发送数据,程序会触发运行时 panic,从而导致程序崩溃。因此,在关闭 channel 后,必须确保不会再向该 channel 发送数据。

    关闭 channel 的场景通常发生在发送者一方,发送者负责关闭 channel,而接收者则可以依赖接收到的零值和 ok 来判断何时结束。


3、 示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import "fmt"

func main() {
ch := make(chan int, 3)
ch <- 1
ch <- 2
ch <- 3
close(ch)

// 继续从已关闭的 channel 中读取数据
for i := 0; i < 4; i++ {
v, ok := <-ch
fmt.Printf("value: %d, ok: %v\n", v, ok)
}

// 尝试向已关闭的 channel 发送数据会导致 panic
// ch <- 4 // 取消这行注释会导致 panic
}

输出结果:

1
2
3
4
value: 1, ok: true
value: 2, ok: true
value: 3, ok: true
value: 0, ok: false

在上面的例子中,ch 是一个带缓冲的 channel,存储了 3 个元素。即使 channel 被关闭,前 3 次读取仍然会得到存储的值。但第 4 次读取时,channel 已经没有值了,因此返回了零值 0ok == false,表示 channel 已经关闭并且没有更多值可接收。


4、总结要点

  • 关闭 channel 后,接收操作不会阻塞,可以继续接收已缓冲的值。
  • 取尽 channel 中的数据后,继续接收将返回该类型的零值
  • 可以通过 ok 来判断 channel 是否已关闭:如果 ok == false,表示 channel 已经关闭。
  • 向已关闭的 channel 发送数据会导致 panic,因此关闭 channel 后应避免再发送数据。

5、最佳实践

  • 谁发送,谁关闭:通常由发送方负责关闭 channel,接收方只负责读取数据。接收方不应该尝试关闭 channel,因为它不知道是否还有其他数据未发送。
  • 不必频繁关闭 channel:关闭 channel 的目的是通知接收方不再有数据到来,而不是释放资源。只有当确实需要通知接收者时,才需要关闭 channel。

  1. Channel的缓冲
查看解析

在 Go 中,channel 可以分为无缓冲区的 channel(unbuffered channel)和有缓冲区的 channel(buffered channel)。两者的主要区别在于它们如何处理数据的发送和接收操作。理解这两种 channel 的行为差异非常重要,尤其是在处理并发编程时。


  1. 无缓冲区的 channel

无缓冲区的 channel(即没有设置缓冲大小,或者缓冲大小为 0)要求发送和接收操作必须同步,即发送和接收必须在同一时间点发生。如果没有接收方在等待,发送操作会阻塞,直到有接收者准备接收数据。反之亦然。

关键点:

  • 同步:发送和接收操作必须同时发生。
  • 阻塞行为
    • 当发送方试图发送数据到无缓冲 channel 时,如果没有接收方在接收,它会阻塞,直到接收者开始接收。
    • 同样,当接收方试图从无缓冲 channel 接收数据时,如果没有发送方发送数据,它也会阻塞,直到发送者发送数据。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import "fmt"

func main() {
ch := make(chan int) // 无缓冲的 channel

go func() {
ch <- 1 // 发送操作会阻塞,直到有接收方接收
fmt.Println("数据已发送")
}()

fmt.Println(<-ch) // 接收数据
fmt.Println("数据已接收")
}

输出:

1
2
3
1
数据已发送
数据已接收

在这个例子中:

  • ch <- 1 这行代码会阻塞,直到 fmt.Println(<-ch) 执行,接收数据之后发送操作才会继续执行。
  • 无缓冲的 channel 强制了发送方和接收方的同步。
  1. 有缓冲区的 channel

有缓冲区的 channel(即创建 channel 时设置了缓冲区大小)允许异步发送。发送操作不会立即阻塞,除非缓冲区已满;接收操作也不会立即阻塞,除非缓冲区为空。

关键点:

  • 异步:有缓冲区的 channel 允许发送方发送多个数据,而不必等待接收方接收,只要缓冲区还未满。
  • 阻塞行为
    • 如果缓冲区满了,发送操作会阻塞,直到有数据被接收以腾出空间。
    • 如果缓冲区空了,接收操作会阻塞,直到有数据被发送到 channel 中。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import "fmt"

func main() {
ch := make(chan int, 2) // 有缓冲的 channel,缓冲区大小为 2

ch <- 1 // 发送不会阻塞,因为缓冲区还有空间
ch <- 2 // 发送不会阻塞,缓冲区满了
fmt.Println("数据已发送到缓冲区")

fmt.Println(<-ch) // 接收数据
fmt.Println(<-ch) // 接收数据
}

输出:

1
2
3
数据已发送到缓冲区
1
2

在这个例子中:

  • ch <- 1ch <- 2 都不会阻塞,因为缓冲区有足够的空间来存储它们。
  • 只有当缓冲区满时,发送操作才会阻塞;而当缓冲区为空时,接收操作会阻塞。

  1. 无缓冲与有缓冲的详细对比
特性 无缓冲区 channel 有缓冲区 channel
发送操作 发送方阻塞,直到接收方开始接收数据 发送方只在缓冲区满时阻塞,否则立即返回
接收操作 接收方阻塞,直到发送方发送数据 接收方只在缓冲区为空时阻塞,否则立即接收
同步性 发送和接收操作必须同步 发送和接收可以异步进行,直到缓冲区满或空
适用场景 适合需要严格同步的场景,如信号同步 适合允许异步通信或数据缓冲的场景

  1. 适用场景

无缓冲区的 channel 适用场景:

  • 同步任务:需要发送方和接收方在同一时间点进行交互。典型的用法是在两个 goroutine 之间传递信号,确保它们在某个操作后相互同步。
  • 信号传递:用于通知某个任务完成或触发事件。

有缓冲区的 channel 适用场景:

  • 异步任务:适合生产者-消费者模式,允许生产者和消费者的速度不匹配。例如,生产者可以先放入缓冲区,消费者可以稍后处理。
  • 批量处理:当需要缓冲一部分数据,或者需要异步处理一批数据时,有缓冲的 channel 是理想选择。

  1. 示例对比

无缓冲的 channel:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import "fmt"

func main() {
ch := make(chan int) // 无缓冲 channel

go func() {
fmt.Println("发送数据:1")
ch <- 1
fmt.Println("发送完成")
}()

fmt.Println("接收数据:", <-ch)
}

输出:

1
2
3
发送数据:1
接收数据:1
发送完成
  • 在无缓冲的 channel 中,发送操作会阻塞,直到接收方开始接收。

有缓冲的 channel:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import "fmt"

func main() {
ch := make(chan int, 2) // 缓冲区大小为 2

ch <- 1
ch <- 2
fmt.Println("缓冲区中的数据发送完成")

fmt.Println("接收数据:", <-ch)
fmt.Println("接收数据:", <-ch)
}

输出:

1
2
3
缓冲区中的数据发送完成
接收数据: 1
接收数据: 2
  • 在有缓冲的 channel 中,发送方可以在缓冲区未满时连续发送,接收方稍后再接收数据。

总结

  • 无缓冲的 channel 强制发送和接收操作同步进行,适合需要强制同步的场景。
  • 有缓冲的 channel 允许发送方和接收方异步操作,直到缓冲区满或空,适合生产者-消费者模式等异步通信的场景。
  • 使用无缓冲的 channel 更加简单明确,但有时会限制并发的灵活性;有缓冲的 channel 提供了更多灵活性,但需要合理设置缓冲区大小来防止性能问题或死锁。

根据你的并发任务的需求选择适合的 channel 类型,并合理设计程序的并发模式,才能最大化地利用 Go 的并发优势。


  1. select
查看解析

在 Go 语言中,select 语句用于在多个 channel 操作中进行选择。它的作用类似于 switch,但它专门用于处理多个 channel 的发送和接收操作。select 允许 Go 程序在多个 channel 上等待,并且只执行一个已经准备好的 channel 操作。


select 的基本使用

select 语句的结构如下:

1
2
3
4
5
6
7
8
select {
case <-channel1:
// 当 channel1 有数据可接收时执行此操作
case channel2 <- value:
// 当 channel2 可以发送数据时执行此操作
default:
// 如果上面都没有准备好,执行此操作(可选)
}
  • 每个 case 语句都必须是一个 channel 操作:接收(<-channel)或发送(channel <- value)。
  • select 会等待其中的某个 channel 准备好(可以接收或发送),一旦某个 channel 操作完成,对应的 case 会被执行。
  • 如果有多个 channel 都可以操作,则随机选择一个执行。
  • 如果没有 channel 准备好,并且 select 中没有 default 分支,select 语句会阻塞,直到有 channel 准备好。

select 的应用场景

  1. 同时监听多个 channelselect 可以让你同时监听多个 channel,并根据哪个 channel 准备好来执行相应的操作。这非常适用于网络通信、并发任务管理等场景。
  2. 超时机制select 可以配合 time.After 实现超时功能。
  3. 阻塞等待多个任务完成:通过 select 可以等待多个 goroutine 的任务完成。

select 语句的行为

  • 当一个 case 准备好时select 会执行该 case 的语句。
  • 当多个 case 同时准备好时select 会随机选择一个执行。
  • 如果所有 case 都没有准备好select 会阻塞,直到有一个 case 可以执行。
  • 如果包含 default 语句,当没有 channel 准备好时,default 分支会被立即执行,不会阻塞。

  1. 示例:基本 select 用法
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
package main

import (
"fmt"
"time"
)

func main() {
ch1 := make(chan string)
ch2 := make(chan string)

go func() {
time.Sleep(1 * time.Second)
ch1 <- "来自 ch1 的消息"
}()

go func() {
time.Sleep(2 * time.Second)
ch2 <- "来自 ch2 的消息"
}()

select {
case msg1 := <-ch1:
fmt.Println(msg1)
case msg2 := <-ch2:
fmt.Println(msg2)
}
}

输出结果(由于 select 随机性,可能会不同):

1
来自 ch1 的消息

在这个例子中,两个 goroutine 分别在不同的时间向 ch1ch2 发送数据。select 会等待其中一个 channel 准备好,并执行相应的操作。在上面的代码中,ch1 先准备好,因此 case msg1 := <-ch1 先执行,打印出 ch1 的消息。


  1. 示例:超时处理

通过 selecttime.After,可以实现超时机制。如果超过指定时间还没有 channel 准备好,可以通过 time.After 提供的 channel 来超时处理。

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)

go func() {
time.Sleep(2 * time.Second)
ch <- "Hello from channel!"
}()

select {
case msg := <-ch:
fmt.Println("接收到消息:", msg)
case <-time.After(1 * time.Second):
fmt.Println("超时!没有接收到消息")
}
}

输出结果:

1
超时!没有接收到消息

在这个例子中,time.After(1 * time.Second) 创建了一个定时器,在 1 秒后向返回的 channel 发送数据。如果超过 1 秒,程序就会执行 超时 的分支。


  1. 示例:阻塞直到多个任务完成

通过 select 可以处理多个 goroutine 的任务完成,下面是等待两个并发任务的例子:

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
package main

import (
"fmt"
"time"
)

func task1(done chan bool) {
time.Sleep(1 * time.Second)
fmt.Println("任务 1 完成")
done <- true
}

func task2(done chan bool) {
time.Sleep(2 * time.Second)
fmt.Println("任务 2 完成")
done <- true
}

func main() {
done := make(chan bool)

go task1(done)
go task2(done)

// 等待两个任务完成
for i := 0; i < 2; i++ {
select {
case <-done:
fmt.Println("任务完成通知")
}
}
}

输出结果:

1
2
3
4
任务 1 完成
任务完成通知
任务 2 完成
任务完成通知

在这个例子中,两个并发任务 task1task2 被执行,main 函数通过 select 语句等待两个任务的完成信号。


  1. 示例:default 分支

select 可以包含 default 分支,当所有 channel 都没有准备好时,select 会执行 default 分支,避免阻塞。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import (
"fmt"
)

func main() {
ch := make(chan string)

select {
case msg := <-ch:
fmt.Println("接收到消息:", msg)
default:
fmt.Println("没有任何 channel 就绪")
}
}

输出结果:

1
没有任何 channel 就绪

在这个例子中,由于 ch 没有数据发送,select 会直接执行 default 分支,避免阻塞。


  1. 示例:多路选择

当你有多个 channel 需要同时监听时,select 非常适合用于多路选择:

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
package main

import (
"fmt"
"time"
)

func main() {
ch1 := make(chan string)
ch2 := make(chan string)

go func() {
time.Sleep(1 * time.Second)
ch1 <- "消息来自 ch1"
}()

go func() {
time.Sleep(2 * time.Second)
ch2 <- "消息来自 ch2"
}()

for i := 0; i < 2; i++ {
select {
case msg1 := <-ch1:
fmt.Println(msg1)
case msg2 := <-ch2:
fmt.Println(msg2)
}
}
}

输出结果:

1
2
消息来自 ch1
消息来自 ch2

这里 select 语句会连续两次从两个 channel 中接收数据,确保每个 channel 都可以被处理。


  1. select 的特性
  • 阻塞等待:如果没有 defaultselect 会阻塞,直到其中一个 channel 可以进行操作。
  • 随机性:如果有多个 channel 同时就绪,select 会随机选择一个执行。
  • 避免死锁select 可以帮助你管理多个 goroutine 之间的通信,避免由于等待 channel 而导致的死锁。

总结

  • select 是 Go 中处理多个 channel 并发操作的关键工具。它允许你同时监听多个 channel,并根据其中哪个准备好执行相应的逻辑。
  • 主要应用场景:多路复用、超时控制、阻塞等待多个任务完成等。
  • 常见结构select 语句包含若干 case,每个 case 中都有一个 channel 操作,当某个 channel 准备好时,相应的 case 会被执行。

select 为 Go 的并发编程提供了强大且灵活的控制流机制,结合 goroutines,它使得 Go 语言能够高效地处理并发任务和通信。






string

  1. string参数传递性能考虑
查看解析

总结如下:

Go 中字符串传参的性能考虑

  1. Go 的字符串是不可变的引用类型

    • 在 Go 中,字符串是不可变的,底层是一个包含指向字节数组的指针长度的结构体。
    • 按值传递字符串时,Go 只拷贝指针和长度,而不会拷贝字符串的内容,因此传递的性能开销是非常小的,和字符串本身的大小无关。
  2. 按值传递字符串的性能开销很小

    • 即使字符串很大,Go 语言在按值传递时只会拷贝 16 字节(在 64 位系统上是 8 字节的指针 + 8 字节的长度)。
    • 这使得在大多数情况下,按值传递字符串是非常高效的,不需要担心额外的性能损耗。
  3. 传递指针 *string 并不会显著提升性能

    • 因为按值传递本身的开销已经非常小了,所以在大多数情况下,使用 *string(传递指针)来避免拷贝几乎没有实际的性能收益。
    • 传递指针反而可能导致更多复杂性(如需要额外处理指针的引用、逃逸到堆等问题)。
  4. 字符串操作(如拼接、切片)才是性能瓶颈

    • 影响性能的往往不是字符串传参,而是对字符串的操作。由于 Go 字符串是不可变的,每次操作(如拼接、切片)都会创建新的字符串对象,导致内存分配和内容拷贝。
    • 如果在函数内频繁对字符串进行操作,比如拼接大字符串,才会带来明显的性能开销,这与传参方式无关。

什么时候可能使用指针 *string

  • 极少数情况下,如果函数需要修改字符串的指针或修改原始字符串引用的值(例如在结构体中持有多个大字符串的引用),可以考虑传递 *string
  • 但这种场景非常少见,通常更适合按值传递字符串。

什么时候直接按值传递

  • 在大多数情况下,直接按值传递字符串是最好的选择。这种方式性能足够高效,且代码简洁易读,不需要额外处理指针的复杂性。
  • 即使在高并发、大规模字符串传递场景下,Go 的编译器优化已经足够使按值传递的开销微乎其微。

结论

  • 字符串大小与传参性能无关:无论字符串多大,Go 按值传递时的开销都很小,传递的是指针和长度而非内容。
  • 传递指针通常没有必要:传递 *string 指针的优化效果微乎其微,除非涉及修改字符串的引用。
  • 性能优化应关注字符串操作:与其优化传参方式,不如优化字符串操作的方式,比如避免频繁的拼接或创建新字符串。

总的来说,在 Go 中,按值传递字符串几乎总是性能友好的,除非你有非常特殊的需求,否则不需要使用指针传递。






defer

  1. defer的性能考虑
查看解析

在 Go 1.14 之前,defer 语句的性能一直被认为是有些开销的,尤其是在高频调用的场景下。而从 Go 1.14 开始,Go 编译器对 defer 语句进行了显著的优化,使其在常见情况下的开销大幅度降低,变得几乎可以忽略不计。以下是 Go 语言新旧版 defer 的性能及实现差异的总结。


1、旧版 defer(Go 1.14 之前)

实现方式:

  • 每次调用 defer 时,Go 编译器都会生成一个 defer 对象,并将其加入到一个链表(defer chain)中。这个链表在函数返回时被遍历,执行其中的所有 defer 操作。
  • defer 对象保存了需要在函数退出时调用的函数信息、参数和上下文。这些 defer 对象的创建和管理增加了运行时的开销。
  • 如果一个函数多次使用 defer,就需要生成多个 defer 对象,每次都要将它们加到链表中。这在高频率调用 defer 的情况下,性能可能会受到一定影响。

性能开销:

  • 显著的开销:每次调用 defer 都会创建和管理一个对象,特别是在频繁调用 defer 的场景(如紧密的循环或高并发的代码中),性能开销较为明显。
  • 无法优化:由于 defer 在旧版本中每次调用都会生成一个新的 defer 对象,编译器无法优化掉这些操作,导致性能瓶颈。

典型场景中的影响:

  • 在需要频繁执行 defer 的场景下,旧版的 defer 可能导致性能下降。比如在一个循环中多次使用 defer 进行资源管理时,旧版本的 Go 由于创建大量的 defer 对象,开销显著。

2、新版 defer(Go 1.14 及之后)

实现方式:

  • 内联优化:在 Go 1.14 中,编译器对简单的 defer 操作进行了内联优化。在某些常见情况下(例如 defer 调用一个无参的内联函数),编译器会将 defer 直接展开为普通函数调用,避免创建 defer 对象和维护链表。
  • 去除不必要的开销:新版 defer 在编译期做了一些优化,使得在函数退出时不再需要遍历 defer 链表,从而减少运行时的开销。简单的 defer 调用可能会被直接展开成对被 defer 函数的直接调用。
  • 优化策略:如果 defer 调用的函数是可以内联的,并且没有复杂的参数处理,编译器会将 defer 转换为更高效的代码。

性能开销:

  • 几乎可以忽略不计:在大多数简单的场景下,defer 的性能开销被优化到几乎和手动调用解锁(或其他资源释放函数)一样快。
  • 局部优化:对于较为复杂的 defer 场景,比如带有复杂参数或闭包的函数,defer 仍然会有一些开销,但整体性能已大幅改善。

典型场景中的影响:

  • 在新版的 Go 中,开发者可以放心地在高频率的场景中使用 defer(如循环中反复加锁、解锁),因为其性能开销几乎不会带来负面影响。
  • 由于编译器的优化,开发者不再需要为性能问题而在代码中避免使用 defer,可以安全、简洁地使用 defer 来管理资源释放。

3、新版 defer 优化的常见场景

  • 锁的管理:在新版 Go 中,defer 的开销几乎与直接调用 Unlock() 相同。因此,在 Lock() 后直接使用 defer Unlock() 是推荐的最佳实践,不再有显著的性能开销。

  • 文件关闭:处理文件、数据库连接等资源时,defer Close() 也可以放心使用,不需要担心其性能影响。

  • 高频调用场景:例如在一个循环内频繁调用 defer 管理资源,Go 1.14 及之后的优化使得这种模式成为一种高效、优雅的代码写法。


4、总结对比

特性 旧版 defer(Go 1.14 之前) 新版 defer(Go 1.14 及之后)
实现方式 生成 defer 对象,加入链表 编译器内联优化,减少对象创建
性能开销 显著,特别是高频调用时影响较大 几乎可以忽略不计,性能优化显著
适用场景 需要避免频繁使用 defer 可以放心在任何场景使用 defer
复杂情况 复杂函数可能增加更多开销 编译器针对简单 defer 优化
推荐写法 尽量避免高频使用 defer 推荐使用 defer 简化代码

5、实际使用建议

  • Go 1.14 之前:在旧版本中,开发者需要谨慎使用 defer,特别是在性能敏感的场景中(如紧密的循环内),需要手动管理资源以避免不必要的性能开销。

  • Go 1.14 及之后:现在的 defer 几乎没有性能瓶颈,开发者可以放心地在代码中广泛使用 defer 来管理资源释放。这不仅提高了代码的可读性,还减少了人为错误的可能性。

新版的 defer 优化使得它成为管理资源释放(如锁、文件、网络连接等)的一种简单且高效的方式,在编写并发和高性能代码时不必再因为性能问题去避免使用 defer


  1. defer和return的顺序
查看解析

return 语句先执行,但它并不会立即返回控制权,而是:

  • 先计算并保存 return 语句的返回值(如果有返回值)。
  • 然后执行所有的 defer 语句。
  • 最后才真正返回控制权给调用者。

defer 语句在函数返回之前执行,而且是按照逆序执行。如果在函数中有多个 defer 语句,它们会以后进先出的顺序执行(即最晚的 defer 最先执行)。


例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import "fmt"

func demo() (res int) {
res = 1
defer func() {
fmt.Printf("defer1: res=%v\n", res) // 直接访问 res
}()
defer func() {
fmt.Printf("defer2: res=%v\n", res) // 直接访问 res
}()
return 10
}

func main() {
demo()
}

输出:

1
2
defer2: res=10
defer1: res=10

  1. defer捕获变量
查看解析

在 Go 中,defer匿名函数(闭包)对于变量的捕获有重要的差异。理解它们的变量捕获机制是编写正确 Go 代码的关键。以下是关于 defer 和匿名函数变量捕获的总结:


1、defer 语句的变量捕获

  • defer 语句会在注册时捕获当时的变量值,而不是在 defer 执行时重新计算变量的值。
  • 在函数执行过程中,defer 语句被遇到时,Go 会将 defer 的函数调用注册到一个队列中,当函数返回时按后进先出(LIFO)的顺序执行这些 defer 调用。
  • 变量的值是在 defer 注册时确定的,并不会因为之后的代码修改该变量而更新捕获的值。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import "fmt"

func demo() (res int) {
res = 1
defer fmt.Println("defer1:", res) // 捕获 res=1
res = 2
defer fmt.Println("defer2:", res) // 捕获 res=2
return 10
}

func main() {
demo()
}

输出:

1
2
defer2: 2
defer1: 1

解释:

  • defer 注册时,res 的值被捕获,因此第一个 defer 捕获了 res=1,第二个捕获了 res=2。即使 return 修改了 res 的值,defer 捕获的值保持不变。

2、匿名函数的变量捕获(闭包)

  • 匿名函数(闭包)在执行时使用的是变量的当前值,而不是注册时的值。闭包会引用其外部作用域中的变量,即捕获它们的地址,而不是复制值。
  • 这意味着当 defer 结合匿名函数时,匿名函数在执行时会读取变量的最新值,而不是注册时的值。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import "fmt"

func demo() (res int) {
res = 1
defer func() {
fmt.Println("defer1:", res) // 执行时 res 的值
}()
res = 2
defer func() {
fmt.Println("defer2:", res) // 执行时 res 的值
}()
return 10
}

func main() {
demo()
}

输出:

1
2
defer2: 10
defer1: 10

解释:

  • 在这个例子中,匿名函数会在函数结束时执行,它们会访问 res 的最新值。由于 return 修改了 res 的值(res = 10),所以两个 defer 都打印了 res=10

3、总结:defer 和匿名函数的变量捕获差异

特性 defer 直接调用 defer 结合匿名函数(闭包)
捕获时机 注册时捕获变量的值 执行时访问变量的当前值
执行顺序 后进先出(LIFO)顺序执行 后进先出(LIFO)顺序执行
值的更新 defer 注册时的变量值不会被更新 匿名函数执行时访问变量的最新值
适用场景 适用于简单的操作,如打印当前状态 适用于复杂的操作,尤其需要动态获取变量的场景
捕获方式 捕获当前的值(值拷贝) 捕获变量的引用(地址)

4、最佳实践和使用建议

  • 普通 defer:适用于处理简单场景,如资源释放(关闭文件、释放锁等),因为 defer 在注册时捕获的值不会受到后续代码修改的影响。
  • 匿名函数的 defer:适用于在 defer 中需要访问动态变量的场景,例如函数返回之后的变量状态。匿名函数可以确保执行时捕获变量的最新值。

5、示例对比

普通 defer 捕获值:

1
2
3
4
5
6
func example() (x int) {
x = 5
defer fmt.Println("defer:", x) // 捕获 x = 5
x = 10
return 15
}

输出:

1
defer: 5

defer 结合匿名函数捕获引用:

1
2
3
4
5
6
7
8
func example() (x int) {
x = 5
defer func() {
fmt.Println("defer:", x) // 执行时 x 的值
}()
x = 10
return 15
}

输出:

1
defer: 15

6、总结

  • 普通 defer:在注册时捕获变量的值(值拷贝),适用于处理简单的资源释放场景。defer 会在函数返回前执行,按后进先出顺序执行。
  • 匿名函数(闭包)与 defer:在函数执行结束时动态获取变量的最新值(捕获引用),适用于需要在 defer 中访问最终状态的场景。

使用时应根据需要选择合适的模式,确保正确的变量捕获行为。






Json

  1. 序列化忽略某些字段
查看解析

在 Go 中,如果你需要在序列化(例如,使用 encoding/json 包将结构体转换为 JSON)时忽略某些字段,可以通过以下几种方式实现:


1、使用结构体标签 json:"-"

Go 提供了结构体标签(struct tags)机制,在序列化时可以通过 json:"-" 来忽略特定字段。这个标签告诉 encoding/json 包在序列化或反序列化时跳过该字段。

示例:

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"
)

type Person struct {
Name string `json:"name"`
Age int `json:"age"`
Password string `json:"-"` // 忽略该字段
}

func main() {
p := Person{
Name: "John",
Age: 30,
Password: "supersecret",
}

jsonData, _ := json.Marshal(p)
fmt.Println(string(jsonData)) // 输出:{"name":"John","age":30}
}

解释:

  • Password 字段加了标签 json:"-",因此在 JSON 序列化时被忽略,不会出现在输出中。

2、使用指针或 omitempty 标签

如果你希望在字段的值为零值时(如 0, "", nil)自动忽略该字段,可以使用 json:"omitempty" 标签。这个标签表示只有当字段有非零值时才会被序列化,零值字段会被忽略。

示例:

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 (
"encoding/json"
"fmt"
)

type Person struct {
Name string `json:"name"`
Age int `json:"age,omitempty"` // 如果 Age 为 0,忽略该字段
Address string `json:"address,omitempty"` // 如果 Address 为空,忽略该字段
}

func main() {
p := Person{
Name: "John",
Age: 0, // Age 为 0,属于零值
}

jsonData, _ := json.Marshal(p)
fmt.Println(string(jsonData)) // 输出:{"name":"John"}
}

解释:

  • AgeAddress 使用了 omitempty 标签,因此如果它们的值为零值(如 0, "", nil),就会被忽略,不会出现在序列化结果中。

3、动态控制字段序列化

如果你希望根据具体的条件动态决定是否序列化某个字段,可以使用自定义的序列化逻辑。通过实现 MarshalJSON 接口,可以手动控制字段序列化。

示例:

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
package main

import (
"encoding/json"
"fmt"
)

type Person struct {
Name string `json:"name"`
Age int `json:"age"`
Password string `json:"-"` // 默认忽略 Password
}

// 自定义序列化逻辑
func (p Person) MarshalJSON() ([]byte, error) {
type Alias Person
return json.Marshal(&struct {
Alias
Password string `json:"password,omitempty"` // 动态控制 Password 是否序列化
}{
Alias: (Alias)(p),
Password: p.Password,
})
}

func main() {
p := Person{
Name: "John",
Age: 30,
Password: "supersecret",
}

jsonData, _ := json.Marshal(p)
fmt.Println(string(jsonData)) // 输出:{"name":"John","age":30,"password":"supersecret"}
}

解释:

  • 在这个示例中,通过实现 MarshalJSON,可以动态决定是否包含 Password 字段。在示例中,虽然 Password 在结构体定义中被标记为忽略(json:"-"),但我们在 MarshalJSON 方法中添加了自定义逻辑,使其可以根据需要序列化 Password

4、通过组合方式忽略字段

通过结构体组合,你可以有选择性地只序列化组合中的某些字段,而忽略其他字段。

示例:

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
package main

import (
"encoding/json"
"fmt"
)

type Credentials struct {
Password string `json:"password"`
}

type Person struct {
Name string `json:"name"`
Age int `json:"age"`
Credentials // Password 会被自动序列化
}

func main() {
p := Person{
Name: "John",
Age: 30,
Credentials: Credentials{
Password: "supersecret",
},
}

jsonData, _ := json.Marshal(p)
fmt.Println(string(jsonData)) // 输出:{"name":"John","age":30,"password":"supersecret"}
}

如果你想要组合结构体中的某些字段不被序列化,可以给这些字段添加 json:"-" 标签,或者使用自定义序列化逻辑。


结论

在 Go 中,序列化时忽略字段的方式包括:

  1. 使用 json:"-" 标签:明确指定字段在序列化时被忽略。
  2. 使用 omitempty 标签:根据字段的值是否为零,自动忽略字段。
  3. 自定义序列化逻辑:通过实现 MarshalJSON 方法动态控制哪些字段被序列化。
  4. 组合方式:利用结构体嵌入,灵活控制序列化行为。

根据你的具体需求选择适合的方式,灵活处理字段的序列化和忽略。