【青训营】go复杂类型

298 阅读17分钟

这是我的第4篇笔记

数组和结构体聚合类型;它们的值由许多元素或成员字段的值组成。数

组是由同构的元素组成——每个数组元素都是完全相同的类型

结构体则是由异构的元素组成的。

数组和结构体都是有固定内存大小的数据结构。

相比之下,slice和map则是动态的数据结构,它们将根据需要动态增长。

range-数组切片map的迭代

//迭代 _为空标识符,不能使用,只能接收值,
//这里_用来接收索引,x为索引对应的值
for _, x := range a {
    fmt.Println(x)
}
for key, value := range a {
    fmt.Println(value)
}

make

返回的不是指针make(T,len,cap),一般只在切片,map用

cap是之后加才会有,没满只有len个

\

new

new返回一个指针,如 i:=new(int) 是返回一个int指针, 并给这个地址赋类型的零值

如果两个类型都是空的,也就是说类型的大小是0,例如struct{}和[0]int,有可能有相同的地址(依赖具体的语言实现)

(译注:请谨慎使用大小为0的类型,因为如果类型的大小为0的话,可能导致Go语言的自动垃圾回收器有不同的行为,具体请查看runtime.SetFinalizer函数相关文档)。

\

注意

new只是一个预定义函数,不是关键字,可以用来命名,但如果有这个标识符,那么new函数不能用

\

\

创建是nil的

如果只是一个声明,那么切片,map值就是nil

切片

切片是nil无所谓,因为可以用append扩容追加

qq:=[]int(nil)

var x []int

map

给值为nil的map加元素会报错,其他map操作不会

var n map[string]int

数组

声明

和其他语言一样是个固定数组,可以用**==判断数组元素和元素顺序是不是完全一样,数组的长度必须是常量表达式**,因为数组的长度需要在编译阶段确定


初始化声明或者单纯声明

[...]是根据初始化来自动计算数组长度,不赋值就给数组的元素零值

var aaa [3]int

var a = [3]int{1, 2, 3}

var b = [...]int{1, 2, 3}

cc:=[...]int{1,2,3}

指定一个索引和对应值列表的方式初始化

a := [...]int{2: 333} 指定索引2为333,其他为零值,因为没有指定长度,因此最大索引+1就是数组长度

数组参数

数组是值传递,拷贝一份给参数,而不是引用/指针传递,如果要传递可以对数组修改的数组,那么参数必须是指针数组

arr *[10]int,注意,这不是装10个指针,而是一个指针数组的指针,其用法仍是 arr[i]

更要注意的是,参数数组和要传入的数组必须同长度,同类型,如果参数是 arr []int 这是切片不是数组

因此数组很死板,参数一般用切片,不用数组


\


切片slice

切片定义

一个slice是一个轻量级的数据结构,提供了访问数组子序列(或者全部)元素的功能,而且slice的底层确实引用一个数组对象。

一个slice由三个部分构成:指针、长度和容量

指针指向第一个slice元素对应的底层数组元素的地址,要注意的是slice的第一个元素并不一定就是数组的第一个元素。

长度对应slice中元素的数目

长度不能超过容量,容量一般是从slice的开始位置到底层数据的结尾位置。内置的len和cap函数分别返回slice的长度和容量。

\

\

但是对于slice并没有指明序列的长度。这会隐式地创建一个合适大小的数组,然后slice的指针指向底层的数组。就像数组字面值一样,slice的字面值也可以按顺序指定初始化值序列,或者是通过索引和元素值指定,或者用两种风格的混合语法初始化。

slice之间不能比较,因此我们不能使用==操作符来判断两个slice是否含有全部相等元素。不过可以用bytes.Equal函数来判断两个字节型slice是否相等([]byte),但是对于其他类型的slice,我们必须自己展开每个元素进行比较

切片扩容

当数组元素==capacity后,append就可以扩容, 或者一个空数组,使用append扩容,第一次加元素为1,之后以2倍扩容,即 1--2--4--8--16--…………

