LoginSignup
72
31

Golang の文字列内で変数を展開する方法(Go言語の interpolation 各種)

Last updated at Posted at 2019-05-26

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 文字列 変数 展開」とググっても「連結演算子(+)でしかできない」という情報が多かったので、自分のググラビリティとして。

TL; DR

確かに Go 言語(以下 Golang)には interpolation(補間、内挿)機能はありません。過去に要望があがったり、提案されるも、否定多数により却下されています。

しかし、工夫次第で可能です。

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

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

  1. 変数と文字列を連結演算子(+)でつなげる。
  2. 文字列フォーマット関数 Sprintf を使う。
  3. 大量に置き換える場合は、texthtml ライブラリの template 関数を使う。
  4. 外部パッケージを検討してみる(例えば以下の 2 つ)。
    • go.uber.org/yarpcinterpolate パッケージ
      • 一般的な Parse("My name is ${name}") な使い方をしたい場合
    • github.com/buildkiteinterpolate パッケージ
      • bash${piyo:-piyopiyo} のようにデフォルトも設定したい場合
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)
}

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"
}
Sprintf関数を使うタイプ
import "fmt"

func GetHogeBySprintf(piyo string) string {
    return fmt.Sprintf("fuga %s mogera\n", piyo)
}
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()
}

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

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 パッケージマネージャーに慣れている人向けです。
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)

デフォルト値も設定する例
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 パッケージ

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 のライブラリを除いた、「変数を文字列内に展開する方法」のサンプルです。

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
}
$ 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 🏖

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/templatehtml/template を使うべし、というのも何となく理解できます。

PHP や Python3 も JIT 時代に入り、いまやコンパイル型に近い(プリ・コンパイル型)言語。型々・・言わないかわりにガタガタとバカにされていた PHP も、PHP7 以降、型をより重視するようになったのもコンパイルや開発(デバッグ)の速度アップには必要だったのかもしれません。

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

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

それでも、Python では v3.6 で内挿が実装されたり、同じ静的型付け言語である Rust でも v1.58 で内挿が実装されたりしているので、やはり Golang も変数の文字列内展開を標準にして欲しいなぁ。

Go もジェネリクス(型とらわれない処理を記述するための仕組み)が v1.18 から使えるようになったので、おそらく遅かれ早かれ実装されることでしょう。

検証バージョン

$ go version
go version go1.12.5 linux/amd64

参考文献

72
31
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
72
31