Skip to content

Go 文字列

Go において、文字列は本質的に不変で読み取り専用のバイトシーケンス(byte sequence)です。ここで「バイトシーケンス」とは、文字列の底辺データが一連の順序付けられたバイトで構成され、これらのバイトが連続したメモリスペースを占有することを指します。

リテラル

前述したように、文字列には 2 つのリテラル表現方法があります。通常文字列と生文字列です。

通常文字列

通常文字列は "" 二重引用符で表され、エスケープをサポートしますが、複数行の記述はサポートしていません。以下にいくつかの通常文字列を示します。

go
"これは通常文字列です\n"
"abcdefghijlmn\nopqrst\t\\uvwxyz"
これは通常文字列です
abcdefghijlmn
opqrst  \uvwxyz

生文字列

生文字列はバッククォートで表され、エスケープをサポートせず、複数行の記述をサポートしています。生文字列内のすべての文字は、改行やインデントを含めてそのまま出力されます。

go
`これは生文字列です、改行
  tab インデント、\t タブ文字ですが無効です、改行
  "これは通常文字列です"

  終了
`
これは生文字列です、改行
        tab インデント、\t タブ文字ですが無効です、改行
        "これは通常文字列です"

        終了

アクセス

文字列は本質的にバイトシーケンスであるため、インデックス操作 str[i] は i 番目のバイトを返すように設計されています。構文はスライスと一致しています。例えば、文字列の最初の要素にアクセスします。

go
func main() {
   str := "this is a string"
   fmt.Println(str[0])
}

出力はバイトエンコーディング値であり、文字ではありません。

116

文字列をスライスします。

go
func main() {
   str := "this is a string"
   fmt.Println(string(str[0:4]))
}
this

文字列要素の変更を試みます。

go
func main() {
   str := "this is a string"
   str[0] = 'a' // コンパイルできません
   fmt.Println(str)
}
main.go:7:2: cannot assign to str[0] (value of type byte)

文字列を変更することはできませんが、上書きすることはできます。

go
func main() {
   str := "this is a string"
   str = "that is a string"
   fmt.Println(str)
}
that is a string

変換

文字列はバイトスライスに変換でき、バイトスライスまたはバイトシーケンスも文字列に変換できます。例を以下に示します。

go
func main() {
   str := "this is a string"
   // 明示的な型変換でバイトスライスに
   bytes := []byte(str)
   fmt.Println(bytes)
   // 明示的な型変換で文字列に
   fmt.Println(string(bytes))
}

文字列のコンテンツは読み取り専用で不変であり、変更できませんが、バイトスライスは変更可能です。

go
func main() {
  str := "this is a string"
  fmt.Println(&str)
  bytes := []byte(str)
    // バイトスライスを変更
  bytes = append(bytes, 96, 97, 98, 99)
    // 元の文字列に代入
  str = string(bytes)
  fmt.Println(str)
}

文字列をバイトスライスに変換すると、両者は無関係になります。Go がバイトスライス用に新しいメモリスペースを割り当て、文字列のメモリをそこにコピーするため、バイトスライスを変更しても元の文字列には影響しません。これはメモリセキュリティのための措置です。

この場合、変換する文字列またはバイトスライスが大きい場合、パフォーマンスのオーバーヘッドが高くなります。ただし、unsafe パッケージを使用してコピーなしの変換を実装することもできますが、背後にあるセキュリティ問題は自己責任となります。例えば、以下の例では b1 と s1 のアドレスは同じです。

go
func main() {
  s1 := "hello world"
  b1 := unsafe.Slice(unsafe.StringData(s1), len(s1))
  fmt.Printf("%p %p", unsafe.StringData(s1), unsafe.SliceData(b1))
}
0xe27bb2 0xe27bb2

長さ

文字列の長さは、実際には文字の数ではなく、バイトシーケンスの長さです。ほとんどの場合、ASCII 文字を処理しているため、各文字はちょうど 1 バイトで表され、バイト長と文字数が偶然一致します。文字列の長さを求めるには、組み込み関数 len を使用します。例を以下に示します。

go
func main() {
   str := "this is a string" // 長さは 16 に見える
   str2 := "これは文字列です" // 長さは 7 に見える
   fmt.Println(len(str), len(str2))
}
16 21

中文字列は英文字列より短く見えますが、実際に求められる長さは英文字列より長くなります。これは unicode エンコーディングでは、漢字 1 文字はほとんどの場合 3 バイトを占有し、英字 1 文字は 1 バイトのみを占有するためです。文字列の最初の要素を出力すると、結果がわかります。

