LoginSignup
7
5

More than 3 years have passed since last update.

Goを真面目に勉強する〜3.関数と型〜

Last updated at Posted at 2019-12-29

はじめに

Goをはじめて1年半。アウトプットが進まない私が、専門家の@tenntennさんから受けたマンツーマンレッスンの内容をまとめて、Goのスキルアップを目指します。Goの基礎から丁寧に学んでいきます。
記事のまとめは以下の通りで順次作成していきます。
今回は「3.関数と型」になります。

シリーズの一覧

  1. Goについて知っておく事
  2. 基本構文
  3. 関数と型(今回)
  4. パッケージとスコープ

本記事の内容

今回学ぶ内容は以下の通りです。

  • 組込み型
  • コンポジット型
    • 構造体
    • 配列
    • スライス
    • マップ
  • ユーザ定義型
  • 関数
    • 組込み関数
    • 関数定義
    • 無名関数
  • メソッド
    • メソッド値
    • メソッド式

組込み型

(本項目は前回記事に記載のものと同様です)
Goでは以下の型が組込み型として用意されています。組込み型の種類と同時に、Goでは重要になってくるゼロ値も記載しました。

型名 説明 ゼロ値
int, int8, int16, int32, int64 符号付き整数型. intは32または64bit 0
uint, uint8, uint16, uint32, uint64 符号なし整数型. uintは32または64bit 0
float32, float64 浮動小数点数 0
uintptr ポインタ値を格納するのに十分な大きさの符号なし整数型 0
byte uint8のエイリアス 0
rune int32のエイリアス 0
string 文字列 ""
bool 真偽値 false
error エラー nil

参考:https://golang.org/ref/spec#Types

コンポジット型

コンポジット型は、複数のデータ型を集めて1つのデータ型にしたものです。構造体や配列がコンポジット型に当たります。Goではその他にスライス、マップといったコンポジット型が用意されています。

種類 説明
構造体 型の異なるデータを集めたデータ型
配列 同じ型のデータを集めて並べたデータ型
スライス 配列の一部を切り出したデータ型
マップ キーと値をマッピングしたデータ型

型リテラルとコンポジットリテラル

参考:https://golang.org/ref/spec#Composite_literals
コンポジット型を変数で利用するためには、型リテラルかコンポジットリテラルで宣言します。

型リテラル

型リテラルは、型の具体的な定義を書き下した型の表現方法で、例えば構造体の場合次のように表現します。


// 構造体の型リテラル
struct {
    name string
    age  int
}

コンポジットリテラル

コンポジットリテラルは、型と値がセットになったもので、以下の書式で表現します。

書式
型リテラル "{" 値リスト "}"


struct {
    name string
    age  int
}{
    name: "Gopher",
    age:  10,
}

コンポジット型の変数宣言

コンポジット型の変数宣言は、組込み型の変数宣言と同様、以下の構文で行います。

  • "var" 変数名 型リテラル
  • "var" 変数名 "=" コンポジットリテラル
  • 変数名 ":=" コンポジットリテラル

次は構造体、配列、スライス、マップのそれぞれの変数宣言をみていきます。

構造体

構造体は型の異なるデータを集めたデータ型で、各変数はフィールドと呼ばれます。各フィールドは異なる型にすることができます。


struct {
    name string // フィールド nameはstring型
    age  int    // フィールド ageはint型
}

フィールドの型をコンポジット型にすることもできます。


struct {
    name    string
    age     int
    address struct { // addressを構造体の型リテラルで宣言する
        street string
        city   string
        state  string
    }
    number []string // numberをstringのスライスで宣言する
}

構造体の変数宣言

書式
"var" 変数名 型リテラル
各フィールドの値は、フィールドの型のゼロ値で初期化されます。


// 変数pを構造体の型リテラルで宣言する
var p struct {
    name string // nameはstring型のゼロ値=""で初期化される
    age  int    // ageはint型のゼロ値=0で初期化される
}

書式
"var" 変数名 "=" コンポジットリテラル
変数宣言時に構造体のフィールドを初期化することができます。


// 変数pを構造体のコンポジットリテラルで宣言する
var p = struct {
    name string
    age  int
}{
    name: "Gopher",
    age:  10,
}

書式
変数名 ":=" コンポジットリテラル
変数宣言時に構造体のフィールドを初期化することができます。この書き方は関数内でしか利用できません。


// 変数pを構造体の型リテラルで宣言する
p := struct {
    name string
    age  int
}{
    name: "Gopher",
    age:  10,
}

構造体のフィールドが構造体の場合

フィールドが構造体の場合は少し注意が必要です。フィールド値は、型が明示されているか、型推論できる必要があります。intstringのような組込み型の場合は値リテラルを指定すれば型推論してくれますが、コンポジットの場合はコンポジットリテラルを与える必要があります。
次の例では、フィールドを構造体で宣言している場合の初期化をおこなっていますが、これを念頭に見れば理解できると思います。


// 変数pを構造体の型リテラルで宣言する
var p = struct {
    name    string
    age     int
    address struct { // addressを構造体の型リテラルで宣言する
        street string
        city   string
    }
}{
    name: "Gopher",
    age:  10,
    address: struct {
        street string
        city   string
    }{
        street: "1-23-4",
        city:   "Osaka",
    },
}

構造体のフィールド参照

構造体のフィールドを参照するには.を使います。


var p = struct {
    name string
    age  int
}{
    name: "Gopher",
    age:  10,
}

p.age++                    // p.ageをインクリメントする
fmt.Println(p.name, p.age) // p.nameとp.ageを表示する

これを実行すると以下のような結果が得られます。

Gopher 11

配列

配列は同じ型のデータを集めて並べたデータ型で、途中で要素数を変更することはできません。また、要素数が違えば違う型になります。


[5]int  // 型と要素がセット
[10]int // [5]intとは違う型

配列の変数宣言

書式
"var" 変数名 型リテラル
各要素の値は、要素の型のゼロ値で初期化されます。


var n [5]int // nの要素はint型のゼロ値=0で初期化される

要素の型にはコンポジット型も使えます。この場合、各要素のフィールド値は、ゼロ値で初期化されます。


var ps [5]struct {
    name string // string型のゼロ値=""で初期化される
    age  int    // int型のゼロ値=0で初期化される
} // 要素の型にはコンポジット型も使える

書式
"var" 変数名 "=" コンポジットリテラル
変数宣言時に配列の要素を初期化することができます。


var n = [5]int{0, 1, 2, 3, 4}

書式
変数名 ":=" コンポジットリテラル
変数宣言時に配列の要素を初期化することができます。この書き方は関数内でしか利用できません。


n := [5]int{0, 1, 2, 3, 4}

配列リテラルで...表記を使った場合、要素数を指定しなくても値から要素数を推論してくれます。また、一部の要素の値だけ指定することもできます。


n := [...]int{0, 1, 2, 3, 4} // len=5, cap=5
m := [...]int{5: 10, 10: 100} // len=11, cap=1

スライス

配列の一部を切り出したデータ型で、型情報に要素数は含めません。その特性から、スライスの背後には配列が存在することが前提になります。

スライス.png

スライスを構成している配列、要素数、容量がどのように管理されているのか、以下のスライスの内部構造を見れば分かります。
https://golang.org/search?q=slice#Global_pkg/runtime


type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}

スライスの型リテラルは以下の通りです。


[]int // 配列から要素数を省いた形で宣言する

スライスの変数宣言

書式
"var" 変数名 型リテラル
この書き方でスライスを宣言した場合、背後の配列が存在しないため、スライスはnilで初期化されます。


var ns []int // nsはint型のスライスで、ゼロ値=nilで初期化される

書式
変数名 ":=" コンポジットリテラル
背後の配列を用意してスライスを宣言することができます。


// nsは要素数3, 容量3で初期化される
// スライスが指す要素は1, 2, 3で初期化される
ns := []int{1, 2, 3}

要素数と容量を指定して初期化することもできます。


// nsは要素数3, 容量10で初期化される
// 各要素は要素の型のゼロ値で初期化される
ns := make([]int, 3, 10)

スライスの拡張

スライスはappend関数を利用して要素を追加することができます。append関数を利用した際の挙動は、背後に用意された配列の容量によって変わります。

  • 容量が足りる場合は、既存の領域に新しい要素をコピーして、スライスの要素を拡張します。
  • 容量が足りない場合は、容量が十分足りるように、新しい配列を確保して、そこに元の配列から全ての要素をコピーします。

ns := make([]int, 3, 5) // 要素3、容量5で宣言する
ns[0] = 1
ns[1] = 2
ns[2] = 3
fmt.Printf("%[1]p len%[2]d cap%[3]d %[1]v\n", ns, len(ns), cap(ns))

ns = append(ns, 4, 5) // 要素を2つ追加する。要素5、容量5になる。
fmt.Printf("%[1]p len%[2]d cap%[3]d %[1]v\n", ns, len(ns), cap(ns))

ns = append(ns, 6, 7) // 要素を2つ追加する。要素7、容量10になる。
fmt.Printf("%[1]p len%[2]d cap%[3]d %[1]v\n", ns, len(ns), cap(ns))

