30のコードで学ぶ 速習 Go言語


前書き

本稿は、Go言語 の基本文法を網羅したチュートリアルです

基本的には単なる文法のリファレンスですが、分かりやすくするために 30個のGo言語コードを添付しています

とは言え、ほぼ A Tour of Go の内容を踏襲しているので、そちらを参照した方が分かりやすいかもしれません

対象読者

  • Go言語初心者
  • Go言語の文法を復習したい人

環境構築

公式サイト からバイナリをダウンロードしてインストールしても良いのですが、ここでは Docker を用いた環境構築を行います

Environment

  • OS:
    • Ubuntu 18.04(Linux系OSを推奨)
      • 本サイトではDocker開発用OS LinuxNight を公開しております
  • Docker: 19.03.5
    • DockerCompose: 1.24.0

Installation

ターミナルで以下のコマンドを実行してください

# Docker構成ファイルをダウンロード&解凍
$ wget -O - https://github.com/amenoyoya/docker-collection/releases/download/0.2.1/golang.tar.gz | tar xzvf -

# golangディレクトリに移動
$ cd golang

# Go言語(on Docker)のバージョン確認
$ docker-compose run --rm go version
go version go1.13.5 linux/amd64

## 初回起動時のみ Docker イメージのダウンロードとコンテナビルドに少し時間がかかる

Usage

基本的に上記 Docker コンテナは、起動して何かをやるというものではなく、コマンドとして実行することを想定しています

そのため docker-compose rungoコンテナを goコマンドとして実行し、実行後はコンテナを削除する(--rm オプション)という流れになります

ここでは例として、hello.go というソースファイルを作成して、これをスクリプトとして実行してみましょう

hello.go
package main

import "fmt"

func main() {
  fmt.Println("Hello, world")
}
# go run <ソースファイル> を実行
$ docker-compose run --rm go run hello.go
Hello, world

30のコードで学ぶ 速習 Go言語

01. Hello, world

何はともあれ、Hello, world を実行して、Go言語コードの大体の構成を理解してみましょう

01_hello.go
// パッケージ: main
// Go言語の各ソースファイルは必ずいずれかのパッケージに所属している必要がある
package main

// fmtパッケージを使う
// fmt.Println 関数を使えるようにする
import "fmt"

/**
 * エントリーポイント(最初に実行されるプログラム)は main関数として定義する
 * 関数定義:
 *	func <関数名> ([引数]) {
 *		<処理内容>
 * 	}
 */
func main() {
  // fmt.Println 関数: 引数に渡された値をコンソールに出力する
  fmt.Println("Hello, world")
}
$ docker-compose run --rm go run 01_hello.go
Hello, world

02. 現在日時を取得する

fmt, time という2つのパッケージを使い、現在日時をコンソールに出力します

02_time.go
package main

// 複数パッケージをインポートする場合、以下のような書き方ができる
// もちろん import 文を2つ書いても良い
import (
	"fmt"
	"time"
)

// エントリーポイント
func main() {
	// "Welcome to the playground!" を表示
	fmt.Println("Welcome to the playground!")

	// time.Now(): 現在日時を取得
	fmt.Println("The time is", time.Now())
}
$ docker-compose run --rm go run 02_time.go
Welcome to the playground!
The time is 2020-01-02 23:06:59.074249272 +0900 JST m=+0.000089952

03. 乱数表示

Go言語で乱数を使うには mathパッケージ内にある randパッケージを使う必要があります

こういった場合、import "math" で読み込んだ後 math.rand.XXX という形で rand パッケージ内の関数を呼び出しても良いのですが、import "math/rand" という読み込み方をすれば rand.XXX という形で関数を呼び出すことができます

03_rand.go
package main

import (
	"fmt"
	"time"
	"math/rand" // mathパッケージ内のrandパッケージ利用
)

func main() {
	// 乱数の種はデフォルトでは 1 で固定となっているため、何回プログラムを実行しても同じ乱数値が返ってくる
	// => 変更する場合は rand.Seed(seed int64) で任意の種を与える必要がある
	rand.Seed(time.Now().UnixNano()) // 現在日時をナノ秒に変換した値を種にする

	// rand.Intn(n int) int: 0..n-1 の整数乱数を返す
	fmt.Println("My favorite number is", rand.Intn(10))
}
$ docker-compose run --rm go run 03_rand.go
My favorite number is 4 # 0~9の任意の数値が出力される

04. 書式付き文字列表示

C言語における printf と同様、Go言語にも fmt.Printf という書式付き文字列表示関数が組み込まれています

使える書式としては以下のようなものがあります

  • 論理値(bool): %t
  • 符号付き整数(int, int8など): %d
  • 符号なし整数(uint, uint8など): %d
  • 浮動小数点数(float64など): %g
  • 複素数(complex128など): %g
  • 文字列(string): %s
  • チャネル(chan): %p
  • ポインタ(pointer): %p
  • デフォルト書式: %v
04_printf.go
package main

import (
	"fmt"
	"math"
)

