じゃあ、おうちで学べる

本能を呼び覚ますこのコードに、君は抗えるか

知識のunlearningをちゃんとやる - Learning Go, 2nd Editionの読書感想文

はじめに

Go言語の入門書として広く知られている"Learning Go"の第二版が発刊されました。第一版を読んだ際、Go言語のシンプルさと美しく整然とした構文に感銘を受けたものの、常に進化を続けているため、過去の知識にとらわれることなく、新しい概念や手法を柔軟に取り入れていく姿勢が何よりも重要であると感じました。 learning.oreilly.com

ソフトウェアエンジニアとして成長を続けるには、アンラーニング(unlearning)の精神、つまり過去の知識にとらわれることなく、絶え間なく新しい知識を吸収し続ける姿勢が欠かせません。どんな達人でも鍛錬を怠れば老いるのが自明ですから、知識の新陳代謝のための学び直しが必要不可欠なのです。

この第二版では、中身がかなり改訂されており、Go言語のベストプラクティスがより深く理解できるようになっています。特に、ジェネリクスのサポートについての記述が追加されているのが嬉しいポイントです。これまでのGoの型システムの制限を克服し、より柔軟で表現力の豊かなコードが書けるようになるはずです(使えるようになるとは言っていません)。

また、並行処理やメモリ管理、パフォーマンスチューニングなどの話題も充実しており、Go言語でシステム開発を行う上で必須の知識が得られます。サンプルコードや練習問題を動かしながら、Go言語の深淵に迫っていくことができます。本書は初心者には基礎知識と理解を、中級者にはステップアップのための明確な道筋を、そして上級者にはさらなる深い洞察と広がりを教えてくれる。一冊だと思います。ちなみに第一版には日本語版もあります。

この本を読みながら、過去の知識にとらわれずに新しい概念を柔軟に取り入れる姿勢の重要性を改めて実感しました。Go言語はシンプルな設計思想を貫きながらも、常に進化を続けています。私自身もGo言語を使った開発を行っており、アンラーニングの精神を持ち続け、新しい知識を積極的に吸収していく必要があります。そうすることで、Go言語を最大限に活用し、よりよいソフトウェアを作り上げていくことができるはずです。

個人的におすすめの書籍としては、「実用 Go言語―システム開発の現場で知っておきたいアドバイス」もあります。Go言語を実務で使う際の実践的なアドバイスが詰まっており、ぜひ合わせて読むことをおすすめします。

この本の構成

序文では、第1版から第2版への変更点、本書の対象読者、表記規則、サンプルコードの使用方法などが説明されています。第1章Go開発環境のセットアップ方法が解説され、第2章から第5章Go言語の基本的な要素である型、宣言、複合型、ブロック、シャドーイング、制御構文、関数などが取り上げられます。第6章から第8章では、ポインタ、型、メソッド、インターフェース、ジェネリクスといったGo言語の特徴的な機能が詳しく説明されています。

第9章ではエラー処理、第10章ではモジュール、パッケージ、インポートが解説されます。第11章Goのツールエコシステムが紹介され、第12章Go の並行処理機能が取り上げられています。第13章と第14章では標準ライブラリcontextパッケージについて詳しく説明されています。

さらに、第15章テストの書き方第16章reflect、unsafe、cgoといったより高度なトピックが解説されています。最後に演習問題が用意されており、Go言語を体系的に学ぶことができる構成となっています。

Chapter 1. Setting Up Your Go Environment

「Chapter 1. Setting Up Your Go Environment」を読んで、改めてGo言語の開発環境について理解を深めることができました。私自身、以前からGo言語を使っていましたが、この章を通して新たな発見もありました。

私は普段、開発環境としてNeovimを使っています。VSCodeやGoLandのような統合開発環境を目指していろいろやっているのですがデフォルトでここまでやれると羨ましくも思います。ただ、Neovimでもプラグインを使えば、コード補完やフォーマットなどの基本的な機能は十分に使えるので、不便は感じていません。設定ファイルはこちらです。アンラーンが大事とか言いながら絶対に譲らないのは草です。

github.com

それでも、VSCodeやGoLandのようなIDEの充実ぶりには驚かされました。特にデバッガやリファクタリング機能は、本格的な開発では重宝しそうです。私としては、プロジェクトの規模や用途に応じて、適切な開発環境を選ぶことが大切だと思いましたが別に変える予定はありまえせん。

The Go Playgroundについては、以前から愛用していました。サンプルコードを試したり、コードを共有したりするのに非常に便利ですよね。複数のファイルを扱うこともできるので、ちょっとしたプロトタイプを作るのにも役立ちます。ただ、機密情報をPlaygroundに貼り付けてしまわないよう、くれぐれも注意が必要だと改めて認識しました。

Makefileの活用方法については、自分でもよく使っているテクニックです。go fmtgo vetgo buildといったコマンドを個別に実行するのは手間なので、Makefileにまとめておくことで開発の効率化が図れます。さらに、以下のようにcleanターゲットを追加しておけば、生成されたバイナリやキャッシュの削除も簡単にできて便利ですね。かつて登壇したので共有しておきます。

speakerdeck.com

.DEFAULT_GOAL := build

.PHONY:fmt vet build
fmt:
        go fmt ./...

vet: fmt
        go vet ./...

build: vet 
        go build

clean:
    go clean
    rm -f hello_world

Go言語の後方互換性については、開発者にとって大きなメリットだと感じています。APIの互換性が保証されているおかげで、バージョンアップによる影響を最小限に抑えられるのは、長期的なプロジェクトの維持においては特に重要なポイントだと思います。一方で、goコマンドについては後方互換性が保証されていないため、注意深くアップデートする必要があるのは確かです。

moneyforward-dev.jp

総じて、この章では改めてGo言語の開発環境について体系的に学ぶことができました。すでにGoを使っている人にとっても、開発環境の選択肢や、コーディングの規約、Makefileの活用など、参考になる情報が多く含まれていたと思います。実際の開発で役立つテクニックが詰まった章だったと言えるでしょう。

私自身、今後もNeovimを主な開発環境として使っていく予定ですが、プロジェクトによってはVSCodeやGoLandの導入も今後も検討したいと思います。検討を重ねて検討を加速させます。みなさんは、どのような開発環境を使っているのか、そのメリットやデメリットも含めて教えていただけると嬉しいです。

この章で得た知見を活かし、より効率的な環境でGoのコードを書けるようになりたいですね。次の章以降では、いよいよ言語の基本的な要素について学んでいくことになります。Goならではの特性を理解し、実践で役立てていきたいと思います。

Chapter 2. Predeclared Types and Declarations

「Chapter 2. Predeclared Types and Declarations」では、Go言語の組み込み型と変数宣言について詳しく解説されていました。この章を通して、Go言語の型システムと変数の扱い方について理解を深めることができました。

go.dev

Go言語の変数宣言は、varキーワードを使う方法と:=を使う方法の2通りがあります。varを使う方法は、変数の型を明示的に指定できるので、意図が明確になります。一方、:=を使う方法は、型推論によって変数の型が自動的に決定されるので、コードがすっきりします。ただし、:=は関数内でしか使えないという制約があるので、適材適所で使い分ける必要があります。

Go言語のリテラルは、デフォルトでは型を持たない(untyped)という特徴があります。これにより、リテラルを柔軟に使うことができます。例えば、整数リテラル浮動小数点数型の変数に代入することができます。ただし、型を持たないリテラルは、デフォルトの型を持っていて、それが変数の型として使われます。

定数はconstキーワードを使って宣言します。Go言語の定数は、コンパイル時に値が決定するという特徴があります。そのため、定数には数値リテラルや文字列リテラルtrue/falseなどの値しか代入できません。定数は型を持つ場合と持たない場合があり、型を持たない定数はリテラルと同じように柔軟に使うことができます。

変数名の付け方には、Go言語らしい流儀があります。キャメルケースを使うのが一般的で、スネークケースはあまり使われません。また、変数のスコープが小さいほど、短い名前を使うのが慣例です。例えば、forループのインデックス変数にはijといった1文字の名前がよく使われます。

総括すると、この章ではGo言語の型システムと変数宣言について網羅的に解説されていました。Go言語の型システムは、シンプルでありながら多様な型を提供しており、柔軟性と安全性のバランスが取れていると感じました。変数宣言の方法も、var:=の2通りがあり、使い分けることでコードの意図を明確にできます。また、リテラルと定数の扱い方にも、Go言語らしい特徴があることがわかりました。

私としては、今後Go言語でコードを書く際は、この章で学んだ知識を活かして、型の選択と変数宣言を適切に行っていきたいと思います。特に、変数のスコープに応じて適切な名前を付けることは、コードの可読性を高めるために重要だと感じました。

みなさんは、普段どのようなルールで変数名を付けているでしょうか。私としては、キャメルケースを使い、スコープが小さい変数には短い名前を使うようにしたいと思います。例えば、以下のように書くのがよいと思います。

func main() {
    n := 10
    for i := 0; i < n; i++ {
        fmt.Println(i)
    }
}

