Enablement Internship for Gophers (6/21~6/23) に参加しました

早稲田大学基幹理工学部表現工学科3年の@takabayapと申します。このたびナレッジワーク社のEnablement Internship for Gophersに参加してきました。

なにしてきたか

プログラミング自体は小学生の頃からやっており、今11年目(!)。Goは高校のときにちょっとだけ触って、本格的に勉強しはじめたのは2021年なので2年目くらい。ハッカソンアプリ開発で利用しています。

github.com

技育CAMP vol.9で作ったウェブアプリ。参加記録を書こうと思ってもう半年が経ったため、引用するブログ記事がありません。

takabayap.hateblo.jp

リリースしたアプリ。バックエンドは全部Goでできてます。

Goはシンプルで、好みが分かれがちではありますが私はかなり好きです。今回のインターンシップはピッタリでした。

Enablement Internship for Gophersとは何か

Enablement Internship for Gophersとはナレッジワーク社が実施してる3daysインターンシップです。
ナレッジワーク社はtoBクラウドサービスを展開している企業で、2020年4月創業とまだ創業して間もない企業であるにもかかわらず製品は誰もが知る日本を代表する企業に導入されています。

knowledgework.cloud

メインとなっている製品はセールスイネーブルメントに焦点を当てたものですが、会社としてエンジニアイネーブルメントにも取り組んでいます。イネーブルメントとは直訳が難しいけど「ひとりひとりの成果・能力の向上」のようなニュアンスでナレッジワーク社は特にGoに力を入れており、今回のインターンシップもその活動の一環と言えるでしょう。

prtimes.jp

今回はGoの主要な機能の1つである並行処理をメインテーマとしていて、講義を行ったあとに個人でOSSとしてなにか1つ関連したものを作るという内容のインターンシップでした。

参加するまで

元々ハッカソンで登録していたサポーターズさんから今回のインターンへの招待が来ていたことが切っ掛けで申し込みました。まず面談を実施し、コーディングテスト、面接を経て参加という流れでした。
面談では会社や製品の概要、大事にしている価値観など丁寧にお話し頂いて大変良かったです。インターンの面接自体初めてだったので緊張していたけれど、初めてがこれで本当に良かったと思っています。ただエンジニアが少数精鋭という雰囲気で非常にレベルが高そうで、今回のインターンシップもGo上級者向けと銘打っているので要求されている能力に対する自分のレベルが少し不安になりました。
実施されたワークショップの動画を見てコーディングテストに備え、コーディングテストは相変らず締切3時間くらい前に開始。内容は聞いていたようにかなり難易度が高く、正直落ちても全然不思議ではないと思っていました。
面接を受けたらまた自分の勉強不足を実感し、ここでも落ちてても全然不思議じゃないという手応えでした。合格できて良かったです。初めてのインターン合格でした。

なにしたか

  • 1日目
    • 講義
    • チーム開発
    • 発表
  • 2日目
    • 講義 sync.Onceのruntime.Goexit()での挙動やGoDebugの話など
    • 個人開発
  • 3日目
    • 個人開発
    • 発表
    • 懇親会

チーム開発

課題

チーム開発で選べる課題は2つあって、1つがsync.Onceに関連するもの、もう1つがcontext.AfterFuncに関連するものでした。うちのチームはせっかくなのでより難しそうなcontext.AfterFuncのほうを選びました。

github.com

Dialしてnet.Connを得るときにcontextを渡してcancelできるように、DialWithContext関数を実装しようという課題です。まずGo 1.21で追加予定のcontext.AfterFuncを利用して実装し、これが出来たので次にAfterFuncを使用しない実装を試みました。シンプルにctx.Done()を受けとってnet.ConnをCloseするようにしていただけだったけれど、これだとWithCancelのcancelを使わずにDialWithContextで得たnet.ConnをCloseするとctx.Done()を監視しているgoroutineが終了することなくリークしてしまう。そこでcancelFuncを返すようにしてconn.CloseしたあとこのcancelFuncを呼び出すと監視しているgoroutineを終了できるようにしました。実装としてはcancelFuncが呼び出されるとDialWithContext内で開けたchanをcloseします。この終了を伝えるchanをctx.Done()と並行して監視し、returnしgoroutineを終了するようにすることでgoroutineリークを無くすことができました。runtime.NumGoroutineを関数実行前後などで呼び出し、goroutineの数が正しく元に戻っていることが確認できました。

func DialWithContext(network, address string, ctx context.Context) (net.Conn, func(), error) {
    conn, err := net.Dial(network, address)
    if err != nil {
        return nil, func() {}, err
    }

    done := make(chan struct{})

    go func() {
        select {
        case <-ctx.Done():
            conn.Close()
        case <-done:
            return
        }
    }()

    return conn, sync.OnceFunc(func() {
        close(done)
    }), nil
}

(tenntennさんからのご指摘により一部コードを修正しています)

感想