因此append的用法是 xx=append(xx,12) ,因为扩容后地址就变了

append可以直接添加一个切片的元素,也可以添加n个元素

\

声明

var x []int //qq==nil,但len(qq)=0 不能加{}

以下4种是切片的隐式赋值,相当于给切片一个一个赋值

var bb = []int{1, 2, 3} aa := []int{1, 2, 3} v := a[1:3]

qq:=make([]int,3,4)

\

\

\

make

qq:=make([]int,3,4) //qq!=nil,但len(qq)=0 qq:=[]int{}也是,但qq:=[]int(nill)就qq==nil

  • make([]int,3) 这只是普通的开辟了3个空间
  • make([]int,3,10)这是开辟了10个空间,但是只能用3个,后面都要用append追加

make不能创建数组,但new可以,x := new([2]int)

make([]T, len)

make([]T, len, cap)

在底层,make创建了一个匿名的数组变量,然后返回一个slice;只有通过返回的slice才能引用底层匿名的数组变量。

在第一种语句中,slice是整个数组的view。

在第二个语句中,slice只引用了底层数组的前len个元素,但是容量将包含整个的数组。额外的元素是留给未来的增长用的。

copy(des,src)把src复制到des里

\

slice的切片操作

slice的切片操作s[i:j],其中0 ≤ i≤ j≤ cap(s) ,用于创建一个新的slice,该slice的元素为s的 [i,j)

如果省略i,默认0,如果省略j,默认len,如果都省略,则是整个s



对字符串和字节数组

x[m:n]切片操作对于字符串则生成一个新字符串,如果x是 []byte的话则生成一个新的[]byte


比较

slice唯一合法的比较操作是和nil比较,例如:

if summer == nil { /* ... */ } 注意,数组并不能与nil比较




使用注意

数组的切片不能为负数

切片只是让切片的数组第0个元素指向切片范围的数组地址

xx := []int{1, 2, 3, 4}
xxx := xx[2:3]
fmt.Println(xxx, " ", cap(xxx), " ", len(xxx))
//[3]   2   1
//切片后返回的数组cap为xx从begin开始的长度,len为切到的长度


xx := []int{1, 2, 3}
xxx := xx[0:2]
fmt.Println(xxx, " ", cap(xxx), " ", len(xxx))
xxx = append(xxx, 89)
fmt.Println(xxx, " ", cap(xxx), " ", len(xxx))
fmt.Println(xx)
xxx = append(xxx, 123)
fmt.Println(xxx, " ", cap(xxx), " ", len(xxx))
fmt.Println(xx)
/*
[1 2]   3   2
[1 2 89]   3   3
[1 2 89]
xxx的2个元素指向xx的[0,2)
xxx有2个元素时,append一个元素,因为这片连续空间还有,所以会把xx[2]的元素换为append的元素

//xxx继续append,大于cap,因此扩容,换了数组地址,所以xxx就不指向xx了
[1 2 89 123]   6   4
[1 2 89]

*/


xx[2] = 111
fmt.Println(xxx, " ", cap(xxx), " ", len(xxx))
fmt.Println(xx)
/*
[1 2 89 123]   6   4
[1 2 111]
*/

Map

定义

哈希表map是一种巧妙并且实用的数据结构。它是一个无序的key/value对的集合,其中所有的key都是不同的,然后通过给定的key可以在常数时间复杂度内检索、更新或删除对应的value。

K对应的key必须是支持==比较运算符的数据类型,所以map可以通过测试key是否相等来判断是否已经存在。虽然浮点数类型也是支持相等运算符比较的,但是将浮点数用做key类型则是一个坏的想法,正如第三章提到的,最坏的情况是可能出现的NaN和任何浮点数都不相等对于V对应的value数据类型则没有任何的限制。


map中的元素并不是一个变量,因此我们不能对map的元素进行取址操作

x += y x++ 等简短赋值语法也可以用在map上



把map当set用---map[string]bool


因为map的key必须能比较,所有可以把slice转为字符串当key