go
func main() {
   str := "this is a string"
   str2 := "これは文字列です"
   fmt.Println(string(str[0]))
   fmt.Println(string(str2[0]))
   fmt.Println(string(str2[0:3]))
}
t // 英字 t
è // 中文字符の「断片」(最初のバイト)のエンコーディング値。偶然イタリア語文字 è のエンコーディング値と同じ
这 // 中国漢字

コピー

配列やスライスのコピー方法に似て、文字列のコピーは実際にはバイトスライスのコピーです。組み込み関数 copy を使用します。

go
func main() {
   var dst, src string
   src = "this is a string"
   desBytes := make([]byte, len(src))
   copy(desBytes, src)
   dst = string(desBytes)
   fmt.Println(src, dst)
}

strings.Clone 関数を使用することもできますが、実際の実装はほぼ同じです。

go
func main() {
   var dst, src string
   src = "this is a string"
   dst = strings.Clone(src)
   fmt.Println(src, dst)
}

連結

文字列の連結には + 演算子を使用します。

go
func main() {
   str := "this is a string"
   str = str + " that is a int"
   fmt.Println(str)
}

バイトスライスに変換してから要素を追加することもできます。

go
func main() {
   str := "this is a string"
   bytes := []byte(str)
   bytes = append(bytes, "that is a int"...)
   str = string(bytes)
   fmt.Println(str)
}

上記の 2 つの連結方法のパフォーマンスはどちらも非常に悪いです。一般的には使用できますが、より高いパフォーマンスが要求される場合は、strings.Builder を使用できます。

go
func main() {
   builder := strings.Builder{}
   builder.WriteString("this is a string ")
   builder.WriteString("that is a int")
   fmt.Println(builder.String())
}
this is a string that is a int

トラバース

本文の冒頭で述べたように、Go の文字列は読み取り専用のバイトスライスです。つまり、文字列の構成単位は文字ではなくバイトです。この状況は、文字列をトラバースする際によく遭遇します。例えば、以下のコード。

go
func main() {
  str := "hello world!"
  for i := 0; i < len(str); i++ {
    fmt.Printf("%d,%x,%s\n", str[i], str[i], string(str[i]))
  }
}

例では、バイトの 10 進形式と 16 進形式をそれぞれ出力しています。

104,68,h
101,65,e
108,6c,l
108,6c,l
111,6f,o
32,20,
119,77,w
111,6f,o
114,72,r
108,6c,l
100,64,d
33,21,!

例の文字はすべて ASCII 文字に属するため、1 バイトで表せるため、結果は偶然にも各バイトが各文字に対応しています。しかし、ASCII 以外の文字が含まれている場合、結果は異なります。

go
func main() {
  str := "hello 世界!"
  for i := 0; i < len(str); i++ {
    fmt.Printf("%d,%x,%s\n", str[i], str[i], string(str[i]))
  }
}

通常、中文字符は 3 バイトを占有するため、以下の結果が表示される可能性があります。

104,68,h
101,65,e
108,6c,l
108,6c,l
111,6f,o
32,20,
228,e4,ä
184,b8,¸
150,96,–
231,e7,ç
149,95,•
140,8c,Œ
33,21,!

バイト単位でトラバースすると、中文字符が分解され、文字化けが発生します。Go の文字列は UTF-8 を明確にサポートしており、この状況に対処するには rune 型を使用する必要があります。for range を使用してトラバースする場合、デフォルトのトラバース単位型は rune です。例えば、以下のコード。

go
func main() {
   str := "hello 世界!"
   for _, r := range str {
      fmt.Printf("%d,%x,%s\n", r, r, string(r))
   }
}

出力は以下の通りです。

104,68,h
101,65,e
108,6c,l
108,6c,l
111,6f,o
32,20,
19990,4e16,世
30028,754c,界
33,21,!

rune は本質的に int32 の型エイリアスです。Unicode 文字セットの範囲は 0x0000 - 0x10FFFF 間にあり、最大でも 3 バイトです。合法な UTF-8 エンコーディングの最大バイト数は 4 バイトであるため、int32 を使用して保存するのは当然です。上記の例で文字列を []rune に変換してからトラバースするのも同じ道理です。

go
func main() {
   str := "hello 世界!"
   runes := []rune(str)
   for i := 0; i < len(runes); i++ {
      fmt.Println(string(runes[i]))
   }
}

utf8 パッケージのツールを使用することもできます。

go
func main() {
  str := "hello 世界!"
  for i, w := 0, 0; i < len(str); i += w {
    r, width := utf8.DecodeRuneInString(str[i:])
    fmt.Println(string(r))
    w = width
  }
}

これら 2 つの例の出力は同じです。

TIP

文字列の詳細については、Strings, bytes, runes and characters in Go をご覧ください。

Golang by www.golangdev.cn edit