func main() {
	/**
	 * fmt.Printf(<書式付き文字列>, ...): 第2引数以降に指定された値が、第1引数で指定された書式に変換されて表示される
	 * なお、fmt.Printf は自動的に改行しないため、文字列の末尾に改行文字 '\n' を指定したほうが良い
	 */
	// math.Sqrt(x float64) float64: xの平方根を返す
	fmt.Printf("Now you have %g problems.\n", math.Sqrt(7))
}
$ docker-compose run --rm go run 04_printf.go
Now you have 2.6457513110645907 problems.

05. Go言語のパッケージ

Go言語では最初の文字が大文字で始まる名前が、外部パッケージから参照可能な名前となっています

例えば Pimathパッケージでエクスポートされておりますが、pi はエクスポートされていません

05_exported.go
package main

import (
	"fmt"
	"math"
)

func main() {
	// math.pi は undefined エラーになる
	// fmt.Println(math.pi)
	fmt.Println(math.Pi)
}
$ docker-compose run --rm go run 05_exported.go
3.141592653589793

06. 関数定義

Go言語で関数を定義する場合は以下のように記述します

func <関数名> ( [<引数> <引数型名>] ) <戻り値型名> {
  <関数内部処理>
}
06_function.go
package main

import "fmt"

// add関数: 2引数 x, y をとり x + y の計算結果を返す
// ※ 関数の2つ以上の引数が同じ型である場合は最後の型を残して省略できる
//    func add(x, y int) int {...}
func add(x int, y int) int {
	return x + y
}

// 関数は複数の戻り値を返すことも可能
// swap関数: 2引数 x, y をとり y, x を返す
func swap(x, y string) (string, string) {
	return y, x
}

// Goでは戻り値となる変数に名前をつけることも可能
// split関数: 1引数 sum をとり x(= sum * 4 / 9), y(= sum - x) を返す
func split(sum int) (x, y int) {
	x = sum * 4 / 9
	y = sum - x
	return // 戻り値のある関数の最後には必ず return 文を入れる必要がある
}

func main() {
	fmt.Println(add(42, 13)) // => 42 + 13 = 55 が表示される
	fmt.Println(swap("Hello", "World")) // "World", "Hello" の順で表示される
	fmt.Println(split(17)) // 7, 10 が表示される
}
$ docker-compose run --rm go run 06_function.go
55
World Hello
7 10

07. 変数と型

Go言語では var ステートメントで変数を宣言できます

var <変数名> <型名> [= <初期値>]

Go言語の基本型(組み込み型)は以下の通りです

  • bool (true/false)
  • string (文字列型)
  • int, int8, int16, int32, int64 (整数型)
  • uint, uint8, uint16, uint32, uint64 (符号なし整数型)
  • byte (uint8 の別名 = 8ビット符号なし整数型)
  • rune (int32 の別名 = 32ビット整数型: Unicodeのコードポイントを表す)
  • float32, float64 (浮動小数点型)
  • complex64, complex128 (複素数型)
07_variables.go
package main

import "fmt"

// bool型変数 c, python, java の宣言
// 同じ型の変数を複数宣言する場合は、最後の変数にのみ型名を指定すれば良い(一つずつに型名を指定しても良い)
// 初期値を与えずに宣言するとゼロ値が与えられる(0, false, "" など)
var c, python, java bool

func main() {
	// int型変数 i: main関数の中でのみ使用可能
	var i int
	// c, python, java は main関数の外で宣言されているが、main関数内部で参照可能(クロージャ)
	fmt.Println(i, c, python, java)

	// 変数宣言時に初期化子(=)により値を代入できる
	var x, y int = 1, 2
	// 初期化子が与えられている場合、型は省略しても良い
	var z = "ゼット"

	fmt.Println(x, y, z)
}
$ docker-compose run --rm go run 07_variables.go
0 false false false
1 2 ゼット

08. 変数への値の割り当て

以下のような構文で、変数宣言と初期化子を短縮して書くことができます

<変数名> := <初期値>

// 上記は以下と同じ意味
var <変数名> = <初期値>

ただし、変数宣言の短縮形は、関数内部でのみ使用可能です

関数の外側で宣言するグローバル変数は var キーワードが必須になります

08_shortvar.go
package main

import "fmt"

func main() {
	// 以下は var i, j int = 1, 2 と同等
	i, j := 1, 2

	fmt.Println(i, j)
}
$ docker-compose run --rm go run 08_shortvar.go
1 2

09. 型変換

以下の構文で型を変換することができます

<型名> ( <変数 | > )
09_cast.go
package main

import (
	"fmt"
	"math"
)

func main() {
	// var x, y int = 3, 4
	x, y := 3, 4

	// f = √(x^2 + y^2) => 64ビット浮動小数点型に型変換
	var f float64 = math.Sqrt(float64(x*x + y*y))

	// z = uint型に型変換した f
	// なお var ステートメントで宣言した型と初期値の型が一致しない場合エラーとなる
	var z uint = uint(f)

	fmt.Println(x, y, f, z)
}
$ docker-compose run --rm go run 09_cast.go
3 4 5 5

10. 定数

var ステートメントで宣言する変数は、「変数」というだけあって初期値以外の値を再代入することができます

