Goは「読みやすい言語」と言われますが、初学者の頃は読み解くのに時間がかかりました。
特に戸惑ったのがポインタです。
var u User
var p *User = &u
例えばTypeScriptでは、オブジェクトを扱う際に明示的なポインタ型を意識することはほとんどありません。オブジェクトは参照を通して扱われますが、Goのようにポインタ型を * で表すことはありません。そのため、Goの * や & が最初は余計な情報に見えました。
しかし、いくつか読み方を整理すると、Goのコードもかなり読みやすくなりました。
* は出てくる場所で判断する
まず、* は文脈ごとに機械的に読むのが分かりやすいです。
型の位置にある *User は「User へのポインタ」です。
var p *User
式の位置にある *p は「p が指している値を取得する」という意味です。ただし、p が nil の場合は、デリファレンスの時点で panic になります。
u := *p
そして、二項演算子として使われていれば、単なる掛け算です。
x := a * b
つまり、普段は次の程度に分類すれば十分です。
*T → Tへのポインタ型
*p → pのデリファレンス
a * b → 掛け算
ポインタを使いたくなる場面
そもそも、なぜ * が出てくるのかも整理しておきます。
Goのstructは値型なので、関数に渡すとコピーされます。そのため、次の関数は呼び出し元の値には影響しません。書き換えているのはコピーだからです。
func Rename(u User) {
u.Name = "new"
}
一方、ポインタを受け取れば、呼び出し元の User が書き換わります。
func Rename(u *User) {
u.Name = "new"
}
つまり * が付いているときは、「どこかにある値を、コピーではなく書き換えたい」という意図が込められていることが多いです。後で出てくるpointer receiverに * が付くのも、基本的には同じ理由です。
TypeScriptは「常にポインタ」なのか
こう考えると、TypeScriptは常にポインタを渡しているように見えます。オブジェクトに関しては、その理解でおおむね合っています。
function rename(u: User) {
u.name = "new"; // 呼び出し元にも反映される
}
Goで同じことをするには、*User を受け取る必要があります。
ただし、完全に同じではありません。TypeScriptで渡されるのは参照のコピーなので、変数そのものへの再代入は呼び出し元に伝わりません。
function replace(u: User) {
u = { name: "new" }; // 呼び出し元には反映されない
}
Goのポインタであれば、こちらも書けます。
func Replace(u *User) {
*u = User{Name: "new"} // 呼び出し元にも反映される
}
また、number や string などのプリミティブは参照ではありません。
まとめると、次のような対応になります。
TypeScriptのオブジェクト → Goの *T に近い
TypeScriptのプリミティブ → Goの T に近い
Goの T(structの値渡し) → TypeScriptでは { ...u } を渡すのが近い
TypeScriptでは選ぶ余地がないため書く必要がなく、Goでは選べるため * として書かれている、という違いだと考えると整理しやすいと思います。
ポインタには実際にアドレスが入っている
ここまで「指している」と書いてきましたが、ポインタの中身は具体的な数値です。つまり、メモリ上のアドレスが入っています。
u := User{Name: "a"}
p := &u
fmt.Printf("%p\n", p) // 実行ごとに変わるが 0x1b78016a2020 のような値
一方、nil はどこも指していない状態です。
var q *User
fmt.Printf("%p\n", q) // 0x0
このように考えると、nil のデリファレンスが panic になるのも自然に思えます。指し先のない場所を見に行こうとしているからです。
ただし、この数値を実際に扱う場面はほとんどありません。Goにはポインタ演算がなく、p + 1 のようには書けないためです。「アドレスが入っていて、それが指す先に実体がある」という程度に捉えておけば十分だと思います。
interface自体をポインタだと考えない
次のコードも最初は分かりにくく感じました。
p := &User{}
var x any = p
p の型は *User ですが、x の静的な型は any です。any は空のinterfaceの別名です。
ここでは、static typeとdynamic typeを分けると理解しやすくなります。
x
├─ static type: any
├─ dynamic type: *User
└─ value: p
つまり、x 自体の静的な型はポインタではありません。
x はinterface値で、その中に *User 型の値が入っています。
TypeScriptの interface と見た目は似ていますが、TypeScriptの interface 宣言は通常、実行時の型情報として残りません。一方、Goのinterface値は実行時にも具体的なdynamic typeを持つ点が異なります。
nil もこの考え方で理解できる
例えば次のコードがあります。
var p *User = nil
var x any = p
fmt.Println(p == nil) // true
fmt.Println(x == nil) // false
同じ nil と比較しているように見えますが、結果が異なります。
まず、x は概念的には次のような状態です。
dynamic type = *User
dynamic value = nil
この nil は、*User 型のnilポインタ値です。
ここで重要なのは、Goの nil 自体には型がないという点です。比較する相手によって、どの型の nil として扱われるかが決まります。
p == nil では、p は *User 型なので、nil も *User のnilポインタ値として比較されます。そのため true です。
一方、x == nil の x はinterface型なので、この nil は型も値も入っていないinterface値(interfaceのゼロ値)として扱われます。x には *User というdynamic typeが入っているため、両者は等しくならず false になります。
さらに、比較相手を明示的に *User 型のnilポインタ値にすると、
fmt.Println(x == (*User)(nil)) // true
となります。
この場合、比較相手の型は *User です。x のdynamic typeも *User で、格納されている値もnilポインタなので、両者は等しいと判定されます。
interfaceとinterfaceでない値を比較するとき、interfaceでない側は一度そのinterface型に変換されてから比べられます。変換後の姿を並べると、違いが見えます。
x : dynamic type = *User , dynamic value = nil ← これと比べる
nil : dynamic type = なし , dynamic value = なし ← 型が違うので false
(*User)(nil) : dynamic type = *User , dynamic value = nil ← 完全に同じなので true
なお、(*User)(nil) そのものはinterfaceではなく、*User 型のnilポインタ値です。比較のためにinterfaceへ変換された結果として、3行目の形になります。
x の中身が変わったわけではありません。比較相手の型が変わったことで、結果が変わっています。
x は「*User というラベルが付いた、中身がnilの箱」だと考えると整理しやすくなります。中身がnilであることと、箱そのものに何も入っていないことは別です。
pointer receiverとフィールドのポインタは別の話
次のようなコードにも戸惑いました。
type StripePayment struct {
client *stripe.Client
}
func (p *StripePayment) Charge(amount int) error {
// 課金処理
return nil
}
一見すると、ポインタが何重にも重なっているように見えます。
しかし、2つの * は別の話です。
client *stripe.Client
これは、
StripePaymentがstripe.Clientへのポインタをフィールドとして持つ
という意味です。
一方、
func (p *StripePayment) Charge(...)
は、
ChargeのreceiverがStripePaymentへのポインタである
という意味です。
図にすると単純です。
p
│
▼
StripePayment
│
└─ client
│
▼
stripe.Client
TypeScriptで、
class StripePayment {
constructor(private client: StripeClient) {}
}
と書く場合にも、概念的には似た参照関係があります。
TypeScriptでも client フィールドはオブジェクトへの参照を保持しますが、Goのポインタと同じものではありません。Goでは *stripe.Client のようにポインタ型であることが明示され、そのポインタ値は nil を取り得ます。この違いを踏まえたうえで、参照関係を読む手がかりの一つが * だと考えると理解しやすくなりました。
p.client はポインタのポインタではない
例えば、
func (p *StripePayment) Charge(amount int) error {
p.client.DoSomething() // 説明用の仮のメソッド
return nil
}
では、
p : *StripePayment
p.client : *stripe.Client
です。
p がポインタだからといって、p.client が **stripe.Client になるわけではありません。
Goでは p.client を概念的に、
(*p).client
として扱います。
まず p が指す StripePayment を見て、その中の client フィールドを取得しているだけです。
なお、p が nil の場合は、(*p).client の評価で panic になります。
この例のinterfaceは「契約」として読む
例えば、
type Payment interface {
Charge(amount int) error
}
があり、
payment.Charge(order.Total)
と書かれていたとします。
このとき、内部でSQLが実行されるのか、Stripe APIが呼ばれるのかまで考える必要はありません。
この例の命名からは、まずは、
paymentに金額を渡して課金処理を依頼し、結果としてerrorを受け取る
という設計意図として読めば十分です。実際に課金が行われることや、error がどのような条件で返るかまでは、interfaceの宣言だけでは保証されません。
必要になった場合だけ具体的な実装を確認します。
呼び出し箇所
↓
何をするのか
interface
↓
どのような契約なのか
具体的な実装
↓
どのように実現しているのか
この順番で読むと、細部に引っ張られにくくなります。
:= だけで型を推測しない
例えば、
user, err := repo.Find(userID)
だけを見て、user は *User だろうと判断することはできません。
Find が、
Find(id int) (*User, error)
なのか、
Find(id int) (User, error)
なのかを確認する必要があります。
Goは「一行を見るだけですべて分かる」言語ではありません。
ただし、宣言まで辿れば、
Find(id int) (*User, error)
のように型が明確に定義されています。
この点はGoの読みやすさの一つだと思います。
Goの読みやすさとは何か
TypeScriptも十分に型情報を持つ言語です。そのため、Goの利点を単純に「型が明示されているから」と説明するのは適切ではありません。
Goの場合は、
- 値とポインタを区別する
- interfaceを必要な操作の小さな契約として表現する
errorを戻り値として明示する- 言語機能や書き方の種類を比較的少なくする
といった方向で、コードの解釈に必要な推測を減らしているように感じます。
一方で、:= による型推論や、アドレス取得可能な値に対して pointer receiver のメソッドを呼び出す際の暗黙的なアドレス取得など、すべてが表記上明示されているわけではありません。
そのため、Goを読む際には、
「内部では本当は何が起きているのか」
を常に考えるのではなく、
*T
→ Tへのポインタ
interface
→ 必要な操作の契約(この例のような基本的なinterface)
struct内の *T
→ Tへのポインタを持つフィールド
pointer receiver
→ `*T` をreceiverに持つメソッド
くらいの粒度で読むのがよさそうです。
TypeScriptでオブジェクト同士の参照を毎回メモリ構造まで展開しないのと同じです。
Goの「読みやすさ」は、一行一行が他の言語より簡潔という意味ではなく、必要になった情報を型やinterfaceの宣言から比較的素直に確認できることにあるのだと思います。
この見方に変えてから、Goの * も「読みにくい記号」ではなく、「必要な情報を明示する記号」としてスッキリと読めるようになりました。