字典、字符串
参考文章,请点击此处
1 字典
map是一种较为特殊的数据结构,在任何一种编程语言中都可以看见他的身影,它是一种键值对结构,通过给定的key可以快速获得对应的value。
go 语言中的字典和python 中的字典特性差不多
相同: 键值对, 无序集合, 每个键都是唯一的, 对一个键多次赋值会更新当前键的值;
不同: go语言的字典里面的类型是定好的, 不可变更, python可以随意写类型.
map初始化与内存分配
首先,必须给map分配内存空间之后,才可以往map中添加元素:
func main() {
var m map[int]int // 使用var语法声明一个map,不会分配内存
m[1] = 1 // 报错:assignment to entry in nil map
}
如果你使用的是make来创建一个map,Go在声明的同时,会自动为map分配内存空间,不会报错:
func main() {
m := make(map[int]int) // make语法创建map
m[1] = 1 // ok
}
1.1 如何定义字典
var m1 map[string]int
m2 := make(map[int]interface{}, 100)
m3 := map[string]string{
"name": "james",
"age": "35",
}
在定义字典时不需要为其指定容量,因为map是可以动态增长的,但是在可以预知map容量的情况下为了提高程序的效率也最好提前标明程序的容量。需要注意的是,不能使用不能比较的元素作为字典的key,例如数组,切片等。而value可以是任意类型的,如果使用interface{}作为value类型,那么就可以接受各种类型的值,只不过在具体使用的时候需要使用类型断言来判断类型。
package main
import "fmt"
//字典和python是一样, 无序的
//声明新的类型
type Fei struct {
id int
name string
}
type Dic map[string]int
func main() {
//声明字典
dict := make(map[string]string)
dicts := map[string]int{ "age": 10, "丙总": 2}
//新类型
result := make(map[int]*Fei)
result[0] = &Fei{ id: 6300, name: "刀妹"}
letter := []string{"a", "b", "c", "d", "e", "f", "g", "h"} //数组
//用 dict[name] = value 来设置值和修改
dict["name"] = "薇恩"
dict["age"] = "18"
dict["label"] = "小飞"
//新类型的值修改
ss := result[0]
ss.id = 4800
//打印字典的值
name := dict["name"]
res := dict["res"]
fmt.Println(name)
fmt.Println(res)
//打印字典元素个数
fmt.Println("lens", len(dict))
//删除字典元素, 有则删除, 无则不删
delete(dict, "label")
delete(dict, "ask")
//删除全部需要循环
for k := range dict{
delete(dict, k)
}
//批量添加字典
for k,v := range letter{
fmt.Println("还可以", k, v)
dicts[v] = k
}
fmt.Println(dict)
fmt.Println(dicts)
fmt.Println(result[0])
//字典的嵌套操作
respon := make(map[string]Dic)
DicNum := make(Dic) //make 初始化, 分配内存地址, 不为 nil
DicNum["id"] = 1
DicNum["age"] = 2
respon["ids"] = DicNum
respon["type"] = Dic{"id": 11, "age": 22}
fmt.Println(respon)
}
1.2 字典操作
向字典中放入元素也非常简单
m3["key1"] = "v1"
m3["key2"] = "v2"
m3["key3"] = "v3"
你可以动手试一下,如果插入的两个元素key相同会发生什么?
与数组和切片一样,我们可以使用len来获取字典的长度。
len(m3)
在有些情况下,我们不能确定键值对是否存在,或者当前value存储的是否就是空值,go语言中我们可以通过下面这种方式很简便的进行判断。
if value, ok := m3["name"]; ok {
fmt.Println(value)
}
上面这段代码的作用就是如果当前字典中存在key为name的字符串则取出对应的value,并返回true,否则返回false。
对于一个已经存在的字典,我们如何对其进行遍历呢?可以使用下面这种方式:
for key, value := range m3 {
fmt.Println("key: ", key, " value: ", value)
}
如果多运行几次上面的这段程序会发现每次的输出顺序并不相同,对于一个字典来说其默认是无序的,那么我们是否可以通过一些方式使其有序呢?你可以动手尝试一下。(提示:可以通过切片来做哦)
如果已经存在与字典中的值已经没有作用了,我们想将其删除怎么办呢?可以使用go的内置函数delete来实现。
delete(m3, "key1")
除了上面的一些简单操作,我们还可以声明值类型为切片的字典以及字典类型的切片等等,你可以动手试试看。
不仅如此我们还可以将函数作为值类型存入到字典中。
func main() {
m := make(map[string]func(a, b int) int)
m["add"] = func(a, b int) int {
return a + b
}
m["multi"] = func(a, b int) int {
return a * b
}
fmt.Println(m["add"](3, 2))
fmt.Println(m["multi"](3, 2))
}
测试map中key值是否存在
测试一个map里面是否存在某个key值是map的一个重要操作,这个操作允许用户确定是否完成了某些操作或者是否map里缓存了特定的数据。
1、可以同时取值以及一个表示key是否存在的标志。
value,exists := colors["Blue"]
if exists {
fmt.Println("存在,value:",value)
}
2、当通过键值来索引,当key值不存在时也会返回一个类型对应的零值。
value := colors["Blue"]
if value!=""{
fmt.Println("存在,value:",value)
}
3、map的遍历
map是通过range来进行遍历,range返回的不是索引和值,而是键值对。
for key,value :=range colors{
fmt.Printf("key:%v,value:%v",key,value)
}
4、如果想把key从一个map中删掉,直接使用内置的delete函数。
delete(colors,"Red")
5、map遍历的无序性
在Go语言中,多次遍历相同的map,得到的结果是不一样的:
func main() {
m := make(map[int]int)
m[0] = 1
m[1] = 2
m[3] = 5
for k, v := range m {
fmt.Println(k, v)
}
// 第一次遍历结果:
0 1
1 2
3 5
// 第二次遍历结果:
3 5
0 1
1 2
}
2 字符串
【Go语言踩坑系列(二)】字符串
字符串应该可以说是所有编程语言中最为常用的一种数据类型,接下来我们就一起探索下go语言中对于字符串的常用操作方式。
为什么需要byte和rune
我们知道,Go语言中有两种特殊的别名类型,是byte和rune,分别代表uint8和int32类型,即1个字节和4个字节。我们在开发中,常常会用到string类型和[]byte、[]rune类型的转换。它可能长下面这个样子:
func main() {
s := "hello 世界"
runeSlice := []rune(s) // len = 8
byteSlice := []byte(s) // len = 12
// 打印每个rune切片元素
for i:= 0; i < len(runeSlice); i++ {
fmt.Println(runeSlice[i])
// 输出104 101 108 108 111 32 19990 30028
}
fmt.Println()
// 打印每个byte切片元素
for i:= 0; i < len(byteSlice); i++ {
fmt.Println(byteSlice[i])
// 输出104 101 108 108 111 32 228 184 150 231 149 140
}
}
我们可以看到,因为Go中的字符串采用UTF-8编码,且由于rune类型是4个字节,所以切片[]rune中,一个rune切片中的单个元素(4个字节),就能够完整的容纳一个UTF-8编码的中文字符(3个字节);而在[]byte中,由于每个byte切片元素只有1个字节,所以需要3个byte切片元素来表示一个中文字符。这样,用[]byte表示的字符串就要比[]rune表示的字符串,切片长度多4(6 - 2),打印结果符合预期。
所以,我个人认为设计rune类型的目的,就是为了更方便的表示类似中文的非英文字符,处理起来更加方便;而byte类型则对英文字符的处理更加友好。这里总结一下:
一个值在从string类型向[]byte类型转换时代表着以 UTF-8
编码的字符串,会被拆分成零散、独立的字节。可能一个完整的字符(如中文),会由多个byte切片中的元素组成。一个值在从string类型向[]rune类型转换时代表着以 UTF-8 编码字符串,会被拆分成一个个完整的字符。
2.1 字符串定义
字符串是一种值类型,在创建字符串之后其值是不可变的,也就是说下面这样操作是不允许的。
s := "hello"
s[0] = 'T'
编译器会提示cannot assign to s[0]。在C语言中字符串是通过\0来标识字符串的结束,而go语言中是通过长度来标识字符串是否结束的。
如果我们想要修改一个字符串的内容,我们可以将其转换为字节切片,再将其转换为字符串,但是也同样需要重新分配内存。
func main() {
s := "hello"
b := []byte(s)
b[0] = 'g'
s = string(b)
fmt.Println(s) //gello
}
与其他数据类型一样也可以通过len函数来获取字符串长度。
len(s)
但是如果字符串中包含中文就不能直接使用byte切片对其进行操作,go语言中我们可以通过这种方式
func main() {
s := "hello你好中国"
fmt.Println(len(s)) //17
fmt.Println(utf8.RuneCountInString(s)) //9
b := []byte(s)
for i := 0; i < len(b); i++ {
fmt.Printf("%c", b[i])
} //helloä½ å¥½ä¸å�½
fmt.Println()
r := []rune(s)
for i := 0; i < len(r); i++ {
fmt.Printf("%c", r[i])
} //hello你好中国
}
在go语言中字符串都是以utf-8的编码格式进行存储的,所以每个中文占三个字节加上hello的5个字节所以长度为17,如果我们通过utf8.RuneCountInString函数获得的包含中文的字符串长度则与我们的直觉相符合。而且由于中文对于每个单独的字节来说是不可打印的,所以可以看到很多奇怪的输出,但是将字符串转为rune切片则没有问题。
2.2 strings包
strings包提供了许多操作字符串的函数。在这里你可以看到都包含哪些函数https://golang.org/pkg/strings/。
下面演示几个例子:
func main() {
var str string = "This is an example of a string"
//判断字符串是否以Th开头
fmt.Printf("%t\n", strings.HasPrefix(str, "Th"))
//判断字符串是否以aa结尾
fmt.Printf("%t\n", strings.HasSuffix(str, "aa"))
//判断字符串是否包含an子串
fmt.Printf("%t\n", strings.Contains(str, "an"))
}
2.3 strconv包
strconv包实现了基本数据类型与字符串之间的转换。在这里你可以看到都包含哪些函数https://golang.org/pkg/strconv/。
下面演示几个例子:
i, err := strconv.Atoi("-42") //将字符串转为int类型
s := strconv.Itoa(-42) //将int类型转为字符串
若转换失败则返回对应的error值。
2.4 字符串拼接
除了以上的操作外,字符串拼接也是很常用的一种操作,在go语言中有多种方式可以实现字符串的拼接,但是每个方式的效率并不相同,下面就对这几种方法进行对比。(关于测试的内容会放在后面的章节进行讲解,这里大家只要知道这些拼接方式即可)
1.SPrintf
const numbers = 100
func BenchmarkSprintf(b *testing.B) {
b.ResetTimer()
for idx := 0; idx < b.N; idx++ {
var s string
for i := 0; i < numbers; i++ {
s = fmt.Sprintf("%v%v", s, i)
}
}
b.StopTimer()
}
2.+拼接
func BenchmarkStringAdd(b *testing.B) {
b.ResetTimer()
for idx := 0; idx < b.N; idx++ {
var s string
for i := 0; i < numbers; i++ {
s += strconv.Itoa(i)
}
}
b.StopTimer()
}
3.bytes.Buffer
func BenchmarkBytesBuf(b *testing.B) {
b.ResetTimer()
for idx := 0; idx < b.N; idx++ {
var buf bytes.Buffer
for i := 0; i < numbers; i++ {
buf.WriteString(strconv.Itoa(i))
}
_ = buf.String()
}
b.StopTimer()
}
4.strings.Builder拼接
func BenchmarkStringBuilder(b *testing.B) {
b.ResetTimer()
for idx := 0; idx < b.N; idx++ {
var builder strings.Builder
for i := 0; i < numbers; i++ {
builder.WriteString(strconv.Itoa(i))
}
_ = builder.String()
}
b.StopTimer()
}
5.对比
BenchmarkSprintf-8 68277 18431 ns/op
BenchmarkStringBuilder-8 1302448 922 ns/op
BenchmarkBytesBuf-8 884354 1264 ns/op
BenchmarkStringAdd-8 208486 5703 ns/op
可以看到通过strings.Builder拼接字符串是最高效的。
备注
从字符编码说起
ASCII
计算机是为人类服务的,我们自然有表示我们人类所有语言与符号的需求。由于计算机底层实现全部为二进制,为了用计算机表示并存储人类文明所有的符号,我们需要构造一个“符号” => “唯一编码”的映射表,且这个编码能够用二进制来表示。这样就实现了用计算机来表示人类的文字与符号。最早的映射表叫做ASCII码表,如:a => 97。这个相信大家都很熟悉了,它是由美国人发明的,自然首先需要满足容纳所有英文字符的需求,所以并没有考虑其他国家的语言与符号要如何用计算机来表示。
但是随着计算机的发展,其他国家也陆续有了使用计算机的需求。由于ASCII码只用1个字节存储,所以最多只能表示256种符号,无法表示其他国家的文字(如中文等)。为了解决ASCII表示范围有限的问题,以容纳其他国家的文字与符号,Unicode出现了。
Unicode
Unicode究竟有多强大?我们举一个例子来直观的感受一下:中文的“世”字,若用Unicode映射规则来表示,为“U+4E16”。U+代表Unicode,我们先不用管。“4E16”就是“世”字在所有人类的字符集中的唯一编码了,可以把这个编码看成数据库中的id,唯一确定“世”这个符号。Unicode能够存储目前世界上所有的文字与符号。
我始终在强调"映射规则"。ASCII、Unicode只是定义了一个“符号” => “唯一编码”的映射规则而已,并不关心具体计算机底层是如何用二进制存储的。
Unicode的存储实现
我们先自己实现一个
接下来我们关注究竟如何用二进制,来表示并存储“世”字这个Unicode编码“4E16”:先抛开业界已有的方案,我们先自己设计一个。按照惯性思维,我们可以直接想到,直接在底层将“4E16"转为二进制进行存储:即01001110 00010110,共2个字节。我们可以看到,这里Unicode规则和计算机二进制编码一一对应,不加任何优化与修改,这就是最早的UTF-16编码方案。
但是UTF-16编码存在一定的问题:无论是ASCII中定义的英文字符,还是复杂的中文字符,它都采用2个字节来存储。如果严格按照2个字节存储,编码号比较小的(如英文字母)的许多高位都为0(如字母t:00000000 01110100)。
这样一来,由于很多英文编码的高位都是0,但仍需要固定的2个字节来存储,所以UTF-16编码就造成了大量的空间浪费。我们怎么优化呢?我们想到,没有必要所有符号都统一都用2个字节来表示。编码号较小的,如英文字符,仅用1个字节表示就可以了;而编码号较大的中文字符,则用3个字节来表示。这种规则就是我们所熟知的UTF-8编码方式。Unicode的实现方式称为Unicode转换格式(Unicode Transformation Format,简称为UTF)
UTF-8
UTF-8编码方式如下:
单字节的字符,字节的第一位设为0,对于英文,UTF-8码只占用一个字节,和ASCII码完全相同;
n个字节的字符(n>1),第一个字节的前n位设为1,第n+1位设为0,后面字节的前两位都设为10,这n个字节的其余空位填充该字符unicode码,高位用0补足。
对于我们之前的例子,“世”需要用3个字节来存储,在UTF-8中以“E4B896”来存储。而对于英文字符“t”则以“74”来存储。所以,我们可以看到,虽然中文所需的存储空间比UTF-16多了1个字节,但是英文字符却减少了一个字节。综合考虑,由于我们使用英文字符的频率远远高于中文字符,所以这种改动是利大于弊的。相较前文的UTF-16编码方式,UTF-8的灵活度更大,也更节省存储空间。
编程范式
综上,UTF-16、UTF-8、还有其他五花八门的编码存储方式,都是Unicode的底层存储实现。用编程范式的语言来描述:Unicode是接口,定义了有哪些映射规则;而UTF-8、UTF-16则是Unicode这个接口的实现,它们在计算机底层实现了这些映射规则。