七叶笔记 » golang编程 » 网络工程师的Golang之路 — Go数据类型(字符串)

网络工程师的Golang之路 — Go数据类型(字符串)

版权声明:我已加入“维权骑士”( )的版权保护计划,知乎专栏“网路行者”下的所有文章均为我本人(知乎ID:弈心)原创,未经允许不得转载。

如果你喜欢我的文章,请关注我的知乎专栏“网路行者” , 里面有更多像本文一样深度讲解计算机网络技术的优质文章。


Go语言的数据类型和Python大致相同,但也有一些区别。 Go 中的数据类型大体上按类别分为布尔型、数字型(包括整数、浮点数、复数)、 字符串 型、错误型( 是的,错误error在Go中是一种数据类型 )、派生型(包括指针、函数、数组、结构化、Channel、切片、接口、Map),本篇主要介绍字符串型。

字符串重要概念

字符串(string)在Go语言和Python中有着一些差别和共同点,具体如下:

  • 根据Go语言官方的定义: In Go, a string is in effect a read-only slice of bytes.

意思是Go中的字符串是一组只读的字节切片(slice of bytes,关于切片的概念后文会讲到,这里你可以把它理解为Python中的列表),每个字符串都使用一个或多个字节表示(当字符为 ASCII 码表上的字符时占用 1 个字节,比如英文字母,其它字符根据需要占用 2-4 个字节,比如汉语、日语中的汉字、平假名、片假名等)。

  • Go中的字符串使用的是UTF-8编码,而Python中的字符串则使用的是Unicode编码(Python3)。
  • 综上两点,Go中的纯英文字符串的字节切片长度和它的字母个数完全相等,比如test这个英文单词有4个字母,那么,len(“test”)返回的值也为4(len()函数在Go和Python中的用法一样,都可用来返回字符串的长度,并且返回的值都为整数)。因为英文字母的编码为 ASCII ,可以用字节表示。 而其他需要用 Unicode 编码的语言,比如中文字符串,那么它的字节切片长度往往不会等于它的文字个数,比如”测试”这个词语虽然只包含两个汉字,但是len(“测试”)会返回6,因为”测”字和”试”字分别需要用3个字节来表示,总共占用6个字节,举例如下:

而在Python3中,字符串默认就是用Unicode表示,因此在Python3中len(“测试”)会返回2,而不是6,举例如下:

Go和Python在字符串上的这个重大差异务必牢记,很多最早从Python3一路学来的Netdevops网工会想当然的认为中文字符串在Go中的长度会和在Python中一样等同于它的汉字个数,结果犯了一个最基本的错误。这里你会问,那怎样在Go中通过len()函数正确地显示中文字符串的长度呢(即让字符串长度等于汉字个数)?答案会在本篇最后的“Rune切片(Rune Slice)”一节中讲到。

  • Go语言中声明一个字符串变量只能用双引号””或者反勾号 (backtick,‘’),而Python中可以使用 单引号 、双引号、三 引号 来声明字符串变量, 在Go中单引号只能用来声明单一字符(character),即rune 。关于字符和字符串的区别,以及什么是rune会在本篇后面的“Rune和 Byte ,字符和字符串”一节中讲到。
  • Go和Python都支持字符串格式化。
  • Go和Python都支持字符串的拼接(concantenation)。
  • Go和Python都支持字符串的索引和切片,但是两者在细节上有较大差异。

字符串格式化

和Python一样,Go语言也支持字符串格式化,不过相较于Python中可以通过取模运算符%、format()函数以及f-strings三种方式来做字符串格式化,字符串格式化在Go中的的形式较为单一,我们可以通过fmt.Printf()配合取模运算符%来实现字符串格式化,举例如下:

 package main

import "fmt"
 
func main() {
    var subnet string = "192.168.1.0/24"
    host := 100
    var IP = "172.16.10.20"
    var usage float32
    usage = 23.8
    fmt.Printf("网段%s下有%d台活跃主机。\n", subnet, host)
    fmt.Printf("交换机%s当前的 CPU使用率 为%f。\n", IP, usage)
}
  

