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

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

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

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

スポンサーリンク

背景

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

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

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

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

String types(文字列型)

文字列型は、文字列値の集合を表します。文字列値は、(空の可能性がある)バイトの列です。バイト数は文字列の長さと呼ばれ、決して負になりません。文字列は不変です。一度作成されると、文字列の内容を変更することは不可能です。事前宣言された文字列型はstringです。これは定義された型です。

文字列sの長さは、組み込み関数lenを使用して調べることができます。文字列が定数の場合、長さはコンパイル時定数です。文字列のバイトは、0からlen(s)-1までの整数インデックスでアクセスできます。そのような要素のアドレスを取得することは不正です。s[i]が文字列のi番目のバイトである場合、&s[i]は無効です。


解説

文字列型とは何か?

文字列型(string) は、テキストデータを扱うための型です。文字列はバイトの列として内部的に保存されます。

たとえ話: 文字列は「本の1ページ」のようなもので、一度印刷されたら内容を書き換えられません(不変性)。新しい内容にするには、新しいページを作る必要があります。

package main

import "fmt"

func main() {
    // 文字列の基本
    var message string = "こんにちは"
    var name string = "太郎"
    
    fmt.Println(message, name)
    
    // 空文字列
    var empty string = ""
    fmt.Printf("空文字列: %q\n", empty)  // ""
}

1. 文字列の基本的な特徴

バイトの列

文字列は内部的にバイトの配列として保存されます。

package main

import "fmt"

func main() {
    str := "Hello"
    
    // 文字列は[]byte(バイトスライス)に変換できる
    bytes := []byte(str)
    fmt.Printf("文字列: %s\n", str)
    fmt.Printf("バイト: %v\n", bytes)  // [72 101 108 108 111]
    fmt.Printf("16進数: % X\n", bytes)  // 48 65 6C 6C 6F
}

長さの取得

package main

import "fmt"

func main() {
    // len()は文字列の「バイト数」を返す
    str1 := "Hello"
    fmt.Println(len(str1))  // 5バイト
    
    // UTF-8では、日本語は1文字3バイト
    str2 := "こんにちは"
    fmt.Println(len(str2))  // 15バイト(5文字 × 3バイト)
    
    // 空文字列
    str3 := ""
    fmt.Println(len(str3))  // 0
}

文字数とバイト数の違い

package main

import (
    "fmt"
    "unicode/utf8"
)

func main() {
    str := "Hello, 世界!"
    
    // バイト数
    byteCount := len(str)
    fmt.Println("バイト数:", byteCount)  // 14
    
    // 文字数(ルーン数)
    runeCount := utf8.RuneCountInString(str)
    fmt.Println("文字数:", runeCount)  // 9
}

2. 不変性(Immutability)

文字列は一度作成したら変更できません。これが文字列の最も重要な特徴です。

文字列は変更できない

package main

import "fmt"

func main() {
    str := "Hello"
    
    // これはエラー!
    // str[0] = 'h'  // cannot assign to str[0]
    
    // 新しい文字列を作る必要がある
    newStr := "h" + str[1:]
    fmt.Println(newStr)  // hello
    
    // 元の文字列は変わらない
    fmt.Println(str)  // Hello
}

文字列の連結は新しい文字列を作る

package main

import "fmt"

func main() {
    str1 := "Hello"
    str2 := "World"
    
    // 連結は新しい文字列を作成する
    str3 := str1 + " " + str2
    fmt.Println(str3)  // Hello World
    
    // str1とstr2は変わらない
    fmt.Println(str1, str2)  // Hello World
}

なぜ不変性が重要か?

package main

import "fmt"

func main() {
    original := "Hello"
    
    // 別の変数に代入
    copy := original
    
    // copyを「変更」(実際は新しい文字列を作成)
    copy = "Goodbye"
    
    // originalは影響を受けない
    fmt.Println("original:", original)  // Hello
    fmt.Println("copy:", copy)          // Goodbye
}

利点:

  1. スレッドセーフ: 複数のゴルーチンで安全に共有できる
  2. 効率的: 同じ内容の文字列は1つだけメモリに保存される(場合による)
  3. 予測可能: 予期しない変更が起きない

3. インデックスアクセス

文字列の各バイトには、インデックスでアクセスできます。

バイト単位のアクセス

package main

import "fmt"

func main() {
    str := "Hello"
    
    // 0から始まるインデックス
    fmt.Printf("str[0] = %c (バイト値: %d)\n", str[0], str[0])  // H (72)
    fmt.Printf("str[1] = %c (バイト値: %d)\n", str[1], str[1])  // e (101)
    fmt.Printf("str[4] = %c (バイト値: %d)\n", str[4], str[4])  // o (111)
    
    // 範囲外アクセスはパニック
    // fmt.Println(str[10])  // panic: index out of range
}

スライス操作

package main

import "fmt"

func main() {
    str := "Hello, World!"
    
    // 部分文字列の取得
    sub1 := str[0:5]   // "Hello"
    sub2 := str[7:12]  // "World"
    sub3 := str[7:]    // "World!" (最後まで)
    sub4 := str[:5]    // "Hello" (最初から)
    
    fmt.Println(sub1, sub2, sub3, sub4)
}