一方で、初期値以外の値にできないようにするには const キーワードで定数宣言する必要があります

10_const.go
package main

import "fmt"

// 定数 Pi: 3.14
const Pi = 3.14

func main() {
	// 定数に別の値を代入することはできない
	Pi = 4.13
}
$ docker-compose run --rm go run 10_const.go
10_const.go:10:5: cannot assign to Pi

11. for ループ

Go言語のループ処理は、C言語に似ており、以下のような構文となります

for [初期化ステートメント]; [条件式]; [後処理ステートメント] {
  <ループ処理>
}

ループの流れとしては以下のようになります

  1. まず初期化ステートメントが実行され、条件式が true なら最初のループ処理が実行される
  2. 後処理ステートメントが実行され、条件式が true なら2回目のループ処理が実行される
  3. 条件式が false になるまで、後処理ステートメント => ループ処理 が繰り返し実行される
11_for.go
package main

import "fmt"

func main() {
	sum := 0
	// i = 0 から始まり i < 10 の条件に合致する限り sum に1を加算し続ける(i は1ずつ加算される)
	// => 10回ループ
	for i := 0; i < 10; i++ {
		sum += 1 // sum++ でも可
	}
	fmt.Println(sum)

	// Goに while 文はなく、初期化ステートメントと後処理ステートメントを省略した for 文で代替される
	// ※条件式も省略すると無限ループになる
	sum = 1
	for sum < 1000 {
		sum += sum // sum に sum を加算 => sum が倍々になる
	}
	fmt.Println(sum)
}
$ docker-compose run --rm go run 11_for.go
10
1024

12. 条件分岐

条件分岐は if, else if, else 構文で制御できます

if [評価ステートメント;] <条件式> {
  <条件合致時の処理>
}
[else if <条件式> { ... }]
[else { ... }]

// 評価ステートメントでは条件式用の変数宣言を行うことができるif, else if, else 式の内部でのみ使用可能な変数
12_if.go
package main

import (
	"fmt"
	"math"
)

// pow関数: x^n を計算し、それが lim1 未満なら x^n を返し、lim1 以上 lim2 未満なら lim1 を返し、lim2 以上なら lim2 を返す
func pow(x, n, lim1, lim2 float64) float64 {
	if v := math.Pow(x, n); v < lim1 {
		return v
	} else if lim1 <= v && v < lim2 {
		return lim1
	} else {
		return lim2
	}
}

func main() {
	fmt.Println(
		pow(3, 2, 18, 40),
		pow(3, 3, 18, 40),
		pow(3, 4, 18, 40), // 最後の , を忘れないように!
	)
}
$ docker-compose run --rm go run 12_if.go
9 18 40

13. switch 条件分岐

switch - case ステートメントを用いて if - else ステートメントを短く書くことができます

switch [評価ステートメント;] <評価対象変数> {
case <合致対象> :
  <条件合致時の処理>
[default: <条件に合致しない場合の処理>]
}

Go言語の switch 文では、他の言語と異なり、該当した case のみ実行され、合致対象も定数である必要はありません

また、評価対象変数も数値型である必要はありません

13_switch.go
package main

import (
	"fmt"
	"runtime"
	"time"
)

func main() {
	fmt.Print("Go runs on ")
	// 実行OSを判定
	switch os := runtime.GOOS; os {
	case "darwin":
		fmt.Println("OS X.")
	case "linux":
		fmt.Println("Linux.")
	default:
		fmt.Printf("%s.", os)
	}

	// switch の評価対象を省略すると switch true と同一のステートメントになる
	// この構文は冗長になりがちな if - else if - else 文をシンプルに表現するために用いられる
	switch t := time.Now(); {
	case t.Hour() < 12:
		fmt.Println("おはよう")
	case t.Hour() < 17:
		fmt.Println("こんにちは")
	default:
		fmt.Println("こんばんは")
	}
}
$ docker-compose run --rm go run 13_switch.go
Go runs on Linux.
おはよう # <- 時間帯により変化

14. defer 遅延実行

defer <関数>([引数]) ステートメントにより関数を遅延実行することができます

defer へ渡した関数の引数はすぐに評価されますが、その関数自体は呼び出し元関数が終了するまで実行されません(デストラクタっぽい挙動を実現できます)

14_defer.go
package main

import "fmt"

func hello() {
	// 以下の関数は hello関数終了時に呼びされる
	// => "hello" "world" の順に表示される
	defer fmt.Println("world")
	
	// 以下の関数が先に実行される
	fmt.Println("hello")
}

func count() {
	// defer へ渡した関数が複数ある場合、それらはスタックへ積まれ、LIFO(last-in-first-out)の順で実行される
	for i := 0; i < 10; i++ {
		defer fmt.Println(i)
	}
	fmt.Println("counting")
}

func main() {
	hello()
	count()
}
$ docker-compose run --rm go run 14_defer.go
hello
world
counting
9
8
7
6
5
4
3
2
1
0

15. ポインタ

Go言語にはC言語同様、ポインタの概念が存在します

ポインタとは、値が格納されているメモリのアドレスを指し示すものです

変数 T のポインタは *T 型で、そのゼロ値は nil となります