和Python一样,在Go中使用取模运算符来做字符串格式化时,常见的格式码(format code)如下:

%T代表数据类型

%c代表字符

%s代表字符串

%d代表十进制整数

%f代表浮点数

其他的格式码比如%v, %q, %U, %P等 网络工程师 使用到的机会不多,这里不做介绍。

用%T查询变量的数据类型

在Go中,要知道一个变量的数据类型,除了可以在源码中寻找答案外,也可以使用 Printf ()配合%T这个格式码,用字符串格式化的方式实现(在Go中,Printf()+T%等同于Python中的type()函数),这里%T中的T就是Type的缩写用来表示变量的数据类型,举例如下:

 package main
 
import "fmt"
 
func main() {
    var vendor string = "Cisco"
    fmt.Println(vendor)
    fmt.Printf("变量vendor的数据类型为%T\n", vendor)
}
  


字符串的拼接

Go语言中做字符串拼接的方式很多,主要有字符串拼接符“+”、strings.Join()、 bytes .Buffer、strings.Builder等四种形式来实现字符串的拼接(从严格意义上来说,上面讲到的字符串格式化也属于字符串拼接的一种),下面一一举例讲解它们的使用方法及区别:

1. 字符串拼接符“+”

字符串拼接符“+”是最常见也是最为简单的字符串拼接方式,需要注意的是和Python一样,在Go用“+”号做拼接前需要确保做拼接的变量的数据类型为字符串,如果不是字符串,则必须用fmt.Sprint()将其转换成字符串才能完成拼接,否则系统会返回”mismatched types string and int”的异常,如下:

 package main
 
import "fmt"
 
func main() {
    var subnet string = "192.168.1.0/24"
    host := 100
    var IP = "172.16.10.20"
    var usage float32
    usage = 23.8
    fmt.Printf("网段" + subnet + "下有" + host + "台活跃主机。\n")
    fmt.Printf("交换机" + IP + "当前的CPU使用率为" + fmt.Sprint(usage) + "。\n")
}
  

变量host为整数类型,这里因为在没有将它转换为字符串之前就使用了字符串拼接符,所以系统返回了”mismatched types string and int”这个异常。正确的做法是使用fmt.Sprint(host)将该变量转换成字符串(fmt.Sprint(usage)同理)然后用字符串拼接符做拼接,如下:

 package main
 
import "fmt"
 
func main() {
    var subnet string = "192.168.1.0/24"
    host := 100
    var IP = "172.16.10.20"
    var usage float32
    usage = 23.8
    fmt.Printf("网段" + subnet + "下有" + fmt.Sprint(host) + "台活跃主机。\n")
    fmt.Printf("交换机" + IP + "当前的CPU使用率为" + fmt.Sprint(usage) + "。\n")
}
  

2. strings.Join()、切片、数组

前面讲到了,在Go中,字符串实际上是一组只读、长度可变的字节切片。要知道strings.Join()的用法,必须知道什么是切片(Slice),而要理解切片,又必须先要知道什么是数组(Array),对有Python基础的网络工程师读者来说,所谓数组可以理解为一种特殊的列表,区别是列表可以包含多种数据类型的元素,而数组只能包含同一种数据类型的元素,即一个数组里的所有元素必须全部为字符串,或者全部为整数(其实在Python中同样存在数组这个数据类型,比如数据分析领域中常用的 numpy 这个模块就会用到数组,只是网络工程师在Python中接触数组的机会不多)。

在Go中,切片是数组衍生出来的概念,两者的区别是:数组的长度是固定的,在声明一个数组时,你必须指定该数组的长度(即该数组里面有多少个元素),以及该数组里元素的数据类型。而切片不同,切片的长度不是固定的,在声明一个切片时,只需指明切片里元素的数据类型即可,这里简单举例说明两者的却别:

 package main

 import  (
 "fmt"
)

func main() {
    vendor1 := [5]string{"C", "i", "s", "c", "o"}  
    vendor2 := []string{"H", "u", "a", "w", "e", "i"}
    fmt.Printf("%T\n", vendor1)
    fmt.Printf("%T\n", vendor2)
}
  

