emahiro/b.log

Drastically Repeat Yourself !!!!

Go で画像を操作する

Overview

業務において Go で画像を操作する方法を調べたので備忘録。Go の標準パッケージにある image package は必要最低限の機能を十分に備えてて便利だなと思いました。

画像の拡張子を操作する

拡張子を変換する。

同僚のブログが参考になりました。

rennnosukesann.hatenablog.com

拡張子を判別する

import (
   // blank import しないと format の判別ができないので注意
    _ "image/gif"
    _ "image/jpeg"
    _ "image/png"
)

url := "$SomeImageURL.jpg"
response, err := http.Get(url)
if err != nil {
    panic(err)
}

defer response.Body.Close()
_, format, err := image.Decode(response.Body)
if err != nil {
    fmt.Println(err)
    return
}
fmt.Println(format) // jpg

判別も簡単にできます。コメントに書いてますが、判別したいフォーマットを blank import していないと unknown になってしまいます。

http.DetectContentType で拡張子を判定する

http package には DetectContentType というバイト配列から画像フォーマットを指定する機能があります。

DetectContentType implements the algorithm described at https://mimesniff.spec.whatwg.org/ to determine the Content-Type of the given data. It considers at most the first 512 bytes of data. DetectContentType always returns a valid MIME type: if it cannot determine a more specific one, it returns "application/octet-stream".

base64 にされた画像の拡張子判定の実装は以下のような感じです。

b, err := base64.StdEncoding.DecodeString("$base64encodedString")
if err != nil {
    panic(err)
}
format = http.DetectContentType(b) // image/png etc...

image package での encode/decode を要しないので http.DetectContentType はメモリに優しいです。
なお、画像の拡張子については実際の判別してるコードを見ると

&exactSig{[]byte("\x89PNG\x0D\x0A\x1A\x0A"), "image/png"},
&exactSig{[]byte("\xFF\xD8\xFF"), "image/jpeg"},

ref: https://cs.opensource.google/go/go/+/refs/tags/go1.17.7:src/net/http/sniff.go;l=125-126;drc=refs%2Ftags%2Fgo1.17.7

という形で先頭の数バイトで一般的な画像の拡張子を判定しています。そのため実際に実装する場合には base64 エンコードされた文字列の先頭数バイトを見ればいいわけです。つまり上記のコードは以下のように変更することができます。

data := "$base64encodedString"
encLen := base64.StdEncoding.EncodedLen(8) // 先頭8バイトを読み込んで判別する。
b, err := base64.StdEncoding.DecodeString(data[:encLen])
if err != nil {
     panic(err)
}
format = http.DetectContentType(b) // image/png etc...

画像を作成する

サクッと画像を生成することもできます。
単体テストで画像を使うケースなど便利かなと思います。

buf := bytes.NewBuffer([]byte{})
img := image.NewRGBA(image.Rect(0, 0, 50, 50))
_ = png.Encode(buf, img)

// 書き出した画像を base64 文字列に変換
enc := base64.StdEncoding.EncodeToString(buf.Bytes())
fmt.Println(enc)

Encode/Decode する

base64

Go Tour が詳しいです。

oohira.github.io

AWS の API Call に Retry option を設定する

サマリ

AWS のリソースを API 経由で Call するときに Retry option を使うと便利、というお話です。
メッセージを trigger にして Lambda を起動させるなど、イベント駆動でプロセスを開始するようなサービスを使うケースでは、失敗しても Retry してくれる機構が予め用意されていたりしますが、直接 AWS のリソースを API で呼び出すケースでは Retry を指定することができます。
普段自分は Go を使っているので AWS-Go-SDK を使う前提の話をしますが、以下のドキュメントに詳しく書いてあります。

aws.github.io

実装方法

sqs を例に取ります。

例えば backoff retry を明示的に指定するのは以下です。

var opt []func(*sqs.Options)
opt = append(opt, func(o *sqs.Options) {
    std := retry.NewStandard()
    o.Retryer = retry.AddWithMaxBackoffDelay(std, time.Second*5) // 5秒間 backoff 期間を設ける
})

リトライの試行回数を指定することもできます。

var opt []func(*sqs.Options)
opt = append(opt, func(o *sqs.Options) {
    std := retry.NewStandard()
    o.Retryer = retry.AddWithMaxAttempts(std, 5)
})

AWS Go SDK V2 ではこうした APIOption が多数用意されているので用途に応じて設定するとよしなに対応してくれます。

ちなみにあとから気づきましたが、AWS の各リソースをインスタンス化する NewFromConfig メソッドを Call するタイミングで、デフォルトで backoff が指定されるので、使うときはよほどのことがない限りは明示的な指定は不要でした。
SQS クライアントをインスタンス化する https://github.com/aws/aws-sdk-go-v2/blob/service/sqs/v1.16.0/service/sqs/api_client.go#L37-L61 で Call されている resolveRetryer(&options) がそれです。この内部で retry.NewStandard() を Call しているので、デフォルトで backoff が 20 sec に設定されます。

まとめ

AWS SDK Go V2  には色々なオプションがあるので、深掘りしてると楽しいですね。

Lambda -> 各種 AWS へのアクセスで権限エラーが発生した場合の調査方法

これは何

Lambda から AWS のリソースに触るときに権限なしエラーが発生したときの調査でいつも手間取るのでその備忘録です。

config ファイルの iamRoleStatements ディレクティブを確認する

大体これで解決します。基本的にはコールしたい API のアクションがあるときには指定したアクションと権限をアタッチしない ARN が指定されていないことが多いのでその指定をします。

例えば SQS に必要な権限については以下のページにまとめられる。

docs.aws.amazon.com

コンソールから割り当てられてる権限を見る

Lambda のコンソールを開いて設定 > アクセス権限から確認しに行きます。
下の画像の部分のリンクです。

f:id:ema_hiro:20220203020148p:plain

まとめ

権限周りでこけるとつらい。

その他

Lambda から SQS の SendMessage の権限追加でハマった話

すでに解決済みではあるんですが、以下のように複数のポリシーをアタッチする際に一向にポリシーが作られない、ということがありました。

  iamRoleStatements:
    - Effect: "Allow"
      Action:
        - "sqs:SendMessage"
      Resource:
        - A
        - B

何度やっても複数設定してると作られなかったので一旦一つだけ作ってみて後から複数設定にしたら無事に作られました。明らかに挙動としては謎挙動だったのでめちゃくちゃハマりました。実際に以下の設定をやってから上記の複数設定したら解決したので、タイミングなのか Severless framework 側の問題っぽさがあります。

  iamRoleStatements:
    - Effect: "Allow"
      Action:
        - "sqs:SendMessage"
      Resource: A

文字列化した時刻を time.Time に parse する

これは何?

https://pkg.go.dev/time#Time.String で Go の時刻を文字列化した値を再度 time.Time に変換する実装について記載してます。

どうやるか?

Time.String() で Go の時刻を文字列として出力した値を time.Time に戻す場合はフォーマットを文字列化された時刻に合わせて調整します。
例えば単純に以下のコードを実行します。

