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

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

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

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

スポンサーリンク

背景

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

Changes from C

なぜ宣言が逆なのか?

C言語に慣れている場合のみ、逆に見えます。C言語では、変数はその型を示す式のように宣言されるという概念があります。これは良いアイデアですが、型と式の文法があまりうまく混ざらず、結果が混乱を招く可能性があります。関数ポインタを考えてみてください。Goは式と型の構文をほぼ分離しており、それが物事を単純化しています(ポインタに接頭辞*を使用するのは、規則を証明する例外です)。C言語では、宣言

int* a, b;

aをポインタとして宣言しますが、bはポインタではありません。Goでは

var a, b *int

は両方をポインタとして宣言します。これはより明確で規則的です。また、:=短縮宣言形式は、完全な変数宣言が:=と同じ順序を示すべきだと主張しているため、

var a uint64 = 1

a := uint64(1)

と同じ効果を持ちます。

また、パースも、式の文法だけでなく型のための独自の文法を持つことで簡素化されます。funcchanなどのキーワードが物事を明確に保ちます。

詳細については、Goの宣言構文に関する記事を参照してください。

解説

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

C言語から来た人が最初に感じる違和感:

// C言語: 型が先、名前が後
int x;
int *p;
// Go: 名前が先、型が後
var x int
var p *int

「なぜ逆なの?」という疑問に、より深く答えます。

基本的な用語
  • 宣言 (declaration): 変数や関数を定義すること
  • 式 (expression): 値を生成するコード片
  • 型文法 (type grammar): 型を表現するルール
  • 式文法 (expression grammar): 式を表現するルール
  • 接頭辞 (prefix): 前に付ける記号(*p*)
  • 接尾辞 (suffix): 後に付ける記号(p[][])
C言語の「良いアイデア」とその問題
C言語の設計思想: 「使い方で宣言する」

C言語の哲学:

// 変数を「使うときの形」で宣言する
int x;      // xを使うと int が得られる
int *p;     // *p を使うと int が得られる
int a[10];  // a[i] を使うと int が得られる
int *f();   // *f() を使うと int が得られる

一見良さそう: 宣言と使用法が対応している!

問題1: 複数変数の宣言

C言語の罠:

int* a, b;  // これは何を宣言している?

多くの人が思うこと:

// 「int*」が型だから、aとbは両方ポインタ?

実際:

int* a, b;
// ↓ コンパイラの解釈
int *a;     // a は int へのポインタ
int b;      // b は int (ポインタじゃない!)

なぜこうなる?

C言語の設計では:

int* a, b;
// は本当はこう書かれるべき
int *a, b;
// * は型の一部ではなく、変数名の一部!

混乱の原因:

見た目: int* が型に見える
実際: * は各変数に付ける修飾子

視覚的誤解を招く!

Goの解決:

var a, b *int  // 明確! 両方ポインタ

*int が型として扱われるので、ab は両方とも *int 型です。

問題2: 複雑な宣言が読めない

C言語の複雑な宣言:

int *(*fp)(int, int);

これは何? 解読が必要:

右から左、左から右へジグザグに読む:

fp は...
(*fp) → ポインタ
(*fp)(...) → 関数へのポインタ
(*fp)(int, int) → 2つのintを受け取る関数へのポインタ
int *(*fp)(int, int) → intへのポインタを返す関数へのポインタ

結論: 「2つのintを受け取り、intへのポインタを返す関数」へのポインタ

もっと複雑な例:

void (*signal(int, void (*)(int)))(int);

これは何? (有名なsignal関数)

signal は...
signal(int, ...) → int を受け取る関数
void (*)(int) → int を受け取り void を返す関数へのポインタ
signal(int, void (*)(int)) → 2つの引数を受け取る関数
void (*...)(int) → 返り値は関数へのポインタ

結論: 読むのに5分かかる...

Goの解決:

var fp func(int, int) *int
// 読み方: 左から右へ自然に
// fp は「2つのintを受け取り、intへのポインタを返す関数」
var signal func(int, func(int)) func(int)
// 左から右へ読めば理解できる!
問題3: 型と式の文法が混ざる

C言語:

// これは式? 宣言?
x * y

コンテキストに依存:

// ケース1: xが変数
int x = 5, y = 3;
x * y;  // 式: 掛け算

// ケース2: xが型名
typedef struct {} x;
x * y;  // 宣言: yはxへのポインタ

パーサーはシンボルテーブルを見ないと判断できない!

Goの解決:

// 掛け算
result := x * y

// ポインタ宣言
var y *x