从上面的例子可以看出,创建数组的语法为:

 [元素个数]元素数据类型{元素}  

而创建切片的语法为:

 []元素数据类型{元素}  

注意,在Go中,数组和切片的数据类型没有专门对应的词(比如Array和Slice)来表达,而是直接以“[元素个数]元素数据类型{元素}”和“[]元素数据类型{元素}”分别作为它们的数据类型,这里我用的是简短方式来分别声明vendor1和vendor2这两个数组和切片变量:

 vendor1 := [5]string{"C", "i", "s", "c", "o"}  
vendor2 := []string{"H", "u", "a", "w", "e", "i"}
  

如果要以标准形式来声明vendor1和vendor2这两个数组和切片变量的话,写法如下:

 var vendor1 [5]string = [5]string{"C", "i", "s", "c", "o"}
var vendor2 []string = []string{"H", "u", "a", "w", "e", "i"}
  

虽然切片是在数组的基础上衍生出来的数据类型,但因为切片的灵活性,实际上它在Go中的应用比数组更广泛,比如strings.Join()就必须配合切片来完成字符串的拼接,举例如下:

 package main
 
import (
 "fmt"
 "strings"
)
 
func main() {
    var vendor1 []string = []string{"C", "i", "s", "c", "o"}
    var vendor2 []string = []string{"H", "u", "a", "w", "e", "i"}
    result1 := strings.Join(vendor1, "")
    result2 := strings.Join(vendor2, "")
    fmt.Println(result1)
    fmt.Println(result2)
}
  

这里我们除fmt之外,还用import导入了strings这个标准包,以便等下使用strings.Join()这个方法。然后我们将vendor1(这里注意vendor1现在不再是数组而是切片)和vendor2两个切片类型的变量作为strings.Join()的参数传入,然后分别赋值给变量result1和result2,注意strings.Join(vendor1, “”)后面的””表示分隔符,这里分隔符为空,表示等下做拼接时,切片里的所有字符串元素将紧贴在一起,因此最后打印出的result1和result2分别为Cisco和Huawei。

3. bytes.Buffer

因为在Go中字符串实际上是一组只读、长度可变的字节切片,因此我们还可以引入Go内置的bytes标准包,通过它创建一个类型为bytes.Buffer的变量(你可以把bytes.Buffer变量理解为组成字符串的字节),然后使用它的WriteString()方法来做拼接,最后通过该变量的String()方法将它转化为字符串,即得到了拼接后的字符串内容,举例如下:

 package main
 
import (
 "bytes"
 "fmt"
)
 
func main() {
    var vendor1 bytes.Buffer
    vendor1.WriteString("C")
    vendor1.WriteString("i")
    vendor1.WriteString("s")
    vendor1.WriteString("c")
    vendor1.WriteString("o")
    var vendor2 bytes.Buffer
    vendor2.WriteString("H")
    vendor2.WriteString("u")
    vendor2.WriteString("a")
    vendor2.WriteString("w")
    vendor2.WriteString("e")
    vendor2.WriteString("i")
    fmt.Println(vendor1.String())
    fmt.Println(vendor2.String())
}
  

这里我们通过import bytes导入了bytes这个标准包,然后创建了vendor1和vendor2两个类型为bytes.Buffer的变量,针对vendor1我们通过bytes.Buffer自带的WriteString()方法将”C”, “i”, “s”, “c”,”o”五个字母做了拼接,得到了“Cisco”这个拼接后的字符串,针对vendor2我们通过WriteString()方法将”H”, “u”, “a”, “w”,”e”, “i”六个字母做了拼接,得到了“Huawei”这个拼接后的字符串,最后分别对vendor1和vendor2使用String()来获得拼接后的字符串,并用fmt.Println()将它们打印出来。 注意使用bytes.Buffer来做字符串拼接可以避免生成一个新的字符串变量(比如前面讲strings.Join()时,我们额外创建了result1和result2两个变量),所有拼接的操作都是在同一个变量上完成的。

4. strings.Builder

