一个字符串是一个不可改变的字节序列,字符串通常是用来包含人类可读的文本数据。和数组不同的是,字符串的元素不可修改,是一个只读的字节数组。每个字符串的长度虽然也是固定的,但是字符串的长度并不是字符串类型的一部分。由于 Go 语言的源代码要求是 UTF8 编码,导致 Go 源代码中出现的字符串面值常量一般也是 UTF8 编码的。源代码中的文本字符串通常被解释为采用 UTF8 编码的 Unicode 码点(rune)序列。
因为字节序列对应的是只读的字节序列,因此字符串可以包含任意的数据,包括 byte 值 0。我们也可以用字符串表示 GBK 等非 UTF8 编码的数据,不过这种时候将字符串看作是一个只读的二进制数组更准确,因为 for range 等语法并不能支持非 UTF8 编码的字符串的遍历。
Go 中的字符串都是采用 UTF-8
字符集编码。字符串是用一对双引号(""
)或反引号括起来定义,它的类型是 string
。
//示例代码
var frenchHello string // 声明变量为字符串的一般方法
var emptyString string = "" // 声明了一个字符串变量,初始化为空字符串
func test() {
no, yes, maybe := "no", "yes", "maybe" // 简短声明,同时声明多个变量
japaneseHello := "Konichiwa" // 同上
frenchHello = "Bonjour" // 常规赋值
}
如果要声明一个多行的字符串怎么办?可以通过`` `来声明:
m := `hello
world`
括起的字符串为 Raw 字符串,即字符串在代码中的形式就是打印时的形式,它没有字符转义,换行也将原样输出。我们也可以在定义字符串时直接将其转化为 byte 数组:
data := []byte(`{
"person": {
"name": {
"first": "Leonid",
"last": "Bugaev",
"fullName": "Leonid Bugaev"
},
"github": {
"handle": "buger",
"followers": 109
},
"avatars": [
{ "url": "https://avatars1.githubusercontent.com/u/14009?v=3&s=460", "type": "thumbnail" }
]
},
"company": {
"name": "Acme"
}
}`)
type Inventory struct {
Material string
Count uint
}
sweaters := Inventory{"wool", 17}
tmpl, err := template.New("test").Parse("{{.Count}} items are made of {{.Material}}")
if err != nil { panic(err) }
err = tmpl.Execute(os.Stdout, sweaters)
if err != nil { panic(err) }
字符串和数组类似,内置的 len 函数返回字符串的长度。也可以通过 reflect.StringHeader 结构访问字符串的长度:
fmt.Println("len(s):", (*reflect.StringHeader)(unsafe.Pointer(&s)).Len) // 12
fmt.Println("len(s1):", (*reflect.StringHeader)(unsafe.Pointer(&s1)).Len) // 5
fmt.Println("len(s2):", (*reflect.StringHeader)(unsafe.Pointer(&s2)).Len) // 5
在 Go 中字符串是不可变的,例如下面的代码编译时会报错:cannot assign to s[0]
var s string = "hello"
s[0] = 'c'
但如果真的想要修改怎么办呢?下面的代码可以实现:
s := "hello"
c := []byte(s)// 将字符串 s 转换为 []byte 类型
c[0] = 'c'
s2 := string(c)// 再转换回 string 类型
fmt.Printf("%s\n", s2)
Go 中可以使用 +
操作符来连接两个字符串:
s := "hello,"
m := " world"
a := s + m
fmt.Printf("%s\n", a)
修改字符串也可写为:
s := "hello"
s = "c" + s[1:] // 字符串虽不能更改,但可进行切片操作
fmt.Printf("%s\n", s)
在日常的生活和工作中,我们经常拿到一些单词的命名字符串,需要将它转换为各种各样格式的命名,像是在程序中,你原本已经定义了某个命名,但你可能会需要将其转为一个或多个 const 常量,这时候如果你人工一个个的修改,那就太繁琐了,并且还有可能改错,如此来往多次,那这工作效率实在是太低了。
实际上我们可以通过编写一个小工具来实现这个功能,一来能够满足自己的需求,二来也能不断迭代,甚至满足一些定制化需求。
func ToUpper(s string) string {
return strings.ToUpper(s)
}
func ToLower(s string) string {
return strings.ToLower(s)
}
func UnderscoreToUpperCamelCase(s string) string {
s = strings.Replace(s, "_", " ", -1)
s = strings.Title(s)
return strings.Replace(s, " ", "", -1)
}
func UnderscoreToLowerCamelCase(s string) string {
s = UnderscoreToUpperCamelCase(s)
return string(unicode.ToLower(rune(s[0]))) + s[1:]
}
func CamelCaseToUnderscore(s string) string {
var output []rune
for i, r := range s {
if i == 0 {
output = append(output, unicode.ToLower(r))
continue
}
if unicode.IsUpper(r) {
output = append(output, '_')
}
output = append(output, unicode.ToLower(r))
}
return string(output)
}
Go 语言字符串的底层结构在 reflect.StringHeader 中定义:
type StringHeader struct {
Data uintptr
Len int
}
字符串结构由两个信息组成:第一个是字符串指向的底层字节数组,第二个是字符串的字节的长度。字符串其实是一个结构体,因此字符串的赋值操作也就是 reflect.StringHeader 结构体的复制过程,并不会涉及底层字节数组的复制。在前面数组一节提到的 [2]string
字符串数组对应的底层结构和 [2]reflect.StringHeader
对应的底层结构是一样的,可以将字符串数组看作一个结构体数组。
Hello, world
本身对应的内存结构:
其底层数据和以下数组是完全一致的:
var data = [...]byte{
'h', 'e', 'l', 'l', 'o', ',', ' ', 'w', 'o', 'r', 'l', 'd',
}
字符串虽然不是切片,但是支持切片操作,不同位置的切片底层也访问的同一块内存数据(因为字符串是只读的,相同的字符串面值常量通常是对应同一个字符串常量):
s := "hello, world"
hello := s[:5]
world := s[7:]
s1 := "hello, world"[:5]
s2 := "hello, world"[7:]
根据 Go 语言规范,Go 语言的源文件都是采用 UTF8 编码。因此,Go 源文件中出现的字符串面值常量一般也是 UTF8 编码的(对于转义字符,则没有这个限制)。提到 Go 字符串时,我们一般都会假设字符串对应的是一个合法的 UTF8 编码的字符序列。可以用内置的 print 调试函数或 fmt.Print 函数直接打印,也可以用 for range 循环直接遍历 UTF8 解码后的 Unicode 码点值。
下面的“Hello, 世界”字符串中包含了中文字符,可以通过打印转型为字节类型来查看字符底层对应的数据:
fmt.Printf("%#v\n", []byte("Hello, 世界"))
// []byte{0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x2c, 0x20, 0xe4, 0xb8, 0x96, 0xe7, 0x95, 0x8c}
0xe4, 0xb8, 0x96 对应中文“世”,0xe7, 0x95, 0x8c 对应中文“界”。我们也可以在字符串面值中直指定 UTF8 编码后的值(源文件中全部是 ASCII 码,可以避免出现多字节的字符)。
fmt.Println("\xe4\xb8\x96") // 打印: 世
fmt.Println("\xe7\x95\x8c") // 打印: 界
下图展示了“Hello, 世界”字符串的内存结构布局: