目录

Go 语言字符串

字符串是 Go 语言中最常用的数据类型之一。Go 的字符串是 UTF-8 编码的不可变字节序列,原生支持 Unicode。理解字符串的本质和操作方式,是高效处理文本数据的基础。

字符串的本质

Go 中字符串的本质是不可变的字节序列。一旦创建,字符串的内容不能被修改。

package main

import "fmt"

func main() {
    s := "Hello, Go"
    // s[0] = 'h' // 编译错误!字符串是不可变的

    fmt.Println(s)
    fmt.Printf("类型: %T\n", s) // string
}

字符串的底层结构包含两个字段:

// runtime 中的简化定义
type StringHeader struct {
    Data uintptr // 指向字节数组的指针
    Len  int     // 字节长度
}

字符串创建

双引号字面量

双引号包裹的字符串支持转义字符,是最常用的创建方式。

package main

import "fmt"

func main() {
    s1 := "Hello, World"
    s2 := "Go\n语言"      // \n 会被解释为换行符
    s3 := "中文\u0041"     // \u0041 是 Unicode 转义,表示 'A'

    fmt.Println(s1) // Hello, World
    fmt.Println(s2) // Go
                  // 语言
    fmt.Println(s3) // 中文A
}

反引号原始字符串

反引号(`)包裹的字符串是原始字符串,不支持转义,可以跨行,常用于多行文本和正则表达式。

package main

import "fmt"

func main() {
    // 原始字符串,\n 不会被转义
    s1 := `Hello\nWorld`
    fmt.Println(s1) // Hello\nWorld

    // 多行字符串
    sql := `SELECT id, name, email
FROM users
WHERE status = 'active'
ORDER BY name`
    fmt.Println(sql)

    // HTML 模板
    html := `<div class="container">
    <h1>标题</h1>
    <p>这是一段 HTML 代码</p>
</div>`
    fmt.Println(html)
}

字符串与字节(byte / []byte)

byteuint8 的别名,表示一个字节。字符串可以转换为字节切片,反之亦然。

package main

import "fmt"

func main() {
    s := "Hello"

    // 字符串转字节切片
    b := []byte(s)
    fmt.Println(b) // [72 101 108 108 111]

    // 字节切片转字符串
    s2 := string(b)
    fmt.Println(s2) // Hello

    // 通过字节切片修改字符串内容
    b[0] = 'h'
    s3 := string(b)
    fmt.Println(s3) // hello
}

注意:对于包含中文的字符串,按字节操作可能会截断字符。

package main

import "fmt"

func main() {
    s := "你好世界"
    b := []byte(s)

    fmt.Printf("字节长度: %d\n", len(b)) // 12(每个中文字符 3 字节)
    fmt.Printf("字节切片: %v\n", b)

    // 按字节截取可能产生乱码
    fmt.Println(string(b[:3])) // 你(恰好是完整字符)
    fmt.Println(string(b[:2])) // 乱码
}

字符串与 Unicode(rune)

runeint32 的别名,表示一个 Unicode 码点。Go 使用 UTF-8 编码,一个中文字符占 3 个字节,但只有 1 个 rune。

package main

import "fmt"

func main() {
    s := "Hello世界"

    // 转换为 rune 切片
    runes := []rune(s)
    fmt.Println(runes)           // [72 101 108 108 111 19990 30028]
    fmt.Println(string(runes[5])) // 世
    fmt.Printf("rune 数量: %d\n", len(runes)) // 7

    // 单个字符用 rune 表示
    ch := '中' // 单引号表示 rune
    fmt.Printf("rune: %c, Unicode: %U\n", ch, ch)
}

字符串遍历

按字节遍历

package main

import "fmt"

func main() {
    s := "Hello"

    for i := 0; i < len(s); i++ {
        fmt.Printf("字节 %d: %c (%d)\n", i, s[i], s[i])
    }
}

按字节遍历时,s[i] 返回的是 byte 类型。对于包含多字节字符(如中文)的字符串,这种方式会逐个字节处理,无法正确识别字符边界。

按 rune 遍历(for-range)

package main

import "fmt"

func main() {
    s := "Hello世界"

    // for-range 自动按 UTF-8 解码,返回 rune
    for i, ch := range s {
        fmt.Printf("索引 %d: %c (U+%04X)\n", i, ch, ch)
    }
}

for-range 遍历字符串时,索引 i 是字节偏移量,chrune 类型。这种方式能正确处理 Unicode 字符。

常用 strings 包函数

Go 的 strings 包提供了丰富的字符串操作函数。

package main

import (
    "fmt"
    "strings"
)

func main() {
    s := "Hello, World! Go is awesome."

    // 判断是否包含子串
    fmt.Println(strings.Contains(s, "Go"))      // true
    fmt.Println(strings.Contains(s, "Java"))     // false

    // 判断是否包含任意字符
    fmt.Println(strings.ContainsAny(s, "abc"))   // true(包含 'a')

    // 分割字符串
    parts := strings.Split(s, " ")
    fmt.Println(parts) // [Hello, World! Go is awesome.]

    // 连接字符串
    joined := strings.Join(parts, "-")
    fmt.Println(joined) // Hello,-World!-Go-is-awesome.

    // 替换
    replaced := strings.Replace(s, "Go", "Golang", 1)
    fmt.Println(replaced) // Hello, World! Golang is awesome.

    // 替换所有
    replacedAll := strings.ReplaceAll("aaa", "a", "b")
    fmt.Println(replacedAll) // bbb

    // 去除首尾空白
    trimmed := strings.TrimSpace("  hello  ")
    fmt.Printf("[%s]\n", trimmed) // [hello]

    // 去除首尾指定字符
    trimmed2 := strings.Trim("##hello##", "#")
    fmt.Println(trimmed2) // hello

    // 判断前缀/后缀
    fmt.Println(strings.HasPrefix(s, "Hello")) // true
    fmt.Println(strings.HasSuffix(s, "awesome.")) // true

    // 转大小写
    fmt.Println(strings.ToUpper("hello")) // HELLO
    fmt.Println(strings.ToLower("HELLO")) // hello

    // 统计子串出现次数
    fmt.Println(strings.Count("banana", "an")) // 2

    // 查找子串首次出现的位置
    fmt.Println(strings.Index(s, "World")) // 7
    fmt.Println(strings.Index(s, "Java"))  // -1
}

字符串格式化

fmt.Sprintf 用于格式化字符串,类似 C 语言的 sprintf

package main

import "fmt"

func main() {
    name := "Golang"
    version := 1.21
    year := 2023

    // 常用格式化动词
    fmt.Println(fmt.Sprintf("名称: %s", name))          // 名称: Golang
    fmt.Println(fmt.Sprintf("版本: %.2f", version))      // 版本: 1.21
    fmt.Println(fmt.Sprintf("年份: %d", year))           // 年份: 2023
    fmt.Println(fmt.Sprintf("十六进制: %x", 255))        // 十六进制: ff
    fmt.Println(fmt.Sprintf("布尔值: %t", true))         // 布尔值: true
    fmt.Println(fmt.Sprintf("宽度对齐: [%10s]", name))   // 宽度对齐: [    Golang]
    fmt.Println(fmt.Sprintf("左对齐: [%-10s]", name))    // 左对齐: [Golang    ]

    // 组合使用
    result := fmt.Sprintf("[%s] 版本 %.1f 发布于 %d 年", name, version, year)
    fmt.Println(result) // [Golang] 版本 1.2 发布于 2023 年
}

字符串拼接方式对比

Go 中字符串是不可变的,每次拼接都会创建新字符串。不同拼接方式的性能差异很大。

使用 + 运算符

package main

import (
    "fmt"
    "time"
)

func main() {
    start := time.Now()
    s := ""
    for i := 0; i < 10000; i++ {
        s += "a"
    }
    fmt.Printf("+ 拼接耗时: %v\n", time.Since(start))
}

+ 拼接在循环中性能最差,每次都会分配新内存。

使用 strings.Builder

package main

import (
    "fmt"
    "strings"
    "time"
)

func main() {
    start := time.Now()
    var builder strings.Builder

    // 可选:预分配容量
    builder.Grow(10000)

    for i := 0; i < 10000; i++ {
        builder.WriteString("a")
    }
    result := builder.String()
    fmt.Printf("Builder 拼接耗时: %v, 长度: %d\n", time.Since(start), len(result))
}

strings.Builder 是推荐的拼接方式,内部使用可增长的字节缓冲区,性能远优于 +

bytes.Bufferstrings.Builder 性能接近,适用于需要同时处理字节和字符串的场景。当已有字符串切片时,strings.Join 是最简洁高效的拼接方式。

strconv 包常用操作

strconv 包用于字符串与其他基本类型之间的转换。

package main

import (
    "fmt"
    "strconv"
)

func main() {
    // 字符串转整数
    n, err := strconv.Atoi("42")
    if err != nil {
        fmt.Println("转换错误:", err)
    }
    fmt.Println(n) // 42

    // 整数转字符串
    s := strconv.Itoa(42)
    fmt.Println(s) // "42"

    // 字符串转浮点数
    f, _ := strconv.ParseFloat("3.14", 64)
    fmt.Println(f) // 3.14

    // 浮点数转字符串
    s3 := strconv.FormatFloat(3.14159, 'f', 2, 64)
    fmt.Println(s3) // "3.14"

    // 字符串转布尔
    b, _ := strconv.ParseBool("true")
    fmt.Println(b) // true

    // 整数转指定进制字符串
    s2 := strconv.FormatInt(255, 16)
    fmt.Println(s2) // "ff"
}

总结

本篇全面介绍了 Go 语言字符串的核心知识:字符串是不可变的 UTF-8 字节序列,支持双引号字面量和反引号原始字符串两种创建方式。byterune 分别用于按字节和按 Unicode 字符处理字符串。strings 包提供了丰富的字符串操作函数,fmt.Sprintf 用于格式化,strconv 包处理字符串与基本类型的转换。在拼接大量字符串时,推荐使用 strings.Builder 以获得最佳性能。