上面讲到的bytes.Buffer()是通过bytes这个模块来操作字符串的拼接,这种方法多少让用户感到有些迷惑。从go1.10开始,Go语言官方在strings这个标准包里新加入了strings.Builder这个数据类型,并且官方鼓励用户在做字符串的拼接时使用strings.Builder,做字节的拼接时使用bytes.Buffer。

strings.Builder的使用方法举例如下:

 package main
 
import (
 "fmt"
 "strings"
)
 
func main() {
    var vendor1 strings.Builder
    vendor1.WriteString("C")
    vendor1.WriteString("i")
    vendor1.WriteString("s")
    vendor1.WriteString("c")
    vendor1.WriteString("o")
    var vendor2 strings.Builder
    vendor2.WriteString("H")
    vendor2.WriteString("u")
    vendor2.WriteString("a")
    vendor2.WriteString("w")
    vendor2.WriteString("e")
    vendor2.WriteString("i")
    fmt.Println(vendor1.String())
    fmt.Println(vendor2.String())
}
  

可以看到,strings.Builder和bytes.Buffer的使用方法几乎一模一样,两者都是通过WriteString()来做字符串的拼接,都是通过String()来获得拼接后的字符串。不过根据Go官方的说法strings.Builder在内存使用上的性能要略优于bytes.Buffer,这里推荐大家按照官方的建议,在做字符串的拼接时使用strings.Builder,做字节的拼接时使用 byte s.Buffer。


字符串的索引

在开发的过程中,有时我们需要获取字符串中的某个或者某段字符,这时就需要对字符串做索引(针对单个字符)或者切片(针对多个连续字符)。

Go语言的字符串索引号从0开始,即字符串里从左往右的第一个字符的 索引 号为0,这点和Python一样。但是和Python不同的是:在Python中对字符串做索引后返回的值依然是字符串,比如一个字符串变量vendor1的值为“Cisco”,那么在Python中print (vendor1[0])会打印出首字母“C”, 但是在Go中,索引返回的值为byte(byte是uint8的别名,uint8的中文叫做无符号8位整数,相关的知识会在下一篇讲解整数的章节中讲到),如果你用fmt.Println(vendor1[0])会打印出字母C对应的uint8的值,即unit8无符号数67,演示如下:

 package main

import (
 "fmt"
)

func main() {
    vendor1 := "Cisco"
    fmt.Println(vendor1[0])
}
  

解决办法有两个:

1. 使用%c格式码(%c格式码表示character,即字符,注意字符不等同于字符串),通过字符串格式化的方式将uint8整数转化为字符:

 package main
 
import "fmt"
 
func main() {
 vendor1 := "Cisco"
    fmt.Printf("%c\n", vendor1[0])
}
  

2. 使用string()函数将unit8整数转化为字符串:

 package main
 
import "fmt"
 
func main() {
    vendor1 := "Cisco"
    fmt.Println(string(vendor1[0]))
}
  

另外Go还有一点和Python不同的是:在Python中索引号可以为负整数,用来表示倒数,比如索引号为-1时表示字符串里最后一个字符,-2表示字符串里倒数第二个字符,以此类推。但是在Go中,索引号不支持使用负整数,只能使用0或正整数,否则系统会返回“invalid string index -1 (index must be non-negative)”的异常,举例如下:

解决的方法是使用len()函数:


字符串的切片

前面讲到了,在开发的过程中,如果需要获取字符串中某段连续的字符,那么我们就需要对字符串做切片。Go语言的字符串切片和Python类似,都是使用“string[start : end]“的语法格式,举例如下:

 package main

import (
 "fmt"
)

func main() {
    vendor1 := "Cisco"
    fmt.Println(vendor1[1:4]) //打印"Cisco"中第2至第4个字符
    fmt.Println(vendor1[1:]) //打印"Cisco"中第2至最后一个字符
    fmt.Println(vendor1[:3]) //打印"Cisco"中前3个字符  
    fmt.Println(vendor1[:]) //打印"Cisco"中所有字符
}

  

