emahiro/b.log

Drastically Repeat Yourself !!!!

Serverless Framework でモノレポっぽい構成にする

Overview

Serverless Framework を使う時に異なるディレクトリに serverless.yml を配置してモノレポっぽい構成の Lambda で作られたサービスを考えます。

※ 今回は Go で Lambda を作ったケースを考えていますが、他のランタイムでも同様です。

考えたきっかけ

通常は以下のような Serverless.yml の設定を 1 つにして Lambda を複数定義する(handlerが複数存在する)のが、Lambda を 1 サービスと見たててマイクロサービスっぽく振る舞わせます。

# 構成

- serverless.yml
- app
    - hogeFunc
        - main.go
    - fugaFunc
        - main.go
  ... 略

この場合 serverless.yml には以下のような設定をすることになります、

functions:
 hogeFunc:
    handler: ...
 fugaFunc:
    handler: ...
 ... 略

これ自体はそんなに難しいことではないですが、マイクロサービスや、あるメインシステムに対してのサブシステムとして Lambda を採用する場合に、いくつもの Lambda を作っていくと serverless.yaml が肥大化することになります。そして何より functions 配下で色んなコンテキストを持つ Lambda が混在することになることに課題を感じてました。これに対して、もちろんリポジトリを分ければ良かったりもするのですが、それだとリポジトリが無尽蔵に増えていくことになり、運用のことを考えるとある程度まとめておきたいなーという気持ちもあって、ものレポっぽい構成にしたいなと考えてました。
serverless.yml は変数を外部ファイルから読み込むこともできる(*)ので分けて整理することもできますが、それでも読み込めない値の定義も存在します。
リポジトリが増えることは抑制してソフトウェアとしては1つにまとめるためにも、ある程度の粒度で個別の serverless.yml として管理したいと考えてました。

*Serverless Frameworkで環境変数を外部ファイルから読み込み、環境毎に自動で切り替えてみる

モノレポ構成で考える

前述したような構成(hogeFunc, fugaFunc..) で複数の serverless.yml を持つ以下のような定義を考えます。

- hogeSvc
    - hogeHandler
        - main.go
    - fugaHanlder
        - main.go
    - serverless.yml
- fugaSvc
    - main.go
    - serverless.yml

各サービスの serverless.yml には以下のような設定が追加されます。

functions:
 hogeHanlder:
    handler: ....
 fugaHandler:
    handler: ...

これだとサービスごと関係する Lambda を追加してサービスごとに設定ファイルをわけ流ことができます。(serverless.yml 1つあたりのの肥大化の抑制)

ハマったところ

デプロイ周り

Lambda のデプロイには以下の公式の GitHub Actions を使います。 (普通にCIの中で serverless framework をインストールして sls コマンドでデプロイすることも可能です。)

github.com

この Actions の設定を使ったときにちょっとデプロイ周りでハマりました。この Actions の設定でそのまま sls deploy を実行した場合、実行するディレクトリと同じ深さに serverless.yml がないとデプロイできない、というエラーが出てしまいます。
このエラーが発生する理由は、今回使用する serverless 公式の Actions で sls deploy を実行する場合、ディレクトリ移動といった設定を Actions の設定ファイルに記述することができないからです。

実際に遭遇したエラーは以下です。

This command can only be run in a Serverless service directory. Make sure to reference a valid config file in the current working directory if you're using a custom config file

このエラーが発生する場合(今回のような複数の serverless.yml を Lambda ごとに定義した場合) はエラーメッセージの後半に記載してある カスタム config ファイルの指定をする必要があります。

config ファイルをカスタムで指定する場合、sls deploy で指定する config ファイル(serverless.yaml) の指定方法は --config の設定を使います。

www.serverless.com

この設定を使うと、以下のようなディレクトリ構成の場合

root
- hogeSvc
- fugaSvc
- ...

以下のように特定サービスの serverless.yml を指定してデプロイします。

$ sls deploy --config ./hogeSvc/serverless.yml

Actions の設定は以下のようになります(抜粋)

- name: serverless deploy stage
    uses: serverless/github-action@master
  with:
    args: deploy --config ./hogeSvc/serverless.yml --verbose --stage=stage
  env:
    AWS_ACCESS_KEY_ID: $ACCESS_KEY
    AWS_SECRET_ACCESS_KEY: $SECRET_ACCESS_KEY

デプロイコマンドを実行する位置からの相対 path を指定する

またこのデプロイするときに注意したいのは serverless.yml に記載している path は sls deploy を実行する path するからの相対 path にしないと正しく読み込まれない、ということです。

例えばdev/prod で参照する変数を分けてる場合、hogeSvc を actions からデプロイする上記のコマンドを実行する際には以下のように変数の読み込みファイルの読み込み先指定を hogeSvc から指定しないといけません。

# hogeSvc/serverless.yml
custom:
  defaultStage: stage
  environment:
-    stage: ${file(./conf/dev.yml)}
-    prod: ${file(./conf/prod.yml)}
+    stage: ${file(./hogeSvc/conf/dev.yml)} // リポジトリのルートからみると conf ファイルは hogeSvc 配下にあることになる。
+    prod: ${file(./hogeSvc/conf/prod.yml)}

また実行バイナリの path も早退パスになりいます。例えば hogeSvc 配下で以下のように実行バイナリを bin ディレクトリに格納していた場合を考えます。

- hogeSvc
- bin
    - hoge

このケースでは serverless.yml の handler の設定を sls deploy を実行する path からの相対 path にしないと以下のような Lambda のエラーが出ます。

{
    "errorMessage": "fork/exec /var/task/bin/factReviewer: no such file or directory",
    "errorType": "PathError"
}

serveless.yml の設定は以下

functions:
    hogeHandler:
        handler: ./hogeSvc/bin/hoge

まとめ

Lambda の構成をマイクロサービスっぽくするときにうまく管理をわけることができないか?というところが発想の起点でしたが、やりようはある、ということがわかりました。
とはいえ、結構相対 path にすることを忘れたりするのでこれも構成の一つと捉えてみると良さそうです。

go-cmp の cmp.Option と可変長引数 `...` について少し調べた話

Overview

Go Test においてテスト対象の出力結果と期待する値を比較する手法として reflect.DeepEqual ではなく go-cmp を使うことが多くなっていると思いますが、この go-cmpcmp.Option が可変長引数として Diff に指定することができるところで、使い方と可変長引数の指定の仕方でちょっとハマったので、思い出し作業もかねて備忘録としてまとめました。

go-cmp の cmp.Option

比較する時に幾つかの Option を追加することができます。 追加できるオプションは例えば以下のような比較する struct に含まれる非公開フィールドを無視するcmpopts.IgunoreUnexportedや特定のフィールドを比較対象から外す cmpopts.IgnoreField があります*。

*CreatedAt や UpdatedAt のような時刻フィールドは時間が絡むので意図せず壊れやすいため最初から比較対象として外すことが多いです。もちろん時刻が重要なテストでは外してはいけません。

How to use cmp.Option

Diff で可変長引数に指定されている opts に cmp.Option を割り当てることができます。

これの割り当て方はいくつかあります。
例えば cmpopts.IgnoreUnexported を例に取ると https://github.com/google/go-cmp/blob/master/cmp/cmpopts/ignore.go#L119 のように無視したい非公開フィールドを持つ struct を複数登録することが可能です。(引数に可変長引数を取っているため)

cmp.Options の中に option で指定したい設定を入れる

   opts := cmp.Options{
        cmpopts.IgnoreUnexported(A{}),
    }

    if diff := cmp.Diff(x, y, opts); diff != "" {
        fmt.Printf("mismatch (-x +y):\n%s", diff)
    }

ref: https://play.golang.org/p/YrjDcmT6P9J

