Goメモ

モジュール/パッケージ
1リポジトリが1モジュールに対応する.リポジトリのルート階層(モジュールのトップディレクトリ)にはgo.modが配置される.以下のコマンドで作成される.
go mod init モジュール名(リポジトリのアドレス)

- 1モジュールに1つ以上のパッケージが存在する.
- 1モジュールには必ず一つは"main"パッケージが存在し,mainパッケージにはmain関数が存在する.main関数はこのモジュールのエントリーポイントとなる.
- パッケージは
package パッケージ名
のように同じパッケージ名で宣言したファイル群で構成される. - 同じパッケージに属するファイルは同じフォルダーにまとめて配置する.

- パッケージのインポートは
import
で行う.importの後は括弧で複数のパッケージを同時にインポートできる.インポートするパッケージは"モジュール名/(フォルダー階層に応じた)パッケージ名"で指定する. - パッケージ名の前に任意の名前を設定することで,パッケージの別名を設定できる(Pythonのasと同じ).
- パッケージの別名を.にすると,パッケージ名を省略してインポートできる.
import (
"fmt" // 標準パッケージ
"my-module/subdir/hoge" // そのほかのパッケージ
fuga "my-module/subdir/fuga" // "fuga"という別名をつける
. "my-module/subdir/piyo" // "piyo"パッケージ内のpublicな変数,関数はパッケージ名を省略して使用できる
)

mainパッケージにはmain関数以外にinit関数がある.これはmain関数の実行前に呼び出される.
パッケージインポート時にパッケージ内の変数の初期化などに使われる.

変数・関数の公開範囲
Goではパッケージ単位で公開するか (exported) どうかを制御する.制御は変数・関数名の先頭文字のキャピタライズで決まる.
例 | 文字種 | 制御 |
---|---|---|
Hoge | 大文字 | パッケージ内外で参照可能. |
fuga | 小文字 | 同一パッケージのみ参照可能. |

配列
固定長配列.初期化後に要素数を変更することはできない.
// 初期化
a := [2]int
b := [2]int{1, 2}
c := [...]int{1, 2, 4} // 要素数を...で指定すると数を自動で推定する
len(c) // len()で長さを取得

スライス
可変長配列.要素を追加・削除できる.変数の値はメモリの実体への参照値.
参照値なので,スライスを操作すると,元のスライスの値も変更される.
実際の長さと容量の概念がある(C++のstd::vectorと同じ).
// 初期化
a := []int
b := []int{1, 2}
c := make([]int, 2) // make() で作成できる
d := make([]int, 3, 4) // lenは3, capは4

インデックスのスライス指定で要素の一部をviewとして取得可能.
スライスで先頭を削った時のlenとcapは,それぞれ削られた分だけ少なくなる.後ろを削った時はlenのみ減りcapは変わらない.
d[1:] // 先頭から最後まで.lenは1,capは2
d[:2] // 先頭から2番目の直前まで.lenは2,capは4
d[:] // 全要素の参照.

スライスの要素を追加するときはappend()
を使う.このときcapが足りない場合は新しいインスタンスとして作成される.
スライスをディープコピーするときはcopy()
を使う.
s := []int{1, 2, 3}
s = append(s, 4, 5) // [1, 2, 3, 4, 5]
l := make([]int, len(s))
copy(s, l) // sの要素をlにコピー

スライスの要素を削除するときはcopy()を駆使して行う.
// aのi番目の要素を削除
a = a[:i+copy(a[i:], a[i+1:])]

pack/unpack演算子
...型
でpack,変数...
でunpack.
// 可変長のintの引数をargvという変数に[]intでpack
func Hoge(argv ...int) {
s := []int{}
append(s, argv...) // スライスsの末尾にargvの要素をunpackして追加
}

マップ
連想配列.参照型.値の取得時には第2戻り値が存在フラグのboolとなっている.
// 初期化
a := map[int]int{ 1: 1, 2: 2}
b := make(map[int]int)
// 値の登録
b[1] = 1
b[2] = 2
// 値の取得
v1 = b[1]
v2, ok := b[0] // 第2戻り値は存在確認フラグ
if ok {
fmt.Println(v2)
}
// 値の削除
delete(b, 1)

変数/定数
変数はvar 変数名 型
で行う.一応型名は省略できる.
セイウチ演算子:=
を使うとvar
や型指定を省略できる.ただし:=
はグローバルスコープで使用できない.
var hoge int // 初期値は0
// piyo := 0 グローバルスコープでは ":=" が使えない
func huga() {
piyo := 0
}

