🌈

Goのプロジェクトに入ってやったキャッチアップ

に公開

はじめに

Go を業務で使って、半年が経過したので、キャッチアップしたことや Go の慣習について PR などで指摘を受けたことをまとめていきたいと思います!

対象読者

  • Tour of Go を完了した方
  • Go で1回、API などを作ってみて、レベルアップしたい方
  • 個人で Go を使用している方

概要

基本的だけど個人開発とかではなかなかそこまで理解に手が回らなかったことをまとめていきます。
あとは、Go を使ってそこそこ経つけど、使ってこなかったエディタの便利機能も紹介します!

迷ったら、Style Guide を読むの大事

https://google.github.io/styleguide/go/

やったこと

その1:変数、関数名につける単語はは、先頭大文字にする

結論:ID と大文字で記述するのが一般的

Go では、ID のようにすべて大文字で記述するのが最も一般的で推奨される作法です。これは、id や Id ではありません。

基本的に以下のように構造体名や変数名、関数はIDと大文字記述するのが一般的です。

type User struct {
    ID   string
    Name string
}

func GetUserByID(userID string) (*User, error) {
    // ...
}

ただし、API のレスポンスフィールドのようにクライアント側に返す時に(クライアント側の事情で)json タグでは、json:"id" validator:omitemptyのようにidを使用する場合は注意が必要です。

個人的にここに若干、つまづきました。
Go コードの変数、関数名は、IDだけど、json タグには、idという・・・

その2:エラーのシャドーイング

外部スコープで宣言されたエラー変数が、内部スコープで同じ名前の新しい変数によって隠される現象です。この問題は、特に if 文や for ループで発生しやすく、プログラマーが意図せず新しいローカル変数を宣言してしまうことで、外部スコープのエラー変数が更新されないままになってしまいます。

package main

import (
	"fmt"
	"os"
)

func main() {
	// 外部スコープでエラー変数を宣言
	err := run()
	if err != nil {
		fmt.Println("Main:", err) // ここではエラーはnilのまま
	}
}

func run() error {
	var err error

	// 最初のエラーを設定
	_, err = strconv.Atoi("invalid")
	if err != nil {
		fmt.Printf("最初のエラー: %v\n", err)
	}

	// 内側のスコープでerrを再宣言(シャドウイング)
	if true {
		file, err := os.Open("nonexistent.txt") // ここでerrがシャドウイングされる
		if err != nil {
			fmt.Printf("ファイルオープンエラー: %v\n", err)
			return // 内側のerrのみ処理される
		}
		defer file.Close()
	}

	// 外側のerrは最初のエラーのまま(予期しない動作)
	fmt.Printf("最終的なエラー状態: %v\n", err)
}

正直めんどくさい仕様だなと思った箇所です・・・。

ただ、linter で指摘してくれるし、Goland であればシャドーイング箇所の色が緑色に変わるので、まぁ気付きやすくはありました。VS Code で、リアルタイムに指摘してくれる拡張ってあるんですか?

いままで、VS Code の Go 公式拡張を使ってきましたが、なかったなと思いまして・・・。

その3:Goland、VS Code の構造体自動埋め機能

まじで、便利すぎた!
特にテスト時!

業務のコードはサンプルと比較できないほどフィールドが多いので、ありがたいです。
個人開発では、そこまで必要に迫られなかった。

  • Goland の場合
type User struct {
    // ここで、右クリックから一番上の構造体の項目を埋めるみたいな選択肢をクリックする
}
  • VS Code の場合
type User struct {
    // ここで、cmd + . を押す(Mac)
    // ここで、ctrl + . を押す(Windows)
}

その4:go-validator の使い方

ここに関しては、いままで、go-validatorをちゃんと使ってこなかった私が悪いんですが、
こんな使い方があるんだぁ〜っていう発見がたくさんあるし、go-validatorめっちゃ色々できるじゃんってなったポイントです!

例をあげると、

  • 入れ子の構造体のバリデーション

diveを入れ子の構造体のフィールドのvalidateタグに設定する

type Address struct {
	Street string `json:"street" validate:"required"`
	City   string `json:"city" validate:"required"`
}

// User はユーザー情報を保持する構造体
type User struct {
	Name      string    `json:"name" validate:"required,min=3"`
	Addresses []Address `json:"addresses" validate:"required,dive"` // ⭐︎ dive タグ
}
  • validateタグの順序
// User はユーザー情報を保持する構造体
type User struct {
	// ⚠️ 悪い例: omitemptyがrequiredより先に記述されている
	// この場合、Emailが空ならバリデーションはスキップされる
	Email string `validate:"omitempty,required,email"`
}