これは素直な実装です。Diff の可変長引数に指定されている ...cmp.Option は呼び出し側の Diff メソッドの内部では cmp.Option の配列として扱われるので、type Options []Option と定義されている (ref: https://pkg.go.dev/github.com/google/go-cmp/cmp#Options) と定義されている cmp.Options はそのまま cmp.Option の配列として Diff の引数に指定することができます。

cmp.Option 単体を指定する

   opts := cmpopts.IgnoreUnexported(A{})

    if diff := cmp.Diff(x, y, opts); diff != "" {
        fmt.Printf("mismatch (-x +y):\n%s", diff)
    }

ref: https://play.golang.org/p/9rKklLLQlQG

可変長引数が呼び出し側で配列(slice) に展開される、ということを考えると若干直感的ではないですが、可変長引数なので単体の cmp.Option を受け取ることも可能です。

slice にして指定する

   opts := []cmp.Option{cmpopts.IgnoreUnexported(A{})}

    if diff := cmp.Diff(x, y, opts...); diff != "" {
        fmt.Printf("mismatch (-x +y):\n%s", diff)
    }

ref: https://play.golang.org/p/-qbaKbwWI42

type Options []Option と定義されている && Diff の内部では opts は slice に展開されて利用されるということを考えると、指定時に cmp.Option の slice にしてしまって可変長引数を指定することも可能です。この場合 slice を可変長引数に指定するので $slice... という形で渡す必要があります。

Go の可変長引数の指定方法について

Go の関数の引数に可変長引数を指定した場合、以下のサンプルに示した3つのパターンで可変長引数を指定することが可能です。

サンプルコードだと 2 のパターンは直感的にはわかりやすいですが、3 のパターンは Go に慣れてないととっつきづらいかもしれません。

type str string

func main() {
    m("1")
    m("1", "2")
    m([]str{"1"}...)
}

func m(s ...str) {
    fmt.Println(s)
}

ref: https://play.golang.org/p/7N75PyB2wBW

cmp.Options をそのまま Diff に指定できるわけ

可変長引数を指定する場合 slice 型にした配列でも ... で展開指定しないと可変長引数には実は指定できません。

type strs []str
type str string

func main() {
    m(strs{"1"}...)
}

func m(s ...str) {
    fmt.Println(s)
}

ref: https://play.golang.org/p/pAlGE6ZZcH1

しかし cmp.Options はそのままでも引数に指定することができます。
※ 実は ... を指定することも可能です。

   opts := cmp.Options{
        cmpopts.IgnoreUnexported(A{}),
    }

    if diff := cmp.Diff(x, y, opts...); diff != "" { // ... を指定してる。
        fmt.Printf("mismatch (-x +y):\n%s", diff)
    }

ref: https://play.golang.org/p/Rq4nL01RNXP

cmp.Options... で展開指定せずとも、可変長引数に指定可能なのは、cmp.Optionscmp.Option の slice でありながら、Option として振る舞うことができる ( filter(s *state, t reflect.Type, vx, vy reflect.Value) を実装してる)からです。
Option の slice でありながら、Option 型と同等に振る舞うことができるので、上記の cmp.Option 単体を指定する と同様のことが 型上許容される という挙動をするようです。

まとめ

よく使っていたライブラリも、ちょっと使う期間が空いてしまうとすぐに使い方を忘れますし、実装方法をちょっと深ぼってみると面白いですね。

TRPL の数当てゲームをやってみる

Overview

The Rust Programing Language のチュートリアルにある「数当てゲーム」* をやってみました。

doc.rust-jp.rs

やった内容は以下にまとめてます。

github.com

*数当てゲームですが、日本語訳だとサンプルコードのシグネチャが古かったのでサンプルコードは英語版の方を参照することをお勧めします -> https://doc.rust-lang.org/stable/book/

Memo

クレートについて

クレートのエコシステムについてはまだ自分の中では色々手探り状態ではありますが、バイナリクレートとライブラリクレートがあって、ライブラリを追加する(Goでいう 3rd package を go get してきて使う)というのは ライブラリクレートを追加する 、という風に使いますし、クレートを追加する、という記述があるとだいたいこの ライブラリクレート のことを指すようです。

どんなクレートがあるかは https://crates.io/ を参照すればわかります。pkg.go.dev や npmjs.com みたいなものですね。

Result 型を返り値に持つメソッドとパターンマッチについて

io::std::read_line メソッドは Result 型を返すので match でパターンマッチでも書くことが可能です。

元々使用していた expect() は Result 型の失敗の可能性を扱うのに使います。

このio::ResultオブジェクトがErr値の場合、expectメソッドはプログラムをクラッシュさせ、 引数として渡されたメッセージを表示します。read_lineメソッドがErrを返したら、 恐らく根底にあるOSによるエラーに起因するのでしょう。 このio::ResultオブジェクトがOk値の場合、expectメソッドは、Ok列挙子が保持する 返り値を取り出して、ただその値を返すので、これを使用することができるでしょう。 今回の場合、その返り値とは、ユーザが標準入力に入力したデータのバイト数になります。

io::stdin().read_line(&mut guess).exprect("Failed to ...") と書くと、Result の結果が Ok だったらそのまま guess に結果が入って Err の場合に expect で指定したエラーメッセージが出力される、というデザインはいいですね。

新しい型での覆い隠し(Shadow)

Rust は変数の再代入があった場合にコンパイラがエラーを吐きますが、これとは別に別の型にして Shadow することは許可されてました。

Rustでは、新しい値でguessの値を覆い隠す(shadow)ことが許されているのです。

let mut guess = String::New() // 文字列型で定義
let guess: u32 = guess.trim().parse().expect("Please type a number!"); // uint32 で別型で覆い隠す (≠再代入)

一見再代入のように見えないこともないですが、これは再代入とは違います。

ちなみに変数の変換に利用されてる parse() メソッドについてですが、parse も Reulst 型を返すのでパターンマッチで書くことが可能です。

※ ここでは変換不能な文字列( ex. aaa とか)が入ってきた場合でもエラーハンドリングせず無視して続行する実装にしてます。

読書 Note -『Righting Software』- 第一部 -「構造」と「組み立て」

Overview

Righting Software を少しずつ読み進めていく中で第一部の 3~5章に書かれているソフトウェアの設計の「構造」と「組み立て」の内容が、今まで目にしてきた設計系の書籍やブログを読んできた中で一番腹落ちした内容 (*) だったので書籍に記載された構造についての自分なりのメモや考えたこと、及び Go で具体的に実装したサンプルをログとしてまとめておくためのエントリ。

*個人的には名著『Clean Architecture』よりもわかりやすい内容だと思った。

今回の学習内容は以下にまとめてある。

github.com

読書 Note

まず Righting Software に構造と組み立ての章に関して気になったところを簡単にまとめる。

ユースケースと要件

  • 要件は必要な機能ではなく、必要な振る舞いを明らかにするものではなければならない。
  • システムが何をすべきかではなく、システムがどのように動作しなければならないかを規定する。

必要な振る舞い

  • 必要な振る舞い(= システムがなんらかの仕事を達成してビジネスに付加価値を与えるためにどうしなければならないのか?) を表現したものが ユースケース
  • ユースケースはシステム内のアクティビティの特定のシーケンスになる。
  • ユースケースはエンドユーザーとシステムのインタラクションやシステムと他のシステムとのインタラクション、バックエンドの処理を記述できる。

表現方法

  • ユースケースは文章でも図でも記述できる。
  • 文章によるユースケースは簡単に作れ、そのことは大きなメリットだが、表現方法として図よりも劣っている。
    • 人間は文章をわざわざ読もうとしない笑

ユースケース図とアクティビティ図

ユースケース

f:id:ema_hiro:20210811162611p:plain

このレベルのユースケース図は簡単に記述できるし、図じゃなくても文章で簡単に記述できる。ただ、最初から図を書いた方がいい。

アクティビティ図

こちらを書籍で推奨しているのはこちら。
アクティビティ図は振る舞いの時間的な側面を捉えることができる(待ちや並列実行など)点もユースケース図よりも表現の幅がある。

f:id:ema_hiro:20210811174457p:plain

階層化されたアプローチ

  • ザ・メソッドでは階層構造を重用している。
  • カプセル化も階層構造である。
  • 個々の階層はそれぞれの変動性と下位階層の変動性をカプセル化して上位階層から見えないようにしている。

サービスの活用

階層を超える方法として望ましいのは サービスの呼び出し である。
サービスを使えば以下のようなメリットが得られる。

  • スケーラビリティ
    • サービスインスタンスは呼び出しごとに生成できる。
    • 進行中の呼び出し数と同じ数だけサービスインスタンスがあればよい。クライアントの数が多くても、比例してバックエンドリソースに負荷をかけずに済む。
  • セキュリティ
    • サービス指向プラットフォームはどれもセキュリティを重視している。
    • サービス間呼び出しにも認証認可が必要になり、なんらかの ID 伝播メカニズムを使うことになる。
  • スループットと可用性
    • サービスはキューを通じた呼び出しを受けつけることができることで、負荷の上限をオーバーする分の処理はそのままキューに貯めておく、という方法で膨大な数のメッセージを処理することができる。
  • 応答性
    • サービスであれば呼び出しをバッファリングして、システムが限界を超えることを避けることができる。
  • 信頼性
    • クライアントとサービス間では到達の保証、ネットワーク接続障害の処理、呼び出し順序の指定などに信頼性の高いメッセージングプロトコルを使うことができる。
  • 整合性
    • 結果整合性を保証する協調的なビジネストランザクションを使えば1つの作業に複数のサービスを参加させることができる。
    • 呼び出しチェーンの過程でエラーが発生したら、作業全体をなかったことにする。
  • 同期
    • クライアントが複数の並行スレッドを使っていたとしても、サービスの呼び出しは自動的に同期される。

階層の種類

本書には以下の階層(レイヤー)があると書かれている。

  • クライアント層
    • プレゼンテーション層とも呼ばれる。
    • エンドユーザーのユーザーアプリケーションを指すことが多い。
  • ビジネスロジック
    • ビジネスロジックの変動性をカプセル化する。
    • このレイヤーがシステムに必要とされる振る舞い(=ユースケース)を実装している。
    • ユースケースは顧客と時間の両方の変動性を孕んでいて、ユースケースの変更はユースケースを構成するためのシーケンスの変化、またま、シーケンス内の各アクティビティの変化の二種類しかない(*1)
    • 実装としては Manager と Engine がこのレイヤーでそれぞれカプセル化する。
      • Manager はシーケンスの変動性をカプセル化する。
        • ex. ビジネス都合で並行処理だったプロセスを直列に変更する etc...
      • Engine はアクティビティの変動性をカプセル化する。
        • ex. あるシーケンスの中に存在する通知アクティビティにおいてユーザーに通知していた内容を社内向けにも通知する etc...
      • Engine は Manager よりも関心の範囲が狭く、ビジネスルールやアクティビティそのものをカプセル化している。

*1. ユースケースの変動性は以下のように並行処理で実現されたユースケースが、ビジネスの変化により直列の処理が必要になった、というようなものがあげられる。

f:id:ema_hiro:20210811181857p:plain

  • リソースアクセス層
    • 文字通りリソースアクセスにまつわる変動性をカプセル化する。
      • なぜならリソースが永続的なストレージか一時的なインメモリストレージ化によってアクセス方法は変動するから。
    • ミドルウェアを類推させるような名前をインターフェースの名前に使わない。
      • インターフェースに select() や insert() があると裏には RDBMS があることを類推させてしまい、そんなにないと思うがリソースの変更に弱い。なぜなら、リソースを変更するとリソースアクセス層も変更しないといけなくなるから。
      • 同様に Open() や Close()、Write() といったインターフェースは裏側にファイルシステムがあることを類推させてしまい同上。
  • リソース層
    • 実際にDBやファイルシステム、キャッシュにアクセスするレイヤー
    • memo
      • ミドルウェアのクライアント部分に当たるので ifra 層などと呼称されるケースもあるレイヤー。
  • ユーティリティ層
    • ロギング、認証認可、pubsub など全てのシステムが横断で利用するものをまとめておくレイヤー。

その他

第3章前半で構造についての説明やガイドライン書かれていて、後半では拡張性やマイクロサービスのことも触れられている。
第4章では第3章で説明された階層や各コンポーネントの組み合わせ方(組み立て)について記載されている。 後半に記載してる Go のサンプルではその組み立て方もある程度参考にしている。 第5章では 3, 4 章で書かれている内容をベースにしたケーススタディになっている。

今回は構造についてまとめておきたかったので第3章の前半をベースにまとめているが、設計については第一部を通読するのは復讐も兼ねて勉強になると思う。

階層についての個人としての解釈・考察

各階層について

  • クライアント層
    • ユーザー及び、システムを使う側の最前面に露出するところなのでもっとも変動性が大きい。
  • ビジネスロジック
    • ここが一番腹落ちした。DDD とかで語られるレイヤーわけでよく見かけるユースケース層というものの本当の意味がわかった気がする。
    • マネージャーはユースケースを表現している。
      • マネージャーはユースケースを管理する。
      • Engine はアクティビティ図における各アクティビティノードを表現している。
      • マネージャーは Engine およびリソースアクセスレイヤーをインスタンス化し、それらをオーケストレーションする役割を担う。
      • ユースケースの変更があっても本来的にはマネージャーのみの変更で済むはずで変更が局所的になり、システムの振る舞いの変更に強くなる。
      • マネージャにはユニットテストが欲しくなる。なぜならビジネスロジックを実装として表現してるものだから。
  • リソース層
    • ミドルウェアのクライアント部分に当たるので ifra 層などと呼称されるケースもあるレイヤー。infra レイヤーと書かれると馴染みのあるレイヤーだなと思う。

ユースケースとアクティビティ図の考え方について

各アクティビティがドメインの受け持つロジックに関心事があり、マネージャーはそのドメインロジックを協調させてユースケース(=ある要件に対してのソフトウェアの振る舞い)を実現するものと捉えると、階層構造をとるアプリケーションの捉え方が少し変わった。
概要で参照してる PullRequest にて実装した設計は Righting Software で言及されている階層と組み合わせを、 ユーザー登録のユースケースをベースにして 以下のようなシーケンスをイメージして実装している。

これを図に起こすと以下のようなイメージになる。

f:id:ema_hiro:20210811191025p:plain

まとめ

階層構造を取るアーキテクチャについてずっとわかったようなわからなかったようなところを行ったり来たりしていたが、『Righting Software』に書かれている構造と組み分けの章を読んでみて、アクティビティ図とセットで考えることでようやく雰囲気で理解していたカプセル化の意味を少し実感できた気がする。
後半のプロジェクトデザインの章については、時間があればまた別のエントリでまとめたいと思う。

Rust で grep コマンドを実装する

Overview

以下のハンズオンの内容にある Rust で grep コマンドを再実装する部分を再度自分でやってみました。

speakerdeck.com

やった内容は以下にまとめてあります。

github.com

Memo

いくつか工夫したりメモしたりしたポイントがあるのでまとめます。

検出した文字列が何行目にあるのか?を追加

grep コマンドなのでファイルの "何行目に" あるのかを表示したいと思って行数を表示する変更を追加しました => https://github.com/emahiro/il/pull/32/commits/3faac486add2181287a8ed5321cb95581e2a62b8

structopt について

github.com

このクレートを追加して struct の引数にマクロを定義すると以下のように簡単にコマンドラインオプションをデフォルトで実装してくれます(めちゃくちゃ便利...!!!)

use structopt::StructOpt;

#[derive(StructOpt)]
#[structopt(name = "rsgrep")]
struct GrepArgs {
    #[structopt(name = "PATTERN")]
    pattern: String,
    #[structopt(name = "FILEPATH")]
    files: Vec<String>,
}

実行サンプル

cargo run # 引数なしで実行する

cargo run
   Compiling rs_grep v0.1.0 ($PATH/src/github.com/emahiro/il/rs_sandbox/rs_grep)
    Finished dev [unoptimized + debuginfo] target(s) in 1.68s
     Running `target/debug/rs_grep`

# 以下が自動的に付与される。
error: The following required arguments were not provided:
    <PATTERN>

USAGE:
    rs_grep <PATTERN> [FILEPATH]...

For more information try --help

ちょっとマクロを読むのは慣れないと難しいな、という感想はありますが。

クレートについて

Rust のパッケージやライブラリ、モジュールについてはまだ全然理解できてないですが、Go の package 単位で依存関係を追加するのは クレートを追加する というようです。

パッケージとクレートの関係については TRPL に記載してました。

doc.rust-jp.rs

Cargo.toml の中身を見ても、src/main.rs については何も書いてありません。これは、Cargoは src/main.rs が、パッケージと同じ名前を持つバイナリクレートのクレートルートであるという慣習に従っているためです。

どんなプロジェクトでもそれ自体はクレートなんですね。

ちなみにクレート (Crate) は木箱という意味なんですね。なるほど。

ja.wikipedia.org

TRPL のクレートとパッケージの章をざっと見ててとりあえず以下のところを最初は覚えておくととっつきやすいのかなと思いました。

doc.rust-jp.rs

ビルドした成果物の格納場所

Cargo.toml があるディレクトリルートの target/debug 配下にバイナリファイルとしておいてあります。

./Cargo.toml
./target/debug/$BuildResult 

サンプルとして rust で grep を実装したコマンドは以下のように表示されます。

./target/debug/rs_grep
error: The following required arguments were not provided:
    <PATTERN>

USAGE:
    rs_grep <PATTERN> [FILEPATH]...

For more information try --help

rayon を利用して並行処理

https://github.com/rayon-rs/rayon を利用してファイルの読み込みを非同期に動かすようにしましたが、その際に関数型っぽく書く書き方に変更しました。
ref: https://github.com/emahiro/il/pull/32/commits/d97d35a14b0f2f55b7100d5e85c41d04f539e5cb

これには理由があって、元々手続き型っぽく書いていたんですが、rayon のクレートを追加した際に変更した iter() -> par_iter() の部分で返り値が Iterator 型から String の Vector 型に変更されていたので Iteration を回す for ... in ... 構文が使えなくなりました。
そのため関数型っぽく for_reach の中でラムダ式を書いて同じような実装を実現してます。

See Also

doc.rust-jp.rs

Cookie に格納された情報を使っている Handler のテストを実装する

Overview

タイトルの通りです。Go で API を書くときなど HTTP の Handler を実装していくと思いますが、ここでログインなどの Cookie にセッションを持たせるような認証機構がセットになってる実装をする場合、この認証機構(特に管理画面でいうところの持ってる権限の判定など) を突破しないと Handler のテストが動かない、と言うケースがあると思います。もしかしたら認証機構を Middleware として切り出して Handler の処理本体では認証を行わない、と言う実装方針も考えることができますが、今回は Handler のなかで認証を実装してるケースにおける Cookie をセットした状態でテストを書くことを念頭におきます。最終的には Handler でチェックしてる認証機構を Middleware に切り出したり、と言ったことも考えられるかなと。

前提: そもそも Go で Cookie を操作するには?

Go の HTTP リクエストの処理において Cookie を操作するにはリクエストとレスポンスで扱うそれぞれのケースがあり、これらは全て net/http package の内部にあります。

Cookie に何を入れるかにもよりますが、おおよそ社内機能のようにアクセスが制限されてる環境であれば、セッション情報を埋め込んでログインだったりに利用することが多いと思います。

Cookie をログインセッションのために使うのはお手軽ですが、万能ではないのでちゃんと使われるサービスの特性を元に検討してください。ここでは Cookie に埋め込んだセッション情報をどう安全に取り扱うかについては言及しません。

Cookie を使った認証機構がある Handler のテスト

Cookie を取り出してその情報を何かしら検証してるテストにおいてはテストする時に Cookie をセットします。

サンプル

以下のような Handler の実装を考えます。

func (h *Handler) Get(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodGet {
        w.WriteHeader(http.StatusMethodNotAllowed)
        w.Write([]byte(http.StatusText(http.StatusMethodNotAllowed)))
        return
    }
    ck, err := r.Cookie("test")
    if err != nil {
        w.WriteHeader(http.StatusUnauthorized)
        w.Write([]byte(http.StatusText(http.StatusUnauthorized)))
        return
    }

    w.WriteHeader(http.StatusOK)
    w.Write([]byte(fmt.Sprintf("OK. cookie value is %#v", ck)))
}

これの Handler のテストにおいてテストを追加します。

Cookie を追加していない時

go test -v .
=== RUN   TestRouter
    main_test.go:37: Unauthorized
--- PASS: TestRouter (0.00s)
PASS
ok      github.com/emahiro/il/cookiesessiontest 0.239s

以下のように Cookie を追加した時

req, _ := http.NewRequest(http.MethodGet, ts.URL, nil)
req.AddCookie(&http.Cookie{
    Name:  "test",
    Value: "test",
})
resp, err := http.DefaultClient.Do(req)
if err != nil {
    t.Fatal(err)
}

この時のテストの結果は以下

go test -v .
=== RUN   TestRouter
    main_test.go:41: OK. cookie value is &http.Cookie{Name:"test", Value:"test", Path:"", Domain:"", Expires:time.Time{wall:0x0, ext:0, loc:(*time.Location)(nil)}, RawExpires:"", MaxAge:0, Secure:false, HttpOnly:false, SameSite:0, Raw:"", Unparsed:[]string(nil)}
--- PASS: TestRouter (0.00s)
PASS
ok    github.com/emahiro/il/cookiesessiontest 0.933s

雑な実装としては上記のとおりです。

SecureCookie を使って Cookie で Session を扱う

単純な Cookie ではなく、Session とセットで扱うために gorilla が提供してる https://pkg.go.dev/github.com/gorilla/securecookie を紹介します。

個人的には go-chi も好きですが、gorilla はさらにいろんなツールセットが整っていて好みなフレームワークです。Routing の仕方が特徴的ですが、ne/http の ServerHTTP を満たすインターフェースをしてる点も go-chi と同じで標準に乗っ取っていて好みです。

net/http 互換なので gorilla のパッケージを go-chi から利用することも可能です。

securecookie を使って Session をやりとりする実装をします。

まず最初に Cookie をセットする箇所は以下のようになります。

req, _ := http.NewRequest(http.MethodGet, ts.URL, nil)
codec := securecookie.CodecsFromPairs(keyPairs)
ckstr, err := codec[0].Encode("test", map [interface{}]interface{}{
    "test": "test",
})
if err != nil {
    t.Fatal(err)
}
req.AddCookie(&http.Cookie{
    Name:  "test",
    Value: ckstr,
})

取り出す側は以下です

store := sessions.NewCookieStore(keyPairs)
store.Options = &sessions.Options{}
session, err := store.Get(r, ckName)
if err != nil {
    // refresh session
    session = sessions.NewSession(store, ckName)
}

テストする側では以下のように securecookie を設定します。

store := sessions.NewCookieStore(keyPairs)
store.Options = &sessions.Options{}
session, err := store.Get(r, ckName)
if err != nil {
    // refresh session
    session = sessions.NewSession(store, ckName)
}

securecookie では codec 後の encode 処理をかける際に上記 interface の map を渡します。key も interface なら value も interface を渡すのでこれは本当の意味でなんでも渡せる君になってます笑。

map を渡せるようになってるので受け取る側では key を指定して SessionStore に格納された値を指定して以下のように取り出すことが可能です。

store := sessions.NewCookieStore(keyPairs)
store.Options = &sessions.Options{}
session, err := store.Get(r, ckName)
if err != nil {
    // refresh session
    session = sessions.NewSession(store, ckName)
}

w.WriteHeader(http.StatusOK)
w.Write([]byte(fmt.Sprintf("OK. cookie name is %s value is %#v", session.Name(), session.Values["test"]))) // map[interface{}]interface{} で指定した key で値を取り出せる

少しややこしいな、と思ったのは net/http の Cookie の実装と違い、ここは Cookie をセットする時は SecureCookie として扱うものの、Cookie からセッション情報を取り出した場合には Session として以下扱っているところです。

ちょっとだけリファクタリングする

最後に handler で Cookie の情報を取り出していた箇所を Middleware にしました -> https://github.com/emahiro/il/pull/31/commits/3ee20b9ead7fc24b8238360dee4e40f06ba87cbb

まとめ

このエントリーでざっと流した内容は https://github.com/emahiro/il/pull/31 にまとめてあります。

Memo

http: Request.RequestURI can't be set in client requests が出る

httptest.NewRequest で httptest.Newserver でインスタンス化したサーバーにリクエストを送ると http: Request.RequestURI can't be set in client requests というエラーが出ます。

=== RUN   TestRouter
    main_test.go:23: http://127.0.0.1:49592
    main_test.go:29: Get "http://127.0.0.1:49592": http: Request.RequestURI can't be set in client requests

httptest で立ち上げたサーバーの URL を指定するとURI が指定されていない、というかクライアントではセットできないと言われてリクエストが不通です。

req, err := http.ReadRequest(bufio.NewReader(strings.NewReader(method + " " + target + " HTTP/1.0\r\n\r\n")))

パッと調べた感じ、httptest の中でより LowLevel な関数を Call しており、この辺が怪しそうでした。

// ReadRequest reads and parses an incoming request from b.
//
// ReadRequest is a low-level function and should only be used for
// specialized applications; most code should use the Server to read
// requests and handle them via the Handler interface. ReadRequest
// only supports HTTP/1.x requests. For HTTP/2, use golang.org/x/net/http2.
func ReadRequest(b *bufio.Reader) (*Request, error) {
    return readRequest(b, deleteHostHeader)
}

この readRequest 関数の内部で parseRequestLine という処理を読んでおり、この中で Request.RequestURI が空になって返ってそうだったので、Request.RequestURI がないことによるリクエストの普通が発生してると考えてます。時間がある時に深掘りしてみようと思います。

Go で作る自作 Linter 開発入門

Overview

Go の静的解析、というか x/tools/go/analysis package を利用して簡単にコードを検査する自作 Linter の実装に入門します。

今回使ったサンプル実装は以下に置いてあります。

github.com

準備

以下のツールを使います。

  • GoAst Viewer
  • 以下の Go のパッケージ
    • golang.org/x/tools/go/analysis
    • golang.org/x/tools/go/analysis/passes/inspect
    • golang.org/x/tools/go/analysis/singlechecker
    • golang.org/x/tools/go/ast/inspector

自作 Linter を作る手順

GoAst Viewer で Ast の構造を視覚的に確認する

僕もそうですが、初心者の一番のハードルって Ast の構造を頭の中でトレースすることができないことだと思います。いまだに慣れません。

そこでこの GoAst Viewer って言うツールはとても重宝してて、検査したい Bad コードに似たサンプルコード*(コンパイル通らなくても良いを載せて、自作検査ツールを作る前にまずどう言う構造をしてるのかをチェックします。

※ プロダクションコードなんかの検査をしたいケースはそのまま form に商用利用してるコードを叩き込むのも不安かなと思うので。機密情報ですし。

追記

x/tools/cmd/gotype

pkg.go.dev

gotype -ast $File を使うと GoAst Viewer の Dump と同じ結果が得られるので、商用利用してるコードの静的解析を行いたい場合はこれを使うと良さそうです。
Dump を読むのはちょっと骨が折れますが。

go get -u golang.org/x/tools/cmd/gotype
gotype -ast ./testdata/src/a/a.go

singlechecker を使って検出する

go/analysis を使ってコードの検査をするには singlechecker package を使うのが簡単です。

analysis.Analyzer を定義してそれを singlechecker に放り込むだけでファイル及びファイルの中を ast の Node に従って検査をしてくれます。

var a = &analysis.Analyzer{
    Name:     "samplechecher",
    Doc:      "sample code checker",
    Run:      run,
    Requires: []*analysis.Analyzer{inspect.Analyzer},
}

func main() {
    singlechecker.Main(a) // run の中が実行されます。
}

func run(pass *analysis.Pass) (interface{}, error) {
    i := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
    filter := []ast.Node{}
    i.Preorder(filter, func(n ast.Node) {})
    return nil, nil
}

ref: https://github.com/emahiro/il/pull/29/commits/1eada5667a829f5f2761faec21d7140ea4fefdab

あとはひたすら ast の Node を掘っていってチェックしたい文字列に対して pass.Report で出力をするだけです。

nodeFilter で階層を指定する

これちょっとハマったんですが、検査対象の ast.Node をフィルタすることができます。例えば GoAst Viewer で確認すると、階層としては FuncDecl > CallExpr と言う順序になって CallExpr の方が 階層が深いのでここで指定した ast.Node 以下のみを検査対象とすることができますが、逆に言うとフィルタリングしてしまった Node より上層の Node については検査できません。

// 以下は FuncDecl 以下の Node を検査対象にできる。
filter := []ast.Node{
        (*ast.FuncDecl)(nil),
}

// 以下は CallExpr 以下の Node を検査対象にできるが、FUncDecl は対象にならない(っぽい)
filter := []ast.Node{
        (*ast.CallExpr)(nil),
}

Tips

SuggestedFixes がめちゃくちゃ便利なので絶対に使う

同僚の daisuzu さんのブログがめっちゃ詳しいですが、コードの修正がコードから簡単に行えます。

daisuzu.hatenablog.com

使い方

サンプルコードは以下です。これは Smaple と言う文字列を Example に変えたいと言う場合に SuggestedFixied を使った例です。

pass.Report(
    analysis.Diagnostic{
        Pos:     ident.Pos(),
        End:     ident.End(),
        Message: "change Sample to Example",
        SuggestedFixes: []analysis.SuggestedFix{
            {
                Message: "Sample -> Example",
                TextEdits: []analysis.TextEdit{
                    {
                        Pos:     ident.Pos(),
                        End:     ident.End(),
                        NewText: []byte("Example"),
                    },
                },
            },
        },
    },
)

このように書いておいてあとは go run . -fix $File のように自作ツールに -fix オプションを設定して実行することで、自作ツールの検査で引っかかったところを直したい文字列に自動で修正してくれます。

ハマったところ

Pos/End の位置

というかよく使ってて間違うんですが Position を取り違えると意図せず丸ごと変換してしまいます。サンプル PR だと以下のコミットの前後です。

このコミットを積むまでは例えば func Sample() error という関数名を func Example() error と変更したくても、position が意図とは違った位置で書き換えを行なってしまい Sample と含まれる全ての行そのものを Example に変換してしまいます。このため、より範囲を限定した ast.Ident を使ってる箇所を書き換えることで意図する挙動にしました。

こればっかりは動かしてみながら調整する他ないかなと思いますが、どの範囲(Ast Viewerで確認できる)を今操作してるのかをみながら実装するのがいいと思います。

検査の主力結果をテストする

検出されるべき文字列をテストすることができます。

自作ツールの動作(警告文など)をテストするツールとしてanalysis パッケージは analysistest を提供しており、その中の https://pkg.go.dev/golang.org/x/tools/go/analysis/analysistest#Run 関数において自作 Linter で検出されるコードが書かれてる行に以下のように want 文を書くと example テストのように検出結果のテストができます。

// ./testdata/src/a/a.go
func Sample() error { // want `change Sample to Example`
    return nil
}

SuggestedFixes を使ってる場合は https://pkg.go.dev/golang.org/x/tools/go/analysis/analysistest#RunWithSuggestedFixes が用意されており、これを使ってテストする場合、検出 & 修正後の結果になっている golden ファイルを要求されます。(これで出力結果だけでなく、文字列修正した場合の結果もテストできます。)

ちなみにテストは go.mod と同階層に testdata ディレクトリを用意してその配下で通常のパッケージ構成のようにテスト用の Go のファイルを用意することで analysistest から簡単にテストを実行できます。

// ディレクトリ構成
go.mod
testdata
    src
        a
            a.go

テストを実装するときは以下のように実装します。

testdata := analysistest.TestData()
analysistest.RunWithSuggestedFixes(t, testdata, a, "a")

※ このとき a package 内に Fixed した後の結果を出力してある golden ファイルがあることが必要です。じゃないと test を回したときに以下のようなエラーが発生します。

analysistest.go:171: error reading $PATH/src/github.com/emahiro/il/go_sandbox/codeanlyzersample/testdata/src/a/a.go.golden: open $PATH/src/github.com/emahiro/il/go_sandbox/codeanlyzersample/testdata/src/a/a.go.golden: no such file or directory

ハマったところ

大きな話でもないんですが、記号(ex. () など)を pass.Report の中で使っている場合、警告メッセージを出す want のないのバッククオテーションのなかで目地的に \(\) のようにエスケープすることが求められます。これをしないと警告文が違うと判定されてテストが延々とおりません。

ReviewDog 🐶 と組み合わせてみる

ReviewDog には検査で引っかかったところを GUI 上で Commit を積んで修正できる suggester の機能があります。これをつかうと自作チェッカーで解析して検査結果を出力したところを手元でコミットを積むことなく GitHub の PR の GUI 上から検査できて良いので自作チェッカーと組み合わせて使いたいとします。

ハマったところ

go run . ./testdata/src/a/a.go
$PATH/src/github.com/emahiro/il/go_sandbox/codeanlyzersample/testdata/src/a/a.go:10:1: change Sample to Example
$PATH/src/github.com/emahiro/il/go_sandbox/codeanlyzersample/testdata/src/a/a.go:14:1: change SampleWithContext to ExampleWithContext
exit status 3

上記の終了時のステータスコードに着目すると exit status が 3で、正常終了してません。

静的解析結果で引っかかったところがあるとツール内(checker の実装内)で正常終了で終わってくれず、例えばツールで引っかかった結果に対して Suggestion Fix をかけたいケースなどでは正常にその先に Suggestion Fix に結果が伝播しない、と言うことがありました。

以下のコードをみるとわかります。

上記にも記載しましたが、GitHub Actions で ReviewDog の Suggester を動かしたい場合は検査結果と変更の提案(diff形式)を検出した後 checker の実行は正常終了として扱い、検出された diff 形式を Reviewdog の Suggester まで渡さないといけません。つまり exit code 3 では困ります。

そこで actions の定義 yaml の中で以下のように Run を定義することで解決しました。

- name: Run reviewdog
        env:
          REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.YourGHSecretToken }}
        run: go run checker -fix $FilePath | exit 0 // パイプでつないで checker の結果の exit code 3 を正常終了(= exit code 0) にねじ曲げる。