関数
関数の戻り値は複数設定できる.受け取る側は_
で値を捨てられる.
func test() (int, int) { return 1, 2 }
_, b := test() // bは2
戻り値の宣言にそれ用の変数を設定することができる.このときreturn
は必須だが値を返さなくても,戻り値の変数に値が設定されていれば問題ない.
func test() (a, b int) {
a = 1
b = 2
return
}

クロージャー
TypeScriptのように,関数を返す関数で,返す関数と同じレベルで宣言された変数を,返す関数内で使用すると,その変数は外に出された関数に参照として生存する.
func test(v int) func (int) int {
x := v + 1
return func (y int) int {
x++; // クロージャーを実行するごとにxは1増える
return y + x;
}
}

クロージャーを使うとジェネレーターも作れる.
func generator(init, add int) func() int {
val := init
return func() int {
defer func() { val += add }()
return val
}
}
gen := generator(0, 2)
fmt.Println(gen()) // 0
fmt.Println(gen()) // 2
fmt.Println(gen()) // 4

if文
ifは初期化と条件式を一緒に書ける.
if a := 1; a + b < 9 {
/* ... */
}

for文
初期化式と事後処理を省略できる(いわゆるwhile).
そのうえ条件式までも省略できる(いわゆるwhile true).
for i := 1; i < 10; i++ {}
for i < 10 {} // while (i < 10) {} と同じ
for {} // while (true) {} と同じ

switch文
- switchのcaseの最後にbreakは不要.
- switchでも初期化式を設定できる. (
switch v := 2; v { ... }
) - caseには複数の条件を同時に設定できる (
case 1, 2:
) - caseは条件式もあてはめられる (
case a < 10
). このときはswitchの条件式も省略できる. - switch内で値の完全一致caseと条件式caseは混在できない.

any/interface{}
C++のstd::anyと同じ.あらゆる値が代入できる.初期値はnil
.
interface{}型を.(T)
で特定の型にキャストする方法を型アサーションと呼ぶ(swiftのas?と同じ).戻り値の第2でアサーション結果が真偽値で取得できる.
var any interface{}
any = 1
v, res = any.(int) // v == 1. res == true
_, res = any.(string) // res == false

defer文
- そのスコープの終了時に実行したい関数をdefer文で登録できる.
- 1つのdefer文では1関数のみ登録できる.
- defer文は複数登録できる.実行順序はFILO.
- 関数以外のものや複数の関数をまとめて登録する場合は即時関数で記述する.
defer fmt.Println("DEF")
defer func() { fmt.Println("ABC") }()
// 呼び出されるのは"ABC" "DEF"の順番

構造体
構造体は値型.
関数の引数で渡すときはそのままだとコピーとなるため,ポインターとして生成する方法が推奨される.
type Hoge struct {
Fuga, Piyo int
}
a := Hoge{Fuga: 1}
b := &Hoge{} // 関数の引数に渡すときにポインター渡しとする

- 関数宣言でレシーバーを追加することで,その構造体やinterfaceのメソッドを実装できる.
- レシーバーには値レシーバーとポインターレシーバーが存在する.
- 値レシーバーは構造体のコピーがselfやthisとして渡されるイメージ
- ポインターレシーバーは構造体の参照がselfやthisとして渡されるイメージ
- 基本的にはポインターレシーバーを使う.
type Hoge struct {}
func (Hoge)Fuga() int { return 1 } // 値レシーバー
func (*Hoge)Piyo() int { return 2 } // ポインターレシーバー

構造体のコンストラクターはいわゆるファクトリー関数で実装する.
// ポインターを返すことでメモリを節約する
func NewT(arg1 int, arg2 int arg3 int) *T {
return &T{Arg1: arg1, Arg2: arg2, Arg3: arg3}
}

Goでは継承ではなく包含関係で移譲として表現する.つまりPolymorphではない.
構造体のなかに別の構造体のフィールドを設定するとき,変数名を入れずに型名だけ指定すると,親構造体のフィールドに子構造体のフィールドが展開されて宣言される.
type Hoge struct { Hogehoge int }
type Fuga struct { Hoge }
a := Hoge{}
a.Hoge.Hogehoge
a.Hogehoge // 変数名を省略すると透過的に参照可能

