代码模式能使你的程序更可靠、更高效,并使你的工作更轻松。
使用Map实现Set
我们经常需要检查对象的存在与否。例如,我们可能想要检查一个文件路径/URL/ID之前是否被访问过。在这些情况下,可以使用map[string]struct{}。例如:
type Crawler struct {
visited map[string]struct{}
}
func (c *Crawler)Crawl(url string) error {
if _, ok := c.visited[url]; ok{
return nil
}
c.visited[url] = struct{}{}
//...
}
使用空结构体struct{},意味着你的map的value部分并不占用空间。有些人可能会使用map[string]bool,但是基准测试表明map[string]struct{}性能更好,可以查看这里。值得一提的是,map操作通常具有O(1)时间复杂度,但是go运行时没有提供这样的保证,可以查阅(StackOverflow)。
使用chan struct{}实现Goroutine的同步
有时使用channel,并不一定需要存放数据。我们只需要它们来实现同步。在下面的例子中,channel携带一个数据类型struct{},它是一个不占用空间的空结构体。这和之前的map例子是一样的技巧:
func hello(quit chan struct{}) {
for {
select {
case <-quit:
return
default:
println("hello")
}
}
}
func main() {
quit := make(chan struct{})
go hello(quit)
//打印hello 10秒
time.Sleep(10* time.Second)
quit <- struct{}{}
}
使用Close广播
继续前面的例子,如果我们运行多个go hello(quit),那么不需要发送多个struct{}{}到quit,我们只需要关闭quit通道来广播信号:
func main() {
quit := make(chan struct{})
go hello(quit)
go hello(quit)
//打印hello 10秒
time.Sleep(10* time.Second)
close(quit)
}
请注意,关闭一个通道来广播一个信号对任意数量的goroutine都生效,所以在前面的示例中close(quit)也适用。
使用Nil channel来阻塞Select语句
有时我们需要在select语句中禁用某些case,例如下面的函数,它从事件源读取事件并将事件发送到分派通道。(这类函数通常涉及处理原始数据以形成事件对象,我门切入正题)。
func (s *Subscription)eventLoop() {
var pending []Event
for {
select {
case e := <- s.eventSource:
pending = append(pending, e) //没有设置容量可能分配大量内存
case s.dispatchC <- pending[0]: //如果长度为0,会panic
pending = pending[1:]
}
}
}
以上代码可以改进地方:
- 如果len(pending) == 0,禁止case.dispatchC分支执行,这样代码不会panic。
- 如果len(pending) >= maxPending,禁止case.eventSource分支执行,这样可以避免分配大量内存。
func (s *Subscription)eventLoop() {
var pending []Event
var dispatchC, eventSource chan Event
var first Event
for {
dispatchC, eventSource, first = nil, nil, nil
if len(pending) > 0 {
dispatchC = s.dispatchC //开启dispatch通道
first = pending[0]
}
const maxPending = 100
if len(pending) < maxPending{
eventSource = s.eventSource //开启source通道
}
select {
case e := <- s.eventSource:
pending = append(pending, e) //没有设置容量可能分配大量内存
case s.dispatchC <- pending[0]: //如果长度为0,会panic
pending = pending[1:]
}
}
}
这里的技巧是使用一个额外的变量来打开/关闭原始通道,然后将该变量放在select情况中使用。
在case语句中,如果通道是nil的话会一直阻塞。
注意:不要同时禁用所有情况,否则for-select循环将停止工作。
非阻塞从通道读取
有时我们想要提供“best-effort”的服务。也就是说,我们希望channel是有损耗的。
例如,当我们有过多的事件要分派给接收者,并且其中一些可能没有响应时,这是有意义的。我们可以忽略那些没有响应的接收者:
1、及时发送给其他接收者
2、避免为挂起事件分配过多的内存
func (s *Subscription)eventLoop() {
var pending []Event
for {
select {
case e := <- s.eventSource:
pending = append(pending, e) //没有设置容量可能分配大量内存
case s.dispatchC <- pending[0]: //如果长度为0,会panic
pending = pending[1:]
default: //不阻塞继续下一个接收者
}
}
}
匿名Struct
有时,我们只是想要一个容器来保存一组相关的值,而这种分组不会出现在其他任何地方。在这些情况下,我们不关心它的类型。在Python中,我们可以为这些情况创建字典或元组。在Go中,可以为这种情况创建一个匿名结构体。我将用两个例子来说明。
case1: Config
你想把配置值组合到一个变量中。但为它创建一个类型似乎有点小题大做了。
取代以下方式:
type MyConfig struct {
Timeout time.Duration
MaxConnections int
}
var Config MyConfig
使用:
var Config struct {
Timeout time.Duration
MaxConnections int
}
注意:Config类型是struct{...},你可以通过Config.Timeout访问各个配制。
Case2: 测试用例
假设你想测试你的Add()函数,而不是像下面这样写很多if-else语句:
func TestAdd(t * testing.T) {
if ans := Add(1, 2); ans != 3{
t.Errorf("Expect %d for %d + %d, got %d", 3, 1, 2, ans)
}
if ans := Add(-1, 2); ans!= 1 {
t.Errorf("Expect %d for %d + %d, got %d", 1, -1, 2, ans)
}
}
你可以像这样分离你的测试用例和测试逻辑:
func TestAdd(t * testing.T) {
testCases := []struct{ in1, in2, expect int }{
{1, 2, 3},
{-1, 2, 1},
}
for _, tc := range testCases {
if got := Add(tc.in1, tc.in2); got != tc.expect {
t.Errorf("Expect %d for %d + %d, got %d", tc.expect, tc.in1, tc.in2, got)
}
}
}
当有许多测试用例时,或者有时需要更改测试逻辑时,这种方式很方便。
当然肯定有更多的场景可以发现使用匿名结构体很方便。例如,当你想要解析以下JSON时,可以使用定义嵌套的匿名结构体,以便使用encoding/ JSON库解析。
用函数包装选项
有时候,我们会遇到一个复杂的结构体,其中有很多可选字段,这时你会怀念在Python中使用可选参数的感觉:
在Go中,我最喜欢的实现方法是使用函数包装这些选项(端口、代理)。也就是说,我们可以构造函数来应用我们的选项值,这些值存储在函数的闭包中。
使用Client例子说明,我们有两个可选字段(port, proxy),用户可以在创建Client实例时指定:
type Client struct {
host, proxy string
port int
}
type Option func(*Client) //使用option时调用这个函数
func WithPort(port int) Option {
return func(client *Client) {
client.port = port
}
}
func WithProxy(proxy string) Option {
return func(client *Client) {
client.proxy = proxy
}
}
func NewClient(host string, options ...Option) *Client {
c := &Client{host: host, port: 80} //默认值
for _, option := range options {
option(c)
}
return c
}
以这种方式包装选项易于使用,更重要的是,易于阅读:
这种实现动态配制方式,在很多Go框架中都用到了,读者可以在自己的应用中考虑应用。
总结
本文讨论了
- 使用map[string]struct{}实现set
- 使用chan struct{}有效同步goroutine,并使用close()将信号广播给任意数量的goroutine。
- 将通道变量设置为nil以禁用select case。
- 通过select-default模式构造有损信道
- 使用匿名结构体对配置值和测试用例进行组合
- 将选项包装为函数
如果您是一个经验丰富的Go程序员,那么你可能以前见过这些代码模式。然而,当我第一次开始用Go编程时,这一点对我来说并不明显。
祝大家国庆节快乐?