var p *int = nil

ある値を格納している変数のポインタを引き出すには & オペレータを使います

i := 42
p = &i

ポインタの指す先の値を取得するには * オペレータを使います

fmt.Println(*p) // ポインタ p を通して i から値を読み出す
15_pointer.go
package main

import "fmt"

func main() {
	i, j := 42, 2701

	p := &i // 変数 i へのポインタ
	fmt.Println(*p) // ポインタ p を通して i から値を読み出す => 42

	p = &j // 変数 j へのポインタ
	*p = *p / 37 // ポインタ p を通して j の値を j / 37 に変更
	fmt.Println(j) // j の値が 2701 / 37 = 73 になっている
}
$ docker-compose run --rm go run 15_pointer.go
42
73

16. 構造体

struct(構造体)は複数の変数をまとめた、フィールド集合体であり、以下のように宣言されます

type <構造体名> struct {
  <フィールド>
}

フィールドに値を入れて構造体を実体化させる場合は <構造体名>{<フィールド値>} でインスタンス化します

16_struct.go
package main

import "fmt"

// 変数 X, Y をもつ Vertex 構造体
type Vertex struct {
	X int
	Y int
}

func main() {
	// X: 1, Y: 2 の Vertex構造体をインスタンス化
	v := Vertex{1, 2}
	fmt.Println(v)

	// v のフィールド X に 4 を代入
	v.X = 4
	fmt.Println(v)

	// 構造体のポインタ
	p := &v

	// ポインタの示す先にある構造体のフィールドにアクセスする場合 (*p).X のように書く
	// ただし Go では、上記表記法は面倒であるため p.X と書いても同一の挙動をするように設計されている
	(*p).X = 1e3 // ポインタ p を通して v.X に 1000 を代入
	p.Y = 1e2 // ポインタ p を通して v.Y に 100 を代入
	fmt.Println(v)

	// var は () をつけると複数行で宣言できる
	var (
		// 構造体インスタンス化の際、特定のフィールドのみに値を割り当てることも可能
		v1 = Vertex{X: 1} // X: 1, Y: ゼロ値 の Vertex をインスタンス化
		v2 = Vertex{} // X: ゼロ値, Y: ゼロ値 の Vertex をインスタンス化
		pv = &Vertex{1, 2} // X: 1, Y: 2 の Vertex インスタンスへのポインタ
	)
	fmt.Println(v1, v2, pv)
}
$ docker-compose run --rm go run 16_struct.go
{1 2}
{4 2}
{1000 100}
{1 0} {0 0} &{1 2}

17. 配列

T 型の変数を n 個もつ配列(Array)は [n]T で表現されます

また、配列 ai 番目の変数にアクセスする際は a[i] でアクセスします

17_array.go
package main

import "fmt"

func main() {
	// string型 2つからなる配列 a を宣言
	var a [2]string
	// Goの配列のインデックスは 0 番からはじまるため、配列 a のインデックスは 0, 1 の2つがある
	a[0] = "Hello"
	a[1] = "World"
	fmt.Println(a)
	
	// 配列を初期化子つきで宣言する場合は {<初期値>} ステートメントを使う
	primes := [6]int{2, 3, 5, 7, 11, 13}
	fmt.Println(primes)

	// 配列の長さは len 関数で取得できる
	fmt.Println(len(primes))
}
$ docker-compose run --rm go run 17_array.go
[Hello World]
[2 3 5 7 11 13]
6

18. スライス

配列は固定長ですが、スライスは可変長です

基本的には配列と同じように宣言しますが、配列の長さは指定せず []T 型で表現されます

スライスは配列への参照のようなものであり、それ自身は一切のデータをもちません(もとの配列の部分列を指し示しているだけ)

したがって、スライスを作成する際は、配列のどの部分を切り取るか low, high の境界を指定する必要があります

// slice: []T, array: [n]T
slice := array[low : high] // 配列の low 番目 high-1 番目high 番目は含まれないのスライスを作成

スライスは配列への参照のようなものであるため、そのゼロ値はポインタと同様 nil となります

18_slice.go
package main

import "fmt"

