Go Conference 2014 Autumnレポート

鵜飼文敏氏「Goに入ってはGoに従え」可読性のあるコードにするために~Go Conference 2014 Autumn基調講演2人目

この記事では2人目の鵜飼文敏氏の基調講演についてレポートを書きたいと思います。この基調講演では「Goに入ってはGoに従え」というタイトルで、Go言語らしく書く方法について話がされましたスライド⁠。

写真1 鵜飼文敏氏の講演の様子
写真1 鵜飼文敏氏の講演の様子

Go言語の可読性レビュー

Go Readability Approver

まずはじめに、⁠Go Readability Approver」と呼ばれる、GoogleにおけるGo言語のReadability(可読性)をレビューするチームについて話がありました。このチームは、コードレビューを通じてGo言語の良いコードの書き方を教えることを目的としているそうです。メンバーは、メインのプロジェクトとは別のプロジェクトのコードをレビューするそうで、同氏は1年ほど前に参加し、20%ルールの時間を使って、200ほどのChange Listをレビューしたと述べていました。

「Go Readability Approver」には、⁠Readabilityスキル」が必要とされるそうです。これは、レビューするプログラミング言語のリテラシーを持っていることと、その言語の「作法にかなったやり方」でコードを読んだり書いたりする能力のことであるそうです。同氏は、作法は言語ごとに異なり、さらにC++ではプロジェクトごとに違うと主張していました。たとえば、Chromeの開発ではChromiumの部分は、Googleのスタイルガイドに従って書かれており、Webkit(Blink)の部分は、WebkitBlinkのコーディングスタイルに従って書かれているそうです。そのため同氏は、C++/Java/Pythonで書くようにGo言語のコードを書くと読みにくいコードになり、Go言語の考え方とは違う書き方で書いてしまって、非常に書きづらくなると指摘していました。

可読性のあるGo言語のコード

同氏は、Go言語のコードを書くうえで、言語の単純さをうまく使って書くことが大切であると主張していました。そして、Go言語で書くと簡単に以下のようなコードにできると述べていました。

  • 明瞭・簡潔
  • 使いやすいAPI
  • 適切なコメント
  • 素直なコードフロー

特にゴルーチンを使った並行プログラミングは、C++などで書いた場合に比べて、非常に読みやすいコードフローになると述べていました。このようにGo言語で書かれたコードは単純で読みやすいコードになりやすく、とあるGoogler(Googleの社員)は、Googleが提供するサービスで、サーバで何が行われるかを調べるときは、Go言語での実装だと余計なコードが少なく、本質が一番わかりやすいと述べていたそうです。

Go言語で読みやすいコードを書くための優れたツールとして、以下のツールが紹介されていました。

  • go fmt:コードをGo言語の標準フォーマットに変換するツール
  • go vet:間違えやすいコードを指摘するツール
  • golint:スタイルの問題を指摘するツール
  • godoc:コードからAPIドキュメントを作るツール

しかし同氏は、読みやすい(情報を認識しやすい/脳に負担がかからない)コードを書くには、これらのツールに頼るだけでは不十分で、書き手と読み手の双方が言語特有の手法・慣用表現を理解する必要があると主張していました。また、Go言語の言語仕様はシンプル(50ページほど)であるため、言語仕様の理解はそんなに大変ではないと述べていました。

この講演では、以下の点に着目してレビューを行い、実際に指摘したことについて説明がされました。

  • ミス/バグはないか
  • 見やすくレイアウトされているか
  • コードフローはわかりやすいか
  • APIはわかりやすいか
写真2 レビューのポイント
写真2 レビューのポイント

エラー

Must-を使った初期化

Go言語では、戻り値の最後にエラーを取ることが一般的で、そのエラーは必ず受け取る必要があります。しかし、_で受け取ることで、エラー処理を行わなくて済むようになります。同氏が行ったレビューにおいても、このようなコードを見かけることがあるそうで、エラーは必ずチェックする必要があると指摘していました。たとえば、正規表現をコンパイルするregexp.Compile関数も第2戻り値にエラーを返しますが、以下のように_で受け取ることでエラー処理を回避できます。

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

同氏は、上のコードは以下のコードのように修正すべきだと述べていました。

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

同氏は、パッケージ変数をvarinit()関数で初期化する場合などは、エラー処理を回避するのではなく、regexp.MustCompileなどを使用するべきだと主張していました。一方、関数やメソッドの内では戻り値のerrorを受け取って適切に処理すべきだと述べていました。

