概要
Goを勉強してわかったことを少しずつまとめていく
環境構築は以下
基本の形
// パッケージ名の宣言
package "main"
// 別パッケージのインポート
import (
"sample"
// ...
)
// プログラムは「mainパッケージ」のmain関数から始まる。
func main() {
}
パッケージ
別の.go
ファイルのコードを読み込んで使いたい場合は、パッケージ単位でインポートする必要がある。
パッケージとはファイルを束ねるもので、以下のファイルは同じ「sample」パッケージに所属するものになる。
1ディレクトリ内に含まれる.go
のパッケージ名は 1 つでなければならない(※)
パッケージ名はディレクトリ名と異なっていても構わないが、通常は統一する(main パッケージ以外)。
package "sample"
func Sample1() int {
return 1
}
package "sample"
func Sample2() int {
return 2
}
※ただし、xxx
パッケージと xxx_test
パッケージについては、同一ディレクトリ内に存在することが可能(参考:Go Fridayこぼれ話:非公開(unexported)な機能を使ったテスト #golang)
変数と定数
package main
import (
"fmt"
)
// [定数]
// constをつけて宣言する
// 定数が変数と異なる点は以下の通り
// ・型宣言が不要(初期化時に格納する値の型によって決まる)
// ・初期化の際に := ではなく = じゃないとダメ
const Pi = 3.14 // パッケージ外でも呼び出せるっぽい
const pi = 3.14 // パッケージ内のみでしか呼び出せないっぽい
func main() {
// [変数]
// ■初期化しない宣言の場合
// ※数値系の型(intやfloat64)の場合: 0、bool型の場合: false、string型の場合は空文字列""で初期化される
var a, b int
// ■初期化する宣言の場合(var s, t int = 1, 2を省略して以下のように書ける)
s, t := 1, 2
// a, bを出力
fmt.Printf("a: %d\nb: %d\n", a, b)
// =>
// a: 0
// b: 0
// s, tを出力
fmt.Printf("s: %d\nt: %d\n", s, t)
// =>
// s: 1
// t: 2
}
型
主な型
- int 整数型
- float64 (64bitコンピュータの場合)
- string 文字列型
- bool 論理型
文字列
- 基本ダブルクォートで囲む
- バッククォートで囲むと、ヒアドキュメントとなる
- 改行は改行として解釈される、エスケープに使うバックスラッシュもそのまま文字のバックスラッシュ扱いになる
- ダブルクォートをそのまま扱うことができるので、JSON文字列の表現でよく見かける
型変換
package main
import (
"fmt"
)
func main() {
// [型変換]
// 関数「<型名>(<変換対象>)」を使うことで変換できる
// Goでは暗黙的型変換はなく、エラーが発生してしまう
// int型で初期化
a := 1
// a(int型)を出力
fmt.Printf("a: %d(%T)\n", a, a)
// aの値をfloat64型へ変換
b := float64(a) // 1 → 1.000
// b(float64型)を出力
fmt.Printf("b: %f(%T)\n", b, b)
}
整数⇔文字列の変換
数値をそのまま文字列に変換しようと普通にstring(<数値>)とすると、その文字コードの文字として扱われてしまう。
そうしたい場合には、strcnv
パッケージのItoa, Atoi関数を使う。
package main
import (
"fmt"
"strconv"
)
func main() {
// 整数 → 文字列への変換
a := 10
strA := strconv.Itoa(a)
fmt.Printf("型: %T, 値: %v\n", a, a)
fmt.Printf("型: %T, 値: %v\n", strA, strA)
// 文字列 → 整数への変換
strB := "-15"
b, _ := strconv.Atoi(strB)
fmt.Printf("型: %T, 値: %v\n", strB, strB)
fmt.Printf("型: %T, 値: %v\n", b, b)
}
配列
package main
import (
"fmt"
)
func main() {
// [配列]
// インスタンス化: [<要素数>]<型>{<インデックス1の要素>,...,<インデックスNの要素>}
arrayInt := [10]int{}
fmt.Printf("arrayInt: %v(%T)\n", arrayInt, arrayInt) // すべて0で初期化される
arrayInt2 := [10]int{1, 2, 3, 4, 5}
fmt.Printf("arrayInt2: %v(%T)\n", arrayInt2, arrayInt2) // インデックス0〜4まで0で初期化される
}
スライス(動的配列)
package main
import (
"fmt"
)
func main() {
// [スライス]
// インスタンス化: []<要素の型>{<インデックス1の要素>,...,<インデックスNの要素>}
sliceInt := []int{} // 要素なしで初期化される
fmt.Printf("sliceInt: %v(型: %T, キャパシティ: %d, 長さ: %d)\n", sliceInt, sliceInt, cap(sliceInt), len(sliceInt))
sliceInt2 := []int{1, 2, 3, 4, 5} // 与えた要素の値で初期化される
fmt.Printf("sliceInt2: %v(型: %T, キャパシティ: %d, 長さ: %d)\n", sliceInt2, sliceInt2, cap(sliceInt2), len(sliceInt2))
// 要素追加: <スライス> = append(<スライス>, <追加要素1>,..., <追加要素N>)
sliceInt2 = append(sliceInt2, 10, 9, 8, 7, 6)
fmt.Printf("sliceInt2: %v(型: %T, キャパシティ: %d, 長さ: %d)\n", sliceInt2, sliceInt2, cap(sliceInt2), len(sliceInt2))
}
構造体
package main
import (
"fmt"
)
// [構造体]定義
type SampleStruct struct {
a int
b float64
}
func main() {
// [構造体]
// インスタンス生成: {}を使う
sampleStruct1 := SampleStruct{1, 2.0} // パターン1(フィールドの定義順に値を渡して初期化)
sampleStruct2 := SampleStruct{} // パターン2(全フィールド、型に応じたデフォルト値で初期化)
sampleStruct3 := SampleStruct{a: 3} // パターン3(フィールドを指定して初期化。指定されてないものはデフォルト値)
fmt.Printf("sampleStruct1: %+v\n", sampleStruct1)
fmt.Printf("sampleStruct2: %+v\n", sampleStruct2)
fmt.Printf("sampleStruct3: %+v\n", sampleStruct3)
// フィールドへのアクセス(.<フィールド>)
sampleStruct1.a = 4
fmt.Printf("sampleStruct1: %+v\n", sampleStruct1)
}
map
package main
import "fmt"
// 適当な構造体
type SampleStruct struct {
a, b int
}
func main() {
// mapのインスタンス生成
sampleStructByString := make(map[string]SampleStruct)
// キー, バリューの挿入
sampleStructByString["key1"] = SampleStruct{1, 2}
sampleStructByString["key2"] = SampleStruct{2, 3}
fmt.Println(sampleStructByString)
// => map[key1:{1 2} key2:{2 3}]
// キー, バリューの削除
delete(sampleStructByString, "key1")
fmt.Println(sampleStructByString)
// => map[key2:{2 3}]
}
ポインタ
package main
import (
"fmt"
)
type SampleStruct struct {
a int
b float64
}
func main() {
// [ポインタ]
intValue := 32
var intPtr *int
intPtr = &intValue
*intPtr = 40 // intValueが40になる
fmt.Printf("intPtr: %d(%T)\n", intPtr, intPtr)
fmt.Printf("intValue: %d\n", intValue)
// 構造体の場合
sampleStruct := SampleStruct{1, 2.0}
sampleStructPtr := &sampleStruct // ポインタ
// 構造体ポインタを介してフィールド値を代入するときも、(*sampleStructPtr).intValueとせずにsampleStructPtr.intValue でOK
sampleStructPtr.a = 4
fmt.Printf("sampleStructPtr: %d\n", sampleStructPtr)
fmt.Printf("sampleStruct: %+v\n", sampleStruct)
}
分岐処理
if
package main
import (
"fmt"
)
func main() {
// [if文]
// if <条件文> {
// ...
// } else if <条件文> {
// ...
// } else {
// ...
// }
a := 3
if a < 3 {
fmt.Printf("a < 3, a: %d\n", a)
} else if a == 3 {
fmt.Printf("a == 3, a: %d\n", a)
} else {
fmt.Printf("a > 3, a: %d\n", a)
}
// <条件文>では、条件に至るまでの処理を複数行で書ける。(;で区切ることでワンライナーで書く)
// <条件文>で宣言された変数はそのif, else if, elseブロック内でも使える
if b := 1; b * 2 < 2 {
fmt.Printf("b * 2 < 2, b: %d\n", b)
} else if b * 2 == 2 {
fmt.Printf("b * 2 == 2, b: %d\n", b)
} else {
fmt.Printf("b * 2 > 2, b: %d\n", b)
}
}
switch 〜 case
package main
import (
"fmt"
)
func main() {
// [switch 〜 case]
// 他言語と違って、末尾のbreakなしで該当するcaseのみ実行される
switch a := 4; a {
case 1:
fmt.Printf("a is 1.\n")
case 2:
fmt.Printf("a is 2.\n")
default:
fmt.Printf("a is not 1 and 2. a: %d\n", a)
}
}
select 〜 case
ゴルーチン(後述)関連の分岐処理(※ゴルーチンのチャネルを理解していないとなんのことやらわからないと思う)。
「複数のチャネルに対してデータ受信要求を行い、もっとも先に受信可能になったものを処理する」という実装をしたい場面で使う。
以下、
のサンプルコード。
package main
import (
"fmt"
"time"
)
// ゴルーチン3
func main() {
// [バッファなしチャネルの生成]
// <チャネル変数名> := make(chan <やりとりするデータ型>)
ch1 := make(chan int)
ch2 := make(chan int)
// ゴルーチン1
go func() {
// [その他処理] 送信側ゴルーチンで2秒待たす
time.Sleep(time.Second * 2)
// [送信要求]
ch1 <- 3
// [送信]
// [その他処理]
fmt.Printf("[ゴルーチン1]チャネルを介してデータ送信したよ(%v)\n", 3)
}()
// ゴルーチン2
go func() {
// [その他処理] 送信側ゴルーチンで2秒待たす
time.Sleep(time.Second * 2)
// [送信要求]
ch2 <- 6
// [送信]
// [その他処理]
fmt.Printf("[ゴルーチン2]チャネルを介してデータ送信したよ(%v)\n", 6)
}()
fmt.Println("ゴルーチンスケジューラにゴルーチン1,2の処理をすることを要求済み")
// [その他処理] 受信側ゴルーチンで5秒待たす
time.Sleep(time.Second * 5)
// ※for-selectパターン
// (送信要求が絶対に2回である前提なので、
// 受信要求も2回で十分ということでループ回数を2回にしている
// 実際には事前に送信要求の回数がわかっているケースはあまりなさそう)
for i:= 0; i < 2; i++ {
// ※select-case: xxxxxxxxxxxxx
select {
case v1 := <-ch1: // [受信要求(ch1)]
// [受信]
// [その他処理]
fmt.Printf("[ゴルーチン3]チャネルを介してデータ受信したよ(%v)\n", v1)
case v2 := <-ch2: // [受信要求(ch2)]
// [受信]
// [その他処理]
fmt.Printf("[ゴルーチン3]チャネルを介してデータ受信したよ(%v)\n", v2)
}
}
// ゴルーチン3(=メインゴルーチン)の処理が終わって
// ゴルーチン1, 2の処理が最後まで終わらぬまま強制終了されるのを防ぐため十分待ってあげてる
// 実際には待たないと思うので本来必要ではない処理。そもそも待ち方が安全ではない
time.Sleep(time.Second * 10)
// [出力結果]
// ゴルーチンスケジューラにゴルーチン1,2の処理をすることを要求済み
// →5秒ほど待った後に以下が出力される(順不同)。受信要求が上がるまで、
// ゴルーチン1,2の処理は「送信要求」以降進めないようブロックされていることがわかる
// [ゴルーチン1]チャネルを介してデータ送信したよ(3)
// [ゴルーチン2]チャネルを介してデータ送信したよ(6)
// [ゴルーチン3]チャネルを介してデータ受信したよ(3)
// [ゴルーチン3]チャネルを介してデータ受信したよ(6)
}
ループ処理
package main
import (
"fmt"
)
func main() {
// [Forループ]
for i := 0; i < 10; i++ {
fmt.Printf("i: %d\n", i)
}
// 多言語のWhileもforで表す
sum := 1
for sum < 10 {
fmt.Printf("sum: %d¥n", sum)
sum += sum
}
// 無限ループ
// for {
// fmt.Printf("1\n")
// }
// スライスのループ
// range: スライスのindex, valueの組でループできる
sampleSlice := []int{1, 2, 3, 4}
for i, v := range sampleSlice { // ループごとにiにインデックス, vに値が入る
fmt.Printf("sampleSlice[%d]: %d\n", i, v)
}
// range利用時に、i, vそれぞれが不要な場合はアンスコで捨てる(捨てないとコンパイラに怒られる)
// なお、vについては捨てるとき省略可能
for i := range sampleSlice {
fmt.Printf("index: %d\n", i)
}
for _, v := range sampleSlice {
fmt.Printf("value: %d\n", v)
}
}
defer文
// defer文
// 呼び出し元のメソッドのreturn後に処理する関数を記述する
// deferへ渡した関数が同メソッド内に複数ある場合は、後にdeferへ渡した関数ほど先に順に実行される。
for i := 0; i < 10; i++ {
defer fmt.Println(i)
}
関数
package main
import (
"fmt"
)
// [関数]定義 func <関数名>(<引数> <引数の型>,...) <返り値の型>
// 同じ型の引数が続く場合はまとめて型宣言できる。
func sampleFunc1(x, y int) int {
return x + y
}
// 複数の返り値を返すことも可能
func sampleFunc2(x, y string) (string, string) {
return y, x
}
// 返り値の変数を定義しておくと、空のreturnでそれらの変数値が返る(可読性的に短い関数で)
func sampleFunc3(x, y string) (retX, retY string) {
retX, retY = x, y
return
}
func main() {
// [関数実行]
a := sampleFunc1(2, 3)
fmt.Printf("a: %d\n", a)
b, c := sampleFunc2("abc", "def")
fmt.Printf("b: %s, c: %s\n", b, c)
d, e := sampleFunc3("abc", "def")
fmt.Printf("d: %s, e: %s\n", d, e)
}
※関数の引数の型、返り値の型はgo バージョン1.18から追加されたジェネリクスという機能で、動的型付け言語っぽくかけるようになった。
メソッド
Goでは、何らかのインスタンスをレシーバとする関数をメソッドと呼んでいる模様。
Goではクラスの概念がなく、「定義済みの型」に対して別途定義するイメージ。
Goのメソッドは、Rubyでいうところのインスタンスメソッドで、クラスメソッド的な概念もない模様。
なお、「変数レシーバ」へのメソッド定義、「ポインタレシーバ」へのメソッド定義ができるが、
「ポインタレシーバへのメソッド定義」でないと、 「レシーバのインスタンスのコピーを生成してこれに対してメソッドが実行される」 形になってしまい、Rubyでいうところのインスタンスメソッドの動きにはならないっぽい。
なので、 「ポインタレシーバへのメソッド定義が一般的」 らしい。
なお、ポインタレシーバのメソッド定義をしたときは、
(*<独自型のポインタ>).<メソッド>
でなく
<独自型のポインタ>.<メソッド>
で実行できる。
package main
import (
"fmt"
)
// [独自の型定義]
// 構造体の定義
type SampleStruct struct {
a, b, result int
}
// プリミティブ型のエイリアス的な型定義
type MyInt int
// [独自の型へのメソッド定義]
// 構造体の変数レシーバのメソッド定義(一般化すると以下のような形)
// func (<変数レシーバのインスタンス変数名> <構造体の型>) <メソッド名>(<引数>) <返り値の型> {
// }
func (sampleStruct SampleStruct) sum_a_b() int {
return sampleStruct.a + sampleStruct.b
}
// 構造体
// 構造体のポインタレシーバのメソッド定義(一般化すると以下のような形)
// func (<ポインタレシーバのインスタンス変数名> *<構造体の型>) <メソッド名>(<引数>) <返り値の型> {
// }
func (sampleStruct *SampleStruct) sub_a_b() {
sampleStruct.result = sampleStruct.a - sampleStruct.b
}
// エイリアス型へのメソッド定義
func (myInt MyInt) multiple_x(x int) int {
return int(myInt) * x // 再度 MyInt → int に型変換したうえで演算しないとエラーになる(多分MyInt * intの計算処理がGoとして定義されていないから)
}
func main() {
// メソッドを使う
// 構造体
sampleStruct := SampleStruct{a:3, b:2}
// 変数レシーバのメソッド実行
sum_a_b := sampleStruct.sum_a_b()
fmt.Printf("[変数レシーバ] a:%d, b: %d, sum_a_b: %d(%T)\n", sampleStruct.a, sampleStruct.b, sum_a_b, sum_a_b)
// =>
// sum_a_b: 5(int)
// ポインタレシーバのメソッド実行
sampleStruct.sub_a_b()
fmt.Printf("[ポインタレシーバ] a:%d, b: %d, sub_a_b result: %d(%T)\n", sampleStruct.a, sampleStruct.b, sampleStruct.result, sampleStruct.result)
// エイリアス型
myInt := MyInt(2) // int型をMyInt型へ型変換
multiple_x := myInt.multiple_x(4) // 2 * 4が実行されるはず
fmt.Printf("myInt: %d(%T)\n", multiple_x, multiple_x)
// =>
// myInt: 8(int)
}
エイリアス的な独自型に対してメソッドを定義しても各所で型変換が必要で、コードが複雑になるから実務的にはあんまり使われてないんじゃなかろうか?
インターフェース
機能としては以下のようなもの。
- とある同名のメソッドが実装されている型同士(★)を、大きなくくりでの型(インターフェース)としてとらえることができる
- インターフェースを引数にとる関数や、★の型で共通して使える
実装者観点のメリット
- インターフェースで定義されているメソッドが、利用時に使ってほしいメソッドであるという印象を与えることができる
- テストコード書くときに便利
以下、例として
- 構造体: 犬
- フィールド: 移動距離
- メソッド: 歩く、走る
- 構造体: 猫
- フィールド: 移動距離
- メソッド: 歩く、走る
- 構造体: 人
- フィールド: 移動距離
- メソッド: 歩く、走る
3つの構造体と、「歩く」・「走る」のメソッドをもつインターフェース「動物」があるとする場合、以下のような実装になる
package main
import (
"fmt"
)
// [独自の型定義]
// 構造体「犬」の定義
type Dog struct {
travel_distance int
}
// 構造体「猫」の定義
type Cat struct {
travel_distance int
}
// 構造体「人」の定義
type Human struct {
travel_distance int
}
// [独自の型へのメソッド定義]
// 構造体「犬」のポインタレシーバのメソッド「歩く」定義
func (dog *Dog) Walk() {
dog.travel_distance += 4
}
// 構造体「犬」のポインタレシーバのメソッド「走る」定義
func (dog *Dog) Run() {
dog.travel_distance += 4 * 2
}
// 構造体「猫」のポインタレシーバのメソッド「歩く」定義
func (cat *Cat) Walk() {
cat.travel_distance += 3
}
// 構造体「猫」のポインタレシーバのメソッド「走る」定義
func (cat *Cat) Run() {
cat.travel_distance += 3 * 2
}
// 構造体「人」のポインタレシーバのメソッド「歩く」定義
func (human *Human) Walk() {
human.travel_distance += 2
}
// 構造体「人」のポインタレシーバのメソッド「走る」定義
func (human *Human) Run() {
human.travel_distance += 2 * 2
}
// [インターフェース]
// メソッド「Walk」「Run」を持つ型を「動物」というおおきなくくりの型と扱い、外部ではこれらを使ってもらいたいという意思表示
type Animal interface {
Walk()
Run()
}
// インターフェース「動物」のメソッド「歩く→走る」定義
// インターフェースのRun(), Walk()の実装はポインタレシーバのメソッドとしているため、
// Animalの型は、 「*Dog」, 「*Cat」, 「*Human」になることに注意。
func walk_and_run(animal Animal) {
animal.Walk()
animal.Run()
}
func main() {
// [犬]
dog := Dog{}
// 歩く
dog.Walk()
fmt.Printf("犬の現在の移動距離: %d\n", dog.travel_distance)
// => 犬の現在の移動距離: 4
// 走る
dog.Run()
fmt.Printf("犬の現在の移動距離: %d\n", dog.travel_distance)
// => 犬の現在の移動距離: 12
// 歩く→走る
walk_and_run(&dog)
fmt.Printf("犬の現在の移動距離: %d\n", dog.travel_distance)
// => 犬の現在の移動距離: 24
// [猫]
cat := Cat{}
walk_and_run(&cat)
fmt.Printf("猫の現在の移動距離: %d\n", cat.travel_distance)
// => 猫の現在の移動距離: 9
}
インターフェースへインターフェースを埋め込む
Goではインターフェース1にインターフェース2を埋め込むことができる。
このとき、インターフェース2でメソッドAが定義されていたならば、
インターフェース1もメソッドAを定義していることになり、メソッドAの実装をした型は、
インターフェース1の実装として扱われる。
他の言語でいうところのクラスの継承(ただしメンバを伴わない)みたいなイメージだろうか。
参考:
https://qiita.com/momotaro98/items/4f6e2facc40a3f37c3c3
Stringer
fmtパッケージで定義されているインターフェース。
# Stringerのインターフェース定義
type Stringer interface {
String() string
}
構造体の中身をfmt.Printfでカスタマイズして出力させたいときに使う(プリントデバッグ用ってことでいいんだろうか?)。
例えば以下のような形で使う。
package main
import (
"fmt"
)
// [独自の型定義]
// 構造体「犬」の定義
type Dog struct {
name string
age int
}
// 構造体「犬」について、String()実装
// ※値レシーバにしておかないと意図した挙動にならないことに注意
func (dog Dog) String() string {
return fmt.Sprintf("型: Dog\nname: %s\nage: %d", dog.name, dog.age)
}
func main() {
// [犬]
dog := Dog{name: "ポチ", age: 3}
fmt.Printf("%+v\n", dog)
fmt.Printf("%v\n", dog)
fmt.Printf("%s\n", dog)
fmt.Println(dog)
// いずれも以下の出力になる
// => 型: Dog
// name: ポチ
// age: 3
}
その他%dやら%iやら指定してみたが、ひとまずいい感じに置換してくれるのは、
fmt.Printfでの%v, %+vと、fmt.Printlnに変数をぶっこむくらいっぽい?
error
こちらもfmtパッケージで定義されているインターフェース。
# errorのインターフェース定義
type error interface {
Error() string
}
こちらは、メソッド実行時のエラー制御で使う位置づけらしい。以下、使用例。
やってることは、
①独自エラー処理のための構造体定義(フィールドとしては、エラー発生時に知りたい情報の筆頭である「日時」と「エラーメッセージ」
②独自エラー処理のError()実装(エラー発生時の出力文字列定義)
③実際にエラーが発生しうるメソッド内で、
- 返り値の型を「独自エラー」構造体にする
- エラー発生時点で独自エラー処理の構造体インスタンス生成(現在日時とエラーメッセージで初期化しつつ)
- 正常系ではnilを返すようにする
④③の呼び出し元で、返り値がnil以外(=エラー発生時)の場合は、返り値をそのまま出力する(= Error()によるエラー文字列が出力される)
package main
import (
"fmt"
"time"
)
// [独自の型定義]
// 構造体「独自エラー」の定義
type MyError struct {
Now time.Time // 現在時刻
Message string // エラーメッセージ
}
// 構造体「独自エラー」について、Error()実装
// ※ポインタレシーバにしておかないと意図した挙動にならないことに注意
func (myError *MyError) Error() string {
const layout = "2006-01-02 15:04:05 (JST)"
return fmt.Sprintf("[エラー][%s]%s\n", (myError.Now).Format(layout), myError.Message)
}
// 構造体「犬」の定義
type Dog struct {
name string
age int
}
// 構造体「犬」について、String()実装
func (dog *Dog) String() string {
return fmt.Sprintf("[Dogのフィールド値] name: %s, age: %d", dog.name, dog.age)
}
// 構造体「犬」について、名前が「ポチ」だったらエラーが発生する処理を実装
func (dog *Dog) checkName() error {
if dog.name != "ポチ" {
fmt.Printf("構造体「犬」の名前はポチじゃない!(%v)", dog)
return nil
}
return &MyError{
time.Now(),
fmt.Sprintf("構造体「犬」の名前がポチです。(%v)", dog),
}
}
func main() {
// [犬]
dog := Dog{name: "ポチ", age: 3}
if err := dog.checkName(); err != nil {
fmt.Printf("%v", err) // ここを標準出力からログ出力に変えればログとして残せそう
}
// => [エラー][2009-11-10 23:00:00 (JST)]構造体「犬」の名前がポチです。([Dogのフィールド値] name: ポチ, age: 3)
dog2 := Dog{name: "クロ", age: 3}
if err := dog2.checkName(); err != nil {
fmt.Printf("%v", err)
}
// => 構造体「犬」の名前はポチじゃない!([Dogのフィールド値] name: クロ, age: 3)
}
データストリームからの読み込み
ioパッケージで定義されている「Readerインターフェース」は、以下の「Read」メソッドの実装がインターフェース実装の条件になっている。
func (T) Read(b []byte) (n int, err error)
Readerインターフェースを実装したGoの標準ライブラリの機能としては、ファイル読み込み、ネットワーク接続などがあるらしい。
Readメソッドは以下の要件を満たすように実装するっぽい。
- レシーバ: 読み込み元のデータ。
- 引数b: 読み込み先のスライス。
T.Read(b)
を実行するたびに、スライスbへ、bのサイズ分のTのデータが格納される。次回のT.Read(b)
を実行する際には、前回読み込んだデータの次のデータから読み込みが始まる。 - 返り値n: 読み込んだデータのサイズ
- 返り値err: 読み込みによってTのデータ終端に達したときに
io.EOF
が返る
軽く調べてみて、現状しっくりきた存在意義は以下くらいだった。
- メモリを圧迫するような巨大なデータを読み込んでファイルとして保存する(参考: https://note.crohaco.net/2019/golang-buffer/)
- Readerインターフェースが実装されている型Tとして何が来るかわからないときに対応できる(参考: https://zenn.dev/hsaki/books/golang-io-package/viewer/io)
これら以外のケースであんまり意識する必要はなさそうなので、現状は頭のカタスミにおいておくレベルにしておく。
並行処理・ゴルーチン
[Go]プログラミングノート - 並行処理・ゴルーチン(限定共有記事)
よくお世話になりそうな機能
プリントデバッグ
※fmt.Printf の方を覚えておけば困ることはなさそう
package main
import (
"fmt"
"reflect"
)
func main() {
a := 1
b := "abc"
// 1行出力(出力対象間に半角スペース、末尾に改行が自動挿入される)
// 具体例.
fmt.Println("a:", a, "(", reflect.TypeOf(a), ")")
fmt.Println("b:", b, "(", reflect.TypeOf(b), ")")
// => a: 1 ( int )
// b: abc ( string )
// フォーマット指定出力(半角スペースや改行の有無を細かくコントロールしたいならこっち)
// %t: bool
// %d: int, uint系
// %g: float系
// %s: string
// %q: string "" で囲んで出力。空文字列もわかりやすい。
// %p: ポインタ
// %T: 型表示
// %v: 表示対象の型からいい感じに表示してくれる(配列、スライスも対応)
// %+v: %vから一歩進んで構造体の中身もいい感じに表示してくれる
fmt.Printf("a: %d(%T)\n", a, a)
fmt.Printf("b: %s(%T)\n", b, b)
// => a: 1(int)
// b: abc(string)
}
フォーマット指定子の記述に厳密な現場でないならば、基本 %v
を使っといて問題があったら他の指定子に差し替えるくらいの意識でいるのが効率的。
(可読性的には、厳密に指定してあげた方がよいのは確か)
型と中身を確認したいがそもそも型がわからなくて(追うのがめんどくさくて)中身を指定するためのフォーマット指定子どうしたらいいかわからねぇ・・・というときにはとりあえず以下のようにして確認するのがよさそう。
fmt.Printf("型: %T, 値: %+v", <表示対象>, <表示対象>)
命名規則
変数・関数
以下のルールで書く。このルールの遵守=権限の設定になる。
- private: キャメルケース(xxxXxx)
- public: パスカルケース(XxxXxx)
ファイルパス
スネークケース(xxx_xxx)で書く
参考
慣習系
メソッドについて