Go 字符串


十二月其一,学习 Go 的字符串相关知识。

本文主要总结 Go 字符串的设计、相关 API 用法,此外还会涉及到一些字符编码知识。


有关 Go 字符串的知识有点散乱,大致可分为下面几点:

字符串是基本数据类型

在 Go 语言中,字符串(string)是基本数据类型,零值是空字符串 ""(而不是 nil)。

字符串可以用 “” 和 `` 两种形式表示

有两种表示字面值的形式:”xxx”(解释字符串)、`xxx`(非解释字符串)。

1
2
println("hello, \t pz") // 打印:hello, 	 pz
println(`hello, \t pz`) // 打印:hello, \t pz

解释字符串会替换转义字符,包含下面几种:

转义字符 转义成
\t 制表符
\r 回车符
\n 换行符
\\ 字符 \
\“ 字符 "
\u 或 \U Unicode 字符,例如 “\u8A79” 转义成字符 ‘詹’

字符串可以看做一个只读数组

Go 的字符串在底层是一片连续的内存空间,可以理解为一个由字符组成的数组。

字符串在运行时使用 reflect.StringHeader 表示,长得很像 Slice 在运行时的结构:

1
2
3
4
type StringHeader struct {
Data uintptr
Len int
}

跟其他语言类似,Go 的字符串不能修改,我们可以通过 string 与 []byte 的来回转换,达到修改字符串的目的,但其实得到的是一个新字符串。

有关 Go 字符串数据结构相关的知识,可以移步《Go 语言设计与实现 - 字符串》。

字符串使用 len() 获取长度

通过 len() 函数获取字符串所占字节的长度(the number of bytes),例如 len(str)

字符串可以使用 + 拼接

两个字符串 s1s2 可以通过 s := s1 + s2 拼接在一起。

字符串还可以使用 += 尾接其他的字符串。


接下来总结 Go 字符串编码相关的知识。


前置知识:UniCode & UTF-8

不可避免地,要先讲一点编码前置知识,主要是搞懂两个名词:Unicode、UTF-8。

Unicode 是字符集,它为世界上的每一个字符赋予了一个编码(当然没那么全,现在收录了 14w+ 个字符),是计算机领域的业界标准。比如在 Unicode 中,编码 U+0061 表示字符 a,编码 U+5F20 表示字符 ,编码 U+1F600 表示字符 😀(emoji 也是字符)。

UTF-8 是 Unicode 的编码方式(8-bit Unicode Transformation Format)。

Unicode 是一个大字符集,但是没有规定里面的每个字符编码,应该表示成什么形式。比如字符 a 的 Unicode 码是 U+0061,用二进制表示就是 01100001,但是当实际编码时,是编码成 011000010000000001100001,还是 000000000000000001100001,这是不确定的。Unicode 有 14w+ 个字符,像字符 a 这样编码值小的,用一个字节就可以表示,但是字符 😀 却至少需要三个字节才能表示完。

UTF-8 是 Unicode 众多编码方式的其中一种,也是使用率最高的一种,它使用 1~4 个字节表示一个符号(变长编码)。

有关 Unicode 和 UTF-8 更多的内容,比如 UTF-8 是怎么编码的,移步《字符编码笔记:ASCII,Unicode 和 UTF-8》。

(字符和字节的区别不说了,不会还有人不知道吧,不会吧不会吧 :D)


字符串以 UTF-8 存储,下标是字节

Go 的字符串以 UTF-8 编码方式存储,也就是说,字符串中的一个字符占用的长度是不一定的,可能是 1~4 个字节。

字符串通过下标获取值(str[i]),得到的不是字符,而是字节。

对于像 ASCII 码这样一个字节长度的字符,通过下标获取值并没有关系,但是像汉字这种 UTF-8 需要三个字节编码的字符,使用下标获取就会出现乱码。以 str := "test测试😀" 这个字符串为例拆解:

