Go言語を基礎から徹底的に叩き込む〜#4-1 基本構文〜
概要
個人的な理由ですが、6 月から新たな環境で仕事することになり、Go 言語でのバックエンド開発がメインになります。
プライベートでは Go を触ってきましたが、ここで改めて Go を基礎から徹底的に学習し直そうと思います。
Go 言語基本構文
変数と関数
変数の定義
- 型を明示的に定義する場合
var id string
id = "hoge"
- 型推論を使う場合
id := "hoge"
関数の定義
func testFunc() {
fmt.Println("testFunc")
}
- 戻り値あり
func testFunc(msg string) string {
return "testFunc " + msg
}
- 戻り値複数あり
go の関数は戻り値を複数返却することができます。
func testFunc(msg string) (string, string) {
return "testFunc " + msg, msg
}
func main() {
s1, s2 := testFunc("hoge")
fmt.Println(s1, s2)
}
main 関数
main 関数はエントリーポイントとなります。
func main() {
// エントリーポイント
}
無名関数
以下、無名関数の定義と実行
f := func(message string) {
fmt.Println(message)
}
f("Hello")
以下、無名関数を定義して即時実行する例
func(message string) {
fmt.Println(message)
}("hello")
クロージャー
関数内で関数を定義する(関数を返す)クロージャーを定義することができる。
func incrementGenerator() func() int {
x := 0
return func() int {
x++
return x
}
}
func main() {
counter := incrementGenerator()
fmt.Println(counter()) //1
fmt.Println(counter()) //2
fmt.Println(counter()) //3
fmt.Println(counter()) //4
counter2 := incrementGenerator()
fmt.Println(counter2()) //1
fmt.Println(counter2()) //2
}
クロージャーの使われ方は、以下のように関数のベースとなる値をケースバイケースで変更したいといった時に使われることがある。
func makeMulti(base int) func(int) int {
return func(factor int) int {
return base * factor
}
}
func main() {
multi2 := makeMulti(2) // 2倍する関数
fmt.Println(multi2(3)) // 6
fmt.Println(multi2(4)) // 8
multi4 := makeMulti(4) //4倍する関数
fmt.Println(multi4(3)) // 12
fmt.Println(multi4(4)) // 16
}
可変長引数
可変長引数は以下のように定義する。
関数内では、スライスとして使用可能。
func foo(params ...int) {
for _, param := range params {
fmt.Println(param)
}
}
func main() {
foo(1, 2, 3) // 引数:[1,2,3]
}
上記の可変長引数の関数に対してスライスを展開して渡す場合
sl := []int{4, 5, 6}
foo(sl...)
基本型
基本型については多言語とそこまで変わりないので、表にざっとまとめておきます!
型 | 内容 | ゼロ値 |
---|---|---|
boole | 論理型。true or false | false |
int8 | 整数型 8 ビット。符号付き(-128~128) | 0 |
int16 | 整数型 16 ビット。符号付き | 0 |
int32 | 整数型 32 ビット。符号付き | 0 |
int64 | 整数型 54 ビット。符号付き | 0 |
int | CPU に応じて 32 ビット or 64 ビットになる | 0 |
uint8 | 整数型 8 ビット。符号なし(0~255) | 0 |
uint16 | 整数型 16 ビット。符号なし | 0 |
uint32 | 整数型 32 ビット。符号なし | 0 |
uint64 | 整数型 54 ビット。符号なし | 0 |
uint | CPU に応じて 32 ビット or 64 ビットになる | 0 |
byte | uint8 の別名 | 0 |
float32 | 浮動小数点 32 ビット | 0 |
float64 | 浮動小数点 64 ビット | 0 |
string | 文字列 | ""(空文字) |
rune | int32 の別名(ひとつの「コードポイント」を表現する型) | 0 |
ゼロ値
上記の図でしれっとゼロ値について記載しておりますが、ゼロ値とは宣言されたが何も割り当てられていない場合のデフォルト値のことを言います。
型変換
数値型のキャスト
数値型どうしの変換についてはキャストで変換可能
f := 1.0
i := int(f)
fmt.Printf("%T %v\n", i, i) //int, 1
f2 := float64(i)
fmt.Printf("%T %v\n", f2, f2) //float64 1
文字列型のキャスト
文字列と byte についても数値型と同様にキャストで変換可能
s := "Hello"
b := s[0] // byte型
fmt.Println(b) // 72(「H」のASCIIコード)
s2 := string(b)
fmt.Println(s2) // H
int → string
int 型から string 型、またはその逆については、全く違う型になるため、上述のキャストは使えない。
これらの型変換にはstrconv
パッケージのメソッドを使用する。
num := 10
s := strconv.Itoa(num)
string → int
string → int に変換する場合は、失敗する可能性があるため、第二引数はエラーが返ってくる。
s := "1"
i, err := strconv.Atoi(id)
ちなみにItoa
やAtoi
のi
はint
というのは容易に分かるが、なぜA
が文字列かというと、これはASCII
である。
配列とスライス
Go には配列とよく似たスライスというものがある。違いは配列は宣言時に要素数を決定する必要があり、スライスは要素数が可変長であるということ。
配列の宣言
以下で要素数 3 の配列を定義
arr := [3]int{1, 2, 3}
要素の範囲外を指定して参照したり、追加すると panic が発生する。
基本的に配列は使われず、スライスが使われる。(配列は制限が多いため)
スライスの宣言
スライスの定義と要素の追加
var sli []string
sli = append(sli, "a")
初期化と同時に要素を追加する場合
sli := []string{"a", "b"}
基本型のみではなく、自分が定義した構造体の型の定義も可能
type User struct {
Id string
Name string
}
sli := []User{
{
Id: "1",
Name: "Mike",
},
{
Id: "2",
Name: "John",
},
}
スライスの比較
スライスは比較可能ではない。(==
や!=
などで比較するとコンパイルエラーになる。)
スライスと比較できるのはnil
だけである。(スライスのゼロ値はnil
)
スライス用の関数
多言語でも配列や List を操作する関数が用意されているが、Go のスライスも同様である。
len
スライスの要素数を返す関数
sli := []int{1, 2, 3}
fmt.Println(len(sli)) //3
append
スライスの末尾に要素を追加する関数。(要素数 0 でも、もちろん使用可能)
append は第一引数で渡されたスライスに対して第二引数以降で渡された要素を追加したスライスを返す
よって、返り値を受け取ってあげないと元の変数の値は変更されない。
sli := []int{1, 2, 3}
sli = append(sli, 4)
fmt.Println(sli) //[1,2,3,4]
複数要素を追加する場合
sli := []int{1, 2, 3}
sli = append(sli, 4, 5, 6)
fmt.Println(sli) //[1,2,3,4,5,6]
...
演算子を使用することで、スライスの要素を展開できる。
これを利用し、スライスにスライスを追加する例が以下
sli := []int{1, 2, 3}
sli2 := []int{4, 5, 6}
sli = append(sli, sli2...)
fmt.Println(sli) //[1,2,3,4,5,6]
cap
スライス(配列)は、要素数とは別にキャパシティという概念がある。
これは要素数よりも大きくなるが、理由はメモリ上に要素数分の値を格納できるようにあらかじめ確保しておく容量のようなものであるため。
このキャパシティを調べる関数がcap
関数である。
sli := []int{1, 2, 3}
fmt.Println(cap(sli))
make
make
を使って、要素数とキャパシティを指定できる。
以下は要素数 5、キャパシティ 5 のスライスとなる。
sli := make([]int, 5)
fmt.Println(cap(sli))
要素数とキャパシティを分けたい場合は以下(要素数 5、キャパシティ 10 のスライス)
sli := make([]int, 5, 10)
fmt.Println(cap(sli))
スライスのスライス
スライスから一部を切り出すことができる。(それをスライスのスライスという)
sli := []int{1, 2, 3, 4, 5}
a := sli[:2]
fmt.Println(a) // [1,2]
b := sli[2:]
fmt.Println(b) //[3,4,5]
c := sli[1:4]
fmt.Println(c) //[2,3,4]
d := sli[:]
fmt.Println(d) //[1,2,3,4,5]
マップ
スライス(配列)は順序のあるデータ構造ですが、マップは順序のない key,value 形式のデータ構造
以下、key が string,value が int のマップ
m := map[string]int{
"apple": 100, "banana": 200,
}
m["banana"] = 300 // key=bananaのvalueを変更
m["orange"] = 500 // 新しいkeyにvalueを追加
まだ定義されていない key については value の型のゼロ値が出力されます。
m := map[string]int{
"apple": 100, "banana": 200,
}
fmt.Println(m["orange"]) //0
カンマ OK イディオム
上記でマップに指定されていない key を指定した場合、value のゼロ値が返ってくることを確認しましたが、
その key がゼロ値と紐づけられているのか、まだ指定されていないのか値を見ただけでは識別できない。
そのため、Go にはカンマ OK イディオムというものがある。
m := map[string]int{
"apple": 100, "banana": 200,
}
v, ok := m["orange"]
fmt.Println(v, ok) //0, false
v, ok = m["apple"]
fmt.Println(v, ok) //100, true
上記のように第一戻り値には値が、第二戻り値にはその key が指定されているか否かが bool 型で返ってくる。
第二戻り値に関しては受け取らなくてもエラーにはならない
v = m["apple"]
nil マップの注意点
nil のマップに対して key,value をセットしようとする panic が発生する点に注意
var m map[string]int
m["apple"] = 100 //panic
以下は OK
m := map[string]int{}
m["apple"] = 100
if 文
条件式の括弧は不要
if id == "10" {
// if
} else if id == "20" {
// else if
} else {
// else
}
条件式に変数の定義も可能
id := "10"
if n , _ := strconv.Atoi(id); n > 20 {
// if
} else if n > 10 {
// else if
} else {
// else
}
この場合、変数 n のスコープは else ブロックまでになります。if 文のブロック外から n を呼ぶことはできません!
これはこれで必要な時に必要な変数のみ定義できるので便利です!
switch 文
switch 文の break は不要です
num := 1
switch num {
case 1:
fmt.Println("1です")
case 2:
fmt.Println("2です")
case 3:
fmt.Println("3です")
default:
fmt.Println("No!!!")
}
複数の条件の場合の処理もまとめて記載できます。上記例で言うと 2or3 の場合の処理をまとめて以下のように記載できます。
switch num {
case 1:
fmt.Println("1です")
case 2, 3:
fmt.Println("2か3です")
default:
fmt.Println("No!!!")
}
ブランク switch
go の switch 文では比較対象の変数を定義しないことができる。これをブランク switch という。
この場合、各ケースに論理的な比較条件が記載できる。
num := 3
switch {
case num > 10:
fmt.Println("10より大きい")
case num > 0:
fmt.Println("1以上9以下")
default:
fmt.Println("0以下")
}
if 文同様にこれも OK
switch s := strconv.Itoa(num); {
case len(s) > 10:
fmt.Println("10文字より多い")
case len(s) > 0:
fmt.Println("1文字以上。9文字以内")
default:
fmt.Println("Empty")
}
for 文
for 文には以下の 4 つの種類がある
- 一般的な for 文
- 条件のみを指定する for 文
- 無限ループ
- for-range
一般的な for 文
お馴染みの for 文
for i := 0; i < 10; i++ {
fmt.Println(i)
}
条件のみを指定する for 文
多言語の while 文と同様の形式
i := 1
for i < 10 {
fmt.Println(i)
i++
}
無限ループ
for 文を抜け出すためには break で抜ける。
i := 1
for {
fmt.Println(i)
i++
if i > 9 {
break
}
}
ちなみにbreak
同様に多言語にもあるcontinue
も for 文で使用可
i := 0
for {
i++
if i == 3 || i == 5 {
continue
}
fmt.Println(i)
if i > 9 {
break
}
}
for-range
for-range はスライスの要素を一つずつ取り出す場合によく使用される。
JavaScript などでいう for-of と同様。
sli := []string{"a", "b", "c"}
for i, v := range sli {
fmt.Println(i, v)
// 0 a
// 1 b
// 2 c
}
defer
defer は遅延実行したい処理を登録できる。
defer が定義された関数が終了した際に実行されるため、以下のような順で実行される。
func testF() {
defer fmt.Println("test defer")
fmt.Println("test")
}
func main() {
defer fmt.Println("main defer")
testF()
fmt.Println("main")
// 出力結果
// test
// test defer
// main
// main defer
}
Go では、他言語の try-catch-finally の構文がないため、最終的に必ず実行したい処理を defer で登録する。(リソースの解放等)
また、defer は LIFO(後入れ先出し・スタック)のため、関数内に複数 defer 文があった場合には後に定義されたものから実行される。
func main() {
defer fmt.Println("1")
defer fmt.Println("2")
defer fmt.Println("3")
// 出力結果
// 3
// 2
// 1
}
log
Go の標準パッケージでlog
が用意されている。
ただ、他の言語のようにステータス(info
やwarn
、error
など)を出力するようなメソッドは標準では用意されていない。
Println, Printf
標準出力の例は以下。日時とともにメッセージを出力してくれる。
log.Println("log") //2024/04/14 16:02:09 log
log.Printf("%s", "log") //2024/04/14 16:02:09 log
Fatalln, Fatalf
Ftalln や Fatalf を使用するとその時点でプログラムが終了してしまう点に注意
log.Fatalln("エラー!") //2024/04/14 16:06:00 エラー!
// 以下は出力されない。
fmt.Println("Hello!")
エラーハンドリング
上記でも記述したが、Go には try-catch-finally のような構文はない。
Go では、関数内で起きたエラーを引数で返すのが一般的なため、以下のように if 文でエラーハンドリングされる。
file, err := os.Open("./test.text")
defer file.Close()
if err != nil {
log.Fatalln(err)
}
上記のとおり、第一引数でも正常終了した場合の戻り値、第二引数で error が返ってくる関数が多い。
また、エラーしか返さない関数の場合、さらに省略して以下のようにエラーハンドリングされることもある。if err := testFn("test"); err != nil {...}
func testFn(msg string) error {
file, err := os.Open("./test.text")
if err != nil {
return err
}
defer file.Close()
// ここでファイルに書き込みを実行
return nil
}
func main() {
if err := testFn("test"); err != nil {
log.Fatalln(err)
}
}
エラーを作成する
標準パッケージでエラーを作成する方法は以下の 2 通り
- errors.New 関数
- fmt.Errorf 関数
使い分けは%d
などで埋め込みの文字列を使いたい場合は、fmt.Errorf
。そうでなければerrors.New
を使えば良いと思います。
パニックとリカバリー
パニックとは Go のランタイムが何をして良いか分からない状態を指し、パニックが発生すると強制終了される。
また、パニックで強制終了する際は、その関数内で定義されている defer が実行され、次のその呼び出し元の defer...と main 関数まで続く。
func connectDB() {
panic("DBに接続できない。パニック!")
}
func save() {
connectDB()
}
func main() {
save() // パニックが発生し、強制終了
}
上記はパニックを明示的に発生させているが、Go ではこのように自らパニックを発生させるようなことは基本的にしない。(エラーハンドリングで見たようにerror
を返すようにする)
そもそもパニックはもうどうしようない場合に発生するものである。
パニックが発生した場合に強制終了しないようにする場合は、リカバリーをする。
func connectDB() {
panic("DBに接続できない。パニック!")
}
func save() {
defer func() {
s := recover()
fmt.Println(s)
}()
connectDB()
}
func main() {
save()
}
上記のようにパニックが発生する前にリカバリーしてやる必要がある。
以下の場合だとパニックが先に発生し、強制終了される。
func connectDB() {
panic("DBに接続できない。パニック!")
}
func save() {
connectDB() // 先にパニックが発生し、強制終了
defer func() {
s := recover()
fmt.Println(s)
}()
}
func main() {
save()
}
ポインタ
ポインタとはある値が保存されているメモリ内の位置(アドレス)を表す変数のこと。
C 言語などと違い、Go はポインタの概念もあるが、併せてガベージコレクタもあるため、メモリ管理は容易。(C 言語とは違いポインタに関する演算などはできない)
ポインタ型の宣言
型の前に*
をつけるとポインタ型になる。以下は int のポインタ型をvar
で宣言した場合
var ptr *int
ポインタのゼロ値
ポインタのゼロ値は nil
アドレス演算子
&
はアドレス演算子といい、変数の前に&
をつけるとその変数のアドレスを返す。
返された値はポインタ型となる。
var num int = 10
var ptr *int = &num
ポインタ型のデリファレンス
*
は間接参照のための演算子であり、ポインタ型の変数の前に*
をつけるとそのアドレスに保存されている値を返す。これをデリファレンスという。
var num int = 10
var ptr *int = &num
fmt.Println(ptr) // アドレス値を返す(例:0xc000094018)
fmt.Println(*ptr) // アドレスに保存されている値を返す(10)
nil のポインタ型をデリファレンスすると panic となるので、必要であれば nil でないことをチェックしてからデリファレンスすると良い。
ポインタとはミュータブルの証拠
ポインタが使用されるケースは変数の値を変更する時が多い。
例えば以下のケースは値を変更できず、main 関数内の n は 1 のままである。
func update(px int) {
px = 20
}
func main() {
n := 1
update(n)
fmt.Println(n) //1
}
なぜこのようになるかというと、関数の引数は渡された値そのものではなく、コピーが渡ってくるからである。
よって、いくら関数内で引数の値をいくら書き換えても main 関数のn
に影響を与えることはない。
この挙動はイミュータブルであり、予期せぬ値の書き換えが起こらない(関数の副作用を受けない)ため、バグを含みにくい。
関数内で引数の値を書き換えるためにはポインター型で渡す必要がある。
しかし、注意点としてはポインター型のアドレス値を変更しても呼び出し側の変数は変更されない。
例として、以下のupdate1
では、main 関数側の値を変更することはできない。
呼び出し元の変数を変更するにはupdate2
のようにポインター型の値を変更する必要がある。
func update1(px *int) {
x := 10
px = &x //関数内で定義した変数のアドレス値を渡す
}
func update2(px *int) {
*px = 10 //アドレス値の値を書き換える
}
func main() {
n1 := 1
update1(&n1)
fmt.Println(n1) //1
n2 := 1
update2(&n2)
fmt.Println(n2) //10
}
このような挙動になる理由としては、上述した通り、引数で渡された値は全てコピーであるため、コピー元のアドレスをいくら変更しても呼び出し元への影響はない。
影響を与えるためには、引数のポインターが指すアドレス先の値を変更する必要があるためである。
構造体
Go にはクラスはないが、構造体がある。
クラスとの違いはざっくりいうと継承ができない。
type User struct {
Id string
Name string
}
func main() {
var user User
user.Id = "123"
user.Name = "hoge"
fmt.Println(user)
}
上記構造体を初期化するには大きく二つの方法がある。User を例にすると
① 値をカンマ区切りにする
user := User {
"123", // id
"hoge", // name
}
この場合、
- 全てのフィールドの値を初期化する必要がある
- 構造体で定義したフィールドの順に初期化値を定義する必要がある
② フィールド名と一緒に初期化する
user := User{
Name: "hoge",
Id: "123",
}
この場合、
- 全てのフィールドを初期化する必要はない
- 好きな順番で定義できる
無名構造体
変数に対して直接構造体を定義することもできる。
var user struct {
id string
name string
}
user.id = "123"
user.name = "hoge"
宣言時に初期化する場合
user2 := struct {
id string
name string
}{
id: "123",
name: "hoge",
}
構造体の比較と型変換
- 全てのフィールドが比較可能な型である場合、2 つの構造体は比較可能(スライスやマップがフィールドにある場合は、比較不可)
- 全てのフィールド名と型、順番が同じ場合に限り別の構造体へ型変換が可能
① 型変換可能な構造体
type User1 struct {
Id string
Name string
}
type User2 struct {
Id string
Name string
}
→==
を使って、これらの構造体のインスタンスを比較することはできない。
② 型変換不可能な場合
type User1 struct {
Id string
Name string
}
type User2 struct {
Name string
Id string
}
→User1 のインスタンスを User2 には型変換できない。フィールドの順番が違うため
type User1 struct {
Id string
Name string
}
type User2 struct {
Id string
FirstName string
}
→ これも型変換できない。フィールド名が違うため
無名構造体の比較について
2 つの構造体変数を比較する場合、少なくとも片方に無名構造体のフィールドがあると型変換なしで比較できる場合がある。
条件はどちらも同じフィールド名、型、順番の必要がある。
type User struct {
Id string
Name string
}
u1 := User{
Id: "123",
Name: "hoge",
}
var u2 struct {
Id string
Name string
}
u2 = u1
fmt.Println(u1 == u2)//true
メソッド
構造体にメソッドを生やすことができる。
type User struct {
Id string
Name string
}
func (u User) PrintId() {
fmt.Println(u.Id)
}
上記は User 構造体に PrintId というメソッドを生やした例。
メソッド名の前に定義されている(u User)
はレシーバといい、メソッドを生やしたい構造体を定義する
使用例は以下
u := User{
Id: "123",
Name: "Mike",
}
u.PrintId() //123
この例で分かるようにメソッドPrintId
は User 構造体のインスタンスのプロパティを参照することができる。
レシーバとポインタレシーバ
まず以下のように User 構造体のプロパティを変更するメソッドを定義したとする。
type User struct {
Id string
Name string
}
func (u User) ChangeUserName(userName string) {
u.Name = userName
}
この場合、実行すると結果は以下のようになる。
u := User{
Id: "123",
Name: "Mike",
}
u.ChangeUserName("John")
fmt.Println(u) //{123 Mike}
要するに ChangeUserName メソッドでは User 構造体のインスタンスのプロパティを変更できていない。
理由としては、単にレシーバでメソッドを定義すると、インスタンスのコピーが渡されるため、プロパティの変更ができない。
インスタンスのプロパティを変更する場合は以下のようにレシーバをポインタで定義する必要がある
func (u *User) ChangeUserName(userName string) {
u.Name = userName
}
このようにレシーバの構造体をポインタで定義することをポインタレシーバという。
iota と列挙型
Go には多言語ではよくある列挙型(Enum)という概念はありませんが、iota を使用することで同様のことはできます。
// まずは列挙型のように扱いたい型を定義する
type WorkCategory int
const (
Uncategory WorkCategory = iota //未分類
Engineer // エンジニア
Teacher //教師
Athlete //アスリート
Comedian //芸人
)
type User struct {
Id string
Name string
Work WorkCategory
}
u := User{
Id: "123",
Name: "Mike",
Work: Engineer,
}
fmt.Println(u.Work) //1
このようにiota
を使用することで 0 スタートで徐々に増加する値を一連の定数に割り当てられる。
もし、文字列として一連の定数を割り当てたい場合は以下のようにする
type Authority string
const (
Admin Authority = Authority("管理者")
General Authority = Authority("一般ユーザー")
)
type User struct {
Id string
Name string
Auth Authority
}
こうすることで、Authority 型はAdmin
or General
のみが指定可能。
interface
interface は実装を提供しない型、つまり抽象型である。
interface には以下の 2 つの側面がある。
- メソッドの集合
- 任意の型(Go1.18 から導入された any 型と同義)
メソッドの集合としての interface
以下のような抽象的なメソッドの集合として定義できる。
type Human interface {
Greeting()
Say(message string) string
}
interface で定義された全てのメソッドを実装した構造体を定義することで、interface の型として扱える。
以下の例でいうと、interface である Human 型で定義したmike
に Person のインスタンスを渡せる。
type Human interface {
Greeting()
Say(message string) string
}
type Person struct {
Name string
}
func (p Person) Greeting() {
fmt.Printf("My Name is %s \n", p.Name)
}
func (p Person) Say(message string) string {
return fmt.Sprintf("%s is say %s", p.Name, message)
}
func main() {
var mike Human = Person{"Mike"}
mike.Greeting()
fmt.Println(mike.Say("Hello"))
}
注意点としては、interface で定義したメソッドの
- メソッド名
- 引数
- 戻り値
が全く同じものを構造体のメソッドとして持たせること。一つでも違えば、その構造体は interface を実装したとは言えない。
このように interface を利用することで、違う型同士を同じ型として扱うことができる。
以下では、Person 型と Dog 型のインスタンスを Animal 型として共通の使い方をする例
type Animal interface {
Cry()
}
type Person struct {
Name string
}
func (p Person) Cry() {
fmt.Println("en-en")
}
type Dog struct {
Name string
}
func (d Dog) Cry() {
fmt.Println("wan-wan")
}
func main() {
mike := Person{"Mike"}
hachi := Dog{"Hachi"}
/*
Person型とDog型は、Animalのインターフェースで定義した関数を全て実装しているので
Animal型として扱える。
*/
as := []Animal{
mike, hachi,
}
for _, v := range as {
v.Cry()
}
}
また、構造体のプロパティを書き換えたい場合、ポインターレシーバを使うことになるが、
その場合、以下のように&Person{"Mike"}
のようにアドレス値として渡してあげないと Human インターフェースを実装したことにはならない。
type Human interface {
ChangeName(name string)
}
type Person struct {
Name string
}
func (p *Person) ChangeName(name string) {
p.Name = name
}
func main() {
var mike Human = &Person{"Mike"} //Person{"Mike"} とするとHuman型の変数には代入できないのでコンパイルエラーが発生
mike.ChangeName("Michael")
fmt.Println(mike) //&{Michael}
}
任意の型としての interface
空の interface を定義した場合、任意の型、つまり any 型となる。
var i interface{} = 10
fmt.Println(i) //10
i = "hoge"
fmt.Println(i) //hoge
型アサーションと SwitchType 文
空の interface 型として定義した変数に何かしらの処理を加える場合、型アサーションして特定の型にする必要がある。これを型アサーションという。
func double(i interface{}) {
ii := i.(int)
ii *= 2
fmt.Println(ii)
}
double(10)
上記は引数のi
に int が渡ってくると仮定してii := i.(int)
で int 型にアサーションしている。
しかし、このように任意の型を引数とした場合、int 型が渡ってくるとは限らないため、それぞれの型で引数が渡された時の処理を記載する方法が switch type 文である。
func do(i interface{}) {
switch v := i.(type) {
case int:
fmt.Println(v * 2)
case string:
fmt.Println(v + "!")
default:
fmt.Println("I dou't know")
}
}
Stringer
構造体のインスタンスを fmt パッケージを用いて出力した場合、デフォルトでは{Mike 18}
と表示される。
type Person struct {
Name string
Age int
}
func main() {
mike := Person{"Mike", 18}
fmt.Println(mike) //{Mike 18}
}
この出力を自分のカスタムした形で書き換えたい場合に Stringer を使う。
type Person struct {
Name string
Age int
}
func (p Person) String() string {
return fmt.Sprintf("Name is %s. Age is %d", p.Name, p.Age)
}
func main() {
mike := Person{"Mike", 18}
fmt.Println(mike) //Name is Mike. Age is 18
}
このように出力をカスタムできるのはfmt
パッケージに以下の Stringer という interface が定義されており、この String メソッドを上書くことができるから。
// Stringer is implemented by any value that has a String method,
// which defines the “native” format for that value.
// The String method is used to print values passed as an operand
// to any format that accepts a string or to an unformatted printer
// such as Print.
type Stringer interface {
String() string
}
要はString()
というメソッドは特別な意味を持つということを意識しておくと良い。
カスタムエラー
Stringer と同様にError()
というメソッドも特別な意味を持つ。
例えばエラーメッセージを自分なりにカスタムしたい場合にError()
メソッドを定義する。
type UserNotFound struct {
UserName string
}
func (u *UserNotFound) Error() string {
return fmt.Sprintf("Error Name : %s", u.UserName)
}
func main() {
mike := UserNotFound{"Mike"}
fmt.Println(mike.Error()) //Error Name : Mike
}
ここで注意点として、Error()
メソッドはポインターレシーバとするのが吉とされる。
詳細は割愛するが、エラー同士の比較の際に違いが出る。
参考
まとめ
基本構文は一旦、この辺にしておきます...
次は UnitTest のライブラリについて見ていきたいと思います。
Discussion