【エンジニアリング基礎】Go言語の基礎文法

本稿では,A Tour of Goを読んで学んだことを整理してまとめています。

目次

Basics

本章では,Go言語の基礎を学びます。

Packages

コードはpackageという単位で管理されます。先頭でpackage宣言をすることでファイルをpackage化することができる他,外部パッケージをimportすることもできます。

package main

import (
	"fmt"
	"math"
)

なお,下記のような記述方法もできます。

import "fmt"
import "math"

以下では,コードを簡潔に表すためpackage宣言とimport文を省略することがあります。

Exported names

外部パッケージから関数や変数を参照する場合は,変数名の先頭を大文字にします。

import (
	"fmt"
	"math"
)

func main() {
	fmt.Println(math.Pi)
}

Declaration

変数名の後ろに型名を指定します。これはC言語の弱みを克服するもので,左から右へとコードを読むことができるように配慮された宣言方法です。

var s string

関数の引数に同じ型が連続する場合は,最後の型を残して省略できます。

var c, python, java bool

宣言と初期化を同時に行うことも可能です。

var i, j int = 1, 2

import宣言と同様に,まとめて変数宣言を行うことも可能です。

var (
  c, python, java bool = true
  i, j int = 1, 2
)

定数を宣言することもできます。

const Pi = 3.14

Functions

返り値の型は関数名の直後に指定します。

func add(x int, y int) int {
  return x + y
}

変数宣言同様,関数は複数の型を返すこともできます。

func swap(x, y string) (string, string) {
	return y, x
}

関数内に限り,var宣言の代わりに暗黙的な型宣言ができます。

var i, j int = 1, 2
k := 3
c, python, java := true, false, "no!"

関数も変数として扱うことができます。

hypot := func(x, y float64) float64 {
	return math.Sqrt(x*x + y*y)
}
fmt.Println(hypot(5, 12)) // 13

Named return values

戻り値に名前を付けると,関数内での変数宣言を行なってくれます。return文に何も指定しないと名前を付けた戻り値が返りますが,可読性の観点から長い関数では推奨されません。

func split(sum int) (x, y int) {
	x = sum * 4 / 9
	y = sum - x
	return
}

Basic types

基本型(プリミティブ型)は次の通りです。

概要組み込み型
真偽bool
文字列string
符号なし整数uint uint8 (byte) uint16 uint32 uint64 uintptr
符号付き整数int int8 int16 int32 (rune) int64
小数float32 float64
複素数complex64 complex128
Goの組み込み型

runeは古代文字を表す言葉で,Unicodeのコードポイント一文字分を表します。また,uintptはポインタを表しますが,符号なし整数として定義されています。

基本型は型変換の関数として利用することもできます。

var i int = 42
var f float64 = float64(i)
var u uint = uint(f)

右側の変数が型を持っている場合は,左側の変数は同じ型になります。

var i int
j := i // jはint

右側が型を指定しない数値である場合は,左側の変数は代入された数値の精度に基づいて型推論します。

i := 42           // int
f := 3.142        // float64
c := 0.867 + 0.5i // complex128

Zero values

変数を初期化しない場合は,ゼロ値と呼ばれる値が変数に格納されます。ゼロ値は基本型によって異なります。

var b bool      // false
var s string    // ""
var i uint      // 0
var i int       // 0
var f float64   // 0
var c complex64 // (0+0i)

Flow control statements

本章では,Go言語におけるフロー制御文を学びます。

For

forステートメントは,下記のように記述されます。

for {初期化}; {条件式}; {後処理} {
}

Goのfor文では,他の主要な言語とは異なりforステートメントを()で括りません。

sum := 0
for i := 0; i < 10; i++ {
	sum += i
}

初期化と後処理は省略できます。

sum := 0
for ; sum < 1000; {
	sum += sum
}

初期化と後処理は省略した場合,セミコロンも省略できます。

sum := 0
for sum < 1000 {
	sum += sum
}

この記述方法は,他言語におけるWhileに相当します。

条件式を省略した場合は,無限ループになります。

for {
}

If