独自型
リテラル型などをtype
で別名をつけたもの.
元のリテラル型からの代入は可能だが,それ以外は元リテラル型とは別の型として扱われる.
またリテラル型ではメソッドを生やせないが,独自型にすることでメソッドを実装できる.
type Id uint
var id Id = 0 // uintからの代入可能
// id = id + 1 はIdとuintの計算なので型の不一致で実行不可
// 独自型にはメソッドを実装できる
func (id Id)Stringer() string { return fmt.Printf("%v", id) }

インターフェース
Rustでいうプロトコル.構造体のメソッド宣言のみ定義したもの.
その実態はインスタンスの型情報(.type)とインスタンスへのアドレスを保持した構造体.
構造体なので値型.
interface Person {
func Say() string
}
構造体には明示的にどのインターフェースを実装したかを記載せず,同名のメソッドを実装する.
struct Hi {}
func (Hi)Say() string { return "yay" }
struct Yo {}
func (*Yo)Say string { return "yo" }
persons := []Person{ &Hi{}, &Yo{} }
インターフェースへの代入はその代入する値の実体(ポインターなら参照先)がインターフェースで定義したメソッド群(メソッドセット)に一致したときに代入可能と判定する.

関数の引数にinterfaceを受け取るときは,通常値型とする.
func Wow(person Person) {
// ...
}
インターフェースのメソッド呼び出しは,内部歴にはインスタンスとして渡された実体のメソッドを.
演算子で呼び出す形になる.
しかしインターフェースのポインターを引数とした場合,.
演算子はポインターの参照先(インターフェース構造体の実体)に実装されているメソッドを探す.
そのためインターフェースが参照する実体のメソッドに辿り着けずエラーとなる

エラー処理
失敗が発生しうる関数は,その戻り値をいわゆるResult型のような構成にする.
第2戻り値をerror
型とし,その値がnilであれば処理は失敗したと判定する.
if v, err := something(); err != nil {
/* vが有効値 */
}

独自エラー型を作成する場合は標準インターフェースerror
を実装したエラー構造体を作成する.
type error interface {
func Error() string
}
関数の戻り値として渡すときはerror
型を返すようにし,実際はカスタムエラーのインスタンスの参照を返すのが良い.
type MyError struct {
ErrorCode int
}
func (MyError)Error() string { return "ah!" }
func Something() (int, error) {
return 0, &MyError{ErrorCode: 1} // ポインターを返す
}
_, err := Something()
err.(*MyError) // 型アサーションでインスタンスの値を取り出す.インターフェースの実体は*MyError型なのでそれにキャストする

テスト
- 単体テストは対象となる処理が記述されるファイルに対して,"対象ファイル名_test.go"というファイルに記述する.
- テストファイルはテスト対象ファイルと同じパッケージにする.
- テストファイルでは
import "testing"
をしておく. - テストケースの関数名は
func TestXXX(t *testng.T)
としておく. - テストの実行は
go test 対象ディレクトリ/...
- "..."でサブディレクトリのテストも実行する.
package main
func IsOdd(v int) bool {
return v % 2 == 0
}
func main() {
_ = IsOdd(1)
}
package main
import "testing"
func TestMain(t *testing.T) {
conditions := []struct {
input int
output bool
}{
{1, false},
{2, true}
}
for _, test := range conditions {
res := IsOdd(test.input)
if res != test.output {
t.Errorf("Expected %v, but actual %v\n", test.output, res)
}
}
}

testingはベンチマーク測定や処理のスキップ,サブテストの設定やテストの並列実行も行える.

goroutine
関数の実行時にgo
とするだけで,その関数を別スレッドで実行する.これをgoroutineと呼ぶ.
go func1()
go func2()

チャネル
goroutine間でデータをやり取りするためのソケットのようなもの.参照型.
チャネルに対してデータの送受信が完了されるまでスレッドはブロックされる.
チャネルのバッファイサイズはスライスと同じ要領で調べられる.
バッファはFIFOキュー.
var c1 chan int // intの値をやり取りする双方向チャネル
var c2 <-chan int // intの値をやり取りする受信専用チャネル
var c3 chan<- int // intの値をやり取りする送信専用チャネル
ch4 := make(chan int) // バッファサイズ1,セマフォ1
ch5 := make(chan int, 2) // バッファサイズ2,セマフォ2
c1 <- 1 // データ送信
v, ok := <-c1 // データ受信.第2戻り値は受信できたかどうか
len(c1) 現在バッファされている要素数
cap(c1) バッファサイズ

