emahiro/b.log

Drastically Repeat Yourself !!!!

Kalidoface3D × OBS を使って Mac でアバター配信に入門する

f:id:ema_hiro:20210716040025p:plain

Overview

Kalidoface3D というブラウザから3DモデルをトラッキングできるツールとOBS (Open Broadcast Software) を使って Mac 使いでもお手軽にアバターを纏ってオンラインMTGに参加できることが判明したのでその手順についてまとめます。

www.moguravr.com

僕自身アバターを纏って Zoom に参加することにはずっと興味がありましたが、如何せん現状の VR 事情だと必要機材が多かったり、メインは Windows世界線だったりしてそもそも始めるのにハードルが高いなーと感じてたのですが、この Kakidoface はブラウザで提供されてるツールなので OS を選ばずに使うことができます。
ブラウザなのでスマートフォンタブレットからもアバターを纏うことが可能になります。

必要機材

  • Mac ( 202201現在M1Max推奨 )
  • ブラウザ(なんでもいいですが Safari だと ボディトラッキングは許可されてないそうです)
  • Kalidoface 3D
  • Open Broadcast Software
  • 配信したいツール(今回は Zoom を想定しています。)

手順

Kalidoface 3D にいってお好きなアバターを選ぶ

Kalidoface 3D - Face & Full Body Tracking にいってお好きなアバターを選びます。

f:id:ema_hiro:20210716040005p:plain

OBS をDL して入力ソースの設定を行う

OBS の DL & Install

Open Broadcast Software から Mac 版を DL してきて起動します。

起動すると最初に使用用途を聞かれるのでどれか適当に回答してください。今回は仮想カメラを使って配信したいだけなので一番下を選択しました。

f:id:ema_hiro:20210716040252p:plain

入力ソースの設定

慣れてないとまず OBS の使い方でつまづきます。
OBS はあくまで仮想カメラなのでカメラに何を入力するのか、という入力ソースを指定する必要があります。今回はブラウザのツールを仮想カメラで表示させたいのでブラウザをキャプチャするのでソースにブラウザを指定します。

ソースタブの中で右クリックして追加 > ウィンドウキャプチャを選択します。

f:id:ema_hiro:20210716041054p:plain

Mac で画面収録が許可されていればウィンドウの一覧で Chrome が出てくるはずです。繰り返しですが、 OBS に画面収録の権限を OBS に渡しておく必要がある ので、もし Chrome がでてこない、という方は Preference > セキュリティとプライバシー > 画面収録で OBS を許可しておきましょう。許可したら OBS 再起動してくださいね。

f:id:ema_hiro:20210716041142p:plain

ウィンドウキャプチャをソースに設定するときの画面が出てくるのでこの画面で Chrome を選択します。
プレビュー画面で Chrome が表示されない場合は空の名前でウィンドウを表示 にもチェックを入れておきます。(最初これがやらないとブラウザの画面がキャプチャされませんでした。何か更新処理でも走ってるのかもしれないです)

ここまでくるとブラウザが OBS にキャプチャされるようになるはずです。キャプチャされたのを確認したら OBS でキャプチャされてる画像が投影されてる画面で右クリックして「プレビュー有効化」を選択します。

f:id:ema_hiro:20210716041746p:plain]

これで OBS 側の基本的な設定は終わりです。Kalidoface3D で動いてるアバターのキャプチャが表示されていることを確認したらコントロールタブの「仮想カメラ開始」をクリックして入力ソースを仮想のカメラとして認識させ始めてください。

f:id:ema_hiro:20210716042007p:plain

うまくいくと以下の画像のようになります。

f:id:ema_hiro:20210716042212p:plain

※ OBS ではブラウザをキャプチャしてるだけなのでブラウザのフォーカスを他のタブにしたらアバターは表示されなくなるのでタブの移動は注意してください。

以下の記事も詳しいです。

create.anigameinfo.com

Zoom で OBS の仮想カメラをカメラのソースに指定する

OBS 側で配信開始をしたら、一度 Zoom を再起動してください。そしたら再起動後にビデオの入力ソースにOBSの仮想カメラあることを確認して、選択してください。

Zoom の設定画面で以下のようになると思います。

f:id:ema_hiro:20210716043016p:plain

ここまできたらもう Zoom MTG でビデオを ON にした段階でアバターを纏った姿が配信されます。

f:id:ema_hiro:20210716043600p:plain

配信時の Tips

クロマキー設定して OBS の背景を透過させる。

アバターの背景を透過させたい場合はグリーンバックにする必要があるので、OBS 側でその設定を行い、Zoom 側で背景とフィルター >「グリーンスクリーンがあります」を選択します。

www.alloneslife-0to1work.jp

このブログが詳しいですが、やりかたとしては OBS のキャプチャしてるシーンの箇所でプレビュー有効化した時の要領で右クリック > フィルタを選択。

以下の画面のエフェクトフィルタ > +ボタン押下 > クロマキーを選択します。

f:id:ema_hiro:20210716043957p:plain

これで背景を透過できるようになります。

Zoom の解像度に合わせて OBS の出力を調整する

ブラウザで Kalidoface3D 開いて OBS でキャプチャして、Zoom でそのソースを参照する + 開発用途でクジラをわんさか動かしてたりすると PC が悲鳴をあげて、性能が著しく劣化します。

焼け石に水感ありますが、すこしでも負荷を下げるために Zoom での配信目的、というだけであれば OBS 側のキャプチャ結果の解像度を Zoom に合わせて下げたりすることもできます。
OBS の設定 > 映像 で最適なものに変更します。今回は Zoom で MAX が HD なので 1280x720 に出力を合わせてしまっても問題ありません。

f:id:ema_hiro:20210716044423p:plain

感想

ツールのセットアップ周りはまだコツがいる、というか知らないとどうにもならない(僕も現職の VR チーム勢にめっちゃ質問してました)ということはありますが、今回の手順はあまりに簡単でもう Mac だからバ美肉できない、なんて言い訳は通用しなくなったなと思います。
とにかくこんなに簡単にできるとは思いもしませんでした。

そして一度アバターを纏うと全ての場所にアバターで露出したくなる衝動に駆られることがわかりました。プライベートな情報を秘匿しつつ、カメラOFFのときより動きがあるからコミュニケーションに齟齬が生じる可能性が少なさそうでとてもいいですね、アバター

宿題系

Bluetooth との噛み合わせがイマイチ

Mac で kalidoface3D を OBS 経由で Zoom に配信する場合 Zoom 側で Bluetooth 接続機器が丸ごと死ぬという事象が発生しました。
これはなんで起きるのかまだわかってませんが、とりあえず Bluetooth で接続してたスピーカーからは相手の声が一切聞こえてこなかったです。
現状とりあえず有線接続の機器だと正常に動くので Zoom で配信するときは有線のヘッドホンをしばらくは使おうと思います。

~どこの噛み合わせが悪いのかまだ分かってません。むしろ上手くいった方いらっしゃったら教えてください。~

=> これはおそらく CPU 使用率が高すぎて Bluetooth が不安定になってる、という状況になってました。

追記

20220125

Kalidoface3D を Mac で使用する最大のボトルネックが PC のパフォーマンスで、アバターかぶって Zoom で配信してる最中は、めちゃくちゃPCが重たくなってしまうのがかなりストレスだったのですが、2022年になって M1Max を手に入れて再度同じ構成で試したところ、気持ち悪いくらいサクサク・ヌルヌルで最高の体験を手に入れてしまいました。
現状手元の M1Max の性能は以下ですが、この性能では Docker を立ち上げた状態で OBS を起動し、仮想カメラ経由で Zoom に配信するという Intel Mac であればファンが悲鳴をあげ、CPU使用率も常に天井に張り付いてしまい、他のアプリケーションの動作に影響を与えてしまうような状態にしても M1Max では何の問題もありません。

f:id:ema_hiro:20220125214847p:plain

ただ、バッテリー長持ちと言われてる M1Max でさえそこそこの速度で電力を消費していきます。
とはいえ気になるといえばそれくらいです。
必要機材には M1Max 推奨と記載しましたが、もし快適なアバター生活を Mac で楽しみたいなら M1Maxは必須 、いや M1Max は人権 と言っても過言ではないでしょう。

2021年上半期をふりかえる

Overview

7月も上旬が終わってしまったけど、2021 年半分が終わったので振り返りをしてみます。

Good

仕事忙しい

いいのか悪いのかわかりませんが、引き続き仕事は忙しいです。
まぁ暇よりはずっといいです。

2020年年末以下のようなエントリーを書いたがありがたいことにこのブログに載せてたことや、そのあとにやったことが評価されていい感じにお賃金も伸ばしてもらいました(多分キャリアの中で1番伸びたんじゃないかなと思います。)
辞めちゃった前の上長にはめちゃくちゃ死ぬほどこれでもないってくらいバチクソ感謝してます。

ema-hiro.hatenablog.com

ただ、コードを書く時間は相変わらず増えてないですし、さらに減った感あります。
コードを書く前作業に時間を使うことは多かったり、プロジェクトをまとめつつ自身はプロジェクト外の案件をワンマンアーミーでやってたりして、2020年と比べると、いろんなコンテキストを持つ案件が常時3~4 つくらい並行して走ってて、それら全部の技術的な取りまとめをしてることがほぼ、みたいな感じでした。
おかげで日中は案件関連に関わる業務をこなして自分の時間が取れたと思ったらもう17時!!みたいなことが日常茶飯事になってました。

さすがにしんどかったのでプロジェクトのメンバーに技術的なところだけでなく、要件周りも関係者とコミュニケーションとって決めてもらうことは増やしました。その代わり、自分はその大前提となる背景だったり、「なんでそれするんだっけ?したいんだっけ?」みたいなところを調整することにフォーカスしてました。

これはこれで仕事の進め方としてはよかったですが、これは楽しいところは手を離し、よりしんどいところばっかりにフォーカスするようにしたことと同義なので、ストレスは増しました。

あと現職だと僕は結構ドキュメント警察なんですが、コンフルだとやっぱりしんどいのかなーと思って、開発メンバーは全員 Figma でやりとりしてもらったりとか、「それをみる人」のスコープに合わせて、ツールの選定を変えたりとかしました。
あと、なんかずっと気になってた Slack のスレッド使いについて実験をしてみたりとか、この手のプロセスやコミュニケーションに関わることを小さくはじめて色々検証してみる、みたいなことをしてました。

ema-hiro.hatenablog.com

僕は結構こういう開発プロセスとかに興味があるので、ここはもし機会があれば色々議論できる人がいたらしてみたいな、と思いました。

家電の入れ替え

2021 上半期でちょうど一人暮らしをして 10 年が経過したので、いい機会だと思って白物家電(冷蔵庫と洗濯機)を買い替えました。
冷蔵庫は大型に、洗濯機はドラム型洗濯乾燥機にしました。

これでわかったのは、白物家電QOL に直結し、暮らしのレベルを根底からグレードアップさせてくれる素晴らしいツールだったということです。

自動製氷は水出しコーヒーをやるには絶対必要だし、乾燥機があると洗濯の回数が段違いになり、いつでも清潔な衣服を着れます。
特に洗濯乾燥機にしたことで、寝具の洗濯頻度が激増したので清潔な寝具で寝れるようになったのがめっちゃよかったです。男の1人暮らしだとどうしても寝具の選択は後回しにしがちなので...。

ちなみに白物家電で何を買うか検討してるの、技術選定に似ててめっちゃ楽しかったです。
特に入れ替え周期が10年スパンなので、仕事でやる技術選定より寿命が長くて頭使いましたね。10年後自分がどうなっているのか?を前提に逆算で何を買うのか決めてるあたりが楽しい。

次は掃除機と炊飯器を買い換えようと計画中です。(掃除機はもう今月か来月に買い替えるかもしれない)

積読消化

無限に積まれていった積読たちの一部消化し始めました。
社内の輪読会にも参加し始めたので、読むペースがついたのはよかったです。
一時期は1週間1冊ペースを維持してました(本当は2,3日で一冊読みたい)

読んでる本としては Product Management や技術は技術でも設計やモデリングやら、その辺の本を読むことが増えました。あとお金関連の本(決算の読み方とか)
特に Product Management 系の書籍は最近のものを読みますが、技術に関連する本は割と古典とかも読んでます。

ちょっとここ最近また忙しくて読むペースが落ちてたのでそろそろ復活させたいなと思います。

ちなみに頑張って紹介しても積まれるペースの方が早くて困ってます。
積読の金額計算したらびびりそうなので計算してません。なぜ人は読むかもわからない本をポンポン買ってしまうのか。

More

生活リズムの崩壊

「仕事忙しい」の反動として生活リズムは崩壊しました。
海外サッカーが好きなのでたびたび朝方までサッカーを見てることはありましたけど、この上半期は退勤即仮眠 -> 夜寝れなくなる -> 作業したりしつつ、サッカーの試合を見て朝方寝る、みたいな生活がデフォルトになってしまいそれが海外サッカーシーズンが終わった今も継続中です。

Slack の自分のチャンネルに AM 3時とか4時台に登校とかしてて、いつ寝てるんだ疑惑が浮上したこともあります(退勤後即です)

有給残り少ない

これはこれでめちゃくちゃいいことだけど、この間ふと有給残日数を確認したら1.5日 とかになっててびっくりしました。笑

ここまでちゃんと有給を消化してしまったことに驚きです。元々使わずに溜め込みがちでリモートなのでなおさら休まなくなったのもあって有給使わないかなーと思ってたんですが、疲れたのでとりあえず休むか、くらいのノリで使ってたり、あと現職は長期休暇シーズンに飛び日があると有休消化推奨期間になってて、まぁここまではよくある話だと思うんですが、経営層はじめ上司や周りがボンボン休むので僕も釣られて休んでしまい、気づいたら消えてました。

