golang 项目实战简明指南

原文地址

开发环境搭建

golang 的开发环境搭建比较简单,由于是编译型语言,写好 golang 源码后,只需要执行 go build 就能将源码编译成对应平台(本文中默认为 linux)上的可执行程序。本文不再赘述如何搭建 golang 开发环境,只说明下需要注意的地方。
从官网下载对应平台的 golang 安装包中包括 golang 的编译器、一些工具程序和标准库源码。早期的 golang 版本中,需要设置 GOROOTGOPATH 两个环境变量。
从 1.8 版开始,GOPATH 不再需要显示设置。如果没有显示设置,则 GOPATH 的默认值为 $HOME/goGOPATH 可以设置多个目录,但推荐只设置一个或直接使用默认值,多个 GOPATH 会造成依赖管理的困难。推荐将 $GOPATH/bin 加到 $PATH 里,这样通过 go install 会安装到 $GOPATH/bin 目录的可执行程序可以像系统命令一样直接运行,不用输入完整路径。
从 1.10 版开始, GOROOT 也不再需要显示设置了,只需要将安装包中的 bin 目录加到 $PATH 里,系统会自动推导出 GOROOT 的值。
编辑器根据个人喜好选择,作者主要使用 vim 和 vscode 。这里介绍了使用 vim 时需要安装的插件(安装过程可能需要FQ,YCM 安装比较复杂可以不要,gocode 够用了)。

hello world

以下是 golang 版本的 hello world:

package main

import (
    "fmt"
)

func main() {
    fmt.Println("hello world")
}

golang 安装包自带的 gofmt 能将源码格式化成官方推荐的风格,建议将这个工具整合到编辑器里。
这个简单的程序用 go build 编译出来可执行程序用 ldd 查看发现没有任何动态库依赖,size 也比较大(1.8M ,对等的 C 程序版本只有 7.5K)。实际上这里也体现了 golang 的哲学:直接通过源代码分发软件,所有的代码编到一整个可执行程序里,基本没有动态库依赖(或者只依赖 C/C++ 运行时库和基本的系统库),这也方便了 docker 化(C/C++ 程序员应试能体会动态库依赖有多恶心)。通过 readelf 查看可执行程序会发现代码段和调试信息段占用了比较大的空间,代码段大是因为 golang 的运行时也在里面。调试信息段方便 golang 进程 panic 时会打印详细的进程堆栈及源码信息,这也是为什么 golang 的可执行程序比较大的原因。

命名规范

golang 的标准库提供了 golang 程序命名规范很好的参考标准,命名规范应该尽量和标准库的风格接近,多看下标准库的代码就能体会到 golang 的命名哲学了。
命名在很大程序上也体现了一名程序员的修养,用好的命名写出的代码通常是自注释的,只需要在有复杂的逻辑需要解释的情况下才额外注释。
好的命名应该具有以下特征:

  • 一致性:见名知义,比如标准库中将对象序列化成字符串的操作名为 String ,在你自己的代码里将自定义类型的对象序列化成字符串也应该叫这个名字,并且签名和标准库要一致;
  • 简明精炼:减少敲键盘的次数;
  • 精确性:不要使用有歧义的命名。

通常变量的作用域越广,变量的名字应该越长,反之亦然。

golang 中一般使用驼峰命名法,尽量不要使用下划线(基本只在全大写的常量命名中使用)。首字母缩略词应该全部大写,比如 ServeHTTP , IDProcessor
本文中出现的必须禁止是指强烈推荐的 golang 风格的规范,但违反这个规范并不会导致程序编译不过。

常量

全大写或者驼峰命名都可以,全大写的情况下可使用下划线分隔单词:

const (
    SEEK_SET int = 0 // seek relative to the origin of the file
    SEEK_CUR int = 1 // seek relative to the current offset
    SEEK_END int = 2 // seek relative to the end
)

const (
    MaxInt8   = 1<<7 - 1
    MinInt8   = -1 << 7
    MaxInt16  = 1<<15 - 1
    MinInt16  = -1 << 15
    MaxInt32  = 1<<31 - 1
    MinInt32  = -1 << 31
    MaxInt64  = 1<<63 - 1
    MinInt64  = -1 << 63
    MaxUint8  = 1<<8 - 1
    MaxUint16 = 1<<16 - 1
    MaxUint32 = 1<<32 - 1
    MaxUint64 = 1<<64 - 1
)

