昔の自分に送るGolangの基礎的なこと(型確認・Slice・method)

Takahito Yamada
Feb 4, 2018 · 10 min read

# Intro

私はプログラミング言語について右も左もわからない時にGolangを始めました。それから一年間ぐらいGolangを書き、その後pythonをひたすら描いたり、アルゴリズムについて学んだりしました。

Golangのことを完全に忘れていた頃、久しぶりにTour of Goをやってみると、前まで理解できなかったことがするすると理解できるようになったのです(海外の記事みたいな見出しになったw)。

そこで、この記事では私がGolangを始めた時に知っておきたかったことを、過去の自分に向けてまとめたものです。初心者向けなので、ちゃんとした理論よりざっくりとした概念を学びます。Tour of Goのお供にでも読んでください。

# Overview

  1. 型を見るときは fmt.Printf("%T", v)
  2. Sliceってなあに
  3. methodってなあに

当たり前かもしれないけれど、知らなかった(笑)。いつも以下のような感じで出力していたが、複雑で毎回調べていました。上の方がわかりやすい( %T はおそらくTypeのTだろう)ので、上を使っていきたいです。

fmt.Println(reflect.TypeOf(v))

Golangは型に厳しい言語なので、気になる変数を見つけたらすぐに型をみた方がいいと思います。


Golangには配列があり、それをArrayと呼びます。pythonとかをやっているとListがよく出てくるのでよくごちゃごちゃになります。当たり前かもしれませんがArrayとListとは全くの別物です。雰囲気似てますが。

# Golangのコード
arr := [3]int{1, 2, 3} # Go's array
import "container/list"
li := list.New() # Go's list
e1 := li.PushBack(1)
# pythonのコード
import array
arr = array.array('i', [1,2,3]) # python's array
li = [1, 2, 3] # python's list

それではArrayとList何が違うかと言われたら、データ構造が違います。ざっくりと言って

  • Arrayは要素の参照は早いが、要素の追加・削除は遅い
  • Listは要素の追加・削除は早いが、要素の参照は遅い

ここで言う要素の追加・削除は、最後の要素に追加(append)・削除させることではなく、要素を途中に割り込ませたい(insert)・途中の要素を削除したいと言う事です。まあ、でも基本的にGolangでは要素を途中に割り込ませたいときもarrayでやるようですが…(参照)

GolangのListについては下をみてください。

前置きが長くなってしまいましたが、ArrayとSliceの話に戻りましょう。Arrayは固定長ですがSliceは動的な配列です。すなわち、Arrayは拡張(append)することはできないけれど、Sliceは拡張(append)することはできると言うことです。拡張と言っても基本的にはappendを用いて末尾に要素を足すことが多いです。

型としては、int型の配列(Sequence?)なら以下のように表されます。違いは[]このカッコ(bruket)に数字(配列の長さ)が入っているか否かです。

a := [3]int{1, 2, 3}
b := []int{}
fmt.Printf("array is %T\n", a) // array is [3]int
fmt.Printf("slice is %T\n", b) // slice is []int

また、配列の一部を切り出してsliceにすることもできます。

a := [3]int{1, 2, 3}
fmt.Printf("array is %T\n", a) // array is [3]int
fmt.Printf("slice is %T\n", a[1:]) // slice is []int

この一部を切り出してできるSliceは参照渡しなので、元の配列を変えるとsliceの値を変えなくても値が変わってしまいます。

まあ、私が実務で使う限り基本的にSliceを使っていたので、あまりSliceのデータ構造とかは気にしなくても構わないと思います(初心者は)。最初はSliceとArrayと言う別の型があって、Sliceの方はapeendで末尾に要素を増やせるんだ、みたいな理解でもある程度は書けるかもしれません。ですが、型としては全く異なるので、関数の引数や返り値などの型を示す時にArrayとSliceどちらなのか意識しなければいけません。

詳しいデータ構造の違いは下に描いてありますが、まだ読んでいません(笑)。