ちなみに現職は有給とは別にコロナ特別休暇みたいなものがここ1年くらい突如与えられたり(※)、福利厚生で家族の記念日に休暇が取れたり、気づくと有給とは別に休む日がめちゃくちゃ増える福利厚生があるので休みに困ったことはないです(これはちょっとだけ宣伝)

※ コロナ特別休暇のおかげで来たる五輪休暇は有給とは別計算で2日増えて6連休です。

英語学習たりない

今年のテーマに置いてますが、あんまり進捗はよろしくないです。

英単語とリスニングはかろうじて続けられてますが、英会話(Speaking & Listening) はてんでダメですね。
同僚に週一でレッスンつけてもらってますがこればっかりは量が勝負なのでなかなか進捗がでないっす。。。

一応進捗してるところもあって英単語と Reading & Listening は続けてるので、綺麗な英語で話すスピードが速くなければ、何を言ってるのか?わからないみたいなことは無くなってきました。あと英作文(チャットでよく使う)も、外国籍の方がどんな表現使ってるのか観察しながらほぼ見様見真似で使いつつ、ニュアンスや定型文をちょっとずつ学習して使うようになりました。これだけでも英作文考える時間が減るのですごくよかったです。
こういう生の英作文を勉強できるのは当たり前のように外国籍の方がいる環境ならではだと思います(一種の福利厚生ではこれ)

とないえ、幼稚園児レベルの英語が小学2年生くらいになっただけなのでせめて下半期で中学生くらいまではいきたいなと思って日々精進です。
(というか、筋トレと同じなので、やればやっただけ結果出るはずなんですけど、やっぱりたまにだれちゃうんですよね...)

Try

生活リズム直す

やりたいです。しかしこのエントリを3時ごろ書いてる時点でそもそも直す気ない気もしてます。

英会話やる

せっかく福利厚生でオンライン英会話レッスンがあるのでそろそろまじで始めたいです。
最近ガチでタイムゾーンが違う人たちと仕事をしないといけない時が来てしまい、それはそれでめっちゃありがたいんですが、初回のMTGで何を話しているのか、言語レベルでさっぱりわからなかったのでヤバみを感じました。

同僚にも週一でレッスンを完全ボランティアでつけてもらってるので少しでも成果を出したい...。

がんばろう...。

その他

上半期買ったものリスト

上半期も色々買ったのでその散財記録です。

洗剤自動投入付きドラム式選択乾燥機

振り返りでも書きましたが上半期一番買ってよかったと思います。
これは元同僚の IE11 だきしめおじさんの話を聞いて自動洗剤投入付きのものにしましたが、洗剤容器を置く場所も削減できたので一石二鳥でした。


冷蔵庫

念願の野菜室 & 肉魚専用チルド & 自動製氷機を手に入れたおかげで自炊や食材の保存がめちゃくちゃ改善しました。
買うときにもう一回り小さいやつも検討しましたが、現在の住んでる家が専用のダイニングがあって、キッチンも広いので思い切ってデカイ方を買いましたが、これが正解でした。
白物家電の原則は予算の範囲内で大は小をかねますね。
あと自炊派なら冷蔵庫に金を惜しむな、は言いたいと思います。


cores コーングラインダー

在宅になってからというもの、コーヒーが趣味になってしまったのでお試し電動ミルを買ってから半年でちゃんとした電動ミルを買ってしましました...
水出しコーヒーもつくれるようになったし、自粛が加速してしまってます。

ちなみに同価格帯で Wilfa やもうちょっと言ったところの NextG とも悩みましたがお手入れのしやすさで cores にしました。


RICHO GRⅢ

念願のカメラをようやく買い替えました。
コロナで外出減っちゃいましたが、ちょっと外に出たときとかに持っていけるのでいい買い物でした。
尊敬してるカメラマンたちもこぞって使って、SONYVlog カメラとも悩みましたが、やっぱりカメラやるなら GR かなと思って GR にしました。

CalDigit TS3 Plus

デスク周辺機器を増やすにつれてコードが増えまくってしまったのと、Mac に接続しないといけない機器が増えてしまったので Dock をもともと買おうと思ってて、変な拡張を買うくらいならガチなやつがいいかなーと思って berkin のやつと悩んでこっちを買いました。
違いは分かりませんが、CalDigit は Apple の公式アクセサリーにもなってるので互換性も問題ないのかなと思って CalDigit を選択しました。
デスクの配線がスッキリしたのでとても満足してます。

Anker PowerWave 3in1

最近Nomadの充電器が壊れたので安心と信頼の Anker に乗り換えました。
無線充電器は便利ですが、どこまでいっても有線には敵わんな、というのが個人的な感想です。これは Apple Watch の Dock も備えてるので便利です。

Anker PowerConf C300

洗濯機に次ぐ上半期ベストバイは多分これかなと思います。多分年間通しても買ってよかったものリストに入ると思います。
これも安心と信頼の Anker。
とにかくコストと品質が全く釣り合ってない(コスパが最高)現時点で WebCam 買うならこれ一択、という商品かと。

Anker Magnetic Cable Holder

これも Anker。
ガジェットの Anker の生活用品の山崎実業は EC で外れないブランド二大巨頭な気がします。

デスク状のコード類をすっきりさせたくて買いました。何度も貼れるシール式なので机を傷つけることもなく配置を変えられるところもお気に入りです。
これもこのままいくと今年買ってよかったものリストに多分入ります。

CIO モバイルバッテリー

たまたま見つけたんですが、高速充電対応してる小型の充電器です。
コロナで出かけなくなったのでモバイルバッテリーが必要な機会が減ったような気もしますが、もともと PC も充電できるごついやつをいつも持ち歩いててめっちゃ重たくて疲れたので、これくらい小さくて容量もあるやつを探してたのでベストマッチでした。

DynamoDB のリクエストで Context Canceled をハンドリングする

Overview

タイトルの通りです。 DynamoDB へのリクエストにおいて Context Cancel エラーになった場合にその Context Cancel をハンドリングします。

DynamoDB 側でのエラーハンドリング

DynamoDB へのリクエストにおいて Context Cancel の実装は https://pkg.go.dev/github.com/aws/smithy-go#CanceledError.CanceledError でされています。

CanceledError returns true to satisfy interfaces checking for canceled errors.

Godoc にも canceled の時に返されると記載されているので、最終的にこの Context cancel エラーをハンドリングしたい箇所で取り出してハンドリングできれば良さそうです。

コードを追っかけていきます

実際に SDK のコードを追っていきます。

Query() 関数をサンプルとして見ていきます。

Query 関数の内部で invokeOperation を 呼び出していて、この invokeOperation の中での smithy-go 内部にある middleware.DecorateHandlerHandle メソッドを Call します。このい Handle メソッドで発生してエラーは invokeOperation メソッドでは OperationError として返されます。 Handle メソッドのエラーが OperationError に入れられて返されることになります。

次にこの Handle の実装以下を追っていきます。

この Handle メソッド内部で Middleware に実装された HandleMiddleware を Call しており、これは BuildStep に実装されたHandleMiddleware を Callします。
さらに、この実装の内部で buildWrapHandler が実装してる HandleBuild を Call しており、HandleBuild 内部の buildWrapHandler.Handler に実装されている Handle メソッドで実際に AWS 本体と通信してる HTTP Client に実装された Handle メソッド の内部で、smithy-go.CanceledError返してる箇所があります。

