15 基本数据类型之字符串

基本数据类型之字符串

一 字符串底层是什么

​ 在 C/C++语言中,并不存在原生的字符串类型,它们通常使用字符数组来表示出字符串的概念,并以字符指针来传递。而在Go语言中,字符串也是一种基本类型,就像其他数据类型(int、bool、float32、float64等)一样。

​ Go语言中,字符串底层是一个切片,该切片指向一个底层数组,数组内元素是一个个字节(byte)

​ 再次强调:字符串底层是一个切片、切片、切片!!!

​ go圣经了里明确说道:字符串与[]byte的结构相同,这个牵扯到数据类型转换,务必要搞清楚!不要被一些文章误导。

goland使用3

二 声明与初始化

声明语法

var 变量名 string = 初始化表达式/字面量

字符串底层就是一个byte组成的切片,切片指向一个底层数组,数组内存放的是一个个byte,byte顾名思义就是字节,是string类型底层的元素类型,源码里type byte = uint8,即byte与uint8是等价类型。

字符串底层数组的byte从何而来呢?有如下两种

  • 1、当字符串面值为文本字符时,go会将用utf8格式将其编码成一个个字节,然后存入底层数组;
var name string = "egon林"
fmt.Println(len(name))  // 7个字节
fmt.Println(name)       // egon林
  • 2、当字符串面值为现成的字节数字时,go就省事了,直接存入底层数组就好
s2:="\xe5\xb3\xb0"    // 字符串内包含的可以直接就是现成的一个个byte,并且每一个byte都可以是任意byte
fmt.Println(len(s2))  // 3个字节
fmt.Println(s2)       // 峰

​ 也就是说:字符串底层数组内存放的字节byte可以是任意字节,即只要是byte就行。

​ 但是在读取字符串时,会从底层数组中取出byte然后按照utf-8个格式转换成字符,所以如果当初存的时候无法无天,打印字符串的时候便有可能会出现乱码。

​ 接下来我们就来详细看一下string字面量,时刻牢记,无论是啥,都会解析成byte存入底层数组,打印时会取出来按照utf8格式进行解码。

2.1 字面量可以是任意字符

字符串的值需要用双引号包含(注意一定要是双引号),双引号内通常是字符串字面量,即人类从字面上就可以读懂的文本字符,比如

var name string = "egon林"

字符串的字面值为文本字符时,go默认采用utf8编码将其编码成一个个byte,然后存入底层数组。1个英文字符占一个byte,1个中文字符占3个byte,上述声明语句底层相当于:

var name = []byte{101,103,111,110,230,158,151}

打印时,同样会采用utf8编码将其解码

fmt.Println(name)  // 取出[]byte里的内容按照utf8格式进行解码,输出"egon林"

字面量中也可以引入转义符号

在一个双引号包含的字符串面值中,可以用以反斜杠\开头的转义序列插入任意的数据。下面的换行、回车和制表符等是常见的ASCII控制代码的转义方式:

\n      换行
\r      回车
\t      制表符
\"      双引号 
\\      反斜杠
// 例如
fmt.Println("file_path := \"d:\\a\\b\\c.txt\"") // 输出:file_path := "d:\a\b\c.txt"

注意:

  • 1、字符串是包含在双引号内的,如果双引号的字符串内还想再包含双引号,那就需要转义了,正如上例所示。但如果双引号内存在单引号,无需转义,会直接打印
fmt.Println("my name is 'egon'") // my name is 'egon'
  • 在go中,单引号内包含的是字符rune类型(后续会提到),如果在字符的单引号内还想再包含单引号那也需要转义
fmt.Println('\'') // 单引号内表示的是字符类型,只能包含一个字符,如果该字符是单引号,那么必须转义

反引号“

使用反引号代替双引号代表是原生字符,原生字符内没有转义操作,全部的内容都是字面的意思,会原样输出字符与换行等

s:=`
当你认清自己的时候
便\n\t\n\n不会再小瞧他人
\u4e0a
`
fmt.Println(s) // 输出如下内容

当你认清自己的时候
便\n\t\n\n不会再小瞧他人
\u4e0a

