2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Go言語入門 学習メモ 6 関数、無名関数、関数型、クロージャ

2
Last updated at Posted at 2022-10-20

はじめに

船井総研デジタルの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

おわりに

今回はここまでです。

2
1
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
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?