func main()  {
	primes := [6]int{2, 3, 5, 7, 11, 13}

	// 配列 primes の 1 〜 4 - 1 (= 3) 番目のスライスを作成
	var s []int = primes[1 : 4] // {3, 5, 7}
	fmt.Println(s)

	// 配列同様、スライスの長さは len 関数で取得できる
	// スライス s は primes[1] 〜 primes[3] のスライスであるため、長さは 3
	fmt.Println(len(s))

	// スライスには容量という概念があり、スライスの最初の要素から数えて、元となる配列の要素数を容量と呼ぶ
	// スライスの容量は cap 関数で取得できる
	// スライス s は primes[1] 〜 のスライスであり、配列 primes の長さは 6 であるため、容量は 6 - 1 = 5 となる
	fmt.Println(cap(s))

	// スライスはもとの配列への参照であるため、スライスに変更を加えるともとの配列にも変更が反映される
	s[0] = 30 // s: {primes[1]: 3, primes[2]: 5, primes[3]: 7} => {primes[1]: 30, primes[2]: 5, primes[3]: 7}
	fmt.Println(s, primes)

	// スライスは長さを持たない配列として宣言することも可能
	// (実際は、宣言された長さの配列を作って、その配列の頭から尾までのスライスを切り取っている)
	// 以下の場合 [3]bool{true, false, true} という配列を(内部的に)作って、その配列の [0 : 3] のスライスを作成していることになる
	r := []bool{true, false, true}
	fmt.Println(r)

	// 配列をスライスするとき、上限値または下限値を省略すると、それらの既定値が使用される
	// 下限の既定値: 0, 上限の既定値: 配列(スライス)の長さ
	fmt.Println(primes[:3]) // = primes[0 : 3] => {2, 30, 5}
	fmt.Println(primes[3:]) // = primes[3 : len(primes)] => {7, 11, 13}

	// スライスは make 関数でも作成することができ、その各要素はゼロ値となる
	//   make(型名, 長さ, 容量)
	a := make([]int, 3) // [3]int{0, 0, 0} という配列に対する [0 : 3] のスライスを作成
	b := make([]int, 0, 3) // 長さ: 0, 容量: 3 のスライスを作成

	fmt.Println(len(a), cap(a))
	fmt.Println(len(b), cap(b))

	// スライスに新しい要素を追加する際は append 関数を使う
	// append 関数は、必要なサイズのスライスを新たに作成し、元のスライスと新規要素を順番に copy する、という挙動をとるため、割と遅い
	a = append(a, 1) // a: {0, 0, 0} => {0, 0, 0, 1}
	fmt.Println(a)

	// なお、スライスから要素を削除する関数は用意されていない
}
$ docker-compose run --rm go run 18_slice.go
[3 5 7]
3
5
[30 5 7] [2 30 5 7 11 13]
[true false true]
[2 30 5]
[7 11 13]
3 3
0 3
[0 0 0 1]

19. range による反復処理

for ループでは range キーワードでスライスやマップ等の要素を逐次反復処理することができます

for <インデックス変数>, <要素変数> := range <スライス等> {
  <反復処理>
}
19_range.go
package main

import "fmt"

var pow = []int{1, 2, 4, 8, 16, 32, 64}

func main() {
	// 以下の for ループは
	//   for i := 0; i < len(pow); i++ { v := pow[i]; .... }
	// と等価
	for i, v := range pow {
		fmt.Printf("2^%d = %d\n", i, v)
	}

	// 使わない値は _ へ代入することで捨てることも可能
	// 以下はインデックスが不要で、スライスの各要素の値のみ必要な場合の処理
	for _, v := range pow {
		fmt.Println(v)
	}
}
$ docker-compose run --rm go run 19_range.go
2^0 = 1
2^1 = 2
2^2 = 4
2^3 = 8
2^4 = 16
2^5 = 32
2^6 = 64
1
2
4
8
16
32
64

20. マップ

他の言語における連想配列(辞書配列)と同じものがGo言語にも存在し、map 型として定義されています

map [ <キーの型> ] <値の型>

マップはキーと値を関連づけるものであり、そのゼロ値は nil です

マップを作成する場合は make 関数を使うか、初期化子 {<キー>: <値>, ...} を用いて宣言します

20_map.go
package main

import "fmt"

// float64型の Lat, Long フィールドをもつ Vertex 構造体
type Vertex struct {
	Lat, Long float64
}

func main() {
	// キーが文字列で値がVertex型であるマップを作成
	m := make(map[string]Vertex)

	// m のキー "Bell Labs" に Vertex{40.68433, -74.39967} を代入
	m["Bell Labs"] = Vertex{40.68433, -74.39967}

	fmt.Println(m)

	// 初期化子つきでマップを作成
	m2 := map[string]Vertex{
		"Bell Labs": Vertex{40.68433, -74.39967},
		"Google": Vertex{37.42202, -122.08408},
	}
	fmt.Println(m2)

	// マップのキーと関連する値を削除する場合は delete 関数を使う
	//   delete(<マップ変数>, <削除対象キー>)
	delete(m2, "Bell Labs")
	fmt.Println(m2)

	// 特定のキーが存在するかどうかは、要素取得時の第2戻り値で確認可能
	_, ok := m2["Bell Labs"]
	fmt.Println("m2 has key 'Bell Labs':", ok)
}
$ docker-compose run --rm go run 20_map.go
map[Bell Labs:{40.68433 -74.39967}]
map[Bell Labs:{40.68433 -74.39967} Google:{37.42202 -122.08408}]
map[Google:{37.42202 -122.08408}]
m2 has key 'Bell Labs': false

21. 関数型

Go言語では関数も変数として扱われ、無名関数(関数名を持たない関数: func([引数]){...})も使えます

関数値は、関数の引数に取ることも、戻り値として利用することも可能です

21_functional.go
package main

import (
	"fmt"
	"math"
)

// compute関数: float64型引数を2つとりfloat64型の値を返す関数を引数にとる
//   引数に指定された関数に対して、引数 3, 4 を与えて実行する
func compute(fn func(float64, float64) float64) float64 {
	return fn(3, 4)
}