func main() {
    t1 := time.Now().UTC()
    tstr := t1.String()
    fmt.Println(tstr)

    t2, err := time.Parse("2006-01-02 15:04:05", tstr)
    if err != nil {
        panic(err)
    }

    fmt.Println(t2)
}

Output

2009-11-10 23:00:00 +0000 UTC
panic: parsing time "2009-11-10 23:00:00 +0000 UTC": extra text: " +0000 UTC"

goroutine 1 [running]:
main.main()
    /tmp/sandbox1254715714/prog.go:17 +0x1ac

Program exited.

ref: https://go.dev/play/p/1d_2RhWC05l

+0000 UTC が format に含まれていないのでこれをつけて parse します。

func main() {
    t1 := time.Now().UTC()
    tstr := t1.String()
    fmt.Println(tstr)

    t2, err := time.Parse("2006-01-02 15:04:05 +0000 UTC", tstr)
    if err != nil {
        panic(err)
    }

    fmt.Println(t2)
}

ref: https://go.dev/play/p/B6_Bwj8PomV

UTC じゃない場合は timezone 設定も入ってきたります。

まとめ

プロダクションコードで時刻を文字列化してやりとりしていたのですが、ややこしいのでやり取りする時は Unix Timestamp あたりに統一しておけばよかったなと後悔しました。

2021 年の振り返り

Overview

2021 年を振り返ります。
今年は仕事もプライベートも激動の1年(特に後半)でした。

去年の振り返りはこちら。

ema-hiro.hatenablog.com

やったこと

入籍した

入籍しました。お祝いのメッセージいただいた皆様ありがとうございました。

ema-hiro.hatenablog.com

脱東京した

入籍前に同棲をしたタイミングで埼玉に引っ越しました。
多少交通の便は悪くなりましたが、仕事も基本は WFH なので、郊外に引っ越せて固定費も下がって今のところ±プラスって感じです。

ただ、今後家買ったりすることを考えると都内(郊外)に戻る可能性もなきにしもあらずなので、脱東京というより脱23区というのが正しいかもしれません。

MVP 獲った

一生懸命仕事してたら光栄にも年間MVPに選んでいただきました。これからもっと頑張らないといけなくなりました。ポエムも交えてログを残しました。

ema-hiro.hatenablog.com

英語に触れる習慣がついた

mikan とスピークバディというサービスを並行して毎日(少なくとも2日に一回)は英語に触れる習慣をつけることができました。また週一で同僚に英会話のレッスンは引き続きつけてもらってたり、最近は通勤などの移動時間に Podcast で英語のリスニングも加えて少しでも接触回数を増やそうとしてます。
単純接触回数は増やすことができたので、来年はその質を上げてアウトプットできるようにしていきたいです。

家電買い換えた

同棲とは一切関係ないですが、大学時代に一人暮らしを始めてから10年間一緒に過ごした冷蔵庫と洗濯機を買い替えました。家電はQOLに直結すると言われつつも単価が単価だけに、、、とひよっていたのですが、WFH もあって一思いに買い替えて本当に良かったです。

読書継続した

1ヶ月あたり2冊を目標に本を読み続けて積読たちを少し減らしました(減らしたと同時にまた増えたんでプラマイゼロですが)
いいペースなので来年も継続していきたいです。

体重が増えた

筋トレ始める前の標準体重から2.5kg 増量しました。
週一筋トレを始めて2年、ようやく体重に転嫁されるまでになりましたし、体質変わった感覚もある(食べてもあんまり太らない)ので筋トレは本当にやればやっただけ結果が出るのでいいですね。

やらなかったこと

新しい技術要素に触れる

個人で Rust に触れたり Next 触ってたりしましたが、何かプロダクトとしてアウトプットを出す、というレベルでは新しい技術要素に触れることはありませんでした。
来年こそは何かに触れてプロダクトとしてアウトプットを出したいなと思ってます(そんな時間があるかは謎です)

スポーツ観戦

これができなかったのが本当に悔しいです。

1月のルヴァン決勝に外れて以降、サッカーも野球も観にいけませんでした。制限があったのもあり、行ける時にはチケットが取れずというのが続いてしまって結局足が遠ざかってしまいました(それでも何食わぬ顔で来年のベイスターズのファンクラブは更新しました)

本当は東京五輪サッカー日本代表戦初戦の南アフリカ戦に行ける予定だったのですが、試合3日前に無観客が決定してしまい、仕方ないと思いつつ非常に残念でした。生であの久保くんのゴール見たかった...。

新株次第なところではありますが、プロスポーツ全体として制限解除の方向に流れているので来年は月一くらいでスポーツ観戦行きたいです。
とりあえず高校サッカー選手権決勝のチケットが取れたので新国立での初観戦も相まって今から楽しみで仕方ありません。代表戦もチケット争奪戦みたいなことにならないことを祈ってますw

また、社内にベイ党仲間ができたのでサッカーだけでなくハマスタにも定期的にベイスターズエールを飲みに行かなければ、と思ってます。

2022 どうする?

特に決めてません。個人としては一つ目標をクリアしたので、今度はチームだったりプロダクト開発にもっとコミットできるようになりたいなと思います。
あと入籍タイミングとこれからのことで家計がすごいことになった(今後もなる予定がある)ので、家計改善を頑張りたいなと思います。
まず来年はあんまりガジェットを買わないようにしないとです...涙

2021 年買ったもの

Overview

今年もそれなりに課金、散財した結果をまとめます。
WFH で浮いた分のお金は今年もガジェットに消えていきました。

ガジェット

iPad mini 6

各所で絶賛されてますが、僕も絶賛します。今年1番買って良かったものはまず何と言ってもこれでしょう。ユーザーが本当に求めているものは絶対に出さない Apple が本当に欲しいものを出してくれました。本当に大きさが最高です。自宅では LINE とモンストする以外では iPhone を使わなくなりました。
eSIM も対応してるところも素晴らしいです。僕は楽天モバイルで 1GB まで無料のプランをセットで契約して、実質タダで屋外でもインターネットに繋いで iPad を利用してます。

www.apple.com

Anker PowerConf C300

iPad mini 6 がなければ間違いなく2021年 No.1 だったであろうベストバイは Anker が今年出したこの Web カメラです。
最大の推しはコスパです。このスペックで 1万円を切ってるのがすごい。さすが信頼と実績の Anker です。まだ持ってない在宅勤務の方は是非買って欲しいです。

BenQ ScreenBar Halo

BenQ の Screenbar シリーズの最新作です。12月に買いましたがこれも買って良かったものでした。今まではこのシリーズの Plus を使っていたのですが、電源が有線でデスク環境の取り回しが難しい + Halo では間接照明がついたので通常のライトとしての用途が広がる、と考えて一思いに買い換えました。
結果すごく良かったですし、一時売り切れるくらい人気でした(今は在庫復活してます)


cores コレス コーングラインダー

