• 使用反引号(``)双引号("")来定义字符串,反引号表示原生的字符串,即不进行转义。

双引号

  1. 字符串使用双引号括起来,其中相关的转义字符将被替换。
1
str := "hello world! \n Hello \n"   // \n 表示换行
1
2
3
4
5
6
7
8
9
func Example()  {
    str := "hello world! \n Hello \n"
    fmt.Printf("%s", str)

    // Output:
    // hello world!
    //  Hello
    // 
}

反引号

  1. 字符串使用反引号括起来,其中相关的转义字符不会被替换。
1
str := `hello world! \n Hello \n`	// \n 表示换行
1
2
3
4
5
6
7
func Example()  {
    str := `hello world! \n Hello \n`
    fmt.Printf("%s", str)

    // Output:
    // hello world! \n Hello \n
}
  1. 双引号反引号存储的区别。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package main

import "fmt"

func main() {
    // 验证两种形式字符串的区别
    s1 := "hello,\nworld" // 12
    s2 := `hello,\nworld` // 13
    // 可见(双引号中,\n当作转义字符在处理),当作一个字节
    // (反引号中,\n当作两个字符处理)当作两个字节
    fmt.Println(len(s1), len(s2))
    // Output:
    // 12 13
}

字符串

  1. Go语言中的string类型是一种值类型,存储的字符串是不可变的。
  2. 如果需要修改string的内容,需要将string转换为[]byte[]rune,并且修改后的string内容是重新生成的。
  3. Go默认使用UTF-8编码,对Unicode的支持非常好。
  4. 字符串存储结构:
1
2
3
4
5
6
type StringStruct struct {
    // 指向字符串的底层数组的首字节地址,就是一个指针地址
    Data unsafe.Pointer
    // 保存字符串的长度,其实就是int类型大小
    Len uintptr
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
type stringStruct struct {
    str unsafe.Pointer
    len uintptr
}

{
    var aa1 [32]byte
    var ll1 uintptr = 6
    
    // 字符串一样被分配在只读内存上,
    // 只是该底层数组不能操作但是能替换stringStruct.str存储的值
    var sss string = "b"

    // 替换掉原先指向的只读内存位置到aa1栈上的数据,
    // 以下代码是使sss底层数组和aa1相关联起来,操作aa1也就是操作sss
    s2s1 := (*stringStruct)(unsafe.Pointer(&sss))
    s2s1.str = unsafe.Pointer(&aa1)	// 替换该值
    s2s1.len = ll1

    bbb1 := aa1[:ll1]

    bbb1[1] = 'a'

    // [0 97 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]  
    // a     
    // [0 97 0 0 0 0]
    fmt.Println(aa1, sss, bbb1)
}

byte 和 rune

  1. 这两个类型是处理字符相关。
    • type byte = uint8type rune = int32
1
2
3
4
5
6
7
// byte 和 rune
type byte = uint8   // 长度 1B
type rune = int32   // 长度 4B
// 类型string类型的零值是长度为零的字符串,即空字符串 ""

var b byte = 'a'    // ASCII码相对应数值
var r rune = '好'   // Unicode相对应编码

Unicode 转 UTF-8

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
package main

import (
    "fmt"
    "unicode/utf8"
)

func main()  {
    var s string = "hello Go语言"	// 8 + 2*3 = 14

    fmt.Println(len(s)) // 14 字节
    fmt.Println(utf8.RuneCountInString(s)) // 10 字符

    // 把字符串s显示转换为[]byte类型,此时会分配新的底层数组空间而不是共用之前
    slice1 := []byte(s)
    
    // utf-8编码 转 unicode
    //   |      Unicode符号范围   |     UTF-8编码方式,编码模板
    // n |      十六进制          |     二进制
    // --+-----------------------+--------------------------------------------------------
    // 1 | 0000 0000 - 0000 007F |                                              0xxxxxxx
    // 2 | 0000 0080 - 0000 07FF |                                     110xxxxx 10xxxxxx
    // 3 | 0000 0800 - 0000 FFFF |                            1110xxxx 10xxxxxx 10xxxxxx
    // 4 | 0001 0000 - 0010 FFFF |                   11110xxx 10xxxxxx 10xxxxxx 10xxxxxx    utf8最大4字节
    // 5 | 0020 0000 - 03FF FFFF |          111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
    // 6 | 0400 0000 - 7FFF FFFF | 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
    // --+-----------------------+--------------------------------------------------------
    // 232 175 173	=utf8转二进制=> 11101000 10101111 10101101 =转unicode=> 10001011 11101101 => 35821  语
    // 232 168 128	=utf8转二进制=> 11101000 10101000 10000000 =转unicode=> 10001010 00000000 => 35328  言
    fmt.Println(slice1) // [104 101 108 108 111 32 71 111 232 175 173 232 168 128]

    // 把字符串s显示转换为[]rune类型
    slice2 := []rune(s)
    fmt.Println(slice2) // [104 101 108 108 111 32 71 111 35821 35328]

    for i, v := range s { // int, rune
        fmt.Printf("i:%d %#U %d \n", i, v, v)
    }

    /*
     * i:0 U+0068 'h' 104
     * i:1 U+0065 'e' 101
     * i:2 U+006C 'l' 108
     * i:3 U+006C 'l' 108
     * i:4 U+006F 'o' 111
     * i:5 U+0020 ' ' 32
     * i:6 U+0047 'G' 71
     * i:7 U+006F 'o' 111
     * i:8 U+8BED '语' 35821
     * i:11 U+8A00 '言' 35328
     */

    // 232 175 173	=> 35821
    // E8  AF  AD   => 8BED
    
    // 字符串的单个字符是byte也就是uint8类型
    fmt.Printf("%T\n", s[0])    // uint8
    
    // 字符串使用切片后依然是字符串类型
    fmt.Printf("%T\n", s[:])    // string
}

字符串比较

  1. 一般的比较运算符(==!=<<=>=>)通过在内存中接字节比较来实现字符串的对比。
  2. 比较源码函数:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//go:linkname runtime_cmpstring runtime.cmpstring
func runtime_cmpstring(a, b string) int {
    l := len(a)
    // l记录a和b最小的一个长度
    if len(b) < l {	
        l = len(b)
    }
    for i := 0; i < l; i++ { // 遍历
        c1, c2 := a[i], b[i]
        if c1 < c2 {        // a < b 返回 -1
            return -1
        }
        if c1 > c2 {        // a > b 返回 +1
            return +1
        }
    }
    
    // 这里说明前面字串都一样,现在比较谁长 
    // 由于上面遍历的最短长度的所以需要再次判断长度比较
    if len(a) < len(b) {	
        return -1
    }
    if len(a) > len(b) {
        return +1
    }
    // a == b 返回 0
    return 0 
}

字符串长度

  1. len()函数获取字符串所占的字节长度,由字符串的结构可知字符串的长度保存在字符串的第二个字段中。
1
2
// ASCII中 a->97    A->65
fmt.Println('a' > 'A') // true
  1. 内置的len()函数获取的是字节的长度和,而不是字符数量。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package main

import (
    "fmt"
    "unicode/utf8"
)

func main()  {
    s := "其实就是rune"

    fmt.Println(len(s)) // 16  字节  4*3 + 4
    fmt.Println(utf8.RuneCountInString(s)) // 8  字符
}
  1. 字符串的内容(纯字节)可以通过标准索引来获取,在中括号[]内写入索引,索引从0开始。
    • 字符串str的第一个字节 str[0]
    • i 字节 str[i - 1]
    • 最后1个字节 str[len(str) - 1]
1
2
3
4
5
6
7
s1 := "hello, world!"

fmt.Printf("%c\n", s1[0])   // h
fmt.Printf("%c\n", s1[7])   // w

// 不能使用&s1[0]这种形式取地址,因为字符串是不可变类型这种形式的取地址没有任何意义
// 但是获取到s1[0]的地址也是有办法的,通过unsafe
  1. 如果字符串含有中文等字符,可以看到每个中文字符的索引值相差3。
  2. Gorange循环在处理字符串的时候,会自动隐式解码UTF-8字符串,关于循环的处理参看for相关文档。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import (
    "fmt"
)

func main()  {
    s := "其实就是rune"

    // v 其实就是rune类型值
    for k, v := range s{ // 【int, rune】
        fmt.Printf("K: %d, V: %c === %d\n", k, v, v)
    }
    
    // Output:
    // K: 0, V: 其 === 20854
    // K: 3, V: 实 === 23454
    // K: 6, V: 就 === 23601
    // K: 9, V: 是 === 26159
    // K: 12, V: r === 114
    // K: 13, V: u === 117
    // K: 14, V: n === 110
    // K: 15, V: e === 101
}

字符串拼接

  1. 将多个字符串拼接成一个字符串。

+ 拼接

  1. 下面的示例,字符串都是不可变的,每次运算都会产生一个新的字符串,所以会产生很多临时的字符串。
  2. 不仅没用还会给垃圾回收带来额外负担,所以性能比较差。
  3. 数量较少的 + 还行,比如 5 个或以下。
1
2
3
4
5
6
7
// 这种由于编辑器会在代码行尾自动补全分号的缘由,加号必须放在第一行
str := "hello" + 
    ",world"

// += 形式拼接字符串
s := "hello" + ",world"
s += "!"

fmt.Sprintf()

  1. 内部使用[]byte实现,不像直接使用+拼接产生临时的字符串。
  2. 但是内部逻辑比较复杂,很多额外的判断,用到了接口,所以性能一般。
1
fmt.Sprintf("%d:%s", 2021, "Golang") // 2021:Golang

strings.Join()

  1. Join会先根据字符串数组的内容,计算一个拼接之后的长度。
  2. 然后申请对应大小的内存,一个一个字符串填入。
  3. 在已有一个数组的情况下效率很高,但是构造一个本来没有的数据代价也不小。
1
strings.Join([]string{"hello,", "world", "Golang"}, "!!!") // hello,!!!world!!!Golang

bytes.Buffer

  1. 比较理想,可以当成可变字符使用,对内存的增长也有优化。
  2. 如果能预估字符串的长度,可以使用buffer.Grow()接口来设置capacity,就是设置切片容量,避免翻倍扩容造成性能下降。
1
2
3
4
5
var buffer bytes.Buffer
buffer.WriteString("hello")
buffer.WriteString(",")
buffer.WriteString("world!")
fmt.Print(buffer.String())

strings.Builder

  1. 内部通过切片来保存和管理内容。
  2. 切片内部则是通过一个指针指向实际保存内容的数组。
  3. strings.Builder同样也提供了Grow()来支持预定于容量,就是设置切片容量,避免翻倍扩容造成性能下降。
  4. 当可以预定义需要使用的容量时,strings.Builder就能避免因扩容产生新的切片。
  5. strings.Builder是非线程安全的,性能和bytes.Buffer相差无几。
1
2
3
4
var b1 string.Builder
b1.WriteString("hello,")
b1.WriteString("world!")
fmt.Print(b1.String())

字符串处理

  1. 标准库四个对字符串处理包:bytesstringsstrconvunicode
描述
strings 提供了许多如字符串的查询、替换、比较、截断、拆分和合并等功能
bytes 该包也提供了类似strings功能的函数,但是针对和字符串有着相同结构的[]byte类型,因为字符串只是只读,因此逐步构建字符串会导致很多分配和复制。在这种情况下,使用bytes.Buffer类型将更有效
strconv 提供了布尔型、整型数、浮点数和对应字符串的相互转换,还提供了双引号转义相关的转换
unicode 提供了IsDigitIsLetterIsUpper和IsLower等类似功能,用于给定字符分类

判断以某字符串开头或结尾

  1. strings.HasPrefix(s, prefix string) bool 判断是否以某个字符串开头。
  2. strings.HasSuffix(s, suffix string) bool 判断是否以某个字符串结尾。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package main

import (
    "fmt"
    "strings"
)

func main()  {
    s := `快樂 \n Ak`
    fmt.Println(strings.HasPrefix(s, "快樂"))   // true
    fmt.Println(strings.HasPrefix(s, "A"))     // false
    
    // Output:
    // true
    // false
}

字符串分割

  1. strings.Split(s, sep string) []string 使用sep字符串分隔s字符串。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package main

import (
    "fmt"
    "strings"
)

func main()  {
    s := `A,B,C,D,E,F,G,H`
    fmt.Println(strings.Split(s, ","))  // [A B C D E F G H]
    
    // Output:
    // [A B C D E F G H]
}

返回子串索引

  1. strings.Index(s, substr string) int 返回第一次匹配到的索引。
  2. strings.LastIndex(a, substr string) int 返回最后一个匹配到的索引。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package main

import (
    "fmt"
    "strings"
)

func main()  {
    s := `A,B,C,D,E,F,G,H`
    fmt.Println(strings.Index(s, ","))      // 1
    fmt.Println(strings.LastIndex(s, ","))  // 13

    fmt.Println(strings.Index(s, "D"))      // 6
    fmt.Println(strings.LastIndex(s, "D"))  // 6

    fmt.Println(strings.Index(s, "M"))      // -1
    fmt.Println(strings.LastIndex(s, "M"))  // -1
}

字符串连接

  1. strings.Join(a []string, sep string) string 使用sep字符串拼接a字符串切片。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package main

import (
    "fmt"
    "strings"
)

func main()  {
    s := []string{"A","D","G","R","G","S","F"}

    // 注意这里的s必须是切片[]string类型
    fmt.Println(strings.Join(s, ","))   // A,D,G,R,G,S,F
    
    // Output:
    // A,D,G,R,G,S,F
}

字符串替换

  1. strings.Replace(s, old, new string, n int) strings字符串中搜索old字符串并替换成new字符串,n替换个数。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import (
    "fmt"
    "strings"
)

func main()  {
    s := "A,D,G,R,G,S,F,D,G,f,d,D,,FW,A,D"
    old := ",D"
    new := "-A-"

    // strings.Replace(s, old, new string, n int)
    // s 原字符串
    // old 需要被替换旧的字符串
    // new 需要被替换新的字符串
    // 替换个数 n<0 默认替换全部 | n=0 不替换 | n=1 默认替换一个
    fmt.Println(strings.Replace(s, old, new, -1)) // A-A-,G,R,G,S,F-A-,G,f,d-A-,,FW,A-A-
    fmt.Println(strings.Replace(s, old, new, 0))  // A,D,G,R,G,S,F,D,G,f,d,D,,FW,A,D
    fmt.Println(strings.Replace(s, old, new, 1))  // A-A-,G,R,G,S,F,D,G,f,d,D,,FW,A,D
    fmt.Println(strings.Replace(s, old, new, 3))  // A-A-,G,R,G,S,F-A-,G,f,d-A-,,FW,A,D
    fmt.Println(strings.Replace(s, old, new, 30)) // A-A-,G,R,G,S,F-A-,G,f,d-A-,,FW,A-A-
}

统计字符在字符串中的次数

  1. strings.Count(s, substr string) int 统计substr字符串在s字符串中出现的次数。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package main

import (
    "fmt"
    "strings"
)

func main()  {
    s := "A,D,G,R,G,S,F,D,G,f,d,D,,FW,A,D"

    fmt.Println(strings.Count(s, ","))  // 15
    fmt.Println(strings.Count(s, "D"))  // 4
    
    // Output:
    // 15
    // 4
}

判断字符串的包含关系

  1. strings.Contains(s, substr string) bool 判断s字符串是否包含substr字符串。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package main

import (
    "fmt"
    "strings"
)

func main()  {
    s := "A,D,G,R,G,S,F,D,G,f,d,D,,FW,A,D"

    fmt.Println(strings.Contains(s, ",D"))  // true
    fmt.Println(strings.Contains(s, "DD"))  // false
    
    // Output:
    // true
    // false
}

字符串转义符

  1. Go 语言的字符串常见转义符包含回车、换行、单双引号、制表符等,如下表所示。
转义 含义
\r 回车符(返回行首)
\n 换行符(直接跳到下一行的同列位置)
\t 制表符
\' 单引号
\" 双引号
\ 反斜杠
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package main

import (
    "fmt"
)

func main() {
    fmt.Println("str := \"c:\\pprof\\main.exe\"")
    
    // Output:
    // str := "c:\pprof\main.exe"
}

byte和rune类型

  1. 组成每个字符串的元素叫做字符
    • 可以通过遍历或者单个获取字符串元素获得字符。
    • 字符用英文单引号(')包裹起来。
1
2
3
4
5
// 以下字符类型都默认 rune类型
var a := '中'   // rune
var b := 'x'    // rune
fmt.Printf("b:%T\n", b) // b:int32
fmt.Printf("a:%T\n", a) // a:int32
  1. Go 语言的字符有以下两种:
    • uint8类型,或者叫byte型,代表了ASCII码的一个字符。
    • rune类型,代表一个Unicode字符。
  2. 当需要处理中文、日文或者其他复合字符时,则需要用到rune类型。
    • rune类型实际是一个int32
    • Go 使用了特殊的 rune 类型来处理 Unicode,让基于Unicode的文本处理更为方便。
  3. 也可以使用 byte 型进行默认字符串处理。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// 遍历字符串
func traversalString() {
    s := "pprof.cn博客"
    for i := 0; i < len(s); i++ { // 【int, byte】
        fmt.Printf("%v(%c) ", s[i], s[i])
    }
    
    fmt.Println()
    
    for _, r := range s { // 【int, rune】
        fmt.Printf("%v(%c) ", r, r)
    }
    
    fmt.Println()
}

修改字符串

  1. 要修改字符串,需要先将其转换成[]rune[]byte,完成后再转换为string
  2. 无论哪种转换,都会重新分配内存。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func changeString() {
    s1 := "hello"
    // 强制类型转换
    byteS1 := []byte(s1)
    byteS1[0] = 'H'
    fmt.Println(string(byteS1))

    s2 := "博客"
    runeS2 := []rune(s2)
    runeS2[0] = '狗'
    fmt.Println(string(runeS2))
}

// 类型转化,下面可见类型转换都是新分配内存地址
// 内存分布是低字节在前高字节在后排序的,不同的平台不同
var u1 uint16 = 0b00000111_00000011

u2 := (uint8)(u1)

fmt.Println(u2, &u1, &u2)

// Output:
// 3 0xc0000140b0 0xc0000140b2
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
type StringStruct struct {
    Data uintptr
    Len uintptr
}

type SliceStruct struct {
    Data uintptr
    Len uintptr
    Cap uintptr
}

func str() {
    s1 := "hello"

    fmt.Println(&s1) // 0xc0000102d0
    fmt.Printf("%#x\n", *(*uintptr)(unsafe.Pointer(&s1))) // 0x4a116b
    
    // 看一下s1的存储的结构
    s := *(*StringStruct)(unsafe.Pointer(&s1))

    fmt.Printf("%#v\n", s) // main.StringStruct{Data:0x4a116b, Len:0x5}

    byteS1 := []byte(s1)

    // 看看byteS1存储的结构,下面结果可见是从新分配的内存
    ss := *(*SliceStruct)(unsafe.Pointer(&byteS1))

    fmt.Printf("%#v\n", ss) // main.SliceStruct{Data:0xc000074e28, Len:0x5, Cap:0x20}

    // 这里为什么容量是32,比5大那么多,具体参看 []byte(s1) 转换的源码
    fmt.Println(cap(byteS1)) // 32

    // Output:
    // 0xc0000102d0
    // 0x4a116b
    // main.StringStruct{Data:0x4a116b, Len:0x5}
    // main.SliceStruct{Data:0xc000074e28, Len:0x5, Cap:0x20}
    // 32
}

类型转换

  1. Go语言中只有显示类型转换,没有隐式类型转换,该语法只能在两个类型之间支持相互转换的时候使用。
  2. 强制类型转换的基本语法如下:
    • T(表达式):注意区分函数调用情况。因为函数调用与显示转换类型形式相似。
    • T表示要转换的类型。
  3. 如计算直角三角形的斜边长时使用math包的Sqrt()函数。
    • 该函数接收的是float64类型的参数,而变量a和b都是int类型的,这个时候就需要将a和b强制类型转换为float64类型。
1
2
3
4
5
6
7
8
func sqrtDemo() {
    var a, b = 3, 4
    var c int
    
    // math.Sqrt()接收的参数是float64类型,需要强制转换
    c = int(math.Sqrt(float64(a*a + b*b)))
    fmt.Println(c)
}

总结

  1. 字符串被设计成只读数据,这样在多线程时操作字符串时不需要加锁避免并发