字典、字符串

字典、字符串

参考文章,请点击此处

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这个接口的实现,它们在计算机底层实现了这些映射规则。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值