ここでは、nという変数名を使って、ループの上限を表しています。また、ループ変数にはiという1文字の名前を使っています。このように、変数名を適切に付けることで、コードの意図が明確になり、可読性が高まります。

Chapter 3. Composite Types

「Chapter 3. Composite Types」では、Go言語の複合型について詳しく解説されていました。配列、スライス、マップ、構造体といった複合型は、Go言語でデータを扱う上で欠かせない要素です。この章を通して、Go言語の複合型の特徴と使い方について理解を深めることができました。

まず、配列の扱いにくさが印象的でした。Go言語の配列は、サイズが型の一部となっているため、非常に硬直的です。関数に任意のサイズの配列を渡すことができないなど、利用シーンが限られています。そのため、ほとんどの場合は配列ではなくスライスを使うのが一般的だと学びました。

スライスは、Go言語で最もよく使われるデータ構造の一つです。宣言方法は複数ありますが、makeを使ってサイズと容量を指定する方法が適切だと感じました。スライスは参照型なので、関数に渡した場合は元のスライスが変更されることに注意が必要です。また、

のように、スライスから別のスライスを作る際は、意図しない部分が共有されてしまうことがあるので、注意が必要だと学びました。

マップは、キーと値のペアを格納するデータ構造です。宣言時にキーの型と値の型を指定します。マップに値を格納するには、m[key] = valueのように角括弧を使います。

のように、存在しないキーにアクセスしようとすると、値の型のゼロ値が返されるのが特徴的でした。また、マップはスライス同様、参照型なので、関数に渡すと元のマップが変更されることを理解しました。

構造体は、任意の型のフィールドを持つ複合型です。構造体を使えば、関連するデータをまとめて扱うことができます。構造体リテラルを使えば、簡潔に構造体を初期化できます。フィールド名を指定しない場合は、宣言された順番で値を指定する必要があります。構造体は比較可能な型のフィールドのみで構成されている場合、==!=で比較できるのが便利だと感じました。

Exercisesは、学んだ内容を実践的に使う良い機会だと思います。スライスのサブスライスを作ったり、文字列のルーンにアクセスしたり、構造体を色々な方法で初期化したりと、複合型の基本的な使い方が身につきそうな課題ばかりでした。

github.com

この章ではGo言語の複合型について網羅的に学ぶことができました。スライスとマップの使い方、構造体の定義方法など、データを扱う上で欠かせない知識が身についたと実感しています。特に、スライスとマップが参照型であることや、意図しないメモリ共有に注意が必要だということは、頭に入れておくべき重要なポイントだと感じました。

Chapter 4. Blocks, Shadows, and Control Structures

「Chapter 4. Blocks, Shadows, and Control Structures」では、Go言語のブロックスコープ、シャドーイング、制御構文について深く理解することができました。これらの概念は、Go言語でコードを書く上で避けて通れない重要なトピックです。Uber Go Style Guide も良いのでおすすめです。

github.com

まず、ブロックスコープについては、変数の生存期間と可視性を適切に管理するために欠かせない概念だと感じました。Go言語では、{}で囲まれた部分がブロックを形成し、そのブロック内で宣言された変数は、ブロックの外からはアクセスできません。これにより、コードの可読性が高まり、意図しない変数の変更を防ぐことができます。

シャドーイングについては、内側のブロックで宣言された変数が、外側のブロックの変数を隠してしまう現象のことを指します。これは、うっかりミスを引き起こしやすいので注意が必要です。特に、forステートメントの中で変数を再宣言してしまうと、期待した結果が得られないことがあります。

一方、制御構文については、ifforswitchgotoの4つが紹介されていました。覚えることが少ないことは良いことです。なぜなら人はコードを書くより読む時間の方が一般的に長いからです。ifステートメントは、他の言語と同様に条件分岐を行うための構文ですが、Go言語では条件式の前に簡単なステートメントを書くことができるのが特徴的です。これにより、条件式で使う変数をifステートメントのスコープ内に閉じ込めることができ、コードの可読性が向上します。

forステートメントは、Go言語で唯一のループ構文であり、4つの形式があります。特に、rangeキーワードを使ったループ処理は、配列やスライス、マップなどの複合型を簡単に反復処理できるので、とても便利です。

switchステートメントは、式を評価し、その値に基づいて条件分岐を行う構文です。Go言語のswitchは、breakを書かなくてもフォールスルーしないのがデフォルトの挙動なので、コードの可読性が高くなります。また、式を書かずに条件式だけを列挙するブランクスイッチも用意されており、複数の条件を簡潔に表現できます。

gotoステートメントについては、安易に使うとコードの可読性を下げてしまうので、慎重に使う必要があると感じました。ただし、ネストが深いループを抜けるために使うなど、限定的な状況では有用であることも分かりました。

本章で学んだ制御構文を使ったコーディングの練習問題が用意されていました。ランダムな数値を生成してスライスに格納したり、forループとifステートメントを組み合わせて条件分岐を行ったりと、基本的な制御構文の使い方が身につく内容でした。解答例を見ると、GoらしいコードのベストプラクティスがEe察でき、とても勉強になりました。

github.com

本章のまとめとして、ブロックスコープ、シャドーイング、制御構文を適切に使いこなすことの重要性が述べられていました。特に、制御構文を適切に使いこなすことで、Goのコードの流れを思い通りに制御できるようになることが強調されていました。

技術的な観点からは、forステートメントの4つの形式についての理解が深まりました。特に、rangeキーワードを使ったforステートメントは、Goでデータ構造を反復処理する上で非常に重要だと感じました。また、switchステートメントのブランクスイッチについても、複数の条件を簡潔に表現できる点が印象的でした。gotoステートメントについては、使うべきシーンを見極めるのが難しいですが、限定的な状況では有用であることが分かりました。

func main() {
    evenVals := []int{2, 4, 6, 8, 10, 12}
    for i, v := range evenVals {
        if i%2 == 0 {
            fmt.Println(v, "is at an even index")
        } else {
            fmt.Println(v, "is at an odd index")
        }
    }
}

上のコードは、forステートメントifステートメントを組み合わせて、スライスの要素のインデックスの偶奇を判定しています。このように、制御構文を適切に使いこなすことで、シンプルかつ読みやすいコードを書くことができます。

Chapter 5. Functions

「Chapter 5. Functions」では、Go言語の関数について詳しく学ぶことができました。関数は、プログラムを構成する上で欠かせない要素であり、Go言語らしい特徴を備えています。この辺は実際に手を動かさないとピンとこないので動かしていってほしいです。

go.dev

Go言語の関数宣言は、キーワードfuncに続いて関数名、入力パラメータ、戻り値の型を指定する形式です。C言語などと同様に、複数の値を返すことができるのが特徴的でした。これにより、関数の戻り値を介してエラーを返すことが可能になり、Goらしいエラーハンドリングが実現できます。

また、名前付き戻り値という機能も印象的でした。これは、関数の戻り値に名前を付けることで、関数内で直接それらの変数を操作できるようにするものです。ただし、可読性を損なわないよう、この機能は慎重に使う必要があると感じました。

一方で、Go言語の関数には、可変長引数がありこれは、任意の数の引数を関数に渡すことができる機能で、fmt.Printlnなどでも使われています。また、無名関数を利用することで、関数内で動的に関数を生成することも可能です。これらの機能は、柔軟かつ表現力豊かなコードを書く上で重要だと感じました。

クロージャは、Go言語の強力な機能の一つです。関数の外で定義された変数を関数内で参照し、その値を変更できるのがクロージャの特徴です。これを応用することで、関数に状態を持たせることができ、より高度なプログラミングが可能になります。例えば、sort.Sliceでは、クロージャを利用してソート条件を指定しています。

また、defer文は、関数の終了時に必ず実行されるコードを登録するための機能です。これを使えば、ファイルのクローズ処理などを簡潔に記述でき、リソースの適切な管理が容易になります。deferは名前付き戻り値と組み合わせることで、エラーハンドリングにも活用できます。

Go言語では、すべての型がValueセマンティクスを持つため、関数の引数として渡された変数は、常にコピーが渡されます。これにより、関数内で引数の値を変更しても、呼び出し元の変数には影響を与えません。ただし、マップやスライスは参照型なので、関数内での変更が呼び出し元に反映されるという特殊な挙動を示します。

は、この違いを端的に表した例だと思います。

Exercisesでは、これまで学んだ関数に関する知識を活用する問題が用意されていました。計算機のプログラムにエラーハンドリングを追加したり、ファイルの長さを返す関数を書いたりと、実践的なコーディングの練習になりました。また、クロージャを使って、プレフィックスを付ける関数を生成する問題もあり、Go言語らしい関数の使い方が身につく内容でした。

github.com

Wrapping Upでは、この章で学んだことの総括として、Go言語の関数の特徴と、それを活かしたプログラミングの重要性が述べられていました。関数を適切に使いこなすことで、Goのコードをより効果的に構成できるようになるでしょう。

技術的な観点からは、可変長引数やクロージャ、名前付き戻り値など、Go言語特有の関数の機能についての理解が深まりました。特に、クロージャを利用した関数の実装は、Go言語らしいイディオムの一つだと感じました。また、defer文についても、リソース管理やエラーハンドリングにおける有用性を実感できました。

