Go 原生支持字符串类型,关键字string用于表示字符串数据类型。

1. 底层实现

Go 中的string实际上是一个struct类型,其本身并不真正存储字符串数据,仅是由一个指向底层数组的指针和字符串的长度字段组成,可以在标准库的reflect包中,找到下面代码:

// $GOROOT/src/reflect/value.go

// StringHeader是一个string的运行时表示
type StringHeader struct {
    Data uintptr
    Len int
}

由于string类型实际上只是一个对底层存储的引用,因此实际传参的开销非常小,另外在生成string变量时,会同时存储字节长度len,所以无论字符串长度怎么变化,获取长度的时间都是O(1)。

底层实现图解:

string的底层实现

三个必须记住的特性:

特性含义
不可变任何“修改”操作都会产生新字符串,原串不变
UTF-8 编码源码中的字符串字面量默认是 UTF-8
零值是 ""不是 nil,var s string 是空串
var s string = "Hello"
// s[0] = 'h'        // 编译错误:cannot assign to s[0]
b := []byte(s)       // 想修改就转成切片(会复制一份)

注意:len(s)返回的是字节数,不是字符数。中文、emoji 等多字节字符会让两者不相等。

2. 字面量与引号

Go 中初始化字符串变量,需要在双引号"中定义值,单引号'用于单个字符( 以及 runes )。

var Name string = "zhangsan"
age := "18"
var a char = 'a'

s1 := "hello\nworld"   // 双引号:支持 \n \t \" \\ 等转义
// 输出:
// hello
// world

s2 := `hello\nworld`   // 反引号(原生字符串):原样保留
// 输出:
// hello\nworld

s3 := ""        // 空串,len == 0

Go 中没有也不支持三引号的写法,使用反引号来表示原生字面量。由于所见即所得,原样保留,可跨行的特点,常用于SQL/JSON/正则表达式中

// 双引号版:要写一堆 \\ 来转义
re := "\\d{4}-\\d{2}-\\d{2}"

// 反引号版:所见即所得
re := `\d{4}-\d{2}-\d{2}`

path := `C:\Users\admin\docs`

3. 字符串常见操作

3.1 拼接字符串

a := "hello"
b := "world"

// + 号,最简单的拼接方式
c := a + ", " + b

// 格式化拼接,支持任意类型
c = fmt.Sprintf("%s, %s", a, b) 

// 大量拼接推荐使用 strings.Builder(避免反复分配内存)
var sb strings.Builder
sb.WriteString(a)
sb.WriteString(", ")
sb.WriteString(b)
c = sb.String()

3.2 索引与切片

s := "hello"
fmt.Println(s[0])            // 104(byte 值 'h'),不是字符串!
fmt.Println(string(s[0]))    // "h"

fmt.Println(s[1:4])          // "ell"  左闭右开
fmt.Println(s[:2])           // "he"
fmt.Println(s[3:])           // "lo"
fmt.Println(s[:])            // "hello"(不分配新内存,共享底层数组)

Go 中的切片操作不会复制数据,只是新建一个header,所以非常高效。但需要注意的是,对多字节字符按字节切分会得到乱码。

3.3 遍历

s := "Hi 你"

// for range:按 rune(Unicode 码点)遍历,i 是字节下标
for i, r := range s {
    fmt.Printf("%d %q\n", i, r)
}
/* 输出:
0 'H'
1 'i'
2 ' '
3 '你'
*/


// 传统 for:按字节遍历
for i := 0; i < len(s); i++ {
    fmt.Printf("%d %d\n", i, s[i])
}
/* 输出:
0 72
1 105
2 32
3 228
4 189
5 160
*/

3.4 常用判断与转换

s := "  Hello World  "

strings.Contains(s, "World")     // true     是否包含
strings.HasPrefix(s, "He")       // false    前缀(注意有空格)
strings.HasSuffix(s, "ld")       // true     后缀
strings.Count(s, "l")            // 3        出现次数
strings.Index(s, "World")        // 8        首次出现位置,没有返回 -1
strings.LastIndex(s, "l")        // 9

strings.ToLower(s)               // "  hello world  "
strings.ToUpper(s)               // "  HELLO WORLD  "
strings.TrimSpace(s)             // "Hello World"
strings.Trim(s, " Hd")           // "ello Worl"   掐掉首尾指定字符集
strings.ReplaceAll(s, "l", "L")  // "  HeLLo WorLd  "
strings.Split("a,b,c", ",")      // []string{"a","b","c"}
strings.Join([]string{"a","b"}, "-") // "a-b"
strings.Repeat("ab", 3)          // "ababab"
strings.Fields(" a  b c ")       // []string{"a","b","c"}  按空白切分

3.5 与其他类型互转

// string <-> []byte
b := []byte("abc")
s := string(b)

// string <-> rune 切片
rs := []rune("你好")     // [20320 22909]
s = string(rs)

// 任意类型 -> string
s = strconv.Itoa(123)            // "123"
s = strconv.FormatBool(true)     // "true"
s = strconv.FormatInt(255, 16)   // "ff"
s = fmt.Sprint(3.14)             // "3.14"

// string -> 其他类型(带 error,注意处理)
n, _ := strconv.Atoi("123")            // 123
f, _ := strconv.ParseFloat("3.14", 64) // 3.14
x, _ := strconv.ParseBool("true")      // true