for文と同様に,Go言語のif文では条件式を()で括りません。

func sqrt(x float64) string {
	if x < 0 {
		return sqrt(-x) + "i"
	}
	return fmt.Sprint(math.Sqrt(x))
}

ifステートメントでは,if-else文内を関数内とする変数を宣言することも可能です。

func pow(x, n, lim float64) float64 {
	if v := math.Pow(x, n); v < lim {
		return v
	} else {
		fmt.Printf("%g >= %g\n", v, lim)
	}
	// vはここでは使えない
	return lim
}

Switch

Go言語のSwitch文は,他の言語とは異なり該当するcaseだけを実行し,後続のcaseは実行されません。それに伴って,breakステートメントは自動で追加されます。

switch os := runtime.GOOS; os {
case "darwin":
	fmt.Println("OS X.")
case "linux":
	fmt.Println("Linux.")
default:
	fmt.Printf("%s.\n", os)
}

条件式を省略することで,長いif-else文を簡潔に表すことができます。

t := time.Now()
switch {
case t.Hour() < 12:
	fmt.Println("Good morning!")
case t.Hour() < 17:
	fmt.Println("Good afternoon.")
default:
	fmt.Println("Good evening.")
}

Defer

Deferステートメントは,渡された関数を呼び出し元がreturnするまで遅延させます。

// hello → world の順番
func main() {
	defer fmt.Println("world")
	fmt.Println("hello")
}

deferを複数用いる場合は,LIFO(last-in-last-out)に従って実行されます。

// 4 → 3 → 2 → 1 → 0 の順番
func main() {
	fmt.Println("counting")

	for i := 0; i < 5; i++ {
		defer fmt.Println(i)
	}

	fmt.Println("done")
}

More types

本章では,組み込み型以外のデータ型を学びます。

Pointers

ポインタは,値が格納されているメモリのアドレスのことを指します。Go言語では,変数Tのポインタは*Tは表されます。ポインタの型も*Tと表され,ゼロ値はnilとなります。&を用いることで,その値へのポインタを取得できます。

var p *int
fmt.Println(p)  // nil

var i int = 42
p = &i          // iへのポインタを取得
fmt.Println(p)  // 0xc000012028
fmt.Println(*p) // 42

*p = 21
fmt.Println(i)  // 21

Struct

オブジェクト指向言語におけるClassに相当する概念が,Go言語におけるStruct(構造体)です。

type {構造体名} struct {
  {field名} {field型}
}

typeという宣言は型を定義しています。すなわち,{構造体名}という名前の型を定義しています。typeはstruct以外にも独自の型を定義する際に利用することができます。

Structの初期化は,オブジェクト志向言語のように直感的に行うことができます。

type Vertex struct {
	X, Y int
}

var v1 = Vertex{1, 2}

一部のフィールドを初期化することもできます。

var v2 = Vertex{X: 1}

Structへはポインタを経由してアクセスすることができますが,Go言語では記法を省略してオブジェクト指向言語におけるフィールドへのアクセスと同様に操作することができます。

func main() {
  v := Vertex{1, 2}
  p := &v
  (*p).X = 3 // p.X = 3と書ける
}

Arrays

[n]Tは,型Tがn個格納された配列を表します。配列は固定長で,ゼロ値は全ての要素が0の配列です。

var a [10]int // 10個のintから構成される固定帳配列

Slices

[]Tは,型Tのスライスを表します。スライスは可変長で,ゼロ値はnilです。

var s []int = a[1:4]

配列への参照となるため,スライスの要素を変更すると,元の配列の要素も変更されます。

names := [4]string{
	"John",
	"Paul",
	"George",
	"Ringo",
}
fmt.Println(names)  // [John Paul George Ringo]

a := names[0:2]
b := names[1:3]
fmt.Println(a, b)   // [John Paul] [Paul George]

b[0] = "XXX"
fmt.Println(a, b)   // [John XXX] [XXX George]
fmt.Println(names)  // [John XXX George Ringo]

スライスの指定方法では,上限・下限・およびその両方を省略することができます。

// 下記は全て等価
a[0:10]
a[:10]
a[0:]
a[:]