func main() {
    nums := []int{1, 2, 3, 4, 5}
    doubles := transform(nums, func(x int) int {
        return x * 2
    })
    fmt.Println(doubles)
}

func transform(slice []int, f func(int) int) []int {
    transformed := make([]int, len(slice))
    for i, v := range slice {
        transformed[i] = f(v)
    }
    return transformed
}

上のコードは、クロージャを利用して、スライスの各要素を変換するtransform関数の例です。このように、関数を引数として受け取ることで、柔軟な処理を実現できます。

総括すると、この章ではGo言語の関数について網羅的かつ体系的に学ぶことができました。関数宣言や複数の戻り値、可変長引数など、他の言語と共通する機能に加えて、クロージャdeferなど、Go言語特有の機能についても詳しく解説されていました。これらを適切に使いこなすことが、Goらしいコードを書く上で重要だと感じました。

また、学んだ関数の機能を実際のコードに落とし込む練習ができたのも良かったです。エラーハンドリングやファイル操作など、実用的な関数の書き方が身についたと思います。

関数は、プログラムを構成する上で中心的な役割を果たします。**Go言語の関数には、シンプルな書き方を維持しつつ、高度なことを実現するための機能が備わっています。

Chapter 6. Pointers

「Chapter 6. Pointers」は、Goプログラミングにおけるポインタの概念と活用方法を深く理解するために非常に重要な章です。ポインタは、他の言語ではしばしば難解で危険なものとして扱われることがありますが、Goではその扱いやすさと効率性が際立っています。

著者は、まずポインタの基本的な文法と動作について丁寧に解説しています。ポインタは、変数が格納されているメモリアドレスを保持する特別な変数であり、アドレス演算子(&)とデリファレンス演算子(*)を用いて操作します。そして、ポインタ型の宣言方法やnilポインタの概念についても触れています。

次に、著者はポインタの活用方法について、他の言語との比較を交えながら詳しく説明しています。Goでは、ポインタを使用するかどうかを開発者が選択できるため、不変性を保ちつつ、必要に応じてデータの変更を行うことができます。この柔軟性は、Goの強力な特徴の一つと言えるでしょう。

また、ポインタを関数の引数や戻り値として使用する際の注意点についても言及されています。特に、nilポインタを関数に渡した場合の動作や、ポインタのコピーがもたらす影響について、具体的なコード例を用いて解説されています。

さらに、著者はポインタの性能面でのメリットについても触れています。大きなデータ構造体をポインタで渡すことで、関数呼び出しのオーバーヘッドを削減できることが示されています。ただし、著者は安易なポインタの使用を戒めており、可能な限り値型を使用するべきだと主張しています。

Figure 6-5. The memory layout of a slice より引用

マップとスライスのポインタ実装の違いについても、詳細に解説されています。マップはポインタとして実装されているため、関数に渡すと元の変数に影響を与えますが、スライスは長さと容量の情報も含むため、より複雑な動作をします。特に、スライスの長さを変更しても元の変数には影響しないという特性は、バッファとしてスライスを活用する際に重要です。

Figure 6-9. Changing the capacity changes the storage より引用

メモリ割り当てとガベージコレクションについても、Goの特徴が詳しく解説されています。Goは、スタックとヒープを適切に使い分けることで、効率的なメモリ管理を実現しています。著者は、ヒープ割り当てを最小限に抑え、ガベージコレクターの負荷を減らすことの重要性を強調しています。この「機械的な共感」の考え方は、Goプログラミングにおいて非常に重要な概念だと言えます。

最後に、著者はガベージコレクターのチューニング方法についても触れています。GOGCとGOMEMLIMITの環境変数を適切に設定することで、ガベージコレクションの頻度やメモリ使用量を制御できることが示されています。

ポインタに関する実践的な演習問題が提供されています。Personの構造体を使ったポインタの活用や、スライスの動作の理解を深める問題など、ポインタの理解を深めるために有益な問題が用意されています。これらの演習を通して、読者はポインタの概念を実際のコードに落とし込む力を身につけることができるでしょう。

この章でポインタの重要性と適切な使用方法について再確認できました。著者は、次章で扱うメソッド、インターフェース、型についても、ポインタの理解が役立つことを示唆しています。

Chapter 7. Types, Methods, and Interfaces

「Chapter 7. Types, Methods, and Interfaces」は、Go言語のオブジェクト指向プログラミングの特徴を理解する上で非常に重要な章です。著者は、Goが他の言語とは異なるアプローチを取っていることを強調しつつ、型、メソッド、インターフェースの使い方とベストプラクティスを丁寧に解説しています。

Goの型システムは、シンプルでありながら非常に強力です。 著者は、ユーザー定義型の宣言方法や、型宣言が「実行可能なドキュメンテーション」としての役割を果たすことを説明しています。また、iotaを使った列挙型の定義方法についても触れ、iotaの適切な使用方法を示しています。

メソッドについては、レシーバーの指定方法や、ポインタレシーバーとバリューレシーバーの使い分けが重要なポイントです。著者は、nilインスタンスを適切に扱うためのテクニックや、メソッドが関数としても扱えることを示し、メソッドと関数の使い分け方についても言及しています。

Goのインターフェースは、型安全な「ダックタイピング(Duck typing)」を実現する強力な機能です。ダックタイピングとは、オブジェクトの 型(クラス)を明示的に宣言せずに 、オブジェクトの振る舞い(メソッド)やプロパティを利用することで、そのオブジェクトの型(クラス)を推測する手法です。。 著者は、インターフェースの暗黙的な実装がもたらす柔軟性と、明示的なインターフェースに比べた利点を詳しく説明しています。また、インターフェースとnilの関係や、インターフェースの比較可能性についても触れ、インターフェースを適切に使いこなすためのヒントを提供しています。

特に印象的だったのは、「Accept Interfaces, Return Structs」というアドバイスです。関数やメソッドの引数としてインターフェースを受け取り、戻り値としてコンクリートな型を返すことで、APIの柔軟性と保守性を高めることができます。 ただし、パフォーマンスとのトレードオフにも注意が必要です。

著者は、Goの暗黙的なインターフェースが、依存性の注入を容易にすることも指摘しています。サンプルコードを用いて、インターフェースを介して依存関係を外部化する方法を具体的に示しており、読者は実践的なスキルを身につけることができます。

type DataStore interface {
    UserNameForID(userID string) (string, bool)
}

type Logger interface {
    Log(message string)
}

type SimpleLogic struct {
    l  Logger
    ds DataStore
}

func (sl SimpleLogic) SayHello(userID string) (string, error) {
    sl.l.Log("in SayHello for " + userID)
    name, ok := sl.ds.UserNameForID(userID)
    if !ok {
        return "", errors.New("unknown user")
    }
    return "Hello, " + name, nil
}

func (sl SimpleLogic) SayGoodbye(userID string) (string, error) {
    sl.l.Log("in SayGoodbye for " + userID)
    name, ok := sl.ds.UserNameForID(userID)
    if !ok {
        return "", errors.New("unknown user")
    }
    return "Goodbye, " + name, nil
}

これまで学んだ概念を応用する練習問題が用意されています。バスケットボールリーグを管理するプログラムを作成する過程で、型、メソッド、インターフェースの使い方を体験的に学ぶことができます。これらの演習を通して、読者はGoの型システムに対する理解を深め、実践的なスキルを磨くことができると思います。章全体としてGoの型システムの特徴とベストプラクティスについて再確認しています。

Chapter 8. Generics

「Chapter 8. Generics」は、Go言語におけるジェネリクスの概念と使用方法を深く理解するために非常に重要な章です。ジェネリクスは、Go言語の型システムに大きな変革をもたらす機能であり、コードの再利用性と柔軟性を大幅に向上させることができます。 著者は、この章を通して、ジェネリクスの必要性、基本的な使い方、制限事項、そして適切な活用方法について丁寧に解説しています。

まず、著者はジェネリクスの必要性について説明しています。Go言語は静的型付け言語であり、関数やデータ構造の型を明示的に指定する必要があります。しかし、異なる型に対して同じロジックを適用したい場合、ジェネリクスがないと、コードの重複が避けられません。これは、コードの保守性を低下させ、バグを引き起こす可能性があります。ジェネリクスを使うことで、型に依存しないアルゴリズムを一度だけ実装し、様々な型に対して再利用できるようになります。

次に、著者はジェネリクスの基本的な使い方について説明しています。Go言語のジェネリクスは、型パラメータを使って実現されます。型パラメータは、関数やデータ構造の定義時に指定し、具体的な型の代わりに使用します。型パラメータには制約を設けることができ、許容する型を限定することができます。 これにより、コンパイル時の型安全性を確保しつつ、柔軟性を維持することができます。

著者は、スタック(stack)のデータ構造を例に、ジェネリクスの使い方を具体的に示しています。ジェネリックなスタックの実装では、要素の型を型パラメータで表現し、anycomparableという組み込みのインターフェースを制約として使用しています。これにより、任意の型の要素を持つスタックを、一つの実装で表現できます。また、comparableを使うことで、要素の比較が必要な操作も、型安全に行えるようになります。

