Go の本当に簡単なプログラムの実行でズブズブにハマった。何にハマったのかと言うとやりたいことは、単純に、JSON のパースをしたいだけなのに、簡単に panic が起こって、問題が特定できないことだ。他の人がこの問題をどうやって解決しているのかはぜひ知りたいところ。
$ go run main.go
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x10b7834]
goroutine 1 [running]:
main.main()
/Users/ushio/Codes/terraform/src/github.com/TsuyoshiUshio/SpikeAutorest/cmd/spike2/main.go:51 +0x320
こんなエラーが簡単に起こってしまう。普段 C# とかで甘やかされていると、びっくりしてしまうだろう。出ている場所はわかるのだが、それが何を表しているのか一瞬わからない。C# とか、Java とかに慣れてると、それらの言語では起こらない箇所で簡単に、このエラーがでる。
例えば実用的だけど、簡単なパースのプログラムの例。
package main
import (
"encoding/json"
"fmt"
"log"
"os"
)
type DataFactoryCreateOrUpdateParameter struct {
ID *string `json:"id,omitempty"`
Name *string `json:"name,omitempty"`
Type *string `json:"type,omitempty"`
Location *string `json:"location,omitempty"`
Tags *map[string]*string `json:"tags,omitempty"`
*DataFactoryCreateOrUpdateProperties `json:"properties,omitempty"`
}
type DataFactoryCreateOrUpdateProperties struct {
DataFactoryID *string `json:"datafactoryId,omitempty"`
ProvisioningState *string `json:"provisioningState,omitempty"`
Error *string `json:"error,omitempty"`
ErrorMessage *string `json:"errorMessage,omitempty"`
}
func main() {
logger := log.New(os.Stdout, "JSON PARSE", log.Lshortfile)
jsonFile := `
{
"name": "gosdktestadfname01",
"id": "/subscriptions/somesubscription/resourcegroups/spikeadf01/providers/Microsoft.DataFactory/datafactories/gosdktestadfname01",
"type": "Microsoft.DataFactory/datafactories",
"location": "West US",
"tags": {},
"properties": {
"dataFactoryId": "1d55e29b-14e8-4e47-a4b6-a61c77c2854a",
"provisioningState": "PendingCreation",
"error": null,
"errorMessage": null
}
}
`
byteString := []byte(jsonFile)
v := &DataFactoryCreateOrUpdateParameter{}
json.Unmarshal(byteString, v)
logger.Printf("Id: %s Name: %s ProvisioningState: %s", *v.ID, *v.Name, *v.DataFactoryCreateOrUpdateProperties.ProvisioningState)
fmt.Printf("Id: %s Name: %s ProvisioningState: %s Error: %s", *v.ID, *v.Name, *v.ProvisioningState, ToString(v.DataFactoryCreateOrUpdateProperties.Error))
}
これのコードは、先ほどのエラーが出る。どこかと言うと、
fmt.Printf("Id: %s Name: %s ProvisioningState: %s Error: %s", *v.ID, *v.Name, *v.ProvisioningState, ToString(v.DataFactoryCreateOrUpdateProperties.Error))
ここの箇所。パースすると、v.DataFactoryCreateOrUpdateProperties.Error
は、nil
になる。C# とかの感覚だと、string の変数が、null になるのは不思議な感覚じゃ無いと思うけど、ここでのポイントは、string
に nil
などと言うものは存在しない。しかし、パース元のJSONでは、null になっている。そう言う場合どうなるかと言うと、ポインタが nil になるっぽい。
だから、このケースでは、v.DataFactoryCreateOrUpdateProperties.Error
へのポインタ自体がnil
なので、参照した瞬間に先のエラーになる。
他にこのエラーを起こそうと思ったら簡単。
v := &DataFactoryCreateOrUpdateParameter{}
を
v := DataFactoryCreateOrUpdateParameter{}
してあげても簡単に起きてしまう。
対策
じゃあどうしたらいいだろう。上記のようなケースは特殊なケースではなく、十分起こりうるユースケースだこれがベストプラクティスかは知らないけど、例えばこんなメソッドを書くのはどうだろう。
func ToString(stringPointer *string) string {
if stringPointer == nil {
return ""
}
return *stringPointer
}
fmt.Printf("Id: %s Name: %s ProvisioningState: %s Error: %s", *v.ID, *v.Name, *v.ProvisioningState, ToString(v.DataFactoryCreateOrUpdateProperties.Error))
こんな感じでラップしてしまえば、ありうるケースに置いて、二度とこのエラーは起きない。
エラーの発見
じゃあ、どうやって、このエラーを発見するか?このポインタとnil の関係がわかってしまえばわかりやすい。行番号を見つけたら、その直前で、デバッガに頼ってブレークポイントを設定して nil
を見つけるのが簡単そうだ。
もし、もっといいコードやデバッグ方法があればぜひお教えください!