【AWS】【Go】GoのSDK(guregu/dynamo)を使ったDynamoDBのテーブル定義とコード設計
DynamoDBとは。
DynamoDBはNoSQLで、速くてサーバレスな奴だが、詳しい説明は他所に任せたい。
DynamoDBをGoで扱うには。
どうやら guregu/dynamo
を使ってやるのは一般的らしい。
しかし、↑やREADMEにはINDEXの話がない・・・手探りでやったので、やり方を記す。
guregu/dynamoでモデルを作る。
モデル(構造体化)
うーんこればっかりはGoの型の制約と構造体にピッタリマッピングさせたいからなのか少し分かりにくい。
type Hoge struct { UserID string `dynamo:"ID,hash"` Created int `dynamo:"Created,range"` Seq int64 `dynamo:"Seq,range" localIndex:"Seq-index,range"` Category string `dynamo:"Category" index:"Category-KeyID-index"` KeyID string `dynamo:"KeyID" index:"Category-KeyID-index"` }
JSONみたいに dynamo
ってラベルをつけると、テーブルに必要なattributeが後の Scan()
メソッドとかやる時に読み込まれるのでつけておく。
分かりにくいのがGSIとLSIで、LSIは localIndex
ラベルだが、GSIは index
ラベルをつけるらしい・・・・。
んで、以下のような関数で定義すればいい。
テーブル作成
こんな感じの関数を定義していく。
func SetupDdbSchema(tableStr string) error { ddb := dynamo.New(session.Must(session.NewSession()), aws.NewConfig().WithRegion(os.Getenv("ap-northeast-1"))) input := ddb.CreateTable(tableStr, Hoge{}). Provision(1, 1). Project("Seq-index", dynamo.KeysOnlyProjection) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() return input.RunWithContext(ctx) }
ポイントは無論 ddb.CreateTable()
関数だ。 Provision()
メソッドは要は awscliでいう--provisioned-throughput '{"ReadCapacityUnits": 1,"WriteCapacityUnits": 1}'
で、キャパシティユニットを決める。あらかじめGSIにたいしてユニットを決めたい場合は ProvisionIndex()
を使う。
一方 Project()
メソッドはawscliで --local-secondary-indexes
とかでKeySchemaにて指定している部分だ。 例えばLSIでQueryを出したとき、KEYだけ持ってきたければ↑みたいに dynamo.KeysOnlyProjection
を第二引数に渡せばいいし、追加でattributeを引きたいのであれば dynamo.IncludeProjection
を指定して持ってきたいキーとattributeを第二、第三引数に渡せばいいとなる。
Goでモデル作成する時のオレオレ戦略
尺が余ったので、俺がguregu/dynamoをGoで扱った時のコード設計をいかに記す。
グローバルに使い回すawsclientを定義。
var globalAwsClient *awsClient type awsClient struct { Session *session.Session S3Client *s3Client DynamoDBClient *dynamoDBClient ... }
これは *session.Session
を使いまわしたい為。公式に使いまわせと書いてあるので。
別にS3Client
とかDynamoDBClient
とかを分けなくてもいいが、clientはサービスごとに分けると、色々メソッドを安全に使えると思う。もっと言えばinterfaceでDIしておけばいいかもしれないが、本題から逸れるので略。
DynamoDBを触る時に使うクライアント型を定義
type dynamoDBClient struct { Config *aws.Config DynamoDBConfigMap map[string]dynamodbConfig // table名で引く } type dynamodbConfig struct { HashKey string RangeKey string LocalSecondaryIndexes localSecondaryIndexMap // ローカルセカンダリインデックスは複数個ありえるので、値として取るときはmapのキー指定で・・・。 } type localSecondaryIndexMap map[string]string // LSIのindex名とそれに対応するrange/hashキー
面倒だと思うのは、DynamoDBの場合はテーブルごとにキーとIndexが分かれる事にある。
なので、今回は DynamoDBのスキーマを定義するごとにmap[<テーブル名>]の形で、HashKeyとかRangeKeyを引く形にしてみた。
LSIはテーブルごとに複数個あり得るので、参照型かなと。インデックスとそれに対応するキーは常に1:1なので配列でもいいが、localSecondaryIndex[0]
って書くよりmapで LocalSecondaryIndexes["Seq-index"]
って書いて値引いた方が可読性高く思えたのでそっちで。
なお、もしGSIとかを使う場合は、適当に以下のような構造体でも設定しておけばいいと思われる。
type dynamodbConfig struct { ... GlobalSecondaryIndexes globalSecondaryIndexMap ... } type globalSecondaryIndexMap map[string]globalSecondaryIndex type globalSecondaryIndex struct { IsHashOnly bool HashKey string RangeKey string }
New
初期化。環境によって変わるだろうところなのだが、今んとここんな感じでやってる。
func newdynamodbConfig(hk, rk string, lsm localSecondaryIndexMap) dynamodbConfig { if len(lsm) == 0 { lsm = map[string]string{} } return dynamodbConfig{ HashKey: hk, RangeKey: rk, LocalSecondaryIndexes: lsm, } } func NewAwsClient() { cfg := aws.NewConfig().WithRegion(os.Getenv("AWS_REGION")) dcfg := cfg if os.Getenv("ENVIRONMENT") == "local" { dcfg.Endpoint = // テスト用のdockerとか ... } // DynamoDBのテーブルの仕様とか変わったらここを直す。 var actTable = map[string]dynamodbConfig{ "Hoge": newdynamodbConfig( "ID", "Created", localSecondaryIndexMap{ "Seq-index": "Seq", }, ), } globalAwsClient = &awsClient{ Session: session.Must(session.NewSession()), DynamoDBClient: &dynamoDBClient{ Config: dcfg, DynamoDBConfigMap: actTable, }, ... } }
Scan
まぁ普通
func (a *awsClient) Scan(ctx context.Context, tableStr string) ([]Hoge, error) { ddb := dynamo.New(a.Session, a.DynamoDBClient.Config) table := ddb.Table(tableStr) var results = []Hoge{} if err := table.Scan().AllWithContext(ctx, &results); err != nil { return []Activity{}, xerrors.Errorf("error_msg: %w", err) } return results, nil }
GetItem
可読性高く、変更にも強い・・・と思ってる。
func (a *awsClient) GetItem(ctx context.Context, tableStr string) ([]Hoge, error) { ddb := dynamo.New(a.Session, a.DynamoDBClient.Config) table := ddb.Table(tableStr) dconfig, ok := a.DynamoDBClient.DynamoDBConfigMap[tableStr] if !ok { return []Hoge{}, xerrors.New(tableStr + " is not found in api config for dynamodb") } var results = []Hoge{} if err := table.Get(dconfig.HashKey, 2).Range(dconfig.RangeKey, dynamo.Greater, 222).AllWithContext(ctx, &results); err != nil { return []Hoge{}, xerrors.Errorf("error_msg: %w", err) } return results, nil }
table.Get(dconfig.HashKey, 2).Range(dconfig.RangeKey, dynamo.Greater, 222).AllWithContext(ctx, &results)
が本体。
説明するまでもないが、結局HashKeyもRangeKeyもテーブル取り壊すまで変わらない訳だし、ならコードに埋め込む形でやればいいよねってスタイル。
構造体から取り出す形にしているのでテストもしやすいはず。
LSI使ってGET
インデックスとか使って引く時。
func (a *awsClient) LSIGet(ctx context.Context, tableStr, lsIndex string) ([]Hoge, error) { ddb := dynamo.New(a.Session, a.DynamoDBClient.Config) table := ddb.Table(tableStr) dconfig, ok := a.DynamoDBClient.DynamoDBConfigMap[tableStr] if !ok { return []Hoge{}, xerrors.New(tableStr + " is not found in api config for dynamodb") } var results = []Hoge{} lsi, ok := dconfig.LocalSecondaryIndexes[lsIndex] if !ok { return []Hoge{}, xerrors.New(lsIndex + " is not found in api config for dynamodb") } if err := table.Get(dconfig.HashKey, 2).Index(lsIndex).Range(lsi, dynamo.Greater, 10000).AllWithContext(ctx, &results); err != nil { return []Hoge{}, xerrors.Errorf("error_msg: %w", err) } return results, nil }
lsi, ok := dconfig.LocalSecondaryIndexes[lsIndex]
でLSIに事前に定義したキーで引く。このキーに関してもテーブル取り壊すまで変わらない訳だし、ならコードに埋め込む形でやればいいよねってスタイル。
感想
DynamoDB難しいけど代替品ないよねって感じで、ありがたく使わせていただいております🙏
【Fluentd】fluentdでdockerのlogを直接読み込む時にfluent-plugin-dockerが使いやすい
dockerのログのデフォルトのフォーマットはjsonな事が多いね
ってか今もデフォルトでjsonだっけ?とりま今現状GKEとかEKSとかECSとかで、アプリがどっかに吐いたJSON形式のログをfluentdのdaemonsetでログを見に行って出力してみるとこんな感じになってる。
{"log":"{\"Level\":\"INFO\",\"Time\":\"2019-02-25T07:14:56.434+0900\",\"Caller\":\"logging/logging.go:57\",\"Msg\":\"HANDLER\",\"method\":\"GET\",\"path\":\"/rd\"}\n","stream":"stdout"}
ただjsonならまだどうにかしようがあるが、例えばこのケースだとバックスラッシュはあるし元々のログに改行があったりするので \n
もあって。せっかくアプリのコードで中身をjsonで出している時がむしろキツいと言う悲しい時がある。とても厄介なことはjsonの値に \n
のようなエスケープシーケンス系が入っていると、このデータはjsonとしても読み込まれなくなる。
どうするの
dockerの機能を使ったり、docker logのフォーマットを起動時に変更すればいいと思うが、そんないつでも変更できる状況にあれば苦労しない。
その場合このままでもfluentd限定だが、以下のgemで解決できたりする。
半日かけてググった結果、このfilterが一番スマートだった。
いぐざんぽー
とりまプラグインをいれないといけないので、起動まえにgemでいれておく。
$ gem install fluent-plugin-docker
か
$ fluent-gem install fluent-plugin-docker
コンフィグは↓こんな感じ
fluent-conf
<source> type tail path /var/log/containers/hoge-*.log pos_file /fluentd/log/hoge-containers.log.pos time_format %Y-%m-%dT%H:%M:%S.%NZ tag hoge format json read_from_head true </source> <filter **> type docker </filter> <match hoge> ...
filter
の type docker
を source
とmatch
の間で挟むだけ・・・簡単すぎて気絶した・・・!
こうすると、
{"log":{"Level":"INFO","Time":"2019-02-26T00:00:11.143+0900","Caller":"logging/logging.go:57","Msg":"HANDLER","method":"GET","path":"/rd"},"stream":"stdout"}
はい解決(^p^)
これでデータとして使いやすくなった。
【Go】zapで出力時のLevelに色をつける
zapって?
uber社が作っているgolangで使う超いけてるログドライバ。
基本というかほとんどの日本語情報は以下を見ればいい。
構造化されたログを吐き出す癖にあらゆる非構造化ログドライバより速いらしい。
(真面目な話、非構造化ってことは往往にして型をリフレクトして結合してるんだからそっちの方が遅いのはそりゃそうか)
いつの間にレベルのカラライズやれるようになっていた
あい。
ちゅーか、2年前くらいからもうimplされてるみたいだから相当昔からサポートされていることになる。
しかし日本語の情報でこれについて書いてる記事ないし、書くかーーーーってなった。
どうやるのかー。
EncoderConfig
の EncodeLevel
で CapitalColorLevelEncoder
を指定すればいい
var StdLogger *zap.Logger func InitLogger() error { var err error level := zap.NewAtomicLevel() level.SetLevel(zapcore.DebugLevel) myConfig := zap.Config{ Level: level, Encoding: "console", EncoderConfig: zapcore.EncoderConfig{ TimeKey: "Time", LevelKey: "Level", NameKey: "Name", CallerKey: "Caller", MessageKey: "Msg", StacktraceKey: "St", EncodeLevel: zapcore.CapitalColorLevelEncoder, // ここをzapcore.CapitalLevelEncoderじゃなくてzapcore.CapitalColorLevelEncoderをつかう。 EncodeTime: zapcore.ISO8601TimeEncoder, EncodeDuration: zapcore.StringDurationEncoder, EncodeCaller: zapcore.ShortCallerEncoder, }, OutputPaths: []string{"stdout"}, ErrorOutputPaths: []string{"stderr"}, } StdLogger, err = myConfig.Build() return err }
注意点
この色つけには注意点があって、この カラライズはコンソール出力に限らず実行される
と言うことだ。
これはどう言うことかと言うと、↑のコードの内 myConfig
を以下のようにし、
myConfig := zap.Config{ Level: level, Encoding: "json", EncoderConfig: zapcore.EncoderConfig{ TimeKey: "Time", LevelKey: "Level", NameKey: "Name", CallerKey: "Caller", MessageKey: "Msg", StacktraceKey: "St", EncodeLevel: zapcore.CapitalColorLevelEncoder, // ここをzapcore.CapitalLevelEncoderじゃなくてzapcore.CapitalColorLevelEncoderをつかう。 EncodeTime: zapcore.ISO8601TimeEncoder, EncodeDuration: zapcore.StringDurationEncoder, EncodeCaller: zapcore.ShortCallerEncoder, }, OutputPaths: []string{"stdout"}, ErrorOutputPaths: []string{"stderr"}, }
吐き出すフォーマットをjsonにすると、
{"Level":"\u001b[34mINFO\u001b[0m","Time":"2019-02-25T11:32:15.845+0900","Caller":"cache/redis.go:40","Msg":"REDIS","msg":"Connecting Redis Parameter is tcp0.0.0.0:6379"} {"Level":"\u001b[34mINFO\u001b[0m","Time":"2019-02-25T11:32:15.845+0900","Caller":"cache/redis.go:41","Msg":"REDIS","msg":"REDIS_MAX_IDLE is 3"} {"Level":"\u001b[34mINFO\u001b[0m","Time":"2019-02-25T11:32:15.845+0900","Caller":"cache/redis.go:42","Msg":"REDIS","msg":"REDIS_MAX_ACTIVE is 1000"} {"Level":"\u001b[34mINFO\u001b[0m","Time":"2019-02-25T11:32:15.845+0900","Caller":"cache/redis.go:43","Msg":"REDIS","msg":"REDIS_IDLE_TIMEOUT_SECONDS is 240"}
エスケープシーケンスがそのままJSONに乗ってしまうので注意されたい。
仮に出力フォーマットがjsonの場合、エスケープシーケンスが色として意味をなしたとしてJSONの値に色つけされた所で嬉しい事はないでしょうしまぁゴミが入ってる状態と思っていい。
環境ごとに zapcore.CapitalLevelEncoder
と zapcore.CapitalColorLevelEncoder
を使い分けるコードをオススメされたい。
【Rust】コードで理解するトレイト
はじめに
主に僕は書籍を通じてRustを勉強していたが、はっきり言って全く理解できなかったので生存期間に引き続きコードで記述する事で理解しようと思った。
Rustのモジュールの考え方について
Rustは任意の型に対して通常 impl
を用いて以下のようにモジュールを追加する。
struct Cat { name: String, eyes: String, } impl Cat { fn new(name: &String, eyes: &String) -> Self { Cat { name: name.to_string(), eyes: eyes.to_string(), } } fn cry(&self, cry_str: String) { println!("{} eyes {} say, {}", &self.eyes, &self.name, cry_str) } } fn main() { let cat = Cat::new(&"JiJi".to_string(), &"Black".to_string()); cat.cry("myaw".to_string()); //-> Black eyes JiJi say, myaw }
Goをやっていた人からすると、コードの流れとしては Go
にすごい似ていることに気づくだろう。 Cat
型を定義した後、impl
キーワードを使って fn cry(&self, cry_str: String)
メソッド関数を定義している。
こうする事で、new
または cry
関数は Cat
型専用の関数となるので、保守性と可読性が増すという訳だ。
Rustのモジュールでの課題
こうする事で、
new
またはcry
関数はCat
型専用の関数となるので、保守性と可読性が増すという訳だ。
この通り、"new
または cry
関数は Cat
型専用の関数" となってしまう訳で、例えば他にも声を出して鳴くであろう同様の Dog
や Devil
やPikachu
など定義する時に、コードが冗長化する。
そこで今回の トレイト (trait)が登場する訳だ。
やっていき
ようは、この new またはcryするという機能
というものAnimal
という名前の trait
として定義して、型に一段DIするという運びとなる。
struct Cat { name: String, eyes: String, } struct Dog { name: String, eyes: String, } trait Animal { fn new(name: &String, eyes: &String) -> Self; fn cry(&self, cry_str: String); } fn animal_new<T: Animal>(name: &String, eyes: &String) -> T { Animal::new(&name.to_string(), &eyes.to_string()) } fn animal_cry<T: Animal>(animal: T, cry_str: String) { animal.cry(cry_str); } impl Animal for Cat { fn new(name: &String, eyes: &String) -> Self { Cat { name: name.to_string(), eyes: eyes.to_string(), } } fn cry(&self, cry_str: String) { println!("Cat, {} eyes {} say, {}", &self.eyes, &self.name, cry_str) } } impl Animal for Dog { fn new(name: &String, eyes: &String) -> Self { Dog { name: name.to_string(), eyes: eyes.to_string(), } } fn cry(&self, cry_str: String) { println!("Dog, {} eyes {} say, {}", &self.eyes, &self.name, cry_str) } } fn main() { let cat: Cat = animal_new(&"JiJi".to_string(), &"Black".to_string()); let dog: Dog = animal_new(&"Mugi".to_string(), &"Red".to_string()); animal_cry(cat, "myaw".to_string()); //-> Cat, Black eyes JiJi say, myaw animal_cry(dog, "bowow".to_string()); //-> Dog, Red eyes Mugi say, bowow }
解説はじめ
トレイト単体で言えば、C++でいう仮想関数や、Goでいうinterfaceと言ったものに非常に似ている。
はじめに animal_new
関数により、2つの異なる型を同時に初期化している。
中身が注目で2つの異なる型のnew
メソッドを Animal
トレイトから出せるようになっている事がわかる。
これにより、 Cat
型と Dog
型を同等に扱える。
そして以下に何やら怪しい形の関数を定義していることに注目されたい。
fn animal_new<T: Animal>(name: &String, eyes: &String) -> T { Animal::new(&name.to_string(), &eyes.to_string()) } <snip> let cat: Cat = animal_new(&"JiJi".to_string(), &"Black".to_string()); let dog: Dog = animal_new(&"Mugi".to_string(), &"Black".to_string()); <snip>
型もメソッドもDIしたのであれば、せっかくなので同じ関数で呼び出したいとなるので、満を辞して ジェネリクス
さんの登場してもらうことになる。
Rustのジェネリクスはとても賢いので、定義したtraitを紐づけてやると、制限つきのジェネリクス、つまりAnimalトレイトをimplしている型だけ使えるようにジェネリクスを縛れる。
こうして定義した関数を ジェネリクス関数 と呼ぶらしい。
縛られているか証拠に試しに Flog
型 を定義してみると期待通りコンパイルエラーとなる。
struct Flog { name: String, eyes: String, } <snip> let flog: Flog = animal_new(&"Pyoko".to_string(), &"Black".to_string()); <snip>
Compiling playground v0.0.1 (/playground) error[E0277]: the trait bound `Flog: Animal` is not satisfied --> src/main.rs:57:22 | 57 | let flog: Flog = animal_new(&"Pyoko".to_string(), &"Black".to_string()); | ^^^^^^^^^^ the trait `Animal` is not implemented for `Flog` |
こうして型安全に初期化されたら、実際に鳴くときもジェネリクス関数を使えば、違う型の違う実装の、しかし同じ名前のメソッドが使える
という訳だ。
fn animal_cry<T: Animal>(animal: T, cry_str: String) { animal.cry(cry_str); } <snip> animal_cry(cat, "myaw".to_string()); //-> Cat, Black eyes JiJi say, myaw animal_cry(dog, "bowow".to_string()); //-> Dog, Red eyes Mugi say, bowow <snip>
感想
ジェネリクスとトレイトを使い回せればかなりコードを簡略化してかけそう😝
【Go】Go言語で指数表記の文字列型を数値型にパースする。
指数表記の文字列型???何それ?
これ。
package main import ( "fmt" ) func main() { fmt.Println(float64(1000000)) //-> 1e+06 }
goで浮動小数点数型をデフォルトではフォーマットした時に 1e+06
のように出力され、こういう表記を指数表記、英語ではexponential notation
、notation
、scientific notation
とか呼ぶ。
それがどうしたの?
って感じだろうが少し待って欲しい、例えば以下のようなreturnScientificNotation関数
的な何かでこの文字列を返されてしまった時、でも自分はその数値を int
として使いたい場合はどうしようってのが今回の話題だ(こんな事起こるの?って感じだが僕の場合、jwtのtokenでこれが起きた)。
package main import ( "fmt" "reflect" ) func returnScientificNotation() string{ // なんかの処理 return "1e+06" } func main() { str := returnScientificNotation() // 1000000が欲しいのになぜか指数表記の文字列型で返された dst := doSomeThing(str) // なんかやって元のint型に直したい if reflect.DeepEqual(1000000, dst) { fmt.Println("same") } else { fmt.Println("different") } }
それの何が問題なの?
実は結構詰んでいる。
例えば文字列->整数は普通こういうパースは strconv.Atoi
パッケージを使うのが定石だが、当然エラーする。
package main import ( "fmt" "strconv" ) func main() { dst, err := strconv.Atoi("1e+06") if err != nil { panic(err) } fmt.Println(dst) }
panic: strconv.Atoi: parsing "1e+06": invalid syntax goroutine 1 [running]: main.main() /tmp/sandbox701385143/main.go:13 +0x180
それっぽい strconv
のモジュールもない。
interfaceの型のキャストもだめ。
package main import ( "fmt" ) func main() { var str interface{} str = "1e+06" dst, ok := str.(int) if !ok { panic("cannot cast") } fmt.Println(dst) }
panic: cannot cast goroutine 1 [running]: main.main() /tmp/sandbox399888292/main.go:14 +0xc0
どうしよ。
こうしよう。
package main import ( "fmt" "strconv" "reflect" ) func parseScientificNotation(str string) (int, error) { float, err := strconv.ParseFloat(str, 64) if err != nil { return 0, err } return int(float), nil } func returnScientificNotation() string{ // なんかの処理 return "1e+06" } func main() { str := returnScientificNotation() // 1000000が欲しいのになぜか指数表記の文字列型で返された dst, _ := parseScientificNotation(str) // なんかやって元のint型に直したい if reflect.DeepEqual(1000000, dst) { fmt.Println("same") // -> same } else { fmt.Println("different") } }
かいせつ
ポイントは strconv.ParseFloat
で string -> float64をした後で int型にキャストしている所で、
strconv.ParseFloat
は実は "0.000001"
のような表記の他、今回のような指数表記の文字列型にも対応している。
今回は冒頭で指数表記の文字列が float64()
のマクロでキャストをしているので、指数表記の文字列がfloat系の型からキャストされた後に出力されてるから・・・と予想できやすいが、いざライブラリとかでいきなり 1e+06
と返されるとこの答えにまず行き着きにくい(という言い訳・・・・w)
ソースではここのファイルの genericFtoa(dst []byte, val float64, fmt byte, prec, bitSize int) []byte
関数を見ればパースの動きがわかるだろう。
感想
まじで fmt.Sprint(float64(int型))
とかで返してくるライブラリやめろ。
【Rust】コードで理解する生存期間
はじめに。
Rustの言語機能の核に、所有権、参照、そして生存期間とあります。
個人的に生存期間が特に理解が難しかったので、復習がてらブログを書きます。
生存期間とは?
Rustコンパイラは全ての参照型に対して、その参照の使われ方によって生じた制約を反映した値として生存期間(lifetime)を割り当てる。
参照型というのは、"&" から始まる変数で、任意の変数の所有権移動を伴わずに借用できる変数の型のことを言う。
生存期間の基本
fn main() { let r; { let x = 1; r = &x; } println!("{}", *r); }
例えば上記のコードはコンパイルに失敗する。
error[E0597]: `x` does not live long enough --> src/main.rs:5:9 | 5 | r = &x; | ^^^^^^ borrowed value does not live long enough 6 | } | - `x` dropped here while still borrowed 7 | println!("{}", *r); | -- borrow later used here
エラーメッセージを見ると「xは十分長く生存出来ない」「xはまだ借用しているのにここでdropしている」と意訳できる。
このように、
と言う2つのルールにより、コンパイルエラーが起きる。
以下のサイトによるとダングリングポインタは、
無効なメモリ領域を指すポインタはダングリングポインタ(dangling pointer)と呼ばれる。とりわけ、本来有効だったメモリ領域が解放処理などによって無効化されたにもかかわらず、そのメモリ領域を参照し続けているポインタのことを、ダングリングポインタと呼ぶ。
ダングリングポインタとは|dangling pointerの危険性と回避 | MaryCore
だそうだ。
この2つのルールを見れば、ここはそこまで難しくなさそうだ。
つまり上記のコードは、xがドロップする前にrを参照すればいい。
fn main() { let r; { let x = 1; r = &x; println!("{}", *r); } }
1
ベクタ型の生存期間
さ、ここからが本番だ。
ベクタの要素が定数
fn main() { let v = vec![1, 2, 3]; { let r = &v[0]; println!("{}", *r); } }
1
参考書によると、「vの生存期間は参照型&v[0]の生存期間を包含してなければならない」、そうだ。要は、rがvより長く生きるのはダメだと言うことだと思われる。
fn main() { let r; { let v = vec![1, 2, 3]; r = &v[0]; } println!("{}", *r); }
error[E0597]: `v` does not live long enough --> src/main.rs:5:14 | 5 | r = &v[0]; | ^ borrowed value does not live long enough 6 | } | - `v` dropped here while still borrowed 7 | println!("{}", *r); | -- borrow later used here error: aborting due to previous error
vは内側のスコープでライフが切れるので、それを参照しようとするとダングリングポインタ扱いにある。まぁ普通の参照系と変わらなそうだ。
ベクタ内が変数
例えば、ベクタ内が全部参照型とした時、当然ながらベクタ越しに参照しても要素達にも生存期間のルールは適用される。
fn main() { let r; let v; { let a = 1; let b = 2; let c = 3; v = vec![&a, &b, &c]; r = &v[0]; }// ここでa, b, cだけライフが切れる。 println!("{}", *r); }
error[E0597]: `a` does not live long enough --> src/main.rs:9:18 | 9 | v = vec![&a, &b, &c]; | ^^ borrowed value does not live long enough 10 | r = &v[0]; 11 | } | - `a` dropped here while still borrowed 12 | println!("{}", *r); | -- borrow later used here error[E0597]: `b` does not live long enough --> src/main.rs:9:22 | 9 | v = vec![&a, &b, &c]; | ^^ borrowed value does not live long enough 10 | r = &v[0]; 11 | } | - `b` dropped here while still borrowed 12 | println!("{}", *r); | -- borrow later used here error[E0597]: `c` does not live long enough --> src/main.rs:9:26 | 9 | v = vec![&a, &b, &c]; | ^^ borrowed value does not live long enough 10 | r = &v[0]; 11 | } | - `c` dropped here while still borrowed 12 | println!("{}", *r); | -- borrow later used here
では、例えばベクタvの方が先にa, b, cより先に生存期間が切れたらどうなるだろう?
fn main() { let r; let a = 1; let b = 2; let c = 3; { let v; v = vec![&a, &b, &c]; r = &v[0]; }// ここでvだけライフが切れる。 println!("{}", *r); }
error[E0597]: `v` does not live long enough --> src/main.rs:10:14 | 10 | r = &v[0]; | ^ borrowed value does not live long enough 11 | } | - `v` dropped here while still borrowed 12 | println!("{}", *r); | -- borrow later used here
まぁそりゃそうだと言う感じにエラーを吐く。
もちろん、生存期間が切れたものは参照出来ない。
関数が引数として参照型を受け取る場合
ここがつらみ。
普通に引数として
同じようなコードで恐縮だ。
この場合でも、しっかり生存期間は普通に参照型を使った時と変わらない。
fn hoge(r: &usize) { println!("{}", *r); } fn main() { let r; { let x = 1; r = &x; } hoge(r) }
error[E0597]: `x` does not live long enough --> src/main.rs:9:9 | 9 | r = &x; | ^^^^^^ borrowed value does not live long enough 10 | } | - `x` dropped here while still borrowed 11 | hoge(r) | - borrow later used here
任意の引数を生存期間とする。
fn hoge<'a>(r: &'a i32) { println!("{}", r); } fn main() { let r; { let x = 1; r = &x; hoge(&r); } }
1
関数が引数として渡されたrが、関数呼び出しを超えて生き残る必要が ない 事を示す。
・・・冷静に考えて見れば当然だ。Rustの言語仕様として我々が「関数が受け取った引数はstaticなスコープを保たない限りは関数が閉じた瞬間、呼び出し先からは消される(が、これはつまり暗黙的な関数の挙動なわけだが・・・・)」と学んだはずだ。つまり、この<' a>(tick aと言うらしい)と言うなんとも魔術じみた記号はただの暗黙的約束を明記しただけとなる。
'staticを生存期間に設定する
この場合、いきなりコンパイルが通らなくなる。
fn hoge(r: &'static i32) { println!("{}", r); } fn main() { let r; { let x = 1; r = &x; hoge(&r); } }
error[E0597]: `x` does not live long enough --> src/main.rs:10:9 | 10 | r = &x; | ^^^^^^ borrowed value does not live long enough 11 | hoge(&r); | -------- argument requires that `x` is borrowed for `'static` 12 | } 13 | } | - `x` dropped here while still borrowed
前段で述べたルールに照らし合わせれば理解できる。この関数hogeは引数に対して、'static、つまりスレッドの終了までの変数の生存期間を要求している。もちろんxはそのような生存期間は持ち合わせていないわけだ。
返り値としての参照
関数に対して生存期間表記がデフォルトで省略されている事がわかった。
しかし返り値はどうだろう?
fn hoge(h: &i32) -> &i32 { h } fn main() { let r; { let x = 1; r = hoge(&x); } println!("{}", *r); }
error[E0597]: `x` does not live long enough --> src/main.rs:9:18 | 9 | r = hoge(&x); | ^^ borrowed value does not live long enough 10 | } | - `x` dropped here while still borrowed 11 | println!("{}", *r); | -- borrow later used here
また同じようなコード???と思われるだろうが少しまたれたい。
今回の場合はxの参照型を関数hogeに渡しているだけだ・・・まぁいつものごとくコンパイルは通らないわけだが。
生存期間の話から振り返ってみる。
実は関数hogeの暗黙的に宣言されている関数全文はこう書ける。
fn hoge<'a>(h: &'a i32) -> &'a i32 { h }
ここはしっかり頭に入れておきたいのだが、 引数と返り値は同じ生存期間を保たなければいけない と言う事になる。
つまりrと参照元であるxは同じ生存期間でなければならないが、xが先にライフ切れになるからアウト、と言うわけである。
構造体
構造体は、暗黙的に生存期間の解決をやってくれない、と覚える。
構造体内の値の生存期間
構造体の中に参照型を埋め込むと、生存期間をいきなり考慮しなければならない。
struct S { r: &i32 } fn main() { let s; { let x = 10; s = S { r : &x, }; println!("{:?}", s.r); } }
error[E0106]: missing lifetime specifier --> src/main.rs:2:8 | 2 | r: &i32 | ^ expected lifetime parameter
なんと、「生存期間を明示しろ」といってくるではないか・・・!あれだけ関数では暗黙的に宣言していたのに・・・😭
つまり以下のように明記してやれば通る。
struct S<'moemoe> { r: &'moemoe i32 } fn main() { let s; { let x = 10; s = S { r : &x, }; println!("{:?}", s.r); } }
10
ちなみに、生存期間を明記するtickの後の英字は変数のように、任意の英字でいい。
このように構造体の場合は、参照型を埋め込む時は明記しないといけないから注意だ。なんで暗黙的に宣言されないかは僕はよくわかっていない・・・・。
構造体内の構造体の値の生存期間
構造体内に構造体を埋め込む場合は、もちろん宣言する必要がでてくるし、以下のように書く。
struct T<'toretore> { t: S<'toretore> } struct S<'moemoe> { r: &'moemoe i32 } fn main() { let s; { let x = 10; s = T { t : S { r : &x }, }; println!("{:?}", s.t.r); } }
10
ネストしたとしてもTの値の生存期間をSから受け継いでるだけで、基本的なルールは変わらない。
構造体内の生存期間の共有
こんな事すんのかって思うが、片方の引数の生存期間を両方の引数に制約をかけれたりもできる。
struct S<'ponyo> { x: &'ponyo i32, y: &'ponyo i32, } fn main() { let x = 10; let r1; { let y = 20; { let s = S { x: &x, y: &y, }; r1 = s.x; } } println!("x:{:?}", r1); }
error[E0597]: `y` does not live long enough --> src/main.rs:14:20 | 14 | y: &y, | ^^ borrowed value does not live long enough ... 18 | } | - `y` dropped here while still borrowed 19 | println!("x:{:?}", r1); | -- borrow later used here
一見良さそうだ。yはsでしか参照されていないし、xの生存期間はmain()が終わるまでだし、フォーマットの呼び出しを与えているのはxだけ。しかしyについて生存期間で怒られる。なぜだろう?
1つは「r= s.xの代入により、 'aはrの生存期間を含んでなければならない。」
2つは「r=s.yは&yで初期化されているため、'aはyの生存期間より長い時間をとってはいけない」
となるので、yのライフより短い上に、rより長い生存期間の変数をRustは探すのだが、選択肢にはないのでコンパイルエラーとなる。まぁつまり、先ほど確認した通り変数上では生存期間に問題はなかったように、例に習った引数通りに構造体の生存期間を満たしてやれば良いと言う話になりそうだ。
struct S<'ponyo, 'sousuke> { x: &'ponyo i32, y: &'sousuke i32, } fn main() { let x = 10; let r1; { let y = 20; { let s = S { x: &x, y: &y, }; r1 = s.x; } } println!("x:{:?}", r1); }
10
さて万事これでOKだ。
実際にコードを書く時は新たに構造体を色々な場所で宣言していくと思うのだが、実際には引数に生存期間パラメータを合わせると言うよりかは、徐々に構造体の生存期間の制約を緩めていく方向に書いていくと上手くいき、省エネなコードになりそうだ。
感想
Rustは難しいけど、コンパイラがマジで懇切丁寧に教えてくれるので学習しやすい。
参考文献
【Rust】nightlyでrlsなどの開発ツールがインストール出来ない時にする事。
Rustのバージョンには。
stable
beta
nightly
という三種類があり、nightly は最新の開発版で、stable がリリース版です。beta では nightly でのテストが終わりリリースに向けた機能が利用できます。普通の感覚では、 stable
を皆手に取るでしょうが、例えばvscodeの rusty code
の依存モジュールの racer
は nightly
にしか今現在対応していません。
nightlyインストールで何で困るのか?
例えば無邪気に、 nightly
をインストールをし、
$ rustup install nightly $ rustup default nightly
vsodeのrlsプラグインを入れようとします。
$ rustup component add rls-preview error: component 'rls-preview' for 'x86_64-pc-windows-msvc' is unavailable for download
おわた・・・・・。
なんでこんなことが起きるのか?
一番最初に書いた通り、 nightly
は常に(そう、この年末年始でさえ)開発されており、処理系とそれに依存する開発モジュールも例に漏れません。開発途中でビルドがこけていると、 rustup
でインストール出来ないようになっております。
どうするか?
ここにたどり着くのに半日かかったぞKS・・・・! 各ディストリやOSごとの状態がわかります。 OSXのx86なら、
Rustup packages availability on x86_64-apple-darwin
ここでみて、使う開発ツールがビルド成功しているバージョンをインストールしてください。
直近だと、 2018-12-27
では rls
がビルド成功していて良さそうですね。
$ rustup install nightly-2018-12-27 $ rustup default nightly-2018-12-27
これでvsvodeで rusty code
と rls
が併用できるようになりました。