emahiro/b.log

日々の勉強の記録とか育児の記録とか。

fmt.Fprintf で ``(バッククオート)で囲った文字列をformatしたときにハマった話

Overview

タイトルの通りです。
Go で文字列の中に改行コードが含まれていた場合 "" なら \\n のようにバックスラッシュを使ってエスケープしますが、そもそもの改行コードを含む文字列をバッククオートで出力した場合、自動で改行コードがエスケープされる、という挙動について fmt の使い方でハマるところがあったので備忘録。

改行コードをエスケープする

シンプルなエスケープは以下

func main() {
    s := "ハロー\\n私は太郎です。"
    str := fmt.Sprintf("%s", s)
    fmt.Println(str)
}

// Output: ハロー\n私は太郎です。

https://play.golang.org/p/JlSVd363_sc

バッククオートを使った場合は以下

func main() {
    s := `ハロー\n私は太郎です。`
    str := fmt.Sprintf("%s", s)
    fmt.Println(str)
}

// Output: ハロー\n私は太郎です。

https://play.golang.org/p/cUJKw6wKOdm

ダブルクオテーションで囲った文字列の場合、そのままでは改行コードがエスケープされません。

fmt package で罠にハマる

バッククオートで囲ってる改行コード付き文字列を fmt を使って%sでフォーマット出力するとエスケープされない、という挙動に出会いました。

func main() {
    s := "ハロー\n私は太郎です。"
    str := fmt.Sprintf("%s", s)
    fmt.Println(str)
}

// Output: ハロー
私は太郎です。

https://play.golang.org/p/yKBsxsoJwRD

これは""で囲われてる文字列の改行コードがそのまま評価されて出力結果が改行されます。

func main() {
    s := `ハロー\n私は太郎です。`
    str := fmt.Sprintf("%s", s)
    fmt.Println(str)
}


Output: ハロー\n私は太郎です。

https://play.golang.org/p/cUJKw6wKOdm

バッククオートで囲った時は改行コードは評価されずに文字列として出力されます。

func main() {
    s := `ハロー\n私は太郎です。`
    str := fmt.Sprintf(`"%s"`, s)
    fmt.Println(str)
}

// Output: "ハロー\n私は太郎です。"

https://play.golang.org/p/i6WgbSnT7Mt

`` で囲われた format の中において %s を使って出力のフォーマットを行う場合 "" で囲われた文字列の中に改行コードがエスケープされずにそのまま出力された文字列になります。
ここで困るのは上記のようなエスケープされていない文字列が出力されてしまう場合、これを JSON も Key などに当ててしまったら JSON の内部では "\n" が評価されしまい、意図しない改行が入ってしまいます。

このため、バッククオートで本来エスケープされるはずの改行コードをエスケープした状態にするには以下の2つの方法があります。

  1. %q を使う -> シングルクオートで出力してくれる -> バッククオートで囲った文字列についてエスケープ済みで出力してくれる。
    ref: https://play.golang.org/p/uFaY3w1d1fG

  2. https://pkg.go.dev/strconv#Quote を使う。
    https://pkg.go.dev/strconv#example-Quote に大体使い方は書いてます。

まとめ

%q とても便利だなと思いました。

追記

String literals

"" と `` はそもそも文字列リテラルが違うので出力が異なるのは仕様。(生の文字列リテラルと解釈された文字列リテラル

ref: https://golang.org/ref/spec#String_literals

特に以下の部分

Raw string literals are character sequences between back quotes, as in foo. Within the quotes, any character may appear except back quote. The value of a raw string literal is the string composed of the uninterpreted (implicitly UTF-8-encoded) characters between the quotes; in particular, backslashes have no special meaning and the string may contain newlines. Carriage return characters ('\r') inside raw string literals are discarded from the raw string value.

生の文字列リテラルは暗黙的に UTF-8エンコードされてる、と仕様で書いてある。