一、字符串
参考
【golang】浅析rune,byte
golang string和[]byte的对比
在源码中builtin.go中使用了类型别名(基础知识可参考Golang关键字--type 类型定义)
// byte is an alias for uint8 and is equivalent to uint8 in all ways. It is
// used, by convention, to distinguish byte values from 8-bit unsigned
// integer values.
type byte = uint8
// rune is an alias for int32 and is equivalent to int32 in all ways. It is
// used, by convention, to distinguish character values from integer values.
type rune = int32
// string is the set of all strings of 8-bit bytes, conventionally but not
// necessarily representing UTF-8-encoded text. A string may be empty, but
// not nil. Values of string type are immutable.
type string string
rune类型的底层类型是int32类型,而byte类型的底层类型是int8类型,这决定了rune能比byte表达更多的数。
字符串是一系列8位字节的集合,通常但不一定代表UTF-8编码的文本。字符串可以为空,但不能为nil。而且字符串的值是不能改变的。不同的语言字符串有不同的实现,在go的源码中src/runtime/string.go,string的定义如下:
type stringStruct struct {
str unsafe.Pointer
len int
}
可以看到str其实是个指针,指向某个数组的首地址,另一个字段是len长度。那到这个数组是什么呢? 在实例化这个stringStruct的时候:
func gostringnocopy(str *byte) string {
ss := stringStruct{str: unsafe.Pointer(str), len: findnull(str)}
s := *(*string)(unsafe.Pointer(&ss))
return s
}
哈哈,其实就是byte数组,而且要注意string其实就是个struct。
在unicode中,一个中文占两个字节,utf-8中一个中文占三个字节,golang默认的编码是utf-8编码,因此默认一个中文占三个字节,但是golang中的字符串底层实际上是一个byte数组。因此可能会出现下面这种奇怪的情况
str := "hello 世界"
fmt.Println(len(str)) //12
我们期望得到的结果应该是8,原因是golang中的string底层是由一个byte数组实现的,而golang默认的编码是utf-8,因此在这里一个中文字符占3个字节,所以获得的长度是12,想要获得我们想要的结果也很简单,golang中的unicode/utf8包提供了用utf-8获取长度的方法
str := "hello 世界"
fmt.Println(utf8.RuneCountInString(str)) //8
上面说了byte类型实际上是一个int8类型,int8适合表达ascii编码的字符,而int32可以表达更多的数,可以更容易的处理unicode字符,因此,我们可以通过rune类型来处理unicode字符
str := "hello 世界"
str2 := []rune(str)
fmt.Println(len(str2)) //8
这里会将申请一块内存,然后将str的内容复制到这块内存,实际上这块内存是一个rune类型的切片,而str2拿到的是一个rune类型的切片的引用,我们可以很容易的证明这是一个引用
str := "hello 世界"
str2 := []rune(str)
t := str2
t[0] = 'w'
fmt.Println(string(str2)) //“wello 世界”
通过把str2赋值给t,t上改变的数据,实际上是改变的是t指向的rune切片,因此,str也会跟着改变
对于字符串,看一下如何遍历吧,也许你会觉得遍历轻而易举,然而刚接触golang的时候,如果这样遍历字符串,那么将是非常糟糕的
str := "hello 世界"
for i := 0;i < len(str);i++ {
fmt.Println(string(str[i]))
}
输出:
h
e
l
l
o
ä
¸
ç
如何解决这个问题呢?
第一个解决方法是用range循环
str := "hello 世界"
for _,v := range str {
fmt.Println(string(v))
}
输出
h
e
l
l
o
界
原因是range会隐式的unicode解码
第二个方法是将str 转换为rune类型的切片,这个方法上面已经说过了,这里就不再赘述了
当然还有很多方法,其本质都是将byte向rune上靠
结论:
byte 等同于int8,常用来处理ascii字符
rune 等同于int32,常用来处理unicode或utf-8字符
以下部分参考《快学 Go 语言》第 7 课 —— 字符串
go语言中是按utf8来存储字符串的,从在线查看字符编码中可以看到“嘻哈”的utf8编码是e598bb e59388。
1.通过下标来访问内部字节数组具体位置上的字节
func main() {
var s = "嘻哈china"
for i:=0;i<len(s);i++ {
fmt.Printf("%x ", s[i])
}
}
-----------
e5 98 bb e5 93 88 63 68 69 6e 61
2.对字符串进行 range 遍历,每次迭代出两个变量 codepoint 和 runeValue。codepoint 表示字符起始位置,runeValue 表示对应的 unicode 编码(类型是 rune)。
package main
import "fmt"
func main() {
var s = "嘻哈china"
for codepoint, runeValue := range s {
fmt.Printf("%d %d ", codepoint, int32(runeValue))
}
}
-----------
0 22075 3 21704 6 99 7 104 8 105 9 110 10 97
对字符串进行 range 遍历,每次迭代出两个变量 codepoint 和 runeValue。codepoint 表示字符起始位置,runeValue 表示对应的 unicode 编码(类型是 rune)。
3.字符串是只读的
你可以使用下标来读取字符串指定位置的字节,但是你无法修改这个位置上的字节内容。如果你尝试使用下标赋值,编译器在语法上直接拒绝你。
package main
func main() {
var s = "hello"
s[0] = 'H'
}
--------
./main.go:5:7: cannot assign to s[0]
字符串的值是不能改变的,这句话其实不完整,应该说字符串的值不能被更改,但可以被替换。 还是以string的结构体来解释吧,所有的string在底层都是这样的一个结构体stringStruct{str: str_point, len: str_len}
,string结构体的str指针指向的是一个字符常量的地址, 这个地址里面的内容是不可以被改变的,因为它是只读的,但是这个指针可以指向不同的地址,我们来对比一下string、[]byte类型重新赋值的区别:
s := "A1" // 分配存储"A1"的内存空间,s结构体里的str指针指向这快内存
s = "A2" // 重新给"A2"的分配内存空间,s结构体里的str指针指向这快内存
其实[]byte和string的差别是更改变量的时候array的内容可以被更改。
s := []byte{1} // 分配存储1数组的内存空间,s结构体的array指针指向这个数组。
s = []byte{2} // 将array的内容改为2
因为string的指针指向的内容是不可以更改的,所以每更改一次字符串,就得重新分配一次内存,之前分配空间的还得由gc回收,这是导致string操作低效的根本原因。
4.字节切片和字符串的相互转换
在使用 Go 语言进行网络编程时,经常需要将来自网络的字节流转换成内存字符串,同时也需要将内存字符串转换成网络字节流。Go 语言直接内置了字节切片和字符串的相互转换语法。
package main
import "fmt"
func main() {
var s1 = "hello world"
var b = []byte(s1) // 字符串转字节切片
var s2 = string(b) // 字节切片转字符串
fmt.Println(b)
fmt.Println(s2)
}
--------
[104 101 108 108 111 32 119 111 114 108 100]
hello world
从节省内存的角度出发,你可能会认为字节切片和字符串的底层字节数组是共享的。但是事实不是这样的,底层字节数组会被拷贝。如果内容很大,那么转换操作是需要一定成本的。
那为什么需要拷贝呢?因为字节切片的底层数组内容是可以修改的,而字符串的底层字节数组是只读的,如果共享了,就会导致字符串的只读属性不再成立。
既然string就是一系列字节,而[]byte也可以表达一系列字节,那么实际运用中应当如何取舍?
- string可以直接比较,而[]byte不可以,所以[]byte不可以当map的key值。
- 因为无法修改string中的某个字符,需要粒度小到操作一个字符时,用[]byte。
- string值不可为nil,所以如果你想要通过返回nil表达额外的含义,就用[]byte。
- []byte切片这么灵活,想要用切片的特性就用[]byte。
- 需要大量字符串处理的时候用[]byte,性能好很多。
5.修改字符串
在 Go 语言中,字符串的内容是不能修改的,也就是说,你不能用 s[0] 这种方式修改字符串中的 UTF-8 编码,如果你一定要修改,那么你可以将字符串的内容复制到一个可写的缓冲区中,然后再进行修改。这样的缓冲区一般是 []byte 或 []rune。如果要对字符串中的字节进行修改,则转换为 []byte 格式,如果要对字符串中的字符进行修改,则转换为 []rune 格式,转换过程会自动复制数据。
angel := "Heros never die"
angleBytes := []byte(angel)
for i := 5; i <= 10; i++ {
angleBytes[i] = ' '
}
fmt.Println(string(angleBytes))
字符串不可变有很多好处,如天生线程安全,大家使用的都是只读对象,无须加锁;再者,方便内存共享,而不必使用写时复制(Copy On Write)等技术;字符串 hash 值也只需要制作一份。
6.关于中文
(1)使用%q打印,使用utf8包
for i, r := range "Hello, 世界" {
fmt.Printf("%d\t%q\t%d\n", i, r, r)
}
ss := "Hello, 世界"
fmt.Println(len(ss)) // "13"
fmt.Println(utf8.RuneCountInString(ss)) // "9"
(2)golang截取中文字符串
在golang中可以通过切片截取一个数组或字符串,但是当截取的字符串是中文时,可能会出现的问题是:由于中文一个字不只是由一个字节组成,所以直接通过切片可能会把一个中文字的编码截成两半,结果导致最后一个字符是乱码。
例如: 想要截取前四个字
name := "我是胡八一"
fmt.Println("name[:4] = ",name[:4])
执行后得到的结果会是这样的:
name[:4] = 我?
解决方法: 先将其转为[]rune,再截取后,转会string
nameRune := []rune(name)
fmt.Println("string(nameRune[:4]) = ",string(nameRune[:4]))
运行结果:
string(nameRune[:4]) = 我是胡八
7.string与int互转
import "strconv" //先导入strconv包
// string到int
int, err := strconv.Atoi(string)
// string到int64
int64, err := strconv.ParseInt(string, 10, 64)
// int到string
string := strconv.Itoa(int)
// int64到string
string := strconv.FormatInt(int64,10)
Go语言字符串高效拼接(一)
Go语言字符串高效拼接(二)
Go语言字符串高效拼接(三)
二、字典
《快学 Go 语言》第 6 课 —— 字典
1.make
func main() {
var m map[int]string = make(map[int]string)
fmt.Println(m, len(m))
}
----------
map[] 0
如果你可以预知字典内部键值对的数量,那么还可以给 make 函数传递一个整数值,通知运行时提前分配好相应的内存。这样可以避免字典在长大的过程中要经历的多次扩容操作。
var m = make(map[int]string, 16)
2.初始化
func main() {
var m map[int]string = map[int]string{
90: "优秀",
80: "良好",
60: "及格", // 注意这里逗号不可缺少,否则会报语法错误
}
fmt.Println(m, len(m))
}
---------------
map[90:优秀 80:良好 60:及格] 3
3.读写
func main() {
var fruits = map[string]int {
"apple": 2,
"banana": 5,
"orange": 8,
}
// 读取元素
var score = fruits["banana"]
fmt.Println(score)
// 增加或修改元素
fruits["pear"] = 3
fmt.Println(fruits)
// 删除元素
delete(fruits, "pear")
fmt.Println(fruits)
}
-----------------------
5
map[apple:2 banana:5 orange:8 pear:3]
map[orange:8 apple:2 banana:5]
删除操作时,如果对应的 key 不存在,delete 函数会静默处理。遗憾的是 delete 函数没有返回值,你无法直接得到 delete 操作是否真的删除了某个元素。这时候必须使用字典的特殊语法,如下
func main() {
var fruits = map[string]int {
"apple": 2,
"banana": 5,
"orange": 8,
}
var score, ok = fruits["durin"]
if ok {
fmt.Println(score)
} else {
fmt.Println("durin not exists")
}
fruits["durin"] = 0
score, ok = fruits["durin"]
if ok {
fmt.Println(score)
} else {
fmt.Println("durin still not exists")
}
}
-------------
durin not exists
0
4.遍历
这个和数组一样的
func main() {
var fruits = map[string]int {
"apple": 2,
"banana": 5,
"orange": 8,
}
for name, score := range fruits {
fmt.Println(name, score)
}
for name := range fruits {
fmt.Println(name)
}
}
------------
orange 8
apple 2
banana 5
apple
banana
orange
奇怪的是,Go 语言的字典没有提供诸于 keys() 和 values() 这样的方法,意味着如果你要获取 key 列表,就得自己循环一下,如下
func main() {
var fruits = map[string]int {
"apple": 2,
"banana": 5,
"orange": 8,
}
var names = make([]string, 0, len(fruits))
var scores = make([]int, 0, len(fruits))
for name, score := range fruits {
names = append(names, name)
scores = append(scores, score)
}
fmt.Println(names, scores)
}
----------
[apple banana orange] [2 5 8]
读完上面的内容,我的理解就是:如果没有设置这个随机数,那么在大多数情况下,golang会表现出map的顺序是固定的情况。但是golang底层并没有保证这一点,或许(现在/以后)会有特殊情况出现顺序不固定的情况。担心开发者们误解这一点,golang就特意去打乱了这个顺序,让开发者们知道golang底层不保证map每次遍历都是同一个顺序。
5.映射的键可以是任何值。这个值的类型可以是内置类型,也可以是结构类型。只要可以使用==运算符做比较。切片、函数以及包含切片的结构类型,由于具有引用语义,不能作为映射的键,使用这些类型会造成编译错误。(Go in Action)
6.Golang map使用注意事项
map中的元素并不是一个变量,而是一个值。因此,我们不能对map的元素进行取址操作。
var m = map[int]int {
0 : 0,
1: 1,
}
func main() {
fmt.Println(&m[0])
}
运行报错:
cannot take the address of m[0]
因此,当 map 的元素为结构体类型的值,那么无法直接修改结构体中的字段值。考察如下示例:
package main
import (
"fmt"
)
type person struct {
name string
age byte
isDead bool
}
func whoIsDead(personMap map[string]person) {
for name, _ := range personMap {
if personMap[name].age < 50 {
personMap[name].isDead = true
}
}
}
func main() {
p1 := person{name: "zzy", age: 100}
p2 := person{name: "dj", age: 99}
p3 := person{name: "px", age: 20}
personMap := map[string]person{
p1.name: p1,
p2.name: p2,
p3.name: p3,
}
whoIsDead(personMap)
for _, v :=range personMap {
if v.isDead {
fmt.Printf("%s is dead\n", v.name)
}
}
}
编译报错:
cannot assign to struct field personMap[name].isDead in map
原因是 map 元素是无法取址的,也就说可以得到 personMap[name],但是无法对其进行修改。解决办法有二,一是 map 的 value用 strct 的指针类型,二是使用临时变量,每次取出来后再设置回去。
(1)将map中的元素改为struct的指针。
package main
import (
"fmt"
)
type person struct {
name string
age byte
isDead bool
}
func whoIsDead(people map[string]*person) {
for name, _ := range people {
if people[name].age < 50 {
people[name].isDead = true
}
}
}
func main() {
p1 := &person{name: "zzy", age: 100}
p2 := &person{name: "dj", age: 99}
p3 := &person{name: "px", age: 20}
personMap := map[string]*person {
p1.name: p1,
p2.name: p2,
p3.name: p3,
}
whoIsDead(personMap)
for _, v :=range personMap {
if v.isDead {
fmt.Printf("%s is dead\n", v.name)
}
}
}
输出结果:
px is dead
(2)使用临时变量覆盖原来的元素。
package main
import (
"fmt"
)
type person struct {
name string
age byte
isDead bool
}
func whoIsDead(people map[string]person) {
for name, _ := range people {
if people[name].age < 50 {
tmp := people[name]
tmp.isDead = true
people[name] = tmp
}
}
}
func main() {
p1 := person{name: "zzy", age: 100}
p2 := person{name: "dj", age: 99}
p3 := person{name: "px", age: 20}
personMap := map[string]person {
p1.name: p1,
p2.name: p2,
p3.name: p3,
}
whoIsDead(personMap)
for _, v :=range personMap {
if v.isDead {
fmt.Printf("%s is dead\n", v.name)
}
}
}
输出结果:
px is dead
7.hash算法