😀

Go Conference mini 2023 Winter in KYOTOに参加しました

2023/12/08に公開

はじめに

株式会社ナレッジワーク Enablement Groupの上田(tenntenn)です。
2023年12月2日(土)に京都で開催されたGo Conference mini 2023 Winter in KYOTOに参加してきました。
https://kyotogo.connpass.com/event/285351/
本記事では、登壇および運営のお手伝いをさせて頂いたため、そのレポートを書きます。
また、当日のオフライン参加者のみに出題したクイズの解説も行います。
なお、この活動はナレッジワークのナレッジスポンサー制度を活用して行っているものです。
https://note.com/knowledgework/n/n70722bd5f441

生成AIによる静的解析ツールの自動生成

筆者は「生成AIによる静的解析ツールの自動生成」というタイトルで20分のセッションを行いました。
https://docs.google.com/presentation/d/1gE7X6OTB5MpZbDlF4iwD-MGUpbsVOuNRWcAaVzQUNqg/edit#slide=id.p
このセッションでは、筆者がOSSで開発している静的解析のスケルトンコード生成ツールのskeletonに生成AIを導入してみた話をしました。
Goの静的解析ツールは、analysisパッケージのエコシステムに従うことで、ある程度雛形が決まっており、比較的簡単に開発を始めることができます。筆者の開発しているskeletonでは、静的解析ツールの雛形となるコードを生成できます。
そこで、筆者は雛形が決まっているコードの生成と生成AIは相性が良いと感じました。
実際に、GPT4を使ってプロンプトを工夫すると、以下のようにコード生成を行えます。
https://chat.openai.com/share/178ce88c-8d64-4ba0-aac0-7db188f50d9a
まだ簡単なツールの生成しか試していませんが、今後は、よりエッジケースにも対応した静的解析ツールを自動生成できるようにチューニングして行きたいと思います。

運営のお手伝い

Go Conference mini 2023 Winter in KYOTOは、京都のGoコミュニティであるKyoto.goと筆者が代表を務める一般社団法人Gophers Japanの共催という形で開催されました。
そのため、筆者も配信をメインに運営のお手伝いをしてきました。
配信は筆者の持ち込んだ機材を中心に以下のような構成で行いました。

なお、詳細を知りたい方は以下のハンドブックをご覧ください。
https://docs.google.com/presentation/d/1q6uQqEp16ZQSg2fkX5JyJ4R4NI7FLuMl1moF4byaOEk/edit?usp=sharing
配信自体は大きなトラブルがなく行えました。配信の音が一部悪かったものの、ローカルでSSDに収録も行っていますので、編集してアップロード予定の動画については音声が良くなる予定です。
筆者が編集する予定ですが、編集用のPCが壊れたため、編集に時間がかかる見込みです。

現地で出題したGoクイズの解説

当日はクロージングの後に、オフライン参加者向けにクイズ大会を行いました。クイズ大会の勝者には、筆者が自費で購入してきたGopherのぬいぐるみやTシャツをプレゼントしました。
クイズの問題は4問で正答数および回答速度で勝者が決まります。なお、問題はGo1.21およびGo1.22以降の新しい機能について出題しました。一部の問題は当日行われた各セッションの復習問題として出題しています。

問題

問題を掲載しますので、当日オフラインで参加していない方はぜひこの機会にチャレンジしてみてください。

問1

問2

問3

問4

解説

問1


この問題のコンセプトは、Goに詳しくなればなるほど惑わされる問題です。
Goでは、パニックの発生はrecover関数を用いて回復できます。そして、recover関数の戻り値は、panic関数を呼び出した場合、その引数となります。そのため、以下の例では、"PANIC!!"という値がrecover関数で取得できます。

// https://go.dev/play/p/n8skDsTXi-4
package main
import "fmt"
func main() {
	defer func() {
		if p := recover(); p != nil {
			fmt.Println("recovered", p)
		}
	}()
	f()
}
func f() {
	panic("PANIC!!")
}

このように考えると、recover関数の戻り値がnilでなければ、パニックしていると考えても良さそうです。では、答えは「① できる」が正解でしょうか?
しかし、実はpanic関数にはnilを渡せ、recover関数からはnilが取得できそうに思えます。

// https://go.dev/play/p/WCNO2qyD8Vf
package main
import "fmt"
func main() {
	defer func() {
		if p := recover(); p != nil {
			fmt.Println("recovered", p)
		}
	}()
	f()
}
func f() {
	panic(nil)
}