これがエラーを返してる本体です。

実際のエラーハンドリング方法

いくつかやり方があると思います。一番単純なのは Dynamo から返されたエラーの文字列の中に context canceled が含まれていればヨシ!とする方法です。
ただし、エラーの文字列一致はそれじゃないと対応できない場合を除いて極力採用するべき方針ではありません。

できることであれば Go のエラー方をそのまま使って errors.Is でハンドリングしたいです。Go のエラーはエラーが返される過程で fmt で wrap されている or 独自エラーで wrap されている限り errors.Is でエラーの判別をすることが可能です。

※ どこかでエラーを書き換えてしまってる場合、正常にハンドリングできません。

実際にエラーが取り出せるかをキャストでやって見ました -> https://play.golang.org/p/KPPzJgqspIk
これは trueが返ってきました(つまり context canceled が発火したということ)

次に errors.Is でハンドリングできるかやって見ました -> https://play.golang.org/p/vT4anMduCL7
※ 余計なエラーで書き換えてない場合のみ。途中に errors.New があったりして元々のエラーからさらに書き換えていると Is で取り出すことはできません。

まとめ

エラーハンドリング、、されどエラーハンドリング。
安易に文字列比較に逃げることなく、Error が定義されてるはずだ、と見通しを立てて実装するって大事だなと思いました。

追記

AWS のリクエストを送信する invokeOperation メソッドが返す smithy.OperationErrorUnwrap実装してる ので、AWS 内では発生した何かしらの OperationErrorerrors.IsUnwrap して取り出すのが良いです。

OperationErroroperation error {$ResourceName}: {$OperationName}, https response error... という文字列で返ってくるエラーのことです。

dynamodbattribute には omitempty が使える

ただの備忘録です。

dynamodbattribute には omitemtpty タグが使えてStruct の中でゼロ値になってしまうところを無視したい(Item の attribute として登録したくない)ケースにおいては使えるなと思いました。

https://docs.aws.amazon.com/sdk-for-go/api/service/dynamodb/dynamodbattribute/#Marshal

The omitempty tag is only used during Marshaling and is ignored for Unmarshal. Any zero value or a value when marshaled results in a AttributeValue NULL will be added to AttributeValue Maps during struct marshal. The omitemptyelem tag works the same as omitempty except it applies to maps and slices instead of struct fields, and will not be included in the marshaled AttributeValue Map, List, or Set.

Loop 内の値渡しと参照渡しで久しぶりにハマった話

Overview

Go の loop 処理の中で Slice の中の全値を書き換えたいと言う処理、において参照渡しと値渡しを取り違えて、意図した処理をできずにバグを生んでしまったのでその懺悔のための備忘録です。

何が起きたか?

以下の処理でSliceの全ての ID フィールドを書き換えることはできません。

type X struct {
    ID int
}

func main() {
    x := []X{{ID: 0}, {ID: 0}, {ID: 0}, {ID: 0}}
    for _, v := range x {
        v.ID = 1
    }
    fmt.Println(x)
}

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

以下の処理では値を書き換えることができます。

type X struct {
    ID int
}

func main() {
    x := []X{{ID: 0}, {ID: 0}, {ID: 0}, {ID: 0}}
    for i := range x {
        x[i].ID = 1
    }
    fmt.Println(x)
}

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

原因: 実態のコピーを上書きしていたので大元の slice の struct の値が書き変わらなかった

1 つ目の実装は struct の slice なので loop の中の v は実体がコピーされて毎回同じメモリ空間に同一の値を書き込み続けますが、これは実体のコピーなので書き換えることができません。
要素を struct の参照にすれば書き換えることができます。

参照型の slice じゃないパターンでは各 loop におけるポインタが不変でそこに対して値を上書きしてしまっています。ただし、この場合、loop 内の struct は全く別のものとして扱われ、別参照に対してずっと値を上書きしようとしてるに過ぎず、元の slice の中身は書き変わらりません。
参照型の slice だと書き変わるのは loop 内で参照する値が参照型で、それが指し示す先の slice も書きかわります。
(これが元で参照の slice の loop の中で値の書き換えを行うと、全部同じ値になってしまうのがいわゆる for loop pointer 問題です。)

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

実際に playground での挙動は以下です。

カスタム struct の slice の場合は別参照に対して書き換えていることがわかり、大物 slice は何も更新されていないことがわかります。

https://play.golang.org/p/k5HOsjGR--b

一方で Index を指定したときは以下です。

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

大元の slice の各要素のポインタに対して値を書き換えています。

まとめ

久しぶりに初心者みたいな実装ミスして、改めて slice の loop 内での挙動を確認してみました。

追記

20210531

https://github.com/golang/tools/blob/master/gopls/doc/analyzers.md#unusedwrite を教えてもらいました。

The analyzer reports instances of writes to struct fields and arrays that are never read. Specifically, when a struct object or an array is copied, its elements are copied implicitly by the compiler, and any element write to this copy does nothing with the original object.

今回みたいな original の slice に上書きできないケースを検知して警告してくれます。早速 VSCode で設定しました。

"gopls": {
        "analyses": {
            "unusedwrite": true, // NEW
        },
}

DynamoDB の Query with FilterExpression の挙動を調べた話

Overview

DynamoDB の Scan の挙動について以下のエントリで調べましたが、FilterExpression を指定した場合の Query の挙動について実際に API で Query を叩いた場合と、AWS Management Console で表示したときで挙動が異なっていたので、ついでに調べた備忘録です。

ema-hiro.hatenablog.com

どういう挙動だったか

ある GSI を Key に指定し、Filter Expression を追加して Query API を叩いて取得した Item の Count 数と AWS Management Console 上で返ってくる Item の個数が、同じ Filter Expression を使っていた、同様のクエリのはずなのに異なっていた、という挙動です。

Conclusion

結論から先に言ってしまうとこれは Scan について調べたときと全く同じでした。
Scan のときと同様、AWS Management Console 上で DevTool を開いてレスポンスを調べた結果が以下です。

f:id:ema_hiro:20210521040042p:plain

どういうことかというと、Filter した結果が Count に入っていて、Count で指定した分を UI でリクエストされる分だけ繰り返し Fetch してた、ということでした。

DynamoDB のクセ的なもの

ここで分かったのは Scan にしろ、Query にしろ、ある母集団の結果を返す、という挙動は同じであり、それが全件走査結果なのか、Partitioning された結果なのかの差は関係なかったです。

そしてもう1つは DynamoDB の Filter Expression は文字通り フィルタ であったことです。
1リクエスト当たり取得した母集団に対して、DynamoDB 側でフィルタをかけて、要素数間引いてる だけでした。言ってしまえば、クライアント側で絞り込みを実装してるようなものですね。それを DynamoDB の API の中でやってくれている、という挙動でした。

Filter Expression はフィルタであってクエリでない、この理解を間違っていて、意図した結果をクライアントに返せない実装になってしまってました。

