这是直接总结好的 12 条,详细的再继续往下看:
- 先处理错误避免嵌套
- 尽量避免重复
- 先写最重要的代码
- 给代码写文档注释
- 命名尽可能简洁
- 使用多文件包
- 使用 go get 可获取你的包
- 了解自己的需求
- 保持包的独立性
- 避免在内部使用并发
- 使用 Goroutine 管理状态
- 避免 Goroutine 泄露
最佳实践
这是一篇翻译文章,为了使读者更好的理解,会在原文翻译的基础增加一些讲解或描述。
来在维基百科:
“A best practice is a method or technique that has consistently shown results superior
to those achieved with other means”
最佳实践是一种方法或技术,其结果始终优于其他方式。
写 Go 代码时的技术要求:
- 简单性
- 可读性
- 可维护性
样例代码
需要优化的代码。
type Gopher struct {
Name string
AgeYears int
}
func (g *Gopher) WriteTo(w io.Writer) (size int64, err error) {
err = binary.Write(w, binary.LittleEndian, int32(len(g.Name)))
if err == nil {
size += 4
var n int
n, err = w.Write([]byte(g.Name))
size += int64(n)
if err == nil {
err = binary.Write(w, binary.LittleEndian, int64(g.AgeYears))
if err == nil {
size += 4
}
return
}
return
}
return
}
看看上面的代码,自己先思索在代码编写方式上怎么更好,我先简单说下代码意思是啥:
- 将 Name 和 AgeYears 字段数据存入 io.Writer 类型中。
- 如果存入的数据是 string 或 []byte 类型,再追加其长度数据。
如果对 binary 这个标准包不知道怎么使用,就看看我的另一篇文章《快速了解 “小字端” 和 “大字端” 及 Go 语言中的使用》。
# 先处理错误避免嵌套 #
func (g *Gopher) WriteTo(w io.Writer) (size int64, err error) {
err = binary.Write(w, binary.LittleEndian, int32(len(g.Name)))
if err != nil {
return
}
size += 4
n, err := w.Write([]byte(g.Name))
size += int64(n)
if err != nil {
return
}
err = binary.Write(w, binary.LittleEndian, int64(g.AgeYears))
if err == nil {
size += 4
}
return
}
减少判断错误的嵌套,会使读者看起来更轻松。
# 尽量避免重复 #
上面代码中 WriteTo 方法中的 Write 出现了 3 次,比较重复,精简后如下:
type binWriter struct {
w io.Writer
size int64
err error
}
// Write writes a value to the provided writer in little endian form.
func (w *binWriter) Write(v interface{}) {
if w.err != nil {
return
}
if w.err = binary.Write(w.w, binary.LittleEndian, v); w.err == nil {
w.size += int64(binary.Size(v))
}
}
使用 binWriter 结构体。
func (g *Gopher) WriteTo(w io.Writer) (int64, error) {
bw := &binWriter{w: w}
bw.Write(int32(len(g.Name)))
bw.Write([]byte(g.Name))
bw.Write(int64(g.AgeYears))
return bw.size, bw.err
}
type-switch 处理不同类型
func (w *binWriter) Write(v interface{}) {
if w.err != nil {
return
}
switch v.(type) {
case string:
s := v.(string)
w.Write(int32(len(s)))
w.Write([]byte(s))
case int:
i := v.(int)
w.Write(int64(i))
default:
if w.err = binary.Write(w.w, binary.LittleEndian, v); w.err == nil {
w.size += int64(binary.Size(v))
}
}
}
func (g *Gopher) WriteTo(w io.Writer) (int64, error) {
bw := &binWriter{w: w}
bw.Write(g.Name)
bw.Write(g.AgeYears)
return bw.size, bw.err
}
type-switch 精简
摒弃了上面代码的 v.(string) 、v.(int) 类型反射使用。
func (w *binWriter) Write(v interface{}) {
if w.err != nil {
return
}
switch x := v.(type) {
case string:
w.Write(int32(len(x)))
w.Write([]byte(x))
case int:
w.Write(int64(x))
default:
if w.err = binary.Write(w.w, binary.LittleEndian, v); w.err == nil {
w.size += int64(binary.Size(v))
}
}
}
进入不同分支,x 变量对应的就是该分支的类型。
# 自行决定是否写入 #
type binWriter struct {
w io.Writer
buf bytes.Buffer
err error
}
// Write writes a value to the provided writer in little endian form.
func (w *binWriter) Write(v interface{}) {
if w.err != nil {
return
}
switch x := v.(type) {
case string:
w.Write(int32(len(x)))
w.Write([]byte(x))
case int:
w.Write(int64(x))
default:
w.err = binary.Write(&w.buf, binary.LittleEndian, v)
}
}
// Flush writes any pending values into the writer if no error has occurred.
// If an error has occurred, earlier or with a write by Flush, the error is
// returned.
func (w *binWriter) Flush() (int64, error) {
if w.err != nil {
return 0, w.err
}
return w.buf.WriteTo(w.w)
}
func (g *Gopher) WriteTo(w io.Writer) (int64, error) {
bw := &binWriter{w: w}
bw.Write(g.Name)
bw.Write(g.AgeYears)
return bw.Flush()
}
WriteTo 方法中,分了两大部分,增加了灵活性:
- 组装信息
- 调用 Flush 方法来决定是否写入 w。
函数适配器
func init() {
http.HandleFunc(“/“, handler)
}
func handler(w http.ResponseWriter, r *http.Request) {
err := doThis()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
log.Printf(“handling %q: %v”, r.RequestURI, err)
return
}
err = doThat()<br /> if err != nil {<br /> http.Error(w, err.Error(), http.StatusInternalServerError)<br /> log.Printf("handling %q: %v", r.RequestURI, err)<br /> return<br /> }<br />}
函数 handler 包含了业务的逻辑和错误处理,下来将错误处理单独写一个函数处理,代码修改如下:
func init() {
http.HandleFunc(“/“, errorHandler(betterHandler))
}
func errorHandler(f func(http.ResponseWriter, http.Request) error) http.HandlerFunc {
return func(w http.ResponseWriter, r http.Request) {
err := f(w, r)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
log.Printf(“handling %q: %v”, r.RequestURI, err)
}
}
}
func betterHandler(w http.ResponseWriter, r *http.Request) error {
if err := doThis(); err != nil {
return fmt.Errorf(“doing this: %v”, err)
}
if err := doThat(); err != nil {<br /> return fmt.Errorf("doing that: %v", err)<br /> }<br /> return nil<br />}
组织你的代码
1. 先写最重要的
许可信息、构建信息、包文档。
import 语句:相关联组使用空行分隔。
import (
“fmt”
“io”
“log”
"golang.org/x/net/websocket"<br />)
2. 文档注释
包名前的相关文档。
// Package playground registers an HTTP handler at “/compile” that
// proxies requests to the golang.org playground service.
package playground
Go 语言中的标示符(变量、结构体等等)在 godoc 导出的文章中应该被正确的记录下来。
// Author represents the person who wrote and/or is presenting the document.
type Author struct {
Elem []Elem
}
// TextElem returns the first text elements of the author details.
// This is used to display the author’ name, job title, and company
// without the contact details.
func (p *Author) TextElem() (elems []Elem) {
扩展:
使用 godoc 工具在网页上查看 go 项目文档。
# 安装
go get golang.org/x/tools/cmd/godoc
# 启动服务
godoc -http=:6060
3. 命名尽可能简洁
或者说,长命名不一定好。
尽可能找到一个可以清晰表达的简短命名,例如:
- MarshalIndent 比 MarshalWithIndentation 好。
不要忘了,在调用包内容时,会先写包名。
标准包 net/http 总共 15734 行代码,被拆分到 47 个文件中。
- 拆分代码和测试。
net/http/cookie.go 和 net/http/cookie_test.go 文件都放置在 http 包下。
测试代码只有在测试时才被编译。
- 拆分包文档
当在一个包内有多个文件时,按照惯例,创建一个 doc.go 文件编写包的文档描述。
个人思考:当一个包的说明信息比较多时,可以考虑创建 doc.go 文件。
5. 使用 go get 可获取你的包
当你的包被提供使用时,应该清晰的让使用者知道哪些可复用,哪些不可复用。
所以,当一些包可能会被复用,有些则不会的情况下怎么做?
例如:定义一些网络协议的包可能会复用,而定义一些可执行命令的包则不会。
- cmd 可执行命令的包,不提供复用
- pkg 可复用的包
个人思考:如果一个项目中的可执行入口比较多,建议放置在 cmd 目录中,而对于 pkg 目录目前是不太建议,所以不用借鉴。
# API #
1. 了解自己的需求
我们继续使用之前的 Gopher 类型。
type Gopher struct {
Name string
AgeYears int
}
我们可以定义这个方法。
func (g Gopher) WriteToFile(f os.File) (int64, error) {
但方法的参数使用具体的类型时会变得难以测试,因此我们使用接口。
func (g *Gopher) WriteToReadWriter(rw io.ReadWriter) (int64, error) {
并且,当使用了接口后,我们应该只需定义我们所需要的方法。
func (g *Gopher) WriteToWriter(f io.Writer) (int64, error) {
2. 保持包的独立性
import (
“golang.org/x/talks/content/2013/bestpractices/funcdraw/drawer”
“golang.org/x/talks/content/2013/bestpractices/funcdraw/parser”
)
// Parse the text into an executable function.
f, err := parser.Parse(text)
if err != nil {
log.Fatalf(“parse %q: %v”, text, err)
}
// Create an image plotting the function.
m := drawer.Draw(f, width, height, xmin, xmax)
// Encode the image into the standard output.
err = png.Encode(os.Stdout, m)
if err != nil {
log.Fatalf(“encode image: %v”, err)
}
代码中 Draw 方法接受了 Parse 函数返回的 f 变量,从逻辑上看 drawer 包依赖 parser 包,下来看看如何取消这种依赖性。
parser 包:
type ParsedFunc struct {
text string
eval func(float64) float64
}
func Parse(text string) (*ParsedFunc, error) {
f, err := parse(text)
if err != nil {
return nil, err
}
return &ParsedFunc{text: text, eval: f}, nil
}
func (f ParsedFunc) Eval(x float64) float64 { return f.eval(x) }
func (f ParsedFunc) String() string { return f.text }
drawer 包:
import (
“image”
"golang.org/x/talks/content/2013/bestpractices/funcdraw/parser"<br />)
// Draw draws an image showing a rendering of the passed ParsedFunc.
func DrawParsedFunc(f parser.ParsedFunc) image.Image {
使用接口类型,避免依赖。
import “image”
// Function represent a drawable mathematical function.
type Function interface {
Eval(float64) float64
}
// Draw draws an image showing a rendering of the passed Function.
func Draw(f Function) image.Image {
测试:接口类型比具体类型更容易测试。
package drawer
import (
“math”
“testing”
)
type TestFunc func(float64) float64
func (f TestFunc) Eval(x float64) float64 { return f(x) }
var (
ident = TestFunc(func(x float64) float64 { return x })
sin = TestFunc(math.Sin)
)
func TestDrawIdent(t *testing.T) {
m := Draw(ident)
// Verify obtained image._
4. 避免在内部使用并发
func doConcurrently(job string, err chan error) {
go func() {
fmt.Println(“doing job”, job)
time.Sleep(1 * time.Second)
err <- errors.New(“something went wrong!”)
}()
}
func main() {
jobs := []string{“one”, “two”, “three”}
errc := make(chan error)<br /> for _, job := range jobs {<br /> doConcurrently(job, errc)<br /> }<br /> for _ = range jobs {<br /> if err := <-errc; err != nil {<br /> fmt.Println(err)<br /> }<br /> }<br />}
如果这样做,那如果我们想同步调用 doConcurrently 该如何做?
func do(job string) error {
fmt.Println(“doing job”, job)
time.Sleep(1 * time.Second)
return errors.New(“something went wrong!”)
}
func main() {
jobs := []string{“one”, “two”, “three”}
errc := make(chan error)<br /> for _, job := range jobs {<br /> go func(job string) {<br /> errc <- do(job)<br /> }(job)<br /> }<br /> for _ = range jobs {<br /> if err := <-errc; err != nil {<br /> fmt.Println(err)<br /> }<br /> }<br />}
对外暴露同步的函数,这样并发调用时也是容易的,同样也满足同步调用。
# 最佳的并发实践 #
1. 使用 Goroutine 管理状态
Goroutine 之间使用一个 “通道” 或带有通道字段的 “结构体” 来通信。
type Server struct{ quit chan bool }
func NewServer() *Server {
s := &Server{make(chan bool)}
go s.run()
return s
}
func (s *Server) run() {
for {
select {
case <-s.quit:
fmt.Println(“finishing task”)
time.Sleep(time.Second)
fmt.Println(“task done”)
s.quit <- true
return
case <-time.After(time.Second):
fmt.Println(“running task”)
}
}
}
func (s *Server) Stop() {
fmt.Println(“server stopping”)
s.quit <- true
<-s.quit
fmt.Println(“server stopped”)
}
func main() {
s := NewServer()
time.Sleep(2 * time.Second)
s.Stop()
}
2. 使用带缓冲的通道避免 Goroutine 泄露
func sendMsg(msg, addr string) error {
conn, err := net.Dial(“tcp”, addr)
if err != nil {
return err
}
defer conn.Close()
_, err = fmt.Fprint(conn, msg)
return err
}
func main() {
addr := []string{“localhost:8080”, “http://google.com"}
err := broadcastMsg(“hi”, addr)
time.Sleep(time.Second)
if err != nil {<br /> fmt.Println(err)<br /> return<br /> }<br /> fmt.Println("everything went fine")<br />}
func broadcastMsg(msg string, addrs []string) error {
errc := make(chan error)
for _, addr := range addrs {
go func(addr string) {
errc <- sendMsg(msg, addr)
fmt.Println(“done”)
}(addr)
}
for _ = range addrs {<br /> if err := <-errc; err != nil {<br /> return err<br /> }<br /> }<br /> return nil<br />}
这段代码有个问题,如果提前返回了 err 变量,errc 通道将不会被读取,因此 Goroutine 将会阻塞。
总结:
- 在写入通道时 Goroutine 被阻塞。
- Goroutine 持有对通道的引用。
- 通道不会被 gc 回收。
使用缓冲通道解决 Goroutine 阻塞问题。
func broadcastMsg(msg string, addrs []string) error {
errc := make(chan error, len(addrs))
for _, addr := range addrs {
go func(addr string) {
errc <- sendMsg(msg, addr)
fmt.Println(“done”)
}(addr)
}
for _ = range addrs {<br /> if err := <-errc; err != nil {<br /> return err<br /> }<br /> }<br /> return nil<br />}
如果我们不能预知通道的缓冲大小,也称容量,那该怎么办?
创建一个传递退出状态的通道来避免 Goroutine 的泄露。
func broadcastMsg(msg string, addrs []string) error {
errc := make(chan error)
quit := make(chan struct{})
defer close(quit)
for _, addr := range addrs {<br /> go func(addr string) {<br /> select {<br /> case errc <- sendMsg(msg, addr):<br /> fmt.Println("done")<br /> case <-quit:<br /> fmt.Println("quit")<br /> }<br /> }(addr)<br /> }
for _ = range addrs {<br /> if err := <-errc; err != nil {<br /> return err<br /> }<br /> }<br /> return nil<br />}
参考
原文链接:https://talks.golang.org/2013/bestpractices.slide#1
视频链接:https://www.youtube.com/watch?v=8D3Vmm1BGoY