この記事の目的と対象
この記事は、今までruby/python/Java/c++などをやってきた僕自身がGo言語をやることになり、「Goの特徴を把握したい」「Goの文法に慣れるまではパッと確認できる場所にしたい」という目的でまとめたものです。よって、説明自体は少し雑ですが、今まで他言語をやっていて、これからGoをやってみようという人にとっては、 Goの特徴・文法がよくわかる内容になっていると思います。
Go言語とは
Goは,2009年にGoogleにより発表されたオープンソースのプログラミング言語です。
Goはシンプルな言語仕様であるため学習が比較的容易で,豊富な標準パッケージが同梱されているためすばやく目的を達成できます。また,巨大なコードでも高速にコンパイルできるため大規模開発にも適しており,Windows,OS X,Linuxなどの環境に合わせた実行ファイルを生成するクロスコンパイルのしくみがあるため作成したプログラムを容易に配布できます。並行処理のサポートも充実しており,ミドルウェアの開発などにも適しているとされています。
というのは、そこらへんに書いてあるGo言語の特徴であり、学習して見て感じたことは、
- 静的型付け言語であり、型やメモリの管理にとても厳しいが、一方でそれを柔軟に操作できる。(型推論があったりなど)
- こんな書き方もできるよ!こんな書き方も!というのが少なく、シンプルで良い(rubyはバリエーションが多すぎた)
- 並列処理がめちゃくちゃ楽
- Goはオブジェクト指向言語ではないのでクラスとかないが、オブジェクト指向言語におけるクラス・オブジェクトの設計は、Goでは構造体・インターフェースの設計に置き換えられる。この関係性をしっかりと把握できれば問題ない。
などです。ここら辺のポイントを見ながら学習を進めていくと分かりやすいかもしれません。
文法
プログラムの構成
Goでは、変数や関数といったプログラムの全ての要素は、何らかのパッケージに属す。そのため、プログラムはパッケージの宣言から始める。package main
によって、このファイルがmain
パッケージに関するプログラムだと示している。
また。ファイル内のプログラムで使用するパッケージを指定するため、importで宣言する。
そして、Goプログラムでは、mainパッケージの中に定義された関数mainから実行が開始されると定められている。これをエントリーポイントという。
zoo
|-- animals
|-- elephant.go
|-- monkey.go
|-- main.go
// zoo/animals/elephant.go
package animals
func ElephantFeed() string {
return "Grass"
}
// zoo/animals/monkey.go
package animals
func MonkeyFeed() string {
return "Banana"
}
// zoo/main.go
package main
import (
"fmt"
"./animals"
)
func main() {
fmt.Println(animals.ElephantFeed()) //=> Grass
fmt.Println(animals.MonkeyFeed()) //=> Banana
}
変数
変数の定義には、明示的な定義と暗黙的な定義が存在する。
明示的には、var [変数名] [変数の型]
のように定義する。
// int型の変数nを定義・代入
var n int
n = 5
// int型の変数x,y,zを定義・代入
var x, y, z int
x, y, z = 1, 3, 5
// int型の変数x,yとstring型のnameを定義
var (
x, y int
name string
)
一方、暗黙的には、[変数名] := [変数の値]
もしくはvar [変数名] = [変数の値]
のように定義する。型推論が行われ、型指定の必要がない
i := 1
b := true
f := 3.14
s := "abc"
var i = 1
var (
b = true
f = 3.14
s = "abc"
)
Goの変数は、定義される場所の違いによって2種類に分かれる。任意の関数の中に定義された変数はローカル変数で、関数定義の外部に定義された変数はパッケージ変数になる。パッケージ変数は同一パッケージであればどこからでも参照できる。
package main
import (
"fmt"
)
var n = 100 // パッケージ変数
func main() {
var a = 50 // ローカル変数
n = n + a // パッケージ変数は参照可能
fmt.Printf("n=%d\n", n) //=> 150
}
基本型
Goは静的型付け言語であり、全ての変数は何らかの型に属し、異なる型同士の演算といった問題点の多くはコンパイル時に検出される。基本型を利用しつつ、必要に応じて専用のデータ型を定義していくことがGoを利用したプログラミングの作業の中核となる。
- 論理値型(bool)
- 数値型(int)
- 符号付整数型
- int8
- int16
- int32
- int64
- 符号なし整数型
- uint8(byte)
- uint16
- uint32
- uint64
- 符号付整数型
- 浮動小数点型
- float32
- float64
- 複素数型
- complex64
- complex128
- rune型(rune)
- 文字列型(string)
配列型
Goの配列型は要素数まで型名に含める厳密なデータ型である、そのため、配列型の拡張や縮小は不可能でサイズは常に固定である。可変長配列のような柔軟なデータ構造は、Goではスライス(slice)が相当する。
配列型の型名は[要素数]要素の型
のように宣言する。型名に続けて{}
で囲むことで初期値を設定することもできる。
a := [5]int{1, 2, 3, 4, 5} // a == "[1, 2, 3, 4, 5]"
b := [5]int{1, 2, 3} // b == "[1, 2, 3, 0, 0]"
c := [5]int{} // c == "[0, 0, 0, 0, 0]"
var d [5]int // d == "[0, 0, 0, 0, 0]"
e := [...]int{1, 2, 3} // e == "[1, 2, 3]"
f := [...]int{1, 2, 3, 4, 5} // e == "[1, 2, 3, 4, 5]"
f[0] // => 1
f[1] = 10
interface{}型
interface{}型はGoにおけるあらゆる型と互換性のある特殊な型であり、Goの柔軟性を担保するための重要な機能である。初期値として<nil>
という特殊な値をとる。
var x interface{}
x = 1
x = 3.14
x = "hello"
x = [...]uint8{1, 2, 3}
全ての型と互換性のあるinterface{}型を用いると、上のように様々な型を引数としてとることができるため、動的に変数の型をチェックすることができる。これを型アサーションといい、x.(T)
のように構成される。
var x interface{} = 3
i := x.(int)
f := x.(float64) // => error
i, isInt := x.(int) // i == 3, isInt == true
f, isFloat64 := x.(float64) // f == 3.0, isFloat64 == false
またswitch文を用いて型アサーションと分岐を組み合わせた処理が簡単にできる。
switch x.(type) {
case bool:
fmt.Println("bool")
case int, uint:
fmt.Println("int or uint")
default:
fmt.Println("don't know")
}
関数
関数はfunc 関数名(引数の定義) 戻り値型 { 関数本体 }
のように定義する。
// 基本
func plus(x, y int) int {
return x + y
}
plus(1, 2) //=> 3
// 戻り値のない関数
func hello() {
fmt.Println("Hello!")
return
}
// 複数の戻り値
func div(a, b int) (int, int) {
q := a / b
r := a % b
return q, r
}
q, r = div(19, 7)
q, _ = div(19, 7) // 戻り値の破棄
_, r = div(19, 7) // 戻り値の破棄
無名関数は、関数というものをある種の「値」として表現したものとみなせる。関数を値として表現できるのであれば、ある関数が関数を引数に取ることも、関数を返す関数を書くことも自在にできる。
func (引数の定義) 戻り値型 { 関数本体 }
のように定義する。
// 無名関数
f := func(x, y int) int { return x + y }
f(2, 3) // == 5
// 名前付関数と無名関数
func plus(x, y int) int {
return x + y
}
var plusAlias = plus
plusAlias(10, 5) // == 15
// 関数を返す関数
func returnFunc() func() {
return func() {
fmt.Println("I'm a function.")
}
}
f := returnFunc()
f() // => "I'm a function."
returnFunc()() // => "I'm a function."
// 関数を引数にとる関数
func callFunction(f func()) {
f()
}
callFunction(func() {
fmt.Println("I'm a function.")
}) // => "I'm a function."
定数
Goの定数は**型なし定数(untyped)と型あり定数(typed)**の2つに分かれている。定数の値に型を与える場合は、定数名の後に型を書くことで明示的に型あり関数を定義できる。
const X = 1
const (
X = 1
Y = 2
)
const (
X int64 = -1
Y float64 = 1.2
)
スコープ
Goのプログラムは複数のパッケージを組み合わせて構成される。各々のパッケージ間で定数や関数を共有するためにパッケージ下に定義された識別子を他のパッケージから参照できるようにしたり、逆にパッケージの内部のみで利用する識別子であれば他のパッケージから隠蔽したりなど、それぞれの識別子の可視範囲をコントロールする必要がある。
パッケージに定義された定数、変数、関数などが他のパッケージから参照可能であるかは、識別子の1文字目が大文字であるかどうかで決定される。
// foo.go
package foo
const (
MAX = 100
internal_const = 1
)
func FooFunc(n int) int {
return internalFunc(n)
}
func internalFunc(n int) int {
return n + 1
}
// main.go
package main
import "foo"
foo.MAX // => 100
foo.internal_const // => error
foo.FooFunc(5) // => 6
foo.internalFunc(5) // => error
if文
x := 5
if x == 1 {
fmt.Println("It's one!")
}
if x == 0 {
fmt.Println("It's zero!")
} else if x > 0 {
fmt.Println("It's positive!")
} else {
fmt.Println("It's negative!")
}
if x, y := 1, 2; x < y {
fmt.Println("y is greater than x.")
}
for文
// 裸のforは無限ループを構成する
for {
fmt.Println("infinite roop")
}
// ループを中断するためのbreak
i := 0
for {
fmt.Println(i)
i++
if i == 100 {
break
}
}
// 条件付きfor
i := 0
for i < 100 {
fmt.Println(i)
i++
}
// 古典的for
for i := 0; i < 100; i++ {
fmt.Println(i)
i++
}
// 次のループにスキップするためのcontinue
for i := 0; i < 100; i++ {
if (i % 2 == 1) {
continue
}
fmt.Println(i)
i++
}
// 範囲節rangeによるfor
fruits := [3]string{"apple", "banana", "grape"}
for i, fruit := range fruits {
}
// ラベル付for文
LOOP:
for {
for {
for {
fmt.Println("start")
break LOOP
}
}
}
fmt.Println("end")
switch文
n := 3
switch n {
case 1, 2:
fmt.Println("1 or 2")
case 3, 4:
fmt.Println("3 or 4")
default:
fmt.Println("others")
}
// 簡易文の使用
switch n := 2; n {
case n > 0 && n < 3:
fmt.Println("1 or 2")
default:
fmt.Println("others")
}
go文
go文は並列処理を司る特別な機能である。Goはスレッドよりも小さい処理単位である**ゴルーチン(goroutine)**が並行して動作するように実装されている。go文はこのゴルーチンを新たに生成して並行して処理される新しい処理の流れをランタイムに追加するための機能である。
func sub() {
for {
fmt.Println("sub loop")
}
}
func main() {
go sub() // start goroutine
for {
fmt.Println("main loop")
}
}
参照型 - スライス slice
スライスはGoで最も使用頻度の高いデータ構造で、いわゆる可変長配列を表現する型である。
var s1 []int
s2 := make([]int, 3)
s2[0] = 5
fmt.Println(s2) // => "[5, 0, 0]"
s3 := []int{1, 2, 3, 4, 5}
s4 := s3[0:2] // => "[1, 2, 3]"
s3 := s3[len(s3)-2] // => "[4, 5]"
また、appendを用いて要素を追加することが可能。
s := []int{1, 2, 3}
s = append(s, 4) // s == "[1, 2, 3, 4]"
s = append(s, 5, 6, 7) // s == "[1, 2, 3, 4, 5, 6, 7]"
s1 := []int{8, 9, 10}
s = s.append(s, s1) // s == "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]"
またスライス式には、3つのパラメータを取る完全スライス式というものがある。a[low:high:max]
と指定し、このときlen(a) == high - low
、cap(a) == max - low
となる。
a := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
s1 := a[2:4] // == "[3, 4]"
len(s1) // == 2
cap(s1) // == 8
s2 := a[2:4:4] // == "[3, 4]"
len(s2) // == 2
cap(s2) // == 2
s3 := a[2:4:6] // == "[3, 4]"
len(s3) // == 2
cap(s3) // == 4
要素数(len)と容量(capacity)
例えば、make([]int, 5, 10)
ならば、要素数は5なので、[n]
によるインデックスで参照・代入できる範囲は0から4に制限される。しかし、容量としては10個分のint型を格納できる領域が内部的に確保されているため、スライスの要素数を拡張して行く際に、メモリ上の新たな領域を確保する必要がない。
要素数と容量が同じになったスライスを拡張するときは、Goのランタイムは元の容量10より大きなメモリ領域を確保して、元のスライスが格納していたデータを丸ごと新しい領域へコピーする。しかし、あまりに容量の拡張が頻繁に発生するという状況は実行効率上は好ましいことではないため、実行効率のよいプログラムを書くためには、メモリ領域の変動を最低限に抑えることが大切。
s1 := make([]int, 5)
len(s1) // == 5
cap(s1) // == 5
s2 := make([]int, 5, 10)
len(s2) // == 5
cap(s2) // == 10
スライスと可変長引数
func sum(s ...int) int {
n := 0
for _, v := range s {
n += v
}
return n
}
sum(1, 2, 3) // == 6
sum(1, 2, 3, 4, 5) // == 15
sum() // == 0
a := []int{1, 2, 3}
sum(a...) // == 6
参照型としてのスライス
配列型を引数にとった関数の呼び出しでは引数は**値渡し(call by value)によってコピーされるが、参照型であるスライスを関数の引数に使った場合は参照渡し(call by reference)**されるという特徴がある。
func pow_a(a [3]int) {
for i, v := range a {
a[i] = v * v
}
return
}
func pow_s(s []int) {
for i, v := range s {
s[i] = v * v
}
return
}
a := [3]int{1, 2, 3}
s := []int{1, 2, 3}
pow(a)
pow(s)
a // == "[1, 2, 3]"
s // == "[1, 4, 9]"
参照型 - マップ map
マップはいわゆる「連想配列」に類するデータ構造である。map[キーの型]要素の型
という書き方で定義する。
// 定義
var m map[int]string
m := make(map[int]string)
// 代入
m[1] = "US"
m[81] = "Japan"
m[86] = "China"
fmt.Println(m) // == map[1: US, 81: Japan, 86: China]
// 定義・代入
m := map[string]string{"Yamada": "Taro", "Suzuki"; "Ichiro"}
// 参照
s := m["Yamada"]
s, ok = m["Sato"]
if _, ok := m["Sato"]; ok {
}
// for
for k, v := range m {
}
// 削除
delete(m, "Yamada")
参照型 - チャネル channel
Goプログラムは非同期に複数実行されるゴルーチンが効率的に動作するようにデザインされている。チャネルは、このゴルーチンとゴルーチンの間でデータの受け渡しを司るためにデザインされたデータ構造である。
チャネルの型名はchan [データ型]
のように書く。また、<-chan
は受信専用チャネル、chan<-
は送信専用チャネルを表す。chan
は受信も送信も可能な双方向のチャネルとして機能する。
var ch1 chan int
var ch2 <-chan int
var ch3 chan<- int
チャネルはキューの性質を備えるデータ構造である。チャネルのバッファとはこのキューを格納する領域であり、バッファサイズとはこのキューのサイズであると見なすことができる。
ch := make(chan int)
ch8 := make(chan int, 8) // バッファサイズ8のチャネル
チャネルが保持するデータに対する操作は「送信」と「受信」の2パターンのみ。送受信共に演算子<-
を使用する。
ch := make(chan int, 10)
ch <- 5 // チャネルに整数5を送信
i: = <-ch // チャネルから整数値を受信
実例は以下のようになる。
package main
import (
"fmt"
"time"
)
func receive(name string, ch <-chan int) {
for {
i, ok := <-ch
if ok == false {
break
}
fmt.Println(name, i)
}
fmt.Println(name + " is done.")
}
func main() {
ch := make(chan int, 20)
go receive("1st goroutine", ch)
go receive("2st goroutine", ch)
go receive("3st goroutine", ch)
i := 0
for i < 100 {
ch <- i
i++
}
close(ch)
time.Sleep(3 * time.Second)
}
1st goroutine 1
1st goroutine 3
1st goroutine 4
...
1st goroutine 98
1st goroutine 99
1st goroutine is done.
3st goroutine 90
3st goroutine is done.
2st goroutine is done.
ポインタ
ポインタとは**値型(value type)**に分類されるデータ構造のメモリ上のアドレスと型の情報。Goではこれを使ってデータ構造を間接的に参照・操作できる。
ポインタ型は*int
のように、ポインタを使って参照・操作する型の前に*
を置くことで定義できる。また、アドレス演算子&
を使って任意の型からそのポインタ型を生成することができる。
演算子*
をポインタ型の変数の前に置くことで、ポインタ型が保持するメモリ上のアドレスを経由してデータ本体を参照することができる。
var ip *int // ポインタ型
var fp *float64
var ap *[3]string
var i int // 値型(int型)
p := &i // ポインタ型
i = 5
fmt.Println("%T\n", p) // => "*int"
fmt.Println(*p) // => "5"
a := &[3]int{1, 2, 3}
p := &a
a[1] // => 2
任意の型のアドレスを保持するポインタの性質を利用すれば、関数の引数へ値型の参照渡しができる。
func inc(p *int) {
p++
}
i := 1
inc(&i)
inc(&i)
inc(&i)
fmt.Println(i) // => "4"
typeによる型エイリアス
type [定義する型] [既存の型]
のように書くことで、すでに定義されている型から新しい型を定義することができる。
type MyInt int
var n1 MyInt = 5
n2 := MyInt(7)
type (
AreaMap map[string][2]float64
)
amap := AreaMap{"Tokyo": {35.689, 139.691}}
構造体 struct
構造体とは「複数の任意の型の値を1つにまとめたもの」である。Javaなどのオブジェクト指向言語におけるクラス・オブジェクトの定義が重要であるように、Goにおける構造体の定義はGoプログラミングにおいて重要な位置を占める。
構造体を使用するには一般的にtype
と組み合わせて新しい型を定義する。構造体は、structで定義された構造体に、typeを使って新しい型名を与えるという順序で定義する。構造体型の変数を定義すると構造体に定義されている各フィールドに必要なメモリ領域が確保され、それぞれのフィールドは型に合わせた初期値をとる。
type Point struct {
X int
Y int
}
type Point struct {
X, Y int
}
var pt Point
pt.X = 10
pt.X // == 10
pt.Y // == 0
pt2 := Point{1, 2}
pt3 := Point{X: 1, Y: 2}
構造体に構造体を含めることができる。また、下のようにa.Feed.Amount
とするべきところを、a.Amount
としているように、Goの構造体では、フィールド名を省略して埋め込まれた構造体のフィールド名が一意に定まる場合に限り、中間フィールド名を省略してアクセスできる。これは、異なる構造体型に共通の性質を持たせるという局面で有効である。
type Feed struct {
Name string
Amount uint
}
type Animal struct {
Name string
Feed
}
a := Animal{
Name: "Monkey",
Feed: Feed{
Name: "Banana",
Amount: 10
}
}
a.Name // == "Monkey"
a.Feed.Name // == "Banana"
a.Feed.Amount // == 10
a.Amount // == 10
また、構造体は値型なので関数の引数に構造体を渡した場合は、構造体のコピーが生成され、その構造体が関数によって処理される。そのため、構造体型を関数に参照渡しする必要がある。このパターンはGoプログラミングにおいて頻出である。
type Point struct {
X, Y int
}
func swap(p *Point) {
x, y = p.X, p.Y
p.X = y
p.Y = x
}
p := Point{X: 1, Y: 2}
swap(&p)
p.X // == 2
p.Y // == 1
指定した型のポインタ型を生成するために組み込み関数newが用意されている。new([型])
という形式で使用する。newを使った構造体型のポインタ生成と、アドレス演算子&を伴った複合リテラルによる構造体型のポインタ生成の間には、動作上ほとんど違いがないため、プログラムの状況に応じて使い分けるのが良い。
type Point struct {
X, Y int
}
p1 := new(Point)
p1.X = 1
p1.Y = 2
p2 := &Point{X: 1, Y: 2}
メソッド Method
Goにはメソッドという特徴的な機能がある。メソッドといってもオブジェクト指向プログラミング言語によくあるメソッドとは違い、 Goのメソッドは任意の型に特化した関数を定義するための仕組みである。
メソッド定義では、関数とは異なりfuncとメソッド名の間に**レシーバー(receiver)**の型とその変数名が必要になる。func (p *Point) MethodName()
のように書き、この場合は*Point
型の変数p
がレシーバーとなる。そして、型に定義されたメソッドは、[レシーバー].[メソッド]
という形式で呼び出すことができる。変数p
が指す、*Point
型へのポインタがレシーバーに該当する。
これにより型特有の関数が定義できる。オブジェクト指向言語でいうインスタンス関数である。
type Point struct{
X, Y int
}
func (p *Point) Distance(dp *Point) float64 {
x, y = p.X - dp.X, p.Y - dp.Y
return math.Sqrt(float64(x*x + y*y))
}
p := &Point{X: 0, Y:0}
p.Distance(&Point{X: 1, Y:1}) // == 1.4142...
// 通常の関数の定義
// func plus(x, y int) int {
// return x + y
// }
また、Goにはオブジェクト指向プログラミング言語に見られるコンストラクタ(constructor)という機能はないが、慣例的に型のコンストラクタというパターンを利用する。New[型名]
のように命名し、構造体User型
に対する初期化のための関数であるコンストラクタは、NewUser
となる。また、返り値は対象の型のポインタ型にするのが望ましい。※プライベートの場合はnewUser
というように先頭を小文字にする。
type User struct {
Id int
Name string
}
func NewUser(id int, name string) *User {
u := new(User)
u.Id = id
u.Name = name
return u
}
user := NewUser(1, "Taro") // => "&{1 Taro}"
インターフェース interface
インターフェースとは、Goプログラミングにおける型の柔軟性を担保する非常に重要な機能である。インターフェースは型の一種であり、任意の方がどのようなメソッドを実装するべきかを規定するための枠組みである。
実例として、Goの組み込み型errorはインターフェースとして定義されている。interface {}
のように定義し、error型では、文字列を返すメソッドErrorのみが定義されている。Goでは、エラーが発生する可能性がある関数やメソッドの戻り値としてerror型が頻出する、そしてこれらはerrorインターフェースによって隠蔽されている。このような仕組みのおかげで、Goのエラー処理はerror型をどのように処理するかという点に共通化されている。
type error interface {
Error() string
}
type MyError struct {
Message string
ErrCode int
}
func (e *MyError) Error() string {
return e.Message
}
func RaiseError error {
return MyError{Message: "An error has occurred.". ErrCode: 1234}
}
err :+ RaiseError()
err.Error() // == "An error has occurred."
インターフェースに関しては、この記事に詳しく書かれている。 インタフェースの実装パターン #golang
最後に
Go言語の勉強を兼ねて文法等をまとめて見ました。まだ入門段階なので、訂正や補足等あればコメント等いただけるとありがたいです。今後、Go言語を実際の開発で使っていく予定なので、その段階で感じたことをこの記事にフィードバックしていけたらいいなと思っています。
ブログもやっているのでぜひ。
https://muscle-programmer.hatenablog.com/