日本語でのインデックスアクセス(注意が必要)

package main

import "fmt"

func main() {
    str := "こんにちは"
    
    // バイト単位でアクセス(推奨されない)
    fmt.Printf("%c\n", str[0])  // Ã (文字化け)
    
    // ルーン(文字)単位でアクセス(推奨)
    runes := []rune(str)
    fmt.Printf("%c\n", runes[0])  // こ
    fmt.Printf("%c\n", runes[1])  // ん
    fmt.Printf("%c\n", runes[2])  // に
}

4. アドレスの取得不可

文字列の要素のアドレスは取得できません。

package main

func main() {
    str := "Hello"
    
    // これはエラー!
    // ptr := &str[0]  // cannot take the address of str[0]
    
    // バイトスライスに変換すればアドレス取得可能
    bytes := []byte(str)
    ptr := &bytes[0]  // OK
    
    _ = ptr
}

理由: 文字列は不変なので、内部表現へのポインタを渡すと、予期しない変更を招く可能性があるため。


5. 文字列の走査

バイト単位の走査(ASCII専用)

package main

import "fmt"

func main() {
    str := "Hello"
    
    // 方法1: for-range
    for i, b := range []byte(str) {
        fmt.Printf("%d: %c (バイト値: %d)\n", i, b, b)
    }
    
    // 方法2: 普通のforループ
    for i := 0; i < len(str); i++ {
        fmt.Printf("%d: %c\n", i, str[i])
    }
}

ルーン(文字)単位の走査(推奨)

package main

import "fmt"

func main() {
    str := "Hello, 世界!"
    
    // for-rangeは自動的にルーン単位で走査
    for i, r := range str {
        fmt.Printf("%d: %c (U+%04X)\n", i, r, r)
    }
    
    // 出力:
    // 0: H (U+0048)
    // 1: e (U+0065)
    // 2: l (U+006C)
    // 3: l (U+006C)
    // 4: o (U+006F)
    // 5: , (U+002C)
    // 6:   (U+0020)
    // 7: 世 (U+4E16)
    // 10: 界 (U+754C)  ← インデックスが10(バイト単位)
    // 13: ! (U+0021)
}

6. 文字列の操作

連結

package main

import (
    "fmt"
    "strings"
)

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

検索・置換

package main

import (
    "fmt"
    "strings"
)

func main() {
    str := "Hello, World!"
    
    // 含まれているか
    fmt.Println(strings.Contains(str, "World"))  // true
    fmt.Println(strings.Contains(str, "Go"))     // false
    
    // 先頭・末尾のチェック
    fmt.Println(strings.HasPrefix(str, "Hello"))  // true
    fmt.Println(strings.HasSuffix(str, "!"))      // true
    
    // 位置を検索
    fmt.Println(strings.Index(str, "World"))  // 7
    fmt.Println(strings.Index(str, "Go"))     // -1 (見つからない)
    
    // 置換
    newStr := strings.Replace(str, "World", "Go", 1)
    fmt.Println(newStr)  // Hello, Go!
    
    // すべて置換
    newStr2 := strings.ReplaceAll("aaa", "a", "b")
    fmt.Println(newStr2)  // bbb
}

分割・結合

package main

import (
    "fmt"
    "strings"
)

func main() {
    // 分割
    str := "apple,banana,cherry"
    fruits := strings.Split(str, ",")
    fmt.Println(fruits)  // [apple banana cherry]
    
    // 空白で分割
    str2 := "Go is awesome"
    words := strings.Fields(str2)
    fmt.Println(words)  // [Go is awesome]
    
    // 結合
    joined := strings.Join(fruits, " and ")
    fmt.Println(joined)  // apple and banana and cherry
}

トリミング

package main

import (
    "fmt"
    "strings"
)

func main() {
    str := "  Hello, World!  "
    
    // 両端の空白を削除
    trimmed := strings.TrimSpace(str)
    fmt.Printf("[%s]\n", trimmed)  // [Hello, World!]
    
    // 特定の文字を削除
    str2 := "!!!Hello!!!"
    trimmed2 := strings.Trim(str2, "!")
    fmt.Println(trimmed2)  // Hello
    
    // 先頭だけ
    trimmed3 := strings.TrimPrefix("Dr. Smith", "Dr. ")
    fmt.Println(trimmed3)  // Smith
}

7. 文字列と他の型の変換

数値との相互変換

package main

import (
    "fmt"
    "strconv"
)

func main() {
    // 文字列 → 整数
    str1 := "123"
    i, err := strconv.Atoi(str1)
    if err == nil {
        fmt.Println(i)  // 123
    }
    
    // 整数 → 文字列
    num := 456
    str2 := strconv.Itoa(num)
    fmt.Println(str2)  // "456"
    
    // 文字列 → 浮動小数点
    str3 := "3.14"
    f, err := strconv.ParseFloat(str3, 64)
    if err == nil {
        fmt.Println(f)  // 3.14
    }
    
    // 浮動小数点 → 文字列
    num2 := 3.14159
    str4 := strconv.FormatFloat(num2, 'f', 2, 64)
    fmt.Println(str4)  // "3.14"
}