DynamoDB について学んだこと

ここ数日 DynamoDB を永続的なデータストアとして使ってるケースにおいて、色んなクエリを叩けるわけではなく、基本的に全件走査を避けつつ、要求される仕様の中でどんなクエリが叩かれるのか?言い換えると、そのユースケースを実現するためのクエリのパターンは何か?ということを DynamoDB を使う上では常に意識する必要がある、ということです。

全然柔軟じゃないなーと思う反面、何かユースケースを追加する依頼が来たときにどれくらいの本気度でユースケースを考えているのか?を確認するモチベーションになります。どれくらい雑でいいのか?ちゃんと厳密に色んなケース(それこそクエリだったり、結果として返ってくるデータセットの並び順など含め)を想定しないといけません。

DynamoDB は GSI に PK/RK というかなりキツイ制約があるので、このすり合わせのモチベーションを高く持てるのがいいところだなと思いました。DynamoDB に限らず、NoSQL だったり NewSQL と呼ばれるものを使う場合は、ユースケースを実現するためのクエリパターンをデータ設計の段階から頭に入れておくことが必須ですね。

DynamoDB の Scan の挙動がわからなかったので調べた話

Overview

DynamoDB の Scan 操作について AWS のマネジメントコンソールと API 直叩きで返ってくる List が違う、ということがあったので調べてみました。

どう違ったのか?

以下のような事象が起きてました。

  • AWS マネジメントコンソール上からあるテーブルに条件 (Filter) を追加して Scan 操作をすると 50 件返ってくる。
  • 同じ条件で AWS CLI から Scan 操作をするとデータが返ってこない。

何が起きていたのか

多分 AWSCLI(要は API 直叩き)の方が一度の Scan 結果としては正しいと思ったので AWS マネジメントコンソールで何が起きてるのか DevTool のネットワークからわかるかなーと思って徐にみてみたところ

f:id:ema_hiro:20210513124746p:plain

マネジメントコンソールで50件表示されていたのは 50 件取れるまで何度も操作をし直していたからでしたw

つまりマネジメントコンソール上では、Scanの操作に対して UI 上は 1 度しか叩いてないように見えても実態は

  1. 一度APICall投げる(countに達しない)
  2. もう一回 Call投げる

1->2->1…で count に達するまでやり続ける、ということをしていたようです。node に分散してるデータを片っ端から取りに行って View で表示してくれてるだけでした。
考えてみれば当たり前なんですがw。目的の FIlter したい条件を一発でとってくるには Index 設定する他ないですね。

ぼやき

ここはぼやきですが、ずっと GCP の Datastore/Firestore に慣れ親しんできて DynamoDB を触るようになって半年くらい経ちますが、全然同じ感覚で使えない(運用してみて、どちらかというと RDB 触っていた頃に感覚としては近い?)なぁと最近感じてます。
良し悪しがあるので、どっちがいい、という話ではないですが、Index の考え方だったり、最低限のクエリ叩く方法だったり、GCP の方が直感的な感じはありますね(個人の感覚です)

終わり。

DynamoDB で Update GSI しようとしてやらかした話

Overview

タイトルの通りなのですが、DynamoDB の Global Secondary Index(以下 GSI) を更新するにあたり、更新方法でミスったのでことの顛末を備忘録として記載しました。

何をしたのか?

terraform で DynamoDB の構成を変更する、今回はすでに運用されてる DynamoDB の Table に対して GSI を更新するというオペレーションを実行しました。

resource "aws_dynamodb_table" "hoge_table" {
  hash_key     = "$HashKey1"
+ range_key    = "$RangeKey"
}

+attribute {
+   name = "$RangeKey"
+   type = "S"
+}

global_secondary_index {
   name            = "AlreadyExistIndex_1"
   hash_key        = "$HashKey1"
+  range_key       = "$RangeKey"
   projection_type = "ALL"
}

global_secondary_index {
   name            = "AlreadyExistIndex_2"
   hash_key        = "$HashKey2"
+  range_key       = "$RangeKey"
   projection_type = "ALL"
}

terraform の定義ファイルとしては上記のような感じで構成して terraform apply しました。

何が起きたか

GSI を更新するときに Index の recreate が発生し、すでにアプリケーションで使用していた AlreadyExistIndex_1 を Key にした Query オペレーションが ValidationException: The table does not have the specified index: AlreadyExistIndex_1 status code: 400 ... で落ちてアプリケーションが正常に動作しなくなりました。

どうして起きたか?

GSI を複数?(複数が関係してるかどうかわかりませんが、)更新するにあたり delete -> create の順で Index の更新が発生し、先に AlreadyExistIndex_1 と AlreadyExistIndex_2 が削除され、改めて Create されるというオペレーションにおいて AlreadyExistIndex_1 をアプリケーションが使用していたために、Create してる最中にもアクセスしてしまい 400 エラーが発生してしまいました。

公式のドキュメントにも

You can create or delete only one global secondary index per UpdateTable operation.

ref: https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_UpdateTable.html#API_UpdateTable_RequestParameters

とあり、更新がタイミングごとに GSI の Delete と Create のオペレーションは1度ずつしか呼ばれません。

これだけだと即時で入れ替えがされそうにも読めますが、今回の挙動とこのドキュメントを参照する限り GSI の更新にあたり UpdateTable で既存の GSI を更新するときにはまず先に Delete Operation が実行され、その後に Create がされるようです。その後に Create がされるようです。つまり Terraform Apply で複数の Index を更新する場合、処理としてはそれは可能だけど既存の Index を全部先に削除(= 更新対象の 2 つとも削除)し、その後新しく Index を生成する、という挙動になるようです。

先に全削除が走ってしまうため、削除したタイミングで AlreadyExistIndex_1 をアプリケーション側で参照していたために、アクセスしてしまいエラーが発生した、と言うのが一連の流れでした。

今後どうすればいいか

すでに運用されてる GSI の更新をする場合には以下の手順で行うといいと思います。

  1. 変更先となる Index を新しい Index として定義して先に Create。
  2. aws dynamodb query ... コマンドを使って新しい Index が意図する動作になってるか確認する。
  3. アプリケーションの参照してる Index を新しい方に変更。
  4. 更新前の Index を Delete。

RDB を運用してる時と似てますね。

まとめ

定期的にやらかすことがあるんですが、今回は久しぶりに知識として知らないまま操作を行なってしまって焦りました。
DynamoDB についてまた一つ詳しくなることができました。

Lambda の同時実行数を制限する

Overview

タイトルの通りです。
久しぶりに Lambda on Serverless Framework を触っていて忘れていたので備忘録です。

Conclusion

Lambda の同時実行数を制限するには Reseved Concurrency の設定を 1 にします。
Serverless Framework は function の reservedConcurrency の設定で同時実行数を調べます。

www.serverless.com

Lambda の同時実行回数について

ReservedConcurrency と ProvisionedConcurrency について

以下のドキュメントにその答えが書いてあります。

docs.aws.amazon.com