このように、標準パッケージが提供する関数で最後の戻り値にエラーを返すものの一部に対して、Mustで始まる関数が用意されている場合があります。Mustで始まる関数は、内部で対応するerrorを返す関数を呼び出し、戻り値のerrornilではない場合は、panicを起こすように定義されています。

また、上記の例では、文字列のエスケープで正規表現が読みにくいため、raw string literalを使うべきだと指摘されていました。

deferにおけるエラー処理

以下のように、deferを使ってファイルをCloseする場合があると思います。deferを使えば、関数からリターンするときにCloseを呼び出すことができます。

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
}

このとき、読み込むために開いたファイルの場合はそのままdefer in.Close()のように閉じてしまっても問題ないですが、書き込みのために開いたファイルの場合はそのまま閉じると、うまく書き込めなかった場合などにエラーが適切に処理できません。そのため、同氏は以下のように修正できると述べていました。

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
}

deferの中で戻り値であるerrnilである場合に、Closeで発生したエラーを代入することで、そのエラーを戻り値として返すことができます。そして、some code以下でエラーがあった場合は、戻り値のerr代入することでそのエラーを返すことができます。しかし、deferを使うと処理が複雑になることがあるため必ずしもdeferを使えば良いというわけではないとのことでした。上記の例では、some code以下がシンプルな場合はdeferを使わず、関数の最後でCloseを呼び出してエラーをチェックするほうがシンプルになると同氏は述べていました。

値とエラーを混ぜない

値としてエラーを使用している場合の例として以下のコードが挙げられていました。

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
}

このコードでは、C言語やC++のコードのように特別な値をエラーとして扱っています。具体的には、time.Durationは期間を表す型なので、マイナスの値をエラーとして扱っています。Go言語では複数の戻り値を返すことができます。そのため同氏は、エラーを値に混ぜて返すのではなく、最後の戻り値としてerror型で返すべきだと主張していました。

エラーの設計

エラー処理の方法によって、エラーの設計方法が分けられるそうです。エラー処理の際に、エラーの区別が必要なく、単にerr != nilでエラーの有無をチェックする場合は以下のようにfmt.Errorferrors.Newを使用すると良いそうです。

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

また、エラーにはいくつか種類があり、それを区別する必要がある場合は以下のように変数に代入しておいて使用するほうが良いそうです。なお、このときの変数名はErr-が一般的なようです。

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

error型はErrorメソッドを持つインターフェースです。そのため同氏は、エラーにいろいろな情報を含めたい場合は、構造体でそれらの情報を定義し、Errorメソッドを実装することでerrorとして使用することができると述べていました。なお、このときのエラー型の名前は-Errorという名前を使用するのが一般的らしいです。

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

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

同氏は、Errorメソッドのレシーバをポインタにした場合、nilとの比較に注意すべきだと指摘していました。インターフェースは値と型情報を持ち、その両方がnilの場合にnilとなります。そのため、以下のコードはif err != nil {trueとなります(参考:FAQ: Why is my nil error value not equal to nil?⁠。

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

また同氏は、panicは極力使わないようにすべきだと主張していました。どうしてもpanicを使いたい場合は、recoverを使ってパッケージ外に出るときはerrorにすべきだと述べていました。

インターフェースの埋込み

以下のコードはscan.Writerインターフェースを埋め込むことでをColumnWriterscan.Writerインターフェースを実装していることを明示的にしようとしています。

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

たしかにscan.Writerを埋め込むことで、ColumnWriterscan.Writerを実装していることになります。しかし、初期化時にscan.Writerを埋め込む必要があるうえにこれではColumnWriterscan.Writerを本当に実装しているかどうかの保証にはなりません。同氏は、以下のようにscan.Writer型に代入することで、*ColumnWriterscan.Writerを実装しているかどうか、コンパイル時にチェックすることができると述べています。この方法であれば、*ColumnWriterscan.Writerインターフェースを実装していない場合には、コンパイルエラーとなります。なお、_で代入を受けているため、実際には変数代入は行われず、型チェックだけ行うことができます。

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

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

なお、構造体がインターフェースの定義するメソッドを明示的に実装せず、さらに構造体にインターフェースを埋め込んで、その値を設定しなかった場合nilの場合⁠⁠、そのメソッドを呼び出すと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
}

同氏は、この挙動はテストで一部のメソッドだけ実装したいときは便利だと述べていました。

コードを見やすくする

構造体のフィールドのレイアウト

以下のように、フィールドの並び順を意識せずに構造体を定義してしまうことがあると思います。しかし同氏は、この定義の仕方だとmuがどのフィールドを保護しているのかわからないと指摘していました。

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

そのため、フィールドリストは関連が深いものをブロックに分け、sync.Mutexはそれが保護しているフィールドのブロックの先頭に置くとわかりやすいとのことでした。

type Modifier struct {
    client        *client.Client

    mu            sync.RWMutex
    pmod          *profile.Modifier
    cache         map[string]time.Time
}

長い行は簡潔な変数名を使って短く

以下のように、関数の引数が多くなり1行が長くなってしまうことがあると思います。

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
}