このように考えると、recover関数の戻り値がnilであっても、パニックしているかどうか判定できないと考えても良さそうです。では、答えは「② できない」が正解でしょうか?
しかし、実はGo1.21からpanic関数の引数にnilを指定すると、recover関数の戻り値は、nilにならず、runtime.PanicNilErrorが返されます。
このように考えると、recover関数の戻り値がnilでなければ、パニックしていると考えても良さそうです。では、答えは「① できる」が正解でしょうか?
しかし、実は環境変数をGODEBUG=panicnil=1のように設定すると、この機能はオプトアウトできます。
つまり、やはりrecover関数の戻り値がnilであっても、パニックしているかどうか判定できないと考えても良さそうです。では、答えは「② できない」が正解でしょうか?
たしかに、recover関数の戻り値での判定は難しいと言わざる得ないでしょう。しかし、実はパニックは関数を途中で抜けるという性質があるため、以下のようなコードを書くと関数が正常に終了していないことを判別できます。

// https://go.dev/play/p/aEdLWjIP7Ka
package main
import "fmt"
func main() {
	var normalReturn bool
	defer func() {
		if p := recover(); !normalReturn {
			fmt.Println("recovered", p)
		}
	}()
	f()
	normalReturn = true
}
func f() {
	panic(nil)
}

この方法は、Go1.21で導入されたsync.OnceValueなどで利用されています。
このように考えると、normalReturnfalseの場合、パニックしていると考えても良さそうです。では、答えは「① できる」が正解でしょうか?
しかし、実は関数を途中で抜けるのはパニックの時だけではありません。runtime.Goexit関数を用いた場合でも関数を抜けます。runtime.Goexit関数は(*testing.T).Fatalメソッドなどでも使われており、return文を書かずにテスト関数を終了させていることに疑問を持っている読者の方もいるでしょう。

// https://go.dev/play/p/SW-FGp6dJnn
package main
import (
	"fmt"
	"runtime"
)
func main() {
	var normalReturn bool
	defer func() {
		if p := recover(); !normalReturn {
			fmt.Println("recovered", p)
		}
	}()
	f()
	normalReturn = true
}
func f() {
	runtime.Goexit()
}

このように考えると、パニックとruntime.Goexit関数を判別するのは難しいと考えても良さそうです。では、答えは「② できない」が正解でしょうか?
runtime.Goexit関数は、あくまで現在のゴールーチンを終了させる機能です。そのため、以下のようにdefer文を二重に重ねることで、うまくフィルターリングできます。

// https://go.dev/play/p/FF7aqxX3XIG
package main
import (
	"fmt"
	"runtime"
)
func main() {
	var (
		normalReturn bool
		recovered    bool
		panicValue   any
	)
	defer func() {
		switch {
		case normalReturn:
			fmt.Println("normal return")
		case recovered:
			fmt.Println("recovered", panicValue)
		default:
			fmt.Println("runtime.Goexit")
		}
	}()
	func() {
		defer func() {
			panicValue = recover()
		}()
		f()
		normalReturn = true
	}()
	if !normalReturn {
		recovered = true
	}
}
func f() {
	runtime.Goexit()
}

この方法は、「double defer sandwich」と呼ばれ、golang.org/x/sync/singleflightパッケージなどで使われています

問2


問2は、セッションでも触れられていたencoding/jsonパッケージの挙動についてです。現在のencoding/jsonパッケージでは、構造体にはomitemptyオプションが効かず、JSONに現れてしまいます。
そのため、答えは「② {"email":""}」です。

問3


Go1.21でツールチェイン周りが大きく代わり、go.modファイルに記載されているGoのバージョンはガイドラインからルールに変更されました。そのため、go.modに記載されているバージョンより低いツールチェインでビルドできなくなりました。その代わりに、Go1.21以降ではビルドができるバージョンに自動的にアップデートする機能が搭載されました。
また、Go1.21では、Go1.21の初回のリリースバージョンをgo 1.21のような記載ではなく、go 1.21.0と記載するようになりました。そのため、go 1.21という記載は、language versionと呼ばれる記法になり、RC版のリリースも含むようになりました。
つまり、Go1.20では1.20rc1 < 1.20rc2 < 1.20rc3 < 1.20 < 1.20.1となりますが、Go1.21では1.21 < 1.21rc1 < 1.21rc2 < 1.21.0 < 1.21.1 < 1.21.2となります。
go.modgo 1.21と記載するとRC版も含むので、代わりにgo 1.21.0と記載するようにしましょう。
詳細は以下のドキュメントを読むと良いでしょう。
https://go.dev/doc/toolchain
さて、この問題の回答は、「② go 1.21rc2」となります。