在Go中另外如果字符串中包含中文时,因为中文使用的编码不是由一个字节组成,Go需要先将该字符串转换成rune数组(关于rune数组会在下一节“整数”中讲解),否则会出现乱码的问题,举例如下:

这里我们尝试对book_name这个字符串变量做切片,打印出它的前五个字,可以看到系统返回的乱码“网??”。

解决方法是先将字符串内容“网络工程师的Golang之路”转换成rune数组并将其赋值给变量book_name,然后再用fmt.Println()配合string()函数将rune数组转换回字符串后将其打印出来,举例如下:

 package main

import (
 "fmt"
)

func main() {
    book_name := []rune("网络工程师的Golang之路")
    fmt.Println(string(book_name[:5]))
}
  


Rune和Byte,字符和字符串

在讲解字符串时我们已经提到过Rune和Byte这两种数据类型。在Go语言中,所谓的Rune和Byte其实分别就是int32有符号整数类型和uint8无符号整数类型的别称(int32和uint8相关的知识会在下一篇讲解整数的章节中讲到),也就是说Rune=int32,Byte=unit8。为了理解Rune和Byte的作用,我们必须知道字符和字符串的区别。

和Python不同,Go语言中有字符(character)的概念,字符和字符串的区别在于 字符表达的是单一的字母、数字、空格、标点符号 而字符串表达的是一个或多个字母、数字、空格和标点符号。 和C、 Java 等语言不同,Go语言中并没有 char 这个专门用来表示字符的数据类型,而是使用Rune和Byte来表达字符。因此Rune虽然是有符号整数类型的一种,但它通常不用于表示231 到 231-1这些整数,而是用来表示长度可以达到32bit的字符(比如Unicode编码格式的字符),之前我们在字符串一节中讲到在对中文字符串做切片时必须将字符串先从默认的8bit的Byte(前面讲到了,在Go中,所谓字符串就是一组字节的切片(slice of bytes))转换成32bit的Rune否则的话会出现乱码就是这个原因,因为汉字通常需要3个字节(24bit)来表示。

而和unit8等价的Byte其实就是我们通常理解的字节,既计算机中最常见的存储单位(1 byte = 8 bits, 1024 bytes = 1 kilobyte, 1024 kilobytes = 1 megabyte…)。和Rune类似,Byte虽然是无符号整数类型的一种,但是它主要的作用并不是用来表示0到255这些整数,而是用来表示长度为8bit的字符。

和用 双引号 声明的字符串不一样,在Go中我们用单引号来声明一个字符 ,字符有两种数据类型:Rune和Byte,默认情况下字符的类型为Rune(即隐式声明字符变量时其类型为Rune),举例如下:

 package main
 
import "fmt"
 
func main() {
    var a rune = 'A'
    fmt.Println(a)
    fmt.Printf("%T\n", a)
    var b = '中'
    fmt.Println(b)
    fmt.Printf("%T\n", b)
}
  

这里我们以显示的方式声明了字符变量a,以隐式的方式声明了字符变量b,此时a和b的类型都为Rune,即int32。

这里你也许会问:为什么我们赋值给变量a的是字符“A”,赋值给变量b的是字符“中”,但变量a和b打印出来的结果却是整数65和20013? 这是因为不管是Rune还是Byte,它们的本质还是整数 ,而字符“A”对应的整数即为65,字符“中”对应的整数即为20013。如果要想将变量a和变量b打印出的内容以原本的字符内容显示,则需要用到格式码%c或者string()函数来将字符转变为字符串,举例如下:

 package main
 
import "fmt"
 
func main() {
 var a rune = 'A'
    fmt.Printf("%c\n", a)
    fmt.Printf("%T\n", a)
 var b = '中'
    fmt.Println(string(b))
    fmt.Printf("%T\n", b)
}
  

因为默认情况下字符的类型为Rune,如果要创建一个Byte类型的字符变量的话,则必须显示声明变量的类型,举例如下:

 package main
 
import "fmt"
 
func main() {
    var a byte = 'A'
    fmt.Printf("%c\n", a)
    fmt.Printf("%T\n", a)
    var b byte = '中'
    fmt.Printf("%c\n", b)
    fmt.Printf("%T\n", b)
}
  

