nがひとつ多い。

えぬなおの技術的なことを書いていくとこ。

【AWS】【Go】GoのSDK(guregu/dynamo)を使ったDynamoDBのテーブル定義とコード設計

DynamoDBとは。

DynamoDBはNoSQLで、速くてサーバレスな奴だが、詳しい説明は他所に任せたい。

www.ketancho.net

dev.classmethod.jp

DynamoDBをGoで扱うには。

どうやら guregu/dynamo を使ってやるのは一般的らしい。

qiita.com

しかし、↑や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で、LSIlocalIndex ラベルだが、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で解決できたりする。

github.com

半日かけてググった結果、この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>
...

filtertype dockersourcematch の間で挟むだけ・・・簡単すぎて気絶した・・・!

こうすると、

{"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で使う超いけてるログドライバ。

github.com

基本というかほとんどの日本語情報は以下を見ればいい。

qiita.com

構造化されたログを吐き出す癖にあらゆる非構造化ログドライバより速いらしい。
(真面目な話、非構造化ってことは往往にして型をリフレクトして結合してるんだからそっちの方が遅いのはそりゃそうか)

いつの間にレベルのカラライズやれるようになっていた

あい。

https://cloud.githubusercontent.com/assets/4228796/23066336/537fe9dc-f51a-11e6-8ec7-0a29f8e9cc38.png

ちゅーか、2年前くらいからもうimplされてるみたいだから相当昔からサポートされていることになる。

github.com

しかし日本語の情報でこれについて書いてる記事ないし、書くかーーーーってなった。

どうやるのかー。

EncoderConfigEncodeLevelCapitalColorLevelEncoder を指定すればいい

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.CapitalLevelEncoderzapcore.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
}

Rust Playground

Goをやっていた人からすると、コードの流れとしては Go にすごい似ていることに気づくだろう。 Cat 型を定義した後、impl キーワードを使って fn cry(&self, cry_str: String) メソッド関数を定義している。

こうする事で、new または cry 関数は Cat 型専用の関数となるので、保守性と可読性が増すという訳だ。

Rustのモジュールでの課題

こうする事で、new または cry 関数は Cat 型専用の関数となるので、保守性と可読性が増すという訳だ。

この通り、"new または cry 関数は Cat 型専用の関数" となってしまう訳で、例えば他にも声を出して鳴くであろう同様の DogDevilPikachu など定義する時に、コードが冗長化する。

そこで今回の トレイト (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
}

Rust Playground

解説はじめ