チャネルでの送受信が終了したときは,チャネルリークを防ぐためclose
を行う.
- チャネルをclose後にチャネルへ送信しようとするとpanicになる.
- チャネルのclose後は常にチャネルから受信できるが,第1戻り値は型のデフォルト値,第2戻り値は
false
となる.
この機能を用いてgoroutineの終了操作を行う,
// groutine 1
ch <- 1
close(ch)
// groutine 2
for {
if v, ok := <-ch; !ok {
// 終了操作
break
}
}
forのrangeでチャネルを受け取るようにしていれば,より簡略化できる.
// あるいは
for v := range ch {
// something
}

select文
複数のチャネルの送受信に対する処理をswitchのようにチャネル操作ごとにケースわけして記載できる.
caseの検証順序は実行に偏りが起こらないようランダムに選択される.
通常のチャネルの受信では,値がない場合は処理をブロックするが,selectではdefaultを使って処理を続けることができる.
select {
case v := <-ch1:
// ch1から値をとれた時の処理
case v:= <-ch2:
// ch2から値をとれた時の処理
default:
// ch1,ch2のいずれからも値が取れなかった時の処理
}

syncパッケージ
非同期処理のユーティリティパッケージ.
sync.Mutex
mutex.Mutex.Lock()
とMutex.Unlock()
で排他制御ができる.
mutex := new(syc.Mutex)
// Mutex自体は値型なので,関数の引数で渡すときはポインターで渡す
func Routine1(mutex *sync.Mutex) {
mutex.Lock()
res.modify()
mutex.Unlock()
}

sync.WaitGroup
複数のgoroutuneがすべて終わるまでメインスレッドの処理をwaitさせる.
wg := new(sync.WaitGroup)
wg.Add(2) // カウンターを2にする
go func() {
wg.Done() // 完了したらカウンターを減らす
}()
go func() {
wg.Done() // 完了したらカウンターを減らす
}()
wg.Wait() // カウンターが0になるまでメインスレッドを待機させる

そのほか一度しか複数のgoroutineからコールされても一度しか実行されないOnceFunc
(C++のstd::call_onceと同じ),MutexなMap
などが用意されている.

jsonパッケージ
JSON操作のユーティリティーパッケージ.
import (
"json"
"fmt"
)
type Data struct {
// `json:キー名`でタグをつける
Id int `json:"id"`
Name string `json:"name"`
}
// マーシャリング
data := Data{Id: 3, Name: "Thuler"}
if bs, err := json.Marshal(data); err == nil {
fmt.Println(string(bs)) // "{"id":3, "name":"Thuler"}"
}
// アンマーシャリング
data2 := new(Data)
if err := json.Unmarshal(bs, data2); err == nil {
...
}
MarshalJSON
やUnmarshalJSON
をデータの構造体のメソッドとして実装すると,マーシャリング・アンマーシャリング時に,フィールドの値を整形するミドルウェアとして設定できる.

contextパッケージ
ある処理によって複数のgoroutine間で処理の終了を制御するための構造体context.Context
を提供するパッケージ.
通常データ問い合わせやデータ編集など様々な手続きをgoroutineとして実行していくと,goroutineから別のgoroutineを呼び出すことになる.よってgoroutineの呼び出し関係は木構造となる.このとき,あるgoroutineでのキャンセル処理で子孫goroutineを終了させるのか続行させるのかそれぞれ制御する必要がある.チャネルで制御するには複雑なため,goroutine間で共通の「コンテキスト」を作成し,それの状態によって書くgoroutineの続行・終了条件を各自が参照して判定できるようにする.

Context
はWithCancel
によるキャンセル通知やWithDeadline
やWithTimeout
によるタイムアウト処理ができる.制御されるgoroutineはselect
などでContext.Done()
によって返される受信チャネルからの応答を補足することで,終了タイミングを補足できる.
Context
は別のContext
と依存関係を持たせることができ,親がキャンセルされると子もキャンセルされる.

url/netパッケージ
URL文字列をパース・作成するユーティリティ関数群を提供するパッケージ.
クエリ文字列やフラグメントも取得できる.

url/httpパッケージ
クライアント・サーバーモデルでの通信をを扱うパッケージ.