スライスの要素数を長さとよび,スライスの最初の要素から参照元の配列の最後までの要素数を容量とよびます。

s := []int{2, 3, 5, 7, 11, 13}
fmt.Println(len(s), cap(s)) // 6 6

s = s[1:4]
fmt.Println(len(s), cap(s)) // 3 5

組み込み関数makeを利用することで,ゼロで初期化された配列の参照としてスライスを作ることができます。

// make([]{型名} {長さ} {容量})
s := make([]int, 0, 5)
fmt.Println(len(s), cap(s)) // 0 5

スライスには,任意の型を含めることができます。

s := [][]string{
  []string{"a", "b", "c"},
  []string{"A", "B", "C"}
}

組み込み関数appendを利用することで,スライスに新しい要素を追加することができます。appendした際にスライスの容量を超えてしまった場合は,新たに容量を増やした配列を確保し,その配列への参照としてスライスを新たに返します。拡張数する容量は,要素の型やメモリの割り当て方によって異なります。

s := make([]int, 0, 3)
s = append(s, 1)
fmt.Println(s, len(s), cap(s)) // [1] 1 3

s = append(s, 2, 3, 4)
fmt.Println(s, len(s), cap(s)) // [1 2 3 4] 4 6

組み込み関数rangeを利用することで,スライスのインデックスと要素を逐次取り出すことができます。

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

for i, v := range pow {
  fmt.Printf("2**%d = %d\n", i, v)
}

/*
2**0 = 1
2**1 = 2
2**2 = 4
2**3 = 8
2**4 = 16
*/

rangeの返り値の受け取り方は,片方を_で受け取ることで捨てることができ,2つ目の値も省略することができます。

for i, _ := range pow
for _, value := range pow
for i := range pow

Map

mapはkeyとvalueのペアを保持します。

map[{keyの型名}]{Valueの型名}

スライスと同様に,組み込み関数のmakeを利用できます。

type Vertex struct {
	Lat, Long float64
}

m = make(map[string]Vertex)
m["Bell Labs"] = Vertex{40.68433, -74.39967}
fmt.Println(m["Bell Labs"]) // {40.68433 -74.39967}

Mapに対して,下記のような操作が可能です。

m := make(map[string]int)

// 代入
m["Answer"] = 42
// 削除
delete(m, "Answer")
// 取得および存在判定
v, ok := m["Answer"]

Closure

Go言語では,下記のような性質を満たす関数を作ることができます。

  • 関数の中で関数が定義できる
  • 内側の関数をそのまま返すことができる
  • 内側の関数で外側の関数内で定義された変数を操作できる
  • 外側の関数内で変数の状態を保持する

内側の関数をそのまま返すため,内側の関数には名前を付ける必要がありません。そのため,内側の関数は匿名関数または無名関数とよばれています。また,匿名関数を利用して,ある関数が宣言された時点での変数の状態を保持するような仕組みをクロージャとよびます。関数内という特定のスコープ内で変数を囲い込むことからクロージャとよばれています。例えば,下記のcounterという関数は,countのクロージャを実現しています。

func counter() func() int {
    count := 0 // 囲い込む変数
    return func() int { // 無名関数
        count++ // 囲い込んだ変数へのアクセス
        return count
    }
}

func main() {
    c := counter()   // cは関数
    fmt.Println(c()) // 1
    fmt.Println(c()) // 2
    fmt.Println(c()) // 3
}

Methods and Interface

本章では,メソッドとインタフェースについて学びます。

Methods

Go言語では,型にメソッドを定義できます。

// 通常の関数は「func 関数名(引数)戻り値の型」である点に注意
func (レシーバ 型) 関数名(引数)戻り値の型 {
}

メソッドはレシーバが同一パッケージに属さないと定義できません。

例えば,構造体にメソッドを定義すると下記のようになります。

type Vertex struct {
  X, Y float64
}

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
}

Pointer reciever

レシーバには,値レシーバとポインタレシーバの二種類があります。

