emahiro/b.log

Drastically Repeat Yourself !!!!

社会人10年目が終わった

社会人 10 年目が終わって、11年目になった。

2014年から社会に出たときには、先のことなんか何も考えられてなかったけど気づいたら10年が過ぎてしまった。

10年目まではちゃんと数えておくか、と思っていたのもあり、来年以降は「社会人N年目」みたいなカウントはもうしなくなるとは思う。

特に社歴や社会人歴が意味を持つとは考えていないけれど、それでも10年は大きいな〜と思うともに「10年間なんやかんや体も壊さずに生き延びたのか〜」と思うとちょっと感慨深いものもあったりする。

30中盤なんて世間一般ではまだまだ若手だと思うけど、この業界では若手とは見られないし、見られ方も変わってくるのでそういう意味では自分の見方も見られ方も変わっているということは意識しないと行けない。

10年で公私とも置かれてる状況は10年前に想像していたものとは全然違うし予想もしてなかった変化を感じるので、次の10年ものんびりと暮らしていこうかなと思う。

VSCode で delve を使った Debugger の環境を整える

Overview

Go + Delve の利用環境を VSCode について記載します。

動機

普段の開発ではずっと古き良き? print debug を使って動作確認を行ってきていました。 もちろん手元の開発だけでなく remote での動作確認等では今でもこれよりシンプルで一般的な手法はないので、今でもバリバリ現役で使っているのですが、こと手元(local) 環境の開発に限ってはもうちょっとイマドキっぽいデバック環境を整えようかなと思って Go + delve の開発環境を Go で用意しました。

設定方法

多くのエントリで書かれてるとおりですが手順は以下のとおりです。

  1. VSCode で Go の拡張をいれる。
  2. VSCode でプロジェクトを開く: VSCode で Go 言語のプロジェクトを開く。
  3. デバッグ設定ファイルを開き .vscode ディレクトリ内の launch.json ファイルを開く。もし launch.json ファイルが存在しない場合は、デバッグサイドバーから「デバッグの構成を開く」(歯車アイコン) をクリックし、Go を選択して新しい launch.json ファイルを作成する。
  4. 環境変数を設定する: launch.json ファイルに env フィールドを追加し、そこに環境変数をキーと値のペアで設定する。
  5. BreakPoint を仕込む。
  6. TestXXX メソッドで debug test でデバッカーを起動する。

以下のように debugger が起動します。

例えば、特定の API エンドポイントや認証トークンなど、デバッグセッション中に必要な環境変数がある場合、以下のように追加します (*)

※ 後述しますがこの方法だとうまく環境変数が反映されていませんでした。

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Launch",
            "type": "go",
            "request": "launch",
            "program": "${fileDirname}",
            "env": {
                "API_ENDPOINT": "https://example.com/api",
                "AUTH_TOKEN": "your_auth_token_here"
            }
        }
    ]
}

設定方法の中で request に launch と attach がありますが、公式には以下のように説明されています。

In VS Code, there are two core debugging modes, Launch and Attach, which handle two different workflows and segments of developers. Depending on your workflow, it can be confusing to know what type of configuration is appropriate for your project.
If you come from a browser Developer Tools background, you might not be used to "launching from your tool," since your browser instance is already open. When you open DevTools, you are simply attaching DevTools to your open browser tab. On the other hand, if you come from a server or desktop background, it's quite normal to have your editor launch your process for you, and your editor automatically attaches its debugger to the newly launched process.
The best way to explain the difference between launch and attach is to think of a launch configuration as a recipe for how to start your app in debug mode before VS Code attaches to it, while an attach configuration is a recipe for how to connect VS Code's debugger to an app or process that's already running.
VS Code debuggers typically support launching a program in debug mode or attaching to an already running program in debug mode. Depending on the request (attach or launch), different attributes are required, and VS Code's launch.json validation and suggestions should help with that.

ref: https://code.visualstudio.com/Docs/editor/debugging#_launch-versus-attach-configurations

debugger を動かすだけであればどちらでも問題なかったです。attach にするとすでに動いてるプロセスに debugger をアタッチして delve を動かせます。attach だと環境変数を設定できないなど、若干の使える設定が異なります。

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Launch Package",
      "type": "go",
      "request": "launch",
      "mode": "auto",
      "program": "${fileDirname}",
      "env": {
        "ENV_VAR": "test"
      }
    },
    {
      "name": "Attach to Process",
      "type": "go",
      "request": "attach", // env の設定を使えない。
      "mode": "local",
      "processId": 0
    }
  ]
}

その他各種設定の項目は以下の公式のドキュメントを参照します。env くらいしか使うものないですが、env が多い場合には envFile を load できたり便利です。

code.visualstudio.com

ハマってるところ

launch.json で設定した環境変数が反映されない or 上書きされてしまう

これは自分の動かしていた環境に依存するかもしれませんが、以下のように、環境変数をあらかじめ env で設定した上で debugger を起動しても環境変数を Go のプロセスから取り出せませんでした。
今はこの env で設定した環境変数がどうして反映されないのか?ということはまた調べます。

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Launch Package",
      "type": "go",
      "request": "launch",
      "mode": "auto",
      "program": "${fileDirname}",
      "env": {
        "ENV_VAR": "test"
      }
    }
  ]
}

Go のコードは以下

// main.go
func DoSomething() {
    v := os.Getenv("ENV_VAR") //  v = "" になってしまう。
    fmt.Println(v)
}


// main_test.go
func TestDoSomething(t *testing.T) {
    DoSomething()
}

追記

環境変数が登録できた

gopls の設定で以下の設定を追加すると環境変数が反映されました。

{
  "go.testEnvVars": {
    "ENV_VAR": "value"
  }
}

user 設定の setting.json にこれを追加してもいいですが、私は workspace の setting.json に追記しました。そのほうが取り回しやすいので。

公式のドキュメントには優先順位には Merged with env and go.toolsEnvVars with precedence env > envFile > go.toolsEnvVars. と書いてあるけど、やってみると env と envFile は反映されず、gopls が優先されていました。

Absolute path to a file containing environment variable definitions, formatted as string key=value pairs. Multiple files can be specified by provided an array of absolute paths. Merged with env and go.toolsEnvVars with precedence env > envFile > go.toolsEnvVars.

ref: https://github.com/golang/vscode-go/wiki/debugging#configuration

Amazon Pinpoint で S3 上のファイルからセグメントを作成する

Overview

Amazon Pinpoint という AWS が提供してるマネージドCRM ツールを使ってセグメント(配信対象)を S3 上のファイルから作成する手順について記載します。

用語の整理

  • セグメント
    • Amazon Pinpoint 上の用語。配信対象を指します。
    • APNS であればデバイストークン、SMS であればメールアドレス、というようにマッピングさせることでモバイル端末に通知を送ることができます。

実装方法

ざっくり S3 にファイル(csv) をアップロードして、そのファイルを元に Pinpoint のセグメント(endpoint) を作成するという流れを考えます。

import の定義は以下に詳細に記載されています。

docs.aws.amazon.com