さらに、著者はジェネリックな関数 MapReduceFilter の実装を紹介しています。これらの関数は、スライスに対する一般的な操作を抽象化したもので、様々な型のスライスに適用できます。これにより、コードの重複を大幅に削減でき、アルゴリズムの本質に集中できるようになります。

また、著者はジェネリクスとインターフェースの関係についても説明しています。インターフェースを型制約として使うことで、ジェネリックな型に特定のメソッドを要求できます。 これにより、より細かな制約を設けることができ、コードの安全性を高められます。さらに、インターフェース自体をジェネリック化することで、より柔軟な抽象化が可能になります。

型の要素(type elements)についても詳しく解説されています。型の要素を使うことで、特定の演算子をサポートする型だけを受け入れるジェネリックな関数を定義できます。 これは、数値計算などで特に役立ちます。著者は、Integerというインターフェースを定義し、整数型に対する演算を抽象化する例を示しています。

ジェネリックな関数とデータ構造を組み合わせることで、より汎用的なコードを書くことができます。著者は、バイナリツリーの例を用いて、比較関数をジェネリック化することで、任意の型に対応できるようになることを示しています。これにより、コードの再利用性が大幅に向上します。

type OrderableFunc[T any] func(t1, t2 T) int

func NewTree[T any](f OrderableFunc[T]) *Tree[T] {
    return &Tree[T]{
        f: f,
    }
}

comparableインターフェースとジェネリクスの関係についても、注意点が説明されています。comparableを型制約として使う場合、比較可能でない型が渡されるとランタイムパニックが発生する可能性があります。これを防ぐためには、コンパイル時に型チェックを行う必要があります。

また、著者はジェネリクスの現在の制限についても言及しています。Go言語のジェネリクスは、他の言語に比べてシンプルな設計になっており、特殊化やカリー化、メタプログラミングなどの機能は提供されていません。 これは、Go言語のシンプルさと読みやすさを維持するための判断だと考えられます。

ジェネリクスの導入により、Go言語のイディオマティックな書き方にも変化が生じます。著者は、float64を汎用的な数値型として使う慣習が廃れ、anyinterface{}に取って代わることを指摘しています。また、ジェネリクスを使うことで、異なる型のスライスを統一的に扱えるようになります。ただし、既存のコードをジェネリクスに置き換える際は、慎重に行う必要があります。

パフォーマンスへの影響については、まだ評価が定まっていません。一部のケースでは、ジェネリクスを使うことでコードが遅くなることが報告されています。しかし、ソートアルゴリズムでは、ジェネリクスを使うことで速度が向上するという報告もあります。著者は、可読性と保守性を重視しつつ、必要に応じてベンチマークを取ることを推奨しています。

標準ライブラリへのジェネリクスの導入は慎重に行われています。 当初はanycomparableのみが追加されましたが、Go 1.21からは、スライスとマップ、並行処理に関する関数が追加されています。これらの関数は、よく使われる操作を抽象化し、コードの重複を削減するのに役立ちます。今後も、ジェネリクスを活用した新しい関数やデータ型が追加されていくことが期待されます。

最後に、著者はジェネリクスによって可能になる将来の機能について言及しています。ジェネリクスを基礎として、より高度な型システムを構築できる可能性があります。 例えば、和型(sum types)を導入することで、型安全性を高めつつ、柔軟なデータ表現が可能になります。また、Goの列挙型の弱点を克服する手段としても、和型は有望視されています。

本章ではジェネリクスに関する実践的な演習問題が用意されています。整数と浮動小数点数の両方に対応する関数の作成や、特定の型を要求するインターフェースの定義など、ジェネリクスの基本的な使い方から応用までを網羅しています。これらの演習を通して、読者はジェネリクスの概念を実際のコードに落とし込む力を身につけることができるでしょう。

github.com

章全体の内容を振り返り、ジェネリクスの重要性と将来の可能性について再確認できました。著者は、ジェネリクスがGo言語の表現力を高め、コードの再利用性を向上させる強力な機能であると強調しています。 一方で、Go言語のシンプルさを維持するために、ジェネリクスの機能は意図的に制限されています。これからのGo言語の発展において、ジェネリクスがどのように活用されていくのか、楽しみにしていると述べています。

ジェネリクスは、Go言語の未来を切り拓く重要な機能であると言えます。 それを適切に使いこなすことで、より柔軟で保守性の高いコードを書けるようになるでしょう。一方で、ジェネリクスの濫用は、かえってコードの複雑さを増し、可読性を損なう恐れがあります。今後、Go言語のエコシステムにおいて、ジェネリクスを活用したライブラリやフレームワークが登場することが期待されますが、それらを適切に評価し、選択していく眼を養う必要があります。

Chapter 9. Errors

「Chapter 9. Errors」は、Go言語におけるエラーハンドリングの基本から応用までを網羅的に解説した章です。この章を通して、Go言語のエラー処理の特徴と、それを適切に使いこなすためのテクニックについて理解を深めることができました。

Go言語のエラー処理は、他の言語の例外処理とは一線を画しています。Goでは、エラーは関数の戻り値として返され、呼び出し元で明示的にチェックする必要があります。 これは一見、冗長で面倒に感じるかもしれませんが、実際には、コードの流れを明確にし、エラーを見落とすリスクを減らすことができます。著者は、この設計の背景にある「Goの哲学」について丁寧に説明しており、納得感を持って読み進めることができました。

Go言語のベストプラクティスとして、「Accept interfaces, return structs」という原則があります。これは、関数やメソッドの引数としてインターフェースを受け取り、戻り値として具体的な構造体を返すことを推奨するものです。エラー処理においても、この原則を応用し、具体的なエラー型を返すことで、呼び出し元が適切にエラーを処理できるようになります。

特に印象的だったのは、カスタムエラー型の定義方法と活用方法です。Goでは、エラーをただの文字列ではなく、構造体として定義することができます。これにより、エラーに付加情報を持たせたり、エラーの種類によって処理を変えたりすることが可能になります。著者は、カスタムエラー型の定義方法から、そのメソッドの実装、そしてerrors.Iserrors.Asを使った高度なエラー処理までを、具体的なコード例を交えて解説しています。

type MyError struct {
    Codes []int
}

func (me MyError) Error() string {
    return fmt.Sprintf("codes: %v", me.Codes)
}

func (me MyError) Is(target error) bool {
    if me2, ok := target.(MyError); ok {
        return slices.Equal(me.Codes, me2.Codes)
    }
    return false
}

また、エラーのラップ(wrapping)とアンラップ(unwrapping)についても詳しく解説されていました。fmt.Errorf%w動詞を使えば、元のエラーを失わずに新しいエラーメッセージを追加できます。これにより、エラーが発生した場所や状況を詳細に伝えつつ、根本原因を追跡することができます。逆に、errors.Unwrapを使えば、ラップされたエラーから元のエラーを取り出すことができます。

実際にカスタムエラー型を定義し、エラーをラップ・アンラップする練習問題が用意されていました。これらの問題を通して、エラー処理の基本的な書き方だけでなく、より実践的なテクニックも身につけることができました。特に、複数のエラーをまとめて返す方法や、deferを使ったエラーハンドリングの例は、実際のプロジェクトでも役立つと感じました。

github.com

一方で、panicrecoverについては、慎重に使うべきだと改めて認識しました。 panicは、回復不可能なエラーが発生した場合に使うべきであり、安易に使うとかえってコードの可読性を損ねてしまいます。recoverは、panicからの復帰を可能にしますが、ライブラリのAPI境界を越えてpanicを伝播させるべきではありません。著者は、panicrecoverの適切な使い方について、具体的な指針を示しています。

func div60(i int) {
    defer func() {
        if v := recover(); v != nil {
            fmt.Println(v)
        }
    }()
    fmt.Println(60 / i)
}

この章でGo言語のエラー処理の特徴とベストプラクティスが再確認されています。 エラーを単なる例外ではなく、値として扱うことで、より柔軟で表現力豊かなエラーハンドリングが可能になります。一方で、その自由度ゆえに、適切なエラー処理を行うには、一定の規律と経験が必要になります。

総括すると、この章ではGo言語のエラー処理について体系的に学ぶことができました。 エラーを値として扱う考え方や、カスタムエラー型の定義方法、エラーのラップとアンラップ、panicrecoverの適切な使い方など、Go言語ならではのエラー処理の特徴と、それを活かすためのベストプラクティスについて理解を深めることができました。

特に、実際のコードを書く上では、エラー処理を適切に行うことが、コードの品質と保守性を大きく左右します。 単にエラーを無視するのではなく、適切にエラーをハンドリングし、ログ出力や監視システムと連携させることが重要です。また、ライブラリやパッケージを設計する際は、エラーをどのように定義し、どのように返すかを慎重に検討する必要があります。

Chapter 10. Modules, Packages, and Imports