この結果は次のように出力されます。
初期化時、要素3までが利用されていて、1回目のappend関数で要素2つを追加したため、確保した容量5に収まっています。
次にappend関数で2つ要素を追加すると、容量が足りないため、別の領域を確保して全ての要素をコピーしていることがわかります。確保される容量は、元の容量のおよそ2倍です。

0x456000 len3 cap5 [1 2 3]
0x456000 len5 cap5 [1 2 3 4 5] // 最初と同じ領域
0x454030 len7 cap12 [1 2 3 4 5 6 7] // 新しく確保した領域

マップ

マップはキーと値をマッピングしたデータ型です。宣言にはキーと値の型を指定します。キーには==で比較できる型しか使うことができません。
関数やスライスは==で比較できないためキーには使えません。また、それらを要素として持つコンポジット型(構造体の場合はフィールド)もキーに使うことはできません。

マップの変数宣言

書式
"var" 変数名 型リテラル
初期値はnilで初期化されます。


// キーがstring、要素がint型のmap mを宣言する
var m map[string]int

// キーが構造体、要素がint型のスライスのmap cを宣言する
var c map[struct {
    x, y float64
}][]int

書式
"var" 変数名 "=" コンポジットリテラル
変数宣言時にマップの要素を初期化することができます。


var m = map[string]int{"x": 10, "y": 20}

書式
変数名 ":=" コンポジットリテラル
変数宣言時にマップの要素を初期化することができます。この書き方は関数内でしか利用できません。


m := map[string]int{"x": 10, "y": 20}

マップの操作

要素の参照

マップから値を取得するには、以下のように対応するキーを指定します。


// 参照
m := map[string]int{"x": 10, "y": 20}
fmt.Println(m["x"]) // 10が表示される

要素の追加

キーを指定して値を入力することで、マップの要素を追加することができます。


// 入力
m := map[string]int{"x": 10, "y": 20}
m["z"] = 30 // 要素が追加される

要素の削除

deleteを利用してマップから要素を削除することができます。


// 削除
m := map[string]int{"x": 10, "y": 20}
delete(m, "y") // キー="y"の要素を削除する

要素の存在の確認

次のように書くことでキーの存在の確認することができます。

  • キーが存在する場合、1つめの戻り値に要素の値が、2つめの戻り値に結果がbool型で得られます。
  • キーが存在しない場合、1つめの戻り値は要素の型のゼロ値が、2つめの戻り値は結果falseが得られます。

var m = map[string]int{"x": 10, "y": 20}

x, ok := m["x"]
fmt.Println(x, ok) // 10 true

z, ok := m["z"]
fmt.Println(z, ok) // 0 false

ユーザー定義型

typeで名前を付けると、任意の型を基底型としてユーザ定義型を定義することができます。


// 組込み型を基にする
type MyInt int

// 他のパッケージの型を基にする
type MyWriter io.Writer

// 型リテラルを基にする
type Person struct {
    Name string
}

構造体をユーザ定義型で定義した場合の例
構造体の節で「構造体のフィールドが構造体の場合」の注意点を挙げていましたが、ユーザ定義型を利用すると分かりやすくなります。


type MyAddress struct {
    street string
    city   string
}

var ns = struct {
    name    string
    age     int
    address MyAddress
}{
    name: "Gopher",
    age:  10,
    address: MyAddress{
        street: "1-23-4",
        city:   "Osaka",
    },
}

ユーザ定義型のキャスト

基底型とユーザー定義型はお互いにキャストすることができます。


type MyInt int // int型を基底にしてMyIntを定義する
var mi MyInt = 10
fmt.Printf("%[1]T %[1]d\n", mi) // main.MyInt 10

var i int = int(mi)
fmt.Printf("%[1]T %[1]d\n", i) // int 10

mi = MyInt(i)
fmt.Printf("%[1]T %[1]d\n", mi) // main.MyInt 10

定数のデフォルトの型からキャストが可能な場合、型無しの定数から明示的なキャストをする必要はありません。


d := 10 * time.Second
fmt.Printf("%T\n", d)
fmt.Printf("%T\n", time.Second)

型エイリアス

参考:https://golang.org/ref/spec#Type_declarations
Go 1.9以上では型のエイリアスを定義することができます。エイリアスは別名のことで、オリジナルの型と同じ動きをします。ただし、型エイリアスでつけた別名を使ってメソッドを定義することはできません。(メソッドはこのあとの節で説明します)

  • 同じ型として機能する
  • 基となる型とはキャスト不要

