Go从入门到精通——字符串常见用法
字符串应用
字符串类型在业务中的应用可以说是最广泛的,所以本节汇总下常见的用法,会不断的更新补充。
1.1、计算字符串长度
内建函数 len(),可以用来获取切片、字符串、通道(channel)等的长度。
package main import "fmt" func main() { a := "welcome come to Shanghai!" fmt.Println(len(a)) b := "上海" fmt.Println(len(b)) }
代码输出如下:
len() 函数的返回值的类型为 int,表示字符串的 ASCII 字符个数或字节长度。
- 输出中的第一行的 25 表示变量 a 的字符个数是 25 个。
- 输出中的第二行的 6 表示变量 b 的字符个数是6。
有人会有疑问了,变量 b 的字符串中明明只有 2 个中文文字:"上海",为什么是6个字符呢?这里的差异是由于 Go 语言的字符串都以 UTF-8 格式保存,每个中文字符占用 3 个字节,因此使用 len() 获得两个中文文字应该对应的是 6 个字节。
如果希望按中文字符个数来计算,就需要使用 Go 语言中 UTF-8 包提供的 RuneCountInString() 函数,统计 Uncode 字符数量。
package main import ( "fmt" "unicode/utf8" ) func main() { fmt.Println(utf8.RuneCountInString("上海")) fmt.Println(utf8.RuneCountInString("我在上海!")) }
总结:
- ASCII 字符串长度使用 len() 函数。
- Unicode 字符串长度使用 utf8.RuneCountInString() 函数。
1.2、遍历字符串——获取每一个字符串元素
遍历字符串有下面 2 种写法。
1.2.1、遍历每一个 ASCII 字符
的数值循环进行遍历,直接取每个字符串的下标获取 ASCII 字符。例子比较简单,这里不举例了。
1.2.2、按 Unicode 字符遍历字符串
的数值循环进行遍历。例子也比较简单,这里不举例了。
1.3、获取字符串的某一段字符
获取字符串的某一段字符是开发中常见的操作。我们一般将字符串中的某一段字符称做:"子串“,英文对应 “substring”。、
strings.Index 介绍见:https://studygolang.com/pkgdoc
//子串sep在字符串s中第一次出现的位置,不存在则返回-1。 func Index(s, sep string) int
举个例子:
package main import ( "fmt" "strings" ) func main() { tracer := "死神来了, 死神 bye bye!" comma := strings.Index(tracer, ",") pos := strings.Index(tracer[comma:], "死神") fmt.Printf("comma的类型是: %T\n", comma) fmt.Println(comma, pos, tracer[comma+pos:]) }
代码说明如下:
- 第 10 行,尝试在 tracer 的字符串中搜索逗号,返回的位置存在 comma 变量中,值是 12,类型是 int,表示从 tracer 字符串开始的 ASCII 码位置。
- 第 12 行,tracer[comma:] 从 tracer 的 comma 位置开始到 tracer 字符串的结尾构造一个子字符串(这时候的 tracer[comma:] 字符串内容实际是 "死神 bye bye!"),返回给 string.Index() 进行再搜索,子字符串是 "死神"。得到的 pos 是相对于 tracer[comma:] 的结果,所以结果是 2。
- comma 逗号的位置是 12,而 pos 是相对位置,值为 3。
- 为了获得第 2 个 "死神" 位置,也是就会逗号后面的字符串,就必须让 comma 加上 pos 的相对偏移量,计算出 15 的偏移,然后再通过切片 tracer[coomma+pos:] 计算出最终的子串,获得最终效果: "死神 bye bye"。
代码输出如下:
总结:字符串索引比较常用的有如下几种方法:
- strings.Index: 正向搜索子字符串。
- strings.LastIndex:反向搜索子字符串。
- 搜索的起始位置可以通过切片偏移来制作。
1.4、修改字符串
Go 语言的字符串无法直接修改每一个字符元素,只能通过重新构造新的字符串并赋值给原来的字符串变量实现。
Go 语言中的字符串和其他高级编程语言(Java、C#)一样,默认是不可变的(immutable)
字符串不可变有很多好处,如天生线程安全,大家使用的都是只读对象,无须加锁;再者,方便内存共享,而不必使用写时赋值(Copy On Write)等技术;字符串 hash 值也只需制作一份。
举个里说来说明下:
package main import ( "fmt" ) func main() { Sh := "Welcome to Shanghai" ShBytes := []byte(Sh) //标准的golang显示转型,将变量 Sh 转换成 []byte 类型,并给 ShBytes。 for i := 10; i <= 18; i++ { ShBytes[i] = ' ' } fmt.Println(string(ShBytes)) }
代码输出后,如下:
所以说,代码中实际修改的是 []byte,[]byte 在 Go 语言中是可变的,本身就是一个切片。
在完成对 []byte 操作后,在第 15 行将字符串转换为 []byte 进行修改。
[]byte 和 string 可以通过强制类型转换互转。
总结:
- Go 语言的字符串是不可变的。
- 修改字符串时,可以将字符串转换为 []byte 进行修改。
- []byte 和 string 可以通过强制类型转换互转。
1.5、连接字符串
1.5.1、使用 "+" 对字符串进行连接操作
使用 " +" 太简单了,不举例了。
1.5.2、使用字节缓冲区 bytes.Buffer 进行连接操作
byte.Buffer 是可以缓冲并可以往里面写入各种字节数组的。字符串也是一种字节数组,使用 WriteString() 方法进行写入。
将需要连接的字符串,通过调用 WriteString()方法,写入 buffer 中,然后再通过 buffer.String() 方法将缓冲转换为字符串。
package main import ( "bytes" "fmt" ) func main() { skill := "死亡缠绕" action := " C出" DK := " 死亡骑士" // 声明字节缓冲 var buffer bytes.Buffer //把字符串写入缓冲 buffer.WriteString(DK) buffer.WriteString(action) buffer.WriteString(skill) //将缓冲以字符串形式输出 fmt.Println(buffer.String()) }
1.6、格式化
格式化在逻辑中非常常用。Go 语言的格式化占位符和 C 语言的占位符很类似,但是要精简的多。使用格式化函数,要注意写法:
//Printf根据format参数生成格式化的字符串并写入标准输出。返回写入的字节数和遇到的任何错误。 func Printf(format string, a ...interface{}) (n int, err error)
Golang 使用 "%v" 作为一个 "万能" 的占位符,编译器可以根据数据类型自动推断最终的数据展示格式。但是,这个通用占位符还是有定制的空间的。
通用占位符对应的特定占位符
数据类型 | 对应的特定占位符 |
bool | %t |
int,int8 等 | %d |
uint,uint8 等 | %d 或%#v(%#v) |
float32, complex64 等 | %g |
string | %s |
chan/指针
|
%p |
struct | {field0 field1…} |
array, slice | [elem0 elem1…] |
maps | map[key1:valule1 key2:value2] |
struct,array,slice,maps 指针
|
&{}, &[], &map[] |
Go 语言专用占用符
%v | 输出默认格式 | fmt.Printf("%v", xxx) |
%+v | 为 struct 类型添加字段名称 |
fmt.Printf("%+v", xxx)
|
%#v | 输出完整语法表示 | fmt.Printf("%#v", xxx) |
%T | 打印数据类型 | fmt.Printf("%T", xxx) |
%% | 打印百分号% | fmt.Printf("%%") |
%t | 输出 true 或 false | fmt.Printf("%t",ture) |
%b | 二进制表示 | fmt.Printf("%b", 233) |
%o | 八进制表示 | fmt.Printf("%o", 233) |
%d | 十进制表示 | fmt.Printf("%d", 0351) |
%x | 十六进制表示 (小写字母) | fmt.Printf("%x", 233) |
%X | 十六进制表示 (大写字母) | fmt.Printf("%X", 233) |
%c | Unicode 码点对于的字符 | fmt.Printf("%c", 0x75D2) |
%U | 输出 Unicode,等价于” U+%04X” |
fmt.Printf("%U", '下') |
%b | 无小数部分,指数基数为二的科学计数法 | fmt.Printf("%b", 65536.0) |
%e | 科学计数法 | fmt.Printf("%e", 65536.0) |
%E | 科学计数法 | fmt.Printf("%e", 65536.0) |
%f | 没有指数的小数 | fmt.Printf("%f", 65536.1) |
%F | 没有指数的小数 | fmt.Printf("%F", 65536.1) |
%g | 以更紧凑的方式自动选择%e 或%f(末尾无 0) | fmt.Printf("%g", 65536.1 |
%G | 以更紧凑的方式自动选择%E 或%f(末尾无 0) | fmt.Printf("%G", 65536.1) |
%2.1g | 输出结果宽度 2(左侧补空格),结果共 1 位数字 | fmt.Printf("%2.1g", 1.21) |
%2.1f | 输出结果共 2 位 (左侧补空格),小数 1 位 | fmt.Printf("%2.1f", 1.21) |
%s | 字符串表示 | fmt.Printf("%s", "hello, world") |
%q | 带双引号的字符串表示 | fmt.Printf("%q", "hello, world") |
%x | 对字节序列的十六进制表示 (小写字母) | fmt.Printf("%x", "hello, world") |
%X | 对字节序列的十六进制表示 (大写字母) | fmt.Printf("X%", "hello, world") |
%p | 十六进制表示指针值, 0x 打头 | fmt.Printf("%p", &var) |
1.7、示例:Base64 编码 —— 电子邮件的基础编码格式
Base64 编码是常见的对 8 比特字节码的编码方式之一。Base64 可以使用 64 个可打印字符来表示二进制数据,电子邮件就是使用这种编码。
Go 语言的标准库自带了 Base64 编码算法,通过几行代码就可以对数据进行编码。
package main import ( "encoding/base64" "fmt" ) func main() { //需要处理的字符串 message := "Away from keyboard. https://golang.org/" //编码消息 encodedMessage := base64.StdEncoding.EncodeToString([]byte(message)) //输出编码完成的消息 fmt.Println(encodedMessage) //解码消息 data, err := base64.StdEncoding.DecodeString(encodedMessage) //出错处理 if err != nil { fmt.Println(err) } else { // 打印解码完成的数据 fmt.Println(string(data)) } }
1.8、示例:从 INI 配置文件中查询需要的值
INI 文件格式是一种古老的配置文件格式。一些操作系统、虚幻游戏引擎、Git 版本管理中都在使用 INI 文件格式。以 xshell 的配色方案为例:
[Solarized Dark] text=ffffff cyan(bold)=2ab5ac text(bold)=ffd700 magenta=6c71c4 green=639a07 green(bold)=77ae1b background=002b36 cyan=2aa198 red(bold)=e14b16 yellow=b58900 magenta(bold)=7684d8 yellow(bold)=c99d00 red=cb4b16 white=eee8d5 blue(bold)=26a0d2 white(bold)=f8f2df black=143f4a blue=268bd2 black(bold)=004954 [Names] name0=Solarized Dark count=1
1.8.1、INI 文件的格式
- INI 文件由多行文本组成,整个配置由 "[]" 拆分为多个 "段"(section)。每个段中又以 "=" 分割为 "键" 和 "值"。
- INI 文件以 ";" 置于行首视为注释,本行将不会被处理和识别。
1.8.2、从 INI 文件中取值的函数
本例并不是将整个 INI 文件读取保存后再获取需要的字段数据并返回,这里使用 getValue() 函数,每次从指定文件中找到需要的段(Section)及键(Key)对应的值。
getValue() 函数的声明如下:
func getValue(filename, expectSection, expectKey string) string 参数说明如下: filename:INI 文件的文件名 expectSection:期望读取的段 expectKey:期望读取段中的键
getValue() 函数调用代码如下:
func main(){ fmt.Println(getValue("Solarized Dark.ini", "Solarized Dark", "green")) }
1.8.3、读取文件
Go 语言的 os 包中提供了文件打开函数 os.Open()。文件读取完成后需要及时关闭,否则文件会发生占用,系统无法释放缓冲资源。
//打开文件 file, err := os.Open(filename) //文件找不到,返回空 if err != nil{ return "" } //在函数结束时,关闭文件 defer file.Close()
1.8.4、读取文本
INI 文本的格式是由多行文本组成,因此需要构造一个循环,不断地读取 INI 文件的所有行。Go 语言总是将文件以二进制格式打开,通过不同的读取方式对二进制文件进行操作。Go 语言对二进制读取有专门的代码抽象,bufio 包即可以方便地以比较常见的方式读取二进制文件。
//使用读取器读取文件 reader := bufio.NewReader(file) //当前读取的段的名字 var sectionName string for{ //读取文件的一行 linestr, err := reader.ReadString('\n') if err != nil { break } //去掉首位空白字符 linestr = strings.TrimSpace(linestr) //忽略空行 if linestr == "" { continue } //忽略注释 if linestr[0] == ";" { continue } //读取段和键值代码 // ... }
1.8.5、读取段
行字符串 linestr 已经去除了空白字符串,段的起止符又以 "[" 开头,以 "]" 结尾,因此可以直接判断行首和行尾的字符串匹配段的起止符匹配时读取的是段。
行首:linestr[0]
行尾:linestr[len(linestr) - 1]
//行首和尾巴分别是方括号的,说明段标记的起止符 if lienstr[0] == '[' && linestr[len(linestr)-1] == ']' { //将段名取出 sectionName = linestr[1:len(linestr)-1] //这个段是希望读取的 }
1.8.6、读取键值
当前行不是段时(不以 "[" 开头),那么行内容一定是键值对。
else if sectionName == expectSection { //切开等号分割的键值对 pair := strings.Split(linestr, "=") //保证切开只有1个等号分割的键值情况 if len(pair) == 2{ //去掉键的多余空白字符 key := strings.TrimSpace(pair[0]) //是期望的键 if key == expectKey{ //返回去掉空白字符串的值 return string.TrimSpace(pair[1]) } } }