Go言語入門:言語仕様 -Vol.15-

スポンサーリンク
Go言語入門:言語仕様 -Vol.15- 用語解説
Go言語入門:言語仕様 -Vol.15-
この記事は約21分で読めます。
よっしー
よっしー

こんにちは。よっしーです(^^)

本日は、Go言語の言語仕様について解説しています。

スポンサーリンク

背景

Go言語を学び始めて、公式の「The Go Programming Language Specification(言語仕様書)」を開いてみたものの、「英語で書かれていて読むのが大変…」「専門用語ばかりで何を言っているのかわからない…」と感じたことはありませんか? 実は、多くのGo初心者が同じ壁にぶつかっています。

言語仕様書は、Go言語の「正式な取扱説明書」のような存在です。プログラミング言語がどのように動くのか、どんなルールで書くべきなのかが詳しく書かれていますが、その分、初めて読む人には難しく感じられるのも事実です。

そこでこの記事では、言語仕様書の導入部分を丁寧な日本語訳とともに、初心者の方でも理解しやすい補足説明を加えてお届けします。「強く型付けされている」「ガベージコレクション」「並行プログラミング」といった専門用語も、具体例を交えながらわかりやすく解説していきます。

言語仕様書は難しそうに見えますが、一つひとつの概念を丁寧に読み解いていけば、必ず理解できます。一緒に、Go言語の基礎をしっかり学んでいきましょう!

String literals(文字列リテラル)

文字列リテラルは、文字のシーケンスを連結して得られる文字列定数を表します。文字列リテラルには、生文字列リテラルと解釈される文字列リテラルの2つの形式があります。

生文字列リテラルは、`foo`のように、バッククォート間の文字シーケンスです。クォート内には、バッククォートを除く任意の文字を使用できます。生文字列リテラルの値は、クォート間の解釈されない(暗黙的にUTF-8エンコードされた)文字で構成される文字列です。特に、バックスラッシュは特別な意味を持たず、文字列に改行を含めることができます。生文字列リテラル内のキャリッジリターン文字(‘\r’)は、生文字列値から破棄されます。