原生字符串面值用于编写正则表达式会很方便,因为正则表达式往往会包含很多反斜杠。原生字符串面值同时被广泛应用于HTML模板、JSON面值、命令行提示信息以及那些需要扩展到多行的场景。

2.3 字面量可以是对应的字节数字

双引号内也可以是直接写好的字节数字,go会将其直接存入底层数组。

切记:存的时候乱存,当然不会报错,但是在打印字符串时会从底层数组取出byte然后按照utf-8格式解码成字符就会乱码了。

// 0、先储备一个知识点
一个字节=8个bit位
    对应十进制最大为255
    对应的十六进制最大为0xff
    对应的八进制最大为\377(go中\开头代表八进制,不要写成\o377)

// 1、双引号内是八进制数构成的字符串
s1:="\110\151" // 底层数组存放的第一个字节是\110,第二个字节是\151
fmt.Println(s1) // Hi
fmt.Println(len(s1)) // 2个字节

// 2、双引号内是十六进制数构成的字节
s2:="\xe5\xb3\xb0\xe5\x93\xa5" // 底层数组存放的第一个字节是\xe5,第二个字节是\xb3,依次类推...
fmt.Println(s2) // 峰哥
fmt.Println(len(s2)) // 6个字节

// 3、双引号内:\u指的是遵循UCS-2标准的unicode,\U指的是遵循UCS-4标签的unicode
s3:="\u771f\U00005e05" // \u771f解码成utf-8编码后对应三个字节,依次存入底层数据,\U...一样
fmt.Println(s3) // 真帅
fmt.Println(len(s3)) // 6个字节

// 4、双引号内是字符串面值
s4:="en说的对" // go会将字符串面值编码成utf-8格式,然后将得到的byte依次存入底层数组
fmt.Println(s4) // en说的对
fmt.Println(len(s4)) // 11个字节

// 双引号内完全可以同时容纳上1、2、3、4所述的内容,甚至顺序都无所谓,因为字符串底层切片指向的就是一个字节序列的数组,至于数组内存的是什么字节byte是无所谓的,但问题出就出在,当我们打印字符串的时候,go会将字符串底层数组中的byte取出来,然后按照utf-8个的格式解码成字符,如果byte非法,那么解码出的字符是一个黑色六角或钻石形状,里面包含一个白色的问号(?)
s5:="\372\xee\xe5\xb3\xb0\xe5\x93\xa5" // 前2个字节都是非法的,后6个合法
fmt.Println(s5) // ��峰哥
fmt.Println(len(s5)) // 8个字节

ps:有一些字符串面值的字符比较特殊,我们用的输入法工具很难输入,有一些甚至是不可见的字符,此时可以在Go语言的字符串面值中,通过使用转义字符加上Unicode码点的方式来输入特殊的字符(如上例1、2、3、4所示)。unicode遵循两种标准UCS-2和UCS-4,UCS-2用两个字节编码(\u对应UCS-2),UCS-4用4个字节编码(\U对应UCS-4)。\uhhhh对应16bit的码点值,\Uhhhhhhhh对应32bit的码点值,其中h是一个十六进制数字;一般常用的是16bit的形式,很少需要使用32bit的形式,这正如我们常说的那样,unicode通常用两个字节表示一个字符。

三 字符类型rune

​ 在python中只有字符串类型,并没有字符类型,而go语言则不同,既有string字符串类型,还有一个rune字符类型,只能存放单个字符,要了解rune类型,我们先来储备一个知识

3.1 储备知识:了解unicode与utf-8

// ==========================> unicode <==========================
在很久以前,世界还是比较简单的,起码计算机世界就只有一个ASCII字符集:美国信息交换标准代码。ASCII,更准确地说是美国的ASCII,使用7bit来表示128个字符:包含英文字母的大小写、数字、各种标点符号和设置控制符。对于早期的计算机程序来说,这些就足够了,但是这也导致了世界上很多其他地区的用户无法直接使用自己的符号系统。随着互联网的发展,混合多种语言的数据变得很常见(译注:比如本身的英文原文或中文翻译都包含了ASCII、中文、日文等多种语言字符)。如何有效处理这些包含了各种语言的丰富多样的文本数据呢?