クライアント操作
レスポンスを取得したら,必ずdefer Response.Body.Close()
をすること.
http.Get
REST APIのGETメソッド.
// GETメソッドで取得
res, err := http.Get("url")
if err != nil {
// ...
}
defer res.Body.Close() // レスポンスを取得したらクローズする
res.StatusCode // HTTPステータスコード
res.Proto // プロトコル
res.Header // ヘッダー情報 (map[string]string)
res.Request.Method // リクエストRSETメソッド
res.Request.URL // リクエスト先URL
body, _ := io.ReadAll(res.Body) // レスポンスボディを取得
http.Post
REST APIのPOSTメソッド.リソースタイプとリソースを指定できる.
jsonText := `{"year":2023,"rank":1}`
res, err := http.Post("url", "application/json", bytes.NewBuffer([]byte(jsonText)))
if err != nil {
// ...
}
defer res.Body.Close() // レスポンスを取得したらクローズする
body, _ := io.ReadAll(res.Body)
http.PostForm
REST APIのPOSTメソッド.ボディにパラメーターを指定できる.
// パラメーターの設定
values := url.Values{}
values.Add("name", "Yoshito")
values.Add("number", 13)
// POSTメソッドで取得
res, err := http.PostForm("url", values)
if err != nil {
// ...
}
defer res.Body.Close() // レスポンスを取得したらクローズする
body, _ := io.ReadAll(res.Body)
http.Client
クライアントを作成し,任意のREST APIメソッドを指定してリクエストの送受信を行う.コンテクストを渡してタイムアウトを指定できる.
client := &http.Client{}
res1, _ client.Get("url") // GETメソッド
// リクエストを作成
ctx := context.withTimeout(context.Background(), 3 * time.Second)
req, err := http.NewRequestWithContext(ctx, http.MethodDelete, "url?id=0", nil)
// リクエスト送受信
res, _ := client.Do(req)
res.Body.Close()

サーバー操作
// エンドポイント"/users"にアクセスしたときのハンドラーを登録
// パスが"/users"で終わるものが一致する
http.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) {
/// レスポンスの書き込み順序に注意
// 最初にヘッダー情報を書き込む
w.Header().Write("Contetn-Type", "text/plain; charset=utf-8");
// ステータスコードを書き込む
w.WriteHeader(200);
// レスポンスボディを書き込む
fmt.Fprint(w, "Hi")
})
// エンドポイント"/users/"にアクセスしたときのハンドラーを登録
// パスが"/users/*"で終わるものに一致する
http.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) {
/// レスポンスの書き込み順序に注意
// 最初にヘッダー情報を書き込む
w.Header().Write("Contetn-Type", "application/json");
// ステータスコードを書き込む
w.WriteHeader(200);
// レスポンスボディを書き込む
fmt.Fprint(w, `{"name": "Yabuta"}`)
})
// 8080番ポート,デフォルトマルチプレクサ
http.ListenAndServe(":8080", nil)
またhttp.Server
によるサーバー設定を行った後にServer.ListenAndServe
を使ってサーバー立ち上げを行える.

Cookie
情報をクライアントのブラウザーに保存する仕組み.ログイン時のセッションIDなどを記憶させるためなどに使用する.
httpパッケージにてレスポンスに付与してクライアントに送信することが可能.
// クッキー作成時
cookie := &http.Cookie{
Name: "token",
Value: "something", // JWTなど
HttpOnly: true, // XSS対策
}
// w http.ResponseWriter
http.SetCookie(w, cookie)
// クッキー確認時
// r *http.Request
token, err := r.Cookie("token")
if roken == "ssomething" {
// Cookie有効
}

エンドポイントの扱い
エンドポイントがhttp.HandleFunc()
で登録されたエンドポイントパターンのどれに扱われるかは,登録されたエンドポイントのうち最も長い一致を持つパターンが選択される.
パターンの末尾がスラッシュで終わる場合,エンドポイントがその後に指定が続いた場合も一致対象とする.逆にスラッシュで終わらない場合は,エンドポイントがそこで終わるもの(完全一致するもの)が対象となる.
"/users"と"/users/"がある場合,エンドポイントが:
- "/users"の場合: "/users"の処理
- "/users/1"の場合: "/users/"の処理
Go1.22からはエンドポイントの前にメソッド指定して,ワイルドカードの指定ができるようになった.
http.handleFunc("GET /users/{id}", func(w http.ResponseWriter, r *http.Request) {
// ワイルドカードはRequest.PathValue()で取得可能
id := r.PathValue("id")
})