字符 t e s t 😀
Unicode U+0074 U+0065 U+0073 U+0074 U+6D4B U+8BD5 U+1F600
UTF-8 01110100 01100101 01110011 01110100 11100110 10110101 10001011 11101000 10101111 10010101 11110000 10011111 10011000 10000000
下标 str[0] str[1] str[2] str[3] str[4] str[5] str[6] str[7] str[8] str[9] str[10] str[11] str[12] str[13]

顺带一提,str 的长度(len(str))是 14,它指的也是字节数,而不是字符数。


byte & rune

Go 有两个跟 string 相关的数据类型:byte(字节)和 rune(字符),这两种数据类型的定义如下:

1
2
3
4
5
6
7
8
// byte is an alias for uint8 and is equivalent to uint8 in all ways. It is
// used, by convention, to distinguish byte values from 8-bit unsigned
// integer values.
type byte = uint8

// rune is an alias for int32 and is equivalent to int32 in all ways. It is
// used, by convention, to distinguish character values from integer values.
type rune = int32

可以看出,byte 是 uint8 的类型别名,rune 是 int32 的类型别名(UTF-8 最多 4 个字节,因此字符用 int32 表示即可)。

下面总结了一下两种类型的使用场景:

  • byte

    • 字符串下标

      1
      str[0]
    • ASCII 字符

      1
      var c byte = 'a'
    • 字符串转换成 byte[]

      1
      var bytes []byte = []byte(str)
  • rune

    • 声明一个字符(ASCII 字符变量在没有指明类型时,默认也是 rune 类型)

      1
      2
      var c1 rune = '詹'
      var c2 = 'a' // 注意:此时c2也是rune类型的
    • 字符串转换成字符数组

      1
      var runes []rune = []rune(str)
    • 字符串 For-range

      1
      2
      3
      4
      5
      6
      str := "test测试"
      // 第一个变量表示下标,第二个变量表示字符
      for i, c := range str {
      fmt.Printf("%d:%c ", i, c)
      }
      // 打印结果:0:t 1:e 2:s 3:t 4:测 7:试

      (但是为什么要这么设计?我没查到说法)


博文《Go 字符串拼接的 7 种姿势》总结了字符串的拼接方法,并测试了性能。

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
str := "foo"

// 1. +=
str += "bar"

// 2. fmt.Sprintf()
str = fmt.Sprintf("%s%s", str, "bar")

// 3. strings.Join()
str = strings.Join([]string{str, "bar"}, "")

// 4. []byte + append()
b := append([]byte(str), "bar"...)
str = string(b)

// 5. []byte + copy()
ts := "bar"
n := 5
tsl := len(ts) * n
bs := make([]byte, tsl)
bl := 0
for bl < tsl {
bl += copy(bs[bl:], ts)
}
str = string(bs)

// 6. bytes.Buffer
buf := new(bytes.Buffer)
buf.WriteString("foo")
buf.WriteString("bar")
str = buf.String()

// 7. strings.Builder
var builder strings.Builder
builder.WriteString("foo")
builder.WriteString("bar")
str = builder.String()

下面整理了一下 strings 包的 API(截止到版本 1.17.4)。

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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
// 统计字符串出现的次数
count := strings.Count("abcABCabc", "a")
fmt.Printf("%d", count) // 打印:2

// 是否包含字符串
contains := strings.Contains("abc", "a")
fmt.Printf("%t", contains) // 打印:true

// 是否包含字符串中的所有字符(不需要连续)
containsAny := strings.ContainsAny("abcde", "ae")
fmt.Printf("%t", containsAny) // 打印:true

// 是否包含字符
containsRune := strings.ContainsRune("abc", 'a')
fmt.Printf("%t", containsRune) // 打印:true

// 首次出现字符串的下标,没有则返回 -1
index := strings.Index("abc", "b")
fmt.Printf("%d", index) // 打印:1

// 首次出现字节的下标(如果不存在,返回-1)
indexByte := strings.IndexByte("abcabc", 'a')
fmt.Printf("%d", indexByte) // 打印:0