同氏は、Go言語には行の長さには制限がないため、コードをgrepすることを考えると1行にする方が良いと述べていました。

しかし同氏は、この場合であれば引数に簡潔な名前を用いることで、1行を短くすることは可能であると述べていました。また変数名などは、与えられたコンテキストの中でわかりやすい名前にすべきで、長い名前が必ずしもわかりやすい名前ではないと指摘していました。そして、以下のように冗長な名前は避けるべきだと述べていました。

  • samplingパッケージのSamplingServerは冗長である。
    • Serverという名前を付ければ、sampling.Serverとなる。
  • レシーバ変数は数文字でよい
  • 引数も型名から推測できる名前は付ける必要はなく、短くて良い。
  • 基本型の引数の場合は、わかりやすい名前にする。
  • ローカル変数も短くindexよりireaderよりrのほうが良い⁠⁠。
  • ローカル変数の名前を短くしても分かるように関数も小さくする。

上記の1行が長いコードは、名前を簡潔にすることで、以下のように短くなります。

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の条件を入れ替えることでもっと簡潔に書けるそうです。

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

また、マップのキーが存在するかどうかを表す変数はいつでも変数名をokにする必要はなく、否定的な意味で使う場合はokだと不自然であると指摘していました。 この場合であれば、okの代わりにfoundという名前を使い、条件式を逆にすることで、!okfoundにすることができるとのことでした。

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

上記のコードのように、うまく条件式を書くことでelseを使う必要がなく、インデントが少なくなります。

関数の分割

以下のコードでは、条件によってHTTPステータスを変え、結果をJSONで返すという処理をしています。しかし、この書き方では基本のコードパスがわかりづらく、どういうときにどういうHTTPステータスが返されるのかわかりにくくなっていると、同氏は指摘していました。

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

そこで、ステータスを決める関数finishStatusを別に作ることで、HTTPステータスを決めて、JSONで返すという基本のコードパスをわかりやすくすることができると同氏は主張していました。また、ステータスを決める関数も条件が判定できれば、すぐにリターンすることができるため、非常にわかりやすくなるそうです。

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

switchを使う

以下のコードでは、Webブラウザのサイズによって対応する文字列"small""medium""large""null"を返す処理をしています。

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

このコードでは、ifが入れ子になっていて、さらにif elseによって長くなっています。 同氏は、このような場合はifではなくswitchを使うと以下のように簡潔に書けると述べていました。

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関数を使うことでtime.Duration型の値を取得できると述べていました。

var rpcTimeoutSecs = 30 // Thirty seconds

以下の2つの例では、確かにtime.Duration型を使用していますが、型変換は不要です。定数は型情報を持たないため、time.Secondと掛け算をするとその結果はtime.Duration型となります。

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

同氏は型変換をなくすと、上記のコードは以下のように簡潔に書くことができると述べていました。また、30 * time.Secondで十分意味が通じるので、// Thirty secondsのような冗長なコメントは必要ないと指摘していました。

var rpcTimeout = 30 * time.Second

チャネルを使う

同氏は、今までC言語やC++でコードを書いてきた人たちが以下のようなコードを書きがちだと述べていました。

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
}

StreamCloseされているかどうかをisConnClosedで管理し、sync.Mutexsync.Condを使って制御しています。同氏は、このコードは決して間違ってはいないが、受信のみのチャネルを使うことでもっと簡潔に書けると指摘していました。

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

