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

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

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

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

スポンサーリンク

背景

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

Changes from C

なぜ波括弧はあるがセミコロンはないのか? そしてなぜ開き括弧を次の行に置けないのか?

Goは文のグループ化に波括弧を使用します。これはC言語ファミリーのいずれかの言語で作業したことのあるプログラマーにとって馴染みのある構文です。しかし、セミコロンはパーサーのためのものであり、人間のためのものではありません。私たちはそれらを可能な限り排除したいと考えました。この目標を達成するために、GoはBCPLからトリックを借用しています。文を区切るセミコロンは形式文法にありますが、先読みなしで、文の終わりになり得る任意の行の末尾でレキサーによって自動的に挿入されます。これは実際には非常にうまく機能しますが、波括弧スタイルを強制する効果があります。例えば、関数の開き括弧は単独で行に現れることができません。

一部の人々は、レキサーが先読みを行って波括弧を次の行に配置できるようにすべきだと主張しています。私たちは同意しません。Goコードはgofmtによって自動的にフォーマットされることを意図しているため、何らかのスタイルを選択する必要があります。そのスタイルはCやJavaで使用してきたものとは異なるかもしれませんが、Goは異なる言語であり、gofmtのスタイルは他のどのスタイルにも劣りません。さらに重要なこと—はるかに重要なこと—は、すべてのGoプログラムに対してプログラムで義務付けられた単一のフォーマットの利点が、特定のスタイルの知覚される欠点を大きく上回ることです。また、Goのスタイルは、Goのインタラクティブな実装が特別なルールなしで標準構文を一度に1行ずつ使用できることを意味することにも注意してください。

解説

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

Goを初めて書くと、こんな違和感を覚えます:

// Go: これはOK
func main() {
    fmt.Println("Hello")
}

// Go: これはエラー!
func main()
{
    fmt.Println("Hello")
}

「なぜ開き括弧を次の行に書けないの?」 「セミコロンはどこ?」

という疑問に答えます。

基本的な用語
  • 波括弧 (braces): {} のこと
  • セミコロン: ; 文の終わりを示す記号
  • レキサー (lexer): ソースコードをトークンに分解するプログラム
  • 先読み (lookahead): 次のトークンを見て判断すること
  • BCPL: C言語の祖先となった言語
  • gofmt: Goコードの自動整形ツール
なぜ波括弧を使うのか?
C言語ファミリーとの継続性

Goの選択:

if x > 0 {
    fmt.Println("positive")
}

理由:

C言語、Java、JavaScript、C++など
多くのプログラマーが慣れている
  ↓
学習コストが低い
  ↓
波括弧を採用

他の選択肢:

# Python: インデント
if x > 0:
    print("positive")
# Ruby: end キーワード
if x > 0
    puts "positive"
end

Goがこれらを選ばなかった理由:

  • 新しい概念を学ぶ必要がある
  • C言語ファミリーの経験が活かせない
  • 波括弧で十分機能する
セミコロンはどこに?
セミコロンは存在する(見えないだけ)

実は、Goにもセミコロンはあります!

// あなたが書くコード
func main() {
    x := 5
    fmt.Println(x)
}

// コンパイラが見るコード
func main() {
    x := 5;
    fmt.Println(x);
}

自動挿入!

セミコロン自動挿入の仕組み

ルール:

行末が「文の終わり」になり得る場合、
自動的にセミコロンを挿入

「文の終わり」になり得るもの:

  • 識別子: x, count, name
  • リテラル: 42, "hello", true
  • break, continue, return
  • ++, --
  • ), ], }

例:

x := 5       // → x := 5;
return true  // → return true;
count++      // → count++;
}            // → };