- uses: reviewdog/action-suggester@v1
        with:
          github_token: ${{ secrets.YoutGHSecretToken }}
          level: "$Severity"
          fail_on_error: false // true にすると CI の検査結果が Fail になります。

まとめ

GoAst Viewer と singlechecker を使うと小難しいことをとりあえず抜きにして、とりあえず簡単に静的解析に入門できます。構造さえわかってしまえは Linter 作成はほぼ筋力で解決できるのでやろうと思えば誰でもできます。個人的には SuggestedFixes が本当に便利なのと、ReviewDog と組み合わせるとレビューの生産性はいくらでも向上すると思っています。

現職でも不具合や障害を起こした実装を繰り返さないようにいくつか有志で自作ツールが実装されて実際に CI の中で動いています。プロダクト固有の実装ルールなどはいくらでもあると思いますし、Go を書く上での一般的な Linter(golangci-linter や staticcheck) でカバーできないものがある場合に非常に有用なのでぜひ実装してみてください。

僕も慣れるまでは何個か時間見つけて作ってみます。ちなみにテンプレはこのコミットを作ったので参考にしてみてください → https://github.com/emahiro/il/pull/29/commits/1eada5667a829f5f2761faec21d7140ea4fefdab

See Also

  • https://github.com/yuroyoro/goast-viewer
  • 今回のサンプルでは使用してませんが、ast の構造上各 Node ごとにより深ぼって検査したい (ex. ast.BlockStmt から CallExpr を取り出して別の検査を走らせたい)場合には ast.Inspect を使うと便利です。具体的には以下のように CallExpr の中に入って検査をすることができます。全部 main で処理しちゃうと複雑なので関数に分けたいケースなどに使います。