// 見れば明確! シンボルテーブル不要
Goの設計: 式と型の分離
明確な分離

Go:

式の世界: 値を計算
  x + y
  f(x)
  *p

型の世界: 型を記述
  int
  *int
  func(int) int
  
明確に分離されている!

C言語:

式と型が混在:
  *p  → 式でも型でも
  a[10] → 式でも型でも
  
混乱の元!
キーワードが明確にする

Go:

func add(x, y int) int  // "func" で関数型と明確
chan int                // "chan" でチャネル型と明確
map[string]int          // "map" でマップ型と明確
interface{}             // "interface" でインターフェース型と明確

これらのキーワードは型の世界だけで使われる:

  • 式には現れない
  • 型の開始を明確に示す
ポインタの * は例外
唯一の例外

Go:

var p *int    // 型: *int
x := *p       // 式: ポインタの参照外し

// * が両方で使われる(C言語から継承)

なぜ例外を許した?

理由1: C言語との概念的な継続性
  プログラマーにとって馴染みがある
  
理由2: ポインタは基本的な概念
  新しい記号を導入するほどではない
  
理由3: それでも十分明確
  var p *int  → 型の文脈
  x := *p     → 式の文脈
  混乱しにくい

記事で言う「例外が規則を証明する」とは:

唯一の例外があるということは、
他のすべては規則通りという証拠
:= との一貫性
短縮宣言形式

Go の :=:

a := 1              // int
b := "hello"        // string
c := uint64(100)    // uint64

順序:

名前 := 値
完全な宣言形式との一貫性

Goの設計判断:

// 短縮形
a := uint64(1)

// 完全形も同じ順序であるべき
var a uint64 = 1

// 順序が一貫している!
名前 型 = 値

C言語だとこうなる:

// C言語には := がない
// もしあったら...
uint64_t a := 1;  // 型が先

// でも完全形は
uint64_t a = 1;   // 同じ順序

// 一貫性はある(型が先)

Goの選択:

:= の自然さ(名前が先)
  ↓
完全形も名前が先にすべき
  ↓
var 名前 型 = 値
具体例で比較
例1: 基本的な変数

C言語:

int x = 10;
int y = 20;
int z;

Go:

var x int = 10
var y int = 20
var z int

// または
x := 10
y := 20
var z int

読み方:

C: 「int型の x は 10」
Go: 「x は int型で 10」

Goの方が自然な語順!
例2: ポインタ

C言語:

int *p1, *p2;     // 両方ポインタ(* を2回書く)
int *p3, q;       // p3はポインタ、qは違う(罠!)

Go:

var p1, p2 *int   // 両方ポインタ(明確!)
var p3 *int       // p3はポインタ
var q int         // qはint
例3: 配列

C言語:

int a[10];              // 10個のint
int *pa[10];            // ポインタ10個
int (*ap)[10];          // 配列へのポインタ

Go:

var a [10]int           // 10個のint
var pa [10]*int         // ポインタ10個
var ap *[10]int         // 配列へのポインタ

// 左から右へ読める!
例4: 関数

C言語:

int add(int x, int y);              // 普通の関数
int (*fp)(int, int);                // 関数ポインタ
int (*(*foo)(int))(int);            // 読めない...

Go:

func add(x, y int) int              // 普通の関数
var fp func(int, int) int           // 関数型の変数

// 関数を返す関数
var foo func(int) func(int) int
// 左から右: int を受け取り、(int を受け取り int を返す関数)を返す
例5: 構造体へのポインタの配列

C言語:

struct Employee {
    char *name;
    int age;
};

struct Employee *workers[100];
// 読み方: workers は... 100個の... ポインタの配列... Employee構造体への

Go:

type Employee struct {
    name string
    age  int
}

var workers [100]*Employee
// 読み方: workers は 100個の Employeeポインタの配列
// 左から右へ自然に読める!
パースの簡素化
型文法と式文法の分離

Goのパーサー:

// 型の開始を示すキーワード
func      // 関数型
chan      // チャネル型
map       // マップ型
struct    // 構造体型
interface // インターフェース型

// または型名
int, string, MyType, etc.

// これらを見れば「型だ!」とすぐわかる

例:

var handler func(http.ResponseWriter, *http.Request)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
          型の文脈(func キーワードで明確)

handler(w, r)
^^^^^^^^^^^^^
式の文脈
C言語のパーサーの苦労

C言語:

x * y;

// パーサーの思考:
// 1. x を見つける
// 2. シンボルテーブルを調べる
//    - xが型名? → ポインタ宣言
//    - xが変数? → 掛け算
// 3. やっと判断

