ADVENT CALENDAR 2019
Golangなんもわからん俺がGolangを解説する
By nyannyan
これは AmusementCreators 2019 アドベントカレンダー その1 の1日目の記事です。 その2もあります。
Go言語なんもわからんけど解説する
AmusementCreators advent calendar 7日目担当にゃんにゃんです。
最近はGo言語書いてるのでGo言語について記事を書きました。
タイトル通り、Go言語わかんないけど頑張ります。
対象者
あんまりわからんって人でもそれなりにわかるようにだいぶ丁寧に書きましたが継承、例外処理、Genericsあたりがわかると容易に理解できると思います。
Goのメソッド
Goはメソッドちょっと特殊なのでそのへん解説していきます。 まず、普通のメソッドはこんな感じです。
// 引数の二乗を返します
func Square(val int) int{
return val * val
}
// 戻り値はタプル(複数戻り値)にできるよ。
func power(val int) (int, int){
// タプルの利用例が見つからないのでなんとなく100を返します。
return val * val, 100
}
なんら普通の言語のメソッドとは変わりありませんね。
もう一つ、Goにはレシーバと呼ばれるものを持つメソッドがあります。
これは構造体にメソッドを付与する、そんな感じの書き方です
// 構造体定義
type Car struct{
Message string
}
// メソッド名の前にあるのがレシーバです。今回は*を付けてポインタレシーバにします(後述)
// 呼び出しはcar.ShowSpeed()の雰囲気で呼びます。
func (c *Car) ShowSpeed(){
fmt.Println(c.Message)
}
こんな感じです。(適当)
ぶっちゃけ第一引数にレシーバの型を置く以下の書き方とそんなに変わらないです。
// 先ほどはcar.ShowSpeed()でよびだす。
// これはShowSpeed(car)で呼び出す。
func ShowSpeed(c *Car){
// 処理
}
ポインタレシーバ
https://go-tour-jp.appspot.com/methods/4
↑こっちのほうがわかりやすいかもね。一応説明しますが。
*
を付けるとポインタレシーバと呼ばれるものになります。
普通のレシーバだと構造体をコピーして関数内で利用しますが、ポインタレシーバは構造体をコピーせずに使います。
つまり、ポインタレシーバで渡さないと構造体のフィールド値の変更がされません。
わからない人向けに次のコードでなにが違うかを説明します。
package main
import (
"fmt"
)
type Car struct {
Message string
}
func (c *Car) DeleteMessage1() {
c.Message = "no message"
}
func (c Car) DeleteMessage2() {
c.Message = "no message"
}
func main() {
car1 := Car{
Message: "nyaaan",
}
car2 := Car{
Message: "nyaaan",
}
car1.DeleteMessage1()
car2.DeleteMessage2()
fmt.Println(car1.Message) // -> no message
fmt.Println(car2.Message) // -> nyaaan
}
実行環境 https://play.golang.org/p/m-H7G7eQVKa
ゴミみたいな関数名は許してください
DeleteMessage2
ではポインタレシーバではないので関数の中で使われているcという変数はコピーされたものです。なのでそのコピーしたものをどんなにぼこぼこにしようと呼び出したcar2
には影響しません。
もう一つ、ここでcar1
はポインタ変数として宣言してないじゃん。みたいな意見があると思います。そうです。いや本当によく気づきましたね。実際&を付けることで以下のようにポインタ変数として宣言できます
car1 := &Car{
Message: "nyaaan",
}
挙動は変わりません。
https://go-tour-jp.appspot.com/methods/6
面倒なのでここ見てください。
勝手にポインタ型に型変換されて利用されます。
car1.DeleteMessage1()
が
(&car1).DeleteMessage1()
みたいに勝手になります。やったぜ?
自分はポインタいじいじする言語そんな書かないのでわからないですが、個人的にちょっと気持ち悪い。
Go言語継承ねぇから!
Goの基礎はまあこの辺にして、そろそろ継承周りの話をします。
Goには継承がないです。やったぜ。
公式のFAQのサイトがあったので載せておきます(英語だけど許して)
https://golang.org/doc/faq#inheritance
この辺の継承を採用しなかった理由を読めばなんとなくわかります。
TypeのWhy is there no type inheritance?に理由が書いてあります。
雰囲気で訳すと、多重継承とかその辺の煩雑さが解決されたぜみたいな感じですかね。知らんけど。
クラスねぇから!
そもそもGoにはクラスといった概念がありません。インタフェースと構造体しかないです。シンプルですね。
ただ、Goではjavaのimplementsにあたるキーワードはありません。ダックタイピングとかいうやつらしいです。
例えばインタフェースCar
がRun()
とStop()
というメソッドを持つ場合、このインタフェースCar
の条件を満たしている判定はRun()
とStop()
を持つか否かです。
コードを書くとこんな感じです。
package main
import (
"fmt"
)
// Carのインタフェースの定義はRun()とStop()を持つ
type Car interface{
Run()
Stop()
}
// Carを継承したい構造体
type RedCar struct {
}
// RedCar.Run()を定義
func (r *RedCar) Run(){
fmt.Println("run")
}
// RedCar.Stop()を定義
func (r *RedCar) Stop(){
fmt.Println("stop")
}
func main() {
var car Car
// CarのインタフェースにRedCarを代入してみる
car = &RedCar{}
car.Run() // ->run
car.Stop() // ->stop
}
実行環境も
https://play.golang.org/p/dGxV0t3hJVf
RedCar
はRun()
とStop()
を持っているからCarのインタフェースに代入しても怒られなかった!やったぜ!
ちなみに試しに上の実行環境で
func (r *RedCar) Run(){
fmt.Println("run")
}
を消してみるとRun()
ねーじゃん!って感じのエラーが出ることが確認できます。
インタフェースは分かったが、あるフィールド値を持っていることを保証したい
例えば継承の利点としてすべての子オブジェクトが親の持つフィールド値を継承してもつというものがあります。(語彙力)(private変数は無理だけど)
これをGoで実現するためには構造体に構造体を埋め込みます。
また例のごとくサンプルコードを。
package main
import (
"fmt"
)
type Car struct {
Velocity int
}
type ExpensiveCar struct {
// Carの構造体を埋め込む
Car
Price int
}
func (c *Car) ShowVel() {
fmt.Println(c.Velocity)
}
func main() {
car := Car{
Velocity: 1,
}
expensiveCar := ExpensiveCar{
Car: Car{
Velocity: 3,
},
Price: 1000,
}
car.ShowVel() // ->1
// expensiveCar.ShowVel()は定義してないが、expensiveCar.Car.ShowVel()が実行される
expensiveCar.ShowVel() // ->3
// 上と同値
expensiveCar.Car.ShowVel() // ->3
// expensiveCar.Velocityは定義してないが、expensive.Car.Velocityを表す
fmt.Println(expensiveCar.Velocity) // ->3
fmt.Println(expensiveCar.Car.Velocity) // ->3
}
expensiveCar.ShowVel()
関数定義がなくても勝手にexpensiveCar.Car.ShowVel()
が呼ばれるんです。
い や わ か り に く い わ
まあ、ともかくこんな感じで継承ライクな雰囲気を醸し出すことができます。
オブジェクト指向で親のメソッド勝手に受け継ぐみたいな雰囲気ですが、
い や 初 見 殺 し
ではここで、こんな感じのコードを入れるとどうなるでしょうか。
func (e *ExpensiveCar) ShowVel() {
fmt.Println("高級車だぜ!!!")
}
見た通りExpensiveCar.ShowVel
が定義できます。
こうするとExpensiveCar.ShowVel
とExpensiveCar.Car.ShowVel
は別の関数となります。
先ほどの環境に入れると
expensiveCar.ShowVel() // ->高級車だぜ!!!
expensiveCar.Car.ShowVel() // ->3
こんな感じにExpensiveCar.ShowVel()
の処理が上書きされます。
さっきさんざんわかりにくいとか言ったけど、オブジェクト指向のメソッドオーバーライドのノリですね。(ほんまか)完全に理解した。(ほんまか)
また、もう予測できると思いますが、フィールドも同じようになります。
type Car struct {
Velocity int
}
type ExpensiveCar struct {
// Carの構造体を埋め込む
Car
Velocity int
}
こうしてみましょう。こうすると、先ほどまでExpensive.Velocity
はExpensive.Car.Velocity
を表すものでしたが、別のものとして判断されます。
この辺りをまとめたコードを以下に載せておきます。
package main
import (
"fmt"
)
type Car struct {
Velocity int
}
type ExpensiveCar struct {
// Carの構造体を埋め込む
Car
Velocity int
}
func (c *Car) ShowVel() {
fmt.Println(c.Velocity)
}
func(e *ExpensiveCar) ShowVel(){
fmt.Println("高級車だぜ!!!")
}
func main() {
car := Car{
Velocity: 1,
}
expensiveCar := ExpensiveCar{
Car: Car{
Velocity: 3,
},
Velocity: 10000,
}
car.ShowVel() // ->1
// expensiveCar.ShowVel()とexpensiveCar.Car.ShowVel()は異なる
expensiveCar.ShowVel() // ->高級車だぜ!!!
expensiveCar.Car.ShowVel() // ->3
// expensiveCar.Velocityとexpensive.Car.Velocityは異なる
fmt.Println(expensiveCar.Velocity) // ->10000
fmt.Println(expensiveCar.Car.Velocity) // ->3
}
https://play.golang.org/p/vbZYLGp_xDy
構造体に名前を付ける場合
構造体の埋め込みには名前を付けることができます。それだけならいいのですが、名前を付けると挙動が変わります。
い や こ れ が 本 当 に 挙 動 か わ る の
例として今度はExpensiveCar
にCar
構造体をMyCarという名前で埋め込んでみました。
package main
import (
"fmt"
)
type Car struct {
Velocity int
}
type ExpensiveCar struct {
// 埋め込みに名前を付けました
MyCar Car
Price int
}
func (c *Car) ShowVel() {
fmt.Println(c.Velocity)
}
func main() {
car := Car{
Velocity: 1,
}
expensiveCar := ExpensiveCar{
MyCar: Car{
Velocity: 3,
},
Price: 1000,
}
car.ShowVel()
expensiveCar.MyCar.ShowVel()
}
https://play.golang.org/p/x5vF4RZyduk
この状態で先ほどのように
ExpensiveCar.ShowVel()
を使おうとするとそんなものねーよ!って怒られます。
名前をつけると埋め込んだもののメソッドとかフィールドを名前を指定して明示的に呼ばなければならないみたいです。
つまり名前をつけなかった時のようにExpensiveCar.MyCar.Velocity
をExpensiveCar.Velocity
で使えません。
(は、わかりにくくね。)
とりあえずこの辺りめちゃくちゃややこしいです。
インタフェース、構造体、継承まとめ
構造体の埋め込みとインタフェースにより継承っぽい雰囲気は醸し出せます。
名前を付けない場合は埋め込んだ構造体のメソッドなどを埋め込まれた側が持つようにふるまうのでこちらのほうがいわゆるオブジェクト指向の継承っぽさはありますね。
名前を付ける場合はまあなんだろう。言葉で説明するのは難しいのですが、役割が分離するというか、本当にただ単純に埋め込まれたのを持ってるという意味になりますね。
try-catch ねぇから!
Goには try-catch がありません。やったぜ!
じゃあどうするか。
なんのためにタプルがあるかをかんがえれば余裕ですね
例
// 特に定義しないけどfは(int , error)を返す関数とする
i,err := f()
if err !=nil
{
// エラー処理
}
// 書き方をかえてみた。
if i,err := f();err != nil{
// エラー処理
}
// こんな感じに無限にエラーチェックします。
関係ないけど地味に二個目のエラー処理の書き方を変えてみました。
if文の条件式の前に変数を宣言できるっていう謎の機能があることをついでに紹介してみたかったので。
Goは無限にエラーチェックするよね。
まぁいいんですけどね。
try-catchはないけどdeferはいいぞ
あとdefer
という予約語があります。これをつけると関数の最後にdefer
で指定した物が実行されます。
使いどころとして例えばhttpレスポンスを受け取ったときにBody.Close()
を必ずしなければなりません。
下に例としてすごいhttp通信しそうなすごい関数を書きました。(えっへん)
func SugoiHttpKannsuu(){
// httpコードを雰囲気で適当に書きます。
url := "その辺のurl"
// error処理とかしらない(面倒)
resp, err := http.Get(url)
defer resp.Body.Close()
// 特に定義しないけどfは(int , error)を返す関数
i,err := f()
if err !=nil
{
// エラー処理
// 本当はresp.Body.Close()をしないといけないがdeferにより実行される
}
i,err := f()
if err !=nil
{
// エラー処理
// 本当はresp.Body.Close()をしないといけないがdeferにより実行される
}
}
普通に書こうとする場合はエラー処理ごとにresp.Body.Close()
を書かないといけないのですが、
こうすればエラーで終ろうと普通に終ろうと必ずresp.Body.Close()
が呼ばれます。
なにかしらのClose処理のし忘れなども減るので結構いい感じにエラー処理ができます。
Genericsねぇから!
GoにはGenericsがありません。やったぜ。
ぶっちゃけGenericsはどうすればいいのか自分もよくわかってないです。
一応ググって付け焼刃的なそりゅーしょんがいくつかあったのでそのあたりを紹介します。
interface{}
というなんでも入る型があります。
意味的にはinterface{}
はメソッドを持たないインタフェースなので、すべての構造体はこのインタフェース条件みたしているから代入できるよねといった感じです。go Genericsでググるとこれ使えみたいなのが多いです。
個人的には、先ほどの継承風の方法をとって自分でインタフェースつくってってやったほうがいいと思います。interface{}
型はどんなメソッドを持っているとかどんなフィールドを持ってるかとか全くわからないので。(自分はちゃんと書いてないのでできるかどうか知らないですがたぶんできるでしょ。)
大昔にinterface{}
を用いろって言われた時の自分が書いたgenerics風のコードをはっておきます
脳筋処理注意
// Goの型スイッチです。
func generic(obj interface{}) (err error) {
switch obj.(type) {
case int:
// intの時の処理
return nil
case float64:
// floatの時の処理
return nil
// こんな感じのcase type を無限に書きました
default:
return errors.New("invalid type")
}
}
↑こんなわけわからんコード書かないためにもちゃんとGoお勉強しましょう。
まとめ
今回は何を書けばいいのかわからず、だいぶざっくばらんな説明になってしまいました。
Goはシンプルな言語設計にした代わりになんかいろいろ機能を削ってます。
これは使い手の設計でなんとかなると思います。自分は脳死設計なのでちょこちょこ躓くことが多いですが。
また、今回は説明しませんでしたが、Goは並列の機能のサポートがいい感じです。ゴルーチンってやつです。
並列処理したい関数の頭にgo
キーワードをつけるだけです。
簡単に並列処理ができるので暇な人はちょっと書いてみたらいかがですかね。
https://go-tour-jp.appspot.com/welcome/1
自分がGoを練習したサイトです。実際にコードを実行したりするので短時間で深い内容を学べます。
ゴルーチンも学べるのでおすすめです。
ではでは。だいぶ長くなりましたが、読んでいただきありがとうございました。
SHARE THIS POST
Tweet