When in Go, do as Gophers do
Go Conference 2014 autumn
30 November 2014
Fumitoshi Ukai
Google Software Engineer - Chrome Infra team
Fumitoshi Ukai
Google Software Engineer - Chrome Infra team
A team to review Go readability.
Literacy of a programming language.
A skill to read or write idiomatic code.
Each programming language has its own preferred style.
In C++, each project chooses a preferred style.
Don't write Go code as you write code in C++/Java/Python.
Write Go code as Gophers write code.
" Want to understand something in google servers? Read the Go implementation! "
go fmt - format Go programs.
go vet - report suspicious code
golint - report coding style errors.
godoc - browse documentation
Readable code == easy to recognize, less burden for brain.
Both writer and reader should have readability skills.
Go is very simple (lang spec is about 50 pages)
original code
var whitespaceRegex, _ = regexp.Compile("\\s+")
revised
var whitespaceRegex = regexp.MustCompile(`\s+`)
var
or init()
).func run() error { in, err := os.Open(*input) if err != nil { return err } defer in.Close() out, err := os.Create(*output) if err != nil { return err } defer out.Close() // some code }
func run() (err error) { in, err := os.Open(*input) if err != nil { return err } defer in.Close() out, err := os.Create(*output) if err != nil { return err } defer func() { if cerr := out.Close(); err == nil { err = cerr } }() // some code }
func proc(it Iterator) (ret time.Duration) { d := it.DurationAt() if d == duration.Unterminated { ret = -1 } else { ret = d } // some code }
// duration.Unterminated = -1 * time.Second func (it Iterator) DurationAt() time.Duration { // some code switch durationUsec := m.GetDurationUsec(); durationUsec { case -1: return duration.Unterminated case -2: return -2 default: return time.Duration(durationUsec) * time.Microsecond } return -3 }
var ( ErrDurationUnterminated = errors.new("duration: unterminated") ErrNoDuration = errors.New("duration: not found") ErrNoIteration = errors.New("duration: no iteration") ) func (it Iterator) DurationAt() (time.Duration, error) { // some code switch durationUsec := m.GetDurationUsec(); durationUsec { case -1: return 0, ErrDurationUnterminated case -2: return 0, ErrNoDuration default: return time.Duation(durationUsec) * time.Microsecond, nil } return 0, ErrNoIteration }
Return error as error, not as some value
13
If client doesn't need to distinguish errors, e.g. ok with err
!=
nil
check only.
fmt.Errorf("error in %s", val) or errors.New("error msg")
If client wants to distinguish several errors by error code.
var ( ErrInternal = errors.New("foo: internal error") ErrBadRequest = errors.New("foo: bad request") )
If you want to put detailed information in error.
type FooError struct { /* fields of error information */ } func (e *FooError) Error() string { return /* error message */ } &FooError{ /* set error data */ }
Don't use panic
.
But when you do, use it only within the package, and return error with catching it by recover.
import "log" type FooError struct{} func (e *FooError) Error() string { return "foo error" } func foo() error { var ferr *FooError // ferr == nil return ferr } func main() { err := foo() if err != nil { log.Fatal(err) } }
FAQ: Why is my nil error value not equal to nil?
interface has 2 data (type and value). interface value is nil == both are nil.
15// Column writer implements the scan.Writer interface. type ColumnWriter struct { scan.Writer tmpDir string // some other fields }
scan.Writer
is an interface.ColumnWriter
will have methods of the scan.Writer
interface (i.e. ColumnWriter
implements the scan.Writer
interface), but ...// ColumnWriter is a writer to write ... type ColumnWriter struct { tmpDir string // some other fields } var _ scan.Writer = (*ColumnWriter)(nil)
ColumnWriter
implements the scan.Writer
interface.If a struct doesn't have a method of a interface explicitly, the interface is embedded in the struct, and you didn't set the interface field to a concrete value (i.e. the interface field value is nil), the method call will panic.
import "fmt" type I interface { Key() string Value() string } type S struct{ I } // S has method sets of I. func (s S) Key() string { return "type S" } func main() { var s S fmt.Println("key", s.Key()) fmt.Println(s.Value()) // runtime error: invalid memory address or nil pointer deference }
It would be useful in a test when you want to implement only a subset of methods in the huge interface.
18type Modifier struct { pmod *profile.Modifier cache map[string]time.Time client *client.Client mu sync.RWMutex }
type Modifier struct { client *client.Client mu sync.RWMutex pmod *profile.Modifier cache map[string]time.Time }
package sampling import ( servicepb "foo/bar/service_proto" ) type SamplingServer struct { // some fields } func (server *SamplingServer) SampleMetrics( sampleRequest *servicepb.Request, sampleResponse *servicepb.Response, latency time.Duration) { // some code }
package sampling import ( servicepb "foo/bar/service_proto" ) type SamplingServer struct { // some fields } func (server *SamplingServer) SampleMetrics(sampleRequest *servicepb.Request, sampleResponse *servicepb.Response, latency time.Duration) { // some code }
Choose good name in the context
Short and accurate names.
Server
, which clients will write as sampling.Server
.i
to index
, r
to reader
.package sampling import ( spb "foo/bar/service_proto" ) type Server struct { // some fields } func (s *Server) SampleMetrics(req *spb.Request, resp *spb.Response, latency time.Duration) { // some code }
original code
if _, ok := f.dirs[dir]; !ok { f.dirs[dir] = new(feedDir) } else { f.addErr(fmt.Errorf("...")) return } // some code
revised
if _, found := f.dirs[dir]; found { f.addErr(fmt.Errorf("...")) return } f.dirs[dir] = new(feedDir) // some code
func (h *RESTHandler) finishReq(op *Operation, req *http.Request, w http.ResponseWriter) { result, complete := op.StatusOrResult() obj := result.Object if complete { status := http.StatusOK if result.Created { status = http.StatusCreated } switch stat := obj.(type) { case *api.Status: if stat.Code != 0 { status = stat.Code } } writeJSON(status, h.codec, obj, w) } else { writeJSON(http.StatusAccepted, h.codec, obj, w) } }
func finishStatus(r Result, complete bool) int { if !complete { return http.StatusAccepted } if stat, ok := r.Object.(*api.Status); ok && stat.Code != 0 { return stat.Code } if r.Created { return http.StatusCreated } return http.StatusOK } func (h *RESTHandler) finishReq(op *Operation, w http.ResponseWriter, req *http.Request) { result, complete := op.StatusOrResult() status := finishStatus(result, complete) writeJSON(status, h.codec, result.Object, w) }
func BrowserHeightBucket(s *session.Event) string { browserSize := sizeFromSession(s) if h := browserSize.GetHeight(); h > 0 { browserHeight := int(h) if browserHeight <= 480 { return "small" } else if browserHeight <= 640 { return "medium" } else { return "large" } } else { return "null" } }
func BrowserHeightBucket(s *session.Event) string { size := sizeFromSession(s) h := size.GetHeight() switch { case h <= 0: return "null" case h <= 480: return "small" case h <= 640: return "medium" default: return "large" } }
Use time.Duration (flag.Duration) rather than int
or float
to represent time duration.
original code
var rpcTimeoutSecs = 30 // Thirty seconds
var rpcTimeout = time.Duration(30 * time.Second) // Thirty seconds
var rpcTimeout = time.Duration(30) * time.Second // Thirty seconds
revised
var rpcTimeout = 30 * time.Second
time.Duration
.type Stream struct { // some fields isConnClosed bool connClosedCond *sync.Cond connClosedLocker sync.Mutex } func (s *Stream) Wait() error { s.connClosedCond.L.Lock() for !s.isConnClosed { s.connClosedCond.Wait() } s.connClosedCond.L.Unlock() // some code } func (s *Stream) Close() { // some code s.connClosedCond.L.Lock() s.isConnClosed = true s.connClosedCond.L.Unlock() s.connClosedCond.Broadcast() } func (s *Stream) IsClosed() bool { return s.isConnClosed }
type Stream struct { // some fields cc chan struct{} } func (s *Stream) Wait() error { <-s.cc // some code } func (s *Stream) Close() { // some code close(s.cc) } func (s *Stream) IsClosed() bool { select { case <-s.cc: return true default: return false } }
type Layers struct { UI, Launch /* more fields */ string } layers := NewLayers(s.Entries) v := reflect.ValueOf(*layers) r := v.Type() // type Layers for i := 0; i < r.NumField(); i++ { if e := v.Field(i).String(); e != "-" { eid := &pb.ExperimentId{ Layer: proto.String(r.Field(i).Name()), ExperimentId: &e, } experimentIDs = append(experimentIDs, eid) } }
type LayerExperiment struct{ Layer, Experiment string } func (t *Layers) Slice() []LayerExperiment { return []LayerExperiment{ {"UI", t.UI}, {"Launch", t.Launch}, /* more fields */ } } layers := NewLayers(s.Entries).Slice() for _, l := range layers { if l.Experiment != "-" { eid := &pb.ExperimentId{ Layer: proto.String(l.Layer), ExperimentId: proto.String(l.Experiment), } experimentIDs = append(experimentIDs, eid) } }
// Typical test code if got, want := testTargetFunc(input), expectedValue; !checkTestResult(got, want) { t.Errorf("testTargetFunc(%v) = %v; want %v", input, got, want) }
func ExampleWrite() { var buf bytes.Buffer var pi float64 = math.Pi err := binary.Write(&buf, binary.LittleEndian, pi) if err != nil { fmt.Println("binary.Write failed:", err) } fmt.Printf("% x", buf.Bytes()) // Output: 18 2d 44 54 fb 21 09 40 }
Write package comment. Write command comment in main package.
Write comments on exported names.
Doc comment should be a complete sentence that starts with the name being declared.
// Package math provides basic constants and mathematical functions. package math // A Request represents a request to run a command. type Request struct { .. // Encode writes the JSON encoding of req to w. func Encode(w io.Writer, req *Request) {
Browse with godoc
$ godoc bytes Buffer $ godoc -http=:6060 # http://localhost:6060/pkg
If you feel comments are unclear or hard to write concisely, reconsider your API design.
41Important to choose a good package name.
util
is not good name, since most packages are utilities of something.Make API simple.
Be articulate:
Keep in mind