まずモブプログラミングという手法を体験するのが初めてでした。自分が理解できたことがなかなかつたわらないともどかしいし、かといって自分が書く側に回ると自分が書きたいコードを勝手に書くわけにもいかないというところに難しさを感じました。結果的には人に説明することで自分の理解も深まるし、書く側が理解できていなかったとしても書く過程で理解が要求されるのでチーム内の理解を半ば強制的に同じ水準まで引き上げるという効果があり、長期的な生産性の向上に繋がるのだろうと納得しました。
GoのRelease Candidateなんて入れたことなかったのでそこも新鮮でした。そもそも普段開発してるときにバージョンほとんど意識してないですし。goroutineの数を意識した開発もしたことがなかったので勉強になりましたが、自分が作ったサービスのコードのどこかでgoroutineがリークしてたらどうしようとちょっと怖くなったのでこれから手を入れていきたいです。

個人開発

github.com

課題

個人開発の課題は以下の3つのいずれかです。

  1. 並行処理のテストツール
  2. Goroutine/channel の可視化ツール
  3. 同期ライブラリ

まず自分で実際に使うようなものを作ろうという気持ちがあり、そこから考えていきました。
テストツールで並行処理の順番を制御し全ての組み合わせでテストして実行順に関係なく動作することを保証しようとしたけれど、goroutineの動く順番のみならず内部の処理の順番も制御しなくてはならず、関数の外側から気軽にテストするものはできそうになく悩んでいました。tenntennさんに相談したところ全部列挙してテストすることに拘らなくても良いのではないかという助言を受けたので、可視化ツールとして1回1回の実行順を可視化する方向を考え始めました。実行順によって問題が発生しそうなものを考えてみたらいいんじゃないと言われたときに、Mutexのロックとアクセスを可視化できたら便利かもしれないと思ったのでMutexの可視化ツールへと舵を切りました。授業でロボットを作り、ロボットのフィールド上の位置を状態として持ちMutexを用いて管理する予定だったので、自分で使うようなものを作るという目標にも適しています。
可視化するにはどうすればいいかというのも問題でしたが、他の方のワークシートをチラチラみていったらGraphvizという便利そうなものを知れたのでそれを使うこととして一旦可視化は後回しに。Mutexのインターフェースを持ち、追加でRead/Setを持つMutexVisualiserという構造体を用意してこの構造体に関する全てのアクセスのログを取り可視化に用いることにしました。

type Mutex[T any] interface {
    sync.Locker
    Set(T)
    Read() T
}

type MutexVisualiser[T any] struct {
    m       sync.Mutex
    actions []action
    value   T
}

どんな型でも値に用いたかったのでジェネリクスを初めて使うことに。Mutexのアクセスを全部持っておくのはそこまで難しくなくて、runtimeから呼び出し元の関数名やgoroutine IDを取得したりtimeを取ったりと結構順調でした。可視化の段階に入って、go-graphvizを使おうと思ったけどこれがなかなか難しく、rankの設定やsubgraphの使い方がドキュメントになかった上に使っている人も見つからなかったので使うのを諦め、他にも色々ないか探したうえで結局graphvizが使いやすそうだということになったのでtemplateを用いてgraphvizを直接書いていくことにしました。ここにかなり時間を使って、なんとか上手く可視化できたところで開発時間が終了しました。

生成されるグラフ

感想

発表ではグラフの見た目が特に評判が良かったです。クラフトマンシップを感じると言われ、拘ってよかったと思いました。メンターの方には手が速いという評価を頂き、自分は手が遅いほうだと思っていたので結構自信に繋がりました。ワークシートに事前に必要そうなナレッジを表として整理した上で計画的に調べ物をして、最後にコーディングをしていくという手法がかなり強力だったので、これからも使っていこうと思います。今までは無計画に調べ物をして満足してしまうことが多々ありました。 他のチームの方で時間内に丁寧なREADMEを書いていらっしゃる方がいて、READMEを含むドキュメントに全然力を割いていなかったので、そこの重要性を再確認しました。

まとめ

全体を通して大変楽しかったです。講義で触れられるものは知らないことばかりでしたし、歩くGoDocことtenntennさんはあらゆる質問に答えてくれるという安心感がありました。気軽に質問できる体制が整っており、コミュニケーションコストが高いというオンラインのインターンシップの弱みがだいぶ薄れていた印象です。
懇親会も含め話した参加者の方は全員修士課程在学中でした。普段Goを書かないという人も参加していましたが、急速にGoを吸収していっていたのでレベルはやっぱり高かったです。

完全オンラインで仕事をするというのがどんな感じなのかというのも体感できて良かったです。基本的に自室で一日が完結してしまうのでストレスに感じてしまう人もいらっしゃるでしょうが、私にとってはとても快適でした。メンターの方も含め会社の方も非常に話しやすく、安心して取り組むことができました。次回も是非参加したいですし、特にGoが好きな方にとてもオススメできるインターンシップでした。