🎄

Goの… (ellipsis) について

2022/12/14に公開約3,400字

Unipos Advent Calendar 2022の記事です。
Zennで記事書いてみる練習を兼ねています。

同僚と会話していてGoの ... は演算子かどうか、みたいな話が出てきたのでGoの言語仕様をあたってみました。

記事の中で引用符使ってたら特記のない限り言語仕様からの引用です。

定義

言語仕様上どのように解釈されるのかを眺める。

言語仕様上意味を持つ字句だという定義は Operators and punctuation 節にある:

The following character sequences represent operators (including assignment operators) and punctuation:

+    &     +=    &=     &&    ==    !=    (    )
-    |     -=    |=     ||    <     <=    [    ]
*    ^     *=    ^=     <-    >     >=    {    }
/    <<    /=    <<=    ++    =     :=    ,    ;
%    >>    %=    >>=    --    !     ...   .    :
     &^          &^=          ~

Operator(演算子)の定義はだいぶ後ろの Operators 節にあって、ここから ... はOperatorでなくPunctuationであるということがわかる。

Operators combine operands into expressions.

Expression = UnaryExpr | Expression binary_op Expression .
UnaryExpr  = PrimaryExpr | unary_op UnaryExpr .

binary_op  = "||" | "&&" | rel_op | add_op | mul_op .
rel_op     = "==" | "!=" | "<" | "<=" | ">" | ">=" .
add_op     = "+" | "-" | "|" | "^" .
mul_op     = "*" | "/" | "%" | "<<" | ">>" | "&" | "&^" .

unary_op   = "+" | "-" | "!" | "^" | "*" | "&" | "<-" .

Punctuation って他のプログラミング言語の字句要素の種別としてあんまり出てこない気がする……? 特別に意味のある記号、くらいで考えればいいのかな。

用法

言語仕様でどのような使われかたが説明されてるか見てみる。

関数の引数が可変長引数であることを示す

Function types 節より。
最後の引数に ... をつけると可変長引数になる。

func(prefix string, values ...int)
func(a, b int, z float64, opt ...interface{}) (success bool)

ついでに: 可変長引数をとる関数にほかの関数の返り値をそのまま渡す

... と直接の関連はないけれど、 Calls 節に書いてあったので。

Goの仕様では、複数の値を受けとる関数 g と 複数の値を返す関数 f があったときに、 g の引数の型や種類と、 f の返り値の型や種類が一致するときに、関数呼出を入れ子にできる。
日本語が若干怪しいかもしれないけど、要はこういうこと:

func Split(s string, pos int) (string, string) {
	return s[0:pos], s[pos:]
} 

func Join(s, t string) string {
	return s + t
}

if Join(Split(value, len(value)/2)) != value {
	log.Panic("test fails")
}

この仕組みは可変長引数を受ける関数にも有効で、以下のような記述ができる:

func f(a int, b ...int) int {
	for _, x := range b {
		a += x
	}
	return a
}

func g(a, b, c int) (int, int, int) {
	return a, b, c
}

f(g(1, 2, 3))  // In function f, a = 1, b = []int{2, 3}

Arrayリテラルの長さとして

Composite literals 節より。
Arrayリテラルを記述するには長さを指定する必要があるが、長さのかわりに ... を記述すると、記述した要素がちょうど収まる長さのArrayになる。

days := [...]string{"Sat", "Sun"}  // len(days) == 2

可変長引数としてスライスを渡す

Passing arguments to ... parameters 節より。

以下のような関数の定義と呼び出しについて:

func Greeting(prefix string, who ...string)
Greeting("nobody")
Greeting("hello:", "Joe", "Anna", "Eileen")

2行目の呼び出しでは関数中で whonil になり、3行目の呼びだしでは []string{"Joe", "Anna", "Eileen"} となる、長さと容量が引数の数に等しいスライスが作成され、 who として渡される。

s := []string{"James", "Jasmine"}
Greeting("goodbye:", s...)

という呼び出しもできて、このときは whos と同じunderlying arrayを持ったsliceとなる。
ここで仕様には

In this case no new slice is created.

とあるけど、関数内で append 呼んでも関数の外で要素が増えてたりはしないのであくまで同じunderlying arrayだよという意味だと思う……。自信はないです!

おわりに

... の用法、だいたい知ってたけどArrayリテラルの長さのかわりに使えるというのはA Tour of Goにもなかったので言語仕様読むまで気付かなかったな〜。
みなさんも気になったことがあるときは公式のドキュメントに目を通してみるのがおすすめです。
それではよいお年を。

Discussion

ログインするとコメントできます