这里在运行该程序时出现了“.\integer.go:9:6: constant 20013 overflows byte”的报错,原因也很简单:Byte对应的是无符号整数类型uint8,而uint8的范围是0到255,而字符”中”对应的整数为20013,显然20013不在unit8的范围内,因此会报错。解决的办法就是避免将中文字符赋值以Byte的类型赋值给变量,因为Byte对应的是ASCII编码,而中文对应的是Unicode或UTF-8,需要用到Rune。

这里将代码修改一下,将字符”b”和字符”c”赋值给Byte变量a和变量b并将它们分别以整数和%c的格式打印出来:

 package main
 
import "fmt"
 
func main() {
 var a byte = 'b'
    fmt.Println(a)
    fmt.Printf("%T\n", a)
 var b byte = 'c'
    fmt.Printf("%c\n", b)
    fmt.Printf("%T\n", b)
}
  

最后再强调一次: 字符表达的是单一的字母、数字、空格、标点符号, 在使用rune或者byte来声明一个字符变量时, 如果字符内容里哪怕只多出1个字母、数字、空格、标点符号,那都是无效的字符 ,Go会返回”more than one character in rune literal”的异常,举例如下:

 package main
 
import "fmt"
 
func main() {
 var r1 rune = 'a'
    fmt.Println(string(r1))
 var r2 rune = 'ab'
    fmt.Println(string(r2))
}
  

这里我们分别声明了r1和r2两个rune类型的字符,r1是正常的字符,而r2仅仅因为在字符a后面多加了一个b就导致了Go返回了”more than one character in rune literal”这个异常。


Rune切片(Rune slices)

在本篇开头提到了:Go和Python都有len()函数,因为每个汉字字符串占3个字节,所以在Go中默认情况下len(“测试”)会返回整数6而不是像Python那样返回整数2。要让Go的len()返回的整数和字符串中的汉字个数相等,需要将字符串从默认的字节切片(Byte Slice)转换为Rune切片(Rune Slice)。举例如下:

 package main
 
import (
 "fmt"
)
 
func main() {
    chinese := "网络工程师"
    fmt.Println(len(chinese))
    rune_chinese := []rune(chinese)
    fmt.Println(len(rune_chinese))
}
  

这里我们声明了一个内容为“网络工程师”的字符串变量chinese,由于总共是5个汉字,因此用len(chinese)返回的是整数15,随后我们将变量chinese通过[]rune()的形式转换为rune切片之后再将它赋值给另外一个变量rune_chinese,此时len(rune_chinese)返回的则是整数5,即“网络工程师”的汉字个数。

为了让大家更直观的看到将中文字符串转换为rune切片后的变化,这里我再举一例:

 package main
 
import (
 "fmt"
)
 
func main() {
    chinese := "网络工程师"
    rune_chinese := []rune(chinese)
 
    for i, v := range chinese {
        fmt.Printf("%d: %c\n", i, v)
    }
    fmt.Println()
    for i, v := range rune_chinese {
        fmt.Printf("%d: %c\n", i, v)
    }
}
  

这里我们用for语句和range语句对变量chinese以及变量rune_chinese做for循环,遍历其中的每个字符(Go中的for循环和Python有较大差异,关于for循环的知识会在后文讲到,这里了解即可)。这种方式会返回两个值:字符的索引号(index),即这里for i,v中的i,以及字符具体的值(value),即这里for i,v中的v。

可以看到v的内容和中文字符串的内容一样,都是“网络工程师”,但是变量chinese的字符索引号一个是0,3,6,9,12,变量rune_chinese的字符索引号0,1,2,3,4。原因也很简单,因为变量chinese是字符串,即字节切片,每个汉字需要占三个字节,因此“网”字的索引号为0,1,2,”络”字的索引号为3,4,5,一次类推。而变量rune_chinese是rune切片,每个rune代表一个汉字,所以这时”网”字的索引号为0,”络”字的索引号为1,依此类推。

相关文章