Open6

書籍「初めてのGo言語」気になったことメモ帳

sugar2456sugar2456

ゼロ値

宣言されているが値が割り当てられていない変数にデフォルトで割り当てられる値をゼロ値という

リテラル

数値、文字、文字列など特定のデータ型の値をソースに直接書いたもの

整数リテラル

数値のこと
10進数、2進数など整数の数値に関わる定義はなんでも数値リテラル

浮動小数点数リテラル

小数の表記に関わるリテラル

runeリテラル

先頭と最後に「'」を置いて表現するリテラル
以下のフォーマットで表現します

'a'という文字をruneリテラルで表現すると以下のようなものになります

  • 一文字のUnicode文字('a')
  • 8bitの8進数('\141'これは'a'と同じ文字)
  • 8bit16進数('\x61')
  • 16bitの16進数('\u0061')
  • 32bitのUnicode('\U00000061')

エスケープシーケンスなど

  • 改行('\n')
  • タブ('\t')
  • シングルクオート(''')
  • バックスラッシュ('\')

文字列リテラル

解釈対象の文字列リテラルとロー文字列リテラルがある

通常は解釈対象の文字列リテラルを使う
解釈対象の文字列リテラルは「"」で囲われた文字列
解釈対象の意味は「\n」などのエスケープ文字を解釈する文字列だから
例)"こんにちは\nいい天気ですね"
こんにちは
いい天気ですね

ロー文字列リテラルは「」で囲われた文字列 これはエスケープ文字列を解釈せずにそのまま文字列で扱う 例)こんにちは\nいい天気ですね`
こんにちは\nいい天気ですね

なおリテラルには型の概念が存在しない

変数の宣言

go言語ではvarをサポートしている

var x int = 10

varは型を推定するのでintは省略することができる

var x = 10

複数代入も可能

var x, y int = 10,  20

異なる型の代入も可能

var x, y = 10, "hello"

以下のような複数の代入も可能

var (
    x int
    y = 20
    z, a = 40, "hello"
)

:=でvarの省略も可能

x := 30

型付きの定数と型のない定数

定数は型が存在するものと存在しない定数がある

型なし定数はリテラルと同じように挙動する

const x = 10

// 許容される
var y int = x
var z float64 = x
var d byte = x

型付きの定数

const typedX int = 10
// コンパイルエラー
var z float64 = typedX
sugar2456sugar2456

合成型

配列リテラル

配列の初期化には「...」を使うことができる

var x = [...]int {10, 20, 30}

スライス

スライスは配列のように長さの制限が存在せず、柔軟にデータを扱うことができる
「[]」で表現する

var x = []int {10, 20 ,30}
// 5のインデックスに4、10のインデックスに100が入る
// [1 0 0 0 0 4 6 15 0 0 100]
var y = []int{1, 5:4, 6, 15, 10:100}

配列との明確な違いはゼロ値の違いがある
スライスではゼロ値にnilが設定される

スライスの比較には専用の関数が用意されている

x := []int{1,2,3}
y := []int{1,2,3}
z := []int{1,2,3,4}
s := []string{"a", "b", "c"}
// true
fmt.Println(slices.Equal(x, y))
// false
fmt.Println(slices.Equal(x, z))
// コンパイルエラー
fmt.Println(slices.Equal(x, s))

スライスはappendで要素を追加することできる

var x []int
x = append(x, 10)

// 展開して追加
y := []int{1,2,3}
x = append(x, y...)

スライスのキャパシティ

スライスには各要素を格納するための連続したメモリ領域が存在する
最初のスライス初期化時に実際の要素より大きなメモリ領域を確保しているので、
len関数で実要素を取得するとcap関数で容量を取得すると差が出ることがある

make

makeでは容量を指定してスライスを作ることができる

x := make([]int, 5)

使い分けとしては最初からスライスの上限が読めているならmakeで初期化
わからないようであればvar宣言のスライスにappendしていくのがいいと思われる

スライスのクリア

スライス配列を各型のゼロ値にするにはclear関数で初期化できる

s := []string{"one", "second", "third"}
clear(s)

スライスのコピー

既存のスライスを操作すると参照を渡しているので、配列を操作する際に予期せぬ動きをすることがある
そこでcopy関数を使って別アドレスにスライス配列をコピーすることもできる

x := []int{1,2,3}
y := make([]int, 4)
num := copy(y, x)

map

マップ型はmap[<キーの型>]<値の型>で定義される

nilMapはゼロ値がnilのmapになる

var nilMap map[string]int

上記の定義だと値の更新ができないので、通常はnilMapは使わずに以下の定義をする

totalWins := map[string]int{}
// mapの追加
totalWins["abc"] = 3
// mapの削除
delete(totalWins, "abc")

カンマok イディオム

mapに対してキーのチェックをすることができる

m := map[string]int {
    "hello" : 5,
    "world" : 0,
}
v, ok := m["hello"]
// 5, true
fmt.Println(v, ok)
v, ok := m["abc"]
// 0, false
fmt.Println(v, ok)

mapのクリア

mapをゼロ値にすることができる

m := map[string]int {
    "hello" : 5,
    "world" : 0,
}
clear(m)

mapの比較

mapsパッケージにEqualという関数でmapを比較できる

m := map[string]int {
    "hello" : 5,
    "world" : 10,
}


n := map[string]int {
    "hello" : 5,
    "world" : 10,
}

maps.Equal(m, n)

構造体

構造体の初期化方法には以下のパターンがサポートされている

type person struct {
    name string
    age int
    pet string
}

julia := person{
    "name 1",
    50,
    "cat"
}
// フィールド名指定初期化
beth := person{
    age:30,
    name: "ベス"
}

無名構造体

構造体を型名に定義せずに直接変数に定義できる
外部API通信などでマーシャルする際などに頻出するらしい

// 変数に直接構造体を宣言
var person struct {
    name string
    age int
    pet string
}
person.name = "ボブ"
person.age = 50
person.pet = "dog"

pet := struct {
    name string
    kind string
} {
    name: "ポチ",
    kind: "dog"
}
sugar2456sugar2456

ブロック、シャドーイング、制御構造

シャドーイング

ブロックがネストしていると内側で同名の変数を宣言することができる
ブロックの外側に移ると内側で宣言した定義が破棄されてしまう

func main() {
    x := 10
    if x > 5 {
        // 10
        fmt.Println(x)
        // シャドーイング変数
        x := 5
        // 5
        fmt.Println(x)
    }
    // 10 内側のブロックで定義された変数は破棄されているため
    fmt.Println(x)
}

変数だけでなくパッケージ名でもシャドーイングが発生する

import "fmt"

func main() {
    x := 10
    fmt.Println(x)
    fmt := "aaaaa"
    // コンパイルエラー
    fmt.Println(fmt)
}

これは仕様としていかがなものかと思う

switch

goのswitch文ではbreakを入れなくてもcaseにヒットしたらそのまま処理を抜ける
C/Javaのようにbreakを入れないと次のcaseに進むということはない。

もしgoで次のcaseも評価したいなら明示的にfallthroughtをcase文の中に入れることができるが、
可読性の観点からあまり推奨されない

sugar2456sugar2456

関数

名前付き引数とオプション引数

Goの関数には名前付き引数とオプション引数が存在しない
もし似たようことがしたい場合はDTO構造体を作成する必要がある

type MyFuncOpts struct {
    FirstName string
    LastName string
    Age int
}

func MyFunc(opts MyFuncOpts) error {
    fmt.Println(opts)
    fmt.Println("< ここで必要な処理を行う>")
    return nul
}

func main() {
    // 省略した構造体にはゼロ値が入る
    // 構造体の要素がパラメータのように使える
    MyFunc(MyFuncOpts {
        LastName: "Patel",
        Age: 50
    })
    MyFunc(MyFuncOpts {
        FirstName: "Joe",
        LastName: "Smith",
    })

複数の戻り値

goの関数には複数の戻り値を返すことができる
returnする際にカンマで区切る
習慣的にerrorは一番最後に返却する

下記の関数は商とあまりとerrorを返却する

func divAndRemainder(num, denom int) (int, int, error) {
	if denom == 0 {
		return 0, 0, errors.New("0で割ることはできません")
	}
	return num / denom, num % denom, nil
}

名前付き戻り値

goでは関数の戻り値に名前をつけることができる
例ではresultなどを戻り値に宣言して、returnで返却している。

なおreturnする時にresultを渡さなくても、returnに割り当てられる値を返却するので、
結構緩い。

シャドーイングが発生するリスクもあるので、あまり多用しない方がいい

func divAndRemainder(num, denom int) (result int, remainder int, err error) {
	if denom == 0 {
		return 0, 0, errors.New("0で割ることはできません")
	}
	result = num / denom
	remainder = num % denom
	return result, remainder, nil
}

ブランクリターン

名前付き戻り値を使うとreturnを空にしても変数に値があれば勝手に返却できる。

何が返却するかトレースが難しくなので使うべきではない
なんでこんな仕様を取り込んだのか

func divAndRemainder(num, denom int) (result int, remainder int, err error) {
	if denom == 0 {
		return 0, 0, errors.New("0で割ることはできません")
	}
	result = num / denom
	remainder = num % denom
	return
}

関数は値

Go言語の関数は値として扱われる
関数の型はfuncで引数と戻り値の型で構成される
これをシグネイチャと呼ぶ

関数を値として扱うことでメリットがある
関数をmapの値として扱うことができる

package calc

import (
	"fmt"
	"strconv"
)

func add(i int, j int) int {
	return i + j
}

func sub(i int, j int) int {
	return i - j
}

func mul(i int, j int) int {
	return i * j
}

func div(i int, j int) int {
	return i / j
}

func Execute() {
    // mapに値として関数を持たせる
	opMap := map[string]func(int, int) int{
		"+": add,
		"-": sub,
		"*": mul,
		"/": div,
	}

	expressions := [][]string{
		{"2", "+", "3"},
		{"2", "-", "3"},
		{"2", "*", "3"},
		{"2", "/", "3"},
		{"2", "%", "3"},
		{"two", "+", "three"},
		{"2", "+", "three"},
		{"5"},
	}
	for _, exexpression := range expressions {
		if len(exexpression) != 3 {
			fmt.Print(exexpression, "不正な式です\n")
			continue
		}
		p1, err := strconv.Atoi(exexpression[0])
		if err != nil {
			fmt.Print(exexpression[0], "不正なオペランドです\n")
			continue
		}
		op := exexpression[1]
		opFunc, ok := opMap[op]
		if !ok {
			fmt.Print(op, "不正な演算子です\n")
			continue
		}
		p2, err := strconv.Atoi(exexpression[2])
		if err != nil {
			fmt.Print(exexpression[2], "不正なオペランドです\n")
			continue
		}
		result := opFunc(p1, p2)
		fmt.Printf("%d %s %d = %d\n", p1, op, p2, result)
	}
}

defer

必ず実施されることを宣言する
リソースのクリーンアップなど、成功しても失敗しても必ず実施する必要がある作業にこの宣言を使って処理を呼び出す

goは値渡し

goでは関数の引数は値渡しで渡されている

sugar2456sugar2456

ポインタ

ポインタ入門

ポインタとはある値が保存されているメモリ内の位置を表す変数。

32bitのintの変数を宣言すればメモリでは4byte分のメモリを占有し、
bool型の変数を宣言すれば1バイトのメモリを確保する
※boolは1bitさえあればtrue falseを表現できるが、goでは1byte単位でしかメモリを確保できない

ポインタはそうした確保したメモリのアドレスの情報となる

var x int32 = 10
var y bool = true
// ポインタ
// %!s(*int32=0xc000010050) 
// %!s(*bool=0xc000010054) 
// %!s(*string=<nil>) 
pointerX := &x
pointerY := &y
// 何のアドレスも指定されていないのでnil
var pointerZ *string

このようにポインタはアドレスなのでデータ量は4byteもしくは8byteになる

Cのような記法をサポートしているがC言語のようにアドレスにビットサイズごとで足し引きして、
配列の位置を移動するような機能はGOにはサポートされていない

「&」はアドレス演算子でその変数の前につけるとその変数のアドレスを返す。
返された値はポインタ型となる。

x := "hello"
pointerX := &x

「*」は間接参照の演算子で、ポインタ型の変数の前につけると、そのポインタが格納している値を参照することができる
間接演算子をつける変数がnilの場合、パニックになるので、nilチェックをつけてあげることも必要

x := "hello"
pointerX := &x
// アドレスが表示
fmt.Println(pointerX)
// 値が表示
fmt.Println(*pointerX)
z := *pointerX + "world"
// hello world
fmt.Println(z)

ポインタ型変数の宣言は以下のようにする

x := 10
var pointerX *int
pointerX = &x

構造体の内部のプリミティブの定義ではポインタ変数を定義することはできない

ポインタは最後の手段

ポインタはデータの流れが分かりにくくなり、ガーベージコレクタの仕事も増えるのでなるべく避けるべき。
ポインタ変数を渡す場面は渡す変数の構造体の型が変更したり、オブジェクトが大きい際に検討すべき

ゼロ値と値なし

Goでポインタ使用法としてもう一つ一般的なのが、ゼロ値を設定されている変数やフィールドと値を全く代入されていない値なしの変数やフィールドとを区別するために使う

変数や構造体フィールドが値無しであることを示すのにnilポインタを使う

nilポインタは値が存在しないのでゼロ値のような操作ができないことを意識する必要がある

sugar2456sugar2456

型、メソッド、インタフェース

Goには他の言語にあるような継承が存在しない、代わりに合成が推奨される

Goの型

抽象型と具象型

型には抽象型と具象型が存在する
具象型はメモリ上に存在する具体的な型、Go言語にはintやfloat64など数値型やstring、構造体、配列、スライス、マップ、チャネルさまざまな具象型が存在する

抽象型はインターフェイスを用いて表現される
インターフェイスは値を直接持たずに他の型の実装通じて間接的にその方の振る舞いを記述する

基底型

基底型とはその型のベースになるような型で、goに事前宣言された型(int, float64)などがそれに該当する

Goの型の定義

以下のような構造体では構造体リテラルを基底型として持ちます

type Person struct {
    FirstName string
    LastName string
    Age int
}

メソッド

ユーザが定義した型に付随する関数を定義できる。
これを型メソッドという。

このようなメソッドはパッケージブロックのレベルで定義される。
funcにレシーバーが追加されている。
レシーバーを渡すことで構造体の情報を簡単にメソッドに渡すことができる

type Person struct {
    FirstName string
    LastName string
    Age int
}

// 型Personに付随するメソッドStringを定義
// このメソッドは「レシーバー」という特別な引数を持つ関数
// レシーバーpにはPerson型のインスタンスが指定されている
func (p Person) String() string {
    return fmt.Sprintf("%s %s:年齢%d歳", p.LastName, p.FirstName, p.Age)
}

ポインタ型レシーバーと値型レシーバー

レシーバーにはポインタ型と値型のレシーバーが存在する

  • メソッドがレシーバーの値を変更するなら、ポインタレシーバーを使わなければならない
  • レシーバーがnilの時があるなら、ポインタレシーバーを使わなければならない
  • メソッドがレシーバーの値を変更しないなら、値レシーバーを使うことができる
type Counter struct {
	total      int
	lastUpdate time.Time
}

// 値を更新するのでポインタ
func (c *Counter) Increment() {
	c.total++
	c.lastUpdate = time.Now()
}

// 値を参照しているのでコピー
func (c Counter) String() string {
	return fmt.Sprintf("total: %d, lastUpdate: %v", c.total, c.lastUpdate)
}
// この関数を呼んでもコピーなので内部のカウントが増えない
func doUpdateWrong(c Counter) {
	c.Increment()
	fmt.Println("Inside doUpdateWrong:", c)
}
// この関数をよぶとポインタなのでカウントが増える
func doUpdateRight(c *Counter) {
	c.Increment()
	fmt.Println("Inside doUpdateRight:", c)
}

nilインスタンスへの対応

package tree

type IntTree struct {
	Left  *IntTree
	Value int
	Right *IntTree
}

func (it *IntTree) Insert(val int) *IntTree {
	if it == nil {
		return &IntTree{Value: val}
	}
	if val < it.Value {
		it.Left = it.Left.Insert(val)
	} else {
		it.Right = it.Right.Insert(val)
	}
	return it
}

func (it *IntTree) Contains(val int) bool {
	if it == nil {
		return false
	}
	if val == it.Value {
		return true
	} else if val < it.Value {
		return it.Left.Contains(val)
	} else {
		return it.Right.Contains(val)
	}
}

iotaと列挙型

goにはenumが存在しない代わりにiotaという概念がある
iotaを使うとそれ以降の定数の宣言で一ずつ増えることができる

const (
    // 一行目はiotaが0
	UnCategorized DefineValue = iota
    // 二行目が1
	CategoryA
    // 三行目が2
	CategoryB
	CategoryC
)
const (
	const_value1 = 0
    // 二行目はiotaが1なので6の値になる
	const_value2 = iota + 5
    // 前の値からインクリメントして7の値になる
	const_value3
	const_value4
)

埋め込みによる合成

Goには継承が存在しないが合成や昇格が存在する。
Employeeの構造体をManagerに埋め込むことでNameやIDなどを参照することができる

package employee

import "fmt"

type Employee struct {
	Name string
	ID   string
}

func (e Employee) Description() string {
	return fmt.Sprintf("Employee Name: %s, ID: %s", e.Name, e.ID)
}

type Manager struct {
	Employee
	Reports []Employee
}

func (m Manager) FindNewEmployee() []Employee {
	newEmployees := []Employee{
		{Name: "Alice", ID: "E001"},
		{Name: "Bob", ID: "E002"},
	}
	return newEmployees
}

インターフェイス

Goでは抽象型を扱うことができるのがインターフェイスだけ
インターフェイスで定義されたメソッドの集まりをメソッドセットと呼ぶ

type Stringer interface {
    String() string
}

インターフェイスは型安全なダックタイピング

package logic

import "fmt"

type LogicProvider struct{}

func (lp LogicProvider) Process(data string) string {
	fmt.Println("Processing data:", data)
	return "Processed"
}

type Logic interface {
	Process(data string) string
}

type Client struct {
	Logic Logic
}

func (c Client) Program() {
	c.Logic.Process("Some data")
}

実行側のコード

package logic

import "testing"

func TestLogic(t *testing.T) {
	lp := LogicProvider{}
	client := Client{Logic: lp}
	client.Program()
}

埋め込みとインターフェイス

構造体に型を埋め込んだようにインターフェイスにインターフェイスを埋め込むことができる

type Reader interface {
    Read(p []byte) (n int, err error)
}
type Closer interface {
    Close() error
}
type ReaderCloser interface {
    Reader
    Closer
}