程序员社区

7个Go代码模式

代码模式能使你的程序更可靠、更高效,并使你的工作更轻松。

7个Go代码模式插图

使用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库解析。

7个Go代码模式插图1

用函数包装选项

有时候,我们会遇到一个复杂的结构体,其中有很多可选字段,这时你会怀念在Python中使用可选参数的感觉:

7个Go代码模式插图2

在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
}

以这种方式包装选项易于使用,更重要的是,易于阅读:

7个Go代码模式插图3
image.png

这种实现动态配制方式,在很多Go框架中都用到了,读者可以在自己的应用中考虑应用。

总结

本文讨论了

  • 使用map[string]struct{}实现set
  • 使用chan struct{}有效同步goroutine,并使用close()将信号广播给任意数量的goroutine。
  • 将通道变量设置为nil以禁用select case。
  • 通过select-default模式构造有损信道
  • 使用匿名结构体对配置值和测试用例进行组合
  • 将选项包装为函数
    如果您是一个经验丰富的Go程序员,那么你可能以前见过这些代码模式。然而,当我第一次开始用Go编程时,这一点对我来说并不明显。

祝大家国庆节快乐?

7个Go代码模式插图4

赞(0) 打赏
未经允许不得转载:IDEA激活码 » 7个Go代码模式

一个分享Java & Python知识的社区