结构体

1,一个结构体(struct)就是一组字段(field)。 type Vertex struct {X ,Y int}

2,通过直接列出字段的值来新分配一个结构体。 使用 Name: 语法可以仅列出部分字段,其余字段使用默认值。

3,结构体字段使用点号来访问: v.X,结构体字段可以通过结构体指针来访问:可以通过 (*p).X 来访问其字段 X。或者使用隐式间接引用,直接使用 p.X,编译器自动改为(*p).X

4,type Vertex struct {}空结构体类型变量不占内存空间,内存地址相同,但是具有结构体的一切属性,如可以拥有方法,可以写入channel。建议用于在通道里传递“信号”。

5,结构体内嵌,如果一个字段的声明中只有字段的类型名而没有字段的名称,那么它就是一个嵌入字段,也可以被称为匿名字段。可以通过此类型变量的名称后跟“.”,再后跟嵌入字段类型的方式引用到该字段。嵌入字段的类型既是类型也是名称。也可以内嵌如结构体的指针,只是指针类型默认初始化为nil,值类型会初始化为字段的默认零值。

 

6,嵌入字段的方法集合和字段会被无条件地合并进被嵌入类型的方法集合和字段中。如果在当前结构体和嵌入结构体间含有同名的字段(无论类型是否相同)或者同名的方法(无论这两个方法的签名是否一致),只要名称相同,被嵌入类型的方法和字段都会被外层方法或者结构体屏蔽掉。并且即使在两个同名的成员一个是字段,另一个是方法的情况下,这种“屏蔽”现象依然会存在。但即使被屏蔽了,仍然可以通过链式的选择表达式,选择到嵌入字段的字段或方法。多层嵌入的问题:“屏蔽”现象会以嵌入的层级为依据,嵌入层级越深的字段或方法越可能被“屏蔽”。如果处于同一个层级的多个嵌入字段拥有同名的字段或方法,那么从被嵌入类型的值那里,选择此名称的时候就会引发一个编译错误。如果不调用冲突的变量就不会出错。

7,Go 语言中根本没有继承的概念,它所做的是通过嵌入字段的方式实现了类型之间的组合。继承:is关系,组合:has关系。面向对象编程中的继承,其实是通过牺牲一定的代码简洁性来换取可扩展性,而且这种可扩展性是通过侵入的方式来实现的。Go 中类型之间的组合采用的是非声明的方式,不需要显式地声明某个类型实现了某个接口,或者一个类型继承了另一个类型。类型组合也是非侵入式的,它不会破坏类型的封装或加重类型之间的耦合,只是把类型当做字段嵌入进来,然后使用嵌入字段所拥有的一切。如果嵌入字段不满足要求,还可以用“包装”或“屏蔽”的方式去调整和优化。总是可以通过嵌入字段的方式把一个类型的属性和能力“嫁接”给另一个类型。被嵌入类型也就自然而然地实现了嵌入字段所实现的接口。组合要比继承更加简洁和清晰,Go 语言可以轻而易举地通过嵌入多个字段来实现功能强大的类型,却不会有多重继承那样复杂的层次结构和可观的管理成本。

8,接口类型之间也可以组合。在 Go 语言中,常常以此来扩展接口定义的行为或者标记接口的特征。

9,Go 没有类,可以为结构体类型定义方法。 即一类带特殊的 接收者参数的函数,将函数和结构体绑定。方法只是个带接收者参数的函数。方法接收者在它自己的参数列表内,位于 func 关键字和方法名之间。

10,也可以为非结构体类型声明方法。

11,接收者的类型定义和方法声明必须在同一包内。

12,不能为内建类型声明方法,接收者类型必须是某个自定义的数据类型,而且不能是接口类型或接口的指针类型。方法隶属的类型其实并不局限于结构体类型,但必须是某个自定义的数据类型,并且不能是任何接口类型。

13,一个数据类型关联的所有方法,共同组成了该类型的方法集合。同一个方法集合中的方法不能出现重名。如果它们所属的是一个结构体类型,那么方法的名称与该类型中任何字段的名称也不能重复。

14,可以为指针接收者声明方法。 这样可以避免在每次调用方法时复制该值。若值的类型为大型结构体时,这样做会更加高效。 并且对值的修改会体现在结构体上,并且调用者只能是指针(若该值是可寻址的,则可以通过值调用, 语言就会自动插入取址操作符来对付一般的通过值调用的指针方法:p.f1() ==>> (&p).f1()), 接收者是该方法所属的那个值的指针值的一个副本,所以指针方法可以修改接收者。 如果接受者是值形式, 方法得到的是值的副本,因此任何修改都将被丢弃,除非这个类型本身是某个引用类型(比如切片或字典)的别名类型(浅拷贝)。

15,以下列表中的值都是不可寻址的。

不可寻址变量影响链式调用指针方法,可以把不可寻址变量赋值给一个变量,实现可寻址。

++--的左边添加的表达式的结果值必须是可寻址的。在赋值语句中,赋值操作符左边的表达式的结果值必须可寻址的。在range关键字左边的表达式的结果值也都必须是可寻址的。指针转换:`普通指针 <- > unsafe.Pointer <-> uintptr

16,一个自定义数据类型的方法集合中仅会包含它的所有值方法,而该类型的指针类型的方法集合却囊括了前者的所有方法,包括所有值方法和所有指针方法。一个类型的方法集合中有哪些方法与它能实现哪些接口类型是息息相关的。如果一个基本类型和它的指针类型的方法集合是不同的,那么它们具体实现的接口类型的数量就也会有差异,除非这两个数量都是零。比如,一个指针类型实现了某某接口类型,但它的基本类型却不一定能够作为该接口的实现类型。

17,Go并不对获取器(getter)和设置器(setter)提供自动支持。若有个名为 owner (小写,未导出)的字段,其获取器应当定义为 Owner(大写,可导出)而非 GetOwner,设置器定义为:SetOwner

线性结构

数组

1,类型 [n]T 表示拥有 nT 类型的值的数组。数组不需要显式的初始化,数组元素会自动初始化为其对应类型的零值,数组对应内存中n个连续的数据,Go的数组是值语义。一个数组变量表示整个数组,它不是指向第一个元素的指针, 当一个数组变量被赋值或者被传递的时候,实际上会复制整个数组。

 

2,在Go中:数组是值,将一个数组赋予另一个数组会复制其所有元素。特别地,若将某个数组传入某个函数,它将接收到该数组的一份副本而非指针。数组的大小是其类型的一部分。类型 [10]int[20]int 是不同的。为提高行为和效率,可以传递一个指向该数组的指针。

切片

1,类型 []T 表示一个元素类型为 T 的切片, 切片类型并没有给定固定的长度。每个数组的大小都是固定的,而切片则为数组元素提供动态大小的、灵活的视角。切片通过两个下标来界定,即一个上界和一个下界:

2,切片并不存储任何数据,它只是描述了底层数组中的一段。 更改切片的元素会修改其底层数组中对应的元素。 与它共享底层数组的切片都会观测到这些修改。

3,用make函数或切片值字面量初始化一个切片时,切片开头指向其底层数组中的第 1 个元素。

数组切片
[2]int{0,1}[]int{0,1}
var a[2]intvar a[]int
a:=[2]int{0,1}a:=b[0:3]

4,切片拥有 长度 和 容量(片段的最大长度)。 切片的长度就是它所包含的元素个数。 切片的容量是从它的第一个元素开始到其底层数组元素末尾的个数。 切片 s 的长度和容量可通过表达式 len(s)cap(s) 来获取。 未初始化的切片:nil 切片的长度和容量为 0 且没有底层数组。 可以通过对自身的切片扩充容量切片增长不能超出其容量,新切片的下界不能超过底层数组的下界。

5,切片的长度决定了可读取数据的上限。只要切片不超出底层数组的限制,它的长度就是可变的,只需将它赋予其自身的切片即可,切片索引以自身切片索引为准,而不是原始数组索引。 切片的容表示该切片可取得的最大长度。切片代表的窗口是无法向左扩展,只能向右扩展。

6,当两个切片同时指向同一个数组时:对一个切片append时,如果超出容量,该切片就指向新的切片,两个切片指向的数据不再相同。对短的切片append,是在切片的末尾添加,可能会影响到长切片的内容。

7,切片可包含任何类型,甚至包括其它的切片。

8,二维切片,由于切片长度是可变的,因此其内部可能拥有多个不同长度的切片

9,make 函数func make([]T, len, cap) []T会分配一个元素为对应类型的零值的数组并返回一个引用了它的切片

10,append函数func append(s []T, vs ...T) []T用于把数据添加到切片中

参数 s 是一个元素类型为 T 的切片,其余类型为 T 的值将会追加到该切片的末尾。 返回结果是一个包含原切片所有元素加上新添加元素的切片。 当 s 的底层数组太小,不足以容纳所有给定的值时,它就会分配一个更大的数组。返回的切片会指向这个新分配的数组,原数组不变。

当不用扩容时:会影响原底层数组,当要用扩容时: 会构建一个全新数组,把原来的数据和要添加的数据存入,并返回,不会影响原底层数组,当原切片的长度小于1024时:新切片的容量将会是原切片容量的 2 倍。当原切片的长度大于或等于1024时:新切片的容量将会是原切片容量的 1.25倍。

11,copy函数func copy(dst, src []T) int从开头开始将源切片的元素复制到目的切片,返回复制元素的数目。 copy 函数支持不同长度的切片之间的复制,它只复制较短切片的长度个元素。 copy 函数可以正确处理源和目的切片有重叠的情况。

12,切片缩容:如果使用对当前切片再次切片的方式,索引按照被切片的切片的索引,而不是原始数组。缩容之后还是会引用底层的原数组,这有时候会造成大量缩容之后的多余内容没有被垃圾回收。应该使用新建一个数组然后copy的方式。

列表

1,List 实现了一个双向环形链表,包含列表的头节点与列表的长度。Element 则代表了链表中元素的结构,包含当前节点的前后节点、节点的值、与节点所属的列表。

2,用于插入新元素的那些方法都只接受interface{}类型的值。这些方法在内部会常见方法使用Element值,包装接收到的新元素。这样做正是为了避免直接使用我们自己生成的元素,主要原因是避免链表的内部关联,遭到外界破坏。List在内部就是一个循环链表。根元素永远不会持有任何实际的元素值,而该元素的存在就是为了连接这个循环链表的首尾两端。

3,常见方法

4,初始化

var l list.List声明的变量l是一个长度为0、根元素是一个空壳的链表,用字面量表示的话就是Element{}l是一个只包含了根元素的列表,可以开箱即用。延迟初始化:把初始化操作延后,仅在实际需要的时候才进行。可以分散初始化操作带来的计算量和存储空间消耗。

5,切片与列表,切片本身有着占用内存少和创建便捷等特点,但它的本质上还是数组。切片的一大好处是可以让我们通过窗口快速地定位并获取,或者修改底层数组中的元素。当想删除切片中的元素的时候,元素复制一般是免不了的(数组内存连续),就算只删除一个元素,有时也会造成大量元素的移动。这时还要注意空出的元素槽位的“清空”,否则很可能会造成内存泄漏。在切片被频繁“扩容”的情况下,新的底层数组会不断产生,这时内存分配的量以及元素复制的次数可能就很可观了,这肯定会对程序的性能产生负面的影响。当我们没有一个合理、有效的”缩容“策略的时候,旧的底层数组无法被回收,新的底层数组中也会有大量无用的元素槽位。过度的内存浪费不但会降低程序的性能,还可能会使内存溢出并导致程序崩溃。一个链表所占用的内存空间,往往要比包含相同元素的数组所占内存大得多。这是由于链表的元素并不是连续存储的,所以相邻的元素之间需要互相保存对方的指针,每个元素还要存有它所属链表的指针。所以链表只持有头部元素(或称为根元素)基本上就可以了。

1,Ring`类型实现的是一个循环链表

