字符串
golang中字符串是一种基本类型(string),是一个不可改变的UTF-8字符序列:
- 一个ASCII码占用1个字节;
- 其它字符根据需要占用2-4个字节;
字符串创建后,就不可改变;即不允许修改。
声明与初始化
golang支持两种类型的字符串字面量:
- 解释型字符串:双引号括起来的字符串(“”),转义字符(如
\n\r等)会被替换掉; - 非解释型字符串:用反引号(键盘左上角上的)括起来的字符串,转义字符不会被解释且可跨行(原样输出);
package main
import "fmt"
func main() {
s1 := "Hello, World!\nThis is a new line."
s2 := `hello\n
world!
line2`
fmt.Println(s1)
fmt.Println(s2)
fmt.Printf("%T", s2)
}
`
>>>
Hello, World!
This is a new line.
hello\n
world!
line2
string
进程 已完成,退出代码为 0
`
长度
len()返回字符串中的字节数(非字符数目),索引操作s[i]返回字符串s中第i个字节的值(非Ascii码返回对应字节的数值)。要获取对应字符数(包含非ASCII字符时)需要使用utf8.RuneCountInString
package main
import (
"fmt"
"unicode/utf8"
)
func main() {
s1 := "W字符"
fmt.Println(len(s1)) // 7 (一个汉字3个字节)
fmt.Println(utf8.RuneCountInString(s1)) // 3
}
比较
字符串比较的方式有如下三种:
-
==:直接比较,区分大小写(效率最高); -
strings.Compare(a,b):返回值为 int, 0 表示两数相等,1 表示 a>b, -1 表示 a<b。区分大小写; -
strings.EqualFold(a,b):直接返回是否相等,不区分大小写。
package main
import (
"fmt"
"strings"
)
func main() {
a := "apple"
b := "banana"
c := "applf"
fmt.Println(strings.Compare(a, b)) // 输出负数,因为 "apple" 在字典序上小于 "banana"
fmt.Println(strings.Compare(b, a)) // 输出正数,因为 "banana" 在字典序上大于 "apple"
fmt.Println(strings.Compare(a, c)) // 输出负数,因为 "apple" 在字典序上小于 "applf"(注意这里不是0)
}
}
strings.Compare 函数会比较两个字符串的字典序(即按照字符的 ASCII 码值顺序),直到找到不同的字符或者到达任一字符串的末尾。
让我们详细分析下 strings.Compare(a, c) 的情况:
- 字符串
a是"apple" - 字符串
c是"applf"
比较这两个字符串时:
- 比较第一个字符:都是
'a',ASCII 码值相同,继续比较下一个字符。 - 比较第二个字符:都是
'p',ASCII 码值相同,继续比较下一个字符。 - 比较第三个字符:都是
'p',ASCII 码值相同,继续比较下一个字符。 - 比较第四个字符:
a是'l',c也是'l',ASCII 码值相同,继续比较下一个字符。 - 比较第五个字符:
a没有字符(到达了字符串的末尾),而c是'f'。因为'f'的 ASCII 码值大于任何不存在的字符(可以认为是无穷大),所以strings.Compare(a, c)会返回一个负数。
拼接
golang中有多种字符串拼接方法:
- 加号(
+):最常用、方便的,但每次拼接都会生成一个新的字符串,效率较低; - fmt.Sprintf():通过格式化字符串方式拼接;
- strings.Join():可以指定拼接时的间隔符;
- bytes.Buffer:可变大小的字节缓冲区,提供方便地读写操作;
- strings.Builder:通过write方法高效地构造字符串,最小化内存拷贝;
一般简单拼接直接使用+,字符串数组/切片拼接为字符串时用Join,多条不相关字符串拼接时用Builder。
package main
import (
"bytes"
"fmt"
"strings"
)
func main() {
var buff bytes.Buffer
buff.WriteString("hello")
buff.WriteRune('字')
buff.WriteString("符")
fmt.Println(buff.String()) // hello字符
var build strings.Builder
build.WriteString("hello")
build.WriteRune('字')
build.WriteString("符")
fmt.Println(build.String()) // hello字符
}
子串
go中可通过切片方式快速获取子串s=src[low:high](指定索引范围,‘左含右不含’)。索引不能越界,否则会导致panic异常。
package main
import (
"fmt"
)
func main() {
s1 := "abcdef"
fmt.Println(s1[1:4], s1[1:], s1[:1]) // bcd bcdef a
fmt.Printf("%T", s1[1:4]) // string
}
对于包含中文的字符串,需要转换为rune后操作(否则会出现乱码):
- 字符串转换为rune切片:
- 切片截取;
- 截取结果转换回string;
package main
import (
"fmt"
"log"
)
func main() {
name := "test中文字符串测试"
runeName := []rune(name)
fmt.Printf("%T\n", runeName)
fmt.Println(runeName)
log.Printf("ori-len: %v, rune-len: %v", len(name), len(runeName))
for i := 2; i <= len(runeName); i++ {
log.Printf("%v-len: %v", i, string(runeName[:i]))
}
}
`
[]int32
[116 101 115 116 20013 25991 23383 31526 20018 27979 35797]
2024/07/01 02:32:03 ori-len: 25, rune-len: 11
2024/07/01 02:32:03 2-len: te
2024/07/01 02:32:03 3-len: tes
2024/07/01 02:32:03 4-len: test
2024/07/01 02:32:03 5-len: test中
2024/07/01 02:32:03 6-len: test中文
2024/07/01 02:32:03 7-len: test中文字
2024/07/01 02:32:03 8-len: test中文字符
2024/07/01 02:32:03 9-len: test中文字符串
2024/07/01 02:32:03 10-len: test中文字符串测
2024/07/01 02:32:03 11-len: test中文字符串测试
`
在 Go 语言中,rune 是 int32 的别名,通常用于表示一个 Unicode 码点(Unicode code point)。当你有一个包含多字节字符(如 UTF-8 编码的字符串)的字符串时,你可能想要以 Unicode 码点的形式来访问或处理这些字符。
[]rune(name) 是一个类型转换表达式,它将一个字符串 name 转换为一个 rune 切片。这样,你就可以按 Unicode 码点迭代字符串中的每个字符,而不仅仅是按字节。这在处理非 ASCII 字符(如中文字符、表情符号等)时特别有用,因为这些字符在 UTF-8 编码中可能占用多个字节。
遍历
有两种遍历方式:下标与range。下标方式遍历时,输出的是每个字节;要输出对应的Unicode字符,需要通过range方式。
package main
import (
"fmt"
)
func main() {
s1 := "W字符"
// 输出7个数值(每个字节对应数值)
for i := 0; i < len(s1); i++ {
fmt.Println(s1[i])
}
// 输出三个字符(分别:W 字 符)
for i, v := range s1 {
fmt.Println(i, string(v))
}
}
`
87
229
173
151
231
172
166
0 W
1 字
4 符
`
修改
golang中字符串内容默认不能修改,若要修改需要转换为[]byte或[]rune类型修改后再转回:
package main
import (
"fmt"
)
func main() {
s1 := "W字符"
b1 := []byte(s1)
fmt.Println(len(b1)) // 7
b1[0] = 'M'
fmt.Println(string(b1)) // M字符
r1 := []rune(s1)
//r1 := []int32(s1)
fmt.Println(len(r1)) // 3
r1[1] = '世'
fmt.Println(string(r1)) //W世符
}
`
7
M字符
3
W世符
`
strings包
strings中包含了一些常用的字符串操作函数,在涉及到字符串修改时,返回新的串。
- 前后缀:HasPrefix、HasSuffix;
- 是否包含:Contains;
- 大小写转换:ToLower、ToUpper、Title;
- 修剪:Trim、Trimleft、TrimRight;TrimSpace去除空白
- 拼接拆分:Join、Split、Fields(根据空白分割);
- 查找位置(-1表示表示未找到):Index、LastIndex、IndexRune;
- 统计数量:Count;
- 重叠串:Repeat,重复n次生成新的串;
- 替换:Replace,可指定替换数量,负数表示全部替换;
Map
func Map(mapping func(rune) rune, s string) string,根据mapping函数修改s中的字符,并返回修改后新的串;若mapping返回负值,则删除对应字符。
package main
import (
"fmt"
"strings"
)
func main() {
rot13 := func(r rune) rune {
switch {
case r >= 'A' && r <= 'Z':
return 'A' + (r-'A'+13)%26
case r >= 'a' && r <= 'z':
return 'a' + (r-'a'+13)%26
}
return r
}
fmt.Println(strings.Map(rot13, "Twas brillig and the slithy gopher..."))
}
`
Gjnf oevyyvt naq gur fyvgul tbcure...
`
Reader
用于读取字符串,实现了io.Reader、io.ReaderAt、io.Seeker、io.WriterTo、io.ByteScanner、io.RuneScanner。
package main
import (
"fmt"
"strings"
)
func main() {
r := strings.NewReader("abcdefghijklmn")
fmt.Println("total len:", r.Len()) // 14
var buf []byte
buf = make([]byte, 5)
readLen, err := r.Read(buf)
fmt.Println("read len:", readLen) // 5
if err != nil {
fmt.Println("error:", err)
}
fmt.Println(string(buf)) // abcde
fmt.Println("remain len:", r.Len()) // 9 读取到了5个 剩余未读是14-5
fmt.Println("size:", r.Size()) // 14 字符串的长度
}
`
total len: 14
read len: 5
abcde
remain len: 9
size: 14
`
Replacer
用于字符串替换;placer中包含要替换字符串的列表,且是线程安全的:
package main
import (
"fmt"
"strings"
)
func main() {
r := strings.NewReplacer("<", ">", ">", "<")
fmt.Println(r.Replace("This is <b>HTML</b>!")) // This is >b<HTML>/b<!
}
strconv包
strconv包用于字符串与其它类型间的转换:
- 整数转字符串:strconv.FormatInt、strconv.FormatUint、strconv.Itoa(相当于
FormatInt(i, 10)); - 布尔转字符串(“true” 或 “false”):strconv.FormatBool;
- 浮点转字符串:strconv.FormatFloat;
- 字符串转整数:strconv.ParseInt、strconv.ParseUint、strconv.Atoi(相当于
ParseInt(s, 10, 0)); - 字符串转布尔:strconv.ParseBool(
1,t,T,TRUE,true,True为真;0,f,F,FALSE,false,False为假); - 字符串转浮点:strconv.ParseFloat;
- appendXXX:在[]byte后追加对应类型;
在转换时,会出现两种类型的错误:
- ErrRange:表示值超过了类型能表示的最大范围。
- ErrSyntax:表示语法错误。
字符串转整数
func ParseInt(s string, base int, bitSize int)(i int64, err error)
- s:要转换的字符串
- base:进位制(2~36 进制),0根据字符串判断进制(0x:16,0:8,其它:10);
- bitSize:指定整数类型(0:int、8:int8、16:int16、32:int32、64:int64)
常用的Atoi,是一个简易版,相当于ParseInt(s, 10, 0)。
浮点数转字符串
func FormatFloat(f float64, fmt byte, prec, bitSize int) string
- f:要转换的浮点数
- fmt:格式标记(b、e、E、f、g、G)
- prec:精度(数字部分的长度,不包括指数部分)
- bitSize:指定浮点类型(32:float32、64:float64)
FormatFloat参数只接收64位浮点,要转换32位浮点可:
- 使用fmt.Sprintf实现;
- 使用FormatFloat:需转为float64,但bitSize使用32;
var v32 float32 = 3.1415926535
var v64 float64 = 3.1415926535
s32 := fmt.Sprintf("%.4f", v32)
s32_1 := strconv.FormatFloat(float64(v32), 'f', 4, 32)
s64 := strconv.FormatFloat(v64, 'f', 4, 64)
格式标记:
- ‘b’ (-ddddp±ddd,二进制指数)
- ‘e’ (-d.dddde±dd,十进制指数)
- ‘E’ (-d.ddddE±dd,十进制指数)
- ‘f’ (-ddd.dddd,没有指数)
- ‘g’ (‘e’:大指数,‘f’:其它情况)
- ‘G’ (‘E’:大指数,‘f’:其它情况)
f := 100.123456789
fmt.Println(strconv.FormatFloat(f, 'b', 5, 32)) // 13123382p-17
fmt.Println(strconv.FormatFloat(f, 'e', 5, 32)) // 1.00123e+02
fmt.Println(strconv.FormatFloat(f, 'E', 5, 32)) // 1.00123E+02
fmt.Println(strconv.FormatFloat(f, 'f', 5, 32)) // 100.12346
fmt.Println(strconv.FormatFloat(f, 'g', 5, 32)) // 100.12
fmt.Println(strconv.FormatFloat(f, 'G', 5, 32)) // 100.12
字符串转浮点数
通过ParseFloat(s string, bitSize int) (float64, error)可方便地把字符串转为float64;若要转换为float32,需要bitSize传32,并把结果直接转换为float32即可。
v := "3.1415926535"
r64, _ := strconv.ParseFloat(v, 64)
r, _ := strconv.ParseFloat(v, 32)
r32 := float32(r)
fmt.Println(r64, r32)
格式化符
对于复杂类型,默认按以下规则打印:
-
struct: {field0 field1 …} -
array, slice: [elem0 elem1 …] -
maps: map[key1:value1 key2:value2 …] -
pointer to above: &{}, &[], &map[]
通用
通用的格式化符:
-
%v:值的默认格式,变量的自然形式(natural format); -
%+v:当打印结构体时,输出结构体成员名与值; -
%#v:当打印结构体时,输出结构体类型名、成员名与值 -
%T:打印类型(相应值的类型); -
%%:打印百分号;
type ListNode struct {
Val int
Next *ListNode
}
tmp := ListNode{
Val: -1,
Next: nil,
}
fmt.Printf(" %v \n %+v \n %#v \n %T \n", tmp, tmp, tmp, tmp)
// {-1 <nil>}
// {Val:-1 Next:<nil>}
// ListNode{Val:-1, Next:(*ListNode)(nil)}
// ListNode
指针
地址格式化符:
-
%p:打印指针的地址(以0x开始);对于切片打印首个元素的地址;
sl := []int{1,2,3}
fmt.Printf(" %v \n %+v \n %#v \n %p \n", sl, sl, sl, sl)
// [1 2 3]
// [1 2 3]
// []int{1, 2, 3}
// 0xc00000c3a0
数值
布尔类型:
-
%t:打印true 或 false
整数:
-
%b:二进制表示 -
%c:相应Unicode码点所表示的字符 -
%d:十进制表示 -
%o:八进制表示 -
%q:单引号围绕的字符字面值 -
%x:十六进制表示,字母形式为小写 a-f -
%X:十六进制表示,字母形式为大写 A-F -
%U:Unicode格式:U+1234,等同于 “U+%04X”
%q格式化符:
n := 65
ch := '字'
s := "字符"
fmt.Printf("%q, %q, %q\n", n, ch, s)
// 'A', '字', "字符"
浮点与复数:
-
%b:指数为2的幂的科学计数法,与 strconv.FormatFloat中的 ‘b’ 转换格式一致。例如 -123456p-78 -
%e:科学计数法,例如 -1234.456e+78 -
%E:科学计数法,例如 -1234.456E+78 -
%f:有小数点而无指数,例如 123.456 -
%g:根据情况选择 %e 或 %f 以产生更紧凑的(无末尾的0)输出 -
%G:根据情况选择 %E 或 %f 以产生更紧凑的(无末尾的0)输出
字符串与字节序列
字符串与byte切片:
-
%s:字符串或切片的无解译字节; -
%q:双引号围绕的字符串; -
%x:十六进制,小写字母,每字节两个字符; -
%X:十六进制,大写字母,每字节两个字符;
s := "字符"
fmt.Printf("%v, %q, %s, %x, % x\n", s, s, s, s, s)
sl := []byte{65, 74, 77}
fmt.Printf("%v, %q, %s, %X, % X\n", sl, sl, sl, sl, sl)
// 字符, "字符", 字符, e5ad97e7aca6, e5 ad 97 e7 ac a6
// [65 74 77], "AJM", AJM, 414A4D, 41 4A 4D
宽度与精度
- 宽度:通过百分号后面的十进制数指定;若未指定宽度,表示除必需之外不作填充;
- 精度(可能有):宽度后跟点号后跟的十进制数指定;若未指定精度,则使用默认精度;如果点号后没有跟数字,表示精度为0。
%f default width, default precision
%9f width 9, default precision
%.2f default width, precision 2
%9.2f width 9, precision 2
%9.f width 9, precision 0
标识符
标识符与填充符号:
-
+: 总是输出数值的正负号;对%q(%+q)会生成全部是ASCII字符的输出(通过转义); -
-: 在输出右边填充空白而不是默认的左边(即从默认的右对齐切换为左对齐); -
#: 切换格式;
八进制数前加0(%#o),十六进制数前加0x(%#x)或0X(%#X),指针去掉前面的0x(%#p);
对%q(%#q),如果strconv.CanBackquote返回真会输出反引号括起来的未转义字符串;
对%U(%#U),如果字符是可打印的,会在输出Unicode格式、空格、单引号括起来的go字面值; -
(空格): 对于数值,正数前加空格而负数前加负号;对于字符串采用%x或%X时(% x或% X)会给各打印的字节之间加空格; -
0: 使用0而不是空格填充,对于数值类型会把填充的0放在正负号后面;
ch := '字'
fmt.Printf("%q, %+q\n", ch, ch)
s := "字符"
fmt.Printf("%q, %+q\n", s, s)
// '字', '\u5b57'
// "字符", "\u5b57\u7b26"
占位符
go支持显示参数占位符,通过在输出格式中指定其输出的顺序即可;参数从1开始,使用中括号括起来。
宽度与精度中的*可通过后面参数替换。
fmt.Printf("%[2]d, %[1]d\n", 11, 22) //22, 11
fmt.Sprintf("%[3]*.[2]*[1]f", 12.0, 2, 6)
// 等价于:fmt.Sprintf("%6.2f", 12.0)
格式化错误
所有格式化错误(如将一个字符串提供给%d),都会输出一个以“%!”开始的错误信息,后跟错误内容。
var i int = 1
fmt.Printf("%s\n", i) //%!s(int=1)
GoStringer & Stringer
Stringer接口包含String()方法;任何类型只要定义了String()方法,就相当于定制了输出(类似C#等中的toString);通过print直接输出或%v输出时,就调用此定制输出:
type Stringer interface {
String() string
}
而GoStringer接口包含了GoString()方法,用于定制%#v的格式化输出:
type GoStringer interface {
GoString() string
}
以定制struct的输出为例:
type Animal struct {
Name string
Age uint
}
// Stringer interface: “native” format for that value
func (a Animal) String() string {
return fmt.Sprintf("Stringer: %v (%d)", a.Name, a.Age)
}
// GoStringer interface: Go syntax for that value (%#v format)
func (a Animal) GoString() string {
return fmt.Sprintf("GoStringer: %v (%d)", a.Name, a.Age)
}
func TestFormat() {
a := Animal{
Name: "Gopher",
Age: 2,
}
fmt.Printf("%v, %#v\n", a, a) // Stringer: Gopher (2), GoStringer: Gopher (2)
fmt.Println(a) // Stringer: Gopher (2)
// 若不定制(即不实现String与GoString),则输出会如下
// {Gopher 2}, format.Animal{Name:"Gopher", Age:0x2}
// {Gopher 2}
}
Scanning
一组读取格式化文本获取值的函数:
- Scan、Scanf和Scanln从标准输入os.Stdin中读取;
- Fscan、Fscanf和Fscanln从指定的io.Reader中读取;
- Sscan、Sscanf和Sscanln从字符串中读取。
Scanln、Fscanln和Sscanln在读取到换行时停止,并要求一次提供一行所有条目;Scanf、Fscanf和Sscanf只有在格式化文本末端有换行时会读取到换行为止,其它函数则将换行符视为空格;Scanf、Fscanf、Sscanf会根据格式字符串解析参数,类似Printf。
要定制自定义格式化输入的类型需要实现Scanner接口:
// Scanner 由自定义类型实现,用于实现该类型的自定义扫描过程。
// 当扫描器需要解析该类型的数据时,会调用其 Scan 方法。
type Scanner interface {
// state 用于获取占位符中的宽度信息,也用于从扫描器中读取数据进行解析。
// verb 是占位符中的动词
Scan(state ScanState, verb rune) error
}
// 由扫描器(Scan 之类的函数)实现,用于给自定义扫描过程提供数据和信息。
type ScanState interface {
// ReadRune 从扫描器中读取一个字符,如果用在 Scanln 类的扫描器中,
// 则该方法会在读到第一个换行符之后或读到指定宽度之后返回 EOF。
// 返回“读取的字符”和“字符编码所占用的字节数”
ReadRune() (r rune, size int, err error)
// UnreadRune 撤消最后一次的 ReadRune 操作,
// 使下次的 ReadRune 操作得到与前一次 ReadRune 相同的结果。
UnreadRune() error
// SkipSpace 为 Scan 方法提供跳过开头空白的能力。
// 根据扫描器的不同(Scan 或 Scanln)决定是否跳过换行符。
SkipSpace()
// Token 用于从扫描器中读取符合要求的字符串,
// Token 从扫描器中读取连续的符合 f(c) 的字符 c,准备解析。
// 如果 f 为 nil,则使用 !unicode.IsSpace(c) 代替 f(c)。
// skipSpace:是否跳过开头的连续空白。返回读取到的数据。
// 注意:token 指向共享的数据,下次的 Token 操作可能会覆盖本次的结果。
Token(skipSpace bool, f func(rune) bool) (token []byte, err error)
// Width 返回占位符中的宽度值以及宽度值是否被设置
Width() (wid int, ok bool)
// 因为上面实现了 ReadRune 方法,所以 Read 方法永远不应该被调用。
// 一个好的 ScanState 应该让 Read 直接返回相应的错误信息。
Read(buf []byte) (n int, err error)
}
Printing
一组格式化输出的函数:
- Print、Println和Printf:将各参数格式化到标准输出os.Stdout;
- Fprint、Fprintln和Fprintf:将各参数格式化到io.Writere中;
- Sprint、Sprintln和Sprintf:将各参数格式化到字符串;
要定制自定义格式化输出的类型需要实现Formatter接口:
// Formatter 由自定义类型实现,用于实现该类型的自定义格式化过程。
// 当格式化器需要格式化该类型的变量时,会调用其 Format 方法。
type Formatter interface {
// f 用于获取占位符的旗标、宽度、精度等信息,也用于输出格式化的结果
// c 是占位符中的动词
Format(f State, c rune)
}
// 由格式化器(Print 之类的函数)实现,用于给自定义格式化过程提供信息
type State interface {
// Formatter 通过 Write 方法将格式化结果写入格式化器中,以便输出。
Write(b []byte) (ret int, err error)
// Formatter 通过 Width 方法获取占位符中的宽度信息及其是否被设置。
Width() (wid int, ok bool)
// Formatter 通过 Precision 方法获取占位符中的精度信息及其是否被设置。
Precision() (prec int, ok bool)
// Formatter 通过 Flag 方法获取占位符中的旗标[+- 0#]是否被设置。
Flag(c int) bool
}
Errorf
go中的返回错误都通过error来表示,errors.New和fmt.Errorf函数用于创建实现error接口的错误对象:
type error interface {
Error() string
}
Errorf功能与Sprintf类似,但将格式化结果字符串包装成error返回,以方便生成所需的error错误类型。
func Dived(i int, j int) (r int, err error) {
if j == 0 {
err = fmt.Errorf("param-2 can not %d", j)
return
}
return i / j, err
}