問4


この問題は、フィールドに_ [0]func()と記載しても良いかという部分が肝となる問題です。
_blank identifierと呼ばれ、型宣言や代入文の左辺の識別子として利用できます。そのため、フィールド名を_としても何ら問題はありません。フィールドを宣言するけど使わない場合に記述する場合があります。
[0]func()という型がフィールドの型として許されるかどうかを考えてみましょう。[1]intのように記述すると、長さ1のint型の配列を表します。そのため、[0]は長さ0の配列を表すように思えます。
論点となるのは、長さ0の配列が許されるかどうかです。もちろん、長さ0の配列も許されるため、[0]のように記述しても問題ありません。
では、func()の部分はどうでしょうか?配列の要素の型にfunc()と記述することはできるのでしょうか?
実はこれも問題なく記述ができ、_ [0]func()はフィールドの宣言として何ら問題がありません。
それでは、問題の回答は①以外になるのでしょうか?
いいえ、実は答えは①で、別の部分でコンパイルエラーが発生します。
Goには、comparable(比較可能)という概念があります。比較不可能な型とは、関数型やスライス型、またフィールドや要素に関数型やスライス型の値を(再帰的に)持つ配列や構造体も比較不可能です。
比較不可能な型をnil以外と比較しようとすると、コンパイルエラーになります。そのため、答えは「① compile error」となります。
実は、この方法はslog.Value構造体でも使われているイディオムです。長さ0の配列を使う理由は、型のサイズがゼロだからです。

// A Value can represent any Go value, but unlike type any,
// it can represent most small values without an allocation.
// The zero Value corresponds to nil.
type Value struct {
	_ [0]func() // disallow ==
	// num holds the value for Kinds Int64, Uint64, Float64, Bool and Duration,
	// the string length for KindString, and nanoseconds since the epoch for KindTime.
	num uint64
	// If any is of type Kind, then the value is in num as described above.
	// If any is of type *time.Location, then the Kind is Time and time.Time value
	// can be constructed from the Unix nanos in num and the location (monotonic time
	// is not preserved).
	// If any is of type stringptr, then the Kind is String and the string value
	// consists of the length in num and the pointer in any.
	// Otherwise, the Kind is Any and any is the value.
	// (This implies that Attrs cannot store values of type Kind, *time.Location
	// or stringptr.)
	any any
}

参加した感想

Go Conference miniといえども、参加者は運営メンバーもあわせて100名ほどいらっしゃいました。久しぶりのオフラインイベントということもあり、非常に熱量の高いカンファレンスでした。東京からの参加者も多く、北は北海道(筆者)、南は沖縄からと日本全国から参加されていました。
セッションの内容も幅広く、そして登壇者の経験を通して得たナレッジをベースに話されていました。
特にsago35tkさんの「続) TinyGo で作る自作キーボードの世界」が面白かったです。20分でTinyGoを使って簡単なキーボードを作成するライブコーディングがとても楽しかったです。途中のアクシデントも含めて楽しいセッションでした。
Go Conference mini 2023 Winter in KYOTOでは、多くのプロポーザルが集まったそうです。それでも、Go1.22以降で入る、いわゆるイテレータの機能についてのセッションありませんでした。どうやら、誰かがプロポーザルを出すだろうと思う人が多かったようです。
イテレータについては、2023年12月9日(土)のDevFestでお話する予定ですので、興味のある方はご参加下さい。
https://gdg-tokyo.connpass.com/event/301690/

おわりに

Go Conference mini 2023 Winter in KYOTOは、運営から懇親会、そして、株式会社はてな様が開催してくれた2次会まで非常に楽しいカンファレンスでした。
久しぶりのオフラインカンファレンスの楽しさを感じる良い機会でした。来年はGo Conference 2024が6月8日(土)に開催される予定なので、そちらも楽しみです。
ナレッジワークでは、バックエンドの開発にGoを用いています。
今後も社内外のエンジニアのイネーブルメント活動に力を入れていきたいと考えています。
ナレッジワークに興味のある方は、ぜひ2023年12月12日(火)18時から行われるウェビナーに参加してみてください。このイベントでは、ナレッジワークエンジニアチームがナレッジワークのマルチプロダクト戦略や体制についてご紹介します。
https://knowledgework.connpass.com/event/303503/

株式会社ナレッジワーク

Discussion