こんにちは。Goのアドベントカレンダー17日目の記事です。
普段はPHPをメインに開発していますが、せっかくの機会なので勉強中のGoで記事を投稿に挑戦してみました。
Goを勉強していてPHPや他の言語に比べて2つの「○○がない」という言語仕様に戸惑いました。それは
- Go言語には継承がない
- Go言語には例外処理がない
です。
この2つの仕組みは他の言語ではよく活用しており、可能なら使った方が良いと思って使い続けてきていたので、とても違和感がありました。
なぜGoにはこの2つの仕組みがないのでしょうか。少しでも言語理解を深めるために自分なりに調べてみました。
さきに結論
- 構造体の埋め込み、インターフェースを利用しよう
- errorインターフェースを利用しよう
Go言語には継承がない
そもそも他言語で継承が使われている理由はなぜなのか?
「継承」とは、クラス定義に共通している部分を別のクラスとしてまとめる仕組みのこと。継承を利用することで同じようなクラスを一から作り直す手間がなくなり、共通している部分はまとめて変更するが可能になります。なので、継承を上手く使うことで全体のコード量を減らすことが可能になり、共通の処理を変更する際も1箇所で済みます。
なぜGo言語には継承がないのか
https://go.dev/doc/faq#inheritance
Goの公式FAQ「Why does Go not have assertions?」の項目をざっくりと要約しますと、Goでは多重継承による複雑さを避けるためにインタフェースの活用を推奨している。
たしかに継承によって階層化されてしまったクラスは結合度が高くなってしまい、基底クラスの変更がサブクラスで予期しないエラーが発生させてしまうことがありますよね
Goとは直接関係ありませんが、継承じゃなくてインタフェース使えば問題ないよねと解説されている記事が分かりやすかったので、リンクを貼っておきます🙋♂️
Why extends is evil
ではどう実装するのか
Goのinterface定義の内容はメソッドリストのような形になります。
PHPのように実装(implements)と明示的に宣言する必要がなく、interfaceに定義されているメソッドをすべて実装していれば、interfaceを利用することができるようになっています。
PHPerの自分としては少し不思議な感覚です。。
package main
import "fmt"
// Interface を宣言
type Accessor interface {
GetName() string
SetName(string)
}
// Accessor を満たす実装
// 明示的な宣言は必要なく、実装と完全に分離している
type User struct {
name string
}
func (u *User) GetName() string {
return u.name
}
func (u *User) SetName(name string) {
u.name = name
}
func main() {
// Userのインスタンスを直接変更しても値渡しになってしまうのでポインタを使用
var user *User = &User{}
user.SetName("user")
fmt.Println(user.GetName())
//Accessor Interface を実装しているのでAccessor 型に代入可能
var acsr Accessor = &User{}
acsr.SetName("accessor")
fmt.Println(acsr.GetName())
}
Go言語には例外処理がない
そもそも他言語で例外処理が使われている理由はなぜなのか?
例外処理を実装することで得られるメリットは
- コードの可読性が上がる
- エラー内容を呼び出し側に伝えることができる
などが上げられると思います。
なぜGo言語には例外処理がないのか
https://go.dev/doc/faq#exceptions
Goの公式FAQ「Why is there no type inheritance?」の項目をざっくりと要約しますと、try-catch-finallyのように例外を制御構造に結びつけると、コードが複雑になるから、Goでは例外処理を用意していない。Goではerrorインタフェースを使うことを推奨しているとのことです。
ではどう実装するのか
継承の話でインターフェースについて触れましたが、errorはGoでよく利用されるインターフェースの1つです。異なる型に共通の性質を付与することでerrorを利用することができます。
処理中のエラー有無はerrがnilか否かで判定することになっています。
package main
import "fmt"
// 独自定義のエラーを表す型
type MyError struct {
Message string
ErrorCode int
}
// errorインターフェースのメソッドを実装
func (e *MyError) Error() string {
return e.Message
}
func RaiseError() error {
return &MyError{Message: "エラーが発生しました。", ErrorCode: 1234}
}
func main() {
err := RaiseError()
if err != nil {
//エラー時の処理(RaiseErrorで代入したのでこちらに処理が流れる)
fmt.Println(err)
fmt.Printf("%T\n", err)
}
}
まとめ
新しい言語を学習すると普段慣れ親しんでいる言語とのギャップから新しい気付きや学びが得られて面白いですね。Go公式FAQには今回の2つ以外にもなんでこの言語仕様になったのか解説がまとめられています。また新い学びがあれば記事にしていこうと思います。
参考記事
Go言語には継承がない
https://qiita.com/tenntenn/items/e04441a40aeb9c31dbaf
https://www.infoworld.com/article/2073649/why-extends-is-evil.html
Go言語には例外処理がない
https://qiita.com/nayuneko/items/3c0b3c0de9e8b27c9548
https://zenn.dev/nobonobo/articles/0b722c9c2b18d5