答案就是使用Unicode(http://unicode.org) ,它收集了这个世界上所有的符号系统,包括重音符号和其它变音符号,制表符和回车符,还有很多神秘的符号,在第八版本的Unicode标准收集了超过120,000个字符,涵盖超过100多种语言。这些在计算机程序和数据中是如何体现的呢?

// ==========================> unicode字符与unicode码点 <==========================
每个符号都被收录进了unicode中,称之为unicode字符,每一个unicode字符都被分配一个唯一的Unicode格式的数字,该数字称之为unincode的码点,通用的表示一个Unicode码点的数据类型是int32,也就是Go语言中的rune类型(rune是int32等价类型);它的同义词rune符文正是这个意思。

unicode遵循两种标准UCS-2和UCS-4,UCS-2用两个字节编码(go中对应的转义字符是\u),UCS-4用4个字节编码(go中对应的转义字符是\U),\uhhhh对应16bit的码点值,\Uhhhhhhhh对应32bit的码点值,其中h是一个十六进制数字;一般很少需要使用32bit的形式,这正如我们常说的那样,unicode通常用两个字节表示一个字符。

我们*可以*将一个符文序列表示为一个int32序列。这种编码方式叫UTF-32或UCS-4,每个Unicode码点都使用同样的大小32bit来表示。这种方式比较简单统一,但如果文本中包含的大多数都是英文字符的话,它会浪费很多存储空间,因为一个英文字符只需要8bit或1字节就能表示。而且我们常用的字符是远少于65,536个的,也就是说用16bit编码方式(UCS-2)就能表达常用字符。为了解决unicode的问题,utf-8应运而生。

// ==========================> utf-8 <==========================
前言:Utf-8就是unicode的一个标准,把unicode的内容进行了编排,它本质其实就是unicode,utf-8全称也是unicode的一个转换格式,在得到一个unicode后,我们通过将其按照utf-8的格式编排一下,更精简更美好

详细:
UTF8编码由Go语言之父Ken Thompson和Rob Pike共同发明的,现在已经是Unicode的标准。
UTF8全称(Unicode Transformation Format 8), 是一种将Unicode码点编码为字节序列的变长编码。
UTF8编码使用1到4个字节来表示每个Unicode码点,其中
    - ASCII部分字符只使用1个字节
    - 常用字符部分使用2或3个字节表示。
每个符号编码后第一个字节的高端bit位用于表示总共有多少编码个字节。如果第一个字节的高端bit为0,则表示对应7bit的ASCII字符,ASCII字符每个字符依然是一个字节,和传统的ASCII编码兼容。如果第一个字节的高端bit是110,则说明需要2个字节;后续的每个高端bit都以10开头。更大的Unicode码点也是采用类似的策略处理。

0xxxxxxx                             runes 0-127    (ASCII)
110xxxxx 10xxxxxx                    128-2047       (values <128 unused)
1110xxxx 10xxxxxx 10xxxxxx           2048-65535     (values <2048 unused)
11110xxx 10xxxxxx 10xxxxxx 10xxxxxx  65536-0x10ffff (other values unused)

utf-8这种变长的编码,因为变长而带来的缺点是:无法直接通过索引来访问第n个字符(python3中字符串被存成unicode的好处就是可以通过索引访问第n个字符,而这一点go语言是做不到的)
但是UTF8也因为变长获得了很多额外的优点:首先UTF8编码比较紧凑,完全兼容ASCII码,并且可以自动同步:它可以通过向前回朔最多2个字节就能确定当前字符编码的开始字节的位置。它也是一个前缀编码,所以当从左向右解码时不会有任何歧义也并不需要向前查看(译注:像GBK之类的编码,如果不知道起点位置则可能会出现歧义)。没有任何字符的编码是其它字符编码的子串,或是其它编码序列的字串,因此搜索一个字符时只要搜索它的字节编码序列即可,不用担心前后的上下文会对搜索结果产生干扰。同时UTF8编码的顺序和Unicode码点的顺序一致,因此可以直接排序UTF8编码序列。同时因为没有嵌入的NUL(0)字节,可以很好地兼容那些使用NUL作为字符串结尾的编程语言。

Go语言的源文件采用UTF8编码,并且Go语言处理UTF8编码的文本也很出色。unicode包提供了诸多处理rune字符相关功能的函数(比如区分字母和数组,或者是字母的大写和小写转换等),unicode/utf8包则提供了用于rune字符序列的UTF8编码和解码的功能。

更多关于字符编码的知识请参考
https://www.cnblogs.com/linhaifeng/articles/5950339.html
https://baike.baidu.com/item/Unicode/750500?fr=aladdin
https://www.kancloud.cn/lbb4511/gopl/1107841
https://www.kancloud.cn/lbb4511/gopl/1107842

3.2 rune类型

  • 1、rune类型介绍

    字符串类型必须用双引号

    rune类型必须用单引号

    字符串类型可用于存放一串字符,这一串字符底层是一个切片,该切片指向一个数组,数组中存一系列byte

    rune类型只能用于存放单个字符,单个字符底层是一个unicode码点,存成int32

源码中
type byte = uint8  // byte是uint8等价类型
type rune = int32  // rune是int32等价类型

// rune与int32完全等价,可以混用,如下,同理byte与uint8完全等价,byte(111)就是uint8(111)
var x rune = 10
var y int32 = 20
fmt.Println(x + y)  // 30

在unicode编码里,每个字符都对应为一个码点
一个码点就是一个数字,该数字对应go语言中的rune类型
  • 2、rune类型示例

    rune表达的是字符类型,存放字符需要用单引号包含,注意单引号内有且仅有一个字符

s1:='汪'
fmt.Println(s1)
fmt.Printf("%T",s1) // int32,不要怀疑你的双眼,int32就是rune,rune就是int32,二者等同
fmt.Printf("%x\n",s1)  // 打印所存int32的十六进制格式:6c6a
fmt.Printf("%d\n",s1)  // 打印所存int32的十进制格式:27754

s2:='汪汪' // 编译错误:invalid character literal (more than one character)

fmt.Println('A')       // 65
fmt.Println('a')       // 97
fmt.Println('A' - 'a') // -32
  • 3、rune类型中包含转义符
单引号字符必须转义
'\''
Unicode转义也可以使用在rune字符中。下面三个字符是等价的:
'世' 
'\u4e16' 
'\U00004e16'

对于小于256码点值可以写在一个十六进制转义字节中,例如'\x41'对应字符'A',但是对于更大的码点则必须使用\u或\U转义形式,因此,'\xe4\xb8\x96'并不是一个合法的rune字符,虽然这三个字节对应一个有效的UTF8编码的码点。
  • 4、区分字节数与字符数
如下字符串由字符串面值构成,会按照utf-8格式编码成13个字节,然后存入底层数组
s := "Hello, 世界"
fmt.Println(len(s))                    // "13",代表底层数组中有13个字节
fmt.Println(utf8.RuneCountInString(s)) // "9",代表上述13个字节对应9个Unicode字符

四 数据类型转换

1、string(十进制数字)按照ASCII解析成字符

x:='A'
fmt.Println(x)  // 65
fmt.Printf("%T\n",x)  // int32
fmt.Println(string(x))  // A

fmt.Println(string(65))  // A
fmt.Println(string(90))  // Z

2、string(十进制数字)按照unicode解析成字符

s1:='汪'
fmt.Printf("%d\n",s1)  // 打印所存int32的十进制格式:27754

fmt.Println(string(27754))  // 汪

3、字符串string与字节切片[]byte互转

// "egon林"是string类型,string类型底层是字节切片(切片指向一个底层数组)
// 所以说string类型与[]byte有相同的结构,于是我们可以进行数据类型转换
var name string = "egon林"

// 例1:string转[]byte
var slice = []byte(name)
fmt.Println(slice)  // [101 103 111 110 230 158 151]

var arr = [7]byte(name)  // 报错:string类型无法转换成[7]byte数组

// 例2:[]byte转换string
z := []byte{101, 103, 111, 110, 230, 158, 151}
fmt.Println(string(z))         // egon林
fmt.Println(string(z) == name) // true

4、字符串string与字符切片[]rune互转

// 1、string转成[]rune
name:="egon林"
x:=[]rune(name)
fmt.Println(x)  // [101 103 111 110 26519],切片内存放一个rune符文,或称unicode码点,本质就是一个个int32类型的数字
fmt.Printf("%T\n",x[0])  // int32

// 2、[]rune转成string
fmt.Println(string(x))  // egon林

五 字符串基本操作

5.1 索引操作之取单个字节/byte

注意字符串的底层是字节切片,根据索引取出的肯定是第n个字节,注意注意注意是byte、而非字符rune

s:="hi你好呀"
fmt.Println(s[0]) // 104
//fmt.Println(s[len(s1)]) // 超出字符串索引范围导致panic异常
//fmt.Println(s[-1]) // 不支持负向索引导致panic异常

// ps:
go中s[i]访问的都是第i个字节而不是字符,因为go的字符串s用的是utf-8编码,是变长的,而变长的编码无法通过索引操作确定字符占用字节的个数,所以只能取字节了,这是它的缺点:

而python中s[i]访问的都是第i个字符,因为python3中字符串用是unicode编码,是定长的

5.2 索引操作之取切片

储备知识:

  • 问:

    既然字符串类型底层是[]byte切片,那么fmt.Println()打印出来的不应该就是[]byte那么,为何打印出的是字符呢???

  • 答:

字符串底层确实是[]byte,但是在fmt.Println()打印时,会调用String()方法,将字节按照utf8解码成字符
即:
var name string = "egon林"  // 底层存成[]byte{101, 103, 111, 110, 230, 158, 151}
fmt.Println(name)  // 底层实际操作:fmt.Println(string(上述切片))
  • 总结

    打印字符串,就是在打印底层字节切片[]byte,而在打印这个字节切片时会调用String()方法将其按照utf8解码成字符

例1:因为字符串底层就是[]byte切片,所以对字符串进行切片操作,得到的数据底层其实仍然是[]byte,我们打印时,打印出的仍然是字符

s1:="hi你好呀"

// 顾头不顾尾
fmt.Printf("%T",s1[0:1])  // string

fmt.Println(s1[0:1]) // h
fmt.Println(s1[0:2]) // hi
fmt.Println(s1[0:3]) // hi�
fmt.Println(s1[0:4]) // hi�
fmt.Println(s1[0:5]) // hi你
fmt.Println(s1[:5]) // hi你,默认从0开始
fmt.Println(s1[2:]) // 你好呀,默认到末尾
fmt.Println(s1[:]) //  hi你好呀,默认从头到末尾

例2:如果只是取到单独某个元素,而不是[]byte,那么打印的就是字节了,如下

fmt.Println(s1[0]) // 104

// 我们当然可以手动调用string()来转换字符
fmt.Println(string(s1[0])) // h

因为104这个字节就对应一个英文字符,可以转换成功

5.3 字符串不可修改

字符串底层是一个[]byte切片,指向byte元素组成的数组,而字符串底层数组内的byte是不可以被替换的,即不能修改字符串中包含的内容。

s:="hi你好呀"
s[0] = 'L' // 编译错误: cannot assign to s[0]

不变性意味如果两个字符串共享相同的底层数据的话也是安全的,这使得

  • 1、复制任何长度的字符串代价是低廉的。

  • 2、同样,一个字符串s和对应的子字符串切片s[7:]的操作也可以安全地共享相同的内存,因此字符串切片操作代价也是低廉的。

在这两种情况下都没有必要分配新的内存。 下图演示了一个字符串和两个字串共享相同的底层数据。

image-20200529215401956

5.4 利用[]rune、[]byte“修改”字符串

string类型值肯定是不可以被修改的,但是我们可以将string转换为[]rune或[]byte来完成修改,修改完毕后,再使用string()转换得到一个新的字符。

1、将string转换为[]rune切片,然后进行修改

s1 := "你是小王吗"

rune1 := []rune(s1)  // []rune生成一个新的数组,数组内存放的是从s1中拷贝过来的内容
rune1[len(rune1)-1] = '吧'  //  改动的是[]rune自己的底层数组,这是可以的,此时改的是unicode码点,对应一个完整的字符,要改一个中文字符,需要用索引取出一个码点进行修改即可

// string()可以按照utf-8编码格式将一个[]rune类型的Unicode字符切片或数组转为string字符串
fmt.Println(string(runeS2)) // 吃鸡吧

ps:
可以将一个UTF8编码的字符串解码为Unicode字符序列,UTF8字符串作为交换格式是非常方便的,但是在程序内部采用rune序列可能更方便,因为rune大小一致,支持数组索引和方便切割。

2、将string转换为[]byte切片,然后进行修改

s2 := "你是小王吗"

byte1 := []byte(s2)  // 产生了新的数组,'吗'对应的byte索引为12、13、14
byte1[12] = 229
byte1[13] = 144
byte1[14] = 167
fmt.Println(string(byte1))  // 你是小王吧

s3:="你是小王吧"
fmt.Println([]byte(s3))  // [...... 229 144 167]

练习题1:

// 把字符串底层的byte转成unicode码点存入切片:[]rune
s := "egon林海峰"
// fmt.Println(r) 默认打印十进制,%x则打印十六机制,% x参数用于在每个十六进制数字前插入一个空格,
fmt.Printf("% x\n", s) // 65 67 6f 6e e6 9e 97 e6 b5 b7 e5 b3 b0

r := []rune(s)
fmt.Printf("%x\n", r) // [65 67 6f 6e 6797 6d77 5cf0]

r[0] = 'E'
r[5] = '\u55e8'
r[6] = '疯'
fmt.Printf("%x\n", r) // [45 67 6f 6e 6797 55e8 75af]

fmt.Println(string(r)) // Egon林嗨疯
fmt.Printf("%q\n", r) // ['E' 'g' 'o' 'n' '林' '嗨' '疯']

练习题2:

msg:="hi吃鸡吗"

// 把字符串底层的byte存入切片:[]byte
x:=[]byte(msg)  // []byte生成一个新的数组,数组内存放的是从s1中拷贝过来的内容
fmt.Println(x)  // [104 105 229 144 131 233 184 161 229 144 151]

// 下述改动的是[]byte自己的底层数组,这是可以的,但只能单独修改一个个的byte,要改一个中文字符,需要用索引取出3个byte来逐一修改。
x[0]='S'    // 之所以用'S'是因为单引号代表的是int32,此处其实就是将一个数字赋值给了x[0],如果用"S"则代表的是一个字节数组,用双引号的"S"就不对了。
x[1]='B'

x[8]='\xe5' // 替换为中文字符"屎"的第1个utf-8码点
x[9]='\xb1' // 替换为中文字符"屎"的第2个utf-8码点
x[10]='\x8e' // 替换为中文字符"屎"的第3个utf-8码点
fmt.Println(x) // [83 66 229 144 131 233 184 161 229 177 142]

fmt.Println(string(x)) // SB吃鸡屎

x[len(x)-1]='a'
fmt.Println(string(x)) // SB吃鸡�a

练习题3

s := "プログラム"
fmt.Printf("% x\n", s) // "e3 83 97 e3 83 ad e3 82 b0 e3 83 a9 e3 83 a0"
r := []rune(s)
fmt.Printf("%x\n", r)  // "[30d7 30ed 30b0 30e9 30e0]"

fmt.Println(string(r)) // "プログラム"

练习题4:

fmt.Println(string(65))     // 注意得到的是字符"A", 而不是 "65"
fmt.Println(string(0x4eac)) // "京"

练习题5:如果对应码点的字符是无效的,则用’\uFFFD’无效字符作为替换:

fmt.Println(string(1234567)) // "(?)"

练习题6:将一个整数转型转换为字符串

fmt.Println(string(65))     // 注意得到的是字符"A", 而不是 "65"
fmt.Println(string(0x4eac)) // "京"

5.5 二元运算

字符串只能与字符串之间进行运算

(1)相等性判断:字符串只能与无类型/有类型的字符串比较是否相等

(2)比大小:字符串之间也可以比大小,底层通过逐个字节比较完成的,如下所示

var x string = "egon会"
var y string = "egon飞"

// 底层
fmt.Println([]byte(x)) // [101 103 111 110 228 188 154]
fmt.Println([]byte(y)) // [101 103 111 110 233 163 158]

// 按次序比较字节
fmt.Println(x > y)  // true
fmt.Println(x < y)  // false

(3)相加:拼接字符串

fmt.Println("hello" + "egon")  // helloegon

// 注意:不能乘
fmt.Println("hello"*3)         // 错误

5.6 遍历字符串的两种方式

依赖索引

为了处理这些真实的字符(即字符串中包含的unicode字符),我们需要一个UTF8解码器。unicode/utf8包提供了该功能,我们可以这样使用:

s := "Hello, 世界"
for i := 0; i < len(s); {
    r, size := utf8.DecodeRuneInString(s[i:]) // r对应字符本身,size代表utf-8编码字符r的字节个数
    fmt.Printf("%d\t%c\n", i, r)
    i += size // 更新索引指向下一个新的字符
}
// 输出
0       H
1       e
2       l
3       l
4       o
5       ,
6        
7       世
10      界

依赖range

但是上述依赖索引的这种方式是笨拙的,我们需要更简洁的语法。幸运的是,Go语言的range循环在处理字符串的时候,会自动隐式解码UTF8字符串。

for i, r := range "Hello, 世界" {
    fmt.Printf("%d\t%q\t%d\n", i, r, r) 
}
// 输出
0       'H'     72
1       'e'     101
2       'l'     108
3       'l'     108
4       'o'     111
5       ','     44
6       ' '     32
7       '世'    19990 // 中文字符”世“的三个字节被合并到了一起,然后被解码
10      '界'    30028

上述range循环运行如图所示;需要注意的是对于非ASCII,索引更新的步长将超过1个字节。

image-20200529233600074

统计字符串中字符个数

基于上述range循环的介绍,我们可以使用一个简单的循环来统计字符串中字符的数目,像这样:

s:="Hello, 世界"
n := 0
for _, _ = range s {
    n++
}
fmt.Println(n) // 9

像其它形式的循环那样,我们也可以忽略不需要的变量:

s:="Hello, 世界"
n := 0
for range s {
    n++
}
fmt.Println(n) // 9

或者我们可以直接调用utf8.RuneCountInString(s)函数。

s:="Hello, 世界"
fmt.Println(utf8.RuneCountInString(s)) // 9

六 其他字符串常用操作

1、fmt.Printf格式化打印字符串

// 字符使用`%c`参数打印,或者是用`%q`参数打印带单引号的字符:
ascii := 'a'
unicode := '国'
newline := '\n'
fmt.Printf("%d %[1]c %[1]q\n", ascii)   // 97 a 'a'
fmt.Printf("%d %[1]c %[1]q\n", unicode) // 22269 国 '国'
fmt.Printf("%d %[1]q\n", newline)       // 10 '\n'

// 字符串格式化时常用的动词及功能表如下
动 词 功 能
%v 按值的本来值输出
%+v 在 %v 基础上,对结构体字段名和值进行展开
%#v 输出 Go 语言语法格式的值
%T 输出 Go 语言语法格式的类型和值
%% 输出 % 本体
%b 整型以二进制方式显示
%o 整型以八进制方式显示
%d 整型以十进制方式显示
%x 整型以十六进制方式显示
%X 整型以十六进制、字母大写方式显示
%U Unicode 字符
%f 浮点数
%p 指针,十六进制方式显示

2、字符串和数字的转换

除了字符串、字符、字节之间的转换,字符串和数值之间的转换也比较常见,需要用到strconv包提供的功能。

  • 2.1 将一个整数转为字符串
// 方法1:用fmt.Sprintf返回一个格式化的字符串
x:=123
x_new := fmt.Sprintf("%d", x)
fmt.Printf("%[1]T,%[1]v\n",x_new) // string,123

// 方法2:用strconv.Itoa(“整数到ASCII”):
y:=456
y_new := fmt.Sprintf("%d", y)
fmt.Printf("%[1]T,%[1]v\n",y_new) // string,456
  • 2.2 将一个整数转为其他进制,并且结果为字符串类型
// 方法1:strconv包内的Format函数如FormatInt和FormatUint函数可以用不同的进制来格式化数字
x:=123
res:=strconv.FormatInt(int64(x), 2) // 10进制整型转成2进制,结果为字符串类型:"1111011"
fmt.Printf("%[1]T,%[1]v\n",res) // string,1111011

// 方法2:fmt.Sprintf结合动词%b、%d、%o和%x等参数,在需要附加额外的信息时可以用该方法,会比方法1更好用
s := fmt.Sprintf("res=%b", x) // "res=1111011"
  • 2.3 字符串类型解析为整数
// 1、字符串解析为整数,结果为int类型
x, err := strconv.Atoi("123")             // x is an int

// 2、字符串解析为整数,结果为int64类型
y, err := strconv.ParseInt("123", 10, 64) // base 10, up to 64 bits

ParseInt函数的第三个参数是用于指定整型数的大小;例如16表示int16,0则表示int。在任何情况下,返回的结果y总是int64类型,你可以通过强制类型转换将它转为更小的整数类型。

// 3、还有用于解析无符号整数的ParseUint函数(了解)

3、其他

// 求长度
len(str) 求长度(字节个数,文本字符采用的是utf-8编码)
例如:len("a你") 结果为4

utf8.RuneCountInString(s)求长度(字符个数,非法字符钻石问号也算字符)
例如:utf8.RuneCountInString("\xff你好hi") 结果为5

// 字符串拼接
+或fmt.Sprintf拼接字符串
例如:d := fmt.Sprintf("%v - %v - %v", "2020", "05", 20) 结果为:2020 - 05 - 20

// strings
strings.HasPrefix(s string,preffix string) bool:
判断字符串s是否以prefix开头

stirngs.HasSuffix(s string,suffix string) bool:
判断字符串s是否以suffix结尾

strings.Index(s string,str string) int:
判断str在s中首次出现的位置,如果没有出现,则返回-1

strings.LastIndex(s string,str string) int:
判断str在s中最后出现的位置,如果没有出现,则返回-1

strings.Replace(str string,old string,new string,n int):
字符串替换

strings.Count(str string,count int)string:
字符串计数

strings.Repeat(str string,count int) string:
重复count次str

strings.ToLower(str string)
转换为小写

strings.ToUpper(str string)string:
转换为大写

strings.TrimSpace(str string):
去掉字符串首位空白字符

strings.Trim(str string,cut string):
去掉字符串首尾cut字符

strings.TrimLeft(str string,cut string):
去掉字符串首部cut字符

strings.TrimRight(str string,cunt string):
去掉字符串尾部cut字符

strings.Field(str string):
返回str空格分隔的所有子串的slice

string.Split(str string,split string):
返回str split分割的所有子串的slice
例如:strings.Split("egon:18:male",":")结果为切片:[egon 18 male]

strings.Join(s1 []string,sep string):
用sep把s1中的所有元素连接起来
例如:strings.Join([]string{"egon","18","male"},":")结果为egon:18:male

strings.contains
判断是否包含
例如:strings.Contains("abcdef","abc")结果为true

// strconv
scronv.Itoa(i int):把一个整数转换成字符串

scronv.Atio(str string)(int,errror):
把一个字符串转换成整数

扩展阅读

得益于UTF8编码优良的设计,诸多字符串操作都不需要解码操作。我们可以不用解码直接测试一个字符串是否是另一个字符串的前缀:

​```Go
func HasPrefix(s, prefix string) bool {
    return len(s) >= len(prefix) && s[:len(prefix)] == prefix
}
​```

或者是后缀测试:

​```Go
func HasSuffix(s, suffix string) bool {
    return len(s) >= len(suffix) && s[len(s)-len(suffix):] == suffix
}
​```

或者是包含子串测试:

​```Go
func Contains(s, substr string) bool {
    for i := 0; i < len(s); i++ {
        if HasPrefix(s[i:], substr) {
            return true
        }
    }
    return false
}
​```

对于UTF8编码后文本的处理和原始的字节处理逻辑是一样的。但是对应很多其它编码则并不是这样的。(上面的函数都来自strings字符串处理包,真实的代码包含了一个用哈希技术优化的Contains 实现。

标准库中有四个包对字符串处理尤为重要:bytes、strings、strconv和unicode包。strings包提供了许多如字符串的查询、替换、比较、截断、拆分和合并等功,后续将会介绍。

七 在函数参数中传递

本节我们其实学了两种类型:字符串类型string与字符类型rune

二者在函数参数中的传递都是值传递

上一篇
下一篇
Copyright © 2022 Egon的技术星球 egonlin.com 版权所有 青浦区尚茂路798弄 联系方式-13697081366