goの基本文法 No.6
2020-09-30執筆
A Tour of Goに沿ってGo言語の文法をまとめます。
これまでにJavaScriptやPythonなどのリッチ言語しか使ったことがなかった自分のためにまとめたものです。
なのでこれまでに上のようなリッチ言語を触ったことがあり、これからGo言語を触ってみようと考えている人の参考になればと思います。
文章の構成は、基本的に
- 概要
- コード
- コードの説明
の構成にしているつもりです。概要で簡単な説明をし、コードで例を示して、さらにコードの説明で細かい説明をするといった構成です。
メソッド
method
Goにはクラスという概念がありません。しかしメソッドを定義することはできます。Goに取ってメソッドはレシーバー変数が持つ関数のことを指します。簡単に言うと個々の変数が専用の関数を持つことができ、それをメソッドといいます。以下の例で確認してみましょう。
// code:6-1
type Vertex struct {
X, Y float64
}
// Vertex型のメソッド①
func (v Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
func main() {
v := Vertex{3, 4}
fmt.Println(v.Abs()) // ②
}
/*実行結果
5
*/
上のコードは、xy平面上の1点を表すVertex型を定義し、それに対して原点からその点までの距離を求めるAbsメソッドを実装しています。main関数でVertex型の変数vを定義しています。Vertex型はAbsメソッドを持っているので変数vもAbsメソッドを持ちます。つまり特定の型に対してメソッドを定義することで、その型の変数がそのメソッドを持つと言うことです。メソッド内の処理は中学数学で理解可能なので読者それぞれで調べてください。
- 上の例からメソッドの書き方はとなります。
func (変数 型) メソッド名(引数 型) 返り値 { // 処理 }
- メソッドを呼び出す時は他の言語同様ドット
変数名.メソッド名
で呼び出します。
メソッドによる値の書き換え
先の例では値を出力するだけで変数そのものを変更したりはしていません。しかし他の言語で値の変更用のメソッドを実装してそこからのみ変数の値にアクセスするようにすることはよくあります。Goではメソッドで値の変更はどうするのかみてみましょう。
// code:6-2
type Vertex struct {
X, Y float64
}
func (v *Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
// 今回注目するメソッド
func (v *Vertex) Scale(f float64) {
v.X = v.X * f
v.Y = v.Y * f
}
func main() {
v := Vertex{3, 4}
v.Scale(10)
fmt.Println(v.Abs())
}
上の例では、先ほどのコードにさらにScaleメソッドを実装しています。Scaleメソッドは浮動小数点型の引数fを受け取り、原点からの距離を引数f倍します。つまり点の位置を書き換えるので元々の変数を書き換えることになります。ポインタのところを思い出して欲しいのですが、スコープの異なる変数へのアクセスには直接メモリアドレスにアクセスするポインタを使えば良かったですね。Scaleメソッドでは(v *Vertex)
とポインタを受け取っています。
また、上の例からもわかるように一つの変数に対して複数のメソッドを実装することもできます。
もう一つ例をみて理解を深めましょう。
// code:6-3
type MyFloat float64
func (f *MyFloat) Abs() {
// (1)
if *f < 0 {
*f = -1 * *f
}
}
func main() {
f := MyFloat(-3.14)
f.Abs() // (2)
fmt.Println(f)
}
/*実行結果
3.14
*/
上のコードはMyFloatという浮動小数点数型に対して必ず正の値になるように値を変更するAbsメソッドを実装しています。これも値を書き換えるのでポインタで渡しています。メソッド内の処理ではポインタからメモリアドレスの値にアクセスするので*f
を使います(1)。また、引数にポインタを渡す場合は&
を使う必要がありましたが、メソッドの場合は必要ありません(2)。
code6-2では値の書き換えをしないAbsメソッドまでポインタにしています。これはレシーバーを値にするかポインタにするか統一するべきだからです。なぜ統一するべきなのか、その理由はこのあとのinterfaceの項で説明します。
インターフェース
interface
ここではインターフェースについてみていきます。リッチ言語だとインターフェースは登場しないイメージなので細かくみていきたいと思います(少なくとも自分が触っている範囲では登場しませんでした)。
インターフェースとは一般的には「何かと何かをつなぐ部分」を指します。例えばユーザーインターフェースはユーザーとコンピュータをつなぐ部分を指します。
プログラミングでは特にオブジェクト指向プログラミングで用いられ、「クラスを作った人とクラスを使う人をつなぐもの」と解釈できます。クラスを作った人が持たせたいメソッドや渡したい引数など意図していることを、クラスを使う人に伝える(強制する)ためのものです。
例えば、Youtubeのようなサイトを作りたくて動画を再生する機能を作っていたとしましょう。動画にはmp4, flv, wmvなどいろいろなタイプのものがあります。そのようないろいろなタイプの動画に対して「再生・停止・早送り・巻き戻し」などの機能(メソッド)を実装する必要があります。もしかしたら「次の動画にジャンプする」という機能を付けるかもしれません。どのようなメソッドを実装するにせよ全ての動画がこれらの機能を満たしていないと困りますよね。そのような時にインターフェースを通して実装すると今後出てくる新しいタイプの動画も含めて実装漏れが防げます。
これは例え話なので実際に動画を再生する機能がこのように実装されているかはわかりませんが、機能の共通化という点ではイメージがついたのではないかと思います。
Goのinterface
Goのインターフェースは型の一つであり、メソッドの名前や引数、返り値などのシグネチャの集合として定義されます。
type インターフェース名 interface {
メソッド名(引数) 返り値
メソッド名(引数) 返り値
...
}
定義の仕方は上の通りです。これだけだとわかりにくいのでcode:6-4をみて確認しましょう。
// code:6-4
type Abser interface { // ①
Abs() float64
}
func main() {
var a Abser // ②
f := MyFloat(-math.Sqrt2)
v := Vertex{3, 4}
s := "hello"
a = f // ③ a MyFloat implements Abser
fmt.Println(a.Abs())
a = &v // ④ a *Vertex implements Abser
fmt.Println(a.Abs())
a = s // ⑤
fmt.Println(s)
}
type MyFloat float64
func (f MyFloat) Abs() float64 {
if f < 0 {
return float64(-f)
}
return float64(f)
}
type Vertex struct {
X, Y float64
}
func (v *Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
/*実行結果
このコードをそのまま実行した場合は表示されない(
1.4142135623730951
5
)
./prog.go:27:4: cannot use s (type string) as type Abser in assignment:
string does not implement Abser (missing Abs method)
*/
code:6-4はインターフェースを使ったコードです。
- 必ず引数を取らず、浮動小数点数型を返すAbsメソッドを持つ必要があるAbserというインターフェース型を定義しています。
- Abserインターフェース型の変数aを宣言しています。この変数に代入できるのはAbsメソッドを持った変数のみです。
- MyFloat型の変数fをaに代入しています。fはAbsメソッドを持っているので代入できます。
- Vertex型の変数vをポインタ(
&v
)としてaに代入しています。vもAbsメソッドを持っているので代入できます。
Vertex型ではaに代入できません。あくまで*VertexレシーバーがAbsメソッドを持っているのでaに代入できるのは*Vertexだけです。 - string型の変数sをaに代入しています。しかし、sはAbsメソッドを持っていないのでエラーになります。
code:6-4をみるとインターフェース型の値(a
)はMyFloat型でもVertex型でも代入できたように、インターフェース型はそれらのメソッドを実装した任意の値を保持することができます。
Goのインターフェースは値と具象型の組みと見なすことができる
// code:6-5
type I interface {
M()
}
type T struct {
S string
}
func (t *T) M() {
fmt.Println(t.S)
}
type F float64
func (f F) M() {
fmt.Println(f)
}
func main() {
var i I
i = &T{"Hello"}
describe(i)
i = F(math.Pi)
describe(i)
}
func describe(i I) {
fmt.Printf("(%v, %T)\n", i, i)
}
/*実行結果
(&{Hello}, *main.T)
(3.141592653589793, main.F)
*/
code:6-5の実行結果のようにインターフェース型は、(値, 具象型)
とみることができます。メソッドを呼び出すと、そのインターフェースの持つ具象型内の同名のメソッドを呼び出します。
あくまで(値, 具象型)
とみることができ、そう考えると理解が深まるだけで、そのような値を返すわけではありません。
nil値を持つインターフェース
値がnilのインターフェースはどのような感じなのでしょうか。以下の例をもとにみてみましょう。
// code:6-6
type I interface {
M()
}
type T struct {
S string
}
func (t *T) M() {
if t == nil { // ①
fmt.Println("<nil>")
return
}
fmt.Println(t.S)
}
func main() {
var i I
var t *T
i = t
describe(i)
i.M()
}
func describe(i I) {
fmt.Printf("(%v, %T)\n", i, i)
}
/*実行結果
(<nil>, *main.T)
<nil>
*/
インターフェースの持つ値がnilの場合、メソッドはnilレシーバーで呼び出されます。といっても特別なメソッドを実装する必要はありません。メソッド内にnilによる例外処理をかけば対応できます。
code:6-6では具体的な値を代入していない構造体tをインターフェース変数iに代入しています。なのでiの値は実行結果からもわかるようにnilになります。ここで①のように実装が必要なMメソッド内にnilが渡された時の例外処理を書いてください。このように例外処理をするのが一般的のようです。
注)インターフェースの持つ値がnilだとしてもインターフェース値i
自体はnilではありません。
ちなみに、例外処理を書かない場合以下のようになります。
// code:6-7
func (t *T) M() {
fmt.Println(t.S)
}
func main() {
var i I
var t *T
i = t
describe(i)
i.M()
}
/*実行結果
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x49a8c6]
goroutine 1 [running]:
main.(*T).M(0x0)
/tmp/sandbox056306968/prog.go:14 +0x26
main.main()
/tmp/sandbox056306968/prog.go:23 +0xa5
*/
nil インターフェース
インターフェース値がnilになる場合はあるのでしょうか。以下の例をみてみましょう。
// code:6-8
type I interface {
M()
}
func main() {
var i I
describe(i)
}
func describe(i I) {
fmt.Printf("(%v, %T)\n", i, i)
}
/*
(<nil>, <nil>)
*/
code:6-8では宣言したインターフェース値i
に何も代入していません。このような場合はnilになります。そしてその時は値も具象型もnilになります。
空のインターフェース
インターフェース定義の時何もメソッドを指定しないと空のインターフェースになります。
type インターフェース名 interface{
// 何も書かない
}
上のように何も書かないと空のインターフェースになります。これから定義される型も含めて全ての型は少なくともゼロ個のメソッドを実装しています。なので空のインターフェースは任意の型を代入できます。
// code:6-9
func main() {
var i interface{}
describe(i)
i = 42
describe(i)
i = "hello"
describe(i)
}
func describe(i interface{}) {
fmt.Printf("(%v, %T)\n", i, i)
}
/*実行結果
(<nil>, <nil>)
(42, int)
(hello, string)
*/
code:6−9では空のインターフェースi
にいろいろな型を代入しています。このように空のインターフェースはどのような型でも受け入れるので型が不明な値を扱うコードで使用されます。例えば、fmt.Printは出力のための関数ですが引数の型はいろいろあります。そのためinterface{}型の引数を任意の数だけ取ります。
型アサーション
型アサーションは、インタフェース値の基礎となる具象値へのアクセスを提供します。
t := i.(T)
このステートメントは、インタフェース値 i が具象型 T を保持していることを主張し、その基礎となる T の値を変数 t に代入します。具体例でもう少し詳しくみてみましょう。
// code:6-10
func main() {
var i interface{} = "hello"
// ①
s := i.(string)
fmt.Println(s)
// ②
f = i.(float64) // panic
fmt.Println(f)
// ③
s, ok := i.(string)
fmt.Println(s, ok)
// ④
f, ok := i.(float64)
fmt.Println(f, ok)
}
/*実行結果
hello
panic: interface conversion: interface {} is string, not float64
hello true
0 false
*/
code:6-10は型アサーションを使った例です。
- インターフェースiの宣言時に
"hello"
を代入しているのでstring
型を保持しているので、予定通りその値"hello"
を変数sに返します。 - ここでは
float64
を主張していますが、保持しているのは、string
型なのでpanicを起こします。 - 代入先として
s, ok
と二つの引数をとることもできます。この場合予定通りなので変数sには保持している値が、okにはtrueが返されます。 - ③と同様にfに値、okには真偽値が入ります。今回は予定通りではないのでokにはfalse, fにはfloat64のゼロ値である0が入ります。この場合②のようにパニックは起こさないので、型アサーションの場合は③、④のような処理をオススメします。その場合予定通りではない値が返ってきた時の例外処理も必要でしょう。
型スイッチ
空のインタフェースは任意の型の値を持つことができました。空のインターフェースを引数にとる場合、それが持つ値の型を確認しそれぞれの型に応じた処理が必要になるでしょう。その時に使うのが型スイッチです。例をみてみましょう。
// code:6-11
func do(i interface{}) {
switch v := i.(type) {
case int:
fmt.Printf("Twice %v is %v\n", v, v*2)
case string:
fmt.Printf("%q is %v bytes long\n", v, len(v))
default:
fmt.Printf("I don't know about type %T!\n", v)
}
}
func main() {
do(21)
do("hello")
do(true)
}
/*実行結果
Twice 21 is 42
"hello" is 5 bytes long
I don't know about type bool!
*/
code:6-11では空のインターフェースを引数にとるdo関数が実装されています。その中に書かれている処理が型スイッチになります。スイッチ文と似ていますが、ケースは値ではなく型を指定し、与えられたインタフェース値が保持する値の型と比較されます。code:6-11のvが持つのはインターフェースの保持する値です。ケースで評価されるのは値の持つ型になります。
ここからはいくつか組み込みインターフェースをみていきましょう。
Stringers
Stringerインターフェースは文字を出力するfmtパッケージに組み込まれているインターフェースです。配列にしろ構造体にしろfmt.Println関数で出力するといい感じの形で出力してくれているのは何故なのか疑問に思ったことはありませんか?実はfmtパッケージが出力する時はこのインターフェースを探しているからです。
type Stringer interface {
String() string
}
StringerインターフェースはStringメソッドを持ち、このStringメソッドが出力する文字列を生成します。変数にStringメソッドを持たせることでStringerインターフェースを満たし任意の出力に変更することができます。以下の例をみてみましょう。
// code:6-12
// 書き換えなし
type Person struct {
Name string
Age int
}
func main() {
a := Person{"Arthur Dent", 42}
z := Person{"Zaphod Beeblebrox", 9001}
fmt.Println(a)
fmt.Println(z)
}
/*実行結果
{Arthur Dent 42}
{Zaphod Beeblebrox 9001}
*/
// 書き換えあり
type Person struct {
Name string
Age int
}
func (p Person) String() string {
return fmt.Sprintf("%v (%v years)", p.Name, p.Age)
}
func main() {
a := Person{"Arthur Dent", 42}
z := Person{"Zaphod Beeblebrox", 9001}
fmt.Println(a)
fmt.Println(z)
}
/*実行結果
Arthur Dent (42 years)
Zaphod Beeblebrox (9001 years)
*/
code:6-12はPerson構造体に対してStringメソッドを実装しているもの(上)とStringメソッドを実装していないもの(下)です。
このようにfmtパッケージにおいて扱うデータはStringメソッドを実装してあげるだけでStringerインターフェースを満たし自分の好きなように出力を変えることができます。
Exercise: Stringers
最後に該当するエクササイズの概要と私の解答を載せておきます。参考までに。
私の解答
Errors
Goではエラーの状態をエラー値で表現します。エラーはfmtパッケージのStringerと同じように組み込みのインターフェースです。
type error interface {
Error() string
}
上のようにエラーインターフェースはErrorメソッドを持ち、このメソッドがエラーメッセージの文字列を返します。fmtパッケージはエラーの際にはこのエラーインターフェースを探してエラーメッセージを出力します。
i, err := strconv.Atoi("42")
if err != nil {
fmt.Printf("couldn't convert number: %v\n", err)
return
}
fmt.Println("Converted integer:", i)
上では1行目で関数が二つの値を返しています。そのうち変数errに代入されるものがエラー値になります。このように関数の中には値の他にエラー値を返すものがあります。基本的には関数内の処理が上手くいかなかった時にエラーが返ってきます。逆に上手くいった時はエラーが返らないのでerrの値はnilになります。なのでGoではしばしば2行目以降のようにif文でerrの値がnilかどうかで処理を分岐しエラーハンドリングします。
Errorメソッドを追加するとErrorインターフェイスを満たすので、エラーを扱うことができエラーメッセージも任意のものを設定できます。以下の例をみてみましょう。
// code:6-13
type NotNumberError struct { // 下でErrorメソッドを実装しているのでエラーインターフェースを満たす。
value interface{}
}
func (e *NotNumberError) Error() string {
return fmt.Sprintf("%v is not a number.", e.value)
}
// 与えられた引数が数値ならその絶対値を、そうでなければエラーを返す。
func intAbs(x interface{}) (a interface{}, err error) {
switch v := x.(type) { //型スイッチ
case int:
if v < 0 {
a = -v
} else {
a = v
}
err = nil
case float64:
if v < 0 {
a = -v
} else {
a = v
}
err = nil
default:
a = 0
err = &NotNumberError{v}
}
return
}
func main() {
v, err := intAbs(true)
if err != nil {
fmt.Println(err)
} else {
fmt.Println(v)
}
}
/*実行結果
true is not a number.(intAbs(true))
hello is not a number.(intAbs("hello"))
13(intAbs(-13))
3.141592653589793(intAbs(-math.Pi))
*/
code:6-13はNotNumberErrorをエラーインターフェースとして実装し、intAbs関数で使用しています。intAbs関数は空のインターフェースを引数に取るのでどのような型でも受け入れます。関数内では型スイッチによりint型かfloat64型なら絶対値を返すようにしています。そうでない場合(default)はNotNumberErrorのErrorメソッドを呼び出し、エラーメッセージを返します。実行結果から上手くいっていることが確認できます。
Exercise: Errors
最後に該当するエクササイズの概要と私の解答を載せておきます。参考までに。
私の解答
Readers
標準入出力のパッケージにio
があります。ioパッケージにはio.Reader
インターフェースがあります。io.ReaderインターフェースはReadメソッドを持ちます。
func (T) Read(b []byte) (n int, err error)
Readメソッドは引数にバイト型の配列を持ちます。そして入力されたバイト数とエラー値を返します。ストリームが終了するとio.EOF
エラーを返します。
実際のコードをみてみましょう。
// code:6-14
import (
"fmt"
"io"
"strings"
)
func main() {
r := strings.NewReader("this is article.") // ①
b := make([]byte, 8) // ②
// ③
for {
n, err := r.Read(b)
fmt.Printf("n = %v err = %v b = %v\n", n, err, b)
fmt.Printf("b[:n] = %q\n", b[:n])
if err == io.EOF {
break
}
}
}
/*実行結果
strings.NewReader("this is article.")の場合
n = 8 err = <nil> b = [116 104 105 115 32 105 115 32]
b[:n] = "this is "
n = 8 err = <nil> b = [97 114 116 105 99 108 101 46]
b[:n] = "article."
n = 0 err = EOF b = [97 114 116 105 99 108 101 46]
b[:n] = ""
strings.NewReader("this is art.")の場合
n = 8 err = <nil> b = [116 104 105 115 32 105 115 32]
b[:n] = "this is "
n = 4 err = <nil> b = [97 114 116 46 32 105 115 32]
b[:n] = "art."
n = 0 err = EOF b = [97 114 116 46 32 105 115 32]
b[:n] = ""
*/
- stringsパッケージは文字列を扱います。NewReader関数は引数の文字列からReaderインターフェースを実装し返します。これで変数rはReadメソッドを持った文字列ということになります。
- 変数bは8バイト分のバイト配列です。
- forループで回して、
r.Read(b)
で8バイトずつ取得していきます。取得する値がなくなるとReadメソッドがEOFエラーを吐くのでif文でブレイクしてあげてループを終了します。
Exercise: Readers
最後に該当するエクササイズの概要と私の解答を載せておきます。参考までに。
私の解答
Exercise: rot13Reader
最後に該当するエクササイズの概要と私の解答を載せておきます。参考までに。
私の解答
Images
imageパッケージはImageインターフェースを持ちます。
type Image interface {
ColorModel() color.Model
Bounds() Rectangle
At(x, y int) color.Color
}
上のようにImageインターフェースは3つのメソッドを持ちます。
ここで扱う画像はラスタ画像に分類されるもので、画面を格子状に分割し、その一つ一つに色を付けていくことで表現します。
また、コンピュータの色の表現も光の三原色である赤・緑・青の3色で表現するRGBや、色相(Hue)・彩度(Saturation)・明度(Brightness)の3つの属性で表現するHSBなど様々です。
- ColorModel: 色の表現(RGB, HSBなど)をどれでするか、そのモデルを返す。
- Bounds : 色を付ける範囲を返す。言い換えると形を指定する。
- At(x, y) : 格子状に分割した一つ一つを座標で指定して、座標一つ一つの色を指定する。
color.Modelとcolor.Color型もインターフェースです。以下の例では定義済みの実装であるcolor.RGBAやcolor.RGBAModelを使うことで無視しています。
// code:6-15
import (
"fmt"
"image"
)
func main() {
m := image.NewRGBA(image.Rect(0, 0, 100, 100))
fmt.Println(m.Bounds())
fmt.Println(m.At(0, 0).RGBA())
}
/*実行結果
(0,0)-(100,100)
0 0 0 0
*/
code:6-15はImageインターフェースの持つメソッドの返り値をそのまま出力しています。
少々説明が少なくイメージが付きにくいですがこれ以上は公式ドキュメントを深堀することになるのでもっと深く理解したい人は、公式ドキュメントを参照してください。大変ですが公式ドキュメントを読むことに慣れておくと便利です。
Exercise: Images
最後に該当するエクササイズの概要と私の解答を載せておきます。参考までに。
私の解答
Discussion