Go言語で文字列中に変数を展開して使いたい
Go言語で以下と同じことをしたい。
bash
でいうhoge="fuga ${piyo:-piyopiyo} mogera"
php
でいう$hoge = "fuga ${piyo} mogera";
ruby
でいうhoge = "fuga #{piyo} mogera"
python
でいうhoge = f'fuga {piyo} mogera'
(python >= 3.6 に限る)
「golang 文字列 変数 展開」とググっても「連結演算子(+
)でしかできない」という情報が多かったので、自分のググラビリティとして。
- 関連記事: 改行などをエスケープして表示したい。【代入済み変数の文字列をエスケープ出力】 @ Qiita
TL; DR
確かに Go 言語(以下 Golang)には
interpolation
(補間、内挿)機能はありません。過去に要望があがったり、提案されるも、否定多数により却下されています。しかし、工夫次第で可能です。
内挿だけに、ないそうです... ... ... つまり、文字列内で変数をパース(展開)する機能は標準では持っていません。コンパイル型の言語なので関数を通すなどの工夫が必要です。
「文字列に変数を展開しての結合」を考える場合、2023 年 9 月現在、大きく以下の 4 パターンが考えられます。
- 変数と文字列を連結演算子(
+
)でつなげる。 - 文字列フォーマット関数
Sprintf
を使う。 - 大量に置き換える場合は、
text
やhtml
ライブラリのtemplate
関数を使う。 - 外部パッケージを検討してみる(例えば以下の 2 つ)。
-
go.uber.org/yarpc の
interpolate
パッケージ- 一般的な
Parse("My name is ${name}")
な使い方をしたい場合
- 一般的な
-
github.com/buildkite の
interpolate
パッケージ-
bash
の${piyo:-piyopiyo}
のようにデフォルトも設定したい場合
-
-
go.uber.org/yarpc の
package main
import (
"fmt"
"github.com/buildkite/interpolate"
)
func main() {
myInfo := Data{
Name: "Giovanni Giorgio",
Nick: "Giorgio",
}
myInfo.Printf("My name is ${Name}, but everybody calls me ${Nick}.\nI am ${Age:-82} yeas old.\n")
// Output:
// My name is Giovanni Giorgio, but everybody calls me Giorgio.
// I am 82 yeas old.
}
type Data struct {
Name string
Nick string
Age int
}
func (d Data) Printf(template string) {
env := interpolate.NewSliceEnv([]string{
"Name=" + d.Name,
"Nick=" + d.Nick,
})
output, _ := interpolate.Interpolate(env, template)
fmt.Print(output)
}
- オンラインで動作をみる @ Go Playground
template
や外部ライブラリを使った方法は、型が不明なものでも内挿しようとする以上、可読性と引き換えに、パフォーマンスを犠牲にしていることを念頭におくべきです。
とは言え、鼻から内挿を否定するのではなく、上記を一通り試したうえで、自分のアプリにおいては内挿は必要か不要かを判断すべきだと思います。
-
Go に入れば Go に従え("In Golang, do what the Gophers do")
筆者は、一巡して
fmt.Sprintf()
とtemplate
の原点に戻りました。Javascript、PHP や Bash などのスクリプト言語から来た直後は強い歯痒さを感じていたのですが、長い目で見た時の合理性に欠けたからです。と言うのも、メンテナンスを行うには Go を知らないといけません。そして慣れてくると、Go のシンプルな基本構文に徹した方が、外部ライブラリ依存の構文の共有や、型の心配を考える必要がなくなったからです。
TS; DR
func GetHogeByGlue(piyo string) string {
return "fuga " + piyo + " mogera" + "\n"
}
import "fmt"
func GetHogeBySprintf(piyo string) string {
return fmt.Sprintf("fuga %s mogera\n", piyo)
}
import "text/template"
type FieldsToReplace struct {
Replace1 string
}
func GetHogeByTemplate(piyo string) string {
var msg_result bytes.Buffer
msg_tpl, msg_err := template.New("myTemplate").Parse("fuga {{.Replace1}} mogera\n")
replace_to := FieldsToReplace {
Replace1: piyo,
}
msg_err = msg_tpl.Execute(&msg_result, replace_to)
return msg_result.String()
}
- 上記の動作をオンラインで見る @ paiza.IO
外部ライブラリを使った例
POSIX Parameter Expansion 形式、つまり bash
などの ${hoge:-fuga}
といった「変数が空の場合のデフォルト値も指定する」ように使いたい場合は、以下の2種類のパッケージがあります。後者の github.com/buildkite
の方がより近い使い方が出来ます。
go.uber.org/yarpc の interpolate
パッケージ
-
go.uber.org/yarpc @ pkg.go.dev
- 正直言ってこのパッケージは使いづらいです。後述の
github.com/buildkite
の方が使いやすいです。
内挿処理を行うinterpolate
パッケージがinternal
に設置されているため、go get
してもinterpolate
を直接触れません。これは Uber 社のメッセージング・プラットホームyarpc
用の内部パッケージとして使われているためです。そのため、リポジトリをクローンやコピーしてinterpolate
パッケージを直接自分のプロジェクトに組み込むなどの手間が必要になります。しかもRagel
という謎技術で肝心のParse()
関数を自動生成しているため、メンテもたいへん。Glide パッケージマネージャーに慣れている人向けです。
- 正直言ってこのパッケージは使いづらいです。後述の
s, err := interpolate.Parse("My name is ${name}")
if err != nil {
panic(err)
}
out, err := s.Render(resolver)
if err != nil {
panic(err)
}
fmt.Println(out)
s, err := interpolate.Parse("My name is ${name:what}")
if err != nil {
panic(err)
}
out, err := s.Render(emptyResolver)
if err != nil {
panic(err)
}
fmt.Println(out)
# My name is what
github.com/buildkite の interpolate
パッケージ
- github.com/buildkite @ pkg.go.dev
// 要インストール go get "github.com/buildkite/interpolate"
import "github.com/buildkite/interpolate"
func GetHogeByInterpolate(piyo string) string {
env := interpolate.NewSliceEnv([]string{
"Replace2="+piyo,
})
output, _ := interpolate.Interpolate(env, "fuga ${Replace2} mogera ${Replace3:-🏖}\n")
return output
}
動作サンプル
上記 Uber のライブラリを除いた、「変数を文字列内に展開する方法」のサンプルです。
package main
import (
"bytes"
"fmt"
"text/template"
"github.com/buildkite/interpolate"
)
func main() {
fmt.Println(GetHogeByGlue("foo")) // 文字列結合子(+)を使ったサンプル
fmt.Println(GetHogeBySprintf("foo")) // fmt パッケージを使ったサンプル
fmt.Println(GetHogeByTemplate("foo")) // template パッケージを使ったサンプル
fmt.Println(GetHogeByInterpolate("foo")) // interpolate パッケージを使ったサンプル
}
func GetHogeByGlue(piyo string) string {
return "fuga " + piyo + " mogera"
}
func GetHogeBySprintf(piyo string) string {
return fmt.Sprintf("fuga %s mogera", piyo)
}
type FieldsToReplace struct {
Replace1 string
}
func GetHogeByTemplate(piyo string) string {
msg_tpl, msg_err := template.New("myTemplate").Parse("fuga {{.Replace1}} mogera")
replace_to := FieldsToReplace{
Replace1: piyo,
}
var msg_result bytes.Buffer
if msg_err = msg_tpl.Execute(&msg_result, replace_to); msg_err != nil {
fmt.Println(msg_err)
}
return msg_result.String()
}
func GetHogeByInterpolate(piyo string) string {
env := interpolate.NewSliceEnv([]string{
"Replace2=" + piyo,
})
output, _ := interpolate.Interpolate(env, "fuga ${Replace2} mogera ${Replace3:-🏖}")
return output
}
$ go get -u github.com/buildkite/interpolate
...
$ ls
sample.go
$ go run sample.go
fuga foo mogera
fuga foo mogera
fuga foo mogera
fuga foo mogera 🏖
- オンラインで動作を見る @ GoPlayground
Golang に文字列内変数置き換えがない理由
If you see the example from PHP or Ruby, you might realize that the they are more for dynamic type system because it does care about what you pass into the interpolation string — the data types of year, month and day, however in a static, compile language like Go, we need to specify types of our variables.
Conclusion
It might looks weird to you, but if you think about the benefit of static type language with compile time error checking, this make a lot of sense, more convenient and easy to debug than string interpolation in dynamic languages.
(「String interpolation in golang」@ Medium より)(筆者訳)
PHP や Ruby の例を見ると、それらは動的型付けに適していることがわかると思います。なぜなら文字列置き換えを重視している型システムだからです。しかし、静的型システム(Go のようなコンパイル言語)の場合は年/月/日といったデータすら変数の型は事前に明確にする必要があるのです。
結論
奇妙に見えるかもしれませんが、コンパイル時のエラーチェックによる静的型言語の利点を考えると、動的言語での文字列補間よりもはるかに理にかなっていて、便利でデバッグがより簡単です。
なるほど。確かにコンパイル時間が短いことを売りにしている Golang では、型チェックが煩雑になりがちな文字列の内挿の実装はデメリットの方が大きいのかもしれません。文字列内の変数の展開は Web 用途に多いので、Golang では text/template
や html/template
を使うべし、というのも何となく理解できます。
PHP や Python3 も JIT 時代に入り、いまやコンパイル型に近い(プリ・コンパイル型)言語。型々言わないかわりにガタガタとバカにされていた PHP も、PHP7 以降、型をより重視するようになったのもコンパイルや開発(デバッグ)の速度アップには必要だったのかもしれません。
しかし、最新の PHP だと Golang より速いことも多くなった(速いとは言ってない)ので、ランタイムなしでバイナリを配布できるのが Golang の一番のメリットかもしれません。つまり、コンパイルうんぬんと言うよりポリシーの問題かと思います。
Golang の Issues でも長く議論されており、「言語の開発者」と「言語の利用者」で重視する点が異なり、なかなか難しそうです。
- proposal: string interpolation #34174 | issues | Golang @ GitHub
それでも、Python では v3.6 で内挿が実装されたり、同じ静的型付け言語である Rust でも v1.58 で内挿が実装されたりしているので、やはり Golang も変数の文字列内展開を標準にして欲しいなぁ。
Go もジェネリクス(型とらわれない処理を記述するための仕組み)が v1.18 から使えるようになったので、おそらく遅かれ早かれ実装されることでしょう。
検証バージョン
$ go version
go version go1.12.5 linux/amd64
参考文献
- 「Goで文字列中で変数展開する」 @ Qiita
- 「String interpolation in golang」@ Medium