// 首次出现字符的下标(如果不存在,返回-1)
indexRune := strings.IndexRune("我喜欢你", '你')
fmt.Printf("%d", indexRune) // 打印:9

// 任意字符最早出现的下标(如果不存在,返回-1)
indexAny := strings.IndexAny("我喜欢你", "喜爱")
fmt.Printf("%d", indexAny) // 打印:3

// 字符串最晚出现的下标(如果不存在,返回-1)
lastIndex := strings.LastIndex("abcabc", "a")
fmt.Printf("%d", lastIndex) // 打印:3

// 字符最晚出现的下标(如果不存在,返回-1)
lastIndexAny := strings.LastIndexAny("abcde", "aez")
fmt.Printf("%d", lastIndexAny) // 打印:4

// 字节最晚出现的下标(如果不存在,返回-1)
lastIndexByte := strings.LastIndexByte("abcabc", 'b')
fmt.Printf("%d", lastIndexByte) // 打印:4

// 符合给定方法的最晚出现的字符(如果不存在,返回-1)
lastIndexFunc := strings.LastIndexFunc("abcabc", func(r rune) bool {
return r == 'c'
})
fmt.Printf("%d", lastIndexFunc) // 打印:5

// 分割字符串
split1 := strings.Split("abc", "b")
// 如果分隔字符串是"",切分每个字符(而不是字节)
split2 := strings.Split("我喜欢你", "")
// 如果分隔字符串不存在,返回只含有原始字符串的切片
split3 := strings.Split("abc", "d")
// 如果分隔字符串连续出现多次,返回结果会包含空字符串
split4 := strings.Split("abbbbc", "b")
fmt.Printf("%v %v %v %#v", split1, split2, split3, split4) // 打印:[a c] [我 喜 欢 你] [abc] []string{"a", "", "", "", "c"}

// 分割字符串,指定分割后的字符串数量
splitN1 := strings.SplitN("abcabcabc", "b", 2)
// 如果没有那么多,返回最多的一种切法
splitN2 := strings.SplitN("abcabcabc", "b", 100)
fmt.Printf("%v %v", splitN1, splitN2) // 打印:[a cabcabc] [a ca ca c]

// 分割字符串,分割字符串跟在前一个的末尾
splitAfter := strings.SplitAfter("abc", "b")
fmt.Printf("%v", splitAfter) // 打印:[ab c]

// 分割字符串,分割字符串跟在前一个的末尾,指定分割后的字符串数量
splitAfterN := strings.SplitAfterN("abcabc", "b", 2)
fmt.Printf("%v", splitAfterN) // 打印:[ab cabc]

// 按空白分割字符串,连续多个空白算一个(空白由 unicode.IsSpace 定义)
fields := strings.Fields("abc abc\t\t\tabc")
fmt.Printf("%v", fields) // 打印:[abc abc abc]

// 按给定方法分割字符串,连续多次符合方法算一次
fieldsFunc := strings.FieldsFunc("abbbbbbc", func(r rune) bool {
return r == 'b'
})
fmt.Printf("%v", fieldsFunc) // 打印:[a c]

// 组合字符串
join := strings.Join([]string{"a", "b", "c"}, "|")
fmt.Printf("%s", join) // 打印:a|b|c

// 是否包含前缀
hasPrefix := strings.HasPrefix("abc", "ab")
fmt.Printf("%t", hasPrefix) // 打印:true

// 是否包含后缀
hasSuffix := strings.HasSuffix("abc", "bc")
fmt.Printf("%t", hasSuffix) // 打印:true

// 将字符串的每个字符映射成另一个字符
map0 := strings.Map(func(r rune) rune {
return r + 1
}, "abc")
fmt.Printf("%s", map0) // 打印:bcd

// 重复字符串
repeat := strings.Repeat("a", 3)
fmt.Printf("%s", repeat) // 打印:aaa

// 所有字符转换成大写
toUpper := strings.ToUpper("abCDe我")
fmt.Printf("%s", toUpper) // 打印:ABCDE我

// 所有字符转换成小写
toLower := strings.ToLower("abCDe我")
fmt.Printf("%s", toLower) // 打印:abcde我

// 首字母转换成 title 格式(对于英文字符就是转换成大写)
title := strings.Title("abc")
fmt.Printf("%s", title) // 打印:Abc

// 所有字符转换成 title 格式
toTitle := strings.ToTitle("abCDe我")
fmt.Printf("%s", toTitle) // 打印:ABCDE我

// 所有字符转换成大写,可以设置一些特殊字符映射(如土耳其语、阿塞拜疆语)
toUpperSpecial := strings.ToUpperSpecial(unicode.AzeriCase, "abCDe我")
fmt.Printf("%s", toUpperSpecial) // 打印:ABCDE我

// 所有字符转换成小写,可以设置一些特殊字符映射(如土耳其语、阿塞拜疆语)
toLowerSpecial := strings.ToLowerSpecial(unicode.AzeriCase, "abCDe我")
fmt.Printf("%s", toLowerSpecial) // 打印:abcde我

// 所有字符转换成 title 格式,可以设置一些特殊字符映射(如土耳其语、阿塞拜疆语)
toTitleSpecial := strings.ToTitleSpecial(unicode.TurkishCase, "abCDe我")
fmt.Printf("%s", toTitleSpecial) // 打印:ABCDE我

// 所有无效的 UTF-8 字节转换成字符串
toValidUTF8 := strings.ToValidUTF8("abc\xc5", "d")
fmt.Printf("%s", toValidUTF8) // 打印:abcd

// 去掉两端指定字符串
trim := strings.Trim("aba", "a")
fmt.Printf("%s", trim) // 打印:b

// 去掉左端所有相关字符(只要出现就去掉,无关顺序),如果没有原封不动
trimLeft := strings.TrimLeft("abc", "ba")
fmt.Printf("%s", trimLeft) // 打印:c

// 去掉右端所有相关字符(只要出现就去掉,无关顺序),如果没有原封不动
trimRight := strings.TrimRight("abc", "bc")
fmt.Printf("%s", trimRight) // 打印:a

// 去掉前缀,如果没有原封不动
trimPrefix := strings.TrimPrefix("abc", "a")
fmt.Printf("%s", trimPrefix) // 打印:bc

// 去掉后缀,如果没有原封不动
trimSuffix := strings.TrimSuffix("abc", "c")
fmt.Printf("%s", trimSuffix) // 打印:ab

// 去掉两端空白(空白由 unicode.IsSpace 定义)
trimSpace := strings.TrimSpace(" abc\t")
fmt.Printf("%s", trimSpace) // 打印:abc

// 根据指定方法,去掉两端字符
trimFunc := strings.TrimFunc("abc", func(r rune) bool {
return r == 'a' || r == 'c'
})
fmt.Printf("%s", trimFunc) // 打印:b

// 根据指定方法,去掉左端字符
trimLeftFunc := strings.TrimLeftFunc("abc", func(r rune) bool {
return r == 'a'
})
fmt.Printf("%s", trimLeftFunc) // 打印:bc

// 根据指定方法,去掉右端字符
trimRightFunc := strings.TrimRightFunc("abc", func(r rune) bool {
return r == 'c'
})
fmt.Printf("%s", trimRightFunc) // 打印:ab

// 替换字符串,指定替换次数(次数小于 0 代表全部替换)
replace := strings.Replace("abcabc", "b", "我", -2)
fmt.Printf("%s", replace) // 打印:a我cabc

// 替换全部字符串
replaceAll := strings.ReplaceAll("abcabc", "b", "我")
fmt.Printf("%s", replaceAll) // 打印:a我ca我c

// 忽略大小写,比较字符串是否相等
equalFold := strings.EqualFold("aBc", "AbC")
fmt.Printf("%t", equalFold) // 打印:true

// 将字符串切成两半,返回三个值:左一半、右一半、是否切开(如果没有切开,右一半返回空字符串)
cutBefore, cutAfter, cutFound := strings.Cut("abcabc", "e")
fmt.Printf("%s %s %t", cutBefore, cutAfter, cutFound) // 打印:a cabc true

下面整理了一下 strconv 包的 API(截止到版本 1.17.4)。

另外这篇文章也整理得挺好的:《Golang学习 - strconv 包

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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
// 格式化 int,给定进制(进制 ∈ [2, 36])
formatInt := strconv.FormatInt(10, 2)
fmt.Printf("%s", formatInt) // 打印:1010

// 格式化 uint,给定进制(进制 ∈ [2, 36])
formatUint := strconv.FormatUint(10, 16)
fmt.Printf("%s", formatUint) // 打印:a

// 格式化 bool
formatBool := strconv.FormatBool(true)
fmt.Printf("%s", formatBool) // 打印:true

// 格式化 float
// 四个参数分别是:数值、格式、精度、数值位长
// 【格式】有很多种,例如 'f' 表示 float 格式,'e' 表示指数格式
// 【精度】与格式有关,表示小数点后数字个数,或总数字个数
// 【数值位长】有两种:32(float32)、64(float64)
formatFloat := strconv.FormatFloat(1.2, 'e', 2, 64)
fmt.Printf("%s", formatFloat) // 打印:1.20e+00

// 格式化复数,规则与 FormatFloat 类似
formatComplex := strconv.FormatComplex(1+2i, 'f', 2, 64)
fmt.Printf("%s", formatComplex) // 打印:(1.00+2.00i)

// 解析 int 字符串
// 三个参数分别是:数值字符串、进制、数值位长(0-64, 0 默认 strconv.IntSize)
parseInt, _ := strconv.ParseInt("10", 10, 32)
fmt.Printf("%d", parseInt) // 打印:10

// 解析 uint 字符串,参数同 ParseInt
parseUint, _ := strconv.ParseUint("10", 10, 32)
fmt.Printf("%d", parseUint) // 打印:10

// 解析 bool 字符串
parseBool, _ := strconv.ParseBool("true")
fmt.Printf("%t", parseBool) // 打印:true

// 解析 float 字符串,第二个参数是数值位长
parseFloat, _ := strconv.ParseFloat("1.20", 32)
fmt.Printf("%.1f", parseFloat) // 打印:1.2

// 解析复数字符串,第二个参数是数值位长
parseComplex, _ := strconv.ParseComplex("1+2i", 32)
fmt.Printf("%.1f", parseComplex) // 打印:(1.0+2.0i)

// 解析 int 字符串,等同于 strconv.ParseInt(s, 10, 0)
atoi, _ := strconv.Atoi("10")
fmt.Printf("%d", atoi) // 打印:10

// 转 int 为字符串,等同于 strconv.FormatInt(int64(i), 10)
itoa := strconv.Itoa(10)
fmt.Printf("%s", itoa) // 打印:10

// 字节数组追加 int 字节
// 三个参数分别是:原始字节数组、追加数值、追加数值进制
appendInt := strconv.AppendInt([]byte("123"), 10, 2)
fmt.Printf("%s", appendInt) // 打印:1231010

// 字节数组追加 uint 字节,参数同 AppendInt
appendUint := strconv.AppendUint([]byte("123"), 10, 2)
fmt.Printf("%s", appendUint) // 打印:1231010

// 字节数组追加 bool 字节
appendBool := strconv.AppendBool([]byte("123"), true)
fmt.Printf("%s", appendBool) // 打印:123true

// 字节数组追加 float 字节
appendFloat := strconv.AppendFloat([]byte("123"), 5.6, 'e', 2, 32)
fmt.Printf("%s", appendFloat) // 打印:1235.60e+00

// 字符串引用
quote := strconv.Quote("123\t456")
fmt.Printf("%s", quote) // 打印:"123\t456"

// 字符引用
quoteRune := strconv.QuoteRune('1')
fmt.Printf("%s", quoteRune) // 打印:'1'

// 字符串转成 ASCII 引用
QuoteToASCII := strconv.QuoteToASCII("我")
fmt.Printf("%s", QuoteToASCII) // 打印:"\u6211"