// adder関数: ローカル変数 sum に x を加算する関数を生成
func adder() func(int) int {
	sum := 0
	// Goの関数はクロージャであるため関数の外部で宣言された変数をキャプチャなしで参照できる
	return func(x int) int {
		sum += x
		return sum
	}
}

func main() {
	// 変数 hypot に無名関数を代入
	hypot := func(x, y float64) float64 {
		return math.Sqrt(x*x + y*y)
	}
	// √5^2 + 12^2 = 13
	fmt.Println(hypot(5, 12))
	// √3^2 + 4^2 = 5
	fmt.Println(compute(hypot))
	// 3^4 = 81
	fmt.Println(compute(math.Pow))

	// pos 関数と neg 関数を生成
	pos, neg := adder(), adder()
	// 10回ループ
	for i := 0; i < 10; i++ {
		fmt.Println(
			pos(i), // 0 + 1 + 2 + ... + 9
			neg(-2 * i), // 0 + (-2) + (-4) + ... + (-18)
		)
	}
}
$ docker-compose run --rm go run 21_functional.go
13
5
81
0 0
1 -2
3 -6
6 -12
10 -20
15 -30
21 -42
28 -56
36 -72
45 -90

22. メソッド

Go言語に class の概念は存在しません

代わりに、構造体に対してメソッドを定義することができます

func (<インスタンス名> <構造体型>) <メソッド名> ([引数]) <戻り値型> {
  <メソッド内部処理>
}

ただし、構造体とメソッドは同じパッケージ内で定義されている必要があります

なお func と メソッド名 の間に記述する引数(<インスタンス名> <構造体型>)をレシーバ引数と呼びます

22_method.go
package main

import (
	"fmt"
	"math"
)

// フィールド X, Y を持つ Vertex 構造体
type Vertex struct {
	X, Y float64
}

// Vertex 構造体に Abs メソッドを定義
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
}

/**
 * メソッドは構造体以外の任意の型(type で定義される型)に対しても定義できる
 */
// float64型の別名で Float 型を定義
type Float float64

// Float 型に Abs メソッドを定義
func (f Float) Abs() Float {
	if f < 0 {
		return -f
	}
	return f
}

func main() {
	// X: 3, Y: 4 の Vertex インスタンス化
	v := Vertex{3, 4}
	// メソッド呼び出し: <構造体インスタンス>.<メソッド名>([引数])
	fmt.Println(v.Abs())
	// v の X, Y を 2倍する
	v.Scale(2)
	fmt.Println(v)

	// -0.1 の Float 型
	f := Float(-0.1)
	fmt.Println(f.Abs())
}
$ docker-compose run --rm go run 22_method.go
5
{6 8}
0.1

23. インタフェース

同じメソッド名を持つ別の構造体を同じように扱うための機能として Interface があります

type <インタフェース名> interface {
    <メソッド名>([引数]) <戻り値型>
    ...
}

メソッドを一つも持たないインタフェースは “空のインタフェース” と呼ばれ、任意の値を保持することができます

type Any interface {}
23_interface.go
package main

import "fmt"

// Greeter インタフェース: Hello メソッドを持つ
type Greeter interface {
	Hello()
}

// hello関数: 引数に渡された構造体の Hello メソッドを実行
func hello(greeter Greeter) {
	greeter.Hello()
}

// Human 構造体: Hello メソッドは "私は {name} です" と出力
type Human struct {
	Name string
}
func (human Human) Hello() {
	fmt.Printf("私は %s です\n", human.Name)
}

// Cat 構造体: Hello メソッドは "ニャー" と出力
type Cat struct {}
func (cat Cat) Hello() {
	fmt.Println("ニャー")
}

func main() {
	// Greeter インタフェースは、Hello メソッドを持つ任意の構造体をインスタンス化できる
	var greeter Greeter = Human{"yoya"} // Name: "yoya" の Human 構造体を Greeter インタフェースを通じてインスタンス化
	// greeter.Hello() を実行
	hello(greeter)

	// greeter に Cat 構造体のインスタンスを代入
	greeter = Cat{}
	// greeter.Hello() を実行
	hello(greeter)
}
$ docker-compose run --rm go run 23_interface.go
私は yoya です
ニャー

24. 型アサーション

型アサーションは、インターフェースの値の基になる具体的な値を利用する手段を提供します

例えば、あるインタフェースの値 i が、具体的な型 T の値を保持している場合に、その値を変数 t に代入したい場合

t := i.(T)

と記述します

このとき、iT 型の値を保持していない場合、この文は panic を起こします

なお、型アサーションは第2戻り値に、型を保持しているかどうかの真偽値を格納するので、panic を起こさないようにするには、この真偽値を確認すれば良いです

t, ok := i.(T)

if !ok {
  // エラー処理
}
24_assertion.go
package main

import "fmt"