database/sqlパッケージ
SQLデータベースに接続するためのユーティリティー関数を提供するパッケージ.

SQLの種類によってドライバーを別途インストールしておく必要がある.
PostgreSQLの場合はgithub.com/lib/pq.
go get github.com/lib/pq
PostgreSQLサーバーの設定
Docker Composeを使ってPostgreSQLサーバーを起動させておく.
services:
db:
image: postgres:16.4-bullseye
volumes:
- ./data:/var/lib/postgresql/data
- ./init:/docker-entrypoint-initdb.d
ports:
- "5432:5432"
environment:
POSTGRES_DB: mydb
POSTGRES_USER: tomato
POSTGRES_PASSWORD: penne

接続
sql.Open
でデータベースに接続する.
package main
import (
"database/sql"
"log"
_ "github.com/lib/pq"
)
func main() {
// 接続情報
dataSource := "port=5432 user=tomato password=penne dbname=mydb sslmode=disable"
// 接続
DB, err := sql.Open("postgres", dataSource)
log.Println("接続中...")
if err != nil {
log.Panicf("接続失敗: %v", err)
}
defer DB.Close()
// 接続確認
err = DB.Ping()
if err != nil {
log.Panicf("接続失敗: %v", err)
}
}

テーブル作成
DB.Exec
の引数にSQL文を渡すことでコマンドを実行する.
cmd := `
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
name VARCHAR(50) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);`
_, err = DB.Exec(cmd)
if err != nil {
log.Panicf("テーブル作成失敗: %v", err)
}

レコード挿入
DB.Exec
は第1引数に渡すSQL文は"$1", "$2", ...の形式で変数展開の指定ができる.展開する変数はDB.Exec
の第2引数以降に渡す.
names := []string{"Nilton", "Horvy", "Estiven"}
cmd = `INSERT INTO users (name) VALUES ($1)`
for _, name := range names {
// スライスの各要素に対してINSERTを実行
_, err = DB.Exec(cmd, name)
if err != nil {
log.Panicf("レコード挿入失敗: %v", err)
}
}

レコード取得
SELECTを実行するときはDB.Query
にSQL文を渡す.戻り値はレコードのイテレーターなので,for rows.Next()
でイテレーターを進めながら,rows.Scan
で変数に値を書き出す.
cmd = `SELECT id, name FROM users`
rows, err := DB.Query(cmd)
if err != nil {
log.Panicf("レコード取得失敗: %v", err)
}
defer rows.Close()
for rows.Next() {
var (
id int
name string
)
err = rows.Scan(&id, &name)
if err != nil {
log.Panicf("レコード取得失敗: %v", err)
}
log.Printf("ID: %d, Name: %s", id, name)
}
// 業読み取り時のエラーチェック
if err = rows.Err(); err != nil {
log.Panicf("エラー: %v", err)
}

Generics
ジェネリクス.特定のインターフェースを満たす(型制約)という型パラメーターを型や関数に指定する.
インターフェースは型の和集合としても表現できる.
// 型に型パラメーターを指定
// MyType[T]はfmt.Stringerインターフェースを満たす型Tのスライス
type MyType[T fmt.Stringer] []T
// 関数に型パラメーターを指定
// any型つまり何でもOK
func MyFunc1[T any](v T) {
// ....
}
// 呼び出し時には型パラメーターの具体型を指定する必要があるが型推論で省略できる
MyFunc[int](1)
MyFunc(1)
// 型パラメーターの"int | uint"は interface { int | uint } と同値
func MyFunc2[T int | uint](s []T) T {
// ...
}
// 型パラメーターの制約に"~"をつけると,その型から派生した型も適合とする
func MyFunc3[T ~int](v T) {
// ...
}
type MyInt int
MyFunc[](MyInt(1))
// 複数の型パラメーターを同時に宣言可能
// パラメーターAが決まればパラメーターBも決まる
type MyMap[A int | uint, B *A] map[A]B

構造体のジェネリクスはメソッドで新たに型パラメーターを追加できないので,構造体の宣言の部分で先に定義しておく.

いわゆるSet型を作るときなどは,値が比較可能でなければならない.そこでcomparable
制約を型パラメーターに課すことができる.
// Tは比較可能な型
type Set[T comparable] map[T]struct{}
func NewSet[T comparable](vs ...T) Set[T] {
s := make(Set[T])
for _, v := range vs {
s[v] = struct{}{}
}
return s
}