解釈される文字列リテラルは、"bar"のように、ダブルクォート間の文字シーケンスです。クォート内には、改行とエスケープされていないダブルクォートを除く任意の文字を使用できます。クォート間のテキストがリテラルの値を形成し、バックスラッシュエスケープはルーンリテラルと同様に解釈されます(\'は不正で\"は正当)。同じ制限が適用されます。3桁の8進数(\nnn)と2桁の16進数(\xnn)のエスケープは、結果の文字列の個々のバイトを表します。他のすべてのエスケープは、個々の文字の(場合によっては複数バイトの)UTF-8エンコーディングを表します。したがって、文字列リテラル内の\377\xFFは値0xFF=255の単一のバイトを表し、ÿ, \u00FF, \U000000FF, \xc3\xbfは文字U+00FFのUTF-8エンコーディングの2バイト0xc3 0xbfを表します。

string_lit             = raw_string_lit | interpreted_string_lit .
raw_string_lit         = "`" { unicode_char | newline } "`" .
interpreted_string_lit = `"` { unicode_value | byte_value } `"` .
`abc`                // "abc"と同じ
`\n
\n`                  // "\\n\n\\n"と同じ
"\n"
"\""                 // `"`と同じ
"Hello, world!\n"
"日本語"
"\u65e5本\U00008a9e"
"\xff\u00FF"
"\uD800"             // 不正: サロゲートハーフ
"\U00110000"         // 不正: 無効なUnicodeコードポイント

これらの例はすべて同じ文字列を表します:

"日本語"                                 // UTF-8入力テキスト
`日本語`                                 // 生リテラルとしてのUTF-8入力テキスト
"\u65e5\u672c\u8a9e"                    // 明示的なUnicodeコードポイント
"\U000065e5\U0000672c\U00008a9e"        // 明示的なUnicodeコードポイント
"\xe6\x97\xa5\xe6\x9c\xac\xe8\xaa\x9e"  // 明示的なUTF-8バイト

ソースコードがアクセント記号と文字を組み合わせた結合形式など、2つのコードポイントとして文字を表現する場合、ルーンリテラルに配置するとエラーになり(単一のコードポイントではないため)、文字列リテラルに配置すると2つのコードポイントとして表示されます。


解説

文字列リテラルとは何か?

文字列リテラルは、プログラム内に直接書かれた文字列の値です。「こんにちは」「Hello」などのテキストデータを表現します。

たとえ話: ルーンが「1文字」を表すのに対し、文字列は「文章」や「単語」を表します。手紙の内容のようなものです。

package main

import "fmt"

func main() {
    // 文字列リテラルの基本
    var message string = "こんにちは、世界!"
    var name string = "太郎"
    
    fmt.Println(message)
    fmt.Println(name)
}

1. 2種類の文字列リテラル

Goには2つの文字列リテラルがあります。

解釈される文字列リテラル(Interpreted string literals)

ダブルクォート(") で囲みます。エスケープシーケンスが解釈されます。

package main

import "fmt"

func main() {
    // ダブルクォートで囲む
    str1 := "Hello, World!"
    str2 := "こんにちは"
    str3 := "Line 1\nLine 2"  // エスケープが解釈される
    
    fmt.Println(str1)
    fmt.Println(str2)
    fmt.Println(str3)
    // 出力:
    // Hello, World!
    // こんにちは
    // Line 1
    // Line 2
}

生文字列リテラル(Raw string literals)

バッククォート(`) で囲みます。エスケープシーケンスは解釈されず、そのままの文字として扱われます。

package main

import "fmt"

func main() {
    // バッククォートで囲む
    raw1 := `Hello, World!`
    raw2 := `こんにちは`
    raw3 := `Line 1\nLine 2`  // \nはそのまま
    
    fmt.Println(raw1)
    fmt.Println(raw2)
    fmt.Println(raw3)
    // 出力:
    // Hello, World!
    // こんにちは
    // Line 1\nLine 2
}

2. 解釈される文字列リテラル(Interpreted)

基本的な使い方

package main

import "fmt"

func main() {
    // 通常のテキスト
    str := "Hello, 世界!"
    fmt.Println(str)
    
    // ダブルクォートを含める場合はエスケープ
    quote := "彼は\"こんにちは\"と言った"
    fmt.Println(quote)  // 彼は"こんにちは"と言った
    
    // バックスラッシュを含める場合もエスケープ
    path := "C:\\Users\\Documents"
    fmt.Println(path)  // C:\Users\Documents
}

エスケープシーケンス

ルーンリテラルと同じエスケープシーケンスが使えます。

package main

import "fmt"

func main() {
    // 改行
    fmt.Println("1行目\n2行目\n3行目")
    
    // タブ
    fmt.Println("列1\t列2\t列3")
    
    // バックスラッシュ
    fmt.Println("ファイルパス: C:\\Program Files\\")
    
    // ダブルクォート
    fmt.Println("彼女は\"ありがとう\"と言った")
    
    // すべてのエスケープシーケンス
    fmt.Println("アラート:\a")  // ベル音
    fmt.Println("バックスペース:\b")
    fmt.Println("改行:\n")
    fmt.Println("タブ:\t")
}

Unicodeエスケープ

package main

import "fmt"

func main() {
    // \u (4桁)
    str1 := "\u3042\u3044\u3046"  // あいう
    fmt.Println(str1)
    
    // \U (8桁)
    str2 := "\U0001F600\U0001F389"  // 😀🎉
    fmt.Println(str2)
    
    // 混在
    str3 := "\u65e5本\U00008a9e"  // 日本語
    fmt.Println(str3)
}

バイトエスケープ

package main

import "fmt"

func main() {
    // \x (2桁16進数) - 1バイトを表す
    str1 := "\x48\x65\x6C\x6C\x6F"  // Hello
    fmt.Println(str1)
    
    // \nnn (3桁8進数) - 1バイトを表す
    str2 := "\110\145\154\154\157"  // Hello
    fmt.Println(str2)
    
    // UTF-8の明示的なバイト指定
    // 「日」= U+65E5 = UTF-8: E6 97 A5
    str3 := "\xe6\x97\xa5"
    fmt.Println(str3)  // 日
}

バイトエスケープとUnicodeエスケープの違い

package main

import "fmt"

func main() {
    // \xFF - 1バイト (255)
    byte1 := "\xFF"
    fmt.Printf("バイト: % X\n", []byte(byte1))  // FF
    
    // \u00FF - 文字 U+00FF (ÿ) = UTF-8で2バイト
    unicode1 := "\u00FF"
    fmt.Printf("Unicode: % X\n", []byte(unicode1))  // C3 BF
    
    // 同じ文字を表す別の方法
    char1 := "ÿ"                    // 直接入力
    char2 := "\u00FF"               // Unicode小
    char3 := "\U000000FF"           // Unicode大
    char4 := "\xc3\xbf"             // UTF-8バイト
    
    fmt.Println(char1 == char2)  // true
    fmt.Println(char2 == char3)  // true
    fmt.Println(char3 == char4)  // true
}

3. 生文字列リテラル(Raw)

基本的な使い方

package main

import "fmt"

func main() {
    // エスケープシーケンスは解釈されない
    raw := `C:\Users\Documents\file.txt`
    fmt.Println(raw)  // C:\Users\Documents\file.txt
    
    // バックスラッシュはそのまま
    raw2 := `\n\t\r`
    fmt.Println(raw2)  // \n\t\r
    
    // ダブルクォートもエスケープ不要
    raw3 := `彼は"こんにちは"と言った`
    fmt.Println(raw3)  // 彼は"こんにちは"と言った
}

複数行の文字列

生文字列の最大の特徴は、改行をそのまま含められることです。

package main

import "fmt"

func main() {
    // 複数行の文字列
    poem := `薔薇は赤い
菫は青い
砂糖は甘い
あなたも甘い`
    
    fmt.Println(poem)
    // 出力:
    // 薔薇は赤い
    // 菫は青い
    // 砂糖は甘い
    // あなたも甘い
}

正規表現に便利

package main

import (
    "fmt"
    "regexp"
)

func main() {
    // 解釈される文字列だとバックスラッシュを2重にする必要がある
    pattern1 := "\\d+\\.\\d+"  // 数字.数字
    
    // 生文字列ならバックスラッシュは1つでOK
    pattern2 := `\d+\.\d+`
    
    fmt.Println(pattern1)  // \d+\.\d+
    fmt.Println(pattern2)  // \d+\.\d+
    
    // 正規表現の使用例
    re := regexp.MustCompile(`\d+\.\d+`)
    fmt.Println(re.FindString("価格は3.14円です"))  // 3.14
}

JSONやHTMLの記述

package main

import "fmt"

func main() {
    // JSON (生文字列が便利)
    json := `{
    "name": "太郎",
    "age": 25,
    "city": "東京"
}`
    
    fmt.Println(json)
    
    // HTML
    html := `<!DOCTYPE html>
<html>
<head>
    <title>サンプルページ</title>
</head>
<body>
    <h1>こんにちは、世界!</h1>
    <p>これはサンプルです。</p>
</body>
</html>`
    
    fmt.Println(html)
}

SQLクエリの記述

package main

import "fmt"

func main() {
    // 複雑なSQLクエリ
    query := `
        SELECT
            u.id,
            u.name,
            u.email,
            COUNT(o.id) as order_count
        FROM users u
        LEFT JOIN orders o ON u.id = o.user_id
        WHERE u.created_at >= '2024-01-01'
        GROUP BY u.id, u.name, u.email
        ORDER BY order_count DESC
        LIMIT 10
    `
    
    fmt.Println(query)
}

4. キャリッジリターンの扱い

生文字列リテラルでは、**キャリッジリターン(\r)**は自動的に削除されます。

package main

import "fmt"

func main() {
    // Windows形式の改行 (CRLF: \r\n)
    raw := `Line1
Line2`  // エディタによっては\r\nになる可能性がある
    
    // \rは削除され、\nだけが残る
    fmt.Printf("% X\n", []byte(raw))
    
    // 解釈される文字列では明示的に指定可能
    interpreted := "Line1\r\nLine2"
    fmt.Printf("% X\n", []byte(interpreted))
}

5. 同じ文字列の様々な表現

「日本語」という文字列を様々な方法で表現できます。

package main

import "fmt"

func main() {
    // すべて同じ文字列を表す
    
    // 方法1: 直接入力(UTF-8)
    str1 := "日本語"
    
    // 方法2: 生文字列
    str2 := `日本語`
    
    // 方法3: Unicodeコードポイント(\u)
    str3 := "\u65e5\u672c\u8a9e"
    
    // 方法4: Unicodeコードポイント(\U)
    str4 := "\U000065e5\U0000672c\U00008a9e"
    
    // 方法5: UTF-8バイト列
    str5 := "\xe6\x97\xa5\xe6\x9c\xac\xe8\xaa\x9e"
    
    // すべて等しい
    fmt.Println(str1 == str2)  // true
    fmt.Println(str2 == str3)  // true
    fmt.Println(str3 == str4)  // true
    fmt.Println(str4 == str5)  // true
    
    // バイト列で確認
    fmt.Printf("% X\n", []byte(str1))  // E6 97 A5 E6 9C AC E8 AA 9E
    fmt.Printf("% X\n", []byte(str5))  // E6 97 A5 E6 9C AC E8 AA 9E
}

6. 結合文字とコードポイント

アクセント記号などの結合文字は、2つのコードポイントで表現されることがあります。

package main

import (
    "fmt"
    "unicode/utf8"
)

func main() {
    // é (単一コードポイント)
    str1 := "\u00e9"  // é (U+00E9)
    
    // e + ´ (2つのコードポイント)
    str2 := "e\u0301"  // e (U+0065) + ´ (U+0301)
    
    fmt.Println("str1:", str1)  // é
    fmt.Println("str2:", str2)  // é
    
    // 見た目は同じだが...
    fmt.Println("等しい?", str1 == str2)  // false
    
    // ルーン数が異なる
    fmt.Println("str1のルーン数:", utf8.RuneCountInString(str1))  // 1
    fmt.Println("str2のルーン数:", utf8.RuneCountInString(str2))  // 2
    
    // ルーンリテラルでは2つのコードポイントは使えない
    // var r rune = 'e\u0301'  // エラー!
}

7. 実用例

例1: ファイルパスの扱い

package main

import "fmt"

func main() {
    // Windowsパス(解釈される文字列)
    path1 := "C:\\Users\\太郎\\Documents\\file.txt"
    
    // Windowsパス(生文字列 - 推奨)
    path2 := `C:\Users\太郎\Documents\file.txt`
    
    fmt.Println(path1)
    fmt.Println(path2)
    
    // Unixパス(バックスラッシュ不要)
    path3 := "/home/taro/documents/file.txt"
    fmt.Println(path3)
}

例2: テンプレート文字列

package main

import "fmt"

func main() {
    // メールテンプレート
    template := `
件名: ご注文確認

%s 様

この度はご注文いただき、ありがとうございます。

ご注文内容:
- 商品名: %s
- 数量: %d
- 金額: %d円

発送まで今しばらくお待ちください。

株式会社サンプル
`
    
    email := fmt.Sprintf(template, "山田太郎", "Go言語入門", 1, 3000)
    fmt.Println(email)
}

例3: ログメッセージ

package main

import (
    "fmt"
    "time"
)

func main() {
    // 構造化ログ
    logMessage := fmt.Sprintf(
        `{"timestamp":"%s","level":"INFO","message":"ユーザーがログインしました","user_id":12345}`,
        time.Now().Format(time.RFC3339),
    )
    
    fmt.Println(logMessage)
}

例4: 文字列連結

package main

import (
    "fmt"
    "strings"
)

func main() {
    // + 演算子
    str1 := "Hello, " + "World!"
    fmt.Println(str1)
    
    // strings.Join
    parts := []string{"Go", "is", "awesome"}
    str2 := strings.Join(parts, " ")
    fmt.Println(str2)
    
    // strings.Builder (効率的)
    var builder strings.Builder
    builder.WriteString("こん")
    builder.WriteString("にち")
    builder.WriteString("は")
    str3 := builder.String()
    fmt.Println(str3)
}

8. 文字列の操作

長さの取得

package main

import (
    "fmt"
    "unicode/utf8"
)

func main() {
    str := "こんにちは"
    
    // バイト数
    byteLen := len(str)
    fmt.Println("バイト数:", byteLen)  // 15 (1文字3バイト × 5文字)
    
    // ルーン(文字)数
    runeLen := utf8.RuneCountInString(str)
    fmt.Println("文字数:", runeLen)  // 5
}

部分文字列の取得

package main

import "fmt"

func main() {
    str := "Hello, 世界!"
    
    // バイト単位のスライス(注意が必要)
    sub1 := str[0:5]  // "Hello"
    fmt.Println(sub1)
    
    // ルーン単位で扱う(推奨)
    runes := []rune(str)
    sub2 := string(runes[0:5])  // "Hello"
    sub3 := string(runes[7:9])  // "世界"
    
    fmt.Println(sub2, sub3)
}

文字列の走査

package main

import "fmt"

func main() {
    str := "Go言語"
    
    // バイト単位(非推奨)
    for i := 0; i < len(str); i++ {
        fmt.Printf("%d: %X\n", i, str[i])
    }
    
    fmt.Println()
    
    // ルーン単位(推奨)
    for i, r := range str {
        fmt.Printf("%d: %c (U+%04X)\n", i, r, r)
    }
}

まとめ: 文字列リテラルで覚えておくべきこと

2種類の文字列リテラル

種類記号エスケープ改行用途
解釈される文字列"..."解釈される不可通常の文字列
生文字列`...`解釈されない可能パス、正規表現、複数行

エスケープシーケンス

"\n"   // 改行
"\t"   // タブ
"\\"   // バックスラッシュ
"\""   // ダブルクォート
"\u3042"  // Unicode (4桁)
"\U0001F600"  // Unicode (8桁)
"\xFF"  // バイト (16進数)
"\377"  // バイト (8進数)

使い分けのガイドライン

package main

import "fmt"

func main() {
    // 通常のテキスト → 解釈される文字列
    message := "こんにちは、世界!"
    
    // ファイルパス、正規表現 → 生文字列
    path := `C:\Users\Documents`
    pattern := `\d+\.\d+`
    
    // 複数行テキスト → 生文字列
    html := `
    <html>
        <body>Hello</body>
    </html>
    `
    
    // エスケープが必要 → 解釈される文字列
    quoted := "彼は\"こんにちは\"と言った"
    
    fmt.Println(message, path, pattern, html, quoted)
}

実用的なアドバイス

package main

import (
    "fmt"
    "unicode/utf8"
)

func main() {
    // 文字列は不変(immutable)
    str := "Hello"
    // str[0] = 'h'  // エラー!
    
    // 変更するには新しい文字列を作る
    newStr := "h" + str[1:]
    fmt.Println(newStr)  // hello
    
    // 文字数を数えるときはutf8パッケージを使う
    text := "こんにちは"
    fmt.Println("文字数:", utf8.RuneCountInString(text))
    
    // 文字列の走査はrangeを使う
    for _, r := range text {
        fmt.Printf("%c ", r)
    }
    fmt.Println()
}

文字列リテラルは、Goプログラミングの基本です。解釈される文字列と生文字列を適切に使い分けることで、読みやすく保守しやすいコードが書けます!

おわりに 

本日は、Go言語の言語仕様について解説しました。

よっしー
よっしー

何か質問や相談があれば、コメントをお願いします。また、エンジニア案件の相談にも随時対応していますので、お気軽にお問い合わせください。

それでは、また明日お会いしましょう(^^)

コメント

タイトルとURLをコピーしました