← melang

Go

melangの各noteで他言語と比較した、Goの設計をまとめたページ。ここに書かれている内容は、各noteの比較表から自動で集めたものです。

型

型推論はあるが、明示する場面も多い。

var age int = 20
var age int = 20
name := "Alice" // := による型推論

`:=` を使った短い変数宣言では型推論が効く一方、関数シグネチャなどでは型を明示するのが基本。

型注釈の位置

型は名前の後ろに置くpostfix方式。シグネチャに推論はない。

var age int = 20
var age int = 20
name := "Alice" // :=で推論

func greet(name string) string { // 引数・戻り値は常に明示
    return "Hello, " + name
}

`var name Type`という一貫したpostfix構文を採用しており、Cの`Type name`とは逆順。`:=`によるローカルの型推論はあるが、関数シグネチャでは常に型を書く必要があり、公開APIの可読性を型推論より優先する設計。

型変換

型名を関数のように使って変換し、パースは(値, error)で返す。

y := int64(x)
var x int32 = 10
y := int64(x) // 型名を使った明示的な変換

var big int64 = 3_000_000_000
z := int32(big) // 黙って切り捨てられる

n, err := strconv.Atoi("42")
if err != nil {
    // 失敗
}

数値型間の変換は`T(x)`の形で、縮小変換でも失敗を検出せず値を切り捨てる。文字列からのパースは`strconv`パッケージの関数が`(値, error)`を返し、呼び出し側が`err`を確認する。失敗を返り値で表す点はmelangの`Result`に近いが、確認を忘れてもコンパイルは通る。

暗黙の型変換

異なる数値型間の変換は常に明示が必要。

var x int32 = 10
var x int32 = 10
var y int64 = x        // コンパイルエラー
var y int64 = int64(x) // OK

int32からint64のような安全な拡大変換であっても暗黙には行われず、必ず`T(x)`という形の明示的な変換を書く。

Enum / Sum Type

組み込みのenumがなく、named type + constで代替する。

type Status int
const ( Pending Status = iota; Running; Completed )
type Status int

const (
    Pending Status = iota
    Running
    Completed
)

switch s {
case Pending:
    // ...
case Running:
    // ...
} // Completedの分岐を書き忘れてもコンパイルは通る

言語組み込みのenumがなく、named type + `const` / `iota`で整数定数を列挙する方式を取る。Go公式Wikiも「Go does not support enums」と明記している。`switch`に網羅性チェックはなく、variant追加時の対応漏れをコンパイル時に検出できない。

値の不変性

struct・slice・mapは変更でき、不変にする仕組みはない。

const n = 1 / var x = 1
const limit = 10 // 数値・文字列などのコンパイル時定数だけ

type User struct{ Name string; Tags []string }
u := User{"alice", []string{"a"}}
u = User{"bob", nil}       // 再代入
u.Name = "bob"             // フィールドの変更
u.Tags = append(u.Tags, "admin")
u.Tags[0] = "x"            // sliceの要素の変更

`const`は数値・文字列・真偽値などのコンパイル時定数にしか使えず、struct・slice・mapを不変にする手段はない。文字列は不変だが、他のデータは変更できる。sliceは同じ配列を共有するため、別の変数から見た値が変わることがある。

名前付き引数

名前付き引数はなく、オプション値は構造体やFunctional Optionsで表現する。

func Foo(width, height int)
func Foo(width, height int) {}
Foo(10, 20) // 引数名は呼び出し側に現れない

type FooOptions struct { Width, Height int }
func Foo2(opts FooOptions) {}
Foo2(FooOptions{Width: 10, Height: 20}) // フィールド名で意味を明示

名前付き引数はなく、オプションが多い関数では構造体を渡すか、Functional Optionsパターンで意味を持たせるのが一般的。

関数の引数

すべて値渡し。参照的に扱うにはポインタを明示的に渡す。

func f(x int) / func f(p *int)
func a(n int) { n = 1 }   // 呼び出し側には影響しない
func b(p *int) { *p = 1 } // ポインタ経由で書き換える

n := 0
a(n)
b(&n) // 呼び出し側に & が付く

// スライスやmapは、ヘッダ自体がコピーされるが中身は共有される

引数はすべて値渡しで、参照的に扱いたければポインタを渡す。ポインタの受け渡しは呼び出し側にも`&`が付くため、書き換えられる可能性が読み取れる。ただしスライスやmapは値渡しでも中身を共有するため、渡し方が型によって分かりにくくなる。

if 文/式

`if`はstatementで、三項演算子もない。`else`を避けたearly returnが慣習になっている。

if cond { ... } else { ... }
// 値の分岐はstatementで書く
var x int
if cond {
    x = 1
} else {
    x = 2
}

// 慣習: returnした後にelseを書かない
if err != nil {
    return err
}
process()

`if`は式ではなく、三項演算子もない。値の分岐は変数を宣言してからstatementで代入する。一方で、`return`で終わるbranchのあとに`else`を書かないという慣習があり、linterも指摘する。melangが目指す「手続きはearly return」に近い文化を持つが、言語としては強制していない。

I/O の明示

すべての関数が同じ形で書け、I/Oも区別されない。

func ReadFile(path string) (string, error)
func ReadFile(path string) (string, error) {
    b, err := os.ReadFile(path)
    return string(b), err
}

func Double(x int) int {
    ReadFile("x") // 戻り値を無視して呼べる
    return x * 2
}

`(T, error)`という戻り値の形はGoの慣用句だが、これは失敗しうることを示す規約であり、I/Oかどうかは示さない。goroutineの起動やチャネル操作も、通常の関数呼び出しと同じ構文で書ける。

言語名

一般名詞で検索性が低く、`Golang`という別名が定着した。

`Go`は日常英単語であり、検索エンジンで言語のドキュメントに絞り込みにくい。この問題を避けるため、コミュニティは`golang`という別名(ドメインも`golang.org`)を早期から用意し、事実上の第二の名前として定着させた。一般名詞をそのまま言語名にすると、名前の一意性を別の手段(別名・ドメイン)で補う必要が生じる例。