具体的な実装に関しては以下のようなシーケンスになります。passRole されてる AWS の内部処理については多分こうしてるんじゃないか?という予想です。

import するファイルのサイズが小さい場合、CreateImportJob API の完了後に作成された SegmentID が返ってくることがありますが、大きい場合 Pinpoint 側でも Batch Job が動いていて都度ステータスを確認して Segment の作成確認を行う必要があるので、CreateImportJob を実行したあとに GetImportJob で Segment の作成状況を確認する必要があります。

なお作成状況のステータスについては以下のドキュメントに記載されています。

docs.aws.amazon.com

sequenceDiagram
    participant app
    participant sts
    participant pp as pinpoint
    participant s3
    
app ->> s3: upload csv
s3 -->> app: url
app ->> sts: assumeRole(pinpoint-role)
sts -->> app: cred
app -->> pp: create import job with with cred & s3URL
alt AWS 内部
    pp ->> sts: passRole to import segment role from s3
    sts -->> pp: role
end
pp ->> s3: get file
s3 -->> pp: data
pp ->> pp: import job
pp -->> app: job status
loop if status != completed 
    app ->> pp: get import job status
    pp -->> app: status and segment id
end

利用する API

ファイル形式

今回はCSV を利用します。 セグメントを作成するための CSV のフォーマットは決まっていて ChannelTypeAddress が必須です。

  • ChannelType は APNS や GCM、SMS などになります。
  • Address は Push であればデバイストークン、メールとなればemail アドレスになります。

ハマったところ

Pinpoint -> S3 に触る role を設定する

先結論だけ書いておくと、Amazon Pinpoint に今回説明しているような外部のファイル(今回は S3上)を読み込ませてセグメント(endpoint) を作成する際は 専用のロールが必要になります。詳細は以下の公式ドキュメントに書いてあります。

docs.aws.amazon.com

なお、上記サンプルで書かれてるCLI のPayload だとうまく動かず、以下のようにしました(JSON のテンプレが古そう?)

# 以下の JSON を用意する。
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": "sts:AssumeRole",
      "Effect": "Allow",
      "Principal": {
        "Service": "pinpoint.amazonaws.com"
      }
    }
  ]
}

aws iam create-role --role-name PinpointSegmentImport --assume-role-policy-document file://PinpointImportTrustPolicy.json

# response で以下の結果が返ってきます。
{
    "Role": {
        "Path": "/",
        "RoleName": "PinpointSegmentImport",
        "RoleId": "$RoleID",
        "Arn": "arn:aws:iam::$AppName:role/PinpointSegmentImport",
        "CreateDate": "2024-01-29T15:37:51+00:00",
        "AssumeRolePolicyDocument": {
            "Version": "2012-10-17",
            "Statement": [
                {
                    "Action": "sts:AssumeRole",
                    "Effect": "Allow",
                    "Principal": {
                        "Service": "pinpoint.amazonaws.com"
                    }
                }
            ]
        }
    }
}

# 作った role に AmazonS3ReadOnlyAccess というポリシーをアタッチします。
aws iam attach-role-policy --policy-arn arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess --role-name PinpointSegmentImport

※ これが最初わからず、以下の記事で AWS の assumeRole から調べ始めました。

dev.classmethod.jp

今回は本業において、とあるアカウントAから別のアカウントBに用意された Pinpoint のリソースを触る、という特殊な方法をベースに考えることになったのはありますが、ある role に Amazon Pinpoint をさわれるポリシーを attach し、その role に assume(変身)して Pinpoint を操作する、というのが基本的な流れになるのは変わりません。

では今回はこの専用ロール(Pinpoint) が S3 上のファイルを指定して Create Import Job を作成するのですが、この 「PinpointがS3上のファイルを指定して」 という部分、ここで結論に記載した専用の role が必要になります。
さらに Pinpoint を触れる role(roleA) が CreateImportJob を発行するときに、この Pinpoint -> S3で触る特別な role に成り代わる( iam.PassRole) 権限がないと、CreateImportJob を発行できません。ここが一番つまづきました。

このため、pinpoint の roleA には S3 を触るための roleB への iam.passRole が許可されてる必要があります。

余談

作成したセグメントを使って Push 通知を配信するには CreateCampaign を利用します。
こちらの手順については別のエントリを書く予定です。

cloud.google.com/go/bigquery.Query の結果を go test の中で mock する

Overview

BigQuery を使った実装をしているときに、API リクエストを mock したいケースがあると思いますが、そういうときに安易に DI を使わずに実装する方法を考えます

やること

やることは難しくなくて BigQuery のクライアントを生成するときにテストでは API のリクエスト先を httptest.Server に向けるだけです。

実際のクライアントを生成する実装は以下のようになります。

ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    fmt.Printf("Request URL: %v\n", r.URL.String())
    var res []byte
    if _, err := w.Write(res); err != nil {...}
})

bq, err := bigquery.NewClient(ctx, "$ProjectID", option.WithEndpoint(ts.URL)
if err != nil {
    return err
} 

httptest.Server を起動し、BigQuery の接続先をOptionで起動したテストサーバーに向けます。

Query Operation の場合

https://pkg.go.dev/cloud.google.com/go/bigquery@v1.59.1#Client.Query のオペレーションを例にサンプルでテストサーバーを実装します。

Query Operation は BigQuery ライブラリの中で job のリストを取得しており、当てはまる jobId を query の API をリクエストする、という振る舞いになっています。

ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    fmt.Printf("Request URL: %v\n", r.URL.String())
    var res []byte

    if r.URL.Path == "/projects/eure-metis-dev/jobs" {
    res = []byte(`{"kind":"bigquery#job","etag":"\"abcdefghijklmnopqrstuvwxyz\"","id":"your-project-id:your-job-id","selfLink":"https://www.googleapis.com/bigquery/v2/projects/your-project-id/jobs/your-job-id","jobReference":{"projectId":"your-project-id","jobId":"your-job-id","location":"US"},"configuration":{"query":{"query":"SELECT user_id FROM test-data","destinationTable":{"projectId":"your-project-id","datasetId":"your-dataset-id","tableId":"your-table-id"},"writeDisposition":"WRITE_TRUNCATE","useQueryCache":true}},"status":{"state":"DONE","errorResult":null,"errors":null},"statistics":{"creationTime":"1600000000000","startTime":"1600000001000","endTime":"1600000005000","totalBytesProcessed":"123456","query":{"totalBytesBilled":"123456","cacheHit":false,"totalBytesProcessed":"123456","totalSlotMs":"1234","numDmlAffectedRows":"0"}},"user_email":"user@example.com"}`)}

    if r.URL.Path == "/projects/your-project-id/queries/your-job-id" {
    res = []byte(`{"kind":"bigquery#getQueryResultsResponse","etag":"\"abcdefghijklmn\"","schema":{"fields":[{"name":"user_id","type":"INTEGER","mode":"NULLABLE"}]},"jobReference":{"projectId":"your-project-id","jobId":"your-job-id"},"totalRows":"10","rows":[{"f":[{"v":"1"}]}],"totalBytesProcessed":"123456","jobComplete":true,"cacheHit":false}`)}

    if _, err := w.Write(res); err != nil {...}
})