トレイト単体で言えば、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`
   |

Rust Playground

こうして型安全に初期化されたら、実際に鳴くときもジェネリクス関数を使えば、違う型の違う実装の、しかし同じ名前のメソッドが使える という訳だ。

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
}

play.golang.org

goで浮動小数点数型をデフォルトではフォーマットした時に 1e+06 のように出力され、こういう表記を指数表記、英語ではexponential notationnotationscientific 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

play.golang.org

それっぽい 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

play.golang.org

どうしよ。

こうしよう。

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")
    }
}

play.golang.org

かいせつ

ポイントは 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 関数を見ればパースの動きがわかるだろう。

golang.org

感想

まじで fmt.Sprint(float64(int型)) とかで返してくるライブラリやめろ。

【Rust】コードで理解する生存期間

はじめに。

Rustの言語機能の核に、所有権、参照、そして生存期間とあります。
個人的に生存期間が特に理解が難しかったので、復習がてらブログを書きます。

生存期間とは?

Rustコンパイラは全ての参照型に対して、その参照の使われ方によって生じた制約を反映した値として生存期間(lifetime)を割り当てる。
参照型というのは、"&" から始まる変数で、任意の変数の所有権移動を伴わずに借用できる変数の型のことを言う。

生存期間の基本

fn main() {
    let r;
    {
        let x = 1;
        r = &x;
    }
    println!("{}", *r);
}

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=12e83a7fae65088e21b06a2a5df29447

例えば上記のコードはコンパイルに失敗する。

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している」と意訳できる。
このように、

  • 変数は宣言したスコープから外れるとdropしてしまう
  • 借用している最中に借用者が先にdropするとダングリングポインタとなってしまう

と言う2つのルールにより、コンパイルエラーが起きる。
以下のサイトによるとダングリングポインタは、

無効なメモリ領域を指すポインタはダングリングポインタ(dangling pointer)と呼ばれる。とりわけ、本来有効だったメモリ領域が解放処理などによって無効化されたにもかかわらず、そのメモリ領域を参照し続けているポインタのことを、ダングリングポインタと呼ぶ。

ダングリングポインタとは|dangling pointerの危険性と回避 | MaryCore

だそうだ。
この2つのルールを見れば、ここはそこまで難しくなさそうだ。

つまり上記のコードは、xがドロップする前にrを参照すればいい

fn main() {
    let r;
    {
        let x = 1;
        r = &x;
        println!("{}", *r);
    }
}
1

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=20d91c94bf1aae47ce374fdaf47a5e9b

ベクタ型の生存期間

さ、ここからが本番だ。

ベクタの要素が定数

fn main() {
    let v = vec![1, 2, 3];
    {
        let r = &v[0];
        println!("{}", *r);
    }
}
1

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=20d91c94bf1aae47ce374fdaf47a5e9b

参考書によると、「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

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=9556547d3603510eb055b6778569ae56

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

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=9da20612ba114da51507d12819a47da4

では、例えばベクタ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

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=aeaf81a22db484ac7bb32ff9a1472b48

まぁそりゃそうだと言う感じにエラーを吐く。
もちろん、生存期間が切れたものは参照出来ない。

関数が引数として参照型を受け取る場合

ここがつらみ。

普通に引数として

同じようなコードで恐縮だ。
この場合でも、しっかり生存期間は普通に参照型を使った時と変わらない。

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

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=75d338872efe687f342bfc3055c254fa

関数が引数として渡された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

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=75d338872efe687f342bfc3055c254fa

また同じようなコード???と思われるだろうが少しまたれたい。
今回の場合は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

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=0fdc8f2ec5f09f3d0d111e517c4aca45

なんと、「生存期間を明示しろ」といってくるではないか・・・!あれだけ関数では暗黙的に宣言していたのに・・・😭

つまり以下のように明記してやれば通る。

struct S<'moemoe> { 
    r: &'moemoe i32
}

fn main() {
    let s;
    {
        let x = 10;
        s = S {
            r : &x,
        };
        println!("{:?}", s.r);
    }
}
10

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=c0441d2ced1dea5572d63eeddb70db91

ちなみに、生存期間を明記する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

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=801109880ca7bf2303375fd21c0c5206

ネストしたとしても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

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=c05d48270c77ea77bde17ccd0a601bc0

一見良さそうだ。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

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=5830439037bb09e247c28ec6a4dfc164

さて万事これでOKだ。
実際にコードを書く時は新たに構造体を色々な場所で宣言していくと思うのだが、実際には引数に生存期間パラメータを合わせると言うよりかは、徐々に構造体の生存期間の制約を緩めていく方向に書いていくと上手くいき、省エネなコードになりそうだ。

感想

Rustは難しいけど、コンパイラがマジで懇切丁寧に教えてくれるので学習しやすい。

参考文献

www.amazon.co.jp

【Rust】nightlyでrlsなどの開発ツールがインストール出来ない時にする事。

Rustのバージョンには。

stable beta nightly という三種類があり、nightly は最新の開発版で、stable がリリース版です。beta では nightly でのテストが終わりリリースに向けた機能が利用できます。普通の感覚では、 stable を皆手に取るでしょうが、例えばvscoderusty code の依存モジュールの racernightly にしか今現在対応していません。

marketplace.visualstudio.com

nightlyインストールで何で困るのか?

例えば無邪気に、 nightly をインストールをし、

$ rustup install nightly
$ rustup default nightly

vsodeのrlsプラグインを入れようとします。

marketplace.visualstudio.com

$ rustup component add rls-preview
error: component 'rls-preview' for 'x86_64-pc-windows-msvc' is unavailable for download

おわた・・・・・。

なんでこんなことが起きるのか?

一番最初に書いた通り、 nightly は常に(そう、この年末年始でさえ)開発されており、処理系とそれに依存する開発モジュールも例に漏れません。開発途中でビルドがこけていると、 rustup でインストール出来ないようになっております。

どうするか?

ここにたどり着くのに半日かかったぞKS・・・・! 各ディストリやOSごとの状態がわかります。 OSXx86なら、

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 coderls が併用できるようになりました。