「Chapter 10. Modules, Packages, and Imports」は、Go言語におけるコード管理と外部ライブラリの利用について、非常に重要な概念を丁寧に解説した章です。この章を通して、私はGoのモジュールシステムの特徴と、それを活用するためのベストプラクティスについて理解を深めることができました。

まず、モジュール、パッケージ、レポジトリの関係性について明確に説明されていました。モジュールはソースコードの集合体であり、バージョン管理されるユニットです。パッケージはモジュールを構成する要素であり、ディレクトリと1対1で対応します。レポジトリはモジュールを格納する場所です。これらの概念を正しく理解することは、Goでのコード管理を行う上で欠かせません。

次に、go.modファイルの役割と記述方法について解説されていました。go.modファイルは、モジュールのメタデータとその依存関係を記述するファイルです。moduleディレクティブでモジュールのパスを宣言し、goディレクティブで必要なGoのバージョンを指定し、requireディレクティブで依存モジュールとそのバージョンを記述する。この構文を理解することで、自分のモジュールを適切に定義し、外部モジュールを利用できるようになります。

また、パッケージの作成方法と命名規則、内部パッケージの役割などについても詳しく説明されていました。パッケージ名はディレクトリ名と一致させるべきであり、機能を適切に分割し、依存関係を最小限に抑えるべきです。 内部パッケージを使えば、モジュール内だけで共有したいコードを適切に隠蔽できます。これらのベストプラクティスを意識することで、保守性の高いコードを書けるようになるでしょう。

さらに、GoDocコメントの書き方と、pkg.go.devを使ったドキュメントの公開方法も紹介されていました。適切なGoDocコメントを書くことで、自分のパッケージをわかりやすく説明でき、pkg.go.devで自動的にドキュメントを公開できます。これにより、他の開発者が自分のパッケージを使いやすくなり、オープンソースへの貢献にもつながります。

モジュールのバージョニングについては、セマンティックバージョニングのルールに従うべきだと強調されていました。APIの互換性を維持しつつ、適切にバージョンを上げていくことが重要です。不適切なバージョンを公開してしまった場合の対処方法として、retractディレクティブの使い方も説明されていました。

モジュールプロキシとチェックサムデータベースの仕組みについても、セキュリティの観点から重要な説明がありました。デフォルトではGoogleが運営するプロキシサーバとチェックサムデータベースが使われ、モジュールの整合性が検証されます。 必要に応じて、独自のプロキシサーバを立てたり、プロキシを無効化したりできることも示されていました。

また、GoのWorkspaceを使えば、複数のモジュールを同時に編集できることが紹介されていました。これにより、モジュール間の変更を簡単にテストでき、開発の効率が上がります。 ただし、Workspaceの情報をバージョン管理システムにコミットしないよう注意が必要です。

サンプルコードを見ると、モジュールの作成から公開、利用までの一連の流れが具体的に示されていました。go mod initでモジュールを初期化し、go getで依存関係を解決し、go buildでビルドする。このような一連のコマンドを適切に使いこなすことが、Goでの開発には欠かせません。

$ go mod init github.com/learning-go-book-2e/money
$ go get ./...
$ go build

Exercisesでは、自分でモジュールを作成し、バージョニングやドキュメンティングを実践する課題が用意されていました。実際にコードを書いて、モジュールの作成から公開までの流れを体験することは、理解を深める上で非常に有効だと感じました。

// Add adds two numbers together and returns the result. 
//
// More information on addition can be found at [https://www.mathsisfun.com/numbers/addition.html](https://www.mathsisfun.com/numbers/addition.html).
func Add[T Number](a, b T) T {
    return a + b
}

Goの優れたモジュールシステムを活用することで、コードの管理がしやすくなり、外部ライブラリも安全に利用できるようになります。一方で、適切なバージョニングやドキュメンティングを行うことが、モジュールの作者としての責務であることも強調されていました。

この章ではGoにおけるコード管理と外部ライブラリ利用のベストプラクティスについて、体系的に学ぶことができました。モジュール、パッケージ、レポジトリの関係性、go.modファイルの記述方法、パッケージの設計原則など、Goでの開発に欠かせない知識が丁寧に解説されていました。またモジュールのバージョニングやドキュメンティングの重要性についても、実例を交えて説明されていました。

特に、モジュールプロキシとチェックサムデータベースの仕組みは、Goの優れたエコシステムを支える重要な基盤だと感じました。セキュリティと利便性を高いレベルで両立させているGoの設計思想に、改めて感銘を受けました(以前、何かの勉強会で聞いた気がするがすっかり忘れていた)。

Chapter 11. Go Tooling

「Chapter 11. Go Tooling」は、Goプログラミングにおける開発ツールの重要性と活用方法について深く理解するための重要な章でした。この章を通して、私はGoの豊富な標準ツールと、サードパーティのツールを組み合わせることで、より効率的で高品質なコードを書けるようになると実感しました。

著者は、まずgo runを使って小さなプログラムを素早く試す方法を紹介しました。これにより、コンパイルと実行を一度に行え、スクリプト言語のような気軽さでGoを使えるようになります。Goがコンパイル言語でありながら、インタプリタ言語のような利便性も兼ね備えている点が印象的でした。

次に、go installを使ってサードパーティのツールをインストールする方法が解説されました。Goのツールエコシステムは非常に充実しており、多くの優れたツールがオープンソースで公開されています。go installを使えば、それらのツールを簡単にインストールし、自分の開発環境に取り込むことができます。

また、著者はgoimportsを使ってインポートの整形を改善する方法も紹介しました。これはgo fmtの機能を拡張したもので、不要なインポートを削除し、必要なインポートを自動的に追加してくれます。コードの可読性と保守性を高めるために、goimportsを活用すべきだと感じました。

コード品質をチェックするツールとして、staticcheckrevivegolangci-lintが紹介されていました。これらのツールは、潜在的なバグや非効率的なコードを検出し、Goのベストプラクティスに沿ったコードを書くのに役立ちます。特にgolangci-lintは、多数のリンターを統合的に使える便利なツールだと感じました。

また、著者はgovulncheckを使って脆弱性のある依存関係をスキャンする方法も解説しました。サードパーティのライブラリを利用する際は、既知の脆弱性がないかチェックすることが重要です。govulncheckを活用することで、セキュリティリスクを早期に発見し、対処できるようになります。

さらに、go:embedを使ってコンテンツをプログラムに埋め込む方法や、go generateを使ってコード生成を自動化する方法も紹介されていました。これらの機能を活用することで、より柔軟でメンテナンスしやすいコードを書けるようになると感じました。

著者は、クロスコンパイルやビルドタグを使って、異なるプラットフォームやバージョンのGoに対応する方法も解説しました。Goのポータビリティの高さを活かすためには、これらの機能を理解し、適切に使いこなすことが重要だと感じました。

Exercisesでは、埋め込みやクロスコンパイル、静的解析など、この章で学んだ機能を実践的に使う課題が用意されていました。実際にコードを書いて試すことで、ツールの使い方や注意点を体験的に学ぶことができました。

//go:embed english_rights.txt
var englishRights string

//go:embed all:
var allRights embed.FS

func main() {
    if len(os.Args) != 2 {
        fmt.Println("Please specify a language")
        os.Exit(1)
    }
    language := os.Args[1]
    data, err := allRights.ReadFile(language + "_rights.txt")
    if err != nil {
        fmt.Printf("No UDHR found for language %s\n", language)
        os.Exit(1)
    }
    fmt.Println(string(data))
}

Goの標準ツールとサードパーティのツールを適切に組み合わせることで、より効率的で高品質なコードを書けるようになります。一方で、ツールを過信せず、その出力を批判的に評価することも大切だと強調されていました。

この章ではGoの開発ツールについて網羅的に学ぶことができました。go rungo installなどの基本的なツールの使い方から、staticcheckgolangci-lintなどの高度なリンターの活用法、go:embedgo generateなどの特殊機能まで、Goでの開発に欠かせないツールの数々が丁寧に解説されていました。

特に、サードパーティのツールを積極的に活用することの重要性を再認識しました。Goの標準ツールは非常に充実していますが、コミュニティの知恵を結集したサードパーティのツールを併用することで、さらに開発の生産性と品質を高められると感じました。

Chapter 12. Concurrency in Go

「Chapter 12. Concurrency in Go」を通して、Go言語の並行処理モデルの特徴と使い方、そしてベストプラクティスについて深く理解することができました。この章は、Go言語を使いこなす上で欠かせない重要なトピックを丁寧に解説しており、実践的な知識を身につけるのに最適だと感じました。また、並列処理に関してはブログも書籍もたくさんあるので気になる方がいましたら是非にです。

Go言語の並行処理モデルは、Communicating Sequential Processes(CSP)をベースにしており、その中心的な概念がgoroutinechannelです。goroutineは、Goランタイムによって管理される軽量のスレッドのようなもので、OS レベルのスレッドよりもはるかに少ないオーバーヘッドで大量に生成・管理できます。一方、channelは、goroutine間でデータを共有するためのパイプのようなもので、型安全でデッドロックを防ぐための仕組みが備わっています。