Reserved concurrency – Reserved concurrency creates a pool of requests that can only be used by its function, and also prevents its function from using unreserved concurrency. Provisioned concurrency – Provisioned concurrency initializes a requested number of execution environments so that they are prepared to respond to your function's invocations.

Reserved concurrency は Lambda (の起動)で使用することができるリクエストのプールを作成し、予約されていない並行のから Lambda の起動を防ぐことできます(= Lambda の並行処理を制御できる )

対して、Provisioned concurrency はリクエストされた実行環境においてあらかじめて Lambda の発火に対応できるように環境を用意しておくことです。

同時実行回数については上記の説明から、reserved concurrency を利用すればいいことがわかりますが、さらに以下で詳細に説明されています。

To ensure that a function can always reach a certain level of concurrency, configure the function with reserved concurrency. When a function has reserved concurrency, no other function can use that concurrency. Reserved concurrency also limits the maximum concurrency for the function, and applies to the function as a whole, including versions and aliases.

並行性のレベル(程度)に達したいとき(= どれくらい並行で実行するのか制御したいとき)は reserved concurrency を利用します。関数は reserved concurrency が設定されてる時はその設定された並行数以上に発火することはありません。reserved concurrecy は最大並行実行数の上限であり、version やエイリアスに渡って全ての Lambda に適用されます。

まとめ

久しぶりに触って色々忘れてましたが、とりあえず迷ったら Serverless Framework の設定を見る前に Lambda の GUI の設定を見ろ でした。
もちろん生の Lambda の設定が Serverless Framework 上でどうマッピングされているのかは理解しないといけませんが、Serverless Framework は Lambda の設定を透過的に使えるものであって、いわばラッパーなので大元を見ないといけないですね(何を当たり前のことを...)

その他

全然本論と関係ないですが、SQS で Lambda を Trigger をするときに Lambda 側でエラーを返さない(=正常終了した)場合に限り、SQS に Queue は戻され成功する(or Queue の有効期限が切れる)までなども Lambda が実行されますが、その Worker で拾われる周期は Visibility Timeout が切れるまでです。
Visibility Timeout 以内であればエラーを起こしても Worker では拾われません。

chi で独自の middleware 使う

Overview

chi で独自 middleware を使う上で使い方を忘れていたのでその備忘録。

備忘録

router.Use(func(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // middleware でやりたい処理
        next.ServeHTTP(w, r) // これがないと次の router に処理が伝播しない
    })
})

middleware 内部で ServeHTTP しないと実際の処理をしてる handler まで処理が伝播しないことを忘れていて時間を溶かしてしまった。

ちなみに以下のような感じで前処理と後処理に分けることができる。

router.Use(func(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // handler の前処理
        next.ServeHTTP(w, r)
        // handler の後処理
    })
})

AWS Go SDK V2 を使ってみる

Overview

AWS Go SDK の V2 が GA になったので早速業務の中で使ってみてハマったところ、V1 との違いでつまづいたところを中心にまとめます。

基本的には https://aws.github.io/aws-sdk-go-v2/docs/migrating/ にまとまっていますが、色々わからないところがあったので補足しながら記載します。

AWS SDK for Go V2 について

1月に GA になっています。

aws.amazon.com

github.com

AWS の設定をロードする

一番最初からなんですが、一番ハマったのはここでした。

AWS SDK for Go を利用する場合、AWS の認証情報をとって設定をロードし、それを各サービスのクライアントに当てはめて AWS の各サービスの API を Call することになります。

単純な Configuration のロードと V1 → V2 へのマイグレーションについては https://aws.github.io/aws-sdk-go-v2/docs/migrating/#configuration-loading に記載されている通りです。 特に独自に環境変数を持っていないケース(AWS Lambda とか?)ではここのマイグレーションドキュメントの通りに実装すると良さそうです。 (Lambda ではまだ実装してないので多分ですが。そのうち Lambda でも使おうと思います。)

独自で環境変数を持ってる場合

自前で AWS環境変数を設定してるケースでは、https://aws.github.io/aws-sdk-go-v2/docs/migrating/#credentials--credential-providers に書かれている Credentials を取り出してから Config を Credentials 付きで Load します。

id := os.Getenv("$YourAccessKeyID")
secret := os.Getenv("$YourSecretKey")
// AWS_SESSION_TOKEN はそのまま取り出してOK
cred := aws.NewCredentialsCache(credentials.NewStaticCredentialsProvider(id, secret, os.Getenv("AWS_SESSION_TOKEN")))
if cred == nil {
    panic("failed to fetch credentials provider")
}
cfg, err := config.LoadDefaultConfig(ctx, config.WithCredentialsProvider(cred))
if err != nil {
    panic(err)
}
// => ここで load した config を AWS の各サービスに入れて使います。

DynamoDB を使う

仕事で使ったのは DynamoDB だったので DynamoDB を例にとって V2 を使った DynamoDB の操作方法について記載します。

DynamoDB の使い方のサンプルは公式が以下のドキュメントを公開しています。

aws.github.io

DynamoDB クライアントを Load する

これは割と簡単で上記で取り出した config 情報を DynamoDB のクライアントに差し込みます。

dynamodb.NewFromConfig(cfg)

リソースを更新する

サンプルとしてリソースの更新する実装は以下です。

update := expression.Set(expression.Name("key"), expression.IfNotExists(expression.Name("key"), expression.Value("val")))
// SET a = :a, b = :b のように複数 key を更新したい場合は以下のように Set を追加します。
update = update.Set(...)

expr, err := expression.NewBuilder().WithUpdate(update).Build()
if err != nil {
    return err
}

input := dynamodb.UpdateItemInput{
    TableName: tableName,
    Key: map[string]types.AttributeValue{
        "$PK": &types.AttributeValueMemberS{ 
            Value: "$PKValue",
        },
    },
    UpdateExpression:          expr.Update(),
    ExpressionAttributeValues: expr.Values(),
    ExpressionAttributeNames:  expr.Names(),
    ConditionExpression:       expr.Condition(),
}

これに対応する aws dynamodb のクエリは以下です。

aws dynamodb update-item \
--table-name $tableName \
--key '{"$PK":{"S":"$PKValue"}}' \
--update-expression "SET key = :val" \
--expression-attribute-values '{":val":{"N":"1"}}' \
--return-value ALL_NEW

ちなみに --key に GSI を指定できないことに気づかずだいぶ時間を溶かしました。ドキュメント読んでて Id を指定しているのをみて気づきました。

※ クエリの表現は以下に大体書いてあります。

docs.aws.amazon.com

expression はまだ feature だった

V1 で存在していた dyanmodb/expression が V2 だと最初見つからずに、愚直に expression を書くしかないのかなーと思ってたのですが、feature package の中にいました。

余談: 毎回めんどいなーところと思うところ

DynamoDB のクライアントの実装してて毎回 aws コマンドぽちぽちしながら一個一個 expression のフィールドに当てはめて実装していくのが結構めんどいなーと思ってます。

毎回こんな感じでちまちま aws dynamodb コマンドを叩いて少しずつ実装をしてます。

