- 最佳实践的定义
- 代码样例
- 1. Avoid nesting by handing errors first
- 2. Avoid repetition when possible
- 3. Important code goes first
- 4. Document your code
- 5. Shorter is better
- 6. Packages with multiple files
- 7. Make your packages "go getable"
- 8. Ask for what you need
- 9. Keep independent packages independent
- 10. Avoid concurrency in your API
- 11. Use goroutines to manage state
- 12. Avoid goroutine leaks
Best practices
From Wikipedia:
"A best practice is a method or technique that has consistently shown results superior to those achieved with other means"
- simple
- readable
- maintainable
Code sample
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
}
1. 先处理错误,来避免嵌套(avoid nesting by handling errors first)
fun (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
}
2. 避免重复(avoid repetition when possible)
type binWriter struct {
w io.Writer
size int64
err error
}
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))
}
}
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 to handle special cases)
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 with short variable declaration)
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))
}
}
}
Write everything or nothing
type binWriter struct {
w io.Writer
buf bytes.Buffer
err error
}
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()
}
适配器(Function adapters)
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()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
log.Printf("handling %q: %v", r.RequestURI, err)
return
}
}
Better
func init() {
http.HandleFunc("/", errorHandler(betterHandler))
}
func errorHandler(f func(http.RequestWriter, *http.Request) error) http.HandleFunc {
return func(w http.ResponseWriter, r *http.Request) {
err := f(w, r)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
log.Printf("handing %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 {
return fmt.Errorf("doing that: %v", err)
}
return nil
}
3. 优先重要的代码(Important code goes first)
- license information
- build tags
- package documentation
- import statements, related groups separated by blank lines
import (
"fmt"
"io"
"log"
"golang.org/x/net/websocket"
)
4. Document your code
package name
// Package playground registers an HTTP handler at "/complie" that
// proxies requests to the golang.org playground service
package playground
exported identifiers appear in godoc, they should be documented correctly
// 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) {
5. Shorter is better
or at least longer is not always better
- Try to find the shortest name that is self explanatory
- Prefer MarshaIndent to MarshalWithIndentation
- Don't forget that the package name will appear before the identifier you chose
- In package encoding/json we find the type Encoder, not JSONEncoder
- It is referred as json.Encoder
也就是说在 encoding/json 里,Encoder 为啥不需要被称作 JSONEncoder呢,是因为调用这个方法的时候,是 json.Encoder,没必要 json.JSONEncoder
6. Packages with multiple files
- 避免大文件,所以要拆分
- 测试代码分离
- package 里包含多个文件时,要写一个 doc.go 文件作为文档说明
7. Make your packages "go getable"
example
github.com/peterbourgon/foo/
circle.yml
Dockerfile
cmd/
foosrv/
main.go
foocli/
main.go
pkg/
fs/
fs.go
fs_test.go
mock.go
mock_test.go
merge/
merge.go
merge_test.go
api/
api.go
api_test.go
8. Ask for what you need
相比于写一个具体的类型,使用接口(interface{})更方便测试
// 不方便测试
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) {
9. Keep independent packages independent
代码样例
import (
"golang.org/x/talks/2013/bestpractices/funcdraw/drawer"
"golang.org/x/talks/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)
}
拆分成
Parsing
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 }
Drawing
import (
"image"
"golang.org/x/talks/2013/bestpractices/funcdraw/parser"
)
// Draw draws an image showing a rendering of the passed Function
func DrawParsedFunc(f parser.ParsedFunc) image.Image {
Avoid dependency by using an interface
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 {
testing
用接口而不是一个实际类型的好处也包括方便测试
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 TestDraw_Ident(t *testing.T) {
m := Draw(ident)
// Verify obtained image.
...
10. Avoid concurrency in your API
API里就不用并行了
以下是不好的例子
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)
for _, job := range jobs {
doConcurrently(job, errc)
}
for _ = range jobs {
if err := <-errc; err != nil {
fmt.Println(err)
}
}
}
以下是好的例子
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)
for _, job := range jobs {
go func(job string) {
errc <- do(job)
}(job)
}
for _ = range jobs {
if err := <-errc; err != nil {
fmt.Println(err)
}
}
}
开发同步的API,这样并行调用它们是很简单的。
11. Use goroutines to manage state
Use a chan or a struct with a chan to communicate with a goroutine
package main
import (
"fmt"
"time"
)
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()
}
12. Avoid goroutine leaks
Use a chan or a struct with a chan to communicate with a 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 {
fmt.Println(err)
return
}
fmt.Println("everything went fine")
}
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 {
if err := <- errc; err != nil {
return err
}
}
return nil
}
上面的问题是:
- the goroutine is blocked on the chan write
- the goroutine holds a reference to the chan
- the chan will never be garbage collected
修正:(给 errc 加了一个长度)
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 {
if err := <- errc; err != nil {
return err
}
}
return nil
}
这里还有个问题,如果我们不知道 addrs 的长度,或者说无法预测 errc 的长度,要怎么办?
改进:(引入 quit channal)
func broadcastMsg(msg string, addrs []string) error {
errc := make(chan error)
quit := make(chan struct{})
defer close(quit)
for _, addr := range addrs {
go func(addr string) {
select {
case errc <- sendMsg(msg, addr):
fmt.Println("done")
case <-quit:
fmt.Println("quit")
}
}(addr)
}
for _ = range addrs {
if err := <- errc; err != nil {
return err
}
}
return nil
}
这里使用了 select 来block channel
The select statement lets a goroutine wait on multiple communication operations.
A select blocks until one of its cases can run, then it executes that case. It chooses one at random if multiple are ready.
网友评论