最初の頃あまり意識していなかったですが、functionとmethodは別のものです。これらの違いを説明する前に、まず一般的な関数について説明します。

Golangでは一般的に関数を以下のように記述します。下は足し算の例です。

func adder(x, y int) (z int) {
z = x + y
return z
}

関数は下のように、func、関数名、引数、帰り値の順番で記述します。このような普通?の関数をfunctionと呼びます。ここまでは簡単ですね。

func <関数名> (引数) (返り値) {
....
}

実はGolangは<関数名>の前にもうひとつ括弧をつけることができます。例えば以下の関数がそうです。

func (s Student) SendEmail(email string) (context string) {
....
}
type Student struct { //構造体
Number int
School string
}

この<関数名>の前の括弧は何を示しているのでしょうか?実はこれは構造体(厳密には構造体でなくてもいい)を取っているのです。よって、関数は以下のように記述することもあるのです。

func (構造体) <関数名> (引数) (返り値) {
....
}

この構造体は何を意味しているのでしょう?これは構造体とその関数が結びついていることを示します。結びついているので、例えば上の例では func SendEmailを実行すると以下のようになります。

s := Student{   //構造体の定義
Number: 100,
School: "tus",
}
context := s.SendEmail("sample@google.ne.jp") //関数の実行

このように実行するときは、 (構造体の変数).<関数名>(引数)というように実行します。これの何が便利なのでしょうか?構造体に作用させたいなら

func SendEmail(u Student, email string) (context string) {
....
}

このように引数に構造体をとってもいいはずです。しかし、構造体と関数が関係しているときは、引数に構造体をとることはあまりしません。

実はこれがGolangのオブジェクト指向なのです。Golangにはclassがないので、structに関数を結びつけます。これによって、構造体と関数の関係がわかりやすくなり、プログラム全体の構成がわかりやすくなります。

例えば、以下は学生(type Student struct)が誰かにメールを送るプログラムです。type Student struct には学籍番号(Number)と学校名(School)を表す値があり、学生のメールアドレスは((学籍番号)@(学校名).ne.jp)で構成されます。

このプログラムではtype Student structfunc SnedEmail が結びついています。これによって全体を見渡した時に、独立した func SendEmail と捉えるのではなく、 type Student struct の一機能(一関数)としてfunc SnedEmail を捉えることができます(なぜなら他の構造体はfunc SnedEmailを実行できないため)。

以上で見たように、一般にclass(構造体)に結びついた関数をmethodと呼び、特にGolangは<関数名>の前に構造体が入った括弧があるものをmethodと呼ぶという風にざっくり理解しましょう。

あとは、methodは構造体自体だけでなくポインタとしても定義できますが、ポインタなのでメソッドの振る舞いが異なることに気をつけてください(構造体の値を書き換える時など)。

go run student.go 
何も実行していない : {100 tus}
ResetNumberを実行した : {100 tus}
ResetNumberWithPointerを実行した : {0 tus}

There are two reasons to use a pointer receiver.

The first is so that the method can modify the value that its receiver points to.

The second is to avoid copying the value on each method call. This can be more efficient if the receiver is a large struct, for example.

In general, all methods on a given type should have either value or pointer receivers, but not a mixture of both.

Choosing a value or pointer receiver


# Conclution

とてもざっくりとした説明になりました。いうまでもないことですが、ちゃんと内部仕様とかも理解しないと、思わぬところで意味不明な振る舞いをするので、ざっくりとした理解の後にちゃんと理解しましょう。Golangの公式ブログは図とかもあってとても読みやすいです。

個人的にGolangはじめてしっかり勉強した言語で、思い入れがあります。型がしっかりとしていて、「コンパイルが通れば」production環境に出した時に、ある程度のバグが避けられる所が好きです。

あと、文法が比較的少なく、複雑な機能を一つ一つ単純なものに分解していって、一から積み上げていく感じが、「コード書いてるうう」という気持ちにさせてくれます。

まだGopherの良さがわかりませんが、これからも頑張ってGolangを書いていきたいです。

# ref

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade