AWS SDK for Goが絶妙に使いづらい理由はなんだろうか
背景
AWSに載せてなんやかんややるというコードは、「使用例が豊富」という理由でboto3で実装となることが多かったのです。
その度にGopherの私は「動的型付けのPython使いづらいよう(注: 個人の意見です)」「try-catchじゃなくてerrをreturnするのが一番わかりやすいよなぁ?(注: 個人の意見です)」「使い慣れた言語で実装したら絶対楽だよなあ」と内心思っておりました。
そしてこの度ついにGoで実装していいよという機会をいただいたので、これ幸いにAWS SDK for Goを使い始めたのですが、その感想が「なんか……思ったほど楽になってない……」でした。
- なんでこういう感想になるのかなあというのを明らかにしたい
- 好きな言語で快適にAWS SDKを触るノウハウや思考メソッドを見出したい
という目的のもと「AWS SDK for Go」の使い心地を深く掘り下げたいと思います。
Goの言語仕様がうまく生きていると思った場所
型名が明示的に記述されていれば、エディタのポップアップでフィールドがすぐ確認できる
例えばこのようなコードがあったとして、「request
に何が入っているんだっけ?」となったときに、request
にカーソルを合わせるだけですぐに構造体フィールドが確認できるのは便利でした。
func handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error)
こういうの
boto3使っていて一番嫌だったのは「変数に含まれているフィールドを調べるために、エディタと公式Docの行ったり来たりになる」スイッチの多さだったので、ここは個人的にはgoodポイントだったのです。
静的型付け言語の強みが生きている箇所だと思います。
Goの言語仕様がうまくハマっていないと思った場所
レスポンスのフィールドが意外とわかりにくい
初めて覚えた言語が静的型付けだったのもあり、私は「この変数の型が何なのか?」というのが気になる性格です。
そのため、boto3で以下のようなコードを書いていたときに、「この後res
に何が含まれているのか、わざわざ公式Docに調べにいかなくても、型がきちんと決まっているGoならエディタの中だけで完結するんだろうなー」と思ってました。
res = client.GetSomething()
しかし、現実には以下のGoのコードがあったとして、res
にカーソルを合わせてわかるのは「変数res
がGetSomethingOutput
型だ」というところまでで、「GetSomethingOutput
型に何がフィールドとして含まれているか」までは出してくれませんでした。
res, err := client.GetSomething()
AWS SDK for Go全体として、APIのinput/outputは基本的にXxxInput
型・XxxOutput
型と抽象的にまとめられてしまっているため[1]、型名が分かったところで大した情報にならなかったです。
本来の関心事である「構造体フィールド一覧」や「フィールドの中身」まで調べようとすると結局公式ドキュメントのXxxOutput
型のページに飛ぶ羽目になるので、あまりboto3のときと労力が変わらない……と思ってしまいました。
-
pkg.go.devのOverviewの箇所を見ればこの傾向は一目瞭然。(例)cloudwatchのSDK ↩︎
XxxInput
型の全容把握が大変
ページ遷移数の観点での比較
例えば、PutLogEventsのAPIを叩くときに、GoのSDKを使っていると以下の3ステップが必要です。
-
PutLogEvents
メソッドのシグネチャから、引数がPutLogEventsInput
型であることを把握する
func (c *CloudWatchLogs) PutLogEvents(input *PutLogEventsInput) (*PutLogEventsOutput, error)
出典: pkg.go.dev - cloudwatchlogs#CloudWatchLogs.PutLogEvents
-
PutLogEventsInput
型のフィールドを確認する
type PutLogEventsInput struct {
LogEvents []*InputLogEvent `locationName:"logEvents" min:"1" type:"list" required:"true"`
LogGroupName *string `locationName:"logGroupName" min:"1" type:"string" required:"true"`
LogStreamName *string `locationName:"logStreamName" min:"1" type:"string" required:"true"`
SequenceToken *string `locationName:"sequenceToken" min:"1" type:"string"`
}
出典: pkg.go.dev - cloudwatchlogs#PutLogEventsInput
-
PutLogEventsInput
型のLogEvents
フィールドに使われていたInputLogEvent
型のフィールドを確認する
type InputLogEvent struct {
Message *string `locationName:"message" min:"1" type:"string" required:"true"`
Timestamp *int64 `locationName:"timestamp" type:"long" required:"true"`
}
出典: pkg.go.dev - cloudwatchlogs#InputLogEvent
このように、cloudwatchlogs
パッケージにて独自で定義された型が出てくるたびに、いちいち公式Docのページを辿りながら詳細を把握していく必要があります。
今回使われていた独自型はPutLogEventsInput
型とInputLogEvent
型の2つだったので、この2つの型のDoc + 元々のPutLogEvents APIのページという3つのページだけで詳細を把握することができました。
これがもっと複雑になるとそれだけステップ数が増えます。
一方boto3の場合には、put_log_events
メソッドのページだけで入力となる引数の全容を把握することができます。
response = client.put_log_events(
logGroupName='string',
logStreamName='string',
logEvents=[
{
'timestamp': 123,
'message': 'string'
},
],
sequenceToken='string'
)
出典: boto3公式Doc
フィールド条件での比較
Goではその代わり「このフィールドが必須フィールドなのか」「このフィールドは〇〇の条件下ならいらない」といった要件をフィールドタグや近くのコメントに書いてくれています。
type InputLogEvent struct {
Timestamp *int64 `locationName:"timestamp" type:"long" required:"true"` // 必須フィールド
}
boto3の場合でその情報を知るにはページスクロールが必要です。
ただ、前述の「何度も行われるページ遷移」に比べると……正直ページスクロールの方がマシでした。
response = client.put_log_events(
logEvents=[
{
'message': 'string'
},
],
)
# ページスクロール
# - timestamp (integer) -- [REQUIRED]
所感
というわけで、AWS SDKに関していうと「型がわかるメリット」が思っていたよりも享受できなかった上で、さらに「Goの静的型付けによるきちっとさ」が逆に牙を向いてきているような体感があるんです。
私は「変数がどんな型なのか分かっていた方がやりやすいに決まっている」というほど静的型付けどっぷり思想で育ったため、正直「型がついている故に逆に使いづらい」という現象に正直戸惑ってます。
皆さん同じ現象・感想を持っているんでしょうか?それとも私の使い方がまだおぼつかないからこういう体感になるんでしょうか?
それとも「そもそもAWS APIが使いづらい」とかいう説もあります?
AWS SDK for Goはv2でもちょっと書きづらいという話をちらほら聞きますね...
以下、Azure SDKで言いますと...
Azure SDKは公開されているAzure REST APIからAzure SDK Designに従ってautorestを使って各言語用のSDK(Web クライアント)を自動生成しているので、言語間の機能差は少ないように思われます。(ちょっと頑張れば、公開中のSDKが対応していない最新のAPIのSDKも手動で生成させることもできます。)
また、GoにはGo用のミドルウェア(go-autorest)が用意されていて、処理の並行実行やデータの型変換などがサポートされています。
ソースコードもGo Wayに寄っていて、素直な方だと思います。
GitHub - Azure/azure-rest-api-specs: The source for REST API specifications for Microsoft Azure.
Go Azure SDK Design Guidelines | Azure SDKs
GitHub - Azure/autorest: OpenAPI (f.k.a Swagger) Specification code generator.
なるほど、頂いた資料いろいろ拝見させていただきましたが、Azureだと直接所望のREST APIを叩く形なのですね。
AWS SDKみたいに独自構造体のオンパレードになるよりも、一般によく知られたRESTに乗っかるこちらの方が学習コストが低くて楽そうだなと思いました。
HTTPリクエスト・レスポンスを扱いやすくするようにgo-autorestも用意されているあたりとても親切ですね、やるなあMicrosoftさん(何様)
個人の感想ですがAWS SDKはただ「自動生成しやすいコード(体裁)」ってだけで使いづらいのかなと思います。
以下のGoのコードがあったとして、resにカーソルを合わせてわかるのは「変数resがGetSomethingOutput型だ」というところまでで、「GetSomethingOutput型に何がフィールドとして含まれているか」までは出してくれませんでした。
...
本来の関心事である「構造体フィールド一覧」や「フィールドの中身」まで調べようとすると結局公式ドキュメントのXxxOutput型のページに飛ぶ羽目になる
webページ開かないとわからなくて大変だ、という主張だと理解したのですが、VSCodeで定義元にジャンプして直接コードファイルをみるのはダメなんでしょうか?
例で言うと(res
じゃなくて申し訳ないですが、) GetSomething
にカーソルをあてて「Go to definition(F12
)」するとfunc GetSomething() (*GetSomethingOutput, error)
が定義されているファイルを開けるので、更に GetSomethingOutput
にカーソルを当ててもう一度 F12
を押下すれば構造体のコードファイルが開きますね。
あとは GetSomething
にカーソルを当てた状態で「Peek definition(Macだと Alt + F12
)」すると定義が開くので、そこで GetSomethingOutput
をマウスホバーすると定義がポップアップで見れますね。
(ファイル開かなくて済むけれどマウス操作が挟まる)
スクショは SQS.SendMessage
に対して上記操作を実行した例です。ファイル移動せずに構造体定義を確認できます。
なるほど・・・!VSCodeの機能をフルに使いこなせてなかったです。
情報ありがとうございます。
webページ開かないとわからなくて大変だ、という主張だと理解したのですが、
もちろんこのような大変さもありましたが、一番はAPIの全容を理解するために行う「webページを開いたり、構造体の定義を追ったり」する作業のホップ数が多いというのが、私が感じていた不便さの本質かなあと思ってます。
(「XxxInput型の全容把握が大変」の箇所で記述)
tenntennですが、AWSのライブラリはかなり昔からあり、その頃からかなり使いづらい印象です。
使う側の気持ちで作られておらず、自動生成のしやすさや提供側の都合で用意されているからだと思います。
パッケージの分け方、型の分け方、命名、コンテキストの扱い、などもイマイチだなと思ってます。
パッケージの分け方、型の分け方、命名、コンテキストの扱い、などもイマイチだなと思ってます。
このコメントを見て、このSDK for Goが使いづらいのは「静的型付けだから」ではなくて、「いまいちな設計のライブラリ独自型に従うことを静的型付けゆえに強制されてしまうから」なのかなと思いました。
コンテキストやパッケージの分け方までは掘り下げてなかったのですが、あまりお手本にしてはいけない例なんですね……。