// 複雑!

Go:

x * y        // これは式(式の文脈)
var y *x     // これは宣言(var キーワードで明確)

// シンボルテーブル不要!
実用的な例
例1: HTTPハンドラー

C言語風(もしあったら):

void (*handler)(Response*, Request*);
// 読みにくい...

Go:

var handler func(*Response, *Request)
// または
handler := func(w *Response, r *Request) {
    // 処理
}

// 読みやすい!
例2: コールバック

C言語:

void register_callback(void (*callback)(int));
// コールバックは int を受け取る関数へのポインタ

Go:

func registerCallback(callback func(int))
// callback は int を受け取る関数

// 使用
registerCallback(func(x int) {
    fmt.Println(x)
})
例3: 複数の返り値

C言語:

// 複数の返り値は不可能
// ポインタを使って回避
void divide(int a, int b, int *quotient, int *remainder) {
    *quotient = a / b;
    *remainder = a % b;
}

// 使用
int q, r;
divide(10, 3, &q, &r);

Go:

// 複数の返り値が自然
func divide(a, b int) (quotient, remainder int) {
    quotient = a / b
    remainder = a % b
    return
}

// 使用
q, r := divide(10, 3)
なぜ「逆」に見えるか?
C言語の視点
C言語脳:
  型が主役 → 型が先に来るべき
  int x   → 正しい順序
  var x int → 逆に見える
自然言語の視点
英語:
  "x is an integer"
  主語(x) → 述語(is) → 補語(integer)
  
日本語:
  "xは整数です"
  主語(x) → 述語(整数)
  
Go:
  var x int
  名前(x) → 型(int)
  
自然言語と同じ順序!
Goの一貫性
すべての宣言が同じパターン

変数:

var name type

定数:

const name type = value

関数:

func name(params) returnType

型定義:

type name underlyingType

すべて「名前 型」の順序!

C言語の不一貫性

変数:

int x;        // 型 名前

typedef:

typedef int MyInt;    // typedef 型 新しい名前
typedef struct {} T;  // typedef struct 型 新しい名前

関数:

int add(int x) { }    // 返り値型 名前(引数)

順序がバラバラ!

まとめ
C言語の問題点
  1. 「使い方で宣言」は一見良いが:
    • 複雑な宣言が読めない
    • int* a, b の罠
    • 型と式が混在
  2. パーサーが困る:
    • シンボルテーブル必須
    • 曖昧な構文
    • ツールが作りにくい
  3. 一貫性がない:
    • 宣言ごとに順序が異なる
Goの解決
  1. 名前が先、型が後: var x int func f() int type T struct{}
  2. 型と式を分離:
    • キーワードで明確化
    • シンボルテーブル不要
    • パースが簡単
  3. 完全な一貫性:
    • すべて「名前 型」
    • := とも一貫
「逆」ではなく「改善」
C言語: 歴史的な選択
  ↓
問題点が蓄積
  ↓
Go: 現代的な再設計
  ↓
より読みやすく、解析しやすい
実用的な影響
観点C言語Go
単純な宣言⭐⭐⭐⭐⭐⭐⭐⭐⭐
複雑な宣言⭐⭐⭐⭐⭐
複数変数宣言⭐⭐ (罠あり)⭐⭐⭐⭐⭐
一貫性⭐⭐⭐⭐⭐⭐⭐
パースの容易さ⭐⭐⭐⭐⭐⭐⭐
あなたへのアドバイス
  1. 「逆」ではなく「違う」と考える C言語 = 正解 Go = 逆 ではなく C言語 = 1つのアプローチ Go = 改善されたアプローチ
  2. 左から右に読む習慣をつける var users []*User // users は、Userポインタのスライス var handler func() error // handler は、errorを返す関数
  3. 一貫性に注目 var x int = 10 x := 10 // 同じ順序!
  4. 複雑な型も読みやすい // C言語なら読めない複雑さも var f func(int) func(int) int // Goなら左から右へ読めば理解できる!
最終的な結論

「逆」ではありません。これは:

  • より読みやすい
  • より一貫性がある
  • よりツールフレンドリー
  • より現代的な設計

の結果です!

C言語に慣れていると最初は違和感がありますが、数週間使えば「Goの方が自然!」と感じるようになります。そして、C言語のコードを見たときに「あれ、読みにくい…」と思うようになります!

この設計判断は、長期的な読みやすさと保守性を優先した、非常に賢明な選択なのです!

おわりに 

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

よっしー
よっしー

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

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

コメント

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