2023/6/21から3日間開催された、ナレッジワークさんのgoの並行処理特化型インターンシップ「Enablement Internship for Gophers」に参加してきました。このインターンを通してたくさんの学びがあったので、忘られないうちにブログ記事という形でアウトプットします。

Enablement Internship for Gophersとは?

Enablement Internship for Gophers

Enablement Internship for Gophersとは、株式会社ナレッジワークさんが開催している3dayのインターンシップです。今年は、2023/6/21~6/23で開催されました。

インターンシップの内容としては主に、チームによるモブプログラミングと個人開発による並行処理に関連するOSS開発に取り組みました。

  • Day1:並行処理関連のライブラリをチームで開発。モブプログラミングで開発を進める。
  • Day2,3:個人で自由に並行処理関連のライブラリやツールを開発&OSSとして公開する。
開発中はナレッジワークさんの現役エンジニアの方達に質問ができ、疑問に思っていることやわからないところはいつでも相談することができる素晴らしい環境でした。また、Goエキスパートである@tenntennさんのGoの講義を受けれる&質問できるというGopersにはたまらない体験でした。

Day1: キャンセル可能なnet.Connの開発

day1ではチームに分かれて並行処理関連のライブラリ開発を行いました。私たちのチームでは、「キャンセル可能なnet.Connの実装」をテーマに取り組みました。
私は、net.Connを今までの実務で使用したことがなく問題を理解するところから苦労しました。幸いなことにチームの中に、net.Connに詳しい方がいたのでその方に助けてもらいながら理解を進めていきました。

開発を進めていく中で、学んだことが2つあります。

  1. 機能が導入された背景を考えることで理解度を高めることができる
  2. goroutineのleakは危険

機能が導入された背景を考えることで理解度を高めることができる

go1.21で導入されたcontext.AfterFuncを使用する際に、ネット上にcontext.AfterFuncに関する十分なリソースがなく、仕様を理解するのに苦しんでいました。その時に、proposalやissueを追うことでその機能が導入された背景を考え、context.AfterFuncの理解を深めることができました。やはり、一次情報は何よりもわかりやすく信頼度が高い。これからは積極的に見ていこうと思いました。

goroutineのleakは危険

私はこのインターンに参加する前はGoの並行処理に関する開発をしたことがありませんでした。そんな私からするとgoroutineの実装をするのに精一杯でleakのことまで考えることができませんでした。day1のチーム開発でも、要件を満たすことに集中してしまい、結局leakが起こり得るコードを書いてしまっていました。goroutineのleakが起こりやすいということを実際に感じることができたのは大きな成果でした。

Day2,3: goroutineのleak検出&可視化ツールの開発

day2,3では、個人開発としてそれぞれが並行処理関連のライブラリやツールの開発に取り組みました。私はday1の「goroutineのleakは危険」という学びから「goroutineのleak検出&可視化ツール」というテーマを選びました。

インターンシップでは、事前にワークシートが用意されていて自分の考え方や行動を文章として残すことで、自分の頭の中を整理したり振り返るのに役立ちました。また、他の参加者のワークシートも覗くことができるので他の人の思考から学んだりすることもできたのが新鮮でした。開発の流れとしては下記のステップで進めていきました。
  1. 開発するものを明確にする
  2. 実現方法の模索(必要なナレッジは?)
  3. コーディング

開発するものを明確にする

まずはざっくりと、「goroutineのleakを検出するツール」を開発するテーマとして定めました。イメージ的には、テストの際にleakがあるかどうかを自動で検出してくれて、leakがある場合はleakしているgoroutineの詳細を表示してくれるテストツールのような感じです。

tenntennさんに開発テーマについて相談させていただいた際に、既存ツールと差別化できる何かがあると良いとアドバイスをいただいたので、goroutineの数の推移を簡易的に表すグラフを自動で生成する機能を付け加えました。

実現方法の模索(必要なナレッジは?)

開発するものを明確にしたのちに、それを実現するにはどのようなナレッジが今の自分には必要なのかをまとめました。
必要ナレッジ 調べたことメモ
似たようなツールの調査

leaktest

goleak

https://github.com/fortytw2/leaktest