func InspectMore(b *ast.BlockStmt) {
    ast.Inspect(ident, func(n ast.Node) bool {
            call, ok := n.(*ast.CallExpr) // BlockStmt -> CallExpr の中に入る.
            if !ok {
                    return true
            }
            // Other lint operation.
            return true // true にすると検査を次に進める.
    })
}

ref: https://pkg.go.dev/go/ast#Inspect

Go の string についてなんとなく調べた

Overview

ちょっと興味があって Go の string の実装周りについてふらっと調べたので備忘録。
そのほかにも何かわかれば追記するかも。

Memo

string 本体について

string is the set of all strings of 8-bit bytes, conventionally but not necessarily representing UTF-8-encoded text. A string may be empty, but not nil. Values of string type are immutable.

ref: https://pkg.go.dev/builtin#string

Go の文字列そのものは 8-bit の byte 配列の文字列の集合。

実行環境での string の実体については以下

// StringHeader is the runtime representation of a string.
// It cannot be used safely or portably and its representation may
// change in a later release.
// Moreover, the Data field is not sufficient to guarantee the data
// it references will not be garbage collected, so programs must keep
// a separate, correctly typed pointer to the underlying data.
type StringHeader struct {
    Data uintptr
    Len  int
}

ref: https://golang.org/src/reflect/value.go