2,listring区别:Ring类型的数据结构仅由它自身即可代表,而List类型则需要由它以及Element类型联合表示。一个Ring类型的值严格来讲,只代表了其所属的循环链表中的一个元素,而一个List类型的值则代表了一个完整的链表。在创建并初始化一个Ring值的时候,可以指定它包含的元素的数量,循环链表一旦被创建,其长度是不可变的。对于一个List值来说却不能这样做(也没有必要这样做)。仅通过var r ring.Ring语句声明的r将会是一个长度为1的循环链表,而List类型的零值则是一个长度为0的链表(根元素不会持有实际元素值,只持有一个root节点),Ring值的Len方法的算法复杂度是 O(N) 的(挨个统计),List值的Len方法的算法复杂度则是 O(1) 的。

1,堆分为两种:最大堆最小堆:在最大堆中,父节点的值比每一个子节点的值都要大。在最小堆中,父节点的值比每一个子节点的值都要小。堆的根节点中存放的是最大或者最小元素,但是其他节点的排序顺序是未知的(与平衡二叉树不用)。在当前层级所有的节点都已经填满之前不允许开是下一层的填充(满二叉树,可以使用数组存储)

2,堆可以使用数组存储,i 是节点的索引,它的父节点和子节点在数组中的位置:

3,如果一个堆有 n 个节点,那么它的高度是 h=log2(n+1)

