【轻知识】Go入门学习整理——第二节

准备工作:你要科学上网。不然一些东西下载不下来。Go的官网可能无法访问。
我英语不好。但尽量在官方文档找知识源。然后看看翻译。不看翻译阅读吃力。部分中文文档studygolang.com上有。
声明:知识点非原创,大都是我组合各个书,文章。所以雷同是正常滴。毕竟是整理。
文章中你看到很多打印方法没有加fmt。那是因为import(. "fmt")

数组

数组是一个由固定长度的特定类型元素组成的序列,一个数组可以由零个或多个元素组成。数组的长度是数组类型的组成部分。因为数组的长度是数组类型的一个部分,不同长度或不同类型的数据组成的数组都是不同的类型,因此在Go语言中很少直接使用数组(不同长度的数组因为类型不同无法直接赋值,所以在 Go 语言的代码里并不是特别常见。相对的,切片确实随处可见的。)。和数组对应的类型是切片,切片是可以动态增长和收缩的序列,切片的功能也更加灵活,但是要理解切片的工作原理还是要先理解数组。

点进去过一遍,https://chai2010.cn/advanced-go-programming-book/ch1-basic/ch1-03-array-string-and-slice.html

需要记住的知识点是:

1.长度也是数组的一个部分,这就是不灵活的地方(意味着:var a [3]int 跟 var b[4]int不能互相赋值,但是可以跟b[3]int互相赋值)。

2.数组是值类型,当传输传递是值传递。在函数内部是拷贝使用,而向c语言变量就当成指针使用。当然传整个数组会带来复制的开销。你可以传递指向数组的指针( &a ),但是数组指针并不是数组。

3.数组的长度一旦声明是不变的。它的len跟cap也就是长度跟容量是一致的。

字符串

字符串定义:string 是所有8位字节的字符串集合(bytes),习惯上用于代表以UTF-8编码的文本,但并不必须如此(由于Go语言的源代码要求是UTF8编码,导致Go源代码中出现的字符串面值常量一般也是UTF8编码的)。 string 可为空,但不为 nil。string 类型的值是不变的。Go语言字符串底层数据也是对应的字节数组,但是字符串的只读属性禁止了在程序中对底层字节数组的元素的修改。

Go语言字符串的底层结构在reflect.StringHeader中定义:

type StringHeader struct {
    Data uintptr
    Len  int
}

字符串结构两部分组成:一个是指向底层字节组数的指针,第二个是字节长度。字符串是一个结构体,因此字符串赋值操作就是reflect.StringHeader结构体的复制过程,并不会涉及底层字节数组的复制。可以看看字符串"hello world"本身对应的内存结构:

550939-922ccdf92719e0c0.png
图片来自于《Go语言高级编程》

与下面数组完全一致

var data = [...]byte{
    'h', 'e', 'l', 'l', 'o', ',', ' ', 'w', 'o', 'r', 'l', 'd',
}

字符串虽然不是切片,但是支持切片操作,

s := "hello, world"
hello := s[:5]
world := s[7:]

s1 := "hello, world"[:5]
s2 := "hello, world"[7:]

字符串的一些操作相关的两个包stringsstrconv 。可以看官方文档。也可以看《the way to go》中文版中strings和strconv包这一小节的部分示例。字符串的操作方法熟练一些常用的吧。想想你php常用的哪些。找到对应的go中的方法。

下面的代码容易理解,字符串跟底层数组的关系。

str1 := "abc"
str2 := str1
str3 := "abc"
str4 := str1[1:3]
str5 := str3[1:3]
fmt.Printf("%#v\n", str1) // "abc"
fmt.Printf("%#v\n", str4) // "bc"
fmt.Printf("0x%x\n", (*reflect.StringHeader)(unsafe.Pointer(&str1)).Data) // 0x4dca9c
fmt.Printf("0x%x\n", (*reflect.StringHeader)(unsafe.Pointer(&str2)).Data) // 0x4dca9c
fmt.Printf("0x%x\n", (*reflect.StringHeader)(unsafe.Pointer(&str3)).Data) // 0x4dca9c
fmt.Printf("0x%x\n", (*reflect.StringHeader)(unsafe.Pointer(&str4)).Data) // 0x4dca9d
fmt.Printf("0x%x\n", (*reflect.StringHeader)(unsafe.Pointer(&str5)).Data) // 0x4dca9d

有一个有意思的点就是str1跟str2,str3地址相同。是不是可以理解str2 := str1,只是做了类似于指针的拷贝(其实就是复制reflect.StringHeader)所以指向相同的数据。

这里有个未解的点。当我看到str1跟str2,str3地址相同的时候。我想起了c语言的字符串。看看下面代码:

#include <stdio.h>  
int main() 
{ 
    char *b; 
    char *c; 
    char a[]="hello world"; 
    b="hello world"; 
    c="hello world"; 
    printf("%d,%d,%d,%d\n",b,a,c,&("hello world")); 
}
[root@bogon c]# ./st
4195856,963490416,4195856,4195856

c语言中,字符串的常量放在静态区。地址是一样的,但是go的字符串是分配在堆区的。目前没找到类似的文档说明(之后我再找找)。

ok,再回过头来看一个问题,字符串值不可变。我们看下下面的代码。

name := "jobs"
Println(name[1]); // o 的accii码,当然也是utf8的码点是111
Printf("%c\n", name[1])
name[2] = 111 // 把第索引为2的字符也改成o,变成joos,这样是不行的。字符串不能修改的。

那你说我把name重新赋值个值吧。比如下面。但是你会发下底层的slice地址变了。

Printf("0x%x\n", (*reflect.StringHeader)(unsafe.Pointer(&name)).Data) // 0x4dcbc6
name = "joos"
Printf("0x%x\n", (*reflect.StringHeader)(unsafe.Pointer(&name)).Data) // 0x4dcbca

需要记住的知识点:

  • 字符串底层结构是什么。
  • 字符串是不可变的
  • 字符串相关的类型转换,例如:[]byte与字符串互转,与[]rune互转。还有strconv包里面的转换方法。
  • strings包常用的方法
  • utf8码点的概念。
  • 字符串的性能

slice

点开简单过一下 https://chai2010.cn/advanced-go-programming-book/ch1-basic/ch1-03-array-string-and-slice.html

底层结构reflect.SliceHeader

type SliceHeader struct {
  Data uintptr
  Len int
  Cap int
}

切片是动态的数组,长度不固定。那切片的长度不是类型的一部分。不像数组长度是类型的一部分。意味着可以下面这样复制。还有一个不同点数组的长度跟容量是相同的。而切片容量必须大于或等于切片的长度。

slice := []int{1,2,3}
slice1 := []int{1,2,3,4}
slice = slice1
Println(slice)
Printf("0x%x\n", (*reflect.SliceHeader)(unsafe.Pointer(&slice)).Data) // 0xc00006c060
Printf("0x%x\n", (*reflect.StringHeader)(unsafe.Pointer(&slice1)).Data) // 0xc00006c060

怎么更好的理解与组的关系呢?我们其实可以把切片看做是对数组的一层简单的封装,因为在每个切片的底层数据结构中,一定会包含一个数组。数组可以被叫做切片的底层数组,而切片也可以被看作是对数组的某个连续片段的引用。切片属于引用类型。数组属于值类型。

下面的代码容易理解,slice 跟数组的关系。 比如:b跟c基于a创建。其实利用的是一份数据。打印地址就清楚了。为什么c的地址是48结尾?因为他只有两个元素。在40的基础上加一个元素的长度8。因为int占了8个字节(一个地址一个字节,int在64位机器是8字节)。

a := [3]int{2, 3, 4}
b := a[0:3]
c := a[1:3]
fmt.Printf("%#v\n", b) // []int{2, 3, 4}
fmt.Printf("%#v\n", c) // []int{3, 4}
fmt.Println((unsafe.Pointer(&a)))// 0xc00000e440
fmt.Printf("0x%x\n", (*reflect.SliceHeader)(unsafe.Pointer(&b)).Data) // 0xc00000e440
fmt.Printf("0x%x\n", (*reflect.SliceHeader)(unsafe.Pointer(&c)).Data) // 0xc00000e448

再根据下面的代码说下,长度,容量,以及容量如何扩张的

slice := []int{1, 2, 3, 4, 5, 6, 7}
Println(len(slice))                                                   // 7
Println(cap(slice))                                                   // 7
Printf("0x%x\n", (*reflect.SliceHeader)(unsafe.Pointer(&slice)).Data) // 0xc00007e040
slice1 := slice[:2]
Println(slice1)                                                        // [1 2]
Println(len(slice1))                                                   // 2
Println(cap(slice1))                                                   // 7
Printf("0x%x\n", (*reflect.SliceHeader)(unsafe.Pointer(&slice1)).Data) // 0xc00007e040
slice2 := slice[:2:2]
Println(slice2)                                                        // [1 2]
Println(len(slice2))                                                   // 2
Println(cap(slice2))                                                   // 2
Printf("0x%x\n", (*reflect.SliceHeader)(unsafe.Pointer(&slice2)).Data) // 0xc00007e040
slice2 = append(slice2, 3)                                             // [1 2 3]
Println(slice2)                                                        // [1 2 3]
Println(len(slice2))                                                   // 3
Println(cap(slice2))                                                   // 4
Printf("0x%x\n", (*reflect.SliceHeader)(unsafe.Pointer(&slice2)).Data) // 0xc00004e0c0
slice3 := slice[:3:3]
Println(slice3)                                                        // [1 2 3]
Println(len(slice3))                                                   // 3
Println(cap(slice3))                                                   // 3
Printf("0x%x\n", (*reflect.SliceHeader)(unsafe.Pointer(&slice3)).Data) // 0xc00007e040
slice3 = append(slice3, 4)                                             // [1 2 3 4]
Println(slice3)                                                        // [1 2 3 4]
Println(len(slice3))                                                   // 4
Println(cap(slice3))                                                   // 6
Printf("0x%x\n", (*reflect.SliceHeader)(unsafe.Pointer(&slice3)).Data) // 0xc000076090

我们看到用append 扩容之后,指针有变化。为什么变了。因为容量不够用了。append之后返回了新的切片。但是如果容量够用的话。那地址是不变的,也就是不会产生新切片,还是原切片。
一旦一个切片无法容纳更多的元素,Go 语言就会想办法扩容。但它并不会改变原来的切片,而是会生成一个容量更大的切片,然后将把原有的元素和新元素一并拷贝到新切片中。在一般的情况下,你可以简单地认为新切片的容量(以下简称新容量)将会是原切片容量(以下简称原容量)的 2 倍。

需要记住的知识点是:

  • 切片的定义
  • slice 跟数组的关系是什么?
  • 切片的容量是介于底层数组长度跟切片长度之间的。内置的len函数返回切片中有效元素的长度,内置的cap函数返回切片容量大小,容量必须大于或等于切片的长度。
  • slice使用append函数发生了什么变化?
  • 它的扩容规则是什么?

map

我们简单过下https://go.fdos.me/08.1.html

然后看下郝林专栏里讲的map的知识点。

Go语言的字典类型其实是一个哈希表(hash table)的特定实现,键和元素的最大不同在于,键的类型是受限的,而元素却可以是任意类型的。

你可以把键理解为元素的一个索引,我们可以在哈希表中通过键查找与它成对的那个元素。

需要记住的知识点是:

  • map底层实现是什么?

  • 什么类型的不能作为map的键?还有什么类型最适合做map的键?

参考资料:

猜你喜欢

转载自blog.csdn.net/weixin_33754065/article/details/86922674