func main() {
	// 任意型(空のインタフェース)の変数 i に "hello" という文字列を代入
	var i interface{} = "hello"
	
	// i が文字列型の値を保持している場合、変数 s にその値を代入
	s := i.(string)
	fmt.Println(s)

	s, ok := i.(string)
	fmt.Println(s, ok)

	// i が浮動小数点型の値を保持している場合、変数 f にその値を代入
	// 変数 ok には、浮動小数点型を保持しているかどうかの真偽値を代入
	f, ok := i.(float64)
	fmt.Println(f, ok)

	// 以下の文は panic を引き起こす
	// panic が起こると以降の処理は実行されない
	//f = i.(float64)
	//fmt.Println(f)

	// 型アサーションは switch 文と組み合わせることも可能
	switch value := i.(type) {
	case string:
		fmt.Printf("\"%s\" は文字列型です\n", value)
	case int:
		fmt.Printf("%d は整数型です\n", value)
	case float64:
		fmt.Printf("%f は浮動小数点型です\n", value)
	default:
		fmt.Println(value, "は知らない型です")
	}
}
$ docker-compose run --rm go run 24_assertion.go
hello
hello true
0 false
"hello" は文字列型です

25. エラー処理

Go言語には例外処理機構はなく、エラーの状態を error 型で表現します

error 型は組み込みのインタフェースで、以下のように定義されています

type error interface {
  Error() string
}

上記のように errorインタフェースは Errorメソッドでエラー内容を返します

また基本的に、エラーが起こりうる関数は error 変数を返します

呼び出し元はエラーが nil かどうかを確認することでエラーをハンドル(取り扱い)することになります

i, err := strconv.Atoi("42") // 文字列を整数に変換 => 変換後の値と error 値を返す
if err != nil {
  // エラーが起こった場合の処理: エラー内容を出力
  fmt.Printf("couldn't convert number: %v\n", err)
  return
}
fmt.Println("Converted integer:", i)
25_error.go
package main

import (
	"fmt"
	"time"
)

// 新しいエラー型: MyError
type MyError struct {
	When time.Time // いつエラーが起こったか
	What string // エラー文
}
// エラー型は Error() string メソッドを持つ必要がある
func (e *MyError) Error() string {
	// いつ、どんなエラーが起きたかを文字列として返す
	return fmt.Sprintf("at %v, %s", e.When, e.What)
}

// run 関数: わざとエラーを返す
func run() error {
	return &MyError{time.Now(), "it didn't work"}
}

func main() {
	if err := run(); err != nil {
		// エラーが起きた場合、そのエラー値を出力
		fmt.Println(err)
	}
}
$ docker-compose run --rm go run 25_error.go
at 2020-01-03 00:55:41.415075888 +0900 JST m=+0.000051409, it didn't work

26. goroutine による並列処理

Go言語は、並列処理を行うための機構として goroutine という軽量なスレッドシステムを持っています

go f(x, y, z)

上記のように記述すれば新しいスレッドが起動し、メイン処理と並列的に f(x, y, z) 関数が実行されます

ただし、goroutine は同じアドレス空間で実行されるため、共有メモリへのアクセスは必ず同期を取る必要があります

26_goroutine.go
package main

import (
	"fmt"
	"time"
)

// say 関数: 引数に与えられた文字列を0.1秒毎に5回出力する
func say(s string) {
	for i := 0; i < 5; i++ {
		time.Sleep(100 * time.Millisecond)
		fmt.Println(s)
	}
}

func main() {
	// say 関数を並列実行
	go say("world")
	
	// 上記とは別に say 関数を実行
	say("hello")
}
$ docker-compose run --rm go run 26_goroutine.go
hello
world
world
hello
world
hello
hello
world
hello

27. チャネル

goroutine でデータの同期をとる方法の1つとしてチャネル(Channel)があります

Channel 型は、チャネルオペレータ(<-)を用いて値の送受信を行う通り道の役割を果たします

ch <- v    // データ v をチャネル ch に送信
v := <- ch // チャネル ch が受信したデータを変数 v に割り当てる

チャネルを用いたデータ伝搬は矢印の方向(右から左)へ流れますが、通常、片方がデータが準備完了になるまで送受信はブロックされます

この性質を利用して goroutine におけるデータの同期ができるという仕組みです

なお、チャネルはマップやスライス同様、make 関数で生成する必要があります

ch := make(chan <型名>)
27_channel.go
package main

import "fmt"

// sum 関数: int配列の値をすべて加算し、チャネル c に送信する
func sum(s []int, c chan int) {
	sum := 0
	for _, v := range s {
		sum += v
	}
	c <- sum // sum の値をチャネル c に送信
}

func main() {
	// 計算対象の int配列
	s := []int{7, 2, 8, -9, 4, 0}

	// int型チャネル生成
	c := make(chan int)

	// s[0 : 2] と s[3 : 5] に対して sum 関数をそれぞれ並列実行
	// => これにより逐次実行より高速に sum 関数を実行できる
	go sum(s[: len(s) / 2], c)
	go sum(s[len(s) / 2 :], c)

	// チャネルを通して計算結果を受信
	// チャネルはスタックされるため、以下のように複数の値を受信可能
	x, y := <-c, <-c

	// x + y を計算すれば sum(s, c) と同じ計算結果になる
	fmt.Println(x, y, x + y)
}
$ docker-compose run --rm go run 27_channel.go
-5 17 12