4,堆的初始化

5,Go内使用最大堆还是最小堆由Less的实现决定。堆内元素必须自己实现接口定义的方法:

映射

1,映射将键映射到值。 其键可以是任何相等性操作符支持的类型, 如整数、浮点数、复数、字符串、指针、接口(只要其动态类型支持相等性判断)、结构以及数组。 不可以是函数类型、字典类型和切片类型,因为它们的相等性还未定义。如果键的类型是接口类型的,那么键值的实际类型也不能是上述三种类型,map[interface{}]int{[]int{2}: 2} ❌。最好不要把字典的键类型设定为任何接口类型

2,对于数组或者结构体这种复合结构,会递归的使用它们成员或字段的hash值合并后作为该结构的hash值。如果键的类型是数组类型,那么还要确保该类型的递归元素类型不是函数类型、字典类型或切片类型。如果键的类型是结构体类型,那么还要保证其中递归字段的映射类型的合法性。

3,先把键值作为参数传给哈希表,哈希表会先用哈希函数(hash function)把键值转换为哈希值。哈希值通常是一个无符号的整数。一个哈希表会持有一定数量的桶(bucket),也可称之为哈希桶,这些哈希桶会均匀地储存其所属哈希表收纳的那些键 - 内部结构对。桶内部结构的结构是“键1、元素1;键2、元素2;...”,是一块连续的内存。哈希表会先用这个键的哈希值的低几位去定位到一个哈希桶,然后再去这个哈希桶中,查找这个键。

4,每个哈希桶都会把自己包含的所有键的哈希值存起来。Go 语言会用被查找键的哈希值与这些哈希值逐个对比,看看是否有相等的。如果一个相等的都没有,那么就说明这个桶中没有要查找的键值。如果有相等的,那就再用键值本身去对比一次(hash运算本质为一种压缩,可能存在hash值相同但原始值不同)。

5,字典不会独立存储任何键的值,但会独立存储它们的哈希值。

6,宽度越小的类型求哈希速度通常越快。优先选用数值类型和指针类型作为键值,通常情况下类型的宽度越小越好。如果非要选择字符串类型的话,最好对键值的长度进行额外的约束。

7,映射也是引用类型。 若将映射传入函数中,并更改了该映射的内容,则此修改对调用者同样可见。

8,由于字典是引用类型,所以当我们仅声明而不初始化一个字典类型的变量的时候,它的值会是nil,除了添加键 - 元素对,我们在一个值为nil的字典上做任何操作都会引起错误。

9,在同一时间段内但在不同的 goroutine(或者说 go 程)中对同一个值进行操作是是不安全的。map并发读写需要加锁,可以使用go run race命令做数据的竞争检测。或者使用并发安全字典: sync.Map

10,make 函数会返回给定类型的映射,并将其初始化备用。

string

1,Go语言中字符串以字节为单位,存储使用UTF-8编码表示Unicode文本,一个字符可能对应多个字节(下标)。Go语言字符串是变宽字符序列。只有在字符串只包含7位的ASCII字符(因为它们都是用一个单一的UTF-8字节表示)时才可以被字节索引(汉语一个汉字对应对多个下标)。

2,for遍历的是以字符为单位,获得字符对应的单个或多个字节的起始下边和字符的编码。下标遍历、切片是以Unicode编码的字节为单位。

