Go言語入門:よくある質問 -Changes from C Vol.4-

スポンサーリンク
Go言語入門:よくある質問 -Changes from C Vol.4- ノウハウ
Go言語入門:よくある質問 -Changes from C Vol.4-
この記事は約15分で読めます。
よっしー
よっしー

こんにちは。よっしーです(^^)

本日は、Go言語のよくある質問 について解説しています。

スポンサーリンク

背景

Go言語を学んでいると「なんでこんな仕様になっているんだろう?」「他の言語と違うのはなぜ?」といった疑問が湧いてきませんか。Go言語の公式サイトにあるFAQページには、そんな疑問に対する開発チームからの丁寧な回答がたくさん載っているんです。ただ、英語で書かれているため読むのに少しハードルがあるのも事実で、今回はこのFAQを日本語に翻訳して、Go言語への理解を深めていけたらと思い、これを読んだ時の内容を備忘として残しました。

Changes from C

なぜ++と--は式ではなく文なのか? そしてなぜ後置で、前置ではないのか?

ポインタ演算がなければ、前置・後置インクリメント演算子の利便性の価値は下がります。それらを式の階層から完全に取り除くことで、式の構文が簡素化され、++--の評価順序に関する厄介な問題(例えばf(i++)p[i] = q[++i])も同様に排除されます。この簡素化は重要です。後置対前置については、どちらでもうまく機能しますが、後置の方がより伝統的です。前置への固執はSTLとともに生まれましたが、これは皮肉にも名前に後置インクリメントを含む言語のためのライブラリです。

解説

この問題は何について説明しているの?

C言語やJavaではこんなコードが書けます:

// C言語
int x = 5;
int y = x++;      // yは5、xは6
int z = ++x;      // zは7、xは7

// 式の中で使える
printf("%d", i++);
arr[i++] = 10;

でも、Goではこれができません:

// Go
x := 5
y := x++          // エラー! x++ は式ではない
x++               // OK: 文として使用

// 式の中で使えない
fmt.Println(i++)  // エラー!
arr[i++] = 10     // エラー!

「なぜ制限されているの?」「なぜ++xがないの?」という疑問に答えます。

基本的な用語
  • 前置インクリメント (prefix): ++i (先に増やして、増やした値を返す)
  • 後置インクリメント (postfix): i++ (現在の値を返して、後で増やす)
  • 式 (expression): 値を返すもの(x + yf()など)
  • 文 (statement): 処理を実行するもの(ifforx++など)
  • 評価順序: 式が計算される順番
  • 副作用 (side effect): 値を返す以外の効果(変数の変更など)
前置と後置の違い(C言語での動作)
後置インクリメント: i++
int i = 5;
int x = i++;   // xには5が代入され、その後iが6になる

// 展開するとこうなる:
int temp = i;  // 現在の値を保存
i = i + 1;     // iを増やす
int x = temp;  // 古い値を返す
前置インクリメント: ++i
int i = 5;
int x = ++i;   // iが先に6になり、xには6が代入される

// 展開するとこうなる:
i = i + 1;     // iを増やす
int x = i;     // 新しい値を返す
違いが分かる例
int i = 5;

printf("%d\n", i++);  // 5を表示、iは6
printf("%d\n", i);    // 6を表示

i = 5;
printf("%d\n", ++i);  // 6を表示、iは6
printf("%d\n", i);    // 6を表示
C言語での混乱する例
例1: 評価順序の問題
int i = 0;
int arr[10] = {0};

arr[i] = i++;   // これは何?

問題:

左辺のiと右辺のi++、どちらが先に評価される?

可能性1:
  arr[0] = i++;  // iは0
  arr[0] = 0;    // 代入
  i = 1;         // インクリメント

可能性2:
  temp = i++;    // iを使って、インクリメント
  arr[1] = temp; // すでにiが1?

結果: 未定義動作!
例2: 複雑な式
int i = 5;
int result = i++ + ++i;

問題:

評価順序は?

可能性1:
  i++ → 5を返す、iは6
  ++i → iを7にして7を返す
  result = 5 + 7 = 12

可能性2:
  ++i → iを6にして6を返す
  i++ → 6を返す、iは7
  result = 6 + 6 = 12

可能性3:
  その他の順序...

結果: コンパイラ依存!
例3: 関数呼び出し
void print(int a, int b) {
    printf("%d, %d\n", a, b);
}

int i = 5;
print(i++, i++);

問題:

引数の評価順序は?

出力の可能性:
  5, 6
  6, 5
  5, 5
  
どれになるか保証されない!
例4: 配列のインデックス
int i = 0;
int arr[5] = {1, 2, 3, 4, 5};
int val = arr[i++] + arr[i++];

問題:

arr[0] + arr[1]? 
arr[1] + arr[0]?
それとも...?

未定義!
Goの設計判断
決断1: 文にする(式ではない)

C言語:

