开发环境搭建

go buildGOROOTGOPATHGOPATHGOPATH$HOME/goGOPATHGOPATH$GOPATH/bin$PATHgo install$GOPATH/binGOROOT$PATHGOROOT

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
ServeHTTPIDProcessor

常量

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

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
}

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

iindexrreaderbbuffer
RuneCountrunecountokv, 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
rrdr

包级导出名

bytesBufferBytesBuffer

接口

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)
}
ernet.Connhttp.ResponseWriterio.ReadWriterReadWriteCloseFlushStringStringToString

错误

ErrorXyzError
type ExitError struct {
...
}
ErrErrXyz
var ErrFormat = errors.New("image: unknown format")

错误描述全部小写,未尾不需要加结束句点。

Getter/Setter

GetSetobjObjSetObj

strconvcrypto/md5net/http/cgisrc/encoding/base64base64
utilcommon

包路径

包路径的最底层路径名和包名一致:

"compress/gzip" // gzip 路径下源文件的的包名也为 gzip

包路径有良好的层级关系但要避免重复罗嗦:

"code.google.com/p/goauth2/oauth2" // bad, goath2 和 oauth2 重复罗嗦

不是所有平台的文件系统都是大小敏感的,包路径名不要有大写字母:

"github.com/Masterminds/glide" // bad

在导入包路径时,按照标准库包、第三方库包、项目内部包的顺序导入,各部分用空行隔开:

import (
"encoding/json"
"strconv"
"time" "github.com/golang/protobuf/proto"
"github.com/gomodule/redigo/redis" "dc_agent/attr"
"dc_agent/dc"
)

禁止使用相对路径导入包:

import (
"./attr" // bad
)

项目代码布局

xauth
git.yingzhongtong.com/combase/xauth # 项目根目录
├── cmd # cmd 目录存放可执行文件(binary)代码
│   ├── client # binary: client 不同的可执行程序各自建立目录存放
│   │   └── main.go
│   └── xauth # binary: xauth
| ├── main.go
│   ├── config # 编译当前可执行程序需要的内部库组织成不同包各自建立目录存放
│   │   └── config.go
│   ├── handler
│   │   └── handler.go
│   ├── httpproxy
│   │   └── httpproxy.go
│   └── zrpcproxy
│   └── zrpcproxy.go
├── pkg # pkg 目录存放库代码
│   ├── model # package: model 不同库组织成不同包,各自建一个目录存放
│   │   └── contract.go
│   ├── ratelimiter # package: ratelimiter
│   │   ├── inmemory.go
│   │   ├── inmemory_test.go
│   │   ├── ratelimiter.go
│   │   ├── redis.go
│   │   └── redis_test.go
│   └── version # package: version
│   └── version.go
├── glide.lock # 项目依赖库文件
├── glide.yaml
├── Makefile
├── README.md # 项目说明文档
├── Dockerfile # 用来创建 docker 镜像
└── xauth.yaml # 项目配置
cmdpkgcmdcmdgit.yingzhongtong.com/combase/xauth/cmd/xauthxauthxauthgit.yingzhongtong.com/combase/xauth/cmd/xauthpkgpkgMakefilemain.go

依赖管理

go getGOPATHGOPATHGOROOT
  • 对依赖的第三方库没有版本管理,每次 go get 时都是下载最新的版本,最新的版本可能存在 bug;
  • 基于域名的第三方库路径可能失效;
  • 多个项目依赖共同的第三方库时,一个项目更新依赖库会影响其他项目。
vendorvendorgo docvendorGOPATHGOROOTvendorvendorGOPATHGOPATHvendorvendorvendorvendorvendorvendorvendorvendorglide.lockglide.yaml

可执行程序版本管理

<大版本号>.<小版本号>.<补丁号>0.0.1git rev-parse --short HEAD
package main

var (
version string
commit string
) func main() {
println("demo server version:", version, "commit:", commit)
// ...
}
versioncommit
VERSION = "0.0.1"
COMMIT = $(shell git rev-parse --short HEAD) all :
go build -ldflags "-X main.version=$(VERSION) -X main.commit=$(COMMIT)"

性能剖析(profiling)

barbarfoo
// test.go

package main

import (
"net/http"
_ "net/http/pprof"
) func foo() []byte {
var buf [1000]byte
return buf[:10]
} var c int func bar(b []byte) {
c++
for i := 0; i < len(b); i++ {
b[i] = byte(c*i*i*i + 4*c*i*i + 8*c*i + 12*c)
}
} func main() {
go http.ListenAndServe(":8200", nil)
for {
b := foo()
bar(b)
}
}
_ "net/http/pprof""runtime/pprof"
# go tool pprof http://localhost:8200/debug/pprof/profile?seconds=5

(pprof) top
Showing nodes accounting for 4990ms, 100% of 4990ms total
flat flat% sum% cum cum%
3290ms 65.93% 65.93% 3290ms 65.93% runtime.duffzero
1540ms 30.86% 96.79% 1540ms 30.86% main.bar
110ms 2.20% 99.00% 3400ms 68.14% main.foo (inline)
50ms 1.00% 100% 4990ms 100% main.main
0 0% 100% 4990ms 100% runtime.main
runtime.duffzero
# sudo yum install graphviz
-pngcpupprof.png
# go tool pprof -png http://localhost:8200/debug/pprof/profile?seconds=5 > cpupprof.png

生成的性能剖析图如下:

foobarfooruntimeduffzerofoobar
# go tool pprof -png http://localhost:8200/debug/pprof/heap > memused.png

以上命令输出的是对象占用空间的视图,默认只有 512KB 以上的内存分配才会写到内存分析文件里,因此建议在程序开始时加上以下代码让每个内存分配都写到到内存分析文件:

func main() {
runtime.MemProfileRate = 1 // 修改默认值 512KB 为 1B
// ...
}
-inuse_objects

测试

golang 语言自带了测试工具和相关库,可以很方便的对 golang 程序进行测试。

推荐表驱动测试的方式进行单元测试,golang 标准库中也有很多例子。以下是一个表驱动测试的示例:

func TestAdd(t *testing.T) {
cases := []struct{ A, B, Expected int }{
// 测试用例表
{1, 1, 2},
{1, -1, 0},
{1, 0, 1},
{0, 0, 0},
}
for _, tc := range cases {
actual := tc.A + tc.B
if actual != expected {
t.Errorf(
"%d + %d = %d, expected %d",
tc.A, tc.B, actual, tc.Expected)
}
}
}

使用表驱动测试可以很方便的增加测试用例测试各种边界条件。这个工具可以很方便的生成表驱动测试的桩代码。

单元测试一般只需要对包中的导出函数进行测试,非导出函数作为内部实现,除非有比较复杂逻辑,一般不用测试。

这个视频(PPT)更详细介绍了 golang 测试的最佳实践,值得一看。

总结

本文不是 golang 语法和工具使用的教程,这些内容在网上可以方便找到。本文假设读者已经对 golang 语法有了基本的了解,给了一些使用 golang 进行实际项目开发时的一些建议和方法指导。文中的主题主要是基于作者的实践经验和一些技术博客的总结,不免带有一些个人偏见。另外 golang 也是一门不断演进中的语言(从官方版本发布频率也可以看出来),文中的内容也非一成不变,保持与时俱进应该是 golang 开发者应有的心态。

参考资料