bq, err := bigquery.NewClient(ctx, "$ProjectID", option.WithEndpoint(ts.URL)
if err != nil {
    return err
} 

これで2つ目の query のレスポンスに入っている、field key の結果が返ってきます。

ハマったところ

BigQuery のライブラリでどの API を Call しているのかわからなかった。

API のリクエストとレスポンスを調べて httptest.Server の中でレスポンスを生成して w.Write すれば終わりかと思っていたのですが、BigQuery のライブラリでは job の一覧を call したあと、該当する job の query を取得する API をリクエストしてることがわかりました。

これは httptest.Server で起動したサーバーでリクエストを dump してみて、job list -> query と順番に Call してることがわかりました。これで httptest.Server の中で call された URL に応じてレスポンスを変えることで、BigQuery の API ライブラリの振る舞いをトレースすることができるようになりました。

BigQuery の API のレスポンスを生成する

API のドキュメントは以下の2つを読めば Query オペレーションの振る舞いはトレースできるようになります。

ただし、この API ドキュメントもそんなに優しいわけではなく、例えば struct みたいな感じで中身が具体的に分からないフィールドもあるのに、API のレスポンスが期待されるレスポンスでないと空のレスポンスを作っているにも関わらず、ライブラリ側の Unmarshal 処理でエラーになっていまう、ということがありました。(レスポンスそれ自体に意味はないけど、ライブラリ側のバリデーションで落とされては振る舞いが完全なものになりません。

ちょっとどうしようか悩んでましたが、ChatGPT に上記のドキュメントを渡して「ダミーのレスポンスを生成して」という雑なプロンプトを投げるだけでサンプルレスポンスを生成してくれたのでこれはこれですぐに解決しました。

GoogleAPI のリクエストを mock するときのサンプルレスポンスの生成は今後全部 ChatGPT に投げれば解決しそうでした。

現職で5年目を迎えた

3/1 で現職に努めて 4年を終え、今年から 5 年目に入る。

今までのキャリアで3年以上同じ会社に努めたことはないので、現在進行系で最長在籍記録を更新中でもある。

3年もいると色々飽きてくるタイプではあるけど、毎年少しずつ違ったことにチャレンジさせてもらえるというのはありがたい限りである。

キャリアとしても20代は幾つかの会社を転々をしつつも、30代になって腰を据えて一つの会社でキャリアを積める、というのは履歴書的にも綺麗だな〜と思ったりも最近している。

今までほど開発そのものに関われる(コードを書くことそのものにコミットする)わけではないけれど、会社として求められてることを着実にこなしていくことにも面白さを感じている。こういうのをいわゆる「目線が変わった」というのかも知れない。
実装や要素技術そのものにコミットするのは自分より得意な人がいるので、適材適所ではないけれど、うまく知恵を借りながら結果を追求していく、ということそのものに面白みを感じるようになった、と言えるのかも知れない。ずっと「技術にこだわったうえで技術は手段と割り切りたい」と思っていたけど少しそこに近づけているような間隔も覚える。まだ道半ばだけど。

まぁ一方で在籍期間も長くなってきて、老害ゾーンにも足を踏み込みつつあるので、身の振り方などは意識していかないとなとも思う。仕事は多くの場合「何をいうかではなく、誰が言うか」という側面が大きく、ここを否定しては大きな結果は得ることができない。自分の影響力なんて微々たるもんだけど、「古株の言ってること」みたいなものには上はともかく現場はどうしても左右されてしまう人もいるので、そうならないように気をつけたい。誰が言ったかでなく、何を言ったかが大事で、そういった感情で動かされない人ももちろんいるけれど、人間は感情で動いてしまう生き物でもあるので、それを理解したうえでコトに向かっていくスタンスは続けていきたい。

正直、ここまで長く在籍すること想像していなかったので、次のキャリアを自分はどうするのか?というのは目下悩んでることである。

この仕事を始めた頃にフワッと思い描いていた社会的に意義のあるプロダクトに関わるということは現職で叶ってしまっているし、そもそも社会の動きが変わっている中の本流で仕事をできることはキャリアとしてもなかなかない体験でもあり、これ自体にはやりがいと関われてることの満足感は強く感じる。

ただ、これを経験してしまった自分の次のキャリアというのは、どこに向かっていくべきなのか?最近よく考えている。尚まだこれといって答えは出てない。
色々お話を聞く機会もあるけれど、ある程度ふわっと固まってるのは「2B よりは 2C の方が肌にあってそう」ということや「組織もいいけどプロダクトの強さ、面白さを語れるところに魅力を感じる」くらいの粒度の粗さを再確認をするのみで、これも明日には考え方が変わっているかも知れない。

まだまだやれることは多いし現職を頑張るつもりでいるけど、「その次」みたいなところは少しずつ考えていきたいなと思う。

VSCode のターミナルで間違ったコマンドを叩くと beep 音が鳴る

気づいたら突如 VSCode に integrate されている terminal で間違ったコマンドを実行したりすると beep 音が鳴るようになっていたのがうるさいので off にします。

これ、a11y のサポートが入って音が鳴るらしいのですが、なんでこの設定が強制的に on になっているのかはわかりませんでした。

stackoverflow-com.translate.goog

"editor.accessibilitySupport": "off" に更新すれば OK でした。

勝手に a11y が有効化されるのも困る...。アプデか何かで影響したのかも知れないです。

actions/setup-go で Go のバージョンを指定する

actions/setup-go は Go のバージョンを指定しないとデフォルトで Go1.20 が使われているっぽい挙動に出くわしたので Go のバージョンを指定する方法のメモ。

github.com

気づいたきっかけは local で使用してる Go のバージョンと CI (GitHub Actions 上)で使用してる Go のバージョンがズレていると、setup-go が落ちる、という事象に出くわしたこと。

Go のバージョン指定方法は公式の Readme に書いてあるとおり。

https://github.com/actions/setup-go?tab=readme-ov-file#basic

go-version というディレクティブに利用したい Go のバージョンを記載すれば良い。

ただこれだと CI を 2つ以上動かしているときに Go のバージョンが上がる度毎回 CI の設定ファイルを更新する必要があり、漏れが発生しやすい。
go.mod を利用するオプションがあるのでこちらを利用しておくと local で利用してる Go のバージョンと CI で利用する Go のバージョンをあわせる事ができそう。

ドキュメントとしては以下に記載してある内容。

https://github.com/actions/setup-go?tab=readme-ov-file#getting-go-version-from-the-gomod-file

BigQuery のクライアントを Go で実装する

Overview

BigQuery のクライアントを Go で実装する手順とハマったことについてまとめます。

基本的には 公式ドキュメント に記載されている内容そのままです。

cloud.google.com

Go で BigQuery のクライアントを実装する

公式ドキュメントのとおりであれば以下

ctx := context.Background()
client, err := bigquery.NewClient(ctx, projectID)
if err != nil {
        return fmt.Errorf("bigquery.NewClient: %v", err)
}
defer client.Close()

q := client.Query(...)
job := q.Run(ctx)

itr, err := job.Read(ctx)
for {
        var row []bigquery.Value
         err := it.Next(&row)
        if err == iterator.Done {
                break
        }
        if err != nil {
                return err
        }
        fmt.Fprintln(w, row)
}

独自の HTTP Client を使いたいときはクライアントの生成のときに Option に差し込みます。

hcl := custom.NewHTTPClient()
ctx := context.Background()
client, err := bigquery.NewClient(ctx, projectID, option.WithHTTPClient(hcl))
if err != nil {
        return fmt.Errorf("bigquery.NewClient: %v", err)
}
defer client.Close()

ここまではドキュメントどおりに実装すれば普通に動く実装です。

以下実装時に気づいたことまとめます。

BigQuery のライブラリは 2種類ある

BigQuery に限りませんが、GCP をサービスを利用するときには利用する Google のライブラリの種類を見る必要があります。多くの場合(ドキュメントのサンプルコードでも)は cloud.google.com/go/... というライブラリを使うことになります。
ただ実は他にも google.golang.org/api/... というライブラリもあり、一見するとどっちを使うべきなのか迷いますが cloud.google.com/go/... のライブラリは google.golang.org/api/... をより使いやすくしたライブラリでもあるので cloud 接頭辞の方のライブラリを使えばよいです。

例えば今回の BigQuery のケースだと、 cloud.google.com/go/bigquery の Query は iterator 型を採用してるので、クエリの結果が終わるまで値を取得することができますが、 google.golang.org/api/bigquery にはありません。一回のクエリのレスポンスはライブラリの側で最大 10MB に制限されてしまい、正しくすべてのクエリ結果を返すことができないシグネチャになっています。

// MaxResults: [Optional] The maximum number of rows of data to return
// per page of results. Setting this flag to a small value such as 1000
// and then paging through results might improve reliability when the
// query result set is large. In addition to this limit, responses are
// also limited to 10 MB. By default, there is no maximum row count, and
// only the byte limit applies.

BigQuery の型はそれぞれ Go で対応する形が決まっている

Next メソッドの GoDoc を見るとわかるのですが、Next メソッドには 1レコードを Struct で表現することでクエリの結果を自動で Struct にマッピングしてくれる という振る舞いがあります。
詳細は上記のメソッドの Example 参照。

ただしこの Struct にマッピングするときは実際の BigQuery の Schema がどういう Schema であるかによって対応する型が異なります。
例えば BigQuery の Schema が Nullable な String のとき Struct に string を割り当てても上手く以下ないことがあります(文字列が入ってくるときは問題ありませんが、カラムが NULL のときにエラーになる)

このため、Nullable なカラムのスキーマの事も考え、BigQuery の対応する Type の方を Struct に割り当てておくと安全です。対応表は上記 Next メソッドの GoDoc に記載してあります。

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
NUMERIC     *big.Rat
BIGNUMERIC  *big.Rat

ref: https://github.com/googleapis/google-cloud-go/blob/bigquery/v1.59.0/bigquery/iterator.go#L124-L137

具体的には以下のような Struct を定義することになります。

type Record struct {
    UserID int64 // Null が許容されていない Number 型のカラム
    HogeID bigquery.NullInt64 // Nullable な Number 型のカラム 
}

q := client.Query("select userID, HogeID from $dataset ...")
job := q.Run(ctx)


itr, err := job.Read(ctx)
for {
        record := Record{}
        err := it.Next(&record)
        if err == iterator.Done {
                break
        }
        if err != nil {
                return err
        }
        fmt.Fprintln(w, row)
}

カラム名とフィールド名は一致させる必要がある

最後にこれにハマりました。 Next メソッドの Godoc を読むと書いてあるのですが、

  1. struct にマッピングさせるときに Struct のフィールド名は Query で Select するカラム名と一致してる必要がある(tag 等での縛りがないので)
  2. struct のフィールド名と Query で Select するカラム名が異なっているときは parse は失敗してフィールドは空の状態になる。

書いてあるのは以下の部分

If dst is pointer to a struct, each column in the schema will be matched with an exported field of the struct that has the same name, ignoring case. Unmatched schema columns and struct fields will be ignored.

ref: https://pkg.go.dev/cloud.google.com/go/bigquery#RowIterator.Next

つまり

  • mapping 対象(dst) の struct はテーブルのスキーマと同じ名前のフィールドを保つ必要がある。
  • フィールドは公開状態にしておく(先頭は大文字)
  • マッチしないケース(ex struct のフィールドが UserID で BQ のテーブルスキーマカラム名が user_id などのとき)にフィールドの値は ignore される。

ということになります。

もしスキーマがずれるケースはクエリを書くときにエイリアスを当てる必要があります。サンプルは以下。

clinet.Query("select user_id as UserID ... from $dataset"

余談: ValueLoader を実装すると独自型を BigQuery の型を parse することができる

bigquery.Value 型は ValueLoader という interface を実装しており、これで Next メソッドの中で自動で与えられた変数(or struct) にクエリの結果をマッピングできるようになります。

ref: https://pkg.go.dev/cloud.google.com/go/bigquery#ValueLoader

これの interface を実装した type は Next メソッドの中で bigquery.Value を parse できるようになり、以下のような書換えが可能になります。

before: もともとは struct をマッピングする実装だったが、返却されるのが []int64

type row struct {
    userID int64
}

var userIDs []int64
q := client.Query("select userID from $dataset ...")
job := q.Run(ctx)
itr, err := job.Read(ctx)
for {
        r := row{}
        err := it.Next(&r)
        if err == iterator.Done {
                break
        }
        if err != nil {
                return err
        }
        userIDs = append(userIDs, r.UserID)
}

可能であれば struct にマッピングせずに int64 の配列をそのままほしいところではあるが、そのままの int64 は ValueLoader を実装していない。

after: 新しい Slice 型を独自型として定義して Next にわたす。

type (
    Int64Slice []*int64
)

func (sl *Int64Slice) Load(v []bigquery.Value, _ bigquery.Schema) error {
    for _, vv := range v {
        switch t := vv.(type) {
        case int64:
            *sl = append(*sl, &t)
        default:
            // ignore if value type is not int64.
            *sl = append(*sl, nil)
        }
    }
    return nil
}


var userIDs []int64
q := client.Query("select userID from $dataset ...")
job := q.Run(ctx)
itr, err := job.Read(ctx)
for {
        var s int64Slice
        err := it.Next(&s)
        if err == iterator.Done {
                break
        }
        if err != nil {
                return err
        }
        userIDs = append(userIDs, s[0])
}

まぁ書いててどちらも大差ないですが、独自型を使えるということの説明をしたかった、ということです...笑

余談2: Generics を使って汎用的な wrapper メソッドを作る

Generics を使った Query メソッドの wrapper が作れるなと思ったので簡単にまとめてみました。

type queryOption func(b *bigquery.Query) *bigquery.Query

func QueryAll[T any](ctx context.Context, queryStr string, opts ...queryOption) ([]T, error) {
    q := bq.Query(queryStr)
    for _, opt := range opts {
        q = opt(q)
    }
    job, err := q.Run(ctx)
    if err != nil {
        return nil, err
    }
    itr, err := job.Read(ctx)
    if err != nil {
        return nil, err
    }

    var rows []T
    for {
        var row T
        err := itr.Next(&row)
        if err == iterator.Done {
            break
        }
        if err != nil {
            return nil, err
        }
        rows = append(rows, row)
    }
    return rows, nil
}

使うときは以下のように使います。

type row struct {
    userID bigquery.NullString
}

q := "select userID from $dataset ..."
rows, err := bigquery.QueryAll[row](ctx, q)

独自型を定義したときは以下です。

q := "select userID from $dataset ...."
rows, err := bigquery.QueryAll[bigquery.Int64Slice](ctx, q)

これもわざわざ Generics で抽象化する必要もないかなとも居ますが、冗長な実装を何度も書くことないのと Generics を使えそうというフィードバックをもらったので試してみました。
特に T には何型が入ってもよく、呼び出し元で record を表現した struct とフィールド名をエイリアスにしたカラム名を使う Select 文を考えるのみでよいでの若干コードはスッキリするかなと思います。

git コマンドが重たくなったら git gc コマンドを使ってみる

Overview

git gc が便利という話です。

git push が重たい

ここ1年くらい git コマンドがものすごく重たい事象に直面しており、特に git push したときに 10秒くらい待たないと push が完了しないという事象が発生してました。
github も頻繁にアプデ繰り返してるので当初は github が重たくなったのかな?と完全に人のせいにしてましたが、 周りは層でもないらしくどうやら「おま環事案」という感じがしました。

git gc

ちょうど git が重たいなら git gc コマンド試してみれば〜ということを教えてもらったので試したところ、手元のゴミファイルやゴミデータを丸っと削除してくれて git push が快速になりました。

git-scm.com

git にはまだまだ知らない機能がありますね。

git が重たいなと感じたら git gc コマンド試してみてください。

子供と過ごす初めての冬休み

まえがき

人生で初めて子供と年末年始を共に過ごしたので思ったことを備忘録として書いておく。

子供過ごす初めての年末年始だった

自分は今までに30回以上(物心ついてから考えると20数回か)年末年始というイベントを過ごしてることになるけど、我が子にとっては当たり前だけど初めての年末年始なわけで、家族として過ごす年末年始も初めてなわけである。

不思議と子供がいるだけでなんとなく初めて年末年始を過ごす、みたいな錯覚に陥った。家族集まって過ごせるのも初めてであるし、そもそも奥さんはカレンダー通りのお休みの職業ではなく、一般的なサラリーマン、公務員が休日のときに仕事があるタイプの職種なので、一昨年結婚してるのに奥さんも含めて家族揃って年末年始を過ごすというのが人生で初めてだった事に気づいた。
そういえば、大学で上京して以降、誰かと年末年始の全てを自分以外の誰かと過ごすというのはこれまた自分でも初めてだった。(帰省はしていたが、実家にいても飽きるし、地元で時間を過ごす相手は歳を重ねるに連れて少なくなって行って今ではほぼいないので実家への滞在期間は長くて2泊程度だったりするのが結婚するまでのここ数年だったりしたので)

話は戻って子供と過ごす年末年始というのは新鮮ではあったものの、基本的な過ごし方はいい意味でいつも通りと変わらなかったので、実を言うと年末年始感はあまりなかった。

ただクリスマスだったり、大晦日だったりといったイベント類はやっぱり家族揃っていることでちょっと豪華に過ごしたりとかそういうことに気が回るようになった。自分ひとりで過ごしていたときはだいぶ異なったことを考えるものだなと結構驚いた。
まぁでも冷静になって考えてみると、今はまだ0歳の我が子も自分たち両親と一緒にこういったイベントを過ごしてくれる機会も数えてみるとそう多くないのかもしれない(*)と思うと、これから先の数々の節目やイベントはそれなりに記憶に残るものにしたいな、ということも考えたりもした。

※ 大学に行くまでは18回。もし付き合ってる人とか出来たらもっと早く両親と過ごしてくれる機会がなくなるのは早いかもしれない。

その他

家族と過ごすお正月だったので他にも気づいたことを書いてみる。

年末年始のテレビ番組は結構面白い

ある年齢を境に正月特番と行ったものをほとんど見なくなってしまっていたけど、今年は結構見ていた。
現職のプロダクトのCMがあるかも、ということもあってTVを意識的につけっぱにしていたことも関係してるが、子供がいると何かしら BGM をかけていたくなるが、動画サービス流しっぱでも同じコンテンツばかりが流れてくることもあって飽きるので、案外 TV というのは BGM としても優秀だなと思ったりもした。
子供が寝静まった後にも見ていたけど、自分たちが物心ついていたときにみていた番組の復刻版とか特番とかもやっていて案外面白いものだなと感じた。思えば子供の頃はお小学特番をめっちゃ見てるタイプの人間だったので、そういえば正月ってこんな過ごし方だったなと懐かしさも感じた。

特に紅白とか見ながら我が子は「どんなアーティストを好きになるんだろうね?」とか「何が好きになるのか?」みたいな会話に花が咲いて、今までの自分では考えもしないことを考えるものだなと自分が一番驚いた。

余談だが、正月早々気が重たくなるニュースが重なったけどテレビ局は比較的いつも通りのテンションで番組を放映してくれたので気の滅入るようなコンテンツを目にする機会を減らせてよかったかもしれない。その分 X (旧 Twitter) はだいぶ地獄な様相だったが。
こういうところも東日本のときの反省が活かされている?のかもしれないなと感じた(災害ニュースばかりを流さない的な)

年末年始は断捨離が捗る

年末年始は大掃除の機会といったけれど確かに家族が揃っていると今まで手を付けてこなかったゴミを捨てたりといった断捨離は格段にはかどりました。やっぱり普段から「これ邪魔だよね?」とか「これもう必要なくない?」といったことを会話していたので、今回は夫婦揃う初めての年末年始だったので一気に片付けたり、捨てた。

ただ、捨てたぶんだけ子育て用品が増えているので捨てたもののプラスマイナスはゼロかなと思う。

年賀状を書く

ある年を堺に年賀状なんて書かなくなっていたけど、子供の写真を載せた年賀状を親戚に送った。結構喜んでもらえた(と母親から連絡があった)

親戚の成長というのは老後の楽しみの一つらしい。まぁ結婚して以降実家に帰る機会もめっきり減ってしまったのでそういう意味では年に一回子供の成長を知らせるというのは親の義務なのかもしれないなと思った。
これも今までの自分では考えもしなかったことだなと思う。

2023 年の振り返り

まえがき

年の瀬なので今年も1年の振り返りをします。

子供が生まれた & 育休を取った

今年一番の大きな出来事は子供が生まれ、父親になったことでした。

私生活のすべてが一変し、また年の半ばに3ヶ月ほど育休取得したことで、今後のキャリアをはじめ自分自身のことや働き方のことを考え直すいい機会にもなりました。

ema-hiro.hatenablog.com

ema-hiro.hatenablog.com

ema-hiro.hatenablog.com

正直育児は楽しいことばかりではなく、子供が生まれたことでできなくなったこともありますが、一方で得たものも多いですし、また個人としては仕事でどこにフォーカスしていくのか、ということが方向性をある程度クリアにできた1年になりました。

筋トレの頻度が増えた

週1程度で4年ほど続けている筋トレですが、これが週2になりました。

子供が生まれて自分1人の時間というものが著しくなくなったのもあり、自分としては結構ストレスが溜まっていたので、そのストレス解消のために週末の数時間1人の時間を作るためにジムに行くようになりました。

家族も増えて今まで以上に体が資本であるのと同時に、1人でできる自分なりのストレス発散方法を持っているというのは大事だなと改めて思いました。

自分の作ったプロダクトが TVCM で放映された

現職で開発に関わっている Pairs のテレビ CM が開始されました。

この業界に入って以降一度は経験したいと思っていた「自分が仕事で関わってるプロダクトがテレビ で CM される」という機会を得ることが出来ました。

この年末年始にも新しい CM が放映されており、こちらはちょうど自分が育休明けに開発に関わった機能がフューチャーされています。

自分の書いたコードで動いてるプロダクトが特集されてそれが日本全国で見られてるかと思うと、正直なかなかエモい気持ちになります。エモい。

数年ぶりに個人開発した

自分は週末にコードを書いたり、個人開発をしたりするタイプのエンジニアではないのですが、育休期間中に自分のポートフォリオのサイトを Nuxt -> Next に書き換えたり、気になっていた Connect を使ってツール作ったりといったことをしてました。

過酷な育児に耐えかねてのほとんど息抜きというか現実逃避というか、そういう感じで久しぶりに仕事以外でコードを書くことを楽しんでいたのですが、仕事してる時よりコード書いてて自分でも驚きました。

コードを書いたり新しい知識や技術を身につけるんだというパワーに少し陰りが見え始めていたなと自分でも自覚があったのですが、時間があれば「息抜きにコード書く」人種なんだなと知ることが出来て自分でも少しホッとしました。なお、仕事に復帰してからは全然週末そういう時間は取れてません...涙

ブログ継続した

気づいたら今年は30記事くらい書いてました。自分でも結構なペースで書いてたんだなと思います。
技術ネタだけじゃなく、育児のことも残すようになったので単純にネタが増えたことも影響してそうです。
毎年思いますが、エンジニアキャリアの半分以上をブログ書いて過ごしてるわけで、完全にこのブログを書くことが趣味というかこれもまた一つの息抜きになっています。

来年もちゃんと継続します。

来年の話

2023 年は1年のちょうど間に育休が挟まっていたので、1年の中で前半と後半での180度生活が変わりました。
来年は今年の後半をベースしないといけないのでそんなに大きな目標はないです。毎年そうなんですが、仕事以外でこれと言ってやりたい目標がないので、育児しながらのらりくらりと過ごしてそうな気がします。
あ、スプラトゥーンはもっと上達したい(XP2000の壁は今年超えられませんでした) のと、サッカーを今年は1試合しか現地観戦できなかったので来年はもう少し現地観戦できる機会を増やしたいなと思っています。
あと、今年息抜きにコード書いたり競プロ(Leetcode) 始めたりしてやっぱりコード書くの楽しいなと思ったので、週末にコード書ける時間は少しでも取れると嬉しいなと思ったりしています。

2023年の買って良かったもの

今年買ったものは育児関連グッズが多すぎるので、育児関連グッズ以外で買ってよかったものを上げておきます。

HHKB Studio

HHKB の新作を買いました。実は Keychron Q2 を買ったり、MISTEL の分割キーボードを買ったりと結構キーボードにお金を使った年だなと思ったんですが、結局 HHKB の配列から逃れられずに HHKB に帰って来たところでこちらの新作が発売されたので買いました。
色々言われてますが、打鍵感が今までの HHKB の中では一番好みで、かつカスタマイズ可能というところが良きです。
ポインティングスティックやジェスチャーパッドは正直感度が微妙なのであってもなくてもどっちでもいいかなと思っていますが、キースイッチだけでも買ってもいいかなと思わされるガジェットでした。正直追加された機能群なしでカスタマイズ可能かつ Studio で採用されたキースイッチを採用した Professional シリーズももう少し安価で出してくれてもいいのでは?と思ったりもしています(そしたら自分はきっとそちらを買うんだと思います。)

https://amzn.to/3RD8Zac

happyhackingkb.com

大きな本棚

最近ですが技術書が縦置きで2列入る奥行きの大きな本棚を買いました。
僕は本を買うなら物理本派(漫画は電子書籍)なので、とにかく本を置く場所に困っていて、部屋の中に積んだりうまく収納を作って置いたりを繰り返してたのですが、今回買った本棚に全て収納仕切れて、部屋も少し広くなったのでとても満足でした。

大体奥行きが50cm くらいある本棚だと技術書を贅沢に置くことが出来て良い感じです。

やはりものを置くものはデカいに限りますね。

www.nitori-net.jp

オーブンレンジ

自宅の電子レンジをオーブンレンジに買い替えました。
ヘルシオとかも考えましたけど、置く場所の寸法の問題で断念して普通のオーブンレンジを買いました。僕の使う範囲では電子レンジの時代と何も変わってませんが奥さんは料理のレパートリーが増えたりあと離乳食作るのに役立ってるみたいです。

www.toshiba-lifestyle.com

SIXPAD パワーガン

夫婦共々育児で深刻な肩こりと腰痛に悩まされるようになっていたので対処療法として購入しました。
僕は筋トレ後も使って翌日のダメージ軽減等にも使っています。
結構充電が持つのでそんなに頻繁に充電しなくても大丈夫なのも良いポイントです。

https://amzn.to/3H15Vjk

www.mtgec.jp

iPhone15 Pro

今年もiPhoneを買い替えました。
毎年似たりよったりのマイナーアプデばっかりで今年も買い替えるか悩んでいたのですが、子供を撮影するコンパクトなカメラだと思ってるのでカメラ買う感覚で買いました。
今年は軽くなったり、Type-C になったりとユーザーが欲しいと思っていたアップデートがあり、買い替える価値はありました。

www.apple.com

Nishikawa Air 4DX

枕を買い替えました。
数年来眠りに投資しよう、まずは枕を買い換えようと思っていて、なかなか出来ていなかったのですが、今年満を持して買い替えてみて、眠りに投資しなかったここ数年を後悔しました。

www.airsleep.jp

go-cmp/cmpopts を使って map の特定の key のみをフィルタする

Overview

go-cmp を使って実際取得した値と期待値を比較するときに go-cmp/cmpopts を使って、二値の比較のオプションを設定することが出来ますが、この二値が map のときに map の中の特定の key に紐づく value のみを比較したいというユースケースがあって、既存の options だけでは満たせなかったのでやり方を考えました。

cmpopts.IgnoreMapEntries を利用する

IgnoreMapEntries を利用します。

IgnoreMapEntries は map 同士を比較、もしくは map と比較するときに特定の key-value のセットを ignore するという options になります。

discardFunc は interface を取っていますが、ここは func(k string, v any) bool という func 型を取り、指定した key を指定して返り値が true となる key-value のセットを ignore します。

繰り返しですが、このメソッドは func(k string, v any) bool の返り値が true のときの key-value のセットを ignore する、というオプションになるので、逆に false の場合は Ignore しない(=残る)というものになります。そのため、以下のようなメソッドを用意して特定の key-value のセットのみを残す(= フィルタする)ということが可能です。

// any のところは比較したい map の型定義に合わせる。例えば map[string]int 同士を比較したい場合は `want map[string]int` をとります。
func FilterWantMapEntires(want map[string]any) cmp.Option {
    return cmpopts.IgnoreMapEntries(func(k string, _ any) bool {
        _, ok := want[k]
        return !ok
    })
}

使い方としては以下のような感じで option の中に組み込むことを想定しています。

func main() {
    got := map[string]int{"1": 1, "2": 2, "3": 3}
    want := map[string]int{"1": 1, "2": 2} // 1 と 2 のみ比較したい。

    opts := []cmp.Option{
        FilterWantMapEntries(want),
    }

    if diff := cmp.Diff(got, want, opts...); diff != "" {
        fmt.Println(diff)
    }
}

ref: https://go.dev/play/p/4eU9w2nTgBp

Ref

あとから調べたのですが、go-cmp の cmpopts の使い方で実際にユースケースでよく使うものをベースに以下がよくまとまっていました。

zenn.dev

育休から復帰してみて変わったこと

はじめに

これは「子育てエンジニアAdvent Calendar 2023」 7日目の記事です。

adventar.org

About Me

  • 都内勤務。
  • 住まいは郊外。会社までは大体ドアドアで1.5時間くらいの通勤時間。
  • 今年の4月に第一子の娘が生まれた。
  • 核家族構成で夫婦共働き(今は奥さんが育休中)
  • 5月~7月末まで3ヶ月間育児休暇をもらった。

生まれた直後や、育休取った話は自分のブログで別でエントリを残していたりします。

ema-hiro.hatenablog.com

ema-hiro.hatenablog.com

今回は育休も明けて復職した今現在、以前と比較して変わったことや、復職してしばらく仕事をしてみたタイミングでどんなことを感じてるのか、ということを言語化してみました。

週末の過ごし方が変わった

元々週末に外に出るタイプでもなかったですが、旅行や家族のお出かけを除いて週末は基本的には家で、しかも子供のいるリビングで過ごす時間が増えました。
奥さんも土日に仕事があるタイプというのもあり、以前は自室で過ごすことが多かったのですが、ちょうど寝返りを打ち始めたくらいから放っておくとどこに行くかわからないので、リビングで過ごす時間は増えました。(ほぼ監視要員ですが)
ただ、ずっとリビングから身動き取れないのもしんどいので、1日にうちどこかは自室に籠らせてもらってます。

出かける先が変わった

週末のお出かけ先が大型ショッピングモールか奥さんの実家、がメインになりました。どちらも車で行ける範囲でかつ、子育て関連(特に乳幼児)のアメニティが充実していることが条件で、これを満たせないお出かけ先はいけなくなりました。
※ 実は生まれてからまだ一度も公共交通機関に乗ってません。

この辺もブログに書いてみました。

ema-hiro.hatenablog.com

もっと子供を連れて都内のイベントとか行くのかな〜とか思っていたんですが、まぁ現実問題かなり厳しいな、、というのが感想です。
都内に住んでいればまた話は変わってくるのかもしれませんが、郊外に暮らしてて都内に子連れで、しかも公共交通機関使う、というのはだいぶ億劫になってしました。

通勤が嫌いじゃなくなる

現職はフルリモートが許可されてはおらず、週に何日か出社する必要がある会社であり、ちょうど育休明けのタイミングでこの出社(RTO)がルール化されたのもあって最初は正直めんどいな〜と思っていたんですが、そうは言ってもサラリーマンなので定常的に出社し続けけてみて「通勤時間、実は一日の中で唯一1人になれる時間」ということに気づきました。
このことに気づいてから、毎日出社は流石にしんどいですが、定期的に出社する理由があるというのは、子供が出来て以降なかなか取ることのできなかった「一人の時間」というものを強制的に確保することに繋がってて、そのおかげか育児中少しずつ削られてたメンタル面が回復しました。
色々言われる RTO ですが、こういうのも考え方1つで前向きになれるものだなと思いました。
ちなみに通勤中はひたすらスプラ3の動画見るか、シーズンごとのアニメを消化しています。
(通勤時間は勉強時間?…え?なんのことですか?)

一人の時間というのは自分たち夫婦でも度々話に出るないようなのですが、子供は可愛いものの、やはり1日中拘束されざる得ないことで多少なりしんどい時間があるのは事実であり、定期的に子供と離れる時間は必要で、自分の場合はそれがたまたま「出社」だった、という感じです。
なお、出社日は丸っと奥さんにワンオペ依頼をしてる身でもあるので、そこは本当に感謝してもしきれません。

スキルアップのスタンスが変わる

これもよく言われてる話ですが、いわゆる「子供ができるとスキルアップの時間が取れなくなる」というこれに自分もぶち当たりました。
自分自身はもともと週末にコードを書いてたりするタイプではありませんでしたが、気になるトピックをつまみ食いしたりとか、本読んだりということに当てていた時間はほぼなくなりました(気分転換の読書はしますが、何かを学んでやる!みたいなモチベではなくなりました)

これに対して正直言うと葛藤はあったのですが、今はたまたまそういう時間が取れないタイミングというだけで、手のかからなくなるタイミングはそのうちくるので、子供の成長に合わせて調整していけばいいかな?くらいの考えでいます。
ただ、スキルアップに限らずなんでもそうですが、好きなことに自分の時間をフルで使える期間というのは思いの外短く、そしてありがたい時間だったんだな、と今思い返すと感じます。

一日のやることを極限まで絞る

上記のスキルアップのスタンスを変える話とも似てるのですが、育児周りのタスクをこなしていると一日の過ぎ去る速度が本当に一瞬で自分自身のやりたいことをする時間は子供が起きてる時間は殆どできないので、一日の中で「これやりたい」ということを普段ものすごく絞っています。
例えば子供向けの椅子を買った週末なんかは、それを「組み立てる」という目標だけ立ててそれさえ週末できれば「偉い!よくやった」と思うようにしてます。一番疲れてるときなんかは「メルカリの発送する」というタスクだけを週末にすることで自分のことを褒めてました。
できる限りハードルを下げて過ごしていないと、突発的なイベント(子供の対象不良など)に対応しきれないし、メンタルの余裕もなくなってしまうので、とにかく自分自身のやること(やりたいこと)を極限まで絞りつつ、かつサボれるところはうまくサボってと、元々決めてたこと以上のことができればすべて儲けものと思える状態を作れると、それだけで前向きになれました。

その他

政治に興味を持つようになった

政治家になりたいとかそういう意味じゃないです。
もともと経済状況とかを軽く眺めるくらいの興味しか持ってなかったですし、どちらかといえば子育て支援に予算回した方がいいんじゃないの〜?くらいの薄い関心しか持っていなかったのですが、子供を持った瞬間に見るポイントが明確に変わりました。
やはり子育てに関わる論点を挙げてる候補者に目が行きますし、政治そのものも子育てのトピックに対してどういうスタンスを取っているのか?ということを真っ先に見るようになり、当事者意識は上がったなと感じます。

良い父親像みたいな理想を捨てる

インターネットに触れてると「父親でもこれくらいできないと」といった発言を偶に目にしますし、自分も当初はそれくらい頑張らなければいけないのかな、、と気張ってもいたのですが、まぁやっていくうちにできることとできないことがある事に気づき、無理しても理想とのギャップでしんどくなるだけなので、そういうものを一切気にしないことにしました。
育児に関しては「最低限の冗長化」を目標としていて、奥さんだけしかできないことがないようにするということは担保しつつ、それ以上のことは自分にも期待しないようにしてますし、家庭でも期待しないで、ということを伝えてます。

おわりに

まとまりのない文章でしたが、書き出してみると「いかにメンタルを安定させるか」ということをフォーカスして自分は復職以降の生活を続けてきたことがわかりました。
自分の子供はもちろん可愛いです。ただ、育児が始まって以降公私問わず気の休まる時間がかなり減って、しんどい面はあるので夫婦でうまくサボれるところはサボりつつ、自分は自分で自分自身のご機嫌を取りつつ、心の余裕を作りながら子供と向き合っていこうと思います。

AWS Go SDK でのエラーハンドリングについて

AWS の Go SDK はその本体のコードを読もうとすると実装がものすごく抽象化されていて、慣れていないと実際のコードを追いかけることも結構難しい。

Go の実装でエラーの中身(型)を見て処理を変えたいケースやそもそもどういうエラーを返すのかを知りたいときは、以下の Go SDK のドキュメントや、コマンドのドキュメントを見に行くと API がどういう振る舞いをするのかを調べることができる。

aws.github.io

ex. これは DeleteObjects のコマンドリファレンスだが、API の仕様に準拠してるので AWSAPI リファレンスを読むよりAPIの振る舞いをつかみやすい。

docs.aws.amazon.com

配列形式のクエリパラメータを受け取る実装について考えてみる

Overview

ID=1, ID=2,... のようにクエリパラメータで指定したパラメータを key にしてリソースを取得する GET の API、というのは一般的かと思いますが、このクエリパラメータで同じ key を複数取って複数のリソースを取得する実装パターンについて API を実装するときのベターなプラクティスとそれに伴う Go の実装について考えてみます。

Motivation

API を書いていたときに RDB 上の PrimaryKey をクエリパラメータに指定してあるリソースを複数取得する Endpoint を実装するときに、クエリパラメータの取り方で実装のフィードバックを受けたのがきっかけです。

実装パターンについて

以下のパターンをベースに考えます。

  1. GET /user?id=1,2,3 という 取得したいパラメータのリストをカンマ区切りの文字列 で表現する方法。
  2. GET /user?id=1&id=2&id=3 という 取得したいパラメータを1つずつ & で繋げて配列形式を表現する 方法。
  3. GET /user?id[]=1&id[]=2&... という 2 と似ているが取得したいパラメータが「配列である」ことを表現するために [] のブラケットをつける 方法。

GET /user?id=1,2,3 のパターン

ほぼ癖ですが、僕は自分が Overview に記載されているような GET の Endpoint を実装するときはほとんどこのパターンを採用してました。強い理由はないですが、URL からもあるリソースに対してどういうフィルタが適用されているのかが直感的でわかりやすいと考えていたからです。

Go で実装するときも以下のようにクエリパラメータを取り出して、split 分割で配列に変換して後続の DB レイヤの処理に回す、という実装をしていました。

var ids []int
query := r.URL.Query()
if q := query.Get("ids"); q != "" {
    split := strings.Split(q, ",")
    if len(split) != 0 {
        for _ , idstr := range split {
            id, _ := strconv.Atoi(idstr)
            ids = appned(ids, id)
        }
    }
}

ただ、このようなクエリパラメータのパターンは iOS で一般的に使用されている Alamofire のような HTTP クライアントのライブラリではそのままで対応してないです ※ ※ 配列を表現したいときは ids[] というようなブラケットが付いてしまいます。

dev.classmethod.jp

Go の実装面でもカンマ区切りの部分を Split して文字列を分割する必要があり、ちょいと工夫が必要です。

GET /user?id=1&id=2&id=3&id=... のパターン

URL 上は若干読みづらい(直感的ではない)ですが、配列にしたい key を複数クエリパラメータに指定することで「配列である」ことを表現します。

この場合、Go 側の実装では以下のようになります。

var ids []int
query := r.URL.Query()
if idsStr = query["id"]; len(idsStr) > 0 {
    for _ , idStr := range idsStr {
        id, _ := strconv.Atoi(idStr)
        ids = append(ids, id)
    }
}

これだと Go 側では文字列の分割処理をする必要がないです。

パターンとしても直感的ではないものの、 Endpoint のクエリパラメータで配列を表現するには一般的らしいです。

stackoverflow.com

GET /user?id[]=1&id[]=2&id[]=3&... のパターン

2 のパターンと同じですが、先に紹介した iOS のライブラリのようにクエリパラメータの中で、クエリパラメータが配列であることを表現します。

Go 側の実装は 2 のときとあまり変わりません。

var ids []int
query := r.URL.Query()
if idsStr = query["id[]"]; len(idsStr) > 0 {
    for _ , idStr := range idsStr {
        id, _ := strconv.Atoi(idStr)
        ids = append(ids, id)
    }
}

RFC 3986 を見る

もともと カンマ区切りの文字列 を採用してるときに iOS との連携時に、クエリパラメータがうまくデコードできないということに気づいて今回のことを考えました。

iOS としては元々 3 のパターンを前提としてクエリパラメータをデコードしていたのですが、どうやら iOS 側で利用してるメジャーなライブラリ(現職の iOS チームが使っていたもの)が RFC 3986 に対応したことで 3 のパターンが使えなくなり、結果としては 2 のパターンで実装する事になりました。

なお、この RFC3986 に対応したことでデコードエラーになるようになったのは、 []予約語扱いになったので、ライブラリ側でデコードエラーとするようになったっぽいです。
ref: https://tex2e.github.io/rfc-translater/html/rfc3986.html#2-2--Reserved-Characters

まとめ

クエリパラメータの中での配列の表現について自分の実装の癖を見直すいいきっかけになりました。
動作としては同じでもモバイル側の都合等々、考慮しないといけないポイントがありますね。