// 字符转成 ASCII 引用
quoteRuneToASCII := strconv.QuoteRuneToASCII('我')
fmt.Printf("%s", quoteRuneToASCII) // 打印:'\u6211'

// 字符串转成 unicode 图形字符串引用(由 strconv.IsGraphic() 定义)
quoteToGraphic := strconv.QuoteToGraphic("!\u00a0")
fmt.Printf("%s", quoteToGraphic) // 打印:"! "

// 字符转成 unicode 图形字符引用(由 strconv.IsGraphic() 定义)
quoteRuneToGraphic := strconv.QuoteRuneToGraphic('a')
fmt.Printf("%s", quoteRuneToGraphic) // 打印:"a"

// 字节数组追加引用字符串引用
appendQuote := strconv.AppendQuote([]byte("123"), "456\t")
fmt.Printf("%s", appendQuote) // 打印:123"456\t"

// 字节数组追加引用字符引用
appendQuoteRune := strconv.AppendQuoteRune([]byte("123"), '4')
fmt.Printf("%s", appendQuoteRune) // 打印:123'4'

// 字节数组追加 ASCII 字符引用
appendQuoteToASCII := strconv.AppendQuoteToASCII([]byte("123"), "我")
fmt.Printf("%s", appendQuoteToASCII) // 打印:123"\u6211"

// 字节数组追加 unicode 图形字符串引用
appendQuoteToGraphic := strconv.AppendQuoteToGraphic([]byte("123"), "4")
fmt.Printf("%s", appendQuoteToGraphic) // 打印:123"4"

// 字节数组追加 unicode 图形字符引用
appendQuoteRuneToGraphic := strconv.AppendQuoteRuneToGraphic([]byte("123"), '4')
fmt.Printf("%s", appendQuoteRuneToGraphic) // 打印:123'4'

// 解析引用字符串
unquote, _ := strconv.Unquote(`"123\t456"`)
fmt.Printf("%s", unquote) // 打印:123 456

// 反转义解码字符串的首个字符
//
// s : 待解码的字符串
// quote: 转义字符,可以是单引号('\'')或双引号('"'),否则表示没有转义字符
//
// value : 首个解码出的字符
// multibyte: 该字符是否是多字节的(比如'a'是单字节,'我'是多字节)
// tail : 还未解码的剩余字符串
// err : error,目前只有一种,就是解析错误(invalid syntax)
//
// 下面代码以解析【\"我\"】为例
// 将这个转义字符串,逐个字符地解析出来
var (
value rune
multibyte bool
tail string = `\"我\"`
err error
)
for len(tail) > 0 {
value, multibyte, tail, err = strconv.UnquoteChar(tail, '"')
if err != nil {
fmt.Printf("err: %s", err)
break
}
fmt.Printf("解码字符:%c\t是否多字节:%t\t剩余字符串:%s\n", value, multibyte, tail)
}
// 打印:
// 解码字符:" 是否多字节:false 剩余字符串:我\"
// 解码字符:我 是否多字节:true 剩余字符串:\"
// 解码字符:" 是否多字节:false 剩余字符串:

// 字符是否可显示(可显示的定义,跟通常所想并不一样)
isPrint1 := strconv.IsPrint('a')
isPrint2 := strconv.IsPrint(' ')
isPrint3 := strconv.IsPrint('\t')
fmt.Printf("%t %t %t", isPrint1, isPrint2, isPrint3) // 打印:true true false

// 字符是否是一个 Unicode 图形字符
isGraphic1 := strconv.IsGraphic('a')
isGraphic2 := strconv.IsGraphic('\t')
fmt.Printf("%t %t", isGraphic1, isGraphic2) // 打印:true false

// 字符串是否可以不被修改地表示为一个单行的反引号字符串
canBackquote1 := strconv.CanBackquote("abc")
canBackquote2 := strconv.CanBackquote("`abc`")
fmt.Printf("%t %t", canBackquote1, canBackquote2) // 打印:true, false