はじめに
昨日に引き続きGo言語の連載3回目です。
基本構文その1より個人的にはGoの特色が出ているだろう基本文法に関して紹介させていただきます。
配列
Goの配列は固定長です。可変長配列は後述するスライスがそれにあたります。たとえば長さが4で要素の型がstringである配列は,
1 | var arr [4]string |
配列は、ほかの言語同様に添字でアクセスします。
1 2 3 4 5 6 7 | var arr [4]string arr[0] = "a" arr[1] = "b" arr[2] = "c" arr[3] = "d" fmt.Println(arr[0]) // a |
宣言と同時に初期化することも可能です。その場合は[…]を用いることで,必要な配列の長さを暗黙的に指定できます。
1 2 3 | // どちらも同じ型 arr := [4]string{"a", "b", "c", "d"} arr := [...]string{"a", "b", "c", "d"} |
他のプログラミング言語に慣れていると少し奇妙に感じられるかもしれませんが、上記の配列の型名は要素数も含んだ[4]stringとなります。次のarr1とarr2は、要素の型は同じstringですが、長さが違うため配列としては別の型です。関数fnは[4]string型を引数にとるため、型の合わないarr2を渡すとコンパイルエラーになります。
1 2 3 4 5 6 7 8 9 10 11 | func fn(arr [4]string) { fmt.Println(arr) } func main() { var arr1 [4]string var arr2 [5]string fn(arr1) // ok fn(arr2) // コンパイルエラー } |
また、配列を渡す場合は値渡しとなり、配列のコピーが渡されます。次のfn()の中で配列に対して行った変更は、main()側には反映されません。
1 2 3 4 5 6 7 8 9 10 | func fn(arr [4]string) { arr[0] = "x" fmt.Println(arr) // [x b c d] } func main() { arr := [4]string{"a", "b", "c", "d"} fn(arr) fmt.Println(arr) // [a b c d] } |
スライス
スライスは、可変長配列として扱うことができます。配列を直接使うのは、シビアなメモリ管理が必要な一部のプログラムだけなので、同じ性質のデータを束ねて扱うという用途であれば、基本的にはスライスを用います。
スライスの宣言
stringのスライスは次のように宣言します。
1 | var s []string |
このように、スライスの型には配列のように長さの情報はありません。
初期化を同時に行う場合は、配列と同じように書くことができます。またスライスも、配列同様に添字でアクセスできます。
1 2 | s := []string{"a", "b", "c", "d"} fmt.Println(s[0]) // "a" |
len
スライスは可変長配列なので、プログラムの実行時に動的に要素数が変化します。スライスの現在の要素数を調べるには、組み込み関数lenを使用します。
1 2 | s := []string{"a", "b", "c", "d"} len(s) // 4 |
append
スライスの末尾に値を追加する場合はappendを使用します。appendは、スライスの末尾に値を追加し、その結果を返す組込み関数です。複数の値を追加することもできます。
1 2 3 4 5 | var s []string s = append(s, "a") // 追加した結果を返す s = append(s, "b") s = append(s, "c", "d") fmt.Println(s) // [a b c d] |
次のように指定すれば、スライスに別のスライスの中身を展開して追加することもできます。
1 2 3 4 | s1 := []string{"a", "b"} s2 := []string{"c", "d"} s1 = append(s1, s2...) // s1にs2を追加 fmt.Println(s1) // [a b c d] |
range
配列やスライスに格納された値を、先頭から順番に処理するような場合は、添字によるアクセスの代わりにrangeを使用できます。
for文の中でrangeを用いると、添字と値の両方が取得できます。
1 2 3 4 5 | s := []string{"a", "b", "c", "d"} for i, s := range arr { // i = 添字, s = 値 fmt.Println(i, s) } |
実行結果は次のようになります。
1 2 3 4 5 | $ go run range.go 0 a 1 b 2 c 3 d |
rangeは配列やスライスのほかに、string、マップ、チャネルに対しても使用できます。マップについては本記事で、チャネルについてはまた別の記事で解説します。
値の切り出し
string、配列、スライスから、値を部分的に切り出すことができます。次のように始点と終点をコロンで挟んで指定すると、その範囲の値を切り出すことができます。始点、終点を省略した場合、それぞれ先頭、末尾になります。
1 2 3 4 5 6 | s := []int{0, 1, 2, 3, 4, 5} fmt.Println(s[2:4]) // [2 3] fmt.Println(s[0:len(s)]) // [0 1 2 3 4 5] fmt.Println(s[:3]) // [0 1 2] fmt.Println(s[3:]) // [3 4 5] fmt.Println(s[:]) // [0 1 2 3 4 5] |
可変長引数
関数において引数を次のように指定すると、可変長引数として、任意の数の引数をその型のスライスとして受け取ることができます。
1 2 3 4 5 6 7 8 9 10 11 | func sum(nums ...int) (result int) { // numsは[]int型 for _, n := range nums { result += n } return } func main() { fmt.Println(sum(1, 2, 3, 4)) // 10 } |
nums …intという引数の定義が、可変長引数のすべての値を[]int型のスライスにまとめる指定として機能します。
関数における可変長引数の定義は「引数の末尾に1つだけ定義できる」という制限があります。可変長引数のあとに別の引数を定義したり、複数の可変長引数を定義することはできません。
map
Goの参照型であるmapは、いわゆる「連想配列」に類する、値をKey-Valueの対応で保存するデータ構造です。
宣言と初期化
たとえばintのキーにstringの値を格納するマップは次のように宣言します。
1 | var month map[int]string = map[int]string{} |
次のようにキーを指定して値を保存します。
1 2 3 | month[1] = "January" month[2] = "February" fmt.Println(month) // map[1:January 2:February] |
宣言と初期化を一緒に行う場合は次のように書きます。
1 2 3 4 5 | month := map[int]string{ 1: "January", 2: "February", } fmt.Println(month) // map[1:January 2:February] |
マップの操作
マップから値を取り出す場合は、次のようにキーを指定し、戻り値として受け取ります。
1 2 | jan := month[1] fmt.Println(jan) // January |
このとき2つ目の戻り値も受け取ると、指定したキーがこのマップに格納されているかをboolで返します。
1 2 3 | if value, ok := month[1]; ok { fmt.Println(value) // January } |
delete
マップからデータを消す場合は組込み関数のdeleteを使用します。
1 2 | delete(month, 2) fmt.Println(month) // map[1:January] |
deleteは「delete(マップ,キーの値)」のように使用します。この際マップに該当するキーの値が存在しなくてもエラーにはならず特に何も処理は行われません。
range
スライス同様、rangeを用いるとfor文でKey-Valueをそれぞれ受け取りながら処理を進めることができます。ただし、マップの場合は取り出される順番は保証されない点に注意してください。
1 2 3 | for key, value := range month { fmt.Printf("%d %s\n", key, value) } |
ポインタ
C言語などを学ばれた方であれば、ポインタは既にご存知かと思いますが、Go言語にもポインタというものがあります。ポインタは、データ構造のメモリ上のアドレスと型の情報になります。
ポインタの定義、アドレス演算子(&)やディリファレンス(*)など構文や利用方法は、C言語と同様になります。
但し、ここでは詳しくは触れませんが、一部Go言語ではC言語と異なり、制約があります。ただ、その制約も通常の開発においては気にすることはあまりないと思います。
それでは、サンプルで簡単に説明したいと思いますので、コードとコード内のコメントをみてください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | package main import ( "fmt" ) func main() { // pointer変数をintのポインタ型として宣言します。 var pointer *int value := 10 // アドレス演算子を利用して、value変数のアドレスをpointer変数に代入します。 pointer = &value // *int, 10, int, 10 と出力されます。 fmt.Printf("%T, %d, %T, %d\n", pointer, *pointer, value, value) value = 20 // valueを20に変更すると同じアドレスを参照しているpointerも変わるのがわかります。 // *int, 20, int, 20 と出力されます。 fmt.Printf("%T, %d, %T, %d\n", pointer, *pointer, value, value) } |
ポインタは構造体で利用されるケースが多いと思います。スライスやマップなどの参照型でも宣言は可能ではありますが、参照型は型の仕組み自体にポインタを使った参照を含んでいることから利用されるケースはあまりないでしょう。
構造体
Goには、構造体というデータ構造があります。構造体は複数のデータを1つにまとめることが基本的な役割ですが、後述するメソッドを持つことができ、RubyやJavaでのクラスに近い役割も担います。
type
構造体の解説の前に、まずは予約語「type」について確認してみましょう。Goではtypeを用いて既存の型や型リテラルに別名をつけることができます。この際、既存の型とtypeを用いた型は別の型という扱いになります。
1 2 3 4 5 | type ID int type Priority int func ProcessTask(id ID, priority Priority) { } |
typeのあとには、型の名前、その型の定義が続きます。上記では、intを拡張してIDと優先度それぞれに型を定義し、この型を用いて関数の定義を書き換えています。
呼び出す際には、型が適合していないとコンパイルエラーになります。
1 2 3 | var id ID = 3 var priority Priority = 5 ProcessTask(priority, id) // コンパイルエラー |
このように適切な型を用意することで、型レベルの整合性をコンパイル時にチェックでき、堅牢なプログラムを記述できます。IDE(Integrated Development Environment、統合開発環境)のサポートも得やすくなり、リファクタリング時のリグレッションなども防ぎやすくなります。
構造体型の宣言
ここでは、id、detail(タスクの詳細)、done(完了フラグ)の3つのフィールドを持つ、Taskという型を定義してみます。
1 2 3 4 5 | type Task struct { ID int Detail string done bool } |
構造体型もtypeを用いて宣言し、構造体名のあとにそのフィールドを記述します。各フィールドの可視性は名前で決まり、大文字で始まる場合はパブリック、小文字の場合はパッケージ内に閉じたスコープとなります。
この型から値を生成するには、次のように各フィールドに値を割り当てます。
1 2 3 4 5 6 7 8 9 10 | func main() { var task Task = Task{ ID: 1, Detail: "buy the milk", done: true, } fmt.Println(task.ID) // 1 fmt.Println(task.Detail) // "buy the milk" fmt.Println(task.done) // true } |
変数taskには、生成された構造体が格納され、各フィールドにはドットでアクセスできます。
構造体に定義した順でパラメータを渡すことで、フィールド名を省略することもできます。
1 2 3 4 5 6 7 8 | func main() { var task Task = Task{ 1, "buy the milk", true, } fmt.Println(task.ID) // 1 fmt.Println(task.Detail) // "buy the milk" fmt.Println(task.done) // true } |
構造体の生成時に値を明示的に指定しなかった場合は、ゼロ値で初期化されます。
1 2 3 4 5 6 | func main() { task := Task{} fmt.Println(task.ID) // 0 fmt.Println(task.Detail) // "" fmt.Println(task.done) // false } |
メソッド
Goにはメソッドという特徴的な機能があります。メソッドといっても、オブジェクト指向プログラミング言語によくあるメソッドとは異なります。Goのメソッドは、任意の型に特化した関数を定義するための仕組みです。
メソッドの定義
メソッドの定義は以下のように記述します。レシーバーに関する記述をする部分が、関数と異なる箇所になります。
1 2 3 | func (<レシーバー>) <関数名>([引数]) [戻り値の型] { [関数の本体] } |
例えばPersonという構造体を定義して、そのPersonのプロフィールを返すメソッドを定義する場合は以下のような記述をします。
1 2 3 4 5 6 7 8 9 10 11 12 | type Person struct { name string age uint } func (p *Person) profile(header string) string { return fmt.Sprintf("%s Profile[name: %s, age: %d]", header, p.name, p.age) } func (p Person) profile2(header string) string { return fmt.Sprintf("%s Profile[name: %s, age: %d]", header, p.name, p.age) } |
型に定義されたメソッドは、 <レシーバ>.<メソッド> という形式で呼び出すことができます。
1 2 3 4 5 6 7 | func main() { person1 := &Person{name: "KimuraJiro", age: 28} fmt.Printf("person1 -> %s\n", person1.profile("!!!")) person2 := &Person{name: "KimuraTaro", age: 38} fmt.Printf("person2 -> %s\n", person2.profile2("@@@")) } |
値渡しとポインタ渡し
メソッドのレシーバーには値渡しかポイント渡しで定義することができます。
値渡しの場合、構造体の新しいコピーが作成され、呼び出されたメソッドに渡されます。コピーの為、メソッド内で値を変更しても呼び出し元は影響を受けません。
ポインタ渡しの場合、構造体のポインタのコピーが渡され、メソッド内で構造体の値を変更した場合、ポインタを経由して書き換えているため、呼び出し側にもその変更が反映されます。次のコードをみてください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | type Person struct { firstName string lastName string } func (p Person) getName() string { return p.firstName + " " + p.lastName } func (p *Person) changeFirstName() { p.firstName = "Lisa" } func main() { person := Person { firstName: "Alice", lastName: "Dow", } fmt.Println(person.getName()) // "Alice Dow" &person.changeFirstName() fmt.Println(person) // {Lisa Dow} } |
getNameのように単純に構造体の値を参照するメソッドでは、ポインタ渡しにする必要性はないので値渡しで良いと思います。逆にchangeFirstNameのようにメソッド内で構造体の値を変更する場合はポインタ渡しでないと、呼び出し側のmain関数で定義されているpersonの値が変更されません。
値渡しとポインタ渡しのどちらを使用するかですが、値渡しはメソッド内で構造体の状態の変更が行われない為、スレッドセーフに進めることができます。基本は値渡しのレシーバを使用することを推奨しますが、メソッド内でどうしても構造体の値を変更する必要がある、もしくは値渡しでは構造体のコピーを新しく作成する為、構造体が巨大でコストの問題が発生する場合はポインタ渡しにするのが良いと思います。
関数としてのメソッド
メソッドはレシーバーの定義を必要とするなど、通常の関数定義とは少々異なります。しかし、実体としてのメソッドはGoの関数になります。メソッドを関数型として参照するときには「[レシーバーの型].[メソッド]」のように書くことができます。
1 2 3 4 5 6 7 8 9 | type Point struct{ X, Y int} func (p *Point) ToString() string { return fmt.Sprintf("[%d,%d]", p.X, p.Y) } /* 変数fは(func(*Point) string))型 */ f := (*Point).Tostring f(&Point{X: 7, Y: 11}) // "[7,11]" |
インターフェース
Go言語におけるオブジェクト指向的な実装をする上で非常に重要な機能であるインターフェースを紹介します。
インターフェースとは
インターフェースは、型の一種であり、メソッドの型だけを宣言したものになります。任意の型に対して、指定のメソッドを実装させることを強制できる仕組みになります。Javaなどの他の言語でもあるインターフェースと同様の扱いです。
ただ、Go言語の場合は、明示的にインターフェースを実装するのではなく、インターフェースが定義しているメソッドを全て実装されていれば、そのインターフェースを実装していることになります。
Go言語は、連載1回目でも軽く触れていますが、クラスという概念がありません。そのため、継承がなく、このインターフェースでのみ、多態性(ポリモーフィズム)を表現できます。
それでは、簡単なサンプルで紹介します。ファイル構成は、下記になります。
1 2 3 4 5 6 7 8 9 | $ tree -I go . ├── go.mod ├── go.sum ├── main.go └── world ├── greeting.go ├── greeting_en.go └── greeting_ja.go |
まずは、インターフェースを宣言している、greeting.goファイルをみてみましょう。Sayというメソッドを一つだけ宣言しています。また、CreateGreetingというファクトリ関数的なものを用意して、引数に応じてGreetingインターフェースを実装した構造体を返却します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | package world type Greeting interface { Say() string } func CreateGreeting(lang string) Greeting { switch lang { case "ja": return &greetingJa{} default: return &greetingEn{} } } |
次にGreetingインターフェースを実装する2つの構造体になります。この2つの構造体は、Sayメソッドを実装しており、返却される文字列が日本語と英語で異なるだけです。
1 2 3 4 5 6 7 8 | package world type greetingJa struct { } func (g *greetingJa) Say() string { return "こんにちは、世界。" } |
1 2 3 4 5 6 7 8 | package world type greetingEn struct { } func (g *greetingEn) Say() string { return "Hello, World." } |
最後に、main.goファイルです。greeting変数は Greeting interface型になりますが、greetingJa構造体もgreetingEn構造体も代入できます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | package main import ( "fmt" "github.com/re-engines/hello_go/world" ) func main() { greeting := world.CreateGreeting("ja") fmt.Println(greeting.Say()) greeting = world.CreateGreeting("en") fmt.Println(greeting.Say()) } |
実行結果は、下記のようになり多態性(ポリモーフィズム)が表現できています。
1 2 3 | $ go run main.go こんにちは、世界。 Hello, World. |
空インターフェース
インターフェースのおまけとして、 interface{}(空インターフェース)についても軽く触れておきます。この空インターフェースは、実装を強制させるメソッドが何もない(空の)インターフェースとなります。つまり、全ての型と互換性がある特殊な型ということで、どんな型でも代入することができるのです。
下記では、数値と文字列を代入するサンプルを紹介します。
1 2 3 4 5 6 7 8 9 10 11 12 | package main import "fmt" func main() { var any interface{} any = 10 fmt.Println(any) // 10と表示されます any = "hello" fmt.Println(any) // helloと表示されます。 } |
nil
varを使用した変数の定義では、例えばint型の変数の初期値であれば0がセットされますが、interface{}型ではどうなるのでしょうか。
1 2 | var x interface{} fmt.Printf("%#v", x) // "<nil>" |
出力させてみると「<nil>」という内容が得られました。nilはGoにおいて「具体的な値を持っていない」状態をあらわす特殊な値です。
型を持つnil
nilは型と値の2つを持ちます。
1 2 3 4 5 6 7 8 9 10 11 12 13 | func main() { var i32 *int32 fmt.Println(reflect.ValueOf(i32).IsNil()) // true fmt.Println(reflect.TypeOf(i32)) // *int32 var arr []int fmt.Println(reflect.ValueOf(arr).IsNil()) // true fmt.Println(reflect.TypeOf(arr)) // []int var m map[string]string fmt.Println(reflect.ValueOf(m).IsNil()) // true fmt.Println(reflect.TypeOf(m)) // map[string]string } |
なので型の違うnil同士だと「==」ではfalseになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 | func main() { var i32 *int32 fmt.Println(i32 == nil) // true var arr []int fmt.Println(arr == nil) // true fmt.Println(compare(i32, arr)) // false } func compare(first, second interface{}) bool { return first == second } |
interface{}での「== nil」
先の例で分かるように、interface{}以外の型であれば型がついていても「== nil」はtrueになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | func main() { var arr []int fmt.Println(arr == nil) // true fmt.Println(reflect.ValueOf(arr).IsNil()) // true fmt.Println(reflect.TypeOf(arr)) // []int var arr2 interface{} = arr fmt.Println(arr2 == nil) // false fmt.Println(reflect.ValueOf(arr2).IsNil()) // true fmt.Println(reflect.TypeOf(arr2)) // []int var arr3 interface{} fmt.Println(arr3 == nil) // true fmt.Println(reflect.TypeOf(arr3)) // <nil> } |
arrとarr2はnilの型も値も同じですが、arr2はinterfaceとして扱っており、その場合は型もnilでないと「== nil」はfalseになります。
newとmake
Go言語の基本的なメモリ割り当てには、new()とmake()の2つがあります。これら2つはそれぞれ異なる働きをし、適用先の型も別となります。混乱させてしまうかもしれませんがルールは単純です。
new
まずnew()について説明します。これは組み込み関数であり、他の言語におけるnew()と基本的に同じです。new(T)は、型Tの新しいアイテム用にゼロ化した領域を割り当て、そのアドレスである*T型の値を返します。Go言語風に言い換えると、new(T)が返す値は、新しく割り当てられた型Tのゼロ値のポインタです。
new()から返されるメモリはゼロ化されています。ゼロ化済みオブジェクトは、さらなる初期化を行わなくても使用できるため、こういったオブジェクトの準備にnew()は便利です。すなわち、データ構造体の利用者がnew()でそれを作成すると、すぐに使える状態となります。
make
組み込み関数make(T, args)は、new(T)とは使用目的が異なります。makeで割り当てできるのはスライス、マップ、チャネルだけであり、初期化された、すなわちゼロ値でないT型(*Tでない)の値を返します。makeとnewを使い分ける理由は、これらの3つの型が隠蔽されたデータ構造への参照であり、このデータ構造が使用前に初期化されている必要があるためです。スライスを例にとると、スライスはデータ(配列内)へのポインタ、長さ、キャパシティという3つの情報から構成されており、それらの情報が初期化されるまではスライスの値はnilです。makeはスライス、マップ、チャネルの内部データ構造を初期化し、使用可能となるよう値を準備します。下は、makeの例です。
1 | make([]int, 10, 100) |
この例では、100個のintを持つ配列を割り当てたあと、その配列の先頭から10個目までの要素を示す、長さが10でキャパシティ100のスライス構造を作成します。(スライス作成時、キャパシティは省略可能です。動的にキャパシティをオーバーした場合メモリが再度割り当てられます)これに対して、new([]int)は新しくメモリを割り当て、ゼロ化したスライス構造のポインタを返します。つまりこれはnilスライス値へのポインタです。
下は、new()とmake()の違いを例示したものです。
1 2 3 4 5 6 7 8 9 | var p *[]int = new([]int) // スライス構造の割り当て(*p == nil)。あまり使わない。 var v []int = make([]int, 100) // vは100個のintを持つ配列への参照 // 必要以上に複雑な書き方 var p *[]int = new([]int) *p = make([]int, 100, 100) // 一般的な書き方 v := make([]int, 100) |
さいごに
今回RE:ENGINESのGo言語勉強会で私が勉強した箇所は以上になります。
Go初心者ではありますが基本構文に関して理解が深まったと思います、まだまだ勉強不足なので勉強は引き続き行い、今回紹介できなかった基礎、基本文法も記事にしていき理解を深めたいと思っています。
Go記事の連載などは、こちらをご覧ください。