// 値レシーバ
func (v Vertex) ScaleValue(f float64) {
	v.X = v.X * f
	v.Y = v.Y * f
}

// ポインタレシーバ
func (v *Vertex) ScalePointer(f float64) {
	v.X = v.X * f
	v.Y = v.Y * f
}

値レシーバはレシーバのコピーに対して操作を行う一方で,ポインタレシーバでは参照元の変数に対して操作を行うことができます。

v := Vertex{3, 4}

v.ScaleValue(10)     // コピーに対して操作するため元のvは変わらない
fmt.Println(v.Abs()) // 5

v.ScalePointer(10)   // ポインタに対して操作するため元のvは変わる
fmt.Println(v.Abs()) // 50

値レシーバとポインタレシーバは,元の変数に対する挙動が異なるため,混用するべきではありません。

Pointers and functions

Go言語の値レシーバとポインタレシーバには,コンパイル時に暗黙的な型変換を行う機能が備わっています。通常の関数として定義した場合,引数で値とポインタの指定を間違えるとコンパイルエラーを起こします。

// 通常の関数として定義
func Abs(v Vertex) float64 {
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

// 通常の関数として定義
func Scale(v Vertex, f float64) {
	v.X = v.X * f
	v.Y = v.Y * f
}

func main() {
	v := Vertex{3, 4}
	Scale(&v, 10) // compile error
}

値レシーバおよびポインタレシーバにおいては,コンパイラが暗黙的に型変換を行なってくれます。

var v Vertex
p := &v

v.ScaleValue(5)  // 値レシーバなので,もちろんOK
p.ScaleValue(10) // 値レシーバなのに,暗黙的な型変換により「(*p).ScaleValue(5)」と解釈されてOK

v.ScalePointer(5)  // ポインタレシーバなのに,暗黙的な型変換により「(&v).ScalePointer(5)」と解釈されてOK
p.ScalePointer(10) // ポインタレシーバなので,もちろんOK

Interfaces

メソッドの集合をインタフェースとよび,インタフェース型の変数では指定されたメソッドを持つことができます。インタフェースを型に実装することになりますが,他の言語とは異なり,明示的な宣言は必要ありません。

// メソッドMを実装する必要のあるインタフェースIを定義
type I interface {
	M()
}

// Sをフィールドとして持つ構造体Tを定義
type T struct {
	S string
}

// インタフェースIの全てのメソッド(ここではMのみ)を実装しているため,インタフェースを暗黙的に実装している
// つまり構造体TはインタフェースIを実装したということ
func (t T) M() {
	fmt.Println(t.S)
}

func main() {
  // インタフェース型の変数iに対してインタフェースIを実装したTを代入する
	var i I = T{"hello"}
  // インタフェースが持つメソッドを呼び出す
	i.M() // "hello"
}

インタフェースに何のメソッドも指定しないこともできます。その場合,任意の型が属するインタフェースを定義することができます。このような空のインタフェースは,未知の型を扱うケースで利用されます。

インタフェースの実体は,型と値のペアです。したがって,インタフェースは型ごとに定義することができます。

// ポインタ型に対して定義
func (t *T) M() {
	fmt.Println(t.S)
}

// float型に対して定義
type F float64
func (f F) M() {
	fmt.Println(f)
}

func main() {
  // インタフェース型の変数の宣言
	var i I

  // Tのポインタ型に対するインタフェースの実装の呼び出し
	i = &T{"Hello"}
	i.M() // "Hello"

  // float型に対するインタフェースの実装の呼び出し
	i = F(math.Pi)
	i.M() // 3.141592653589793
}

Go言語では他の言語とは異なり,インタフェースを実装したメソッドの引数(レシーバ)にnilが入ってきても正常に処理することが可能です。

func main() {
	var i I

	var t *T
	i = t // nilが代入される
	i.M() // 構造体Tのポインタには M() メソッドが定義されているためエラーにはならない
}

インタフェース自体は何も具体的な値を保持しているわけではないため,インタフェースからメソッドを呼び出そうとすると,いわゆるヌルポ(Goの場合はニルポ?)が発生します。

// もしTがM()を実装していない場合
func main() {
	var i I
	i.M() // ランタイムエラー(invalid memory address or nil pointer dereference)
}

Type assertions

空のインタフェースを利用すると,元の型の情報が欠落してしまいます。そこで,元の型情報を判別するための方法として,Go言語では型アサーションという機能を有しています。

value, ok := {変数}.({型})

型アサーションを用いることで,型判定を下記のように行うことができます。

var i interface{} = "hello"

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

f, ok := i.(float64)
fmt.Println(f, ok) // 0 false

okがfalseだった場合に,第二引数を受け取っていないとエラーが起きてしまいます。

f = i.(float64) // panic: interface conversion: interface {} is string, not float64

型アサーションとswitch文を組み合わせることで,型チェックを直列に行うことができます。

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)       // Twice 21 is 42
	do("hello")  // "hello" is 5 bytes long
	do(true)     // I don't know about type bool!
}

