Steins;Lab

  • 项目
  • 折腾
  • 笔记
  • 图册
  • 杂谈
  • 文章索引 - 博主自荐博文
  • 关于/留言
Steins;Lab
某团的自留研究所
  1. 首页
  2. 学习笔记
  3. 正文

100 Go Mistakes 阅读随记 - 01 Code and project organization

2022年12月17日 2759点热度 1人点赞 0条评论

100 Go Mistakes and How to Avoid Them 一书主要描述了使用 Go 语言编程时的常见问题。本文是博主对第二章 - Code and project organization 的阅读笔记。

Table of Contents

  • 引言
  • 第 2 章: 代码和工程组织
    • #2 不合适的代码嵌套
    • #3 误用 init 函数
    • #5 interface 滥用
    • #6 接口在生产者端定义
    • #7 返回 interface
    • #8 使用 Any
    • #9 错误使用泛型
    • #10 没有注意 type embedding 引入的问题
    • #11 不会使用函数选项模式(functional options pattern)
    • #12 项目布局错乱
    • #13 创建 utility 包
    • #15 文档不规范
    • #16 不使用 linters
  • 小结

引言

最近发现了一本书籍,叫做 100 Go Mistakes and How to Avoid Them。主要描述了使用 Go 语言编程时的常见问题。
其中第一章文中即很有趣地引用了句谚语:

Tell me and I forget.
Teach me and I remember.
Involve me and I learn.

于是博主决定挑出其中自己阅读过程中印象深刻的示例,结合自己粗浅的经验,跑一跑例程,同时也激励自己勤耕不辍写一点点笔记出来。
笔者不想完全 copy 书中的内容,所以随记尽量精简。如果读者能在碎片化时间中对某些 "Mistakes" 有印象,是本系列笔记的荣幸!同时推荐有时间的读者直接阅读原文。

第 2 章: 代码和工程组织

#2 不合适的代码嵌套

刚刚接触 go 语言时,我们可以发现其惯用 if err!= nil 这种形式处理错误。
即优先判断错误,如果出错即返回抛出。

func join(s1, s2 string, max int) (string, error) {
    if s1 == "" {
        return "", errors.New("s1 is empty")
    }
    if s2 == "" {
        return "", errors.New("s2 is empty")
    }
    concat, err := concatenate(s1, s2)
    if err != nil {
        return "", err
    }
    if len(concat) > max {
        return concat[:max], nil
    }
    return concat, nil
}
func concatenate(s1 string, s2 string) (string, error) {
    // ...
}

其实对于这种错误有一些争议,认为 if err!= nil 这种语句占用了过多的阅读空间。Goland 对其处理是在 IDE 层面对排版进行格式化。

书中引用 Go Time 博客提出 'happy path' 准则,即让符合预期的行尽量靠前对齐。笔者认为自己在日常编程时有意识到这条准则即可。

#3 误用 init 函数

Go 中约定的 init 函数根据依赖顺序有遵循自己的初始化顺序。且失败处理是受限的。
书中提出,如果在初始化 sql 或者 redis,失败即 panic 的场景使用 init 函数。如果初始化一定不会失败,可以使用。

笔者自己曾经有项目被 code review 指出初始化的错误。也有的项目,mysql 初始化时需要保证 apollo 配置中心已经初始化成功并拉取最新的配置。因此笔者喜欢自己指定初始化顺序。

总之,使用约定的 init 函数时要格外注意顺序和错误处理。

#5 interface 滥用

interface 是 Go 语言的特色之一。 接口的滥用是指过度设计和使用,从而引入过于抽象却不必要的代码。
最适合示范的接口设计就是标准库中的 io.Reader 和 io.Writer。

type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}

// ReadWriter is the interface that groups the basic Read and Write methods.
type ReadWriter interface {
    Reader
    Writer
}

包括标准库在内,Go 更倾向于使用 1~2 个方法的接口。如 Go 语言之父 Rob Pike 解释过的:

The bigger the interface, the weaker the abstraction.

笔者引用一张 Go 标准库接口大小的统计图。可以看到大部分接口大小在 1~3 个。

图:stdlib, docker, kubernets 的 Go 接口规模统计

使用时机

书中给出了 3 种时机:

  • 共通行为: 比如 sort.Interface,为几乎所有排序算法提供了统一的调用接口
    type Interface interface {
        Len() int
        Less(i, j int) bool
        Swap(i, j int)
    }
  • 解耦合: 比如避免读写类和数据库强耦合,有助于编写单元测试
  • 限制行为: 比如屏蔽掉 set 方法,只暴露给外部 set 方法。

书中最后建议我们:

接口应当是需要的时候才被发现的,而不是强行创建的。我们应该在需要的时候创建它,而不是在早期就预见我们需要它。