WFH が定着していこう完全にコーヒーが趣味になり、この1年でいろんな器具に課金し続け、ついにグラインダーに手を出してしまいました。他に同価格帯のグラインダーもあったので悩みましたが、Youtube いろんな人の使用動画を研究し、手入れのしやすさを加味して Cores にしました。
WFH 下では一日三杯くらいコーヒー飲むのと、嫁もコーヒー大好き人間でめちゃくちゃ豆の消費が激しいのでこれは買っておいて良かったです。今まで手引きミルを使っていたのですが、挽き目がまちまちだったりやっぱり手間だったりして面倒くさかったので。

平台車(キャスター付き)

空気清浄機があると掃除がしづらい問題を解消するのに買いました。空気清浄機を動かしやすくなるだけで色々お部屋にとって嬉しいことも多いのでこれは地味にいい買い物でした。


AfterShokz Aeropex

カナル型イヤホンを使いすぎると発生しがちな外耳炎防止に買いました。音質こだわらないのであれば、外音を聞きながら MTG できますし、何より耳に入れるタイプのイヤホンとは違って固定の仕方が耳にかけるタイプなので耳のトラブルにも優しいです。骨伝導イヤホンになってから宅配便を取り損ねる、ということは減りました。ただし、メガネに干渉するので若干鬱陶しいです。ちなみにメガネに干渉するのが嫌な時は最新の Airpods3 を使っています。

ロジクール MX Master 3

元々ロジのトラックボールマウスを使っていましたが、Master の方についているスクロールホイールが良すぎて乗り換えました。若干マウスで手首は疲れますが、それを凌駕するスクロールホイールの利便性があります。ただ、トラックボールマウスでスクロールホイールが実装されたら MX Master 3 は使わなくなるかもしれません。

WF-1000XM4

Sony の本気のノイキャンイヤホンとして注目していたので一思いに買いました。正直同価格帯である AirPods pro と比較しても今なら 1000XM4 を買う方が総合的に満足できるとは思います(価格がそんなに変わらないので)
Airpods が搭載以降標準化されている、耳につけると接続される、という機能も踏襲しております。普段使ってるイヤホンはこれ、っていうくらい満足度は高いんですが、これもまた Airpods pro 2 が発売されたら使わなくなるんだろうなーとか思っています。

AirPods 3

今年もユーザーが本当に欲しい iPhone が発表されなかった勢いでカッとなって買いました。一体何個イヤホン買えば気が済むのかわからないです。もはやAppleへのお布施です。 ノイキャンがないので移動時は Sony の 1000XM4 使ってますが、これは在宅勤務だったりリモート会議する時に重宝すると思います。外音聞けるので骨伝導と同じような使い方ができます。カナル型でもないので耳への負担が少ないです。また、音質については AirPods Pro と同等とされていますが、どちらも使った身としては多分こちらの方が音質良くなってる気がします(あくまで体感の話ですが)
カナル型イヤホンが苦手な人にはマストで買うべきかなと思いますが、カナル型 && ノイキャン欲しい場合には今なら他のメーカー買った方が満足度高いと思います。

www.apple.com

RICOH GR III

一眼を持ち歩かなくなり、たいていの写真は iPhone で事足りていたのですが、普段使っている iPhone が SE がカメラが貧弱なのとやっぱりカメラが好きなのでカメラ欲しいなと思ってずっと憧れていた GR3 を買いました。スマホよりカメラで撮る写真の方がやっぱり写真の感じが好みなのと、この大きさだとどこでも持ち出せるのがいいですね。

CalDigit TS3 Plus

デスク周りの配線がごちゃついてきて、全部が全部 Mac やディスプレイで事足りるわけでもない + 安物買ってすぐにダメになるのも嫌だと思ってハイエンドなドッキングステーション買いました。同じ価格帯だと Belkin とも悩みましたが CalDigit は Apple の公式アクセサリーにもなっているので Mac との互換性も気にする必要がないかなと思って買ってます。実際買ってみて役に立ってるか?と言われると出力の高いハブとしての役割は十分に果たしてくれてますが、M1Max を使い始めた時に大丈夫か?という不安は実は残っています。

GROVEMADE Wood Apple Keyboard Tray

憧れていた https://grovemade.com/ さんのデスク用品を買いました。マジで完全に自己満足の買い物ですが、買って良かったです。

grovemade.com

Philips Hue

スマートホーム化のために買いました。自室だけですが、Google Home と連携させて声で電灯を操作したくて購入しました。もっと欲しいですけど今のところまだ1つで足りているのでもう少し家が広くなったら再購入を考えたいなと思っています。

サービス系

スピークバディ

元同僚が転職した先の英会話学習サービスです。これはロボットに話しかけながら英会話を練習するのですが、既存のオンライン英会話レッスンと違って予約要らずで自分のペースで学習を続けることができるところが気に入ってます。しかも喋った会話の文章をかなりの精度で検出してくれるので自分の発音の癖とかも認識できます。
正直英語を頑張ろうと思っててもなかなかこれ!っていう学習方法が見つからずに悩んでいたのですが、これは実際にやってみて続けられているし、そこそこ実践的なのもあって続けられそうです。英会話は mikan、それ以外のリスニング、スピーキング、リーディングはスピークバディという感じでここ一ヶ月くらいは落ち着いてます。ちょっとずつ喋る恥ずかしさは消えてきました。

app.speakbuddy.me

mikan

英語を頑張るためにはまずは語彙力ってことでいくつかアプリありますが、ずっと知ってた mikan を愛用してます。基本的には語彙を増やすためだけに使ってて TOEIC 向けの単語集を一日20個回してます。開始してから1年近くこれを使っててもう生活に馴染んじゃいました。 結局英語学習は色々課金して試しましたけど、mikan とスピークバディに落ち着いてます。あと同僚に週一で英語ランチつけてもらいながら少しずつ触れる時間を増やしてます。

英単語アプリ mikan

英単語アプリ mikan

  • mikan Co.,Ltd.
  • 教育
  • 無料
apps.apple.com

play.google.com

その他

Blue Microphones Yeti

これはチームからの誕生日プレゼントでもらいました。マイクを買っても自分では何がいいのかいまいちわかりませんが、WFH においてマイクを買うのは利他の心の体現です。声は権利なのでもし利他の心があるならマイクは買った方がいいです。声がクリアな方がそうでない人と比べて意見が通りやすい、みたいなことがあるかもしれません(そんなことはない)

今年の MVP を獲った話

Overview

技術的な話ではないですが、現職での1つの節目として2021年の年間 MVP を獲ることができたのでそのことについて少し書こうかなと思って筆を取りました。深夜のノリで走り書きしてますのでそのつもりで読んでください。

前書き

まず、こう言った賞をもらうのはエンジニア人生で初めての経験で、普通に嬉しかったです。というかめっちゃくちゃ嬉しかったです。

自分としても引き続き裁量を与えてもらった中で、去年から試行錯誤して小さく実行してきたアウトプットが評価された形での受賞でした。もちろん、個人だけでなく、チームとしての活動を評価されての受賞でもあり、僕1人の力で取ったわけではなく、関わった人たちに “獲らせて” もらった賞でもあります。一緒に仕事をした人方みんなに本当に感謝しています。

なお、これはキャリアの一つの節目として考えたことを淡々書いている文章です。自慢っぽくならないように注意してますが気分を害された方がいらっしゃった場合はすみません。先に謝っておきます。