具体例として,fmtパッケージで定義されているStringerインタフェースがあります。

type Stringer interface {
    String() string
}

fmtパッケージの仕様として,fmt.Printlnを呼び出したときにstringメソッドが自動で呼び出されますので,このインタフェースを実装することでfmt.Printlnの出力をカスタマイズすることができます。

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}
	fmt.Println(a) // Arthur Dent (42 years)
}

Errors

Go言語のエラーは組み込み型のインタフェースとして定義されています。

type error interface {
    Error() string
}

例えば,MyErrorという構造体で組み込み型のErrorインタフェースを実装すると,下記のようになります。

// 自分のカスタムエラー型を定義
type MyError struct {
	message string
}

// MyError型にError()メソッドを追加してErrorインターフェースを実装
func (e *MyError) Error() string {
	return e.message
}

Go言語では,関数の返り値を二つ受け取り,errがnilかどうかを判別することでエラーハンドリングします。

// 関数を定義してカスタムエラーを返す例
func divide(a, b int) (int, error) {
	if b == 0 {
		return 0, &MyError{"ゼロで割ることはできません"}
	}
	return a / b, nil
}

func main() {
	result, err := divide(10, 2)
	if err != nil {
		fmt.Println("エラー:", err)
	} else {
		fmt.Println("結果:", result)
	}

	result, err = divide(10, 0)
	if err != nil {
		fmt.Println("エラー:", err)
	} else {
		fmt.Println("結果:", result)
	}
}

Readers

ioパッケージ内のReadメソッドを用いることで,データのストリーム処理が可能になります。

r := strings.NewReader("Hello, Reader!")

b := make([]byte, 8)
for {
	n, err := r.Read(b) // 8 byteごとに読み込み
	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
	}
}
/*
n = 8 err = <nil> b = [72 101 108 108 111 44 32 82]
b[:n] = "Hello, R"
n = 6 err = <nil> b = [101 97 100 101 114 33 32 82]
b[:n] = "eader!"
n = 0 err = EOF b = [101 97 100 101 114 33 32 82]
b[:n] = ""
*/

Concurrency

本章では,Go言語で同時並列処理を実現する方法を学びます。

Goroutines

Go言語には,Coroutineとよばれる同時実行のための軽量スレッド機能があります。Goで書かれた全てのプログラムはgoroutineを持ちますので,main関数も一つのgoroutineになります。下記の例では,main関数のgoroutineの中で,別のgoroutineを呼び出しています。しかしながら,二つのSay関数が実行完了する前にmain関数が実行完了してしまうため,何も出力されません。

func Say(s string) {
  fmt.Println(s)
}

// 何も出力されない
func main() {
  go Say("hello")
  go Say("world")
}

WaitGroupを利用することで,二つのgoroutineが完了するまでmain関数の終了を待つことができます。

func Say(s string, wg *sync.WaitGroup) {
  defer wg.Done() // WaitGroupを1つ終了
  fmt.Println(s)
}

// 「hello world」または「world hello」のいずれかが出力される
func main() {
  var wg sync.WaitGroup

  wg.Add(2) // WaitGroupを2つ追加

  go Say("hello", &wg)
  go Say("world", &wg)

  wg.Wait() // 2つのgoroutineが終了するのを待つ
}