所以,如果没有充分的理由需要添加接口,就应当反问自己,为什么不直接去实现?

如 Go 语言之父 Rob Pike 解释过的:

Don’t design with interfaces, discover them.

#6 接口在生产者端定义


图:接口在生产者或消费者端定义

在 Go 语言中,接口在实际使用端定义可以更好地满足里氏替换原则。(比如 store 中暴露了很多方法,但 client 中只想用其中几个,这其自己定义会更加符合最小需求。)

但这一经验不是绝对的,比如标准库 encoding/json 就在生产端定义了接口。除非已经能够明确该种抽象能够被广泛需要,否则一般定义在使用端。

#7 返回 interface

会带来副作用。RFC 761: Transmission Control Protocol提到的一种设计哲学:

Be conservative in what you do, be liberal in what you accept from others. 自己做的时候要谨慎,接受别人的时候开放。

那么对于 Go 来讲,就是

  • 返回 struct,而不是 interface
  • 尽可能接收 interface

其实一般没有 100% 禁止或推荐;包括接下来的建议。有一些在标准库中,由于顶层设计知道即使违反经验也会带来好处,那么就不是 "Mistakes"。

#8 使用 Any

除非在一些格式化场景,使用 Any 即 interface{} 会带来严重的代码解释性下降。也许多写几行代码覆盖函数签名中所有可能的 type 传入,也比直接使用 Any 要靠谱。

#9 错误使用泛型

Go 1.18 引入的泛型,具体使用这篇笔记不表。
但是和接口一样,必须有足够的理由使用泛型,且不能大大增加代码的复杂性。

#10 没有注意 type embedding 引入的问题

比如以下 type embedding 实现会把 sync.Mutex 暴露给使用者。

type InMem struct {
    sync.Mutex
    m map[string]int
}
func New() *InMem {
    return &InMem{m: make(map[string]int)}
}
func (i *InMem) Get(key string) (int, bool) {
    i.Lock()
    v, contains := i.m[key]
    i.Unlock()
    return v, contains
}

使用者可以意外使用 mutex 方法

m := inmem.New()
m.Lock() // unexpected

因此如下定义

type InMem struct {
    mu sync.Mutex
    m map[string]int
}

书中给出使用 embedding 的 2 个原则

  • embedding 不要被简单地当做语法糖使用(比如为了使用 Foo.Baz(),而不是使用 Foo.Bar.Baz())。
  • 如果我们要隐藏数据和方法,就不要使用 embedding。比如上面意外暴露 Lock() 方法的例子

#11 不会使用函数选项模式(functional options pattern)

适用于提供很多选项初始化结构体。同时提供默认选项。

笔者直接列举书中的例子,就明白了。

type options struct {
    port *int
}

type Option func(options *options) error

func WithPort(port int) Option {
    return func(options *options) error {
        if port < 0 {
            return errors.New("port should be positive")
        }
        options.port = &port
        return nil
    }
}

func NewServer(addr string, opts ...Option) (
    *http.Server, error) {
    var options options
    for _, opt := range opts {
        err := opt(&options)
        if err != nil {
            return nil, err
        }
    }
    // At this stage, the options struct is built and contains the config
    // Therefore, we can implement our logic related to port configuration
    var port int
    if options.port == nil {
        port = defaultHTTPPort
    } else {
        if *options.port == 0 {
            port = randomPort()
        } else {
            port = *options.port
        }
    }
    // ...
}

使用时,直接使用 withXXX() 即可。也可以不提供选项。

server, err := httplib.NewServer("localhost",
httplib.WithPort(8080),
httplib.WithTimeout(time.Second))

#12 项目布局错乱

https://github.com/golang-standards/project-layout

不过一般只是参考。以组织约定的项目结构为准。

#13 创建 utility 包

一般认为在 Go 工程中创建 utils/base/common 没什么意义。不如根据其用途直接命名,比如 stringset

#15 文档不规范

根据 Go 的规范标明包描述、方法描述、是否 deprecated 等。这样做不光能自动生成 html 文档。

对于笔者来说,规范的文档能开发者直接在 IDE 中看到方法的用法和 deprecated 警告,一股对产出优秀文档开发者的敬意油然而生。


图:现代 IDE 会自动提示方法文档

#16 不使用 linters

在各个公司项目中,git commit 前使用 go lint 格式化工程基本上是强制的流程。

go lint 的格式,可能与开发者的审美相左,但是保证同一项目在所有开发者的机器上都是统一的,这对工程来讲是一件大好事。

小结

本篇笔记是对《第二章: 代码和工程组织》的总结。笔者觉得这本书很接地气,适合翻阅入门和进阶,推荐!

相关

标签: 暂无
最后更新:2023年1月18日

SPtuan

SPtuan 是一名普通的工程师,最大的愿望是度过平静的时光。 当前从事网络/CDN/对象存储研发。

