この度はこの本をお手に取っていただき誠にありがとうございます。この本は、二人のエンジニアによるGo言語に関する内容をまとめた書籍になります。それぞれの執筆者から、本の内容をご紹介いたします。
本書は、Goを学びながらWebルータを作っていくことを目的としています。そのため、第1章や第2章は、Goの基礎やルーティングの基礎といった内容になっています。サンプルコードが多いと思うので、写経するだけでもGoの基礎知識が身につき、GoでWebルータを作ることができると思います。また、3章ではパスパラメータルーティングを含むルーティングの実装について詳しくお話しします。4章ではベンチマークをとってGoにおける高速化や最適化のちょっとしたコツを説明しています。今後のGo高速化の参考になれば幸いです。
わからない時は、GitHubに本書を作成する上で自作したコードを公開しているので、それを参考にしていただけたらと思います。
また、こうしたプログラムが多い本には付き物だと思いますが、記載したコードにバグなどがあればGitHubにissueやPRを投げていただければ助かります。
まず、技術書展13に誘ってくださったメンターの佐々木さんにとても感謝してます。佐々木さんのお誘いがなかったら本書が世に出ることはなかったと思いますし、このように自分の学んだ内容をまとめる機会はなかったと思います。いろいろとメンタリング面談などで本書の進捗などを確認していただいてとても助かりました。これからもよろしくお願いいたします。
また、本書を作成するにあたり大変参考にさせていただいたOSSのEcho、Gin、Chi、Gorillaなどのコントリビュータの方々に感謝しております。
この本では、家計簿アプリケーションで利用想定のバックエンド(以降は家計簿サービスと記述)のAPIを作りながら、Goの基本文法やGoでのWebアプリケーションの実装の流れやTipsなどを学んでいく内容となっています。Go言語の特徴として、機能がシンプルになるように設計されており、コードの書き方も人によらずに書けるようにフォーマットが明確に設計されているため、チーム開発などにとても使いやすい言語となっています。
APIの実装を通じてGoでのサーバーサイドの開発について学び、ぜひご自身でのアプリ作成に活かしていただけたら幸いです。
対象読者としては、以下に記載したようにプログラミング初心者や、サーバーサイドやGoの入門者を対象としています。
・プログラミングを学んでみたい方
・Go言語でWebアプリケーションの作り方を学んでみたい方
・サーバーサイドのWebアプリケーションの開発について学んでみたい方
なおこの本では、メインとしてGoでのWebアプリケーションのサーバーサイドのAPI開発に焦点を絞って解説しています。フロントエンド側のアプリケーションの開発については別の書籍を参照ください。
本書に記載された内容は、情報の提供のみを目的としています。したがって、本書を用いた開発、製作、運用は、必ずご自身の責任と判断によって行ってください。これらの情報による開発、製作、運用の結果について、著者はいかなる責任も負いません。
本書に記載されている会社名、製品名などは、一般に各社の登録商標または商標、商品名です。会社名、製品名については、本文中では©、®、™マークなどは表示していません。
本書は、Goを学びながらWebルーティングを作っていくことを目的としています。Goを触ったことがない読者向けに、本章ではWebルーティングを作っていく過程で必要になってくる、Goの基本的な文法を説明していきます。
既にGoの基礎知識がある人は、2章のルーティングの基礎や3章のGoでのルーティングの実装から読み進めることをおすすめします。
この節では、Goの変数宣言の仕方を説明します。Goは大きく分けて変数の宣言の仕方が2つあります。
1.varによる変数の宣言
1.セイウチ演算子(:=)による変数の宣言
1: package main
2:
3: import "fmt"
4:
5: func main() {
6: // 単純な変数の宣言
7: var a int
8: // 変数の宣言と初期値の設定
9: var b int = 10
10:
11: // expect: "a: 0, b: 10"
12: fmt.Printf("a: %d, b: %d\n", a, b)
13: }
varで変数を初期値を設定せずに宣言すると、intの場合は0が初期値として代入されます。
プリミティブ型の変数をvarで宣言した場合の初期値は以下のようになります。
型 | 初期値 |
真偽型(bool) | false |
整数型(uint,int,byteなど) | 0 |
浮動小数点型(float32,float64など) | 0.0 |
複素数型(complex64,complex128など) | 0.0 + 0.0i |
文字列型(string) | "" |
1: package main
2:
3: import "fmt"
4:
5: func main() {
6: a := 0
7: b := 10
8:
9: // expect: "a: 0, b: 10"
10: fmt.Printf("a: %d, b: %d\n", a, b)
11: }
セイウチ演算子(:=)による変数宣言は、初期代入する変数によって型が決まります。Goは型推論自体は行なっていないのですが、型推論のような挙動で変数の型を決めることができる仕様があります。
また、セイウチ演算子(:=)によって宣言された変数は、再度':='で初期化することはできません。定義された変数へ値を再代入する際は`=`を使って代入を行います。
この節では、連続したデータを扱うデータ構造として配列(array)とスライス(slice)について説明していきます。
配列とスライスは以下のように宣言することができます。
1: package main
2:
3: import "fmt"
4:
5: func main() {
6: // 配列(array)の定義
7: var a = [5]int{1, 2, 3, 4, 5}
8: // expect: "a: [1 2 3 4 5]"
9: fmt.Printf("a: %v\n", a)
10:
11: // スライス(slice)の定義
12: var b = []int{1, 2, 3, 4, 5}
13: // expect: "b: [1 2 3 4 5]"
14: fmt.Printf("b: %v\n", b)
15: }
似たような振る舞いをするデータ構造ですが、プログラムを書く上では、配列は固定長、スライスは可変長という違いに気を付ける必要があります。
配列は、append関数を使って末尾に要素を追加することはできません。一方、スライスはappendを使って末尾に要素を追加することができます。ただし、可変長のスライスも配列の長さ以上のindexにアクセスするとpanicが発生します。配列は、配列の値の参照ではなく値の実体を保持したデータ構造になっており、スライスは連続データの参照を保持しています。
メモリ管理において、配列とスライスの間には大きな違いがあります。
配列のコピーを作成した場合、配列の値はメモリの別領域にコピーされるため、元の配列を変更してもコピー先の配列の値は変更されません。
一方で、スライスのコピーを作成した場合、スライスはメモリ上の連続データの参照を保持しているため、元のスライスの要素の値を変更するとコピー先のスライスの要素の値も変更されます。以下のコードで配列自体が値の実体を保持したデータ構造であることと、スライスがメモリ上の連続データの参照を保持したデータ構造であることを確認します。
1: package main
2:
3: import "fmt"
4:
5: func main() {
6: // 配列とスライスの定義
7: var a = [5]int{1, 2, 3, 4, 5}
8: var b = []int{1, 2, 3, 4, 5}
9:
10: // cにaの配列をコピー
11: c := a
12: // a[0]を変更する
13: a[0] = 100
14:
15: // dにbのスライスをコピー
16: d := b
17: // b[0]を変更する
18: b[0] = 100
19:
20: // 配列の実体がコピーされていることを確認する
21: // expect:
22: /*
23: a: [100 2 3 4 5]
24: c: [1 2 3 4 5]
25: */
26: fmt.Printf("a: %v\nc: %v\n", a, c)
27:
28: // アドレスが違うことを確認する
29: fmt.Printf("&a[0]: %v\n&c[0]: %v\n", &a[0], &c[0])
30:
31: // スライスが配列の参照がコピーされていることを確認する
32: // expect:
33: /*
34: b: [100 2 3 4 5]
35: d: [100 2 3 4 5]
36: */
37: fmt.Printf("b: %v\nd: %v\n", b, d)
38:
39: // アドレスが同じことを確認する
40: fmt.Printf("&b[0]: %v\n&d[0]: %v\n", &b[0], &d[0])
41: }
組み込み関数のmakeを使ったスライスの宣言について説明します。スライスを宣言するためにmakeを使った場合は、長さとキャパシティを指定することができます。それに応じて、makeは指定した長さとキャパシティのスライスを作成することができます。
長さとキャパシティの違いですが、長さは初期化したスライスの要素数、キャパシティはスライスの要素を保持できる最大数になります。
make関数を使ったスライスの定義は以下のようになります。
1: package main
2:
3: import "fmt"
4:
5: func main() {
6: // 長さの指定のみ
7: a := make([]int, 5)
8: // expect: "[0 0 0 0 0]"
9: fmt.Printf("%v\n", a)
10:
11: // キャパシティの指定
12: b := make([]int, 0, 5)
13: // expect: "[]"
14: fmt.Printf("%v\n", b)
15: }
append関数は、スライスの末尾に要素を追加することができます。
Tips: append関数はスライスの長さ以上の要素をappendするとき、appendの都度allocateが行われるため、予めmake関数を使ってキャパシティや長さを適切に指定しておくと高速に処理が行われます。
1: package main
2:
3: import "fmt"
4:
5: func main() {
6: // 長さの指定のみ
7: a := make([]int, 5)
8:
9: a = append(a, 100)
10: // expect: "[0 0 0 0 0 100]"
11: fmt.Printf("%v\n", a)
12:
13: // キャパシティの指定
14: b := make([]int, 0, 5)
15: b = append(b, 100)
16: // expect: "[100]"
17: fmt.Printf("%v\n", b)
18: }
if文は条件式の真偽に応じて処理を記述することができます。複数の条件式を指定する場合において、else ifやelseなどを使用することができます。
if 条件式1 {
// 処理
} else if 条件式2 {
// 処理
} else {
// 処理
}
if文の条件式内で関数を実行することができ、実行した関数の戻り値に応じて処理の分岐をすることが可能です。戻り値のスコープはif文のブロック内でのみ有効です。
Tips: 良くエラーの処理などでこの記述は使用されます。
if a, err := hoge(); err != nil {
// エラーハンドリング
}
for文を使用して、ループ処理を記述することができます。
ループの回数を指定する場合のループの記述は以下のようになります。
for i := 0; i < 10; i++ {
// 処理
}
while文のように条件式に応じたループの記述は以下のようになります。他言語では、条件に応じて処理を繰り返す時はwhileを使う一方で、Goは、forで記述します。
for 条件式 {
// 条件式がtrueの時に行う処理
}
無限ループを作る場合には、for文のループ条件に何も指定しないことで無限ループになります。
for {
// 処理
}
forEachのようにスライスやmapの要素のキーと値をループで処理することもできます。ここでのkey、valueのスコープはループのブロック内でのみ有効です。
for key, value := range map {
// 処理
}
Goは複数のデータをまとめて扱うことができます。構造体は複数のデータをまとめて扱うために使用されます。
構造体の宣言と初期化は以下のようにすることで行うことができます。
1: package main
2:
3: import "fmt"
4:
5: type Person struct {
6: Name string
7: Age int
8: From string
9: }
10:
11: func main() {
12: p := Person{
13: Name: "keikun",
14: Age: 24,
15: From: "Japan",
16: }
17: /*
18: このように宣言することもできます。
19: var p = Person{
20: Name: "keikun",
21: Age: 24,
22: From: "Japan",
23: }
24: */
25: // expect:
26: // {keikun 24 Japan}
27: fmt.Println(p)
28: }
構造体のフィールドへアクセスする場合は、.を使用してフィールド名を指定することができます。
上記の例を基にNameにアクセスする場合は、p.Nameで値にアクセスすることが可能です。
Goでは構造体のフィールドにタグ付けすることが可能です。Goは、タグ付けすることにより、フィールドにJSONなどのデータをマッピングする際に、そのタグ情報を基にマッピングを行うことができます。タグ情報をつけることでコードの可読性が向上し、安全にjsonのデータをマッピングすることができることを確認します。
1: package main
2:
3: import (
4: "encoding/json"
5: "fmt"
6: )
7:
8: const jsonstr = `
9: {
10: "person_name": "keikun",
11: "person_age": 24,
12: "person_from": "Japan"
13: }
14: `
15:
16: type PersonNotTag struct {
17: Name string
18: Age int
19: From string
20: }
21:
22: type PersonTag struct {
23: Name string `json:"person_name"`
24: Age int `json:"person_age"`
25: From string `json:"person_from"`
26: }
27:
28: func main() {
29: p1 := new(PersonNotTag)
30: p2 := new(PersonTag)
31: json.Unmarshal(([]byte)(jsonstr), p1)
32: json.Unmarshal(([]byte)(jsonstr), p2)
33: // expect: &{ 0 }
34: fmt.Println(p1)
35: // expect: &{keikun 24 Japan}
36: fmt.Println(p2)
37: }
関数は以下のように定義することができます。
1: func hoge(a, b int) {
2: // 処理
3: }
Goは戻り値を複数指定することができます。戻り値がひとつの場合は、関数の引数の後に戻り値の型を指定することができます。複数戻り値の場合は、戻り値の型定義の箇所をタプルのように指定することで複数戻り値を返すことができます。
1: // 戻り値が1つの場合
2: func hoge() int {
3: var a int
4: // 処理
5: return a
6: }
7:
8: // 戻り値が複数の場合
9: func hoge(a, b int) (int, int) {
10: // 処理
11: return a, b
12: }
Tips: 関数の引数にポインタを渡す場合と実体を渡す場合の違いについて
Goは、関数の引数にポインタと実体の両方を渡すことができます。関数に実体を渡す場合は呼び出し先でコピーが行われます。そのため、関数の処理の中で呼び出し元の実体の副作用が発生することはありません。ただし、呼び出しの度にコピーが行われるため大きい構造体を渡す関数をループ中で呼び出す場合は効率が悪いです。
一方、ポインタを渡す呼び出しは、実体を渡す場合と比べて呼び出し元の実体の副作用が発生します。実体を渡す場合と比べて、コピーが発生しないため効率が良いです。
1: package bench
2:
3: import (
4: "net/http"
5: "testing"
6: )
7:
8: type Value struct {
9: str string
10: handler http.HandlerFunc
11: }
12:
13: func ValueFunc(str Value) {
14: // do something
15: }
16:
17: func PointerFunc(str *Value) {
18: // do something
19: }
20:
21: func BenchmarkValueFunc(b *testing.B) {
22: b.ResetTimer()
23: value := Value{
24: str: "hello",
25: handler: func(w http.ResponseWriter, r *http.Request) {},
26: }
27: for i := 0; i < b.N; i++ {
28: ValueFunc(value)
29: }
30: }
31:
32: func BenchmarkPointerFunc(b *testing.B) {
33: b.ResetTimer()
34: value := Value{
35: str: "hello",
36: handler: func(w http.ResponseWriter, r *http.Request) {},
37: }
38: for i := 0; i < b.N; i++ {
39: PointerFunc(&value)
40: }
41: }
Tips: benchmarkの実行は、 go test コマンドに -bench や -benchmem オプションをつけることで実行ができます。
構造体のメソッドとして関数を定義する場合、2種類のレシーバの定義の仕方があります。ここでは、値レシーバとポインタレシーバの違いについて説明していきます。
・値レシーバ
─メソッドの実行時に構造体のフィールドの値がコピーされます。
─コピーに対して実行されるため、呼び出し元の構造体に対して副作用が生じることはありません。
・ポインタレシーバ
─値レシーバと違い、構造体のフィールドの値はコピーされません。
─構造体のコピーが実行されないため、値レシーバと比べると呼び出しは効率的です。
─呼び出し元の構造体のフィールドに対して副作用が生じます。
値レシーバとポインタレシーバのメソッド定義は次のようになります。
1: type Person struct {
2: Name string
3: Age int
4: From string
5: }
6:
7: // 値レシーバのメソッド
8: func (p Person) Hoge() {
9: // 処理
10: }
11:
12: // ポインタレシーバのメソッド
13: func (p *Person) Fuga() {
14: // 処理
15: }
この章では、Goのよく使う文法や制御構文など説明を行っていきました。Goの制御構文は、全部で40程度しかなく、文法も非常に簡潔に記述することができるため、学習コストが低い言語だと思います。
この章で紹介しきれなかった制御構文などもあるので、興味がある方は公式ドキュメントやA Tour Of Goなどをやってみると網羅的に学べると思います。