データ = 文字列の中身を表す参照と長さの情報を持っている。GC から守られてるわけではない(GCで回収されうる)

fmt.Fprintf で ``(バッククオート)で囲った文字列をformatしたときにハマった話

Overview

タイトルの通りです。
Go で文字列の中に改行コードが含まれていた場合 "" なら \\n のようにバックスラッシュを使ってエスケープしますが、そもそもの改行コードを含む文字列をバッククオートで出力した場合、自動で改行コードがエスケープされる、という挙動について fmt の使い方でハマるところがあったので備忘録。

改行コードをエスケープする

シンプルなエスケープは以下

func main() {
    s := "ハロー\\n私は太郎です。"
    str := fmt.Sprintf("%s", s)
    fmt.Println(str)
}

// Output: ハロー\n私は太郎です。

https://play.golang.org/p/JlSVd363_sc

バッククオートを使った場合は以下

func main() {
    s := `ハロー\n私は太郎です。`
    str := fmt.Sprintf("%s", s)
    fmt.Println(str)
}

// Output: ハロー\n私は太郎です。

https://play.golang.org/p/cUJKw6wKOdm

ダブルクオテーションで囲った文字列の場合、そのままでは改行コードがエスケープされません。

fmt package で罠にハマる

バッククオートで囲ってる改行コード付き文字列を fmt を使って%sでフォーマット出力するとエスケープされない、という挙動に出会いました。