aws dynamodb query \
--index-name $indexName \
--table-name $tableTame \
--key-condition-expression "key = :val" \
--expression-attribute-values '{":val":{"S":"$Value"}}'

Transaction について

DynamoDB のトランザクションについてまだちゃんと見てないですが使う機会がありそうなのでその時に調べようと思います。

ざっと調べた感じ例えば Put の処理に対して Transaction を貼る場合はhttps://github.com/aws/aws-sdk-go-v2/blob/67f74949e4831edc2d0a8da2aba8c4b356b27fff/service/dynamodb/api_op_TransactWriteItems.go#L95 の TransactionItems に Put したいテーブルの Attributes を突っ込んでいって一括で処理するっぽいですね。

Put の中身自体は https://github.com/aws/aws-sdk-go-v2/blob/67f74949e4831edc2d0a8da2aba8c4b356b27fff/service/dynamodb/types/types.go#L1876-L1905 の構造体に定義があるのでこれを一個一個作って突っ込んでいくのが良さそうです。

以下で仕様周りのことは記載してあります。

docs.aws.amazon.com

ここはまた実装してみた後にまとめたいと思います。

See Also

strings.Split は分割できない時でも長さ1の slice を返す。

https://play.golang.org/p/ss6gWvidE2u でなんで長さ 1 の slice が返ってくるんだろーって悩んでたのですが、、、

// Split slices s into all substrings separated by sep and returns a slice of
// the substrings between those separators.
//
// If s does not contain sep and sep is not empty, Split returns a
// slice of length 1 whose only element is s.
//
// If sep is empty, Split splits after each UTF-8 sequence. If both s
// and sep are empty, Split returns an empty slice.
//
// It is equivalent to SplitN with a count of -1.
func Split(s, sep string) []string { return genSplit(s, sep, 0, -1) }

Godoc にもちゃんと 1 が返ってくるって書いてあるし、そもそも https://pkg.go.dev/strings#Split はsep に指定した文字列で分割できない時は s に指定した文字列を slice にしてそのまま返す、って言う当たり前の振る舞いを忘れてただけでした。

Slack のスレッド利用「原則非推奨」という実験をやってみた

Overview

僕の担当してるプロジェクトの Slack チャンネルで1週間限定で スレッド利用原則非推奨 というルールを作って運用してみた振り返りです。

プロジェクトに関わるメンバーに協力してもらって以下のルールで1週間 Slack を利用してみました。

  1. 全ての投稿はチャンネルへの投稿する。ある依頼への返事やMTGに遅刻してる人を呼ぶDM的な使い方も含む。
  2. Slack の流量が多くなることが想定されたので、気づいて欲しい時は臆せずメンションをつけること。

他部署や他プロジェクトの人に実験を強制するわけにはいかないので、その場合は例外的にスレッドの利用を許可しました。ただし、プロジェクトメンバーがスレッドで発言するときは also send to $Channel を必須にしてもらい、回答がチャンネルに流れるようにしてもらいした。

背景

そもそもこんな実験をしようと思った背景についてですが、僕は Slack に「スレッド機能」が登場した当初から Slack には不要な機能だなと思っていました。この点に関しては、もしかしたら同じような考えを持ってる人が一定いるかもしれません。

Slack (に限らず、こういったチャットベースのコラボレーションツール) に対しての僕の基本的な考えは以下です。

  • Slack は情報のフローであって情報をストックするツールではない。
  • スレッドは通常のチャンネルから UI 上 1 階層深いところにあるため、大量のチャンネルに入っていたり、プロジェクトを跨いで仕事をしてる人(要は忙しい人)ほど、深いところの情報を追うのが億劫になって見なくなりがち(僕はできることなら見に行きたくない)
  • 見るのが億劫になることで、意思決定する人が検知しないところで話が進んでしまう、みたいな潜在的なリスクを孕んでいる。
  • スレッド内で話される内容は当たり前だがコンテキストが高い内容が多くただでさえ、自身の意識を奪われがち、かつ会話の開始タイミングから間が空いてるものを急に掘り返されたりするケースがあるとコンテキストスイッチが変えるコストに加えて思い出しコストが乗ってきて、マジでキツい。
  • 「情報は流れていってしまうもの」という認識をチームで統一することで、ドキュメントに残す(情報を別の場所にストックする)インセンティブ(※)をより働かせる。

※ 僕が担当してるプロジェクトではプロジェクト間で共有するべき情報はコンフルに、プロジェクト内の開発に関係するメンバーは Figma(or Whimsical) に仕様・議論の経過などを一元的にストックするようにしています。

振り返り

3/4~12 までの1週間、このルールを回してみて、昨日ちょうど振り返りをしたので、協力してくれたメンバーのフィードバックをまとめると大体以下のような感じでした。

Good

  • プロジェクトに関連する情報がチャンネルのファーストビューに流れるので、拾い読みがしやすく、ながら時間でのキャッチアップがしやすい。
  • ハイコンテキストな会話が他部署とのやり取りに制限されて負担は減った。
  • 考察とかの会話重ねて練度上げていく投稿がスレッドじゃなくてチャンネル上に存在するので、チャンネル閲覧者の情報理解度が上がった。
  • ざっと、他の人が何しているかわかり透明性が高くなった。
  • スレッドに潜らなくても主要なやりとりは可視化された。

etc...

More

  • スクロールが長い。
  • チャットがあるアジェンダで盛り上がってるときに別の話題で投稿しづらい。
  • 数往復程度のやりとりや単一のアジェンダであればスレッドにするまでもないが、議論の足が長かったり、複雑な内容の会話になる場合はスレッドの方が向いてる。
  • 他部署絡むとスレッド作った方が効率的な場合がある。
  • Slack の機能的な制約により厳しい側面がある(発話元リンク投下しても見づらいし、検索機能がいけてないので後追いに時間がかかるなど...)

etc...

以上が振り返りです。

個人的な所感

Good/More を元にして個人的に想定していたこと、想定していなかったことについて記載します。

まず振り返りをして思ったのは、期間限定とはいえコミュニケーションの仕方をある種変えることを強制することになるので、反発やストレスがあるかなと思ったんですが、プロジェクト内のメンバーから概ねポジティブなフィードバックが多かったのが意外でした。
この辺はもしかしたら普段から大量の情報に触れてたり、ある程度今何がどうなっているのかを主体的にキャッチアップしようとするメンバーが多いという恵まれた状況にあったからかもしれませんが、多少の混線やUI的な見づらさはあれど、致命的にネガティブなものはなく、キャッチアップコストの低減と透明化の観点からはスレッドに使わずチャンネルにどんどん投稿した方がいいのかなと思います。事実、チャンネル投稿を推奨しても生産性が大きく削がれた、というフィードバックはありませんでした。

スレッド自体も機能の1つなので、発話者が使った方が良い、と思えば使うことはいいと思います。あえて強制力のあるルールを作る意図もありません。 ただ、迷ったらチャンネルに投稿しておく方が無難だし、公開したい範囲を狭める必要がないならチャンネルに投稿しておくのがいいと思います。