著者は、まず並行処理を使うべきケースについて説明しました。並行処理は、必ずしもプログラムを高速化するわけではなく、IO バウンドなタスクでない限り、オーバーヘッドが大きくなる可能性があると指摘しています。そのため、並行処理を適用する前に、ベンチマークを取って本当にメリットがあるかを確認すべきだと強調していました。

次に、goroutineとchannelの基本的な使い方が解説されました。goroutineは、goキーワードを関数呼び出しの前に置くだけで簡単に生成でき、channelはmake関数で作成します。unbuffered channelとbuffered channelの違いや、for-rangeループを使ったchannelの読み取り方法なども丁寧に説明されていました。

また、channelを適切にクローズすることの重要性も強調されていました。クローズされたchannelからの読み取りは、その型のゼロ値を返すという特殊な動作を理解しておく必要があります。複数のgoroutineが同じchannelに書き込む場合は、sync.WaitGroupを使ってすべてのgoroutineの終了を待ってからクローズすべきだとのアドバイスもありました。

select文は、複数のchannelを扱う際に欠かせない重要な構文です。selectを使えば、複数のchannelを監視し、読み書き可能になったchannelを選択して処理できます。著者は、selectデッドロックを防ぐ上で重要な役割を果たすことを具体的なコード例で示していました。

続いて、並行処理のベストプラクティスとパターンが紹介されました。特に印象的だったのは、APIをconcurrency-freeに保つべきというアドバイスです。並行処理はあくまで実装の詳細であり、ユーザーに不要な複雑さを押し付けるべきではないというのは、納得のいく指摘だと感じました。

また、goroutineのリークを防ぐために、必ず終了するようにすべきだという点も重要でした。contextパッケージを使ってgoroutineをキャンセルする方法や、for-selectループから適切に抜ける方法などが具体的に示されていました。

バッファ付きchannelの使いどころについても、詳しく解説されていました。バッファ付きchannelは、goroutineの数を制限したり、キューに溜まった処理を制御したりするのに便利です。一方で、不適切に使うとデッドロックを引き起こす可能性もあるため、慎重に検討すべきだと強調されていました。

また、バックプレッシャーを実装する方法として、バッファ付きchannelとselect文を組み合わせるアプローチが紹介されていました。これにより、同時実行数を制限しつつ、処理を適切にブロックできるようになります。

さらに、select文のcaseを動的にオン・オフする方法として、nil channelを活用するテクニックも面白かったです。クローズしたchannelからの読み込みが、ゼロ値を返し続けてしまう問題を回避できる優れたアイデアだと感じました。

一方で、mutexについても適切に使いこなすことの重要性が述べられていました。goroutineとchannelだけでは実現が難しい、共有リソースの保護などでは、mutexが適していると指摘されていました。ただし、mutexの濫用は避けるべきで、可能な限りchannelを使うのがよいとのアドバイスもありました。

最後に、これまで学んだ概念を組み合わせて、非同期なパイプラインを実装するサンプルコードが示されていました。goroutine、channel、selectcontextを適切に使い分けることで、タイムアウト制御や複数のWeb APIを並行に呼び出す処理を、わずか100行程度の読みやすいコードで実現できることはめちゃくちゃメリットだと思います。

練習問題では、goroutineとchannelを使った基本的な並行処理プログラムから、selectsync.WaitGroupを使ったより複雑な処理まで、幅広い題材が扱われていました。自分で実際にコードを書いて試すことで、並行処理の基本的なパターンが身についたと実感しています。

github.com

この章を通して、Go言語の並行処理モデルの優れた設計思想と、それを適切に使いこなすためのベストプラクティスについて深く理解することができました。goroutineとchannelを中心とするシンプルな仕組みの中に、デッドロックを防ぎつつ安全に並行処理を行うための知恵が詰まっていることを実感しました。

一方で、並行処理の適用は慎重に検討すべきであり、安易に使うとかえって複雑さを増してしまうことも学びました。並行処理はあくまでツールであり、ボトルネックの特定と適切な設計が何より重要だというのは、肝に銘じるべき教訓だと感じました。

特に、現実のシステム開発においては、並行処理とエラーハンドリング、リソース管理などを総合的に考える必要があります。goroutineのリークを防ぎ、適切にリソースを解放する方法や、mutexとchannelの使い分け方など、実践的なスキルを再確認できたのは良かったです。

私自身、普段からGoを使った並行処理プログラムを書くことが多いのですが、この章で得た知見を活かして、より堅牢で効率的なコードを書けるようになりたいと思います。特に、contextパッケージを活用したgoroutineのキャンセル処理や、バッファ付きチャネルを使ったバックプレッシャーの実装などは、実際のプロジェクトですぐにでも試してみたいテクニックです。

並行処理はGoの最も強力な武器の一つですが、それを適切に使いこなすためには、深い理解と経験が必要不可欠です。この章で学んだ基本的な概念と、数多くのベストプラクティスを、自分の経験として血肉化していくことが、Goのプロフェッショナルとして成長するための鍵になるのだと感じました。

Chapter 13. The Standard Library

「Chapter 13. The Standard Library」では、Goの標準ライブラリの中でも特に重要なパッケージについて深く掘り下げています。この章を読んで、Goの標準ライブラリがいかにベストプラクティスに基づいて設計されているかを強く実感しました。そこには、他の言語の標準ライブラリにはない優れた設計思想が随所に見られます。

印象的だったのは、「io」パッケージの設計です。「io.Reader」と「io.Writer」というシンプルなインターフェースを中心に、様々な入出力処理を抽象化している点は、Goならではの美しい設計だと感じました。この設計のおかげで、ファイルやネットワーク、圧縮、暗号化など、様々な入出力処理を統一的に扱えるようになっています。

また、「time」パッケージも非常に使いやすく設計されています。「time.Duration」と「time.Time」という2つの型を中心に、時間関連の処理を直感的に記述できるのは、Goの大きな強みだと思います。特に、モノトニック時間の採用により、リープセカンドなどの影響を受けずに正確な時間計測ができるようになっているのは、システムプログラミングにおいて重要な点だと感じました。

「encoding/json」パッケージは、構造体のタグを活用してJSONエンコーディングとデコーディングを制御できる点が優れています。これにより、JSONのフィールド名と構造体のフィールド名を柔軟にマッピングできるだけでなく、フィールドの省略やデフォルト値の指定なども簡単に行えます。以下のサンプルコードのように、タグを使ってJSONのフィールド名を指定できるのは非常に便利です。

type Person struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
}

「net/http」パッケージは、Goの標準ライブラリの中でも特に重要なパッケージの1つです。「http.Handler」インターフェースを中心としたシンプルな設計により、高性能で使いやすいHTTPサーバーとクライアントを簡単に構築できます。また、ミドルウェアのパターンを活用することで、ログ出力やエラーハンドリング、認証、圧縮など、様々な機能を柔軟に追加できる点も優れています。

「log/slog」パッケージは、2023年にリリースされたGo 1.21で新たに追加された構造化ロギングのためのパッケージです。従来の「log」パッケージの課題を解決し、より使いやすく、パフォーマンスにも優れた設計になっています。以下のように、ログレベルやコンテキスト、属性などを柔軟に指定できるのが特徴です。

logger := slog.New(slog.NewTextHandler(os.Stdout))
logger.Info("Hello, World!", "name", "Alice", "age", 30)

これらの知識を活用して、現在時刻を返すWebサーバーや、ミドルウェアを使ったJSONログ出力、JSONとテキストの切り替えなどを実装する課題が用意されていました。これらの課題に取り組むことで、標準ライブラリの使い方をより深く理解することができました。

Goの標準ライブラリがベストプラクティスに基づいて設計されていること、そして後方互換性を尊重しながら進化を続けていることが改めて強調されていました。Goの標準ライブラリは、私たちが日々のプログラミングで参考にすべき優れたお手本だと言えます。

個人的な感想としては、Goの標準ライブラリは、シンプルさと実用性のバランスが非常に優れていると感じました。必要十分な機能を提供しながらも、無駄に複雑になることなく、使いやすさを追求しているのが印象的です。特に、インターフェースを活用した抽象化と、具体的な実装の使い分けが絶妙だと思います。

また、Goの標準ライブラリは、並行処理やエラーハンドリング、テストなど、現代的なプログラミングに欠かせない要素をしっかりとサポートしているのも大きな特徴だと感じました。これらの機能を標準ライブラリレベルでサポートしているからこそ、Goは大規模なシステム開発に適した言語になっているのだと思います。

Chapter 14. The Context

「Chapter 14. The Context」は、Go言語プログラミングにおける重要な概念である「コンテキスト」について深く掘り下げた章でした。コンテキストは、リクエストのメタデータを管理し、タイムアウトやキャンセル、値の受け渡しを制御するための強力な仕組みです。この章を通して、コンテキストの適切な使い方とベストプラクティスについて理解を深めることができました。

zenn.dev

Go言語による並行処理にもContext について記載してあるので読んで下さい。

learning.oreilly.com

著者は、まずコンテキストの基本的な文法と使い方から説明しています。コンテキストは、context.Contextインターフェースを満たす値として表現され、関数の第一引数として明示的に渡すのが慣例です。context.Background関数でルートコンテキストを作成し、そこから子コンテキストを派生させていくのが基本的なパターンだと学びました。