上記のチャネルを使ったコードでは、チャネルが開いているか閉じているかをStreamが開いているか閉じているかに対応させています。Closeメソッドはチャネルを閉じるだけで実現しています。そして、Waitメソッドは単にチャネルから受信すればよく、チャネルが閉じられれば直ちにゼロ値が送られてくるそうです。またIsClosedメソッドは、開いているチャネルから受信を行うと何かしらの値が送信されるまで処理がブロックされることを利用して実現されています。チャネルが開いていると、selectcase <-s.ccのケースは実行されず、defaultのケースが実行されます。一方、チャネルが閉じていると直ちにゼロ値が送られてくるため、case <-s.ccのケースが実行されます。

型がわかっている場合にreflectは使わない

同氏は、reflectパッケージは強力であり、効果的に使える場合もあるが安易に使うべきではないと主張していました。以下のコードは、いくつかのフィールドに対して似たような処理を行う必要があり、そのために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の対象のLayersがどのような型であるかコンパイル時にわかっているため、reflectを使う必要はないと同氏は指摘していました。そして、以下のようにSliceというLayerExperimentの対のスライスを返すメソッドを追加することで、同様の処理が書けると述べていました。同氏はプログラム自体は長くなるが、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)
        }
    }

テスト

同氏は可読性のレビューにおいて、テストがうまく書けていない・可読性に欠けていることを指摘することがよくあると述べていました。典型的なテストコードは以下のように、ある処理において期待した結果が出なかった場合に、t.Errorfメソッドで入力と期待する出力と実際の出力をメッセージとして出すものであると述べていました。このように書いておけば、どういう場合にどのような値が出るのかがわかりやすいとのことでした。

// 典型的なテストコード
if got, want := テスト対象(input), 期待値; !テスト(got, want) {
    t.Errorf("テスト対象(%v) = %v; want %v", input, got, want)
}

なお、多くの人が独自アサート機能を実装しがちですが、同氏は言語の機能を使うことをお勧めしていました。また、コメントにAPIの使い方を詳細に書くのであれば、以下のようなExampleテストを書くべきだと主張していました。Exampleテストはテストとして実行されるため、より正しいExampleとして提供することが可能とのことでした。

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
}

コメント

レビューにおいて、コメントに対する指摘を行うことが多いと同氏は述べていました。そして、よく指摘する項目として

を挙げていました。

上記の3つの項目を満たす正しいコメントは以下のようになるそうです。

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

このように書いておくと、grepもしやすくgodocで確認した場合も見やすくなるとのことでした。また、同氏はgodoc-httpオプションをつけてWebブラウザで確認することもできると述べていました。

$ godoc bytes Buffer

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

同氏は、コメントがわかりにくい/うまく書けないときは、APIの設計を考え直したほうが良いと主張していました。また、上記のようにコメントを書いてgodocで確認することで、APIの設計が問題ないか考えることができると述べていました。

APIデザイン

同氏は、APIをデザインするうえで最も大切なことは適切な名前のパッケージを作ることであると述べていました。たとえば、よくutilというパッケージを作ってそこにいろいろなものを入れてしまいがちだが、もっと細かくパッケージを分けるか、もっと大きなパッケージの一部として定義するかのどちらかにするべきだと指摘していました。

同氏はAPIをシンプルにするためのルールとして以下の項目を挙げていました。

  • 複数の戻り値を使えるので、出力変数としてポインタは使わない。
  • スライス、マップ、チャネル、インターフェースへのポインタは使わない。
  • エラーはerrorで返す(panicは使わない⁠⁠。
  • 一般的なインターフェース(fmt.Stringerio.Readerなど)に挙動が適合する場合は合わせる。
  • 引数などはインターフェースのほうが組み合わせやすいしテストもしやすい。
    • 例:ファイルから読む関数などは*os.Fileよりio.Readerなどが引数のほうが良い。
  • 非同期APIより同期API(パッケージを越えてチャネルあまり使わない⁠⁠。
  • 非同期にするにはAPIを使う側がゴルーチンとチャネルで制御する。

まとめ

同氏は、コードはコミュニケーションであると述べていました。 そのためには明瞭に表現することが大切で、以下のことを守る必要があると述べていました。

  • 適切な名前を選ぶ
  • シンプルなAPIを提供する
  • わかりやすいドキュメントを書く
  • 複雑にしすぎない

この基調講演では、どうやったらGo言語らしく書けるのかということを具体例とともに説明されていました。聴講した多くの方にとって、Go言語のコードを書くことはあってもレビューされる/する機会というのはまだ少ないと思います。そのため、この講演は、可読性のあるコードにするために、どういった観点でレビューをされるのか知る貴重な機会になったかと思います。

おすすめ記事

記事・ニュース一覧