解説

  1. 上記の例では、Email フィールドのタグが "omitempty,required,email" となっています。
  2. omitempty が最初にあるため、バリデーターはまずフィールドが空かどうかをチェックします。
  3. user.Email は空文字列 "" です。
  4. omitempty のルールに従い、バリデーターは残りのすべてのルール(required や email)を無視してバリデーションを終了します。

⬇️

その結果、required ルールが評価されないです!
Email フィールドが空でもエラーは返されず、バリデーションは成功と見なされてしまいます。

// ✅ 正しい例: Emailが空の場合はバリデーションしない
type UserGood struct {
	Email string `validate:"omitempty,email"`
}

この例では、Email が空文字列の場合はバリデーションがスキップされますが、"a"のような無効な値が設定されている場合は email タグでエラーになります。これは omitempty の意図された使い方です。

  • require_ifなどの条件付きバリデーション

go-validator の required_if タグは、特定のフィールドの値に基づいて別のフィールドの必須性を動的に制御するための非常に便利な機能です。これは、フォーム入力や API リクエストなど、特定の条件に応じてフィールドが必須になる場合に役立ちます。

// User はユーザー情報を保持する構造体
type User struct {
	// AccountTypeは"無料会員"または"有料会員"
	AccountType string `json:"account_type" validate:"required,oneof=無料会員 有料会員"`
	// AccountTypeが"有料会員"の場合のみ、PaymentInfoは必須
	PaymentInfo string `json:"payment_info" validate:"required_if=AccountType 有料会員"`
}
  1. バリデーションが成功する例(無料会員)
func main() {
	validate := validator.New()

	// PaymentInfoは空でもOK
	userFree := User{
		AccountType: "無料会員",
		PaymentInfo: "",
	}
	err := validate.Struct(userFree)
	if err != nil {
		fmt.Println("無料会員のバリデーション失敗!")
		return
	}
	fmt.Println("無料会員のバリデーション成功!")
}
  1. バリデーションが成功する例(有料会員)
func main() {
	validate := validator.New()

	// PaymentInfoが入力されているためOK
	userPremiumValid := User{
		AccountType: "有料会員",
		PaymentInfo: "credit_card_details",
	}
	err = validate.Struct(userPremiumValid)
	if err != nil {
		fmt.Println("有料会員のバリデーション失敗!")
		return
	}
	fmt.Println("有料会員のバリデーション成功!")
}
  1. バリデーションが失敗する例(有料会員なのに PaymentInfo が空)
func main() {
	validate := validator.New()

	userPremiumInvalid := User{
		AccountType: "有料会員",
		PaymentInfo: "", // 有料会員なのにPaymentInfoが空でエラー
	}
	err := validate.Struct(userPremiumInvalid)
	if err != nil {
		fmt.Println("\nバリデーション失敗...")
		for _, err := range err.(validator.ValidationErrors) {
			fmt.Printf("フィールド: %s, タグ: %s\n", err.Field(), err.Tag())
		}
		return
	}
	fmt.Println("\nバリデーション成功!")
}

解説

  • PaymentInfo フィールドには、validate:"required_if=AccountType 有料会員"というタグが付いています。

    • これは「AccountType フィールドの値が有料会員である場合に、PaymentInfo フィールドを必須にする」という意味です。
  • userFree の例では、AccountType が無料会員なので、PaymentInfo が空でも required_if ルールは適用されず、バリデーションは成功します

  • userPremiumInvalid の例では、AccountType が有料会員なので、required_if ルールが適用され、空の PaymentInfo に対して required エラーが発生します。

その他の条件付きバリデーションタグ

タグ 意味
required_unless 別のフィールドが指定された値でない場合に必須にします。
required_with 指定されたすべてのフィールドが存在する場合に必須にします。
required_without 指定されたフィールドのいずれかが存在しない場合に必須にします。
required_without_all 指定されたすべてのフィールドが存在しない場合に必須にします。

まとめ

業務で Go を使用してみてのキャッチアップというか、個人でやっている時の違いに若干つまづいた部分を記事にしてみました。この記事が、Go 初心者の方の Go 参考になれば幸いです。

Go 初心者で、Go で何か作りたいよ!っていう方は、以下の本はめちゃくちゃおすすめです!

詳解 Go 言語 Web アプリケーション開発

https://amzn.asia/d/c2YkR3Y

私自身、この本がきっかけで Go のことがより好きになりましたし、もっと Go でプログラミングしたいと思えました!

ここで、宣伝をさせてください!

⭐️ 宣伝 ⭐️

2025/9/27 サイバーエージェント アベマタワーにて、Go Conference 2025 が行われます!

公式サイトは以下です!

https://gocon.jp/2025/

さい Go まで、お読みいただきありがとうございました。

Discussion