golang注意事项
channel
- 如果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
来判断何时结束。 - 如果你尝试向一个已关闭的 channel 发送数据,程序会触发运行时 panic,从而导致程序崩溃。因此,在关闭
3、 示例代码:
1 |
|
输出结果:
1 |
|
在上面的例子中,ch
是一个带缓冲的 channel,存储了 3 个元素。即使 channel
被关闭,前 3 次读取仍然会得到存储的值。但第 4 次读取时,channel
已经没有值了,因此返回了零值 0
和 ok == false
,表示 channel
已经关闭并且没有更多值可接收。
4、总结要点:
- 关闭
channel
后,接收操作不会阻塞,可以继续接收已缓冲的值。 - 取尽
channel
中的数据后,继续接收将返回该类型的零值。 - 可以通过
ok
来判断channel
是否已关闭:如果ok == false
,表示channel
已经关闭。 - 向已关闭的 channel 发送数据会导致 panic,因此关闭
channel
后应避免再发送数据。
5、最佳实践:
- 谁发送,谁关闭:通常由发送方负责关闭 channel,接收方只负责读取数据。接收方不应该尝试关闭 channel,因为它不知道是否还有其他数据未发送。
- 不必频繁关闭 channel:关闭 channel 的目的是通知接收方不再有数据到来,而不是释放资源。只有当确实需要通知接收者时,才需要关闭 channel。
- Channel的缓冲
查看解析
在 Go 中,channel
可以分为无缓冲区的 channel(unbuffered channel)和有缓冲区的 channel(buffered channel)。两者的主要区别在于它们如何处理数据的发送和接收操作。理解这两种 channel 的行为差异非常重要,尤其是在处理并发编程时。
- 无缓冲区的 channel
无缓冲区的 channel(即没有设置缓冲大小,或者缓冲大小为 0)要求发送和接收操作必须同步,即发送和接收必须在同一时间点发生。如果没有接收方在等待,发送操作会阻塞,直到有接收者准备接收数据。反之亦然。
关键点:
- 同步:发送和接收操作必须同时发生。
- 阻塞行为:
- 当发送方试图发送数据到无缓冲 channel 时,如果没有接收方在接收,它会阻塞,直到接收者开始接收。
- 同样,当接收方试图从无缓冲 channel 接收数据时,如果没有发送方发送数据,它也会阻塞,直到发送者发送数据。
示例:
1 |
|
输出:
1 |
|
在这个例子中:
ch <- 1
这行代码会阻塞,直到fmt.Println(<-ch)
执行,接收数据之后发送操作才会继续执行。- 无缓冲的 channel 强制了发送方和接收方的同步。
- 有缓冲区的 channel
有缓冲区的 channel(即创建 channel 时设置了缓冲区大小)允许异步发送。发送操作不会立即阻塞,除非缓冲区已满;接收操作也不会立即阻塞,除非缓冲区为空。
关键点:
- 异步:有缓冲区的 channel 允许发送方发送多个数据,而不必等待接收方接收,只要缓冲区还未满。
- 阻塞行为:
- 如果缓冲区满了,发送操作会阻塞,直到有数据被接收以腾出空间。
- 如果缓冲区空了,接收操作会阻塞,直到有数据被发送到 channel 中。
示例:
1 |
|
输出:
1 |
|
在这个例子中:
ch <- 1
和ch <- 2
都不会阻塞,因为缓冲区有足够的空间来存储它们。- 只有当缓冲区满时,发送操作才会阻塞;而当缓冲区为空时,接收操作会阻塞。
- 无缓冲与有缓冲的详细对比
特性 | 无缓冲区 channel | 有缓冲区 channel |
---|---|---|
发送操作 | 发送方阻塞,直到接收方开始接收数据 | 发送方只在缓冲区满时阻塞,否则立即返回 |
接收操作 | 接收方阻塞,直到发送方发送数据 | 接收方只在缓冲区为空时阻塞,否则立即接收 |
同步性 | 发送和接收操作必须同步 | 发送和接收可以异步进行,直到缓冲区满或空 |
适用场景 | 适合需要严格同步的场景,如信号同步 | 适合允许异步通信或数据缓冲的场景 |
- 适用场景
无缓冲区的 channel 适用场景:
- 同步任务:需要发送方和接收方在同一时间点进行交互。典型的用法是在两个 goroutine 之间传递信号,确保它们在某个操作后相互同步。
- 信号传递:用于通知某个任务完成或触发事件。
有缓冲区的 channel 适用场景:
- 异步任务:适合生产者-消费者模式,允许生产者和消费者的速度不匹配。例如,生产者可以先放入缓冲区,消费者可以稍后处理。
- 批量处理:当需要缓冲一部分数据,或者需要异步处理一批数据时,有缓冲的 channel 是理想选择。
- 示例对比
无缓冲的 channel:
1 |
|
输出:
1 |
|
- 在无缓冲的
channel
中,发送操作会阻塞,直到接收方开始接收。
有缓冲的 channel:
1 |
|
输出:
1 |
|
- 在有缓冲的
channel
中,发送方可以在缓冲区未满时连续发送,接收方稍后再接收数据。
总结
- 无缓冲的 channel 强制发送和接收操作同步进行,适合需要强制同步的场景。
- 有缓冲的 channel 允许发送方和接收方异步操作,直到缓冲区满或空,适合生产者-消费者模式等异步通信的场景。
- 使用无缓冲的 channel 更加简单明确,但有时会限制并发的灵活性;有缓冲的 channel 提供了更多灵活性,但需要合理设置缓冲区大小来防止性能问题或死锁。
根据你的并发任务的需求选择适合的 channel
类型,并合理设计程序的并发模式,才能最大化地利用 Go 的并发优势。
- select
查看解析
在 Go 语言中,select
语句用于在多个 channel
操作中进行选择。它的作用类似于 switch
,但它专门用于处理多个 channel
的发送和接收操作。select
允许 Go 程序在多个 channel
上等待,并且只执行一个已经准备好的 channel
操作。
select
的基本使用
select
语句的结构如下:
1 |
|
- 每个
case
语句都必须是一个channel
操作:接收(<-channel
)或发送(channel <- value
)。 select
会等待其中的某个channel
准备好(可以接收或发送),一旦某个channel
操作完成,对应的case
会被执行。- 如果有多个
channel
都可以操作,则随机选择一个执行。 - 如果没有
channel
准备好,并且select
中没有default
分支,select
语句会阻塞,直到有channel
准备好。
select
的应用场景
- 同时监听多个
channel
:select
可以让你同时监听多个channel
,并根据哪个channel
准备好来执行相应的操作。这非常适用于网络通信、并发任务管理等场景。 - 超时机制:
select
可以配合time.After
实现超时功能。 - 阻塞等待多个任务完成:通过
select
可以等待多个goroutine
的任务完成。
select
语句的行为
- 当一个
case
准备好时,select
会执行该case
的语句。 - 当多个
case
同时准备好时,select
会随机选择一个执行。 - 如果所有
case
都没有准备好,select
会阻塞,直到有一个case
可以执行。 - 如果包含
default
语句,当没有channel
准备好时,default
分支会被立即执行,不会阻塞。
- 示例:基本
select
用法
1 |
|
输出结果(由于 select 随机性,可能会不同):
1 |
|
在这个例子中,两个 goroutine
分别在不同的时间向 ch1
和 ch2
发送数据。select
会等待其中一个 channel
准备好,并执行相应的操作。在上面的代码中,ch1
先准备好,因此 case msg1 := <-ch1
先执行,打印出 ch1
的消息。
- 示例:超时处理
通过 select
和 time.After
,可以实现超时机制。如果超过指定时间还没有 channel
准备好,可以通过 time.After
提供的 channel 来超时处理。
1 |
|
输出结果:
1 |
|
在这个例子中,time.After(1 * time.Second)
创建了一个定时器,在 1 秒后向返回的 channel
发送数据。如果超过 1 秒,程序就会执行 超时
的分支。
- 示例:阻塞直到多个任务完成
通过 select
可以处理多个 goroutine
的任务完成,下面是等待两个并发任务的例子:
1 |
|
输出结果:
1 |
|
在这个例子中,两个并发任务 task1
和 task2
被执行,main
函数通过 select
语句等待两个任务的完成信号。
- 示例:
default
分支
select
可以包含 default
分支,当所有 channel
都没有准备好时,select
会执行 default
分支,避免阻塞。
1 |
|
输出结果:
1 |
|
在这个例子中,由于 ch
没有数据发送,select
会直接执行 default
分支,避免阻塞。
- 示例:多路选择
当你有多个 channel
需要同时监听时,select
非常适合用于多路选择:
1 |
|
输出结果:
1 |
|
这里 select
语句会连续两次从两个 channel
中接收数据,确保每个 channel
都可以被处理。
select
的特性
- 阻塞等待:如果没有
default
,select
会阻塞,直到其中一个channel
可以进行操作。 - 随机性:如果有多个
channel
同时就绪,select
会随机选择一个执行。 - 避免死锁:
select
可以帮助你管理多个 goroutine 之间的通信,避免由于等待channel
而导致的死锁。
总结
select
是 Go 中处理多个channel
并发操作的关键工具。它允许你同时监听多个channel
,并根据其中哪个准备好执行相应的逻辑。- 主要应用场景:多路复用、超时控制、阻塞等待多个任务完成等。
- 常见结构:
select
语句包含若干case
,每个case
中都有一个channel
操作,当某个channel
准备好时,相应的case
会被执行。
select
为 Go 的并发编程提供了强大且灵活的控制流机制,结合 goroutines
,它使得 Go 语言能够高效地处理并发任务和通信。
string
- string参数传递性能考虑
查看解析
总结如下:
Go 中字符串传参的性能考虑
-
Go 的字符串是不可变的引用类型:
- 在 Go 中,字符串是不可变的,底层是一个包含指向字节数组的指针和长度的结构体。
- 按值传递字符串时,Go 只拷贝指针和长度,而不会拷贝字符串的内容,因此传递的性能开销是非常小的,和字符串本身的大小无关。
-
按值传递字符串的性能开销很小:
- 即使字符串很大,Go 语言在按值传递时只会拷贝 16 字节(在 64 位系统上是 8 字节的指针 + 8 字节的长度)。
- 这使得在大多数情况下,按值传递字符串是非常高效的,不需要担心额外的性能损耗。
-
传递指针
*string
并不会显著提升性能:- 因为按值传递本身的开销已经非常小了,所以在大多数情况下,使用
*string
(传递指针)来避免拷贝几乎没有实际的性能收益。 - 传递指针反而可能导致更多复杂性(如需要额外处理指针的引用、逃逸到堆等问题)。
- 因为按值传递本身的开销已经非常小了,所以在大多数情况下,使用
-
字符串操作(如拼接、切片)才是性能瓶颈:
- 影响性能的往往不是字符串传参,而是对字符串的操作。由于 Go 字符串是不可变的,每次操作(如拼接、切片)都会创建新的字符串对象,导致内存分配和内容拷贝。
- 如果在函数内频繁对字符串进行操作,比如拼接大字符串,才会带来明显的性能开销,这与传参方式无关。
什么时候可能使用指针 *string
?
- 极少数情况下,如果函数需要修改字符串的指针或修改原始字符串引用的值(例如在结构体中持有多个大字符串的引用),可以考虑传递
*string
。 - 但这种场景非常少见,通常更适合按值传递字符串。
什么时候直接按值传递?
- 在大多数情况下,直接按值传递字符串是最好的选择。这种方式性能足够高效,且代码简洁易读,不需要额外处理指针的复杂性。
- 即使在高并发、大规模字符串传递场景下,Go 的编译器优化已经足够使按值传递的开销微乎其微。
结论
- 字符串大小与传参性能无关:无论字符串多大,Go 按值传递时的开销都很小,传递的是指针和长度而非内容。
- 传递指针通常没有必要:传递
*string
指针的优化效果微乎其微,除非涉及修改字符串的引用。 - 性能优化应关注字符串操作:与其优化传参方式,不如优化字符串操作的方式,比如避免频繁的拼接或创建新字符串。
总的来说,在 Go 中,按值传递字符串几乎总是性能友好的,除非你有非常特殊的需求,否则不需要使用指针传递。
defer
- 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
。
- defer和return的顺序
查看解析
return
语句先执行,但它并不会立即返回控制权,而是:
- 先计算并保存
return
语句的返回值(如果有返回值)。 - 然后执行所有的
defer
语句。 - 最后才真正返回控制权给调用者。
defer
语句在函数返回之前执行,而且是按照逆序执行。如果在函数中有多个 defer
语句,它们会以后进先出的顺序执行(即最晚的 defer
最先执行)。
例:
1 |
|
输出:
1 |
|
- defer捕获变量
查看解析
在 Go 中,defer
和 匿名函数(闭包)对于变量的捕获有重要的差异。理解它们的变量捕获机制是编写正确 Go 代码的关键。以下是关于 defer
和匿名函数变量捕获的总结:
1、defer
语句的变量捕获
defer
语句会在注册时捕获当时的变量值,而不是在defer
执行时重新计算变量的值。- 在函数执行过程中,
defer
语句被遇到时,Go 会将defer
的函数调用注册到一个队列中,当函数返回时按后进先出(LIFO)的顺序执行这些defer
调用。 - 变量的值是在
defer
注册时确定的,并不会因为之后的代码修改该变量而更新捕获的值。
示例:
1 |
|
输出:
1 |
|
解释:
- 在
defer
注册时,res
的值被捕获,因此第一个defer
捕获了res=1
,第二个捕获了res=2
。即使return
修改了res
的值,defer
捕获的值保持不变。
2、匿名函数的变量捕获(闭包)
- 匿名函数(闭包)在执行时使用的是变量的当前值,而不是注册时的值。闭包会引用其外部作用域中的变量,即捕获它们的地址,而不是复制值。
- 这意味着当
defer
结合匿名函数时,匿名函数在执行时会读取变量的最新值,而不是注册时的值。
示例:
1 |
|
输出:
1 |
|
解释:
- 在这个例子中,匿名函数会在函数结束时执行,它们会访问
res
的最新值。由于return
修改了res
的值(res = 10
),所以两个defer
都打印了res=10
。
3、总结:defer
和匿名函数的变量捕获差异
特性 | defer 直接调用 |
defer 结合匿名函数(闭包) |
---|---|---|
捕获时机 | 注册时捕获变量的值 | 执行时访问变量的当前值 |
执行顺序 | 后进先出(LIFO)顺序执行 | 后进先出(LIFO)顺序执行 |
值的更新 | defer 注册时的变量值不会被更新 |
匿名函数执行时访问变量的最新值 |
适用场景 | 适用于简单的操作,如打印当前状态 | 适用于复杂的操作,尤其需要动态获取变量的场景 |
捕获方式 | 捕获当前的值(值拷贝) | 捕获变量的引用(地址) |
4、最佳实践和使用建议
- 普通
defer
:适用于处理简单场景,如资源释放(关闭文件、释放锁等),因为defer
在注册时捕获的值不会受到后续代码修改的影响。 - 匿名函数的
defer
:适用于在defer
中需要访问动态变量的场景,例如函数返回之后的变量状态。匿名函数可以确保执行时捕获变量的最新值。
5、示例对比
普通 defer
捕获值:
1 |
|
输出:
1 |
|
defer
结合匿名函数捕获引用:
1 |
|
输出:
1 |
|
6、总结:
- 普通
defer
:在注册时捕获变量的值(值拷贝),适用于处理简单的资源释放场景。defer
会在函数返回前执行,按后进先出顺序执行。 - 匿名函数(闭包)与
defer
:在函数执行结束时动态获取变量的最新值(捕获引用),适用于需要在defer
中访问最终状态的场景。
使用时应根据需要选择合适的模式,确保正确的变量捕获行为。
Json
- 序列化忽略某些字段
查看解析
在 Go 中,如果你需要在序列化(例如,使用 encoding/json
包将结构体转换为 JSON)时忽略某些字段,可以通过以下几种方式实现:
1、使用结构体标签 json:"-"
Go 提供了结构体标签(struct tags)机制,在序列化时可以通过 json:"-"
来忽略特定字段。这个标签告诉 encoding/json
包在序列化或反序列化时跳过该字段。
示例:
1 |
|
解释:
Password
字段加了标签json:"-"
,因此在 JSON 序列化时被忽略,不会出现在输出中。
2、使用指针或 omitempty
标签
如果你希望在字段的值为空或零值时(如 0
, ""
, nil
)自动忽略该字段,可以使用 json:"omitempty"
标签。这个标签表示只有当字段有非零值时才会被序列化,零值字段会被忽略。
示例:
1 |
|
解释:
Age
和Address
使用了omitempty
标签,因此如果它们的值为零值(如0
,""
,nil
),就会被忽略,不会出现在序列化结果中。
3、动态控制字段序列化
如果你希望根据具体的条件动态决定是否序列化某个字段,可以使用自定义的序列化逻辑。通过实现 MarshalJSON
接口,可以手动控制字段序列化。
示例:
1 |
|
解释:
- 在这个示例中,通过实现
MarshalJSON
,可以动态决定是否包含Password
字段。在示例中,虽然Password
在结构体定义中被标记为忽略(json:"-"
),但我们在MarshalJSON
方法中添加了自定义逻辑,使其可以根据需要序列化Password
。
4、通过组合方式忽略字段
通过结构体组合,你可以有选择性地只序列化组合中的某些字段,而忽略其他字段。
示例:
1 |
|
如果你想要组合结构体中的某些字段不被序列化,可以给这些字段添加 json:"-"
标签,或者使用自定义序列化逻辑。
结论
在 Go 中,序列化时忽略字段的方式包括:
- 使用
json:"-"
标签:明确指定字段在序列化时被忽略。 - 使用
omitempty
标签:根据字段的值是否为零,自动忽略字段。 - 自定义序列化逻辑:通过实现
MarshalJSON
方法动态控制哪些字段被序列化。 - 组合方式:利用结构体嵌入,灵活控制序列化行为。
根据你的具体需求选择适合的方式,灵活处理字段的序列化和忽略。