func k(list []string) string { return fmt.Sprintf("%q", list) }
func Add(list []string)       { m[k(list)]++ }
func Count(list []string) int { return m[k(list)] }

不能用==

声明

  • ages := make(map[string]int)ages := make(map[string]int,len)容量为len的map
  • a:=map[string]int{}
  • var n map[string]int这个map 为nil,给这个值为nil的map加元素会报错,其他map操作不会

ages := map[string]int{ "alice": 31, "charlie": 34, }

等同于ages := make(map[string]int) ages["alice"] = 31 ages["charlie"] = 34

\

\

操作

删除

delete(m, "214")删除m里的键值对,如果没有这个key,也是安全操作

查找

x:=m["a"]如果有"a"这个key,返回key和value,否则返回value对应的零值,和false


如果value是int类型的,不存在key将返回0,可以用别的方式判断

如果没有字符串"bob",则返回0和false

if age, ok := m["bob"]; !ok { fmt.Println(age, " ", ok) }


迭代

因为map是无序的key/value,因此每次遍历都不一样,是乱序的

并且不同的哈希函数实现可能导致不同的遍历顺序。在实践中,遍历的顺序是随机的,每一次遍历的顺序都不相同。

这是故意的,每次都使用随机的遍历顺序可以强制要求程序不会依赖具体的哈希函数实现

如果要按顺序遍历key/value对,我们必须显式地对key进行排序,可以使用sort包的Strings函数对字符串slice进行排序

\

\

结构体

和c语言差不多


一个命名为S的结构体类型将不能再包含S类型的成员:因为一个聚合的值不能包含它自身。(该限制同样适用于数组。)但是S类型的结构体可以包含*S指针类型的成员,这可以让我们创建递归的数据结构,比如链表和树结构

结构体类型的零值是每个成员都是零值


如果结构体没有任何成员的话就是空结构体,写作struct{} 。它的大小为0, 也不包含任何信息,但是有时候依然是有价值的。有些Go语言程序员用map来模拟set数据结构时,用它来代替map中布尔类型的value,只是强调key的重要性,但是因为节约的空间有限,而且语法比较复杂,所以我们通常会避免这样的用法。

seen := make(map[string]struct{}) seen["sss"] = struct{}{}



零值

结构体对象的值没有nil,如果是指针才有nil

结构体类型的零值是每个成员都是零值。通常会将零值作为最合理的默认值

如果只是声明一个结构体变量,这个结构体变量是没有值的,但还是可以调用方法,但不能调用字段


比较

如果结构体成员都是可以比较的,那么这个结构体是可比较的,可以用==

当且仅当两个对象的所有字段相等,两个对象才相等

可比较的结构体可以用于map的key


声明例子

通常,我们只是将相关的成员写到一起。

type Student struct {
	id   int
	name string
	age  int
}


func main() {
	//创建结构体变量
	var stu Student
	stu.name = "sb"
	stu.id = 250
	stu.age = 100

	//指针指向结构体成员
	var name = &stu.name
	*name = "db"
	fmt.Println(*name, " ", stu.name)

	//指向结构体变量的指针
	var sptr = &stu
	//sptr.id==>(*sptr).id
	fmt.Println(sptr.id, "  ", sptr.name, "  ", sptr.age)
	fmt.Println((*sptr).id, "  ", (*sptr).name, "  ", (*sptr).age)
}


必须指定key初始化结构体

空结构体大小为0

type S struct {
	a int
	_ struct{}
}

结构体指针切片

结构体指针切片在初始化的时候,不用取地址符,但其他情况要

type Int struct {
	val int
}
ints := []*Int{
    {val: 12},
    {val: 124},
}


函数返回结构体指针和值

//假设get返回Student类型的变量

//这样可以,但并没有改变原来的stu,只是拷贝了一份stu
s := get(stu.id)
s.age = 111111
fmt.Println(stu.age)

//这样不可以,因为函数返回的是值,并不是一个可取地址的变量
get(stu.id).age=124


