Skip to content
AIこの記事はAIによって生成されたコンテンツです。

Go Issue #22647 全コメント翻訳 ― 関数の戻り値や定数のアドレスを取りたい

golang/go#22647 は、2017年11月に提出された「Go 2: 関数の戻り値や定数のアドレスを取れるようにする」という提案だ。AWS SDKの苦痛やprotobufのヘルパー関数問題を背景に、&time.Now()&"foo" のような構文を求める声が上がった。

最終的に2018年3月、より一般的な構文を議論する #9097 に統合されてクローズされた。

この記事では、本文と全17件のコメントを漏れなく日本語に翻訳する。


Issue本文

著者: benhoyt | 投稿日: 2017-11-09

タイトル: proposal: Go 2: allow taking the address of a function result value or constant

http://benhoyt.com/writings/learning-go/#general-quirks に書いたように、Goを学ぶ際に気づいた(数少ない)言語の癖の一つは、関数の戻り値のアドレスを取れないことだった。例えば、最初にこう書いて、動くと思っていた:

go
mystruct.TimePtr = &time.Now()

しかしもちろん動かなかった。仕様によれば、アドレスを取る対象は「アドレス可能(addressable)」であるか、特殊ケースとして&Point{2, 3}のようなcomposite literalである必要がある。

これをGo言語仕様に対する後方互換性のある変更として許可し、コンパイラにアドレスを取るための一時変数(ヒープまたはスタック上)を作成する必要があることを判断させることができるように思える。コンパイラは以下と同等のコードに展開できるだろう:

go
tmp := time.Now()
mystruct.TimePtr = &tmp

そして私だけではない ― 少しずつ異なる形で同じ問題にぶつかった人がいる。1つ目2つ目3つ目4つ目を参照。

特に、Amazon AWS SDKにはaws.String(ドキュメント)のようなヘルパー関数が大量にある。これは入力構造体のポインタフィールドに「オプショナルパラメータ」として定数を使えるようにするためだけのものだ。例えば:

go
input := &s3.PutObjectInput{
    ACL:         aws.String("public-read"),
    Body:        bytes.NewReader(data),
    Bucket:      aws.String("mybucket"),
    ContentType: aws.String("image/jpeg"),
    Key:         &key,
}
_, err := s3ImageClient.PutObject(input)

これが追加される前(こちらを参照)は、非常に扱いにくい方法で書く必要があった:

go
acl := "public-read"
bucket := "mybucket"
contentType := "image/jpeg"
input := &s3.PutObjectInput{
    ACL:         &acl,
    Body:        bytes.NewReader(data),
    Bucket:      &bucket,
    ContentType: &contentType,
    Key:         &key,
}

この提案が実現すれば、AWS SDKはそれらのaws.Stringスタイルのヘルパーをすべて廃止できるだけでなく、明白できれいな書き方ができるようになる:

go
input := &s3.PutObjectInput{
    ACL:         &"public-read",
    Body:        bytes.NewReader(data),
    Bucket:      &"mybucket",
    ContentType: &"image/jpeg",
    Key:         &key,
}

明らかにいくつか考慮すべきことがある:

  1. 人々が必要とするもの、つまり関数呼び出しと定数だけに追加するのか? それとも&(x + 1234)のような任意の式に対してか? 任意の式を許す方がすっきりして直交性があると思うが、保守的にすべき良い理由があるなら、&functionCall()&constantだけでも素晴らしいスタートだと思う。
  2. この構文は完全に後方互換性があるか? 構文の専門家ではないが、そうだと思う。既に&Point{2, 3}でできる特殊ケースの単純な拡張に過ぎないからだ。
  3. 複数値を返す関数のアドレスは取れない(コンパイル時エラーになる)。しかし複数値を返す関数は既にいくつかの点で特殊なので、それで問題ないだろう。

ラベル: LanguageChange, v2, Proposal, FrozenDueToAge


コメント 1

著者: dsnet | 投稿日: 2017-11-09

これは #19966 に関連している。同じことだが、プリミティブ型に対するものだ。

また #9097 にも関連している。


コメント 2

著者: benhoyt | 投稿日: 2017-11-09

ああ、ありがとう @dsnet、見逃していた(検索はしたが、&ではなくaddressで検索してしまった。&は検索しづらい!)。

&functionCall()が私がぶつかったものだが、&constantの方がおそらくより重要だと思う ― 選り好みするのであれば。だから #19966 がGo 2に含まれれば、3/4は満足だ。😃

#9097 を見たが、new(int, 5)&int(5)構文もあまりピンとこない ― どちらも特に明白ではないように思える。しかし、new(5)を提案するこの提案も見た ― ストレートな&5ほど好きではないが、妥当に思える。

標準ライブラリにあるnewInt() / newString()ワークアラウンドのヘルパー関数の2つの例も指摘しておきたい(テスト内だが): encoding/asn1/asn1_test.gotext/template/exec_test.go


コメント 3

著者: as | 投稿日: 2017-11-10

あのAmazon APIを書く最も簡単な方法は、(不変型を含め)あらゆるものにポインタを容赦なく使わないことだっただろう。この提案で既に言及されている例以外に、この需要が示されている場所はあるのか?


コメント 4

著者: benhoyt | 投稿日: 2017-11-10

@as しかしAWSのissueスレッドを見ればわかるように、彼らには理由があってそうしている ― ゼロ値(例えば0や空文字列)と未設定値を区別する必要があるからだ:

注意すべき重要な点で、他のSDKがどう処理しているかわからないが、AWS APIにおける「未設定値」の区別がある ― これが、プリミティブ値に対してもすべての型がポインタであることを確保してきた理由だ。

Go protobufライブラリも同様だ ― オプショナルフィールドの設定を助けるために、すべての基本型に対するヘルパー関数がある。

つまり、この問題を抱える2つの主要ライブラリがあり、さらに標準ライブラリのテスト内の2つの事例、リンクした2つの人気あるStackOverflowの質問と2つのgolang-nutsスレッド、そして私自身の経験がある。そうした観点から、9つの例と2つの既存issueの外に「需要が示されている場所はあるか」と聞くのは、少し軽視しているように思える…


コメント 5

著者: as | 投稿日: 2017-11-10

9つの例と2つの既存issueの外に「需要が示されている場所はあるか」と聞くのは、少し軽視しているように思える…

申し訳ない。軽視するつもりはなかった。目的は探求であり、議論を広げる役割を果たした。

ゼロ値(例えば0や空文字列)と未設定値を区別する必要があるから

私の意見では、彼らがそうする必要があるのか、それとも単にそうしたいのかを見極めることが重要だ。そのスレッドでは、Amazonのサービスが将来自身のセマンティクスを変更し、nilemptyが区別できない場合にSDKが壊れることへの懸念が示されている。これが唯一の理由なら、スコープに対して自己本位であり、「なぜ」の根本に迫っていない。他の言語やフレームワークとの一貫性以外に、nil""を区別すべき明確な理由はあるのか? AWSの例に異議を唱えたのはこの理由だ。ただし、プロトコルバッファについての事情は知らない。


コメント 6

著者: rasky | 投稿日: 2017-11-10

もう一つの関連するユースケースはCreate().Use()だ。Createが値を返し、Useがポインタレシーバを持つ場合だ。Goではこれができず、一時変数を使わざるを得ないが、この制限に正当な理由があるようには見えない。


コメント 7

著者: benhoyt | 投稿日: 2017-11-10

@as ありがとう。そして以下の指摘はもっともだ:

私の意見では、彼らがそうする必要があるのか、それとも単にそうしたいのかを見極めることが重要だ。… 他の言語やフレームワークとの一貫性以外に、nilと""を区別すべき明確な理由はあるのか?

ゼロ値と未設定値を区別する必要がある場合は確かにあると思うが、実際のs3.PutObjectInput構造体をざっと見ると、私が示したキーとほとんどの他のキーは、必須(BucketKey)であるか、空文字列は無効だ(ACLContentType)。実際、ACLの場合、もし私がAPIを手で設計するなら、おそらくenumを使うだろう: ACLPublicReadなど。AWSのAPI面は非常に大きいので、(部分的に?完全に?)API記述からコードを自動生成しているのだろう(例えばこのコミットを参照)。