pgxパッケージ
PostgreSQLのドライバーpqがメンテナンスモードとなっているため,現在はpgxの使用が推奨されている.
これ一つでpqのみならずdatabase/sqlの役割も果たす.

import (
"context"
"github.com/jackc/pgx/v5/pgxpool"
)
// データベース接続
pool, err := pgxpool.New(context.Background(), URI)
defer pol.Close()
// コマンド実行
cmd := `INSERT INTO users (id, name) VALUES ($1, $2)`
_, err = pool.Exec(cmd, 1, "Mogi")
// 1行取得クエリ実行
cmd = `SELECT * FROM users WHERE id = $1`
row := pool.QueryRow(context.Background(), cmd, 1)
var (
id int,
name string
)
err = row.Scan(&id, &name)
// 複数行取得クエリ
cmd = `SELECT id FROM users WHERE name = $1`
rows, err := pool.Query(context.Background(), cmd, "Sohma")
defer rows.Close()
for rows.Next() {
var id int
err := rows.Scan(&id)
}
// トランザクション開始
tx, _ := pool.Begin(context.Background())
defer tx.Rollback()
// txを通したコマンド・クエリ処理
// コミット
err = tx.Commit(context.Background())

stringとrune
GoはUTF-8を問題なく扱えるため,通常はstring
で保持して問題ない.しかし,文字数など文字単位で操作が必要な時はrune
を扱う.
string
は[]byte
のエイリアスで単なるバイト列だが,rune
はint32
のエイリアスでUnicodeコードポイントとして表現される.そのためrune
では一文字の長さ(len
によるもの)が1であることが保証される.

タグ
変数の宣言時に型名の後に生文字リテラルでタグ名:"タグ値"
の形式で「タグ」というメタ情報を変数に付与できる.これにより実行時にメタ情報をリフレクションで読み出して,プログラムの動作を制御することができる.
主なタグにjsonパッケージのjson:"keyname"
やgo-playground/validatorのvalidate:"required"
がある.

go-playground/validator
構造体のフィールドにvalidate:"制約"
の形でタグをつけることで,構造体のフィールドに対してバリデーションを行う機能を提供するパッケージ.
主な制約
-
required
: デフォルト値以外であること.intなら0以外,stringならnil
や""
以外. -
min=X
/max=X
: 最大値・最小値.数値ならその値に対して.文字列なら長さに対して. -
eq=X
/ne=X
: 同値・同値でない. -
lt=X
/gt=X
: 未満・超過.数値ならその値に対して.文字列なら長さに対して.

gomock
インターフェースを用意していれば,そのインターフェースに適合するモックを実装したファイルを生成してくれるツール及びパッケージ.
オリジナルはメンテナンスモードなのでUberが管理しているものを使う.
go get go.uber.org/mock/gomock
go install go.uber.org/mock/mockgen@latest
// Protoで管理しているときは,go install先のGOPATHを認識しないので直接パスを指定する
$(go env GOPATH)/bin/mockgen \
-source=${インターフェースのあるファイル} \
-package=${生成するファイルのパッケージ} \
-mock_names=${モックの名前("インターフェース名=モック名"をカンマつなぎで複数指定可)} \
-destination=${ファイル出力先}
例えば,ルーターに渡すコントローラーの持つfunc GetAllTodos(w *http.ResponseWriter,r *http.Request) int
が1回呼び出されたかをテストする場合は以下のように記述する.
func TestGetTodos(t *testing.T) {
mc := gomock.NewController(t)
defer mc.Finish()
mockController := controller_mock.NewTodoControllerMock(mc)
mockController.EXPECT().GetAllTodos(gomock.Any(), gomock.Any()).Return(gomock.Any()).Times(1)
router := NewTodoRouter(mockController)
handler := router.SetupRoutes()
req := httptest.NewRequest(http.MethodGet, "/todos", nil)
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
}

rs/corsパッケージ
CORSの設定ができるパッケージ.
net/httpのハンドラーと組み合わせて使う.
mux := http.NewServeMux()
mux.HandleFunc("/todos", func (w http.ResponseWriter, r *http.Request) {
// 何かの処理
}
// CORS設定
c := cors.New(cors.Options{
AllowedOrigins: []string{"*"},
AllowCredentials: true,
AllowedMethods: []string{"GET", "POST", "PATCH", "DELETE"},
})
handler := c.Handler(mux)