int x = i++;    // 式として使える
printf("%d", i++);

Go:

x := i++        // エラー! 式ではない
fmt.Println(i++) // エラー!

// 正しい書き方
i++             // 文として独立
x := i          // xに値を代入

利点:

式として使えない
  ↓
評価順序の問題が起きない
  ↓
混乱が減る
  ↓
コードが明確
決断2: 後置のみ

Go:

i++   // OK
++i   // エラー! 前置は存在しない

理由:

前置と後置の違い = 式として使う時のみ重要
  ↓
式として使えないなら違いは無意味
  ↓
1つに統一
  ↓
後置の方が伝統的
なぜ後置を選んだか?
理由1: 歴史と伝統
C言語(1972年):
  i++; が一般的
  
後置の方が直感的:
  「iを使って、その後増やす」
  
前置の普及:
  STL(C++標準テンプレートライブラリ)の影響
  反復子(iterator)で ++it が推奨された
理由2: C++の名前の皮肉
C++ という名前:
  "C" + "++"
  
意味:
  「Cの次のバージョン」
  「Cをインクリメント」
  
皮肉:
  C++という名前自体が後置インクリメント!
  でもSTLは前置を推奨

Bjarne Stroustrup(C++の作者)のジョーク:

「C++という名前は、Cを増やすが、古い値を使う
 = Cの問題を継承している」

もし前置なら"++C"だったかも?
理由3: どちらでもいい
Goでは:
  i++ は文
  値を返さない
  ↓
前置も後置も動作は同じ
  ↓
どちらか1つを選ぶだけ
  ↓
より伝統的な後置を選択
実用的な影響
C言語でよくあるパターン
// パターン1: ループ
for (int i = 0; i < 10; i++) {
    // ...
}

// パターン2: ポインタ操作
while (*p++ != '\0') {
    // pを進めながら処理
}

// パターン3: 配列操作
arr[i++] = value;
Goでの書き方
// パターン1: ループ
for i := 0; i < 10; i++ {  // 同じ!
    // ...
}

// パターン2: スライス操作
for _, ch := range str {   // より良い方法
    // ...
}

// パターン3: 配列操作
arr[i] = value  // 代入
i++             // インクリメント(別の行)
混乱が排除された例
例1: 関数呼び出し

C言語(混乱):

int i = 0;
printf("%d %d\n", i++, i++);  // 何が出力される?

Go(明確):

i := 0
fmt.Println(i, i)  // 明確!
i++
i++
例2: 複雑な式

C言語(混乱):

result = arr[i++] + arr[i++];  // 未定義!

Go(明確):

result := arr[i] + arr[i+1]  // 明確!
i += 2

// または
a := arr[i]
i++
b := arr[i]
i++
result := a + b
例3: 代入との組み合わせ

C言語(混乱):

x = y = i++;  // yは何? xは何?

Go(エラー):

x := y := i++  // コンパイルエラー!

// 正しい書き方
x := i
y := i
i++
ポインタ演算との関係
C言語でのポインタ+インクリメント
char *p = str;
while (*p++ != '\0') {
    // pを進めながら、現在の文字を処理
}

// これは便利だが...

便利な理由:

*p++  // 1文字読んで、ポインタを進める
      // 1行で2つの操作
Goでは不要
// ポインタ演算自体がない
// スライスとrangeを使う

for i, ch := range str {
    // iとchを使って処理
    // 明確で安全!
}

// または
for i := 0; i < len(str); i++ {
    ch := str[i]
    // ...
}

Goの設計:

ポインタ演算なし
  ↓
++/-- を式として使う必要性が減少
  ↓
文にしても問題ない
  ↓
簡素化のチャンス!
実際のコード例
C言語スタイル(複雑)
// C言語
int copy_and_count(char *dest, char *src) {
    int count = 0;
    while ((*dest++ = *src++) != '\0') {
        count++;
    }
    return count;
}

// 1行で多くのことが起きている:
// 1. *src を読む
// 2. *dest に書く
// 3. src をインクリメント
// 4. dest をインクリメント
// 5. '\0' と比較

問題点:

  • 読みにくい
  • デバッグしにくい
  • 間違えやすい
Goスタイル(明確)
// Go
func copyAndCount(dest, src []byte) int {
    count := 0
    for i := 0; i < len(src) && src[i] != 0; i++ {
        dest[i] = src[i]
        count++
    }
    return count
}

// または、もっとGoらしく
func copyAndCount(dest, src []byte) int {
    count := copy(dest, src)
    return count
}

利点:

  • 各行が1つのことをする
  • 読みやすい
  • デバッグしやすい
Goでの推奨パターン
パターン1: 単独の文として使う
// Good
i++
count++

// Bad(エラー)
x := i++
fmt.Println(count++)
パターン2: ループ
// Good: for文の後置部分
for i := 0; i < n; i++ {
    // ...
}