それでも、そのようなユースケースは合理的だ ― APIのサイズと一貫性の必要性を考えると、おそらく軽い気持ちでこの方法を選んだわけではないだろう。ほとんどのパラメータを値にしていくつか(例えばContentLength?)をポインタ/nil許容にするのも同様に不格好だろう。

いずれにせよ、より重要なユースケースはおそらく私がぶつかった場面、つまりnull許容のデータベースフィールドのようなものだと思う。もしGoにoption型があれば(そのための提案もあることは知っている)、この状況も解決するだろう ― 本当に欲しいのはポインタではなく、フィールドが設定済みか未設定かを示す方法だ。


コメント 8

著者: ianlancetaylor | 投稿日: 2017-11-10

Create().Use()は興味深いケースだ。Useがポインタレシーバを取り、レシーバが指すメモリを変更する場合、Create().Use()と書くのはバグである可能性が高い。

一般的にも興味深い問題だ: 一時的なメモリ位置を作成して任意のEに対して&Eを許すなら、Mがポインタレシーバを使う場合に任意のEに対してE.M()でも同じことをすべきか? 後者の方が前者よりもバグを導入しやすいと推測するが、間違っているかもしれない。


コメント 9

著者: faiface | 投稿日: 2017-11-10

@ianlancetaylor Create().Use()は、Create()がポインタを返す場合(かつUseがポインタレシーバを持つ場合)、既に完全に可能だ。また時に便利であり、バグの場合でも他のバグより見つけにくいわけではない。

Create()(非ポインタ)型を返すが、Use()がポインタレシーバを持つ状況は、IMHOとても珍しい。構造体型は通常、ポインタ型と値型の2つのカテゴリに分かれる。ポインタ型のコンストラクタはポインタを返し、メソッドも同様にポインタレシーバを使う。値型のコンストラクタは値を返し、メソッドは値レシーバを使う。


コメント 10

著者: rasky | 投稿日: 2017-11-10

Create().Use()は興味深いケースだ。Useがポインタレシーバを取り、レシーバが指すメモリを変更する場合、Create().Use()と書くのはバグである可能性が高い。

一般的にはそうだとは思わない。例えば、上記の構文はロギングライブラリでは非常に一般的で、ユーザーの意図は行を出力することだ。その過程でオブジェクトが変更されるかどうかはライブラリ自体の実装詳細だ。

明らかに、今日でもライブラリは書ける。CreateとUseの両方で常にポインタを使うか、常に値を使えばいい。しかし、混在が禁止されるべき理由(しかも一時変数の場合だけで、明示的な一時変数なら許される)がまだわからない。

パフォーマンスの議論もある。Goのエスケープ分析は完璧からは程遠く、不要なレシーバのコピーの省略は存在しない。値レシーバを持つメソッド呼び出しはすべて、コピーが実際に必要かどうかに関係なく、レシーバのコピーを生成する。実際、メソッドが値を変更しない場合、コピーはほとんどの場合不要だ。私はこの理由で、値が本当にシンプル(例えば基本型)でない限り、ポインタレシーバを好む傾向がある。


コメント 11

著者: as | 投稿日: 2017-11-11

パフォーマンスの議論もある。Goのエスケープ分析は完璧からは程遠く、不要なレシーバのコピーの省略は存在しない。値レシーバを持つメソッド呼び出しはすべて、コピーが実際に必要かどうかに関係なく、レシーバのコピーを生成する。実際、メソッドが値を変更しない場合、コピーはほとんどの場合不要だ。私はこの理由で、値が本当にシンプル(例えば基本型)でない限り、ポインタレシーバを好む傾向がある。

それらの記述は実装の詳細であり、互換性の約束の対象ではない(提案された変更がそうなるのとは異なり)。たとえそれらが正しいとしても(ベンチマークなしにはそれを知ることは不可能だ)。


コメント 12

著者: bcmills | 投稿日: 2018-02-21

Useがポインタレシーバを取り、レシーバが指すメモリを変更する場合、Create().Use()と書くのはバグである可能性が高い。

現時点では、ポインタレシーバは本来直交すべき2つの性質を混同している: 効率性(不要なコピーの回避)とアイデンティティ(書き込みが基となるオブジェクトに反映されるかどうかの保証)だ。