HTTPサーバーでコンテキストを使う場合は、http.RequestContextメソッドとWithContextメソッドを使って、ミドルウェア間でコンテキストを受け渡しするのが適切な方法だと分かりました。ハンドラ関数では、req.Context()でコンテキストを取得し、ビジネスロジックの第一引数として渡すべきだと強調されていました。

コンテキストの主な用途の一つが、キャンセル処理だと理解しました。context.WithCancel関数でキャンセル可能なコンテキストを作成し、キャンセル関数を適切にdeferすることで、リソースのリークを防げることが示されていました。Goの標準ライブラリのHTTPクライアントは、コンテキストのキャンセルを尊重して、リクエストを適切に中止してくれるそうです。

また、context.WithTimeoutcontext.WithDeadlineを使えば、コンテキストに時間制限を設定できることも分かりました。これにより、リクエストの処理時間を適切に管理し、サーバーのリソースを公平に配分できるようになります。子コンテキストのタイムアウトは、親コンテキストのタイムアウトに制約されるというルールも重要だと感じました。

コンテキストのキャンセル原因を伝えるために、context.WithCancelCausecontext.Causeを使う方法も印象的でした。エラーの伝搬と情報の集約に、コンテキストが効果的に活用できることが分かりました。一方で、キャンセル関数の呼び出しを複数回行っても問題ないという点は、意外でした。

自作のコードでコンテキストのキャンセルをサポートする場合は、select文でctx.Done()をチェックするパターンと、定期的にcontext.Cause(ctx)をチェックするパターンの2つが紹介されていました。長時間実行される処理では、コンテキストのキャンセルに対応することが重要だと再認識しました。

Exercisesでは、これまで学んだコンテキストの知識を活用する問題が用意されていました。ミドルウェアタイムアウトを設定する課題や、時間制限付きの計算を行う課題、ロギングレベルをコンテキストで制御する課題など、実践的なユースケースが網羅されていたと思います。

github.com

冒頭でも述べたように、コンテキストはGo言語プログラミングにおける重要な概念です。この章を通して、コンテキストの適切な使い方とベストプラクティスについて体系的に学ぶことができました。特に、キャンセル処理とタイムアウト制御は、サーバーの堅牢性と公平性を確保する上で欠かせない機能だと実感しました。

一方で、コンテキストの乱用は、かえってコードの複雑さを増してしまう恐れがあります。コンテキストは、主にリクエストスコープのメタデータを扱うために使うべきで、安易に値の受け渡しに使うのは避けるべきだと強調されていました。この指針は、コンテキストを適切に使いこなす上で重要だと感じました。

自作のコードでコンテキストのキャンセルをサポートすることの重要性も再認識できました。特に、select文とctx.Done()を使ったパターンは、Goらしい簡潔で効果的な手法だと感じました。長時間実行される処理では、定期的にコンテキストのキャンセルをチェックすることを習慣づけたいと思います。

func longRunningTask(ctx context.Context) error {
    for {
        select {
        case <-ctx.Done():
            return ctx.Err()
        default:
            // Do some work
        }
    }
}

この章ではGoのコンテキストについて網羅的かつ実践的に学ぶことができました。コンテキストの基本的な使い方から、キャンセル処理、タイムアウト制御、値の受け渡しまで、幅広いトピックが丁寧に解説されていました。Exercisesで提示された問題は、コンテキストの理解を深め、実際のコードに落とし込む力を養うのに役立つ内容でした。

コンテキストは、Goのプログラミングスタイルと哲学を体現する重要な機能だと言えます。明示的なデータの受け渡しを重視しつつ、キャンセルとタイムアウトという横断的な関心事をエレガントに扱える点が、Goらしさを感じさせます。一方で、その柔軟性ゆえに、適切に使いこなすには一定の規律と見識が求められます。

Chapter 15. Writing Tests

「Chapter 15. Writing Tests」は、Goにおけるテストの書き方と品質向上のためのツールについて詳細に解説している章です。この章を読んで、ユニットテストの書き方、テーブルテストの実行、テストのセットアップとティアダウン、HTTPのスタブ化、ファジングテスト、データ競合の検出など、テストに関する多くの知見を得ることができました。また、テストに関しては他の書籍を読んでも良いかもと思いました。

特に印象に残ったのは、テーブルテストの実行方法です。テーブルテストを使うことで、同じテストロジックに対して様々なデータパターンを適用することができ、コードの可読性と保守性が向上します。また、並行テストの実行方法も非常に興味深かったです。並行テストを適切に実行することで、テストの実行時間を大幅に短縮できます。ただし、共有される可変状態に依存するテストを並行して実行すると、予期しない結果になる可能性があるため注意が必要です。

コードカバレッジの計測も重要なトピックの一つでした。go test -coverを使ってカバレッジを計測し、go tool coverカバレッジ情報を可視化する方法は、テストの網羅性を確認する上で非常に有用です。ただし、100%のコードカバレッジを達成しても、バグが存在する可能性があることに注意が必要です。

ファジングテストは、ランダムなデータを生成してコードに入力し、予期しない入力に対する動作を検証する手法です。ファジングテストを行うことで、開発者が想定していなかったようなコーナーケースのバグを発見することができます。また、データ競合の検出には-raceフラグを使用します。これにより、複数のゴルーチンから同時にアクセスされる変数を特定し、適切なロックを設定することができます。

サンプルコードとして、sample_code/adderディレクトリにあるaddNumbers関数のテストコードが示されていました。また、sample_code/tableディレクトリではテーブルテストの例が、sample_code/solverディレクトリではスタブを使ったテストの例が示されていました。これらのサンプルコードは、実際のテストコードを書く際の参考になります。

Exercisesでは、Simple Web Appプログラムに対してユニットテストを書き、コードカバレッジを計測することが求められていました。また、データ競合を検出して修正し、parser関数に対してファジングテストを実行することも求められていました。これらの演習を通じて、テストに関する知識を実践的に応用することができます。

この章で学んだテストとコード品質向上のためのツールについて総括されていました。次の章では、unsafeパッケージ、リフレクション、cgoなど、Goの一般的なルールを破るような機能について探求していくとのことでした。

総括すると、この章ではGoにおけるテストの書き方とコード品質向上のためのツールについて網羅的に解説されていました。ユニットテストの書き方、テーブルテストの実行、並行テストの実行、コードカバレッジの計測、ファジングテスト、データ競合の検出など、テストに関する重要なトピックが幅広くカバーされていました。また、サンプルコードや演習問題も充実しており、実践的な知識を身につけることができる内容でした。

Chapter 16. Here Be Dragons: Reflect, Unsafe, and Cgo

「Chapter 16. Here Be Dragons: Reflect, Unsafe, and Cgo」を通して、Go言語プログラミングにおいて、安全性と規約を一時的に無視してメモリやデータの細かな操作を行う仕組みであるreflect、unsafe、cgoについて深く理解することができました。これらの機能は、Go言語のセキュリティと型安全性を一時的に破ることから、非常に注意深く扱う必要があります。しかし、特定の場面では威力を発揮するため、適切な活用方法を学ぶことが重要だと感じました。

reflect

reflectは、Go言語の型システムを動的に操作するための強力な仕組みです。コンパイル時には型が特定できない場合、または外部データを動的にマッピングする必要がある場合などに、reflectを使って型の情報を取得したり、値を設定することができます。reflectで扱う主な概念は「型(reflect.Type)」「種類(reflect.Kind)」「値(reflect.Value)」の3つです。

reflectを使えば、構造体のフィールドにアクセスしたり構造体を生成できますが、コーディングが冗長になりがちで、正しい操作を行わないとパニックを起こす可能性があります。そのため、適切なエラーハンドリングと注釈を残すことが重要です。特に、型の種類(reflect.Kind)に応じて、呼び出し可能なメソッドが変わるため、種類をしっかり確認する必要があります。

type Foo struct {
    A int    `myTag:"value"`
    B string `myTag:"value2"`
}

var f Foo
ft := reflect.TypeOf(f)
for i := 0; i < ft.NumField(); i++ {
    curField := ft.Field(i)
    fmt.Println(curField.Name, curField.Type.Name(),
        curField.Tag.Get("myTag"))
}

上記のサンプルコードは、reflectを使って構造体のタグフィールドにアクセスする方法を示しています。このように、reflectは型情報を動的に取得したり、値を設定・生成したりするのに役立ちますが、過度に使い過ぎるとコードの可読性を下げる恐れがあります。

reflectを利用する主なユースケースは、データベースやJSONなどの外部データの読み書き、テンプレートエンジンによるデータのレンダリング、型に依存しないソートアルゴリズムの実装などです。Go言語の標準ライブラリでも、これらの用途でreflectが活用されています。

一方、reflectを使うと通常の処理に比べてパフォーマンスが大幅に低下するという課題もあります。サンプルコードのベンチマークでは、reflectを使ったフィルタ処理は通常の場合に比べて50~75倍も遅く、多数のメモリ確保を行うことが分かります。