Channels

複数のgoroutineの実行フローを制御するために,Go言語ではChannelという型が利用されます。イメージとしては,キューのようなものだと捉えておけばよいでしょう。通常,片方のgoroutineがchannelからデータを受信する場合は,もう片方のgoroutineがchannelにデータを送信するまで待ちます。この挙動により,goroutineでは明示的なロックや条件指定がなくても,同期処理が可能になります。

channelは組み込み関数makeを利用することで作成でき,送受信は下記のように行います。

// channel作成
ch := make(chan string)
// channelにdataを送信する
ch <- data
// channelから受信した値をdataに格納する
data := <-ch

channelを用いることで二つのgoroutineが並列処理を行うことを可能にしています。新しいgoroutineとmain関数のgoroutineが並列に実行されますが,main関数のgoroutineは受信から開始するため,まずは新しいgoroutineの送信から実行されます。main関数のgoroutineが受信を行います。すると,再びmain関数と新しいgoroutineが並列実行されますが,新しいgoroutineは受信を開始するため,結局main関数の送信が先に実行されます。したがって,最後に新しいgoroutineの受信が行われます。

func Say(c chan string) {
	data := <-c // 受信
	fmt.Println(data)
	data = "GO"
	c <- data   // 送信
}

/*
「go GO」が出力される
*/
func main() {
  ch := make(chan string)

  go func(c chan string) {
    data := "go"
    c <- data  // 送信
    data = <-c // 受信
    fmt.Println(data)
  }(ch)
  Say(ch)
}

Buffer

channelの容量はbufferとよばれ,bufferを超える数のデータがchannelに送信された場合はエラーが起こります。シンプルに,bufferはキューのサイズのことを指していると捉えればよいでしょう。

func main() {
	ch := make(chan int, 2)
	ch <- 1
	ch <- 2
	ch <- 3 // fatal error: all goroutines are asleep - deadlock!
}

Close

受け手に対してchannelが使われなくなることを明示的に示したい場合は,channelをcloseすることができます。逆に,errと同様に,受け手はchannelが閉じているかどうかを第二引数で知ることができます。

close(c)      // channelのclose
v, ok := <-ch // okにはfalseが返る

Select

channelの状態を複数記述したい場合は,switch文に相当するselect文を利用することができます。

select {
case <-ch1:      // ch1から受信したときに実行(変数代入なし)
case v := <-c2:  // ch2から受信したときに実行(変数代入あり)
case ch3 <- y:   // ch3に送信したときに実行
default:         // どのcaseにも該当しないときに実行
}

複数のcaseが該当する場合は,ランダムにcaseが実行されます。

sync.Mutex

goroutineが同時並列処理を実現するための軽量スレッドであることは上で述べてきました。しかしながら,複数のスレッドが同一の変数を参照する場合には,意図しない変数の書き換え等の副作用が生じる場合があります。そこで,Go言語ではsyncパッケージのmutexで定義してあるLockとUnlockを利用することで,排他制御を実現しています。

type SafeCounter struct {
  mu sync.Mutex
  v map[string]int
}

// 排他制御を用いて与えられたkeyに対するvalueをインクリメントする
func (c *SafeCounter) Inc(key string) {
  c.mu.Lock()
  c.v[key]++
  c.mu.Unlock()
}

// 排他制御を用いて与えられたkeyに対するvalueを返す
func (c *SafeCounter) Value(key string) int {
  c.mu.Lock()
  defer c.mu.Unlock() // Unlockすることを保証するためにdeferを用いることもできる
  return c.v[key]
}

func main() {
  c := SafeCounter{v: make(map[string]int)}
  for i := 0; i < 1000; i++ {
    go c.Inc("somekey")
  }

  // この処理を入れないとfor文内のgoroutineが全て完了する前にprintlnしてしまう
  time.Sleep(time.Second)
  fmt.Println(c.Value("somekey"))
}
シェアはこちらからお願いします!

コメント

コメントする

※ Please enter your comments in Japanese to distinguish from spam.

目次