Useが効率性の理由でポインタレシーバを受け取る場合、Create().Use()は許されるべきだ。Useがアイデンティティを保持するためにポインタレシーバを受け取る場合、それは確かにバグである可能性が高い。

実際のコードでは、これはBuilderパターンの形で現れる: GoでBuilder的なAPIを書きたいが効率性のためにポインタを渡したい場合、値レシーバを持つために追加のstruct型でそれらのポインタをラップしなければならない。(ありがたいことに、通常composite literalを使えるため、Builderパターンはあまり頻出しない。)


コメント 13

著者: bcmills | 投稿日: 2018-02-21

Goのエスケープ分析は完璧からは程遠く、不要なレシーバのコピーの省略は存在しない。値レシーバを持つメソッド呼び出しはすべて、コピーが実際に必要かどうかに関係なく、レシーバのコピーを生成する。 [...]

それらの記述は実装の詳細だ

それは正しいかもしれないが、抽象化としては非常にリーキーだ。根本的に、エスケープ分析とコピー省略はどちらもある種のエイリアス分析に依存しており、それを正確に行うのは難しい ― 特にGoのように動的な言語では。関数引数のコピーを省略するには、呼び出し先が引数を直接変更しないことだけでなく、(渡された関数やインターフェース引数などを通じて)間接的に変更しないことも知る必要がある。

十分に賢いコンパイラならその分析ができるはずだと主張することもできるが、Goコンパイラが近い将来そこまで十分に賢くなることはなさそうだ。だから実行可能な選択肢は、ポインタレシーバによる最適化が時にコールサイトを不格好にすることを受け入れるか、ポインタレシーバによるアイデンティティがエイリアシングのバグを許すことを受け入れるかのどちらかのようだ。

ガベージコレクションの使用は既に、エイリアシングのバグを犠牲にしてよりクリーンなコールサイトに向けてGoを偏らせていると思うので、関数引数をアドレス可能にすることは現在の言語とかなり一貫していると思う。(しかしこれは完全に主観的であることは認める。)


コメント 14

著者: ianlancetaylor | 投稿日: 2018-03-27

#9097 は関数呼び出しや定数だけでなく、あらゆる種類の式に対して動作するより一般的な構文を提案している。このissueを #9097 を優先してクローズする。構文の詳細はそちらで議論できる。


コメント 15

著者: benhoyt | 投稿日: 2018-03-28

@ianlancetaylor うーん、明らかにバイアスがあるが、いくつかの理由でこのissueの方が #9097 より良いと思う:

  1. レポートがより充実しており明確に書かれており、単に「これを追加すべき」ではなく実例(「経験報告」)を示している。説明には他の人がStackOverflowやgolang-nutsで同じことを聞いているリンクも含まれている。
  2. タイトルは少し狭いかもしれないが、このissueは式のアドレスを取るという一般的なサポートもカバーしている。単に演算子の優先順位の問題だ ― 説明の最後にある&(x + 1234)のような内容を参照。とはいえ、一般的な式のアドレスを取ることはまれだと思う。
  3. &"foo"&1234の構文は既存の&Foo{42}構文の自然な拡張だと思う: 何かを記述し、そのアドレスを取る。一方&int(foo)は型強制や関数呼び出しのように見え、単に「アドレスを取る」ようには見えない。さらに、&1234の方が簡潔だ(それ自体は必ずしも美徳ではないが)。
  4. 人気コンテストではないとわかっているが、こちらには12の賛成票があり、あちらには5つしかない。

誤解しないでほしいが、&int(1234)構文はまずまずで勝利になると思う。ただ、このissueは少し性急にクローズされたように思える。あるいは #9097 でよりシンプルな&1234構文を主張するかもしれない。😃


コメント 16

著者: ianlancetaylor | 投稿日: 2018-03-28

はい、#9097 で議論してほしい。2つのissueは基本的に構文が異なるだけで同じアイデアなので、議論を1か所、より古いissueに統合している。


コメント 17

著者: benhoyt | 投稿日: 2018-03-28

そうする、ありがとう。


関連Issue

  • #9097&T(v) / new(T, v) 構文の提案(本issueの統合先)
  • #19966 ― プリミティブ型のアドレスを取る提案
  • #45624 ― Rob Pikeによる再提案(最終的な後継issue)