BenchmarkFilterReflectString-8     5870  203962 ns/op  46616 B/op  2219 allocs/op
BenchmarkFilterGenericString-8   294355    3920 ns/op  16384 B/op     1 allocs/op
BenchmarkFilterString-8          302636    3885 ns/op  16384 B/op     1 allocs/op

そのため、必要不可欠な場面でのみreflectを使い、通常の処理ではジェネリクスなどの代替手段を使うべきです。

ただし、マーシャリング・アンマーシャリングや動的なメモ化などは、reflectを使わざるを得ない場合もあります。このように、Go言語の規約を一時的に無視する仕組みとして、reflectを適切に使いこなすことが重要だと言えます。

unsafe

unsafeパッケージは、Go言語の型安全性とメモリ安全性をある程度無視して、低水準のメモリ操作を行う仕組みを提供します。主な使用例は、OSとのシステムコール連携や、バイナリデータの高速なマーシャリング・アンマーシャリングなどです。内部で扱われるデータ型はunsafe.Pointerで、任意のポインタ型やuintptrとの相互キャストが可能です。

メモリのレイアウトを調べるためのunsafe.Sizeofunsafe.Offsetof関数があり、構造体フィールドの並び替えによるメモリ使用量の最適化などに役立ちます。

type BoolInt struct {
    b bool
    i int64
}

type IntBool struct {
    i int64
    b bool
}

fmt.Println(unsafe.Sizeof(BoolInt{}), unsafe.Offsetof(BoolInt{}.b), unsafe.Offsetof(BoolInt{}.i))
// Output: 16 0 8 

fmt.Println(unsafe.Sizeof(IntBool{}), unsafe.Offsetof(IntBool{}.i), unsafe.Offsetof(IntBool{}.b))
// Output: 16 0 8

上記のサンプルコードでは、構造体の並び方によってメモリのパディングが変わり、全体のサイズが変化することが分かります。このように、unsafeパッケージを使えば、メモリのレイアウトを細かく制御できます。

さらに、unsafeを使えば、バイナリデータをスムーズに構造体へマッピングすることも可能です。

func DataFromBytesUnsafe(b [16]byte) Data {
    data := (*Data)(unsafe.Pointer(&b))
    if isLE {
        data.Value = bits.ReverseBytes32(data.Value)
    }
    return data
}

このコードは、バイト配列をunsafe.Pointerでキャストし、最終的に構造体へマッピングしています。パフォーマンス的には、unsafeを使ったバイト配列から構造体へのマッピングは、安全な手法に比べて2〜2.5倍程度高速だと言われています。

BenchmarkDataFromBytes-8             538443861  2.186 ns/op   0 B/op 0 allocs/op
BenchmarkDataFromBytesUnsafe-8      1000000000  1.160 ns/op   0 B/op 0 allocs/op

ただし、unsafeの濫用は危険を伴うため、必要最小限の使用に留める必要があります。Go言語のセキュリティモデルを部分的に無視する代わりに、そのパフォーマンスメリットを手に入れるかどうかは、用途次第です。通常のプログラミングでは、安全な手法を使うことがベストプラクティスです。

また、unsafeを使う際は、ランタイムフラグ -gcflags=-d=checkptrを付けてポインタの不適切な使用をチェックすることが推奨されています。ドキュメントの注意事項にあるとおり、正しく使えば高速で強力なコードを書けますが、間違った使い方をすると簡単にセキュリティホールを作ってしまう可能性もあるため、unsafeの適切な使用には熟達した技術が求められます。

cgo

最後に、cgoについてですが、これはGo言語とCコードを連携する仕組みです。CコードをC拡張構文のコメント中に直接記述したり、関数の宣言をコメントに記述して外部のCコード中の関数をリンクしたりできます。Go関数をCコードにエクスポートすることも可能です。

/*
#include <math.h>
int add(int a, int b) {
    int sum = a + b;
    printf("a: %d, b: %d, sum %d\n", a, b, sum);
    return sum;
}
*/
import "C"

func main() {
    sum := C.add(3, 2)
    fmt.Println(sum)           // Output: 5
    fmt.Println(C.sqrt(100))   // Output: 10
}

cgoは、OSのシステムコールへアクセスしたり、既存のCライブラリを活用したりするのに便利です。ただし、Go言語がGCで自動メモリ管理しているのに対し、Cコードではメモリ解放を手動で行う必要があるため、ポインタの扱いには注意が必要です。Go側の変数をCコードに渡す際は、cgo.HandleでラップしてCコードに安全に渡す必要があります。

p := Person{Name: "Jon", Age: 21}
C.in_c(C.uintptr_t(cgo.NewHandle(p)))

このように、cgoを使うと、Go言語の安全性とCコードの低レイヤー制御が両立できます。ただし、その実行コストは約40nsと高く、基本的にはパフォーマンス向上のためというよりは、既存のCライブラリ活用のために使われることが多いようです。

パフォーマンス向上のためだけにはあまりメリットがない上、ポインタの危険な操作が必要になるため、cgoは「cgo is not Go」と言われています。そのため、自前でCラッピングを書く前に、サードパーティ製のGoラッパーを探すことが推奨されています。また、設計やメモリモデルの違いから、cgoの使用は避けられない場合に限り、それでもできるだけコア部分は安全なGoコードで書くべきだとされています。

この章では、Go言語のセキュリティや規約を部分的に無視するreflect、unsafe、cgoについて詳しく学びました。これらの機能は、強力な反面で危険が伴うため、一定の知識と経験が必要とされます。特に、reflectは外部データのマッピングや標準ライブラリの一部の処理に必要不可欠ですが、パフォーマンスの低下が避けられません。unsafeはメモリレイアウトの制御や高速なデータマーシャリングに使えますが、安全性を無視する代わりのメリットが必要です。cgoは既存のCライブラリをラッピングする手段ですが、実行コストが高い上に設計の違いからGoの規約を完全に守ることができません。

つまり、これらは例外的な処理を行うための機能であり、通常の処理ではなるべく使わず、代わりにジェネリクスなどの安全な手段を活用するのがベストプラクティスだと言えます。Go言語の利点は、まさにその規約とセキュリティにあるためです。ただし、一方でそれらを無視する機能さえも用意されていることで、Go言語はかえって強力で柔軟な言語になっていると言えるでしょう。

Exercisesでは、構造体のバリデーションやメモリレイアウトの最適化、CコードのラッピングなどGoの規約を一時的に無視する練習問題が用意されていました。Wrapping Upでは、Go言語の安全性は基本原則ですが、例外的に規約を無視することで強力で柔軟な機能が提供されていると総括されていました。つまり、これらの高度な機能は「ツール」に過ぎず、適切な使い分けが重要なのです。

Go言語は、基本的にはシンプルで規約に従うことで、保守性の高いソフトウェアを作ることを目指しています。一方で、状況次第では規約を一時的に無視して高度な操作を行う必要が出てくる場合もあります。そういった際にこそ、reflect、unsafe、cgoといった高度な機能が役立つのです。これらは、Go言語の最も興味深い部分の1つと言えるでしょう。

しかし同時に、これらの機能の濫用はセキュリティホールやバグにつながりかねません。そのため、十分な理解とコントロールが求められます。Go言語のセキュリティやベストプラクティスに反するような例外的な処理は、確実に必要な場面でのみ、そして最小限に留めるべきです。

Go言語では、規約に従うことで、強固で長期的に保守しやすいソフトウェアを書くことを目指しています。その一方で、例外的な状況においては、reflectやunsafe、cgoなどの例外的な仕組みを適切に使いこなすスキルも要求されます。つまり、Go言語では一般的なユースケースでは退屈で規約に従うことを推奨しつつ、特殊なケースでは必要最小限の例外を認める、そんな設計思想が貫かれていると言えるのです。今後50年のコンピューティングを支えるソフトウェアを作り上げていく上で、この思想を体現するスキルが求められるでしょう。

おわりに

ここ間違っているとかあればDMください。

本書「Learning Go Second Edition」を通して、Go言語プログラミングについて体系的かつ実践的に学ぶことができました。Goのシンプルさと実用性を重視する設計思想、並行処理やエラーハンドリング、テストなどの実践的な要素に言語レベルでサポートされている点が特に印象的でした。一方で、Goのシンプルさは時として制約にもなり得ます。Goの設計思想を理解し、適材適所で機能を使い分けて周知していくことが大事だと思いました。

サンプルコードと練習問題を通して、学んだ概念を実際のコードに落とし込み、体験的に理解を更に深めることができました。特に知らなかったことがいくつかあったのと実践的なスキルとして利用したいものがいくつかあったので充分すぎる収穫かと思いました。

Goのシンプルさの中に宿る美しさに惹かれ、ベストプラクティスを習慣化し、美しいコードを書けるようになりたいと思いました。並行処理とエラーハンドリングは、Goプログラミングの醍醐味だと感じています。モジュールシステムとテストの重要性についても再認識できました。Goのシンプルで実用的な設計思想を自分の糧として、プログラマとしてのマインドセットを磨いていきたいと思います。**