28. バッファチャネル

チャネルは、バッファとして配列のような使い方も可能です

バッファを持つチャネルを初期化するには、make 関数の2つ目の引数にバッファの長さを与えます

ch := make(chan <型名>, <バッファの長さ>)

バッファが詰まった時(指定した長さまでデータが入った時)は、チャネルへの送信はブロックされます

また、バッファが空の時には、チャネルへの受信がブロックされます

明示的にチャネルへの送信を close することも可能ですが、必ず送信側のチャネルを close するよう注意が必要です(close したチャネルにデータを送信すると panic が起きる)

28_fibonacci.go
package main

import "fmt"

// fibonacci 関数: n までのフィボナッチ数列をバッファチャネルに送信
func fibonacci(n int, c chan int) {
	x, y := 0, 1
	for i := 0; i < n; i++ {
		c <- x // バッファチャネルに x を送信
		x, y = y, x + y
	}
	// チャネルへの送信を終了する
	// 今回の場合 for range ループでバッファチャネルを処理するため、明示的に close する必要がある
	close(c)
}

func main() {
	// 10個のデータを受け入れられるバッファチャネルを作成
	c := make(chan int, 10)

	// バッファチャネルの容量分のフィボナッチ数列を生成(並列実行)
	go fibonacci(cap(c), c)
	// フィボナッチ数列のデータが受信されるたびに、その数値を出力
	for v := range c { // バッファチャネルに対して for range ループを実行
		fmt.Println(v)
	}
}
$ docker-compose run --rm go run 28_fibonacci.go
0
1
1
2
3
5
8
13
21
34

29. select チャネル選択

select ステートメントは、複数の case のいずれかが準備完了するまでチャネルをブロックします

複数の case が準備完了になった場合は、ランダムに選択されます

select {
case [<変数> :=] <- <チャネル> :
    <指定チャネルが準備完了した時の処理>
[default: <どのチャネルも準備完了していない時の処理>]
}
29_select.go
package main

import (
	"fmt"
	"time"
)

func main() {
	// time.Tick(n): n秒ごとの実行プロセスを作成
	tick := time.Tick(100 * time.Millisecond) // 0.1秒ごとの処理

	// time.After(n): n秒後の実行プロセスを作成
	boom := time.After(500 * time.Millisecond) // 0.5秒後の処理

	// 無限ループ
	for {
		select {
		case <-tick:
			// 0.1秒ごとに "tick." を出力
			fmt.Println("tick.")
		case <-boom:
			// 0.5秒後に "BOOM!" を出力してループ終了
			fmt.Println("BOOM!")
			return
		default:
			// 上記のいずれのチャネルも準備されていない場合は "    ." を出力して 0.05秒待機
			fmt.Println("    .")
			time.Sleep(50 * time.Millisecond)
		}
	}
}
$ docker-compose run --rm go run 29_select.go
    .
    .
tick.
    .
    .
tick.
    .
    .
tick.
    .
    .
tick.
    .
    .
BOOM!

30. sync.Mutex による排他制御

goroutine でデータ同期をするためにチャネルは有用ですが、1つのデータに対して排他制御を行いたい場合は sync.Mutex ライブラリを使った方が良い場合もあります

func (sync.Mutex) Lock()   // データをロックし、排他制御を開始する
func (sync.Mutex) Unlock() // データをアンロックしデータへの書き込みを可能にする
30_mutex.go
package main

import (
	"fmt"
	"sync"
	"time"
)

// SafeCounter: 任意のキー文字列に紐づくカウントを排他的に制御可能な構造体
type SafeCounter struct {
	v   map[string]int
	mux sync.Mutex // 排他制御用ミューテックス
}

// SafeCounter.Inc メソッド: 指定文字列に紐づくカウントを進める
// SafeCounter のフィールドに変更を加えるため、レシーバ引数は SafeCounter のポインタにする
func (counter *SafeCounter) Inc(key string) {
	counter.mux.Lock()   // 並列実行される他のプロセスから書き込みされるのを防ぐため、データロック
	counter.v[key]++     // 指定キーのカウントをインクリメント
	counter.mux.Unlock() // 排他制御を終了
}

// SafeCounter.Value メソッド: 指定文字列に紐づくカウント値を取得
func (counter *SafeCounter) Value(key string) int {
	counter.mux.Lock()
	// カウント値を返した後にデータをアンロックするため defer で遅延実行
	defer counter.mux.Unlock()
	// カウント値を返す
	return counter.v[key]
}

func main() {
	counter := SafeCounter{v: make(map[string]int)}
	// "somekey" に紐づくカウントを1000回分進める(並列実行)
	for i := 0; i < 1000; i++ {
		go counter.Inc("somekey")
	}
	// 1秒待機
	time.Sleep(time.Second)
	// 1秒待機した後、並列実行されていた Inc メソッドにより、どこまでカウント値が進んでいるか確認
	fmt.Println(counter.Value("somekey"))
}
$ docker-compose run --rm go run 30_mutex.go
1000
Avatar
Ameno Yoya
Webプログラマ

経験はログに残して初めて資産となる

comments powered by Disqus