局部变量

通过以下代码片断举例说明局部变量的命名原则:

func RuneCount(buffer []byte) int {
    runeCount := 0
    for index := 0; index < len(buffer); {
        if buffer[index] < RuneSelf {
            index++
        } else {
            _, size := DecodeRune(buffer[index:])
            index += size
        }
        runeCount++
    }
    return runeCount
}

惯用的变量名应该尽可能短:

  • 使用 i 而不是 index
  • 使用 r 而不是 reader
  • 使用 b 而不是 buffer

这几个字母在 golang 中有约定俗成的含义,使用单字母名字是更 golang 的方式(可能在其他语言的规范中是反例),其他可以举一反三。
变量名中不要有冗余的信息,在函数 RuneCount 里,计数器命名就不需再把 rune 包含进来了,直接用 count 就好了。
在判断 Map 中是否存在某个键值或者接口的转型操作里,通常用 ok 来接收判断结果:v, ok := m[k]
上文中的示例代码按照以上原则重构后应该是这个样子:

func RuneCount(b []byte) int {
    count := 0
    for i := 0; i < len(b); {
        if b[i] < RuneSelf {
            i++
        } else {
            _, n := DecodeRune(b[i:])
            i += n
        }
        count++
    }
    return count
}

形参

形参的命名原则和局部变量一致。另外 golang 软件是以源代码形式发布的,形参连同函数签名通常会作为接口文档的一部分,所以形参的命名规范还有以下特点。
如果形参的类型已经能明确说明形参的含义了,形参的名字就可以尽量简短:

func AfterFunc(d Duration, f func()) *Timer

func Escape(w io.Writer, s []byte)

如果形参类型不能说明形参的含义,形参的命名则应该做到见名知义:

func Unix(sec, nsec int64) Time

func HasPrefix(s, prefix []byte) bool

返回值

跟形参一样,可导出函数的返回值也是接口文档的一部分,所以可导出函数的必须使用命名返回值:

func Copy(dst Writer, src Reader) (written int64, err error)

func ScanBytes(data []byte, atEOF bool) (advance int, token []byte, err error)

接收器(Receivers)

习惯上接收器的命名命名一般是 1 到 2 个字母的接收器类型的缩写:

func (b *Buffer) Read(p []byte) (n int, err error)

func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request)

func (r Rectangle) Size() Point

同个类型的不同方法中接收器命名要保持一致,不要在一个方法中叫 r ,在另一个方法中又变成了 rdr

包级导出名

包导出的变量、常量、函数、类型使用时有包名的修饰。这些导出名字里就不再需要包含包名的信息了,所以标准库中 bytes 包里的 Buffer 不需要叫 BytesBuffer

接口

只有 1 个方法的接口名通常用方法名加上 er 后缀,不引起迷惑的前提下方法名可以使用缩写:

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

type Execer interface {
    Exec(query string, args []Value) (Result, error)
}

方法名本身是复合词的情况下,可以酌情调整以符合英文文法:

type ByteReader interface {
    ReadByte() (c byte, err error)
}

如果接口有多个方法,则需要选择一个最能精确概括描述接口目的的名词命名(有点难度),但是禁止用多个方法中的某个方法加上 er 后缀来命名,否则别人会误解此接口只有一个方法。可以参考标准库这几个接口所包含的方法及接口的命名:net.Conn, http.ResponseWriter, io.ReadWriter
Read, Write, Close, Flush, String 这几个方法在标准库里已经有约定俗成的含义和签名。自定义的接口方法应该要避免使用这几个名字,除非方法的行为确实和标准库这几个接口方法一致,这时候可以使用这些名字,但必须要确保方法的签名和标准库一致。序列化成字符串的方法命名成 String 而不是 ToString

错误

自定义错误类型以 Error 作为后缀,采用 XyzError 的格式命名:

type ExitError struct {
    ...
}

错误值以 Err 作为前缀,采用 ErrXyz 的格式命名:

var ErrFormat = errors.New("image: unknown format")

错误描述