
こんにちは。よっしーです(^^)
本日は、Go言語を効果的に使うためのガイドラインについて解説しています。
背景
Go言語を学び始めて、より良いコードを書きたいと思い、Go言語の公式ドキュメント「Effective Go」を知りました。これは、いわば「Goらしいコードの書き方指南書」になります。単に動くコードではなく、効率的で保守性の高いコードを書くためのベストプラクティスが詰まっているので、これを読んだ時の内容を備忘として残しました。
多重代入におけるブランク識別子
for
range
ループでのブランク識別子の使用は、一般的な状況の特殊なケースです:多重代入です。
代入で左辺に複数の値が必要だが、そのうち1つの値がプログラムで使用されない場合、代入の左辺でブランク識別子を使用することで、ダミー変数を作成する必要がなくなり、その値が破棄されることが明確になります。例えば、値とエラーを返す関数を呼び出すが、エラーだけが重要な場合、ブランク識別子を使用して無関係な値を破棄します。
if _, err := os.Stat(path); os.IsNotExist(err) {
fmt.Printf("%s does not exist\n", path)
}
時々、エラーを無視するためにエラー値を破棄するコードを見かけることがあります。これは恐ろしい慣行です。常にエラー戻り値をチェックしてください。それらには理由があって提供されています。
// 悪い!このコードはpathが存在しない場合にクラッシュします。
fi, _ := os.Stat(path)
if fi.IsDir() {
fmt.Printf("%s is a directory\n", path)
}
多重代入とは?
多重代入は、1つの文で複数の変数に同時に値を代入することです。Goでは関数が複数の値を返すことが一般的なため、この機能が重要になります。
基本的な多重代入の例
package main
import (
"fmt"
"os"
"strconv"
"strings"
)
func main() {
// 1. 基本的な多重代入
a, b := 1, 2
fmt.Printf("a = %d, b = %d\n", a, b)
// 2. 関数の複数戻り値を受け取る
quotient, remainder := divide(10, 3)
fmt.Printf("10 ÷ 3 = %d 余り %d\n", quotient, remainder)
// 3. マップの値取得と存在確認
ages := map[string]int{"太郎": 25, "花子": 30}
age, exists := ages["太郎"]
if exists {
fmt.Printf("太郎の年齢: %d歳\n", age)
}
}
func divide(a, b int) (int, int) {
return a / b, a % b
}
ブランク識別子を使った多重代入
package main
import (
"fmt"
"io"
"os"
"strconv"
"strings"
)
func main() {
path := "example.txt"
// 良い例:値を無視してエラーだけをチェック
fmt.Println("== ファイル存在チェック ==")
if _, err := os.Stat(path); os.IsNotExist(err) {
fmt.Printf("%s は存在しません\n", path)
} else if err != nil {
fmt.Printf("エラー: %v\n", err)
} else {
fmt.Printf("%s は存在します\n", path)
}
// 良い例:エラーを無視して値だけを取得(安全な場合のみ)
fmt.Println("\n== 数値変換(安全な場合) ==")
validNumbers := []string{"123", "456", "789"}
for _, numStr := range validNumbers {
// 事前に検証済みなので安全
num, _ := strconv.Atoi(numStr)
fmt.Printf("%s -> %d\n", numStr, num)
}
// 良い例:複数戻り値の一部だけ使用
fmt.Println("\n== 文字列分割 ==")
email := "user@example.com"
username, _ := splitString(email, "@") // ドメイン部分は不要
fmt.Printf("ユーザー名: %s\n", username)
// 良い例:マップで存在確認のみ
fmt.Println("\n== マップの存在確認 ==")
permissions := map[string]bool{
"read": true,
"write": false,
}
_, hasRead := permissions["read"]
if hasRead {
fmt.Println("読み取り権限の設定があります")
}
}
func splitString(s, sep string) (string, string) {
parts := strings.Split(s, sep)
if len(parts) >= 2 {
return parts[0], parts[1]
}
return s, ""
}
危険な使用例とその対策
package main
import (
"fmt"
"os"
"strconv"
)
func badExample() {
fmt.Println("== 悪い例:エラーを無視 ==")
path := "nonexistent.txt"
// 危険!ファイルが存在しない場合、fiはnilになる
fi, _ := os.Stat(path) // エラーを無視
// ここでクラッシュする可能性がある
if fi != nil && fi.IsDir() {
fmt.Printf("%s はディレクトリです\n", path)
} else {
fmt.Printf("%s はファイルです(または存在しません)\n", path)
}
}
func goodExample() {
fmt.Println("== 良い例:適切なエラーハンドリング ==")
path := "nonexistent.txt"
fi, err := os.Stat(path)
if err != nil {
if os.IsNotExist(err) {
fmt.Printf("%s は存在しません\n", path)
} else {
fmt.Printf("エラー: %v\n", err)
}
return
}
if fi.IsDir() {
fmt.Printf("%s はディレクトリです\n", path)
} else {
fmt.Printf("%s はファイルです\n", path)
}
}
func demonstrateErrorIgnoring() {
fmt.Println("== エラー無視の危険性 ==")
// 無効な数値文字列
invalidNumber := "abc123"
// 悪い例:エラーを無視
num, _ := strconv.Atoi(invalidNumber) // エラーを無視
fmt.Printf("悪い例 - 結果: %d (ゼロ値)\n", num)
// 良い例:エラーをチェック
num2, err := strconv.Atoi(invalidNumber)
if err != nil {
fmt.Printf("良い例 - 変換エラー: %v\n", err)
} else {
fmt.Printf("良い例 - 結果: %d\n", num2)
}
}
func main() {
badExample()
fmt.Println()
goodExample()
fmt.Println()
demonstrateErrorIgnoring()
}
実用的な使用パターン
package main
import (
"fmt"
"io"
"net/http"
"os"
"time"
)
// パターン1:ファイル操作での活用
func copyFile(src, dst string) error {
srcFile, err := os.Open(src)
if err != nil {
return err
}
defer srcFile.Close()
dstFile, err := os.Create(dst)
if err != nil {
return err
}
defer dstFile.Close()
// コピーしたバイト数は不要、エラーのみチェック
_, err = io.Copy(dstFile, srcFile)
return err
}
// パターン2:HTTPリクエストでの活用
func checkWebsite(url string) bool {
// レスポンスボディは不要、ステータスのみチェック
resp, err := http.Get(url)
if err != nil {
return false
}
defer resp.Body.Close()
// レスポンスボディを読み捨て(接続を適切にクローズするため)
_, _ = io.Copy(io.Discard, resp.Body)
return resp.StatusCode == http.StatusOK
}
// パターン3:チャンネル操作での活用
func worker(jobs <-chan int, results chan<- string) {
for job := range jobs {
// 作業時間をシミュレート
time.Sleep(100 * time.Millisecond)
// 作業結果の詳細は不要、完了通知のみ
select {
case results <- fmt.Sprintf("Job %d completed", job):
case <-time.After(1 * time.Second):
fmt.Printf("Job %d timed out\n", job)
}
}
}
// パターン4:構造体のフィールド分割代入
type Person struct {
Name string
Age int
City string
}
func getPersonInfo() (string, int, string) {
p := Person{Name: "太郎", Age: 30, City: "東京"}
return p.Name, p.Age, p.City
}
func main() {
// ファイルコピーのテスト
err := copyFile("source.txt", "dest.txt")
if err != nil {
fmt.Printf("ファイルコピーエラー: %v\n", err)
}
// Webサイトチェック
isOnline := checkWebsite("https://www.google.com")
fmt.Printf("Googleはオンライン: %v\n", isOnline)
// 名前だけ必要で、年齢と都市は不要
name, _, _ := getPersonInfo()
fmt.Printf("名前: %s\n", name)
// 年齢だけ必要
_, age, _ := getPersonInfo()
fmt.Printf("年齢: %d歳\n", age)
}
よくある間違いと対策
package main
import (
"encoding/json"
"fmt"
"os"
)
type Config struct {
Host string `json:"host"`
Port int `json:"port"`
}
// 間違い:エラーを無視する関数
func loadConfigBad(filename string) Config {
data, _ := os.ReadFile(filename) // ファイル読み取りエラーを無視
var config Config
_ = json.Unmarshal(data, &config) // JSON解析エラーを無視
return config
}
// 正しい:適切なエラーハンドリング
func loadConfigGood(filename string) (Config, error) {
var config Config
data, err := os.ReadFile(filename)
if err != nil {
return config, fmt.Errorf("ファイル読み取りエラー: %w", err)
}
err = json.Unmarshal(data, &config)
if err != nil {
return config, fmt.Errorf("JSON解析エラー: %w", err)
}
return config, nil
}
// 適切な場合:値の一部のみ使用
func processConfig() {
config, err := loadConfigGood("config.json")
if err != nil {
fmt.Printf("設定読み込みエラー: %v\n", err)
return
}
// ポート番号のみ必要な場合
fmt.Printf("サーバーポート: %d\n", config.Port)
// ここでホスト名は使用しないが、これは問題ない
// なぜなら、config構造体全体を取得する必要があるため
}
func main() {
processConfig()
}
重要なポイント
1. 適切な使用場面
- 複数戻り値の一部のみ必要な場合
- マップの存在確認のみ行う場合
- ループでインデックスや値の一方のみ使用する場合
2. 避けるべき使用
- エラーを無視する場合(特に重要)
- デバッグ情報として有用な値を無視する場合
- 将来的に必要になる可能性がある値を無視する場合
3. エラーハンドリングの原則
- エラーは常にチェックする
- エラーを無視する場合は、その理由をコメントで明記
- 予期しないエラーでプログラムがクラッシュしないようにする
4. コードの可読性
_
を使うことで「意図的に無視している」ことを明示- ダミー変数名(例:
dummy
,unused
)よりも_
の方が明確 - コードレビューアにとって意図が分かりやすい
5. パフォーマンス考慮
_
に代入された値は実際には保存されない- 大きなデータ構造を無視する場合にメモリ効率が良い
ブランク識別子は強力な機能ですが、特にエラー処理において慎重に使用する必要があります。「エラーを無視しない」という原則を常に念頭に置いてください。
おわりに
本日は、Go言語を効果的に使うためのガイドラインについて解説しました。

何か質問や相談があれば、コメントをお願いします。また、エンジニア案件の相談にも随時対応していますので、お気軽にお問い合わせください。
それでは、また明日お会いしましょう(^^)
コメント