「文の終わり」にならないもの:

  • 演算子: +, -, *, /
  • カンマ: ,
  • 開き括弧: (, [, {

例:

// これは1つの文
x := 5 +
     10 +
     15
// 各行末に + があるので、セミコロンは挿入されない
なぜ開き括弧を次の行に置けないのか?
問題のあるコード
func main()
{
    fmt.Println("Hello")
}

何が起こるか:

// あなたが書いたつもり:
func main()
{
    fmt.Println("Hello")
}

// コンパイラが見るコード:
func main();  // ← セミコロンが自動挿入!
{
    fmt.Println("Hello");
}

// これは:
// 1. main関数の宣言(本体なし) → エラー!
// 2. 謎のブロック

エラーメッセージ:

syntax error: unexpected semicolon or newline before {
なぜセミコロンが挿入されるか?

レキサーの判断:

func main()
         ^^
         これは「文の終わり」になり得る
         ↓
         セミコロンを挿入!

レキサーの視点:

"func" → 関数宣言の開始
"main" → 関数名
"(" → パラメータリストの開始
")" → パラメータリストの終了
改行 → この行は終わった? たぶんそう!
     → セミコロンを挿入
正しい書き方
func main() {  // 開き括弧を同じ行に!
    fmt.Println("Hello")
}

レキサーの判断:

"func" → 関数宣言
"main" → 関数名
"(" → パラメータ開始
")" → パラメータ終了
"{" → 本体開始(文はまだ続く)
改行 → セミコロン挿入しない
様々なケースでの挙動
ケース1: if文
// ❌ エラー
if x > 0
{
    fmt.Println("positive")
}

// コンパイラが見るコード:
if x > 0;  // セミコロン挿入 → エラー!
{
    fmt.Println("positive");
}

// ✅ 正しい
if x > 0 {
    fmt.Println("positive")
}
ケース2: for文
// ❌ エラー
for i := 0; i < 10; i++
{
    fmt.Println(i)
}

// コンパイラが見るコード:
for i := 0; i < 10; i++;  // 余分なセミコロン!
{
    fmt.Println(i);
}

// ✅ 正しい
for i := 0; i < 10; i++ {
    fmt.Println(i)
}
ケース3: 構造体リテラル
// ❌ エラー
person := Person{
    Name: "Alice",
    Age: 30
}  // ← ここに問題が!

// コンパイラが見るコード:
person := Person{
    Name: "Alice",
    Age: 30;  // セミコロン挿入
};  // さらにセミコロン

// ✅ 正しい: カンマを付ける
person := Person{
    Name: "Alice",
    Age: 30,  // ← カンマ!
}

// または1行に
person := Person{Name: "Alice", Age: 30}

理由:

30の後の改行
  ↓
30は「文の終わり」になり得る
  ↓
セミコロン挿入
  ↓
でもまだ構造体リテラルが終わっていない
  ↓
エラー!

カンマがあれば:
  ↓
カンマは「文の終わり」ではない
  ↓
セミコロン挿入されない
  ↓
OK!
ケース4: 長い式
// ✅ OK: 演算子で終わる
result := longVariableName +
          anotherLongName +
          yetAnotherName

// 各行末が + なので、セミコロン挿入されない

// ❌ エラー: 識別子で終わる
result := longVariableName
          + anotherLongName
          + yetAnotherName

// コンパイラが見るコード:
result := longVariableName;  // セミコロン挿入!
          + anotherLongName;
          + yetAnotherName;
ケース5: return文
// ❌ 意図しない動作
func getValue() int {
    return
        42
}

// コンパイラが見るコード:
func getValue() int {
    return;  // ← ここで終了! 0を返す
    42;      // ← 到達不能コード
}

// ✅ 正しい
func getValue() int {
    return 42
}
BCPLからのトリック
BCPLとは?
BCPL (1967年)
  ↓
B言語 (1969年)
  ↓
C言語 (1972年)
  ↓
C++、Java、Go...

BCPLの特徴:

  • セミコロンは省略可能
  • 改行を文の区切りとして扱う
  • シンプルで読みやすい

Goの採用:

BCPLのアイデア
  ↓
セミコロン自動挿入
  ↓
書く必要がない
  ↓
でも文法上は存在
なぜ「先読み」しないのか?
先読みとは?

仮に先読みがあれば:

// これが許可されるかも?
func main()
{
    fmt.Println("Hello")
}

// レキサーの思考:
func main()  ← 次の行を見る(先読み)
{            ← あ、開き括弧だ!
             ← セミコロン挿入しない

でも、Goはこれをしない。

Goが先読みをしない理由

理由1: シンプルさ

先読みなし:
  現在の行だけ見る
  ↓
  ルールがシンプル
  ↓
  レキサーが単純
  
先読みあり:
  次の行も見る
  ↓
  複雑な判断が必要
  ↓
  レキサーが複雑

理由2: パフォーマンス

先読みなし:
  1行ずつ処理
  ↓
  高速
  
先読みあり:
  複数行を考慮
  ↓
  やや遅い

理由3: どうせgofmtがある

スタイルを議論する必要がない
  ↓
gofmtが自動的に整形
  ↓
1つのスタイルに統一
  ↓
先読みは不要
gofmt: 統一されたスタイル
gofmtとは?

公式の整形ツール:

# ファイルを整形
gofmt -w main.go

# ディレクトリ全体を整形
gofmt -w .

動作:

// 整形前
func  main(){
x:=5
fmt.Println(x)}

// 整形後
func main() {
    x := 5
    fmt.Println(x)
}

すべてのGoコードが同じスタイルに!

スタイルの強制

C言語の世界:

// スタイル1: K&R
int main() {
    printf("Hello");
}

// スタイル2: Allman
int main()
{
    printf("Hello");
}

// スタイル3: GNU
int main()
  {
    printf("Hello");
  }

// endless debate...

議論が尽きない:

  • どのスタイルが良い?
  • チームで統一できない
  • コードレビューで揉める
  • 時間の無駄

Goの世界:

// スタイル1つだけ
func main() {
    fmt.Println("Hello")
}

// これ以外は許可されない

議論の終わり:

  • 1つのスタイルのみ
  • 自動整形
  • 議論不要
  • 本質的な作業に集中
gofmtの威力

統計(Go公式ブログより):

Goのオープンソースコードの分析:
  
70%以上が gofmt でそのまま整形可能
  ↓
ほぼすべてのGoコードが同じスタイル
  ↓
コードレビューがスムーズ
  ↓
生産性向上

他の言語との比較:

C/C++:
  clang-format(設定ファイルが複雑)
  各プロジェクトで設定が異なる
  
Java:
  Google Style, Sun Style, etc.
  統一されていない
  
Python:
  PEP 8(ガイドライン)
  black(自動整形)が登場
  
Go:
  gofmt(公式、1つのスタイル)
  議論の余地なし
インタラクティブ実装への影響
REPLでの動作

インタラクティブGoシェル:

// 1行ずつ入力できる
>>> x := 5
>>> fmt.Println(x)
5

// 複数行の入力
>>> func add(a, b int) int {
...     return a + b
... }
>>> add(2, 3)
5

Goのスタイルなら:

開き括弧が同じ行
  ↓
1行目で関数宣言が完結しない
  ↓
インタープリターは続きを待つ
  ↓
シンプルに実装できる

もし開き括弧が次の行なら:

>>> func add(a, b int) int
(ここで Enter)
  ↓
インタープリター:
  「これで終わり? それとも続く?」
  「次の行を見ないと分からない」
  ↓
複雑な実装が必要
実用的なアドバイス
エディタの設定

VS Code:

{
    "[go]": {
        "editor.formatOnSave": true
    }
}

保存時に自動整形されるので、スタイルを気にする必要なし!

GoLand:

設定 → Tools → File Watchers
→ gofmt を有効化

Vim/Neovim:

" .vimrc
let g:go_fmt_command = "gofmt"
au BufWritePre *.go :GoFmt
よくある間違い

間違い1: 開き括弧を次の行に

// ❌
func calculate()
{
    return 42
}

// ✅
func calculate() {
    return 42
}

間違い2: returnと値の間に改行

// ❌
func getValue() int {
    return
        42  // これは返されない!
}

// ✅
func getValue() int {
    return 42
}

間違い3: 構造体リテラルの最後にカンマなし

// ❌
person := Person{
    Name: "Alice",
    Age: 30  // ← カンマがない
}

// ✅
person := Person{
    Name: "Alice",
    Age: 30,  // ← カンマ!
}
最初の違和感を乗り越える

C/Java プログラマーの典型的な反応:

1週目:
  「なんで開き括弧を次の行に書けないの?」
  「不便...」

2週目:
  「まあ、慣れてきた」
  「gofmt便利かも」

1ヶ月後:
  「スタイルで悩まなくていい!」
  「全てのGoコードが同じスタイル!」
  「楽!」

3ヶ月後:
  「C++のコードレビューで
   インデントとか括弧位置で揉めてる...
   無駄だなぁ」
他の言語の動向
Rustの選択
// Rust: Goと似たアプローチ
fn main() {
    println!("Hello");
}

// rustfmt で自動整形
Pythonの選択
# Python: インデント強制
def main():
    print("Hello")
    
# black で自動整形
JavaScriptの混沌
// JavaScript: セミコロン挿入あり(でもGoほど厳格ではない)

// これはOK
function main() {
    console.log("Hello")
}

// これもOK(でも非推奨)
function main()
{
    console.log("Hello")
}

// Prettier で整形(設定多数)

Goの利点:

JavaScriptよりはるかに厳格
  ↓
1つのスタイルのみ
  ↓
議論不要
まとめ
なぜ波括弧があるのか?
C言語ファミリーとの継続性
  ↓
多くのプログラマーが慣れている
  ↓
学習コストが低い
なぜセミコロンがないのか?
セミコロンは人間のためではない
  ↓
自動挿入で省略
  ↓
コードがすっきり

でも実は:

セミコロンは存在する
  ↓
レキサーが自動挿入
  ↓
プログラマーは書かなくていい
なぜ開き括弧を次の行に置けないのか?
セミコロン自動挿入のルール
  ↓
行末が「文の終わり」ならセミコロン挿入
  ↓
func main() の後で挿入されてしまう
  ↓
エラー

解決策:

開き括弧を同じ行に書く
  ↓
問題解決
なぜ先読みしないのか?
理由1: シンプルさ
理由2: パフォーマンス
理由3: どうせgofmtがある

最も重要:

1つのスタイルに統一
  ↓
gofmtで自動整形
  ↓
スタイルの議論が不要
  ↓
本質的な作業に集中
実用的な影響
観点C/JavaGo
スタイル数多数1つ
スタイル議論頻繁なし
自動整形部分的完全
チーム統一困難自動
コードレビュースタイルで時間消費本質に集中
トレードオフ

失うもの:

  • スタイルの選択肢
  • 個人の好みの表現

得るもの:

  • ✅ すべてのGoコードが同じスタイル
  • ✅ スタイル議論の終焉
  • ✅ 自動整形
  • ✅ コードレビューの効率化
  • ✅ 新しいコードベースへの適応が速い
Goの哲学
「個人の好み」より「チームの生産性」
「選択肢」より「一貫性」
「議論」より「自動化」

実例:

C++プロジェクト:
  スタイルガイド: 50ページ
  コードレビュー: 30%がスタイルの指摘
  
Goプロジェクト:
  スタイルガイド: 「gofmt を使え」(1行)
  コードレビュー: 100%がロジックの議論
初心者へのメッセージ

1. 最初は違和感があって当然

C/Javaから来たら:
  開き括弧の位置が気になる
  
1-2週間で慣れる

2. gofmtを信頼する

エディタに統合
  ↓
保存時に自動整形
  ↓
スタイルを考える必要なし

3. ルールを覚える

重要なルール:
1. 開き括弧は同じ行
2. returnと値は同じ行
3. 構造体リテラルの最後にカンマ

4. Goらしさを受け入れる

「C++/Javaのスタイルで書きたい」
  ↓ ではなく
「Goのスタイルで書く」
  ↓
生産性向上
長期的な効果

Google の報告(Go導入後):

コードレビューの質的変化:
  
以前(C++):
  「この括弧の位置が...」
  「インデントが...」
  「スペースが...」
  
現在(Go):
  「このアルゴリズムは...」
  「エッジケースは...」
  「エラー処理は...」
  
本質的な議論にシフト!

オープンソースプロジェクト:

Docker、Kubernetes、など:
  数千人の貢献者
  ↓
すべて同じスタイル
  ↓
誰が書いても同じコード
  ↓
レビューが簡単
  ↓
高品質なコードベース
最終的な結論
波括弧あり、セミコロン自動挿入、
スタイル強制は:
  
制限ではなく、
意図的な設計判断
  ↓
一貫性と生産性のため
  ↓
長期的な利益が大きい

歴史的視点:

1970-2000年代:
  「プログラマーの自由」が重視
  各自が好きなスタイルで
  
2000-2020年代:
  大規模プロジェクトの増加
  チームの一貫性が重要に
  
現代:
  自動整形がスタンダード
  Goはこの流れのパイオニア

他の言語への影響:

Go(2009年):
  gofmt でスタイル統一
  
影響を受けた言語:
  Rust → rustfmt
  Python → black
  JavaScript → prettier
  
トレンド: 自動整形が標準化

この設計判断により、Goは「個人の好み」よりも「チーム全体の生産性」を優先しました。最初は制約に感じるかもしれませんが、これは数千、数万人規模のコードベースで威力を発揮します。

スタイルで悩まず、ビジネスロジックに集中できる。これがGoの最大の強みの1つです!

実際、多くの企業がGoを採用する理由の1つが「コードの一貫性が保証される」ことです。新しいメンバーがプロジェクトに参加しても、すぐにコードを理解できる。これは、スケールするチームにとって非常に重要なのです!

おわりに 

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

よっしー
よっしー

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

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

コメント

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