func main() {
    s := "ハロー\n私は太郎です。"
    str := fmt.Sprintf("%s", s)
    fmt.Println(str)
}

// Output: ハロー
私は太郎です。

https://play.golang.org/p/yKBsxsoJwRD

これは""で囲われてる文字列の改行コードがそのまま評価されて出力結果が改行されます。

func main() {
    s := `ハロー\n私は太郎です。`
    str := fmt.Sprintf("%s", s)
    fmt.Println(str)
}


Output: ハロー\n私は太郎です。

https://play.golang.org/p/cUJKw6wKOdm

バッククオートで囲った時は改行コードは評価されずに文字列として出力されます。

func main() {
    s := `ハロー\n私は太郎です。`
    str := fmt.Sprintf(`"%s"`, s)
    fmt.Println(str)
}

// Output: "ハロー\n私は太郎です。"

https://play.golang.org/p/i6WgbSnT7Mt

`` で囲われた format の中において %s を使って出力のフォーマットを行う場合 "" で囲われた文字列の中に改行コードがエスケープされずにそのまま出力された文字列になります。
ここで困るのは上記のようなエスケープされていない文字列が出力されてしまう場合、これを JSON も Key などに当ててしまったら JSON の内部では "\n" が評価されしまい、意図しない改行が入ってしまいます。

このため、バッククオートで本来エスケープされるはずの改行コードをエスケープした状態にするには以下の2つの方法があります。

  1. %q を使う -> シングルクオートで出力してくれる -> バッククオートで囲った文字列についてエスケープ済みで出力してくれる。
    ref: https://play.golang.org/p/uFaY3w1d1fG

  2. https://pkg.go.dev/strconv#Quote を使う。
    https://pkg.go.dev/strconv#example-Quote に大体使い方は書いてます。

まとめ

%q とても便利だなと思いました。

追記

String literals

"" と `` はそもそも文字列リテラルが違うので出力が異なるのは仕様。(生の文字列リテラルと解釈された文字列リテラル

ref: https://golang.org/ref/spec#String_literals

特に以下の部分

Raw string literals are character sequences between back quotes, as in foo. Within the quotes, any character may appear except back quote. The value of a raw string literal is the string composed of the uninterpreted (implicitly UTF-8-encoded) characters between the quotes; in particular, backslashes have no special meaning and the string may contain newlines. Carriage return characters ('\r') inside raw string literals are discarded from the raw string value.

生の文字列リテラルは暗黙的に UTF-8エンコードされてる、と仕様で書いてある。

Rust のパターン記法 ( Pattern matching )

Overview

Rust の Pattern matching がとても便利だなと思ったので備忘録です。

TRPL には以下のように記載されています。

doc.rust-jp.rs

これを読めばパターン記法に関して必要なことは大体網羅されてるので本当にドキュメントが充実してるいい環境だなと思います。Rust。

単純なパターンマッチング

let x = 10;
match x {
    1 => print!("one\n"),
    2 => print!("two\n"),
    _ => print!("none\n")
};

switch ケースの default 的などこにもマッチしなかった場合には match 記法では _ で表現します。
わかりやすいなーと思います。

Some/None というパターンマッチング

個人的にいいな思ったパターンマッチングのデザインがこれです。

let x: Option<i32> = Some(10);
match x {
    Some(n) => print!("found {}\n", n),
    None => print!("none")
};

サンプルは上記の通りなんですが、パターンマッチさせる対象の型がはっきりしない場合(= Option 型の場合)に match 記法を使うとエディターの補完サポートなどもあって、マッチして取り出した型がなんなのかがわかります。

これで実装するときに有用だなと思ったのが、コマンドラインツールを実装するときで、cat コマンドを自作するときに引数の2つ目(index=1) に何が入るのかを取り出すのに使えます。

use std::{env::args, fs::read_to_string};

fn main() {
    match args().nth(1) {
        Some(path) => print!("path is {}\n", path),
        None => print!("No path is specified\n")
    }
}

サンプルコードは上記です。実行するとこんな感じで指定したファイルを標準出力に吐きます。

cargo run ./src/main.rs
   Compiling rs_cathandson v0.1.0 ($HOME/src/github.com/emahiro/il/rs_sandbox/rs_cat_handson)
    Finished dev [unoptimized + debuginfo] target(s) in 0.39s
     Running `target/debug/rs_cathandson ./src/main.rs`
use std::{env::args, fs::read_to_string};

fn run_cat(path: String) {
    match read_to_string(path) {
        Ok(content) => print!("{}\n", content),
        Err (err) => print!("error: {}\n", err)
    };
}

fn main() {
    match args().nth(1) {
        Some(path) => run_cat(path),
        None => print!("No path is specified\n")
    }
}

if let 記法

Option 型は値がない可能性があることを表す型で Some で値を取り出し None は値がなかったことになります。
パターンマッチングにおいて値があるときだけ処理を続けたい、というケースを実装する場合 None が冗長だったりします。そこで使うのが if let です。

fn main() {
    if let Some(path) = args().nth(1) {
        run_cat(path)
    }
}

シンプルですね。ただこれだけで match Some/None を表現してるかと思うと表現力の高い言語だなーと思います。

タプルのパターンマッチング

パターンマッチングしたいターゲットが2つある場合タプルが使えます。サンプルは以下です。match 記法に対して (A, B) というタプルを取ることができます。

let arg1 = args().nth(1);
let arg2 = args().nth(2);

match (arg1, arg2) {
    (Some(arg1), Some(arg2)) => println!("arg1: {}, arg2: {}", arg1, arg2),
    _ => println!("Pattern or path is not specified.\n"),
};

この場合 None のとりうる値が増えます。上記のサンプルでは default を表現するために _ を使ってますが、(Some(A), Some(B)) のようにタプルでパターンマッチをかける場合、以下のように Some に対応した None をそれぞれカバーしないとコンパイラに怒られます。

non-exhaustive patterns: `(None, Some(_))` and `(Some(_), None)` not covered patterns `(None, Some(_))` and `(Some(_), None)` not covered
  1. Some(A), Some(B)
  2. None, Some(_)
  3. Some(_), None
  4. None, None

_ は 2,3,4 全部のケースを1つにまとめることができる記法です。

まとめ

Rust をぼちぼち書き始めてますが、パターンマッチやResult型でのエラーハンドリングなど普段使ってる Go にはない要素が多数あって描いてて新鮮です。

Rust で制御構文を書く

Overview

4連休なので Rust のお勉強をしてる記録です。いつまで続くかはわかりません。

ema-hiro.hatenablog.com

Rust のハンズオンを開催してもらって以降、開発できる環境を整備したりしてましたので今回は改めて制御構文について単純な Fizzbuzz を実装した内容をまとめます。

Rust の制御構文

Rust の制御構文は式なので変数にアサインすることができます。しばらく制御構文が式と評価される言語を触っていなかったので割と新鮮でした。
制御構文が式なので以下のように書くことができます。

let fizzbuss = if num % 15 == 0 {
    "FizzBuzz".to_string()
} else if num % 5 == 0 {
    "Buzz".to_string()
} else {
    "Fizz".to_string()
};
  • 式なので制御構文の中で値を必ず返します。
  • else if を使った場合は最後に必ず else をつける必要があります。

その他やったこと

Iterator で loop を実装する

Go でいうところの for 文で値を回してみます。

fn iter() {
    for num in 0..100{
        println!("{}", num)
    }
}

シンプルですね。これは index 0 から 99 までの loop を回すことができます。

コマンドライン引数を取り出してみる

シンプルな FizzBuzz を実装しただけでは面白くなかったのでコマンドライン引数を取り出して指定した値の評価する実装をします。
コマンドライン引数の取り出し方は TRPL にそのまま書かれてます。

doc.rust-jp.rs

※ 標準ライブラリの std::env ライブラリで標準入出力関連で使うライブラリなのでしばらくお世話になりそうかなと思いました。

String を Int32 に変換する

以下を参考にしました。先に変換先の型で定義した変数に対して parse と unwrap をかけるとその型に変換してくれるのスマートでいいなと思いましたね。

stackoverflow.com

実装

というわけでこんな風に実装しました。

use std::env::{self};

fn main() {
    let args: Vec<String> = env::args().collect();
    let numstr = &args[1].to_string();
    let num: i32 = numstr.parse().unwrap();
    let fizzbuss = if num % 15 == 0 {
        "FizzBuzz".to_string()
    } else if num % 5 == 0 {
        "Buzz".to_string()
    } else {
        "Fizz".to_string()
    };
    println!("{}", fizzbuss);
}

これを実行してみると以下のようになります。

cargo run 15
   Compiling rust_handson_20210716 v0.1.0 (~/src/github.com/emahiro/il/rs_sandbox/rust_handson_20210716)
    Finished dev [unoptimized + debuginfo] target(s) in 0.93s
     Running `target/debug/rust_handson_20210716 15`
FizzBuzz

まとめ

制御構文とiteratorコマンドライン引数についてざっと触ってみました。

byte 文字列を Int に変換する

Overview

ちょっと凝ったユースケースですが、byte 文字列を Int に変換したいユースケースがあったときに使えるテクニックについてまとめます。

以下のようなユースケースを考えます。

func main() {
    b := []byte("123456")

    for idx := range b {
        fmt.Println(b[idx])
    }

}

// OUTPUT:
// 
// 49 <- 1 
// 50 <- 2
// 51 <- 3
// 52 <- 4
// 53 <- 5
// 54 <- 6

ここで byte 配列に格納された書く byte 文字列を自然数の配列に変換したいとします。
byte 文字列で 49(ASCIIコード上) は 1 なのでこの与えられた byte 配列を 1,2,3,... という Int の配列に変換することを目指します。

ASCII コードの 0 を使う

要は各 byte 文字列から 48 を引いた値が Int に変換したときの数字に一致するので 48 を引いてあげると良さそうです。

文字としての 0 は ASCII コード表 の上では 48 番目で対応する ASCII コードは 0x30 です。 なのでこれを与えられた数から引きます。

func main() {
    b := []byte("123456")

    for idx := range b {
        fmt.Println(b[idx] - 0x30)
    }

}

これで目的とする byte 文字列を Int に変換することができます。

Atoi の実装を参考にする。

文字列から int に変換するときによく使う strconv.Atoi の実装で似たようなことをしています。
ref: https://golang.org/src/strconv/atoi.go?s=5658:5690#L241

// Atoi is equivalent to ParseInt(s, 10, 0), converted to type int.
func Atoi(s string) (int, error) {
    const fnAtoi = "Atoi"

    sLen := len(s)
    if intSize == 32 && (0 < sLen && sLen < 10) ||
        intSize == 64 && (0 < sLen && sLen < 19) {
        // Fast path for small integers that fit int type.
        s0 := s
        if s[0] == '-' || s[0] == '+' {
            s = s[1:]
            if len(s) < 1 {
                return 0, &NumError{fnAtoi, s0, ErrSyntax}
            }
        }

        n := 0
        for _, ch := range []byte(s) {
            ch -= '0'
            if ch > 9 {
                return 0, &NumError{fnAtoi, s0, ErrSyntax}
            }
            n = n*10 + int(ch)
        }
        if s0[0] == '-' {
            n = -n
        }
        return n, nil
    }

    // Slow path for invalid, big, or underscored integers.
    i64, err := ParseInt(s, 10, 0)
    if nerr, ok := err.(*NumError); ok {
        nerr.Func = fnAtoi
    }
    return int(i64), err
}

ch -= '0' に注目します。一つ前では文字列 0 を表す ASCII コードを目的とする byte 文字列から引いて目的とする Int を取り出してましたが、 Atoi の実装ではUnicodeコードポイントを持つ rune で 0 を表現してそれを byte から引くことで実現してます。

'0' の中身を出力してみるとわかります。

func main() {
    fmt.Println('0')
}

// OUTPUT:
//
// 48

つまり '0' を使うと byte 文字列からそのまま目的とする int に変換することができます。

func main() {
    b := []byte("123456")
    for idx := range b {
        fmt.Println(b[idx] - '0')
    }

}

これでも同様に動作させることができます。

まとめ

byte -> int という実装で以下のことがわかりました。

  1. byte文字列と ASCII コードはセットで計算できる
  2. byte文字列と Unicode コードポイントはセットで計算できる。

文字列の変換、奥が深い...。

Go でテキストを1行ずつ読み込む

Overview

Go でテキストファイルを1行ずつ読み込んでいく方法を忘れてたので思い出しついでに備忘録です。

bufio を使う

bufio を忘れてました。

こんな感じで処理できます。

package main

import (
    "bufio"
    "bytes"
    "fmt"
)

// text ファイルに書かれてる内容だとする
const s = "aaaaaa\nbbbbbb\ncccccc\n"

func main() {
    buf := bytes.NewBufferString(s)
    scanner := bufio.NewScanner(buf)
    for scanner.Scan() {
        fmt.Println(scanner.Text())
    }
}

// output:
// 
// aaaaaa
// bbbbbb
// cccccc

ref: https://play.golang.org/p/1uRGl8Ox_jK

VSCode で Rust の開発環境を整備する

Overview

VSCode での Rust の開発環境を整備する手順にまとめます。PR としては以下にまとめました。

github.com

Install Rust

以下に書いてある手順そのままです。

doc.rust-jp.rs

Rust ではこの rustup というツールという公式のツールをつかって色々とインストールするんですね。

ツール

公式の Rust の Extension + rust-analyzer を入れれば完了です。

marketplace.visualstudio.com

marketplace.visualstudio.com

rust-analyzer を動かす

標準の Rust の Extension を入れた状態だと rust-analyzer が動いてくれませんでした。

f:id:ema_hiro:20210720025430p:plain

これは静的検査に必要なツールが入っていないからだったので、これらを入れます。必要なのは以下の3つのツールです。rustup コマンドを使ってインストールします。

 rustup component add rust-src
 rustup component add rust-analysis
 rustup component add rls

これで VSCode をリロードして main.rs を開くと自動補完始め、開発しやすい状態が整います。

その他

play.rust-lang.org

See Also

あとは以下を読みながら少しずつ進めることにします。

blog-dry.com

Rust ハンズオンを開催したブログを書きました

会社のブログで金曜日に開催した Rust のハンズオンに関するエントリを書きました。

link.medium.com

登壇者からずっと Rust やらないんすか?とプレッシャーをかけられていて、本まで貰っていたのですが、ようやく踏ん切りついてはじめました。
ハンズオンは非常に盛り上がって色々脱線してもしまって最後まで行けなかったので、まずは題材となってる grep コマンドの実装を最後までやってから Web API 書いてみる、くらいのことはしようかなと思っています。

前提となる知識もそれなりに必要なソフトウェアなのでとっつきづらさはありますが、エコシステム自体は非常に成熟してるので使う上では安心して使えそうな印象を持ちました。やっていくぞ!