3,双引号用来创建可解析的字符串,支持转义,但不能用来引用多行;反引号用来创建原生的字符串字面量,可能由多行组成但不支持转义,可以包含除了反引号外其他所有字符。

4,[]int32(str):转为编码,一个字符对应一个编码。[] byte(str):字符串会被拆分成零散、独立的字节。遍历字符串:字节编码 -> 字符编码 ->字符

5,string对象不可变,更改字符串实际是重新定义一个字符串,存储新的值,再将指针指向新的字符串。

接口

1,接口类型 是由一组方法签名定义的集合。 接口类型的变量可以保存任何实现了这些方法的结构体的值(区分指针与值接收者)。

2,若某种现有的类型仅实现了一个接口,且除此之外并无可导出的方法,则该类型本身就无需导出。 仅导出该接口能让我们更专注于其行为而非实现(面向抽象编程,持有上层接口,忽略底层实现差异),其它属性不同的实现则能镜像该原始类型的行为。

3,判断某个类型是否是实现了某个接口:

4,接口区分接受者是值类型还是指针类型

5,接口内嵌,只有接口能被嵌入到接口中。

ReadWriter 能够做任何 Reader`Writer 可以做到的事情,它是内嵌接口的联合体 (它们必须是不相交的方法集,无命名冲突)。只要组合的接口之间有同名的方法就会产生冲突,从而无法通过编译,即使同名方法的签名彼此不同也会是如此。

6,接口也是值,它们可以像其它值一样传递。 接口值可以用作函数的参数或返回值。

7,在内部,接口值可以看做包含值和具体类型的元组: (value, type),接口值保存了一个具体底层类型的具体值。 接口值调用方法时会执行其底层类型的同名方法(面向抽象编程)。

8,赋给接口类型变量的值可以叫做它的实际值(也称动态值),而该值的类型可以被叫做这个变量的实际类型(也称动态类型)。对于变量来讲,它的静态类型就是接口类型。

9,即便接口内的具体值为 nil,方法仍然会被 nil 接收者调用。保存了 nil 具体值的接口其自身并不为 nil。 但是通过方法访问变量的属性,则会返回该类型的默认零值

10,nil 接口值既不保存值也不保存具体类型。 为 nil 接口调用方法会产生运行时错误,因为接口的元组内并未包含能够指明该调用哪个 具体 方法的类型。

11,指定了零个方法的接口值被称为 空接口,任何类型都是它的实现类型,空接口可保存任何类型的值。(因为每个类型都至少实现了零个方法。) 空接口被用来处理未知类型的值,也能用作函数形参类型,由于他没有任何方法,可以防止在方法内调用参数的方法,破坏对象封装。

12,只包含一个方法的接口应当以该方法的名称加上-er后缀来命名,如 Reader

13,Go 程序使用 error 值来表示错误状态。 error 类型是一个内建接口:

14,Stringer 是一个可以用字符串描述自己的类型。

15,当我们给一个接口变量赋值的时候,该变量的动态类型会与它的动态值的副本一起被存储在一个专用的数据结构中。实例会包含两个指针,一个是指向类型信息的指针,另一个是指向动态值副本的指针(区分实现接口的是值类型,还是指针类型)。一个接口变量的值其实是这个专用数据结构的一个实例,而不是我们赋给该变量的那个实际的值,因为接口方法的接收者为值类型,方法内部接收到的是接口提的浅拷贝,通过接口变量对结构体的基本数据类型修改,不会体现在原本结构体的值。如果是指针类型,复制的是指向结构体的指针的值(如果指针指向的结构体发生改变,接口数据结构中指针仍旧指向旧的结构体),方法的接收者为指针类型,方法内部对结构体的操作能体现在外部结构体。

 

16,我们把一个有类型的nil赋给接口变量,那么这个变量的值就一定不会是那个真正的nil。此时接口变量就已经有了该nil值具体的类型作为类型信息,并对该nil值依据类型值包装作为动态值。除非我们只声明而不初始化,或者显式地赋给它nil,否则接口变量的值就不会为nil

类型

1,类型断言 提供了访问接口值底层具体值的方式。

i要明确声明为接口

2,类型选择 是一种按顺序从几个类型断言中选择分支的结构,来判断某个 interface 变量中实际存储的变量类型。