はじめに
船井総研デジタルのoswです。業務でGo言語を使うことになったのでこれから学習していきます。その備忘録です。参考になる方がいらっしゃれば幸いです。
対象読者
- これからGo言語を学習する方
- 既に他の言語で基本構文を学習されている方
学習環境
学習環境は次のようになっています。この環境の構築メモは下記記事にまとめてあります。ご興味がある方はご参照ください。
- Windows 11 Home / 22H2
- VSCode / 1.72.2
- go version go1.19.2 windows/amd64
- git version 2.38.0.windows.1
前回までの学習
前回は type、for..range を学習しました。
関数
Goの関数は戻り値の型を引数リストの後方に書きます。また、戻り値は複数指定することができるようです。命名は外部パッケージへ公開するならパスカルケース、そうでないならキャメルケース。
何が参照渡しになるか実験したところ、次の結果になりました。
| 関数へ渡す型 | 値 or 参照渡し |
|---|---|
| 数値 | 値 |
| 文字列 | 値 |
| 配列 | 値 |
| スライス | 参照 |
| マップ | 参照 |
関数へ渡す際、"&"を付けて明示的にアドレスを渡す場合は数値型、構造体も当然変数が上書きされます。意外だったのが配列は値渡しになるようです。値が上書きされません。
ですが、配列からスライスを生成し、それを渡してやれば結果として同じことはできるようです。注意としては配列は長さを型情報に含んでいるため、要素数が異なる配列は渡せません。
なお、戻り値には名前を付けることができ、それを変数として扱うことができるようです。宣言で変数を用意しておけば改めて用意する必要がないようですが、一般的に名前付き戻り値は積極的に使うべきではないようです。何が戻り値なのか判別しづらくなる、ということでこれは書いていて納得しました。
ドキュメントに記す意味合いで推奨する感じでしょうか。
// 通常の定義
func 関数名 (引数1 型1, 引数2 型2, 略...) 戻り値の型 {
// 処理
return 戻り値
}
// 引数の型を省略
func 関数名 (引数1, 引数2 型) 戻り値の型 {
// 処理
return 戻り値
}
// 戻り値が2つの場合
func 関数名 (引数1 型1, 引数2 型2, 略...) (戻り値1の型, 戻り値2の型) {
// 処理
return 戻り値1, 戻り値2
}
// 戻り値を名前付き変数にする
func 関数名 (引数1 型1, 引数2 型2, 略...) (戻り値名1 型1, 戻り値名2 型2) {
// 処理
// 戻り値を明示しなければ、戻り値名で宣言した全ての変数を返す
return
// 上記returnは下記と同義
// return 戻り値名1, 戻り値名2
// 戻り値は全て省略するか、全て明示するかのどちらか。下記はできない
// return 戻り値名1
}
// for..rangeでインデックス、値が2つ返るように、複数の値を返すなら次のようにまとめて受け取る
a, b := 関数()
// いらないデータは破棄できる
_, b := 関数()
a, _ := 関数()
package main
import "fmt"
type Person struct {
Name string
Age int
}
func main() {
var ret1 int
var ret2 int
fmt.Println("Func1")
ret1 = Func1(100, "test")
fmt.Println("戻り値:", ret1)
fmt.Println("\nFunc2")
num1 := 101
num2 := 102
Func2(num1, num2)
fmt.Printf("num1: %d, num2: %d\n", num1, num2)
fmt.Println("\nFunc3")
_, ret2 = Func3(103, 104)
// fmt.Printf("戻り値1: %d, 戻り値2: %d", _, ret2) コンパイルエラー
fmt.Printf("戻り値1: _, 戻り値2: %d\n", ret2)
fmt.Println("\nFunc4")
ret1, _ = Func4(105, 106)
// fmt.Printf("戻り値1: %d, 戻り値2: %d", ret1, _) コンパイルエラー
fmt.Printf("戻り値1: %d, 戻り値2: _\n", ret1)
fmt.Println("\nFunc5")
Func5()
fmt.Println("\n参照渡しの実験")
tmp := 0
fmt.Println("関数呼び出し前 tmp:", tmp)
PointerFunc(&tmp)
fmt.Println("関数呼び出し後 tmp:", tmp)
str := "ABCDEFG"
fmt.Println("\n関数呼び出し前 str:", str)
StringFunc(str)
fmt.Println("関数呼び出し後 str:", str)
person := &Person {
Name: "HOGE",
Age: 1,
}
fmt.Println("\n関数呼び出し前 person:", person)
StructFunc(person)
fmt.Println("関数呼び出し後 person:", person)
array := [...]int {0, 1, 2, 3, 4}
slice := array[:]
mp := map[int]string {
0: "tanaka",
1: "sato",
2: "suzuki",
3: "takahashi",
4: "saito",
}
fmt.Println("\n関数呼び出し前 array:", array)
ArrayFunc(array)
fmt.Println("関数呼び出し後 array:", array)
fmt.Println("\n関数呼び出し前 array[:]:", array)
SliceFunc(array[:])
fmt.Println("関数呼び出し後 array[:]:", array)
fmt.Println("\n関数呼び出し前 slice:", slice)
SliceFunc(slice)
fmt.Println("関数呼び出し後 slice:", slice)
fmt.Println("\n関数呼び出し前 mp:", mp)
MapFunc(mp)
fmt.Println("関数呼び出し後 mp:", mp)
}
// 通常の定義
func Func1(arg1 int, arg2 string) int {
fmt.Println("arg1: ", arg1)
fmt.Println("arg2: ", arg2)
return 0
}
// 引数の型を省略
func Func2(arg1, arg2 int) int {
fmt.Println("arg1:", arg1)
fmt.Println("arg2:", arg2)
arg1 += 100
arg2 += 100
return 0
}
// 戻り値が2つの場合
func Func3(arg1, arg2 int) (int, int) {
fmt.Println("arg1: ", arg1)
fmt.Println("arg2: ", arg2)
// 戻り値が名前付き変数でなければ値は省略できない
// return
return 0, 1
}
// 戻り値を名前付き変数にする
func Func4(arg1, arg2 int) (ret1, ret2 int) {
fmt.Println("arg1: ", arg1)
fmt.Println("arg2: ", arg2)
ret1 = arg1 + 1
ret2 = arg2 + 1
// 戻り値を明示していないのでret1, ret2が自動的に返る
return
}
func Func5() {
fmt.Println("引数、戻り値なし関数")
}
/*
* 参照渡しの実験用関数
*/
func PointerFunc(arg *int) {
(*arg)++
}
// 文字列を受け取る
func StringFunc(str string) {
str = "TEST"
}
// 構造体を受け取る
func StructFunc(person *Person) {
person.Name = "PERSON"
person.Age = 120
}
// 要素数5の配列しか受け付けない
func ArrayFunc(array [5]int) {
for i := 0; i < len(array); i++ {
array[i] += 100
}
}
// スライスを受け取る
func SliceFunc(slice []int) {
for i := 0; i < len(slice); i++ {
slice[i] += 100
}
}
// マップを受け取る
func MapFunc(mp map[int]string) {
for i := 0; i < len(mp); i++ {
mp[i] = "TEST"
}
}
Func1
arg1: 100
arg2: test
戻り値: 0
Func2
arg1: 101
arg2: 102
num1: 101, num2: 102
Func3
arg1: 103
arg2: 104
戻り値1: _, 戻り値2: 1
Func4
arg1: 105
arg2: 106
戻り値1: 106, 戻り値2: _
Func5
引数、戻り値なし関数
参照渡しの実験
関数呼び出し前 tmp: 0
関数呼び出し後 tmp: 1
関数呼び出し前 str: ABCDEFG
関数呼び出し後 str: ABCDEFG
関数呼び出し前 person: &{HOGE 1}
関数呼び出し後 person: &{PERSON 120}
関数呼び出し前 array: [0 1 2 3 4]
関数呼び出し後 array: [0 1 2 3 4]
関数呼び出し前 array[:]: [0 1 2 3 4]
関数呼び出し後 array[:]: [100 101 102 103 104]
関数呼び出し前 slice: [100 101 102 103 104]
関数呼び出し後 slice: [200 201 202 203 204]
関数呼び出し前 mp: map[0:tanaka 1:sato 2:suzuki 3:takahashi 4:saito]
関数呼び出し後 mp: map[0:TEST 1:TEST 2:TEST 3:TEST 4:TEST]
小技
Goでは一時的な変数を宣言せずswapが可能です。
// 記述した順に格納される
a, b = b, a
package main
import "fmt"
func main() {
a := 0
b := 100
fmt.Println("a:", a)
fmt.Println("b:", b)
fmt.Println("\nswap")
a, b = b, a
fmt.Println("\na:", a)
fmt.Println("b:", b)
}
a: 0
b: 100
swap
a: 100
b: 0
無名関数
その場で定義される名前なしの関数。無名関数は定義されたスコープ内に存在する変数を参照することができるが、バグの温床になるため注意が必要。
//何らかの関数内
func () {
// 処理
}()
package main
import "fmt"
func main() {
doNotAccess := 100
// 無名関数を実行
str := func() string {
doNotAccess = 0
return "TEST"
}()
fmt.Println("doNotAccess:", doNotAccess) // アクセスできるため上書きされている
fmt.Println("str :", str)
}
doNotAccess: 0
str : TEST
関数型
GoはCでいうところの関数ポインタはないようですが、定義した関数を変数に格納することができ、コールバックなどそれっぽく使うことができるようです。
// 宣言
var something = func () {
// 処理
}
// 格納された関数を実行
something()
package main
import "fmt"
func main() {
// 関数型変数を宣言
// func() int型のスライスを作成、要素数は3
funcList := make([]func() int, 3)
// 初期化
funcList[0] = func() int { return 0 }
funcList[1] = func() int { return 1 }
funcList[2] = func() int { return 2 }
for _, f := range funcList {
ret := f()
fmt.Println("ret:", ret)
}
x := 1
y := 1
fmt.Println("x + y =", doSomething(x, y, add))
fmt.Println("x - y =", doSomething(x, y, sub))
}
// コールバックに指定する関数型は変数名を省略可
func doSomething(x, y int, f func(int, int) int) int {
return f(x, y)
}
func add(x, y int) int {
return x + y
}
func sub(x, y int) int {
return x - y
}
ret: 0
ret: 1
ret: 2
x + y = 2
x - y = 0
クロージャ
下記がわかりやすかったです。wikiの定義だとクロージャは機能、概念のようなので正解ではないかもしれませんが、サンプルコードを実行した現在の理解では、クロージャは 「無名関数を戻り値として返す関数」 なのかな、という理解です。
// クロージャを用いた簡単なコード
func closure() func() int {
x := 0
// 無名関数を返す
return func() int {
x++
return x
}
}
上記構文のclosure()の戻り値を複数の変数で受け取りそれぞれを実行すると、xの値は変数ごとに独立して変化する。メモリ領域が独立してるんですね。
ということで確認してみました。すると、closure()が返す関数のアドレス(アドレスだと思うんですが)は同じですが、closure()は呼ばれる度にスタックにxを積むため毎度生成されます。そのため、closure()が参照するxのメモリ領域は毎度異なり、c1, c2で加算するxは別物になる。
クロージャの戻り値はその毎度異なる変数のアドレスと返される無名関数を一緒に管理してるんでしょうか。じゃないとアクセスできないですし。とりあえず今はそんな理解で済まそうと思います。概要は理解しました。
package main
import "fmt"
func closure() func() int {
x := 0
fmt.Printf("xのアドレス: %p\n", &x)
// 無名関数を返す
return func() int {
x++
return x
}
}
func main() {
f1 := test1
fmt.Println("関数のアドレスらしきものは見えるらしい")
fmt.Printf("f1 : %p\n", f1)
fmt.Printf("test1: %p\n", test1)
fmt.Printf("test2: %p\n", test2)
c1 := closure()
c2 := closure()
fmt.Printf("c1に格納されている関数: %p\n", c1)
fmt.Printf("c2に格納されている関数: %p\n", c2)
// c1, c2をそれぞれ異なる回数実行してみる
for i := 0; i < 5; i++ {
fmt.Println("c1:", c1())
}
fmt.Println("c2:", c2())
}
func test1() {
}
func test2() {
}
関数のアドレスらしきものは見えるらしい
f1 : 0x460100
test1: 0x460100
test2: 0x460120
クロージャが返す関数は異なっているはず
c1: 1
c1: 2
c1: 3
c1: 4
c1: 5
c2: 1
c1に格納されている関数: 0x4600e0
c2に格納されている関数: 0x4600c0
おわりに
今回はここまでです。