こういうコミュニケーションについて考えているとやたら関係者や話題と絞りたがる欲求が強いなと感じる場面が多いのです。ただ、情報の取捨選択は受け取る側でやるべきことなので、とりあえず迷ったら垂れ流しておいてくれる方が助かる場面は多いです。
Good にも記載してますが、「拾い読み」の効用は大きいなと感じるケースは多いです。たまたま気になるトピックを見つけたことから話が広がってアイデアに繋がることや、逆に事前にブロッカーを取り除けたり、リスクに気づけたりする機会も生まれたりすることは、僕は貴重だと思います。

何よりリモートが前提となってる今だからこそ、チャンネルを賑やかにさせる空気感?みたいなものを作るためにも、どんどんチャンネルに投稿した方がいいのでは?という気すらしてきます。

そういえば、この実験してる最中に Slack を作ってる Slack 社では Slack をどのように使っているのか?とナイスタイミングでまとめてくれてるエントリがありました。

qiita.com

なお、エントリの中で引用されているメルカリのガイドラインは本当に秀逸で、Slack を使う場合は全てこのガイドラインが最低ラインであり基準になると思っています。

mercan.mercari.com

コミュニケーションコストについて

ここで改めてコミュニケーションコストというところについて考えてみたいと思います。

チャンネルへのそのまま投稿にしろ、スレッドにしろ、目指したい姿は 実行のスループットを上げるためのコミュニケーションコストの低減 であるべきです。

ハイコンテキストな内容であれば、関係者間で話を進めた方が早いでしょう。それは事実だと思います。ただし、それはともすると公開される場所に置いて意図的に部屋を分ける行為(言い方を変えるとある種の密室状態)になります。Slack のスレッドは密室じゃない、という意見もありそうですが、1階層深いところで話されている時点で本来知るべきメンバーにも公開されていない or 自分で取りに行かないといけないわけで、それが密室でないとは僕自身は思いません。
ある程度話が進んだ段階で、あとから部屋に呼ばれた時の「いやそれは聞いてねーよ」みたいなことが発生する可能性を孕んでることが、本当に目指したかったところに繋がってるのかは個人的には疑問です。
僕自身もそこまで多くはないですが、全然違う仕事をしていたときに、いきなりかなり会話の進んだスレッドに巻き込まれて、話について行くのに時間がかかったことが、この1年だけでも何回かあります。

こういうことが起きそうだなーと考えたとときに、「誰でもみれる場所に大量に流れている情報から必要なものを取り出し整理する状況」と「準密室でコンテキストが限定された会話の内容を自分で取りに行かないといけない状況」を比較したときに、僕自身は前者に振る方が全体としてはコミュニケーションコストが減ると考えています。

最後に

個人的には全ての情報がチャンネルに垂れ流されてるという状態は、情報のキャッチアップコストの低下、及びプロジェクト内の透明性の観点からありがたかったのですが、一方で大量の情報から必要な情報をぱっとピックアップして、整理できるというのは、それ自体が個人の能力に依存したスキルの1つと捉えるようになりました。
ありがたいことに、スキルは後天的的につけることが可能です。

そしてこの大量の情報の流れの中から自分に必要な情報や気になる情報をパッと取り出すスキルの習得にうってつけなのは 間違いなく Twitterです。
仕事の生産性向上のために、業務中とか関係なく全力でTwitter をやって行きましょう。Twitter するのも仕事のうちです。

osimai

参照型変換メソッドを func で表現する

Overview

タイトルの通りなんですが、Go の struct をするときにその struct のあるフィールドを参照型にすることがあると思います。
この参照型のフィールドに値を代入するために、プリミティブな型を参照型に変換するだけの関数を作成することがあるかと思いますが、そのために unexported な toIntPtr みたいな関数を作成するのではなく func を使って代入することができるなーと思ったのでそのことについて書いてみます。

どう言うときに使うの?

主にテストを書くときのことを想定しています。

例えば、時刻を扱う表現で MySQL の DATETIME 型の初期値を NULL にしたいケースではフィールドの型を *time.Time にするケースは多くあります。こういった struct を使ったテストをするときにシンプルに時刻を生成してフィールドにアサインしようとするとコンパイルエラーが発生してしまいます(※)

time.Time*time.Time は別の型なので。

こう言うときにわざわざ参照型の変数をフィールドに代入するためだけに toDatetimePtr みたいなメソッドを定義することがありますが、実装で使うならまだしもテストの時だけに使うためにこういった参照型変換関数を用意するのは微妙です。

Example

type User struct {
    BirthDay *time.Time
}

みたいな User エンティティを使ったテストを書くときに

func TestUserXXXX(t *testing.T) {
    tests := [] struct {
        name string
        src  User
    }{
        {name: "xxxxx....",  src: User{ BirthDay: toDatetimePtr(time.Now())} },
    }
}

func toDatetimePtr(t time.Time) *time.Time {
    return &t
}

わざわざ time.Time の参照型である Birthday のためだけに toDatetimePtr を定義するのはちょっと微妙なので以下のようにしたらいいのではないかなと思いました。

func TestUserXXXX(t *testing.T) {
    tests := [] struct {
        name string
        src     User
    }{
        {
            name: "xxxxx....",  
            src: User{ 
                BirthDay: func(t time.Time) *time.Time {
                    return &t
                }(time.Now()),
            }, 
        },
    }
}

まとめ

これを書いてる最中にじゃあ参照型の型を新しく定義すれば?と思いましたが、いずれにしても利用用途が限定的なケースにおいてあんまり書きたいコードではないなと思いました。

Go では通常の実装においても func をそのまま使うと有用なケースが多いですね。

追伸

func の方が func を呼び出すコストがかかるので使いどころは選ばないといけません。
教えてもらった以下の Compier Explore によると確かに func を持っている時の方がコストが高いです。

https://go.godbolt.org/z/P9rWPP

同一文字列を繰り返す strings.Repeat

Overview

テストケースで使えそうな strings の機能を教えてもらったので書いておきます。

Strings.Repeat

https://golang.org/pkg/strings/#Repeat はある文字列を指定した回数分繰り返して文字列を生成するメソッドです。

こんな感じで繰り返し出力してくれます。

package main

import (
    "fmt"
    "strings"
)

func main() {
    s := strings.Repeat("s", 10)
    fmt.Println(s)
}
// Output: ssssssssss

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

テスト書くのに使えそう?

テストケースで文字数の境界値のテストをしたいケースってあると思いますよね。ある関数、メソッドのテストをしたいケースにおいて以下のように使うことができます。

func TestMethodA() (t *testing.T) {
    tests := []struct{}{
        name  string
        input string
        want  bool
    }{
        {name: "case_a", input: strings.Repeat("s", 10), true},
    }

    for _, tt := range tests {
        tt := tt
        t.Run(tt.name, func(t *testing.T){
            // MethodA のテスト
        })
    }
}

別に境界値分のStringを用意してもいいですが、意味のある文字列(JSONとかHTMLとかね)以外の単純な文字列であれば strings.Repeat を使って長い文字列を生成するといいのではないかなと思いました。