何をやってきたのか

プロダクトや業務として具体的に何をやってきたのか?ということについては書けないこともあるので割愛します。しかし去年からどういうことを仕込んで1年間実行してきたのか?ということについては今年の会社のアドベントカレンダーに書いたのでぜひ読んでもらえればと思います。

medium.com

個人的な振り返り

上記のエントリについで個人的な振り返りをしてみると、この2021年というのは コードよりチームとプロダクトについて考えた年だった ということになるのかなと思います。

誤解してほしくないので先に書いておくと、実装に関する時間はあんまり減ってません。なぜなら細々した改善や、気づいたら直してたり、ライブラリのバージョン上げたりと言ったサブタスク系のことはちまちまやっていたからです。ただ、それでもプロダクトの本流の実装という観点から見ると、そこから離れる時間が多かったのは事実です。

今でもコードを書くのは好きですし、できるならコードを書いていたい気持ちはありますが、今年に限って言えば、自分がコードを書く以外のことに注力した方が全体としてスループットが出そうだったのでそちらを選択した年だった、ということになります。

なぜそう思ったのか考えてみると、去年からプロジェクトをリードする役割を担う中で、組織の中で自分がどう振る舞うのが、プロダクトやチームにとって1番いいんだろう?と考えたときに、実装よりも実装に入る前段階にコミットした方が全体としてのスループットが出せそうだし、その “前段階” にコミットすることに対して僕は “ネガティブ” な気持ちにならなかったからだろうと思います。

コードから離れることに抵抗感を感じる人がいるのは事実ですし、僕もそう思ってた1人ではあるんですが、一方でチームを作って実際に実行そのものに責任を持つ役割になってみてわかったのは 機能要件の実装力だけでは解決できない問題がある こと、そしてその問題が機能要件の実装のボトルネックになっていたことでした。ボトルネックを見つけたら改善せずにはいられないのがエンジニアの性だと思うので、結局僕がやっていたのはコードは書かないけど “エンジニアリング” に近いことだったのかなと思います。

例えばコミュニケーションに関するボトルネックを改善する試行錯誤の作業とか結構面白かったです。以下のエントリに記録として残してます。

ema-hiro.hatenablog.com

ちなみに最初にも触れましたが、コードから離れた割にはコードを書く時間は減らず、むしろ Director が PM やってるプロジェクトの実装担当に緊急アサインされて、ゴリゴリコード書いてたり、プロジェクトチーム外で突如発生したタスクを巻き取る形でゴリゴリコード書いてました。

結果、作業時間が2倍になっても単位時間あたりの生産性は個人としてはあまり進歩がなかったので、単純に労働時間が増えました... 。
寝不足で白髪が爆増したのが本当に悲しかったです...。

まぁつらつら書いたのですが、チームやプロダクトのことを考えていたとしてもアプローチの仕方はエンジニアチックだったりしたので、案外コードから離れてもエンジニアリングから離れることはできないんだなーって個人的に気づけたことはいい学びになりました。

余談: 憧れを実現するにはベースが大事

ポエムです。ここに書かれる文章は完全に余談で、ちょっと生意気なことを言ってるかもしれませんが、ここ1, 2年ずーっと考えていたことを文章にしてみます。

今年の僕の役割というのはいわゆる “企画も開発もするエンジニア” とか “プロダクトにも口を出すエンジニア” という類のものになると思います。これは自分がこの世界に飛び込んできたときにある種憧れていた役割そのものであり、こう言った憧れを持ってこの世界に飛び込んでくる人ってたくさんいるんじゃないかと思います。(技術そのものが好きだったり、と言った方もいると思いますのでこれが全てではないことは重々承知してます)

ただ、この役割を担うにあたり、ふと振り返って思ったのは、僕は今年コードにそこまでコミットしなかった、言うなれば技術的な側面については新しい何かを業務の中で学んだということはなく、完全に今までの貯金を切り崩して仕事をしていたことでした。もちろん小さいところで新しく知った知識みたいなことはたくさんあります。しかし、単純に技術的に何か伸ばせた!新しい技術要素をキャッチアップしてプロダクトにぶち込んだ!みたいなことは今年はせず、安全運転&現状維持だったのが 2021 年です。

この技術的には特に伸びを感じなかった1年の中で自分が憧れていた役割をある種全うできたことをふと考えると、自分の中に “CRUDするだけの API であればサクッと作れる”、 “業務レベルであれば Go について1から調べる必要がない程度には習熟してる” というベースのスキルがあったからだと思います。(ここだけ読むと無限にマサカリが飛んできそうな文章書いてますね僕。笑)

別に Go について完全に理解したとも、ややこしい仕様の実装であれば一筋縄でいかないことも重々わかっていますが、ここで言いたいのは ググらなくてもある程度業務レベルで戦うことのできるベースのスキルを持ってることはレバレッジをかける上で保険になる ということです。

エンジニアとして色んな役割をこなしていきたいと思うことは素晴らしいことだと思います。プロダクトにもコミットしたいし、分析して改善できるようになりたいし、アプリケーションからインフラまで理解してコミットしたい、いわゆる “フルスタック” なエンジニアは誰しも一度は憧れると思いますが、時間は有限(1人に割り当てられた1日の時間は24時間しかない)で、その中で全てをこなせるようになる、というのは相当な天才でもない限り難しいことかなと思います。

ただ、それを求められることもあるわけで、そうしたときに有限な時間の中である程度仕事をマルチにこなしていくためには、一定 “ググらずともなんとかなるスキル”、いわば自分でコントロールできる範囲をもっておく、というのが大事になります。スタートアップにおける技術選定で「新しいことよりコントロールできることを優先する」ことと似ていると思っています。

昨日まで実装してた人に急に PjM をやってくれ、なんて言って急にできる人は稀です。急に出る人はよほど準備をしていた人です。大体は知識を入れながら、多かれ少なかれ試行錯誤してある程度ものになる、みたいな状況になるとおもいます。プロジェクトのことをも進めながら技術面では都度ググりながら実装していく、といった高強度なマルチタスクをこなしていくのは相当きついです。何よりスループットが全然上がって来ないと思います。なぜならググるのにも時間がかかるからです。ある程度習熟してるスキルを使うのであれば、ググらずとも実装できますし、仮にわからなくてもコードジャンプ使ってソフトウェア本体のコードを見て解決したりできます。完全にエディタの中で世界が閉じているのでブラウザを開くオーバーヘッドはありません。一回は大したコストではないかもしれませんが、このオーバーヘッドの差のチリツモが、不確実性を相手にする上での大きな差になってきます。

そもそも別の役割をこなすということは、キャリアにレバレッジをかけていく、ということになります。自分で自分にリスクをかけるわけです。リスクを取る時の基本は、失敗しても “死なない" 程度の保険を予めかけておくです。”ググらずともある程度戦えるスキル” というのはこの保険に該当します。何の保証もなくリスクをとる、ということはそもそも事業会社においては極めて稀でしょう。自分ごととして考えてみても、足元が不安定な状況でさらに負荷をかけられたらすぐに倒れてしまう可能性が高くて、チャレンジできないなと思ってしまいます。