// Good: 必要なら手動で
for {
    if condition {
        break
    }
    // ...
    i++
}
パターン3: インクリメントが必要な場所
// Bad(C言語スタイル)
// arr[i++] = value  // エラー!

// Good
arr[i] = value
i++

// または
i++
arr[i] = value
パターン4: 値も使いたい場合
// Bad(エラー)
// x := i++

// Good
x := i
i++

// または(目的による)
i++
x := i
他の言語との比較
Python
# Pythonには ++ がない!
i = 0
i += 1  # インクリメント

# なぜ?
# Guido van Rossum: 「不要。i += 1 で十分」
Rust
// Rustも ++ がない!
let mut i = 0;
i += 1;

// 理由: 式と文を明確に分離
Swift
// Swift 3.0 で ++ と -- を削除!
var i = 0
i += 1

// 理由: 混乱を避ける、シンプルに

トレンド:

現代の言語:
  C/C++/Java の ++ を見直す
  ↓
  シンプルさと明確さを優先
  ↓
  ++ を削除または制限
まとめ
なぜ文なのか?

理由1: 評価順序の問題を排除

式として使える
  ↓
複雑な式で使われる
  ↓
評価順序が問題に
  ↓
未定義動作

文にする
  ↓
単独でのみ使える
  ↓
評価順序の問題なし
  ↓
常に明確

理由2: ポインタ演算がない

ポインタ演算あり
  ↓
*p++ が便利
  ↓
式として必要

ポインタ演算なし
  ↓
式としての利便性↓
  ↓
文で十分

理由3: 構文の簡素化

式の階層から除去
  ↓
文法がシンプルに
  ↓
パーサーが簡単
  ↓
ツールが作りやすい
なぜ後置なのか?

理由1: 伝統

C言語: i++ が一般的
歴史: 後置が先
後置: より直感的

理由2: どちらでもいい

文として使う
  ↓
前置も後置も同じ動作
  ↓
1つ選ぶだけ
  ↓
伝統的な後置を選択

理由3: C++の皮肉

C++ = "C" + "++"
名前自体が後置!
でもSTLは前置推奨
皮肉を込めて後置を選択?
実用的な影響
観点C/JavaGo
混乱する式arr[i++] = i++不可能(エラー)
評価順序コンパイラ依存常に明確
前置vs後置使い分け必要後置のみ
デバッグ難しい簡単
学習曲線急(注意点多い)緩やか
トレードオフ

失うもの:

  • 「簡潔な」コード(1行で複数の操作)
  • C言語スタイルのイディオム

得るもの:

  • ✅ 明確さ
  • ✅ 予測可能性
  • ✅ デバッグのしやすさ
  • ✅ コードレビューのしやすさ
  • ✅ 評価順序の問題の排除
Goの哲学
「簡潔」より「明確」
「賢い」より「読みやすい」
「1行で全部」より「1行1つのこと」

実例:

// C言語: 「賢い」コード
while (*dst++ = *src++);

// Go: 「明確な」コード
for i := 0; i < len(src); i++ {
    dst[i] = src[i]
}

// または
copy(dst, src)
初心者へのアドバイス

1. C言語の癖を忘れる

C言語: i++ を式として使う
Go: i++ は独立した文

新しい考え方に慣れる

2. 1行1つのことを

// Bad(エラーだが、もし可能でも避けるべき)
x := i++

// Good
x := i
i++

3. より良い方法を使う

// C言語的
for i := 0; i < len(arr); i++ {
    process(arr[i])
}

// Goらしい
for _, item := range arr {
    process(item)
}

4. ++の代わりに+=も検討

i++      // OK
i += 1   // より明示的

count += 5  // 5増やす
最終的な結論
++ を文にして後置のみにしたのは:
  
制限ではなく、
意図的な簡素化
  ↓
評価順序の問題を根絶
  ↓
コードの明確さ向上
  ↓
長期的な保守性向上

歴史的視点:

C言語(1970年代):
  *p++ のような「賢い」コードが美徳
  
現代(2020年代):
  明確で読みやすいコードが美徳
  
Go:
  現代の価値観を反映

実際の効果:

Googleのコードレビューより:
  「C++時代: ++i と i++ の議論に時間を費やす」
  「Go時代: そんな議論は起きない」
  
生産性向上!

この設計判断により、Goは「動作するコード」だけでなく、「読みやすく保守しやすいコード」を書くことを推奨しています。++を文にして後置のみにしたことは、Goの「シンプルで明確」という哲学の完璧な例なのです! 🎯

実際、多くの経験豊富なGoプログラマーは「最初は制限に感じたが、今では C言語の i++ 式が混乱の元だったと気づいた」と言います。これは、良い言語設計が長期的にプログラマーを守ってくれる例です! 🛡️

おわりに 

本日は、Go言語のよくある質問について解説しました。

よっしー
よっしー

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

それでは、また明日お会いしましょう(^^)

コメント

タイトルとURLをコピーしました