バイトスライスとの相互変換

package main

import "fmt"

func main() {
    // 文字列 → []byte
    str := "Hello"
    bytes := []byte(str)
    fmt.Printf("% X\n", bytes)  // 48 65 6C 6C 6F
    
    // []byte → 文字列
    bytes2 := []byte{72, 101, 108, 108, 111}
    str2 := string(bytes2)
    fmt.Println(str2)  // Hello
}

ルーンスライスとの相互変換

package main

import "fmt"

func main() {
    // 文字列 → []rune
    str := "こんにちは"
    runes := []rune(str)
    fmt.Println(len(runes))  // 5 (文字数)
    
    // 個別にアクセス
    for i, r := range runes {
        fmt.Printf("%d: %c\n", i, r)
    }
    
    // []rune → 文字列
    runes2 := []rune{'H', 'e', 'l', 'l', 'o'}
    str2 := string(runes2)
    fmt.Println(str2)  // Hello
}

8. 実用例

例1: バリデーション

package main

import (
    "fmt"
    "strings"
    "unicode"
)

func IsValidEmail(email string) bool {
    return strings.Contains(email, "@") && strings.Contains(email, ".")
}

func IsAlphanumeric(s string) bool {
    for _, r := range s {
        if !unicode.IsLetter(r) && !unicode.IsDigit(r) {
            return false
        }
    }
    return true
}

func main() {
    email := "user@example.com"
    fmt.Println("有効なメール?", IsValidEmail(email))  // true
    
    username := "user123"
    fmt.Println("英数字のみ?", IsAlphanumeric(username))  // true
}

例2: 文字列の整形

package main

import (
    "fmt"
    "strings"
)

func FormatName(firstName, lastName string) string {
    // 両端の空白を削除し、タイトルケースに
    first := strings.TrimSpace(firstName)
    last := strings.TrimSpace(lastName)
    
    first = strings.Title(strings.ToLower(first))
    last = strings.Title(strings.ToLower(last))
    
    return first + " " + last
}

func main() {
    name := FormatName("  TARO  ", "  yamada  ")
    fmt.Println(name)  // Taro Yamada
}

例3: CSVパーサー(簡易版)

package main

import (
    "fmt"
    "strings"
)

func ParseCSVLine(line string) []string {
    return strings.Split(line, ",")
}

func main() {
    csv := "太郎,25,東京"
    fields := ParseCSVLine(csv)
    
    name := fields[0]
    age := fields[1]
    city := fields[2]
    
    fmt.Printf("名前: %s, 年齢: %s, 都市: %s\n", name, age, city)
}

例4: テンプレート置換

package main

import (
    "fmt"
    "strings"
)

func ReplaceTemplate(template string, values map[string]string) string {
    result := template
    for key, value := range values {
        placeholder := "{{" + key + "}}"
        result = strings.ReplaceAll(result, placeholder, value)
    }
    return result
}

func main() {
    template := "こんにちは、{{name}}さん。ようこそ{{city}}へ!"
    values := map[string]string{
        "name": "太郎",
        "city": "東京",
    }
    
    message := ReplaceTemplate(template, values)
    fmt.Println(message)
    // こんにちは、太郎さん。ようこそ東京へ!
}

まとめ: 文字列型で覚えておくべきこと

基本的な特徴

  1. 型名: string
  2. 不変: 一度作成したら変更できない
  3. バイトの列: 内部的には[]byteとして保存
  4. 長さ: len()でバイト数を取得
  5. インデックス: 0からlen(s)-1でアクセス可能

文字数とバイト数

str := "Hello, 世界!"

// バイト数
len(str)  // 14

// 文字数
utf8.RuneCountInString(str)  // 9

走査の方法

// バイト単位(ASCII専用)
for i := 0; i < len(str); i++ {
    fmt.Println(str[i])
}

// ルーン(文字)単位(推奨)
for _, r := range str {
    fmt.Printf("%c\n", r)
}

実用的なアドバイス

package main

import (
    "fmt"
    "strings"
    "unicode/utf8"
)

func main() {
    // 1. 文字列は不変
    str := "Hello"
    // str[0] = 'h'  // エラー!
    newStr := "h" + str[1:]  // 新しい文字列を作る
    
    // 2. 日本語はルーン単位で扱う
    text := "こんにちは"
    runes := []rune(text)
    fmt.Println(len(runes))  // 5文字
    
    // 3. 文字数を数える
    count := utf8.RuneCountInString(text)
    fmt.Println(count)  // 5
    
    // 4. 効率的な連結にはstrings.Builder
    var builder strings.Builder
    for i := 0; i < 100; i++ {
        builder.WriteString("a")
    }
    result := builder.String()
    
    fmt.Println(newStr, result)
}

文字列型は、テキスト処理の基礎です。不変性を理解し、適切なメソッドを使うことで、効率的で安全なコードが書けます!

おわりに 

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

よっしー
よっしー

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

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

コメント

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