//如果get返回结构体指针
//这是成立的,因为函数返回是一个可取地址的变量,修改了返回的stu的age
get(stu.id).age=124
//=====>(*get(stu.id)).age=124


结构体字面值---构造器/匿名结构体

按定义成员顺序赋值

  1. var s1=Student{1,"sb",100}

要求以结构体成员定义的顺序为每个结构体成员指定一个字面值。这种语法一般在定义结构体的包内部使用,或者是在较小的结构体中使用,这些结构体的成员排列比较规则

指定成员赋值

  1. var s2=Student{id: 1,name: "db",age: 1000}

可以只赋值部分成员\

指针

使用指针传参,效率比较高

pp := &Point{1, 2} &Point{ 1 , 2 }这可以当参数

或者

pp := new(Point) *pp = Point{1, 2}

结构体类型的导出

//这是错误的,因为结构体的成员并没有被导出,没有首字母大写
package p
type T struct{ a, b int } 

package q
import "p"
var _ = p.T{a: 1, b: 2}
var _ = p.T{1, 2}      



//这是正确的,结构体和结构体的成员都被导出
package p
type T struct{ A, B int } 

package q
import "p"
var _ = p.T{A: 1, B: 2}
var _ = p.T{1, 2}  

\

因为匿名成员也有一个隐式的名字,因此不能同时包含两个类型相同的匿名成员这会导致名字冲突。同时,因为成员的名字是由其类型隐式地决定的,所以匿名成员也有可见性的规则约束。(同样是大写才能导出)

匿名成员特性是对访问嵌套成员的点运算符提供了简短的语法糖,但匿名成员并不要求是结构体类型其实任何命名的类型都可以作为结构体的匿名成员

但是为什么要嵌入一个没有任何子成员类型的匿名成员类型呢

答案是匿名类型的方法集。简短的点运算符语法可以用于选择匿名成员嵌套的成员,也可以用于访问它们的方法。实际上,外层的结构体不仅仅是获得了匿名成员类型的所有成员,而且也获得了该类型导出的全部的方法。这个机制可以用于将一些有简单行为的对象组合成有复杂行为的对象

组合是Go语言中面向对象编程的核心

\

\

结构体嵌入和匿名成员--类似于继承

\

匿名成员

只声明一个成员对应的数据类型而不指名成员的名字

匿名成员的数据类型必须是 命名的类型指向一个命名的类型的指针 ,其名字就是该命令类型的名字

如,type Person {A,B int} type Student { Person,Score int}

或者type Student { *Person,Score int}

匿名成员不是不能访问了,仍可以通过命名的类型或者该指针来访问

type Point struct {
	X, Y int
}
type Circle struct {
	Point
	Radius int
}

type Wheel struct {
	Circle
	Spokes int
}

func main() {
	var w Wheel
	w.X = 8       // 等价于 to w.Circle.Point.X = 8
	w.Y = 8       // 等价于 to w.Circle.Point.Y = 8
	w.Radius = 5  // 等价于 to w.Circle.Radius = 5
	w.Spokes = 20 // 这个是它自己的成员
}

赋予嵌入结构体结构体字面值

下面是错误的

w = Wheel{8, 8, 5, 20} // compile error: unknown fields

w = Wheel{X: 8, Y: 8, Radius: 5, Spokes: 20} // compile error: unknown fields

下面两种都可以

w := Wheel{Circle{Point{8, 8}, 5}, 20}

w = Wheel{
    Circle: Circle{
        Point:  Point{X: 8, Y: 8},
        Radius: 5,
    },
    Spokes: 20, // NOTE: trailing comma necessary here (and at Radius)
}

fmt.Printf("%#v\n", w)

输出结构体信息

fmt.Printf("%#v\n", w)

v 是输出结构体的值,

# 是以该类型的形式输出,如 main.Wheel{Circle:main.Circle{Point:main.Point{X:8, Y:8}, Radius:5}, Spokes:20}, main.Point是类型名,

没有#,将输出,{{{8 8} 5} 20}



