【Golang】 文字列内で変数を展開・内挿する方法（Go言語の interpolation 各種）

## Go言語で文字列中に変数を展開して使いたい

> Go言語で以下と同じことをしたい。
>
>   - `bash` でいう `hoge="fuga ${piyo:-piyopiyo} mogera"`
>   - `php` &nbsp; でいう `$hoge = "fuga ${piyo} mogera";`
>   - `ruby` でいう `hoge = "fuga #{piyo} mogera"`
>   - `python` でいう `hoge = f'fuga {piyo} mogera'` （python >= 3.6 に限る）

[「golang 文字列 変数 展開」とググって](https://www.google.com/search?q=golang+%E6%96%87%E5%AD%97%E5%88%97+%E5%A4%89%E6%95%B0+%E5%B1%95%E9%96%8B)も「連結演算子（`+`）でしかできない」という情報が多かったので、自分のググラビリティとして。

- 関連記事: [改行などをエスケープして表示したい。【代入済み変数の文字列をエスケープ出力】](https://qiita.com/KEINOS/items/9bf8d33b0c616f85ba4a) @ Qiita

## TL; DR

> 確かに Go 言語（以下 Golang）には `interpolation`（補間、内挿）機能はありません。過去に[要望があがった](https://github.com/golang/go/issues/34174#issuecomment-1450932232)り、[提案される](https://github.com/golang/go/issues/57616)も、否定多数により却下されています。
>
> しかし、**工夫次第で可能**です。

<ruby>内挿<rt>ないそう</rt></ruby>だけに、<ruby>ないそう<rt>・・・・</rt></ruby>です... ... ... つまり、**文字列内で変数をパース（展開）する機能は標準では持っていません**。コンパイル型の言語なので関数を通すなどの工夫が必要です。

「文字列に変数を展開しての結合」を考える場合、2023 年 9 月現在、大きく以下の 4 パターンが考えられます。

1. 変数と文字列を連結演算子（`+`）でつなげる。
2. 文字列フォーマット関数 `Sprintf` を使う。
3. 大量に置き換える場合は、`text` や `html` ライブラリの `template` 関数を使う。
4. 外部パッケージを検討してみる（例えば以下の 2 つ）。
    - [go.uber.org/yarpc](https://pkg.go.dev/go.uber.org/yarpc/internal/interpolate?tab=doc) の `interpolate` パッケージ
      - 一般的な `Parse("My name is ${name}")` な使い方をしたい場合
    - [github.com/buildkite](https://pkg.go.dev/mod/github.com/buildkite/interpolate) の `interpolate` パッケージ
      - `bash` の `${piyo:-piyopiyo}` のようにデフォルトも設定したい場合

```go:buildkite/interpolateを使った例
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)
}
```

- [オンラインで動作をみる](https://go.dev/play/p/NkrLfx1S4k0) @ Go Playground

:::note info
`template` や外部ライブラリを使った方法は、型が不明なものでも内挿しようとする以上、<font color="darkorange">**可読性と引き換えに、パフォーマンスを犠牲にしている**</font>ことを念頭におくべきです。

とは言え、鼻から内挿を否定するのではなく、上記を一通り試したうえで、**自分のアプリにおいては内挿は必要か不要かを判断すべき**だと思います。

- **Go に入れば Go に従え（"In Golang, do what the Gophers do")**

    筆者は、一巡して `fmt.Sprintf()` と `template` の原点に戻りました。Javascript、PHP や Bash などのスクリプト言語から来た直後は強い歯痒さを感じていたのですが、長い目で見た時の合理性に欠けたからです。と言うのも、メンテナンスを行うには Go を知らないといけません。そして慣れてくると、Go のシンプルな基本構文に徹した方が、外部ライブラリ依存の構文の共有や、型の心配を考える必要がなくなったからです。
:::

## TS; DR

```golang:+演算子で結合する王道＆愚直なタイプ
func GetHogeByGlue(piyo string) string {
    return "fuga " + piyo + " mogera" + "\n"
}
```
```golang:Sprintf関数を使うタイプ
import "fmt"

func GetHogeBySprintf(piyo string) string {
    return fmt.Sprintf("fuga %s mogera\n", piyo)
}
```
```golang:Template関数を使うタイプ
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()
}
```

- [上記の動作をオンラインで見る](https://paiza.io/projects/gUtLavzeKBfJgfmhpe0HHA) @ paiza.IO

### 外部ライブラリを使った例

[POSIX Parameter Expansion](http://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_06_02) 形式、つまり `bash` などの `${hoge:-fuga}` といった「変数が空の場合のデフォルト値も指定する」ように使いたい場合は、以下の２種類のパッケージがあります。後者の `github.com/buildkite` の方がより近い使い方が出来ます。

#### go.uber.org/yarpc の `interpolate` パッケージ

- [go.uber.org/yarpc](https://pkg.go.dev/go.uber.org/yarpc/internal/interpolate?tab=doc) @ pkg.go.dev
  - <sub><sup>正直言ってこのパッケージは使いづらいです。後述の `github.com/buildkite` の方が使いやすいです。<br>内挿処理を行う [`interpolate` パッケージが `internal` に設置されている](https://github.com/yarpc/yarpc-go/tree/8e057b722815da87db2d463779f47b7829f8183c/internal/interpolate)ため、`go get` しても `interpolate` を直接触れません。これは Uber 社のメッセージング・プラットホーム `yarpc` 用の内部パッケージとして使われているためです。そのため、リポジトリをクローンやコピーして `interpolate` パッケージを直接自分のプロジェクトに組み込むなどの手間が必要になります。しかも [`Ragel`](https://www.colm.net/open-source/ragel/) という謎技術で肝心の `Parse()` 関数を自動生成しているため、メンテもたいへん。[Glide](https://github.com/Masterminds/glide) パッケージマネージャーに慣れている人向けです。</sup></sub>

```golang:yarpcのinterpolateパッケージを使う（interpolateパッケージをプロジェクトに追加している場合）
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)

```

```golang:デフォルト値も設定する例
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](https://pkg.go.dev/mod/github.com/buildkite/interpolate) @ pkg.go.dev

```golang:buildkiteのinterpolateパッケージを使う
// 要インストール 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 のライブラリを除いた、「変数を文字列内に展開する方法」のサンプルです。

```golang:sample.go
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
}
```

```shell-session
$ 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 🏖
```

- [オンラインで動作を見る](https://go.dev/play/p/X8fRa4c-se9) @ 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.
> <sup>（「[String interpolation in golang](https://medium.com/@channaly/string-interpolation-in-golang-ecb3bcb2d600)」@ Medium より）</sup>
>
> （筆者訳）
> PHP や Ruby の例を見ると、それらは[動的型付け](https://ja.wikipedia.org/wiki/%E5%9E%8B%E3%82%B7%E3%82%B9%E3%83%86%E3%83%A0#%E9%9D%99%E7%9A%84%E5%9E%8B%E4%BB%98%E3%81%91%E3%81%A8%E5%8B%95%E7%9A%84%E5%9E%8B%E4%BB%98%E3%81%91)に適していることがわかると思います。なぜなら文字列置き換えを重視している[型システム](https://ja.wikipedia.org/wiki/%E5%9E%8B%E3%82%B7%E3%82%B9%E3%83%86%E3%83%A0)だからです。しかし、静的型システム（Go のようなコンパイル言語）の場合は年／月／日といったデータすら変数の型は事前に明確にする必要があるのです。
> **結論**
> 奇妙に見えるかもしれませんが、コンパイル時のエラーチェックによる静的型言語の利点を考えると、動的言語での文字列補間よりもはるかに理にかなっていて、便利でデバッグがより簡単です。

なるほど。確かにコンパイル時間が短いことを売りにしている Golang では、型チェックが煩雑になりがちな文字列の内挿の実装はデメリットの方が大きいのかもしれません。文字列内の変数の展開は Web 用途に多いので、Golang では `text/template` や `html/template` を使うべし、というのも何となく理解できます。

PHP や Python3 も [JIT](https://ja.wikipedia.org/wiki/%E5%AE%9F%E8%A1%8C%E6%99%82%E3%82%B3%E3%83%B3%E3%83%91%E3%82%A4%E3%83%A9) 時代に入り、いまやコンパイル型に近い（プリ・コンパイル型）言語。<ruby>型々<rt>・・</rt></ruby>言わないかわりにガタガタとバカにされていた PHP も、PHP7 以降、型をより重視するようになったのもコンパイルや開発（デバッグ）の速度アップには必要だったのかもしれません。

しかし、最新の PHP だと Golang より速いことも多くなった（速いとは言ってない）ので、**ランタイムなしでバイナリを配布できるのが Golang の一番のメリット**かもしれません。つまり、コンパイルうんぬんと言うよりポリシーの問題かと思います。

Golang の Issues でも長く議論されており、「言語の開発者」と「言語の利用者」で重視する点が異なり、なかなか難しそうです。

- [proposal: string interpolation #34174](https://github.com/golang/go/issues/34174) | issues | Golang @ GitHub

それでも、Python では [v3.6 で内挿が実装された](https://docs.python.org/ja/3/whatsnew/3.6.html#pep-498-formatted-string-literals)り、同じ静的型付け言語である Rust でも [v1.58 で内挿が実装された](https://blog.rust-lang.org/2022/01/13/Rust-1.58.0.html#captured-identifiers-in-format-strings)りしているので、やはり **Golang も変数の文字列内展開を標準にして欲しい**なぁ。

Go も[ジェネリクス](https://ja.wikipedia.org/wiki/%E3%82%B8%E3%82%A7%E3%83%8D%E3%83%AA%E3%83%83%E3%82%AF%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0)（型とらわれない処理を記述するための仕組み）が [v1.18 から使えるようになった](https://tip.golang.org/doc/go1.18#generics)ので、おそらく遅かれ早かれ実装されることでしょう。

### 検証バージョン

```
$ go version
go version go1.12.5 linux/amd64
```

### 参考文献

- 「[Goで文字列中で変数展開する](https://qiita.com/joniyjoniy/items/06e18c0819f9e69ae285)」 @ Qiita
- 「[String interpolation in golang](https://medium.com/@channaly/string-interpolation-in-golang-ecb3bcb2d600)」@ Medium