僕の周囲のエンジニアは、みんな2, 3領域においてベースとなる(何かあってもコントロールできる)スキル持ってる方がたくさんいます。本当に尊敬します。そう言った人が率先してプロダクトやプロジェクトをリードする役割を担っています。僕も何度その専門性に救われ、そして自分のスキルの無さを嘆いたかわかりません。

そういう経験を経て、自分もある程度ベースとなるスキル(ググらずとも実装できる範囲がある程度ある + 効率的なググり方 or 最速で回答を得られる質問の仕方とその回答を理解する程度の知識)があるからこそ、自分が昔思い描いていたそれっぽい仕事の仕方をできたのかなと思いますし、経験したことのなかった「チーム作り」という業務にチャレンジして、少なくとも死なずに今年を終えることができたのではないかと思います。

"何となる or させる" 力があることは別の領域に越境していくときの必要条件です。そういう意味では、昔から言われてることですが、1つのスキルをある程度深掘りする、というのはこういう時に効いてくるんだな、ということを身をもって体験しました。

ポエムの章はサクッと終わらせるつもりが思考をそのまま吐き出したので、思ったより分量が多くなってしました。

最後に

そういえばこのエントリで “節目” とか書いてますけど、現職を特に辞めるつもりはないです。笑

あとこれは現職でも話したのであえて再掲ですが、こう言った賞を取ることは自分もキャリアの中で1つの目標にしており、受賞できてとても嬉しかったので、これからは自分と一緒に仕事をした人に受賞してもらえるようにサポートしたり、そういった機会を作ってあげたいなと思いました。

Go で BigQuery を叩くときは公式の Nullable 型を利用する

Overview

Go で BigQuery (以下 BQ) のクライアントの実装して返されたクエリの結果を Entity に Unmarshal する方法について調べたのでその備忘録です。

pkg.go.dev

Note

結論から言ってしまうと自前でゴリゴリ型指定して Unmarshal するよりも BQ の package に BQ のデータ型の定義があるのでこれを参考にすると良さそうです。

// Each BigQuery column type corresponds to one or more Go types; a matching struct
// field must be of the correct type. The correspondences are:
//
//   STRING      string
//   BOOL        bool
//   INTEGER     int, int8, int16, int32, int64, uint8, uint16, uint32
//   FLOAT       float32, float64
//   BYTES       []byte
//   TIMESTAMP   time.Time
//   DATE        civil.Date
//   TIME        civil.Time
//   DATETIME    civil.DateTime
//
// A repeated field corresponds to a slice or array of the element type. A STRUCT
// type (RECORD or nested schema) corresponds to a nested struct or struct pointer.
// All calls to Next on the same iterator must use the same struct type.
//
// It is an error to attempt to read a BigQuery NULL value into a struct field,
// unless the field is of type []byte or is one of the special Null types: NullInt64,
// NullFloat64, NullBool, NullString, NullTimestamp, NullDate, NullTime or
// NullDateTime. You can also use a *[]Value or *map[string]Value to read from a
// table with NULLs.

ref: https://github.com/googleapis/google-cloud-go/blob/v0.46.3/bigquery/iterator.go#L83-L104

特に以下の部分

It is an error to attempt to read a BigQuery NULL value into a struct field, unless the field is of type byte or is one of the special Null types: NullInt64, NullFloat64, NullBool, NullString, NullTimestamp, NullDate, NullTime or NullDateTime. You can also use a Value or map[string]Value to read from a table with NULLs.

カラムによっては NULL のケースもあると思うので以下のように BQ での NULL を安全に処理する型が用意されているのは助かります。

type NullInt64 struct {
    Int64 int64
    Valid bool // Valid is true if Int64 is not NULL.
}

https://github.com/googleapis/google-cloud-go/blob/5a2ed6b2cd1c304e0f59daa29959863bff9b5c29/bigquery/nulls.go#L40-L43

最初自前でEntity の定義をして時刻型や INTERGER あたりでデバックしながら進めてたんですが、カラム数が多いとカラムによってあったりなかったりする(= Null の扱い) のが面倒だったりそこそこなデータ量のクエリ叩くのでお金の不安もあり、ミスるとやり直してたりして辛かったのですが、GoDoc 眺めに行ったら一発で解決しました。

Go に Contribute する

f:id:ema_hiro:20211206155511p:plain

Overview

これは 2021年Goアドベントカレンダー 7日目の記事です。
つい最近だいぶ久しぶりに Go 本体に Commit する機会があったので、復習も兼ねてまとめてみました。
OSS に Commit するのは慣れないとすごい敷居が高く感じます(僕もそうでした)が、Go は一度覚えると2回目以降は難なく Commit できる仕組みが整ってているソフトウェアだなと思っているのでこのエントリで少しでも Go にコミットする人が増えればとても嬉しいです。

なお今だと GitHub からも Commit できることがありますが、今回は Contribution Guide に則った方法を記載してます。公式の https://go.dev/doc/contribute#sending_a_change_gerrit に記載されてる内容に沿ってます。

準備

この準備が(多分)一番ハードルが高いと思いますが、 Go は Contribution に関する情報が全て以下のサイトにまとまっており、基本的にはこの通りに準備をすれば Go への Contribution はできるようになります。

go.dev

簡単に順を追って説明します(僕が初めてコントリビュートした時のものをそのままトレースしてます)

CLA の同意とアカウント作成

まず Contributor License Agreements に同意してアカウントの登録をします。以下のような画面が出ると思うのでとりあえず On Yourself で登録して先に進めます。

f:id:ema_hiro:20211206044335p:plain

CLA 同意後(Google アカウント認証後) に https://go.googlesource.com (Gerrit) でもアカウントが作られていると思います。

次に googlesource.com のパスワードを設定します。https://go.googlesource.com の画面で Generate Password を押下します。

f:id:ema_hiro:20211206044416p:plain

パスワードの設定のために Google 認証を進めると、専用のパスワード生成コマンドが出力されます。
これを実行すると .gitcookies というファイルが生成されて git codereview change (後述するレビュー用の CLI ツール) するときに自動で誰が commit したのかを Gerrit 上で確認できるようになります。

ここまででコミットするためのアカウント準備およびコミットする準備の第一段階は終わりです。
このアカウント作成のフローが終わると Go の Review Dashboard にアクセスできるようになっているはずなのでアクセスしてみてください。
https://go-review.googlesource.com/q/status:open+-is:wip が今 CL(*) が上がってる一覧で、実際に開発されてる Go の機能を一覧で見ることができます。

レビューツールを入れる

Go のコントリビュートには専用のツールを使います。
ref: git-codereview

使い方ですが、まず、Contribute Guide に従って go install してきます。
インストールすると git codereview というエイリアスが使えるようになります。

GitHub で使うエイリアスとはちょっと異なりますが、おおむね以下のような対応表です。

git codereview changes = git commit
git codereview mail = git push

より詳細については git codereview help を参照してください。

早速コミットしてみる

https://go.googlesource.com/ から Go の本体を落としてきます。