書式
"type" 識別子(エイリアス名) "=" 基となる型
識別子(エイリアス名)のスコープ内でエイリアスとして機能し、%Tで出力すると基となる型が表示されます。


type Applicant = http.Client
fmt.Printf("%T\n", Applicant{}) // http.Client

関数

関数は一連の処理をまとめたもので、引数で受け取った値を基に処理を行い、戻り値として結果を返す機能を持ちます。引数や戻り値は複数持つことも、1つも持たないこともできます。

組込み関数

Goには次のような組込み関数が用意されています。

関数名 説明
print, println 表示を行う
make コンポジット型の初期化
new 指定した型のデータ領域を確保
len, cap 配列やスライスの長さ、容量を取得する
append スライスの要素を追加する
delete 指定した要素を削除する
panic, revocer パニックを発生、回復する

参考:https://golang.org/ref/spec#Predeclared_identifiers

ちなみに、これらは予約語ではないので、同じ名前の関数を定義することもできます。

関数の定義

参考:https://golang.org/ref/spec#Function_declarations
関数定義の書式は以下の通りです。引数、戻り値は省略できます。

書式
"func" 関数名"("[引数]")" [戻り値] "{" 処理 "}"


// xとyの加算結果を返す関数addを定義する
func add(x int, y int) int {
    return x + y
}

// 引数、戻り値が無い関数printHogeを定義する
func printHoge() {
    println("Hoge")
}

関数の引数は、型が同じであればまとめることができます。


// xとyの加算結果を返す関数addを定義する
func add(x, y int) int {
    return x + y
}

多値を返す関数を定義する場合、戻り値のリストを括弧で括ります。


// 多値を返す場合は、戻り値のリストに括弧をつける
func swap(x, y int) (int, int) {
    return x, y // returnはカンマ区切りにする
}

多値の受け取りかた

多値を返す関数の場合、呼び出し側で複数の変数を用意して受け取ります。


// xが第一戻り値、yが第二戻り値
x, y := swap(10, 20)

Goでは未使用の変数を許していないため、一部の戻り値を利用しない場合は、受け取りを省略します。省略にはブランク変数_を利用します。


// 第一戻り値を省略
_, y := swap(10, 20)

// 第二戻り値を省略
x, _ := swap(10, 20)

名前付きの戻り値

引数と同様、戻り値にも変数名をつけて関数内で利用することができます。


// 戻り値にrx, ryという名前をつける
func swap(x, y int) (rx, ry int) {
    rx, ry = x, y
    return
}

無名関数(関数リテラル、クロージャ)

参考:https://golang.org/ref/spec#Function_literals
無名関数はその名の通り、名前を付けていない関数のことで、関数リテラルで表現するものです。クロージャとも呼ばれます。
無名関数定義の書式は以下の通りで、引数、戻り値は省略可能です。

書式
"func""("[引数]")" [戻り値] "{" 処理 "}"

無名関数では関数外の変数を参照することができます。


// msgの値を表示する無名関数を定義し、実行している
msg := "Hello world!"
func() {
    fmt.Println(msg)
}()

関数は値の一種なので、変数に代入することができます。


i := 0
f := func() { // func()型の変数fに代入する
    i++
    fmt.Println(i)
}
f() // 関数fを実行する

無名関数でも引数、戻り値を使えます


r := func(x, y int) int {
    return x + y
}
fmt.Println(r(1, 2)) // 3

無名関数を使用する上で注意するべきは、無名関数内で使っている変数のスコープです。次の場合、f()を実行すると何が表示されるでしょうか?


ns := []int{1, 2, 3}
fs := make([]func(), len(ns))
for i, n := range ns {
    fs[i] = func() {
        fmt.Println(i * n)
    }
}
for _, f := range fs {
    f() // どうなる?
}

f()を実行すると次の結果が得られます。
0×1, 1×2, 2×3ではなく、2×3, 2×3, 2×3が実行されます。

6
6
6

f()で使われている変数inは、fs[i]に代入したタイミングではなく、f()を実行する時点でのinの値で計算されます。

メソッド

参考:https://golang.org/ref/spec#Method_declarations
Goにはクラスという概念はありませんが、データと操作を紐付ける仕組みはメソッドとレシーバによって作ることができます。

関数名 説明
メソッド レシーバと紐付けられた"関数"。メソッドにはメソッド名を付ける必要がある。レシーバの基本型にバインドされている。
レシーバ メソッドと紐付けられた"値"。レシーバの型には定義済みの型か、定義済みの型のポインタを使用できる。ただし、レシーバの型はメソッドと同じパッケージ内で定義している必要がある。

メソッドの定義と呼び出し

