Goに入ってはGoに従え

Go Conference 2014 autumn

30 November 2014

鵜飼文敏

Fumitoshi Ukai

Google Software Engineer - Chrome Infra team

Go Readability Approver

Go言語のReadabilityをレビューするチーム

Gopher by Renée French

Readabilityスキルとは?

プログラミング言語のリテラシー

作法にかなったやりかた で、読んだり書いたりできる能力

言語ごとに作法が違う
C++などはプロジェクトごとに作法が違う

C++/Java/Pythonで書くようにGo言語のコードを書いても駄目。

Go言語のコードは

" Want to understand something in google servers? Read the Go implementation! "

by some Googler
Gopher by Renée French

優れたツール

go fmt - Goの標準フォーマットに変換
go vet - 間違いやすいコードを指摘
golint - スタイルの問題を指摘
godoc - ドキュメンテーション

ツールだけでは十分ではない

読みやすいコード == 情報が認識しやすい/脳に負担がかからない
書き手/読み手、双方が言語特有の作法・慣用表現を理解する必要がある
Go言語はシンプル(言語仕様は50ページほど)

Gopher by Renée French

Readability Reviews

Gopher by Renée French, and tenntenn

ミス/バグ

errorチェック

元のコード

var whitespaceRegex, _ = regexp.Compile("\\s+")

修正後

var whitespaceRegex = regexp.MustCompile(`\s+`)

errorチェック: 元のコード

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
}

errorチェック: 修正後

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
}

値とerrorをまぜない: 元のコード

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.Duation(durationUsec) * time.Microsecond
    }
    return -3
}

値とerrorをわけてかえす: 修正後

var (
    ErrDurationUnterminated = errors.new("duration: unterminated")
    ErrNoDuration           = errors.New("duration: not found")
    ErrNoIteration          = errors.New("duration: not interation")
)

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
}

エラーはerrorでかえす

errorの設計

エラー処理の区別が必要ない場合: err != nil チェックのみ

fmt.Errorf("error in %s", val) もしくは errors.New("error msg")

いくつかのエラーコードがあって区別したい場合

var (
  ErrInternal   = errors.New("foo: inetrnal error")
  ErrBadRequest = errors.New("foo: bad request")
)

エラーにいろいろな情報を含めたい場合

type FooError struct { /* エラー情報のフィールド */ }
func (e *FooError) Error() string { return エラーメッセージ }

&FooError{ /* エラー情報 */ }

panicは使わない

nil error

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は2つの情報をもつ。interfaceの値がnil == 両方がnilのとき

interfaceの埋め込み: 元のコード

// Column writer implements the scan.Writer interface.
type ColumnWriter struct {
    scan.Writer
    tmpDir      string
    // some other fields
}

interfaceのチェック: 修正後

// ColumnWriter is a writer to write ...
type ColumnWriter struct {
    tmpDir string
    // some other fields
}

var _ scan.Writer = (*ColumnWriter)(nil)

interfaceの埋め込み

structがinterfaceのメソッドを明示的に定義せず、interfaceをstructに埋め込んで、interfaceの値をセットしなかった(nil)場合、interfaceのメソッドを呼びだすとpanicする

import "fmt"

type I interface {
    Key() string
    Value() string
}
type S struct{ I }  // Sは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
}

testで一部のメソッドだけを実装したい時は便利

見やすく

structフィールドのレイアウト: 元のコード

type Modifier struct {
    pmod          *profile.Modifier
    cache         map[string]time.Time
    client        *client.Client
    mu            sync.RWMutex
}

structフィールドのレイアウト: 修正後

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
}

簡潔な名前を使う

与えられたコンテキストの中でわかりやすい名前にする

冗長な名前をさける

一行に

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
}

素直なコードフロー

条件分岐

元のコード

    if _, ok := f.dirs[dir]; !ok {
        f.dirs[dir] = new(feedDir)
    } else {
        f.addErr(fmt.Errorf("..."))
        return
    }
    // some code

修正後

    if _, found := f.dirs[dir]; found {
        f.addErr(fmt.Errorf("..."))
        return
    }
    f.dirs[dir] = new(feedDir)
    // some code

条件分岐 (2) 元のコード

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)
    }
}

条件分岐 (2) 修正後

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)
}

条件分岐 (3): 元のコード

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"
    }
}

条件分岐 (3): 修正後

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"
    }
}

シンプルに

time.Duration

期間を表すのはintなどよりtime.Duration (flag.Duration)を使うべき

元のコード

var rpcTimeoutSecs = 30 // Thirty seconds
var rpcTimeout = time.Duration(30 * time.Second) // Thirty seconds
var rpcTimeout = time.Duration(30) * time.Second // Thirty seconds

修正後

var rpcTimeout = 30 * time.Second

sync.Mutexとsync.Cond: 元のコード

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
}

chan: 修正後

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
    }
}

reflect: 元のコード

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)
        }
    }

reflectなし: 修正後

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)
        }
    }

テスト

テストコード

    // 典型的なテストコード
    if got, want := テスト対象(input), 期待値; !テスト(got, want) {
        t.Errorf("テスト対象(%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
}

コメント

コメントのつけかた

packageコメントを書く。 mainパッケージはコマンドのコメント。
Exportしている名前にはコメントをつける
コメントは対象としているものの名前からはじまる文にする

// 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) {

godoc で確認

$ godoc bytes Buffer

$ godoc -http=:6060  # http://localhost:6060/pkg

コメントがわかりにくい/うまく書けない時はAPIの設計を考えなおしたほうがいい。

APIデザイン

適切な名前のpackageをつくることが大事

APIはシンプルに

読みやすいコードを書くには

コードはコミュニケーション

明瞭に表現すること

Gopher by Renée French

コードを書くときは

をこころがけましょう

参考文献

Gopher by Renée French

Thank you

鵜飼文敏

Fumitoshi Ukai

Google Software Engineer - Chrome Infra team

Use the left and right arrow keys or click the left and right edges of the page to navigate between slides.
(Press 'H' or navigate to hide this message.)