# Go 本体
git clone https://go.googlesource.com/go

# tools
git clone https://go.googlesource.com/tools

Go 本体に移動してチェックアウト & Add します。ちなみにチェックアウトしたブランチ名が使われるわけではないので実際チェックアウトするかどうかは任意です。自分はわかりやすいのでチェックアウトしておくことが多いですが。
また、地味に大事なことなのですが、チェックアウトした先でコミットログを記載するときは どのパッケージに対して変更を行うのか?ということがわかるように pacakge 名を prefix で追加 します。これ怠るとレビューで Commit コメントについて指摘を貰います。こういうところまでかっちりみられるのも OSS っぽいですね。

cd PATH/TO/go.googlesource.com
git checkout

# 変更

git add -u
git codereview change 

internal/xxx: hogehoge

- TODO

git codereview mail
# これで差分が Gerrit に反映されます。
# Gerrit では GitHub のように commit message が履歴としてずらずら残っていくことがありませんので最初のコミットを書いたらそれに change を加えていく形になります。

ref: https://go.dev/doc/contribute#sending_a_change_gerrit

codereview mail コマンド実行後に上記の Dashboard を見にいくと自分の作った差分(Gerrit では ChangeList* ) が作成されているのが確認できます。
あとは自動的に bot が走ってコアメンバーの方のレビューを受けることができます。ただ、実際にアサインが確定するまでには10分程度ラグがあるのと時差があるので初回のレビューが日本時間の深夜 or 早朝、みたいなことは普通にあるので気長に In Review になるのを待ちましょう。

※ 略して CL という呼称も使われます。

余談ですが、レビューを受けてるとコメントでコミュニケーション取る機会があるのですが、コメント部分にただ記載しても Draft のままなのでちゃんと Reply するという明示的なボタンを押下しないとずっと、下書きのまま残されて続けて、差分を入れたのにレビューがされない、ということが起こります。僕はよく忘れて2,3 日後に気づくことがあります。

まとめ

もともと別で公開しようと思っていたエントリでしたが、いい機会なのでアドベンドカレンダーで公開しました。
Go 本体に Contribute できる人が増えてほしい、という思いから Go 本体に Commit する準備やら実際のレビューの進み方やらをまとめてます。コミットに関する詳細は Contribution Guide に全て書いてあるのでそちらを参照してもらうのが確実かと思いますが、やってみたいけどやり方わからない、という方のために本当に最初の最初のところ(アカウント作成周りとか)だけ簡単に説明させてもらいました。

実際にレビューを受けると Commit メッセージレベルまでレビューを受けることもあります。
本当は普段の開発からそういったところまで意識するべきとも思いますが、なかなか意識が回らないのも事実で、受けたら受けたで新鮮な気分になります。
何よりちゃんと英語勉強しよう、というモチベーションにもなります。
そして初めて Contribute した場合には Contribute に感謝するメッセージを貰えてモチベーションにも繋がりますので、是非受け取ってみてください。

僕も普段 OSS に積極的にコミットするタイプのエンジニアではありませんが、こういう OSS へのコミット方法を教えてもらえる機会がなかったのでまとめてみました。
せっかくの冬休み期間ですし普段お世話になっている恩返しついでにソフトウェアの歴史に名を残す作業をしてみてはどうでしょうか?

おしまい。

余談

VSCode で Go の最新版を開発する (Go1.18 の場合)

Go で開発する時に editor 側で使用する Go のバージョンを指定したり、自動的に指定されたりすると思いますが、ソフトウェア本体の開発をする、という場合にはまだリリースされていないバージョンを使用するのが当たり前になります。
Go は比較的簡単に開発環境をセットアップできる言語だと思っていますが、今回 Go 本体を開発する際にこの 開発されている最新版 の Go を VSCode に設定することが必要で、これで少しハマったので備忘録として残しておきます。

VSCode のセットアップ方法

gotip を使う方法も考えたのですが、うまくいかなったので現状以下のように Go の開発最新版(master ブランチ)を取得してビルド => VSCode で使用するワークスペースを設定後、ワークスペース設定で GOPATH を取得してきた本体に合わせる、という手法をとっています。

# Go 本体の時
{
    "go.goroot": "PATHTO/go.googlesource.com/go",
    "gopls": {
        "build.experimentalWorkspaceModule": true
    }
}

# Tools のとき
{
    "go.goroot": "~/src/go.googlesource.com/go",
    "gopls": {
        "build.experimentalWorkspaceModule": false
    }
}

ビルドの仕方は以下の Intall Go に記載されています。

go.dev

※ The Gopher image in this entry is created by emahiro via https://gopherize.me/. The Gopher character is based on the Go mascot designed by Renée French.

入籍しました

全然技術ネタじゃないけどご報告です。

12/4 に入籍しました。
いわゆる Pairs 婚ってやつです。
まさか自分の人生変えてもらうなんて夢にも思ってなかったので、Pairs は本当にすごいサービスだなと今更ながら実感しました。

そして実は2ヶ月前くらいから脱東京して埼玉に住んでます。
現職が基本的には週1程度の出社推奨で、このご時世もあって昔ほど飲み会などもなくなり、仲良い友達もだんだん家庭を持ってそんなに夜外に出てくるわけでもなく、そしてなによりパートナーの仕事の都合で都内に住み続ける理由がなくなったので一思いに引っ越しました。
なお、引っ越して通勤時間はドアtoドアで倍くらいになったけど、出社時刻は縛られていなので気分転換&コーヒー目当てに週1程度は出社してます。なので脱東京後もここ1年の生活と実はあんまり変わっていません。

脱東京をした結果良かったことと言えば、家が 1.5倍くらい広くなったけど固定費(駐車場込)は落ちました。郊外の安さ(というか23区の家賃の高さ)に驚かされます。
仮に近い将来家を買うにしても東京の西側〜埼玉あたりで考えることにはなって、多分23区内にはもう戻らないんじゃないかなと思います。
なお、生活上車がほぼ必須な状況になったので、車欲爆発して SUVYoutube 漁りが日課になってます。手元にキャッシュさえあればすぐにでも買いたい...。

余談ですが、休み多すぎるで有名な現職では、結婚すると結婚記念日と自分以外の家族の人数分有給もらえる福利厚生があるので、結婚と同時に有給が2日増えました笑。
詳しくはこちら -> Benefits & Perks

雑な報告で特にほしい物リスト等は用意してませんが、今後ともよろしくお願いします。

12.6 emahiro

TypeScript で集合関係にある型の値の再代入をする

Overview

以下のような集合関係にある2つの型の間で値の再代入をしたいケースでハマってしまい、同僚のフロントエンドエンジニアに手伝ってもらって一定の方針で解決したのでその備忘録です。

type ObjA = {
    a: number;
    b: string;
}

type ObjB = {
    a: number;
    b: string;
    c: string;
}

この2つの関係はフィールドが集合関係にあり ObjB は ObjA として振る舞うこともできます。

調べたこと

存在しないフィールドに対して never 型にアサインしようとして型エラーになるケースがある

以下のような実装では値の再代入時に以下のエラーが発生します。