メソッドを定義する構文は以下の通りで、引数、戻り値は省略可能です。
書式
"func" "("レシーバ")" メソッド名"("[引数]")" [戻り値] "{" 処理 "}"

メソッドを呼び出す構文は以下の通りです。
書式
レシーバ "." メソッド名 "(" [引数] ")"


type Hex int

func (h Hex) String() string { // Hex型の変数hをレシーバとして受け取る
    return fmt.Sprintf("%x", int(h))
}

func main() {
    var hex Hex = 100
    fmt.Println(hex.String()) // Stringメソッドを呼び出す
}

レシーバとして渡す値にはコピーが発生する
メソッド呼び出し時にレシーバとして渡した値は、通常の引数と同じ扱いになるためコピーが発生します。したがって、メソッド内で変更を加えてもレシーバには反映されません。


type T int

func (t T) f() {
    t++ // インクリメント
}

func main() {
    var t T = 100
    t.f()
    fmt.Println(t) // 100
}

レシーバをポインタにすることで、メソッド内の変更を反映することができます。レシーバがポインタの場合でも、.でメソッドを呼び出すことができます。


type T int

func (t *T) f() {
    *t++ // インクリメント
}

func main() {
    var t T = 100
    t.f()          // (&t).f()でも同じ
    fmt.Println(t) // 101
}

*T型はTメソッドも自身のメソッドとして扱われる
t.f()(&t).f()と同じであることから分かるように、*TはTのメソッドを呼び出すことができます。ただし、その逆、Tが*Tのメソッドを呼び出すことはできません。


func (t T) f()  {}
func (t *T) g() {}
func main() {
    (T{}).f()   // T
    (&T{}).f()  // *T
    (*&T{}).f() // T

    (T{}).g()   // T これはできない
    (&T{}).g()  // *T
    (*&T{}).g() // T
}

メソッドの定義における禁止事項

  • ポインタ型を基底とする型やインターフェース型は利用できない

type T *int

func (t T) f() { // Tはint型のポインタなのでレシーバに使えない
    fmt.Println("hello world!")
}
  • 構造体をレシーバとする場合、構造体のフィールド名と同じメソッド名を付けることはできない

type Member struct {
    name string // フィールド名 "name"
}

func (m Member) name() { // フィールド名と同じメソッド名 "name"は付けられない
    fmt.Println(m.name)
}

メソッド値

参考:https://golang.org/ref/spec#Method_values
メソッド値はメソッドを値として表したもので、レシーバは束縛された状態になります。

書式
レシーバ "." メソッド名

以下の例では、fgにメソッド値を値として保存しているので、保存した時点のhexの値を使って関数が実行されます。


type Hex int

func (h Hex) String() string {
    return fmt.Sprintf("%x", int(h))
}

func main() {
    var hex Hex = 100
    f := hex.String // メソッド値はレシーバとしてHex型の値100を束縛している
    hex++
    g := hex.String // メソッド値はレシーバとしてHex型の値101を束縛している
    fmt.Println(f(), g()) // 64 65
}

次の例のようにレシーバをポインタにした場合、束縛されるレシーバ値が変数へのポインタなので、fgにメソッド値を値として保存するのもポインタになりますので、上の結果とは異なります。


type Hex int

func (h *Hex) String() string {
    return fmt.Sprintf("%x", int(*h))
}

func main() {
    var hex Hex = 100
    f := hex.String // メソッド値はレシーバとしてHex型の値へのポインタを束縛している
    hex++
    g := hex.String // メソッド値はレシーバとしてHex型の値へのポインタを束縛している
    fmt.Println(f(), g()) // 65 65
}

メソッド式

参考:https://golang.org/ref/spec#Method_expressions
メソッド式はメソッドを式として表したもので、レシーバを第1引数とした関数になります。

書式
レシーバ型 "." メソッド名


type Hex int

func (h Hex) String() string {
    return fmt.Sprintf("%x", int(h))
}

func main() {
    var hex Hex = 100
    f := Hex.String                   // メソッド式
    fmt.Printf("%T\n%s\n", f, f(hex)) // 実行時にレシーバを第一引数に渡す
}

余談
メソッドの説明を書くためにサンプルコード作っていてハマったもの。
https://play.golang.org/p/dRwiBklOuD9

最後に

無名関数はほぼ使ったことが無かったため理解するのに少し時間がかかりました。メソッド値、メソッド式については触れたことが無いので、これを書いている時点でも不安がありました。
前回までよりも量が多くて、書き終えるのに時間がかかってしまったので、次に書くパッケージとスコープについては、もう少しスピーディに進めようと思います。

7
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
5