JSON

JSON是对JavaScript中各种类型的值——字符串、数字、布尔值和对象——Unicode本文编码

Go语言对于这些标准格式的编码和解码都有良好的支持,由标准库中的encoding/json、encoding/xml、encoding/asn1等包提供支持

Protocol Buffers的支持由 github.com/golang/protobuf 包提供

JSON(JavaScript Object Notation)的对象类型可以用于编码Go语言的map类型(key类型是字符串)和结构体,

而且JSON就是JS的子集,从JS的对象演变出来的是一种轻量级的数据交换格式

\

编组

编码Marshal,是不是指针都可以

把Go数据---->JSON数据

\

Movie数据类型和一个典型的表示电影的值列表

将一个Go语言中类似movies的结构体slice转为JSON的过程叫编组(marshaling)。编组通过调用json.Marshal函数完成

在编码时,默认使用Go语言结构体的成员名字作为JSON的对象只有导出的结构体成员才会被编码,这也就是我们为什么选择用大写字母开头的成员名称

————————另外,只要编码这个结构体,这个结构体所有字段都会被编码为JSON字符串

type Movie struct {
	Title  string
    //`json:"released"`是结构体成员Tag,是和在编译阶段关联到该成员的元信息字符串
	Year   int  `json:"released"`
	Color  bool `json:"color,omitempty"`
	Actors []string
}

var movies = []Movie{
	{Title: "Casablanca", Year: 1942, Color: false,
		Actors: []string{"Humphrey Bogart", "Ingrid Bergman"}},
	{Title: "Cool Hand Luke", Year: 1967, Color: true,
		Actors: []string{"Paul Newman"}},
	{Title: "Bullitt", Year: 1968, Color: true,
		Actors: []string{"Steve McQueen", "Jacqueline Bisset"}},
	// ...
}

func main() {
    //json.Marshal(movies)  只返回无格式化的uint切片
    
    //Indent--缩进
    //json.MarshalIndent(movies, "", "    ")    返回格式化的uint切片
    //2个参数用于表示每一行输出的前缀和每一个层级的缩进
	data, err := json.MarshalIndent(movies, "", "    ")
	if err != nil {
		log.Fatalf("JSON marshaling failed: %s", err)
	}
	fmt.Printf("%s\n", data)
}

这将输出------------就是JS的对象数组

结构体成员Tag

**----**是和在编译阶段关联到该成员的元信息字符串

如,

Year int `json:"released"`

Color bool `json:"color,omitempty"`

在编码后,Year对应的键名变成released,而Color变成color,Color 为空或零值时不生成该JSON对象的


结构体的成员Tag可以是任意的字符串面值,但是通常是一系列用空格分隔的 key:"value" 键值对序列;

因为值中含有双引号字符因此成员Tag一般用原生字符串面值的形式书写

json开头键名对应的值用于控制encoding/json包的编码和解码的行为,并且encoding/...下面其它的包也遵循这个约定

成员Tag中json对应值的第一部分用于指定JSON对象的 键名

而omitempty表示 当Go语言结构体成员为空或零值时不生成该JSON对象(这里false为零值)



解码unmarshal,要指针

\

把JSON数据---->GO数据


下面的代码将JSON格式的电影数据解码为一个结构体slice,结构体中只有Title成员---因此不用特意定义一个结构体,使用这种形式var titles []struct{ Title string }

通过定义合适的Go语言数据结构,我们可以选择性地解码JSON中感兴趣的成员。当Unmarshal函数调用返回,slice将被只含有Title信息的值填充,其它JSON成员将被忽略。


格式化JSON

//2个参数用于表示每一行输出的前缀和每一个层级的缩进

data, err := json.MarshalIndent(movies, "", " ")

var titles []struct{ Title string }
if err := json.Unmarshal(data, &titles); err != nil {
    log.Fatalf("JSON unmarshaling failed: %s", err)
}
fmt.Println(titles) // "[{Casablanca} {Cool Hand Luke} {Bullitt}]"