点赞
< 上一篇
下一篇 >
0 0 votes
文章评分
Subscribe
Login
提醒
guest

guest

0 评论
最新
最旧 得票最多
Inline Feedbacks
View all comments

SPtuan

SPtuan 是一名普通的工程师,最大的愿望是度过平静的时光。
当前从事网络/CDN/对象存储研发。

  • 引言
  • 第 2 章: 代码和工程组织
    • #2 不合适的代码嵌套
    • #3 误用 init 函数
    • #5 interface 滥用
    • #6 接口在生产者端定义
    • #7 返回 interface
    • #8 使用 Any
    • #9 错误使用泛型
    • #10 没有注意 type embedding 引入的问题
    • #11 不会使用函数选项模式(functional options pattern)
    • #12 项目布局错乱
    • #13 创建 utility 包
    • #15 文档不规范
    • #16 不使用 linters
  • 小结
分类
  • Uncategorized
  • 图册
  • 学习笔记
  • 库
  • 折腾
  • 杂谈
  • 瞎**扯
  • 碎碎念
  • 项目跟踪
最近评论
SPtuan 发布于 2 个月前(03月22日) 书签: 关于 disk-io 的经验, 异步/同步 io 系统设计的经验 https://you...
SPtuan 发布于 2 个月前(03月21日) 如果公司不是对外提供这些服务的,这种岗位都是 infra 部门,平均年龄确实会大一些。尤其构建和维护...
HUA 发布于 2 个月前(03月19日) 想请问博主对于国内CDN行业,以及CDN调度、DNS托管类服务相关岗位的看法,以及是否还推荐校招新人...
SPtuan 发布于 3 个月前(02月03日) 2025 注: 长辈对于只身去深圳的担忧,更多地来自于 80s/90s 治安情况。近几年了解了严打...
SPtuan 发布于 4 个月前(01月16日) 哈哈,100就100吧,新年快乐!
热门主题 & 页面
  • 全球互联网拓扑探索 (1) : 互联网是如何工作的
  • Intel Movidius Neural Compute Stick - 英特尔Movidius神经计算棒上手体验
  • iowait 到底是什么?
  • 使用 WSL2 + X11 转发 - 在 Windows10 中打造 GNU/Linux 学习生产环境
  • 动手做!基于nRF24L01P的Arduino无线通信
归档
  • 2025 年 5 月
  • 2025 年 3 月
  • 2024 年 12 月
  • 2024 年 9 月
  • 2024 年 8 月
  • 2024 年 5 月
  • 2024 年 3 月
  • 2024 年 2 月
  • 2023 年 12 月
  • 2023 年 11 月
  • 2023 年 9 月
  • 2023 年 8 月
  • 2023 年 4 月
  • 2023 年 1 月
  • 2022 年 12 月
  • 2022 年 10 月
  • 2022 年 9 月
  • 2022 年 7 月
  • 2022 年 6 月
  • 2022 年 2 月
  • 2021 年 12 月
  • 2021 年 11 月
  • 2021 年 2 月
  • 2021 年 1 月
  • 2020 年 9 月
  • 2020 年 4 月
  • 2020 年 3 月
  • 2020 年 1 月
  • 2019 年 8 月
  • 2019 年 7 月
  • 2019 年 5 月
  • 2019 年 4 月
  • 2019 年 3 月
  • 2019 年 2 月
  • 2018 年 12 月
  • 2018 年 10 月
  • 2018 年 9 月
  • 2018 年 8 月
  • 2018 年 5 月
  • 2018 年 2 月
  • 2018 年 1 月
  • 2017 年 11 月
  • 2017 年 9 月
  • 2017 年 7 月
  • 2017 年 6 月
  • 2017 年 5 月
  • 2017 年 4 月
  • 2017 年 3 月
  • 2017 年 2 月
  • 2017 年 1 月
  • 2016 年 12 月
  • 2016 年 11 月
  • 2016 年 10 月
  • 2016 年 9 月
  • 2016 年 8 月
  • 2016 年 7 月
  • 2016 年 6 月
  • 2016 年 5 月
  • 2016 年 4 月
  • 2016 年 3 月
  • 2016 年 2 月
  • 2016 年 1 月
  • 2015 年 12 月
  • 2015 年 11 月
  • 2015 年 9 月

友情链接:

Blessing Studio hahaschool 绘枫和畅 魔法少女Fandy monsterx Clarke的博客 Luminous’ Home Shintaku's Blog
蓝黑的博客 haruhi.club Yida的博客 Bo2SS 涛叔 TangBao 同和君Hocassian

Steins;Lab 团子神社 zdfmc.net

steinslab.io built with ❤. Thanks for all 2015-2025.

Theme Kratos Made By Seaton Jiang

wpDiscuz