https://github.com/uber-go/goleak

gorutineの情報取得
  • どのようにgoroutineの情報を保持するのか
  • IDや実行時間の取得方法は?
  • runtime.Stack()を理解する
https://pkg.go.dev/runtime#NumGoroutine
leakの検出方法
  • どのようにleakがあると判断するのか
  • そもそもleakとはの確認
  • 今動いているgoroutineの取得方法
https://golangbyexample.com/number-currently-running-active-goroutines/
検出結果の表示方法
  • どんなフォーマットで表示するか
  • どこまでの情報を表示するのか
https://github.com/guptarohit/asciigraph

コーディング

インターン中に実装するべき要件として2つを定めました。
  1. goroutineのleakを検出する機能
  2. goroutineの数を取得して、グラフを生成する機能

goroutineのleakを検出する

goroutineのleakを検出する方法として、現在のgoroutineの数を取得するruntime.NumGoroutine()を使用しました。

ロジックとしては、LeakDetect{}の呼び出しの際に引数として渡されるruntime.NumGoroutine()とdeferとして呼ばれるstop()で検出するgoroutineの数が等しいかどうかでleakを検出しています。

func TestNoLeakDetect(t *testing.T) {
	t.Run("sync.WaitGroup", func(t *testing.T) {
		leakDetect := LeakDetect{runtime.NumGoroutine()}
		defer leakDetect.testStop()
		
		var wg sync.WaitGroup

		for i := 0; i < 10; i++ {
			wg.Add(1)
			go func() {
				defer wg.Done()
			}()
		}

		wg.Wait()
	})
}
LeakDetectメソッドとは別に、leak検出とgoroutine数の推移をグラフ化するLeakGraph{}を作成しました。 start()を呼び出すことでgoroutineの数を定期的に取得しsliceにappendするgoroutineを呼び出し、stop()の呼び出しによってチャネルを通じてstart()を終了させています。goroutineのleakを検出するツールを実装するためにそもそものツール内でもleakをしないようにするのが大変でした笑。
// Detect Goroutine Leak
type LeakGraph struct {
	startNumberOfGoroutine 	int
	goroutineData 		 	[]float64
	done 					chan bool
}

func (l *LeakGraph) start(duration time.Duration) {
	l.goroutineData = append(l.goroutineData, float64(runtime.NumGoroutine()))
	for {
		select {
		case  <- l.done:
			return
		case <- time.After(duration):
			l.goroutineData = append(l.goroutineData, float64(runtime.NumGoroutine()))
		}
	}
}

func (l *LeakGraph) stop() {
	fmt.Println("=== RUN LeakGraph")
	l.done <- true

	// show goroutine graph
	graph := asciigraph.Plot(l.goroutineData)
	fmt.Println(graph)

	if l.startNumberOfGoroutine == runtime.NumGoroutine() {
		/*
			when there are no goroutine leak
		*/
		fmt.Println("No goroutine leaks")
		fmt.Println("=== PASS: LeakGraph")
	} else {
		/*
			when there are some goroutine leak
		*/
		panic("This code may cause goroutine leak")
	}

}
stop()が呼ばれたタイミングでstart()を終了させるために、select caseでtime.After()とstop()が呼ばれたかを確認するdoneチャネル使用しました。
LeakGraphを使用すると下記のようなGraphが表示され、leakがない場合は「No goroutine leaks」、leakがある場合はleakの詳細情報が表示されます。
LaekGraph

インターン中にたくさん迷いながらも、tenntennさんやyudofuさんに助けていただき最低限の機能を実装することができました。これからは、testやexample、read.meを充実させていき、より精度の高く使いやすいOSSにしていきたいと思います。

インターンを終えて

まずは、このような貴重な機会を提供してくださったナレッジワークの皆さんに感謝致します。ありがとうございました。

最初は、並行処理に関する知識がほぼゼロの状態でしたが、このインターンを通して並行処理で気をつけるべき競合の回避やLeakなどたくさんのことを学び、Enablementを毎日感じることができたインターンシップでした。

今回のインターンシップで学んだことは数えきれませんが、この経験を活かして、これからも「できるようになる喜び」を感じながら励んでいこうと思います。