Type 'string | number' is not assignable to type 'never'.
  Type 'string' is not assignable to type 'never'.(

https://www.typescriptlang.org/play?ts=3.9.7&ssl=15&ssc=1&pln=16&pc=1#code/C4TwDgpgBA8gRgKwIJQLxQN4Cgq6gQwC4oA7AVwFs4IAnAbhzzmIGdgaBLEgcwYF8sWUJFiIAQmkyNcRUpWr1pUZlDaceDPFADGrdl15YBWbQHsSbAsXjJJGIgEYANMsIAiAExu+DMxeCuoggS6PaEHi7MbgDMbi66bgAs3gxYABQ2ENrAAHQA1hAgLGn4AJQELFBINDT4IAA8BSCmAGZBSAB8pTktpjQAovjaABZpUHloHVJaHG1pE1zK5dhaWvgA2nkAupJwm1uaeAJ8pal+LKYANhA5l6bcaQAG+FAAbviXZNAclQAk9jl8HxHqcgA

以下の実装だとエラーは発生しません。これはどうしてえらーが起きないのか現在調査中です。

https://www.typescriptlang.org/play?ts=3.9.7#code/C4TwDgpgBA8gRgKwIJQLxQN4Cgq6gQwC4oA7AVwFs4IAnAbhzzmPKtoYF8stRJZEAQmkyNcRUpWr1RUZhLbS8UAMbEAzsBoBLEgHNO3ZQHsSGgsXjJhGIgEYANLMIAmDg2OngT-giHobLo7MAMyOqgBEACzhbtwAFJYQysAAdADWECBqcfgAlARqUEg0NPggADwZIEYAZj5IAHy5KTVGNACi+MoAFnFQaWgNIkpadXEDOrL52EpK+ADaaQC6wnCLSwxKXBy5DFgeakYANhApR0a6cQAG+FAAbvhHZNBahQAkNin4HFe7QA

ObjA と ObjB の構造の違いですが、前者のケースでは最初から key の方が number | string になります。

解決策

1 - オブジェクトにアクセスするための field を用意する

アクセス用の [key: string] ... の フィールドを埋め込むとエラーを解消できます(= field を指定して異なる方でも値を代入できます)

type ObjA = {
    a: number;
    b: string;

    [key: string]: string|number;
}

type ObjB = {
    a: number;
    b: string;
    c: string;
    
    [key:string]: string|number;
}

ref: https://www.typescriptlang.org/play?ts=3.9.7#code/C4TwDgpgBAGlC8UDeAoK6oEMBcUB2ArgLYBGEATmhiboaRVegNoDWuAzsOQJZ4DmAXVrEy5AD6ce-FAF8UoSFACaCZIywcuvPuppRJ29a01TBw+uIPS5KAMYB7PJygAPXHESoMGqAEYANLq4AbIodo7OILgqnuo4UABEvgmB3npJKaHhTvYANhAAdLn2fAAUCWQAZvbkECmuAJQA3GGlAPIkAFYQtsAFLBAg7KUuDVjsUACC5OSYIAA8AyD2lbAAfA0F1eQAopi2ABalLAhrat4urAKqIFctMs1AA

ただし Runtime での型を確定するためには typeof で再代入先のフィールドと型が同じかを確認しないといけないです。

以下のような number 型なのに型が変更されて値が再代入されてしまい、型の意味がなくなります。

https://www.typescriptlang.org/play?ts=3.9.7#code/C4TwDgpgBAGlC8UDeAoK6oEMBcUB2ArgLYBGEATmhiboaRVegNoDWuAzsOQJZ4DmAXVrEy5AD6ce-FAF8UoSFACaCZIywcuvPuppRJ29a01TBw+uIPS5KAMYB7PJygAPXHESoMGqAEYANLq4AbIodo7OILgqnuo4UABEvgmB3npJKaHhTvYANhAAdLn2fAAUCWQAZvbkECmuAJQA3GGlAPIkAFYQtsAFLBAg7KUuDVjsUACC5OSYIAA8AyD2lbAAfA0F1eQAopi2ABalLAhrat4urAKqIFctMs1hDjn5RSXlmJXAFPWjQA

2 - 再代入する時に一度展開してから field に対応する値を再度代入する。

以下のように値に再代入する前に一度展開して全てのフィールドを埋め、フィールドの型を確定した状態で代入します。

type X = {
    a: number
    b: string
}
type Y = {
    a: number
    b: string
    c: string
}

let x: X = {
    a: 1,
    b: "1",
}

const y: Y = {
    a: 2,
    b: "2",
    c: "3"
}

(Object.keys(x) as Array<keyof X>).forEach(k => {
    x = {
        ...x,
        [k]: y[k]
    }
});

https://www.typescriptlang.org/play?ts=3.9.7&ssl=10&ssc=1&pln=11&pc=13#code/C4TwDgpgBAGlC8UDeAoK6oEMBcUB2ArgLYBGEATmhibgM7DkCWeA5igL4qiRQCaCyKuhz5iZShig0o9JqyFQAxnQbM2nFABsIwKAA9ccRKkkiAjABoF0gERmbVjYoD2eelBC5+xhSIBMVpK2fg4KylA2AMw2HCgoLm7O2gB0ms4sABQ2ZABmzuQQDvoAlADccRkA8iQAVhCKwMkA1hAgtBl6xVi0UACC5OSYIAA8LSDOObAAfMXJeeQAopiKABYZTQhTgpJ6AiaSGMlHeoEH6ADaTQC6uCCXVwqc7GVxCbRJEKnpWZg5wBRFTpAA

この手法では大元の定義されている型の中であえて型情報を削ってしまう accessor を実装することなく局所的に解決します。先に値を全てスプレッド構文で埋めてしまい、どのフィールドがどの型なのかの情報を入れているので集合関係にあるフィールドの値を入れ込むことができます。

ただしこの場合でも上記のような型情報が失われて別の Type の値を埋め込んでしまうことができるという意図しない挙動が発生するので、このケースでも typeof での型チェックは必要です。

またこの方法は for で再代入するごとに展開して入れ直す = 新しくオブジェクトを alloc して更新したいオブジェクトに入れ直すことになるので、メモリを無駄に消費している実装になりますのでこのことに留意する必要はあります。

考察

ある Map において任意の property の型が不明な場合、同じような構造を持つ(or 集合関係にある構造をもつ)型の値で埋めようとしても、別の型として認識されてしまい、例え代入先の property の型が一致するような場合でも割り当て不可のエラーが発生する模様です。

Google で検索しても割り当てする側の型の情報だけでは割り当てできない、というエラーがいくつか出てきます。

まとめ

TypeScript とても難しい。

単なる備忘録であり半分メモなので何かわかったらまた追記します。

追記

20211115

こんな感じで assignable で値を入れ替えるとよさそう、と教えてもらいました。
https://www.typescriptlang.org/play?target=99&ts=3.9.7&ssl=31&ssc=1&pln=32&pc=1#code/C4TwDgpgBA8gRgKwIJQLxQN4Cgq6gQwC4oA7AVwFs4IAnAbhzzmIGdgaBLEgcwYF8sWUJFiIAQmkyNcRUpWr1pUZlDaceDPFADGrdl15YBWADYRgBYvGSSMlqAEYANMuIAiAExuofBmYsq1hLodrIeLipuAMxuLrpQbgAs3r6CAPRpUAAU1hDawAB0ANYQICxZ+ACUBCxQSDQ0+CAAPCUgAPYAZqLIAHyVBZ3tNACi+NoAFllQRWi9UhlaUBzdWbNcytXYi0t4+ADaRQC6knCHRww7eAKLfJUMWNrtJGxQZGAAJvjAEADqHMAJkgWCwONwSPg4GZJM0lAAVKAQAAePxIH1qACU8sMPs01AYXOQqLQoAAfVT6Hi9JxKACqiJREDRmOxNFx+J4hPkJPJHO4vSwvSySmA+Bo3HMxDhNK0LHaZBo2ggxFpWEqUrmUi0NHMCpIPTyhU6NHaFBGJH0EHKSi0uXyBSZlvKovF5gGFHwYCyWUOpRcADd8CYyBAjtVUPNsLtditsm1lvq5QqlVAAGSpqDCCBdKCB4PQVCFzPgbPdJOKiC+kBhrXRus64B6qBVlzlpVVo4XG3RgR17W6mj6lu5oMhrt1u5Kar4Wpw-gPfCSd5fH7-QHA0HgyFmCoRe6CR7POVmAomdrcLIAA0XeZDy1qABIMPgCvg+Jf7kA

メソッドを持つ参照型をフィールドに持つ Struct の初期化後のアクセスについて

Overview

メソッドを持つ参照型をフィールドを持つカスタム struct を初期化したときの参照型が持つ(見かけ上ネストしてる)メソッドにアクセスした時の振る舞いがたまたま職場の雑談で上がり、挙動について調べてみたのその備忘録。

Sample

type Y struct{}

func (*Y) Hello() {
    fmt.Println("Hello")
}

type X struct {
    y *Y
}

X は Hello というメソッドを持つ Y の参照型をフィールドに持つカスタム struct です。この Y を明示的に初期化せず X を初期化したときに Hello にアクセスするケースを考えます。

func NewX() *X {
    return &X{}
}

func main() {
    x := NewX()
    x.y.Hello()

}

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

このとき Y は明示的に初期化されていない (= &Y{} みたいなケース)ので、 X の持つ y property には nil が入りますが、Hello メソッドは問題なく Call できて Hello という文字列が出力されます。
Go をある程度書き慣れていると、y にアクセスした時点で nilpo で panic が発生しそうなコードに思えますが、これで panic が発生することはありません。
これは振る舞いとしてはX が初期化されたタイミングで、Y は参照型なのでその実体は nil であるもの Hello メソッドは Y の参照型に対する メソッドなので nil でも呼べる、というものになります。
たとえフィールド自体が nil でも Y の参照型として実体 nil なのであって、参照型が持っているメソッド自体は nilpo にならずに Call できる、というものです。書き慣れてきたからこそ分かりづらい仕様です。

参考までに以下のような nil の中身に触るコードは nilpo が発生します。

package main

import (
    "fmt"
)

type Name struct{}

type Y struct {
    nameStruct *Name
}

func (y *Y) getNameStruct() *Name {
    return y.nameStruct
}

func (y *Y) Hello() {
    fmt.Printf("%v\n", y.getNameStruct())
}

type X struct {
    y *Y
}

func NewX() *X {
    return &X{}
}

func main() {
    x := NewX()
    x.y.Hello()

}

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

これは nil である変数の中身に触っているからです。上記のような挙動を確認するためには以下のようなコードで nilpo が起きないことでさらに確認ができます。

package main

import (
    "fmt"
)

type Name struct{}

type Y struct {
    nameStruct *Name
}

func (y *Y) getNameStruct() *Name {
    return &Name{}
}

func (y *Y) Hello() {
    fmt.Printf("%v\n", y.getNameStruct())
}

type X struct {
    y *Y
}

func NewX() *X {
    return &X{}
}

func main() {
    x := NewX()
    fmt.Printf("%#v\n", x)
    x.y.Hello()

}

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

このケースでは getNameStruct メソッドの中で y の実体(nil) に触ってないので nilpo は発生しません。
ちなみにアドレスを確認しても nil を挿してます。 ref: https://play.golang.org/p/d1HE52yuavk

まとめ

たまに Go だとこの振る舞いどうだっけー?っていうのを調べると面白いですが、今回紹介したようなコードは Go に慣れると混乱しかねない仕様だなと思ったので、struct を初期化するときには明示的に nil や参照型の struct を指定してあげると良いのではないかなと思いました。nil でも呼べるけど意図しない nil だった、みたいなことがないように、という意図です。

func NewX() *X {
    return &X{
        y: nil,
    }
}

// or 

func NewX() *X {
    return &X{
        y: &Y{},
    }
}

Docker Compose v2 以降環境変数にインラインコマンドが使えないっぽい

Overview

タイトルの通りです。

direnv などを使ってディレクトリごとに環境変数を差し変えることはよくある local の環境構築の手法だと思います。
そして、その環境変数に対してインラインコメントで環境変数を動的に差し替えたいケースなどもよくあると思います。そのために .env ファイルに以下のようなインラインコマンドを書くケースもあると思います。

ENV_NAME=\`some command...\`

このインラインコマンドで動的に取り出した値を環境変数に追加場合、Docker Compose V2 以降だとインラインコマンドが解釈されずに invalid character エラーが発生してしまいます。

Docker Compose はそもそもインラインコマンドを想定していない?

https://docs.docker.com/compose/env-file/#syntax-rules の公式に以下のように書いてありました。

Compose expects each line in an env file to be in VAR=VAL format.

まとめ

もともとインラインコマンドに対応していなかったっぽい。バグかと思ったけどこれは V1 のバグというか曖昧な書式でも受け付けてくれる仕様に助けられているだけでした。
(一瞬調べてくれた同僚にまじで感謝)

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

Overview

Deprecated

Deprecation code: NESTED_CUSTOM_CONFIGURATION_PATH

Note: Applies only to eventual programmatic usage of the Framework

Service configuration in all cases should be put at root folder of a service. All paths in this configuration are resolved against service directory, and it's also the case if configuration is nested in sub directory.

To avoid confusing behavior starting with v3.0.0 Framework will no longer permit to rely on configurations placed in sub directories

ref https://www.serverless.com/framework/docs/deprecations

Serverless Framework V3 で sub directory の configuration ファイルに依存することは非推奨になってデプロイ時に警告が出るようになりました。
これにより、このモノレポの構成が使えるのは V2 までになります。なので今後この構成を選択することできなくなります。
今後は ディレクトリの TOP に配置した serverless.yaml のみに依存し、sub directory に配置していた FaaS たちは Handler で直指定する(設定はすべてサービスで共通)形になりそうです。V3 でも環境変数の値を FaaS ごとに変更するケースがほしい場合があると思うので、別途調べてエントリにしようと思います。


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 単体を指定する と同様のことが 型上許容される という挙動をするようです。

まとめ

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