目录
1,简单数据类型
数据类型 | 关键字 | 占用字节 | 初始默认值 | 范围 | 说明 |
---|---|---|---|---|---|
布尔型 | bool | 1字节 | false | false,true | 不能用0,1表示 |
整数型 | int8 | 1字节 | 0 | -128~127 | |
uint8(byte) | 1字节 | 0 | 0~255 | byte是uint8的别称 | |
int16 | 2字节 | 0 | -32768~32767 | ||
uint16 | 2字节 | 0 | 0~65535 | ||
int32(rune) | 4字节 | 0 | -2147483648~2147483647 | rune是int32的别称,rune一般作为字符型,unicode字符集字符 | |
uint32 | 4字节 | 0 | 0~4294967295 | ||
int64 | 8字节 | 0 | 0~18446744073709551615 | ||
uint64 | 8字节 | 0 | -9223372036854775808~ 9223372036854775807 | ||
浮点型 | float32 | 4字节 | 0.0 | -3.403E38~3.403E38 | |
float64 | 8字节 | 0.0 | -1.798E308~1.798E308 | ||
复数型 | complex64 | 8字节 | real()用于取复数的实部,imag()用于取复数的虚部 | ||
complex128 | 16字节 | ||||
字符串型 | string | 空字符串 | 使用双引号("")或反引号(``)定义,内部是utf-8编码 | ||
int,uint和uintptr这三个与平台相关的整形类型,长度不是固 |
2,指针(pointer)
在 Go 中存在着三种指针:类型安全指针,unsafe.Pointer(任意类型的变量),uintptr(存指针地址的变量,可以参与计算)。
//类型安全指针
var i int = 32 //定义一个整数 也可以简写 i := 32
var p1 *int = &i //定义一个int型指针,指向变量i的地址,也可以简写 p1 := &i
var p2 *int = new(int) //定义一个int指针,也可以简写 p2 := new(int)
*p1 = 99 //赋值
*p1 = i //赋值
fmt.Println(*p1) //这个是取指针里的值
fmt.Println(p1) //这个是取指针的地址
//unsafe.Pointer 指向任意类型的指针,一般配合uintptr一起使用
//uintptr 是一个整数类型,能够存放任意指针,可以参与计算
var a int= int(1)
var b *int64= (*int64)(unsafe.Pointer(&a)) //将*int先转为Pointer,在转为*int64
c := uintptr(unsafe.Pointer(&a)) //将*int先转为Pointer,在转为uintptr
/*
uintptr(unsafe.Pointer(&u)) 先取出u(定义的变量)的地址,变成Pointer,然后再转化为uintptr
转化为uintptr后才能进行地址的计算,
unsafe.Offsetof(age) 这个是取age(定义的变量)类型的长度,然后地址加上这个长度
在把uintptr地址转化为Pointer,然后再转化为*int地址
*/
var ii *int = (*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&u))+unsafe.Offsetof(age)))
3,数组(array)
Go语言数组是具有相同类型的一组已经编号且长度固定的数据项序列,数组元素可以通过索引(位置)来读取(或者修改),索引从 0 开始,第一个元素索引为 0,第二个索引为 1。数组长度定义的时候确定,后面就不能修改。
//定义数组的第一种方式,var <数组名称> [<数组长度>]<数组元素>
var ar1 [2]int //这种没有初始化,也可以var ar1 [2]int = [2]int{3, 4}
//第二种不确定长度
var ar3 = [...]int{23, 3, 4, 5, 6} //根据后面的初始化值数量确定数组长度
//第三种定义索引的数值,根据最后一个索引值确定数组长度
var ar4 = [...]int{0:-1,3:6} //最后的索引值是3,所以数组长度是4
len(ar4) //获取数组的长度
4,切片(slice),动态数组
Go语言中的切片(Slice)是一种存储一系列相同类型的元素。切片可以看作是对底层数组的一个引用,它提供了动态大小、自动扩容的能力。
4.1,切片的基本用法
/*切片的定义和数组很相似,但是有不同,切片中的[]中是空的,数组[]里是长度或者...
*/
var ar1 []int //定义切片
ar2 := []int{1, 2, 3} //定义切片,并初始化
var ar3 = make([]int, 5, 5) //定义切片 var 切片变量 = make([]类型,长度, 容量)
var ar3 = make([]int, 5) //定义切片 var 切片变量 = make([]类型,长度) 容量默认和长度相同
var ar4 = []int{1:1, 2:6, 5:10} //定义,初始化切片 索引为1的元素为1,索引2为6, 索引5为10
len(ar2) //获取切片的长度,这个表示放入了多少个元素
cap(slar2) /*获取切片的容量,这个表示申请了多少个元素的位置,扩容的时候这个值会改变
开始初始化4个元素,长度,容量都是4,再入一个元素,由于放不下了,就会扩容(2倍的扩),
容量变成了8,长度是5*/
nar := append(ar1, 1, 3) //尾部添加2个元素,返回一个新的切片
nar := append(ar1, ar4...) //切片ar1后加入ar4切片的全部数据,返回一个新的切片
copy(ar1, ar2) //复制切片,把ar1的数据全部拷贝到ar2
ar1 = ar1[1:] //删除切片的第一个元素
ar1 = ar1[:len(ar1) - 1] //删除切片的最后一个元素
//切片的底层结构
type SliceHeader struct {
Data uintptr //一个数组的指针,这个数值的长度是Cap
Len int //长度,放入元素的个数
Cap int //容量
}
4.2,切片的扩容过程
5,字典(map)
存储的都是键值对,不能保证顺序性,不能插入键相同的元素。
5.1,map的基本用法
m := map[int]int{1: 11,2: 12,3: 13}//定义并初始化map
m2 := map[int]string{} //m2, m3的创建方法是等价的
m3 := make(map[int]string)
m4 := make(map[int]string, 10) //m := make(map[keyT]valueT, cap),第2个参数指定容量
m4[1] = "aa" //添加键值对
m4[1] = "bb" //由于key为1的键值对已经存在,这个值bb会覆盖原来的aa
val, ok := m4["c1"] //通过key获取值,如果ok=true,说明有值,ok=false说明没有值
delete(m4, 1) //删除m中,key是1的元素
5.2,map的底层结构
type hmap struct {
count int // map中元素的个数,len()返回此值
flags uint8 // 状态标识符,key和value是否包指针、是否正在扩容、是否已经被迭代
B uint8 // map中桶数组的数量,桶数组的长度的对数,len(buckets) == 2^B,可以最多容纳 6.5 * 2 ^ B 个元素,6.5为装载因子
noverflow uint16 // 溢出桶的大概数量,当B小于16时是准确值,大于等于16时是大概的值
hash0 uint32 // 哈希种子,用于计算哈希值,为哈希函数的结果引入一定的随机性
buckets unsafe.Pointer // 指向桶数组的指针,长度为 2^B ,如果元素个数为0,就为 nil
oldbuckets unsafe.Pointer // 指向一个旧桶数组,用于扩容,它的长度是当前桶数组的一半
nevacuate uintptr // 搬迁进度,小于此地址的桶数组迁移完成
extra *mapextra // 可选字段,用于gc,指向所有的溢出桶,避免gc时扫描整个map,仅扫描所有溢出桶就足够了
}
// 溢出桶结构
type mapextra struct {
overflow *[]*bmap // 指针数组,指向所有溢出桶
oldoverflow *[]*bmap // 指针数组,发生扩容时,指向所有旧的溢出桶
nextOverflow *bmap // 指向所有溢出桶中下一个可以使用的溢出桶
}
//这个结构体更好理解
type bmap struct {
topbits [8]uint8 // 存放key哈希值的高8位,用于决定kv键值对放在桶内的哪个位置
keys [8]keytype // 存放key的数组
values [8]valuetype // 存放value的数组
pad uintptr // 用于对齐内存
overflow uintptr // 指向下一个桶,即溢出桶,拉链法
}
5.3,map的扩容
扩容触发条件:1,元素个数>桶个数*6.5(每个桶里可以存放8个键值对),这种情况就是元素太多 了,hash冲突的概率增大,通过增量扩容减少冲突概率。
2,溢出桶数量太多,当桶总数<2^15时,溢出桶总数>=桶总数,则认为溢出桶过 多;当桶总数>=2^15时,溢出桶总数>=2^15时,则认为溢出桶太多,这种情况 就是元素太过集中到某些hash,导致溢出桶多,通过等量扩容,让元素分散。
增量扩容:适用于第一种情况的扩容,把桶的数量增加一倍,然后把数据重新搬迁到新的桶中。
等量扩容:并不扩大桶数量,通过重新计算键值,把元素重新排列一次,让元素分散。
6,通道(channel)
channel是golang在goroutine之间的通讯方式,channel是引用类型,使用的时候必须通过make进行初始化;线程安全,多个goroutine
访问时,不需要加锁,也就是channel
本身就是线程安全的。
6.1,channel的创建,接收和发送
创建形式:make(chan 元素类型, 容量) 或者 make(chan 元素类型) ,不写容量默认是0
接收:re := <-ch 读取ch的数据赋给re,如果通道关闭了,还能接收通道里的值,直到通道返回对应类型的零值,如果通道没有值,会返回对应的零值。
发送: ch <- 10 向ch中写入10,如果通道关闭了,在发送会导致panic
关闭:close(ch) 关闭ch,如果关闭一个已经关闭的通道会导致panic
ch := make(chan int, 3) //申明channel,make(chan 元素类型, 容量),容量可以是0
ch <- 10 //把10传递给ch
x := <-ch //从ch中接收值并赋值给变量x
v, ok := <-ch //如果ok=false,通道被关闭或者通道无值
close(ch) //关闭管道
ch1 := make(chan int, 1) //定义一个channel
ch2 := make(chan int , 1) //定义另一个channel
select { //select可以监控多个channel,哪个有数据就执行相应的逻辑部分
case <-ch1:
fmt.Println("ch1")
case <-ch2:
fmt.Println("ch2")
}
6.2,判断channel是否关闭
channel关闭了,任然可以接收值
v, ok := <-ch
如果ch关闭了,ch里面有内容,v接收的内容,ok=true
如果ch关闭了,ch里面没有内容,v接收的是对应类型的零值,ok=false
如果ch没有关闭,ch里面有内容,v接收的内容,ok=true
如果ch没有关闭,ch里面没有内容,会报错死锁
6.3,无缓存区的channel(阻塞)
//无缓冲区channel
ch := make(chan int) //或者ch := make(chan int, 0)
使用无缓存区的channel,完全是一种同步化的操作,只有同时有发送者和接收者的时候,才能正常,否则会报错,因为一直处于阻塞状态,会形成死锁。
//会报错,只有发送者,没有接收者
ch1 := make(chan int)
ch1 <- 10
//会报错,只有接收者,无发送者
ch2 := make(chan int)
<- ch2
//不会报错,有接收者,有发送者
ch3 := make(chan int)
go func(){
re := <-ch3
fmt.Println(re)
}()
ch3 <- 10
6.4,有缓冲区的channel(非阻塞)
//有缓冲区channel
ch := make(chan int, 2) //后面不一定是2,可以是大于0的其他整数
如果缓冲区满了,还发送值,会报错死锁;如果缓存区没有内容了,还接收,也会报错死锁
//发送
ch3 := make(chan int, 1) //有缓冲区,缓冲区大小是1
ch3 <- 10 //这个发送正常,因为缓冲区有1单位
ch3 <- 10 //这个会报错,缓存区满了,发送会阻塞,产生死锁报错
//接收
ch4 := make(chan int, 1) //有缓冲区,缓冲区大小是1
ch4 <- 10 //这个发送正常,因为缓冲区有1单位
<-ch4 //这个接收正常,缓冲区有内容
<-ch4 //这个会报错,因为缓冲区已经没有内容了,会阻塞,产生死锁报错
如果不想报错,可以使用cap,len先获取下元素树,比较后在决定。cap(ch4)获取的是channel的容量,len(ch4)获取的是元素个数,如果len返回的大于0,接收是没有问题的,如果len返回的值小于cap返回的值,发送是没有问题的。
6.5,单向channel
只能发送值的channel或者只能接收值的channl,一般是作为方法参数修饰,表示在方法里只能发送或者只能接收。
//只能接收的channel
func inChannel(inch <-chan int){
}
//只能发送的channel
func outChannel(outch chan<- int){
}
6.6,channel取值用法
channel取值分别有for range,for,select三种。
//for range 方式遍历,如果ch4处于未关闭状态,获取完后最后会阻塞,直到死锁报错
for v := range ch4{
fmt.Println(v)
}
//for方式遍历
for {
<-ch3
if(len(ch3) == 0){
break
}
}
//chan1,chan2那个channel有数据,执行那部分逻辑
select {
case <-chan1:
fmt.Println("chan1 ready.")
case <-chan2:
fmt.Println("chan2 ready.")
default:
fmt.Println("default")
}
6.7,channel异常情况
channel | nil(例如定义了 没有初始化) | 非空 | 空的 | 满了 | 没满 |
---|---|---|---|---|---|
接收 | 阻塞 | 接收值 | 阻塞 | 接收值 | 接收值 |
发送 | 阻塞 | 发送值 | 发送值 | 阻塞 | 发送值 |
关闭 | panic | 关闭成功,读完 数据后返回零值 | 关闭成功,返回零值 | 关闭成功,读完数据后返回零值 | 关闭成功,读完数据后返回零值 |
6.8,channel底层原理
//channel底层结果
type hchan struct {
closed uint32 // 标识关闭状态:表示当前通道是否处于关闭状态。创建通道后,该字段设置为0,即通道打开; 通过调用close将其设置为1,通道关闭。
qcount uint // 当前队列列中剩余元素个数,len返回的
dataqsiz uint // 环形队列长度,即可以存放的元素个数,即make(chan T,N)的N值,cap的值
buf unsafe.Pointer // 环形队列列指针,ring buffer 环形队列
elemsize uint16 // 每个元素的⼤⼩
elemtype *_type // 元素类型:用于数据传递过程中的赋值;
sendx uint // 队列下标,指示元素写⼊入时存放到队列列中的位置 x
recvx uint // 队列下标,指示元素从队列列的该位置读出
recvq waitq // 等待读消息的goroutine队列
sendq waitq // 等待写消息的goroutine队列
lock mutex // 互斥锁,chan不允许并发读写
}
channel底层存储的数据就是一个固定长度的数组,可以看成是一个环状的,分别用两个下标标记发送,读取位置。发送一个数据,元素个数加1,sendx下标加1,如果到数组尾部,就重新定为0;接收一个数据,数组元素减1,recvx下标加1,如果到数组尾部,重新定为0,如果元素数量为0就是没有元素了,如果元素数量等于容量,就是数组满了。
7,结构体(struct)
Go语言提供了一种自定义数据类型,可以封装多个基本数据类型,这种数据类型叫结构体。Go语言中没有class,是通过struct来实现面向对象。
7.1,结构体的基本形式和用法
go语言的结构体和c语言的结构体形式差不多。结构体中属性名必须唯一,可以是简单类型,也可以是其他结构体。
/*
结构体定义类型,可以是基础类型,也可以是符合类型,或者其他的结构体,属性的定义
结构体中字段大写开头表示可公开访问,小写表示私有(仅在定义当前结构体的包中可访问)
type 类型名 struct {
字段名 字段类型
字段名 字段类型
…
}
*/
//定义了一个Person结构体,包括name,age两个属性
type Person struct {
id int
name string
age int8
}
//结构体初始化
//初始化的值顺序和定义的顺序一致,且每个属性都需要初始化
var per1 Person = Person{2,"name",25}
//部分属性初始化,通过key:v这种形式,不用关心顺序
var per2 Person = Person{name:"name",age:26}
//结构体实例化 取地址实例化
per3 := &Person{}
per3.mame = "version"
//实例化和初始化一块进行
per4 := &Person{name:"name123"} //取地址实例化
//类似类的方式
var per5 Person
per5.id=12
per5.name="name"
per5.age = 27
//结构体实例化 格式 ins := new(T)
per6 := new(Person)
per6.name = "Canon"
7.2,结构体方法
//定义的结构体
type Person struct {
id int
name string
age int8
}
//给结构体Person添加方法,方法名Info,返回值类型string
func (r Person) Info() string {
return r.name
}
//调用方法
p := Person{name:"name123"} //相当于类的实例化
p.Info() //调用类方法
结构体没有构造方法之说,但是可以通过定义方法,实现类似的构造方法功能。
//定义的结构体
type Person struct {
id int
name string
age int8
}
//给结构体Person添加方法,方法名Info,返回值类型string
func (r Person) Info() string {
return r.name
}
//可以看作构造方法
func NewPerson(id int, name string, age int8) *Person{
return &Person{
id:id,
name:name,
age:age, //最后一个,不能少
}
}
p := NewPerson(12, "name123", 28) //相当于调用构造方法,返回Person指针
p.Info() //调用Person的方法Info
7.3,结构体字段,方法的可见性
其他语言中通过public,protected,private控制变量和方法的可见性,在Go语言中并没有这些关键字,它是通过结构体名称,字段名,方法名首字母大小写来控制公共,私有(这个私有是指当前结构体所在的包都能访问,和其他语言的private有区别)。结构体名称,字段名,方法名首字母小写,只能在该结构体的包中可以访问,结构体名称,字段名,方法名首字母大写,本包和其他包都可以访问。
//结构体名Person,首字母大写,可以在包外访问,如果是小写,只能在本包中访问
type Person struct{
name string //字段名name首字母小写,只能所在的包中可以访问
Age int8 //字段名Age首字母大写,其他包,本包中都可以访问
}
func (p Person) getName() string{ //方法名getName首字母小写,只能所在包中访问
return p.name
}
func (p Person) GetAge() string{ //方法名GetAge首字母大写,包中,包外都可以访问
return p.Age
}
7.4,结构体的匿名字段
匿名字段是一种特殊的字段类型,它没有字段名,只有字段类型。在结构体中,可以使用匿名字段来表示继承关系或者组合关系。
type Address struct{
addr string //这个是正常字段,有类型,有名称,不是匿名字段
int //这个地方是匿名字段,只有类型,没用名称
}
//匿名调用访问,匿名字段,以它的类型作为名进行访问
p := Address{
addr:"beijin",
int:45, //匿名字段
}
p.int = 85 //匿名字段赋值
fmt.Println(p.int) //访问匿名字段
7.5,结构体的嵌套
通过结构体的嵌套,可以实现其他语言的类似与类继承,覆盖等功能。
type Address struct{
addr string
}
func (ad Address) GetAddr() string{
return ad.addr
}
func (ad Address) Ress() string{
return "ress"
}
type Person struct{
Address //嵌套结构体 需要匿名
name string
}
func (p Person) MyInfo() string{
return p.name + p.addr //由于Address是匿名,Address的addr可以直接访问,属性实现继承
}
func (p Person) GetAddr() string{
return "p addr"
}
p := Person{
Address:Address{"beijin",}, //Address是匿名的,用类型作为名称
name:"name",
}
fmt.Println(p.Ress()) //这个是Address的方法,实现继承调用
fmt.Println(p.GetAddr()) //这个覆盖了Address的GetAddr方法,调用的是自己的方法
fmt.Println(p.Address.GetAddr()) //这个是Address的GetAddr方法
fmt.Println(p.MyInfo()) //调用自己的方法
7.6,结构体tag
tag可以给结构体属性添加说明或者备注标签,这些说明可以通过反射获取到,类似其他语言的注解。tag格式 `key01:"value01" key02:"value02" key03:"value03"` ,多个之间有空格。
type Person struct{
Name string `label:"name label"` //包裹的就是tag
Age int8 `label:"age label" type:"int8"` //多个之间,中间要有空格
}
type Man struct{
Name string `json:"name"` //这个是struct和json转换的时候经常用到,json的字段名和结构体字段名可以不一样
Age int `json:"age"`
}
7.7,空结构体的作用
结构体是没有没有任何成员的就叫空结构体,它的大小是0,也不包含任何信息。
空结构体作用:
1,实现set集合,可以使用map[KeyType]struct{}的形式实现一个set集合,其大小为0,可以避免内存损耗。
2,实现通道信号,在并发编程中,如果在不同的goroutine之间进行状态(有无)传递,可以使用struct{}作为通道信号。
3,只有方法的结构体,结合type可以实现只有方法的结构体。
type Emp struct { //空结构体
}
8,接口(interface)
接口实际上是一组方法的集合,是否继承这个结构,不需要明确的指定,如果某个类型实现了接口的所有方法,就可以说该类型实现了该interface。
8.1,接口的定义
接口的作用是规范代码,实现复杂的功能,接口里的方法只需要定义就可以,不能实现。
//接口的定义格式
/*
type 接口名 interface{
方法名1(参数列表1) 返回值列表1
方法名2(参数列表2) 返回值列表2
…
}
*/
//定义了一个名为CarInterface的接口
type CarInterface interface{
Run() //Run方法,无参数,无返回值
getName() string //getName方法,无参数,返回值string类型
setName(s string) //setName方法,有参数,无返回值
}
8.2,接口的继承
GO语言中的接口继承支持多继承的,继承关系不需要明确指定,不是耦合的,只要某个接口中的方法全部实现了,就认为继承了这个接口,这点和其他语言是不一样的。
type Child interface{
cry()
}
type Person interface{
speak()
}
type Man struct{
}
func (m Man) cry(){
fmt.Println("cry")
}
func (m Man) speak(){
fmt.Println("speak")
}
/*上面定义了两个接口,分别是Child,Person,定义了一个结构体Man,
由于结构体实现了Child的全部方法(cry),所以说结构体Man继承了接口Child,
由于结构体实现了Person的全部方法(speak),所以说结构体Man继承了接口Person
*/
8.3,接口的嵌套
接口的嵌套也可以看成接口的继承,嵌套后就会继承嵌套接口的方法
/*定义一个新的接口,里面嵌套了Child,Person两个接口,增加了一个方法charge()
由于是嵌套,只有实现了Child全部方法,Person全部方法,以及自己的方法charge,才能说继承了这个接口
*/
type Robot interface{
Child
Person
charge()
}
8.3,接口的使用
GO语言的接口的使用,和其他语言的接口使用方法类似。
type Child interface{
cry()
}
type Person interface{
speak()
}
type Robot interface{
Child
Person
charge()
}
type Man struct{
}
func (m Man) cry(){
fmt.Println("cry")
}
func (m Man) speak(){
fmt.Println("speak")
}
func (m Man) charge(){
fmt.Println("charge")
}
var c Child = Man{}
c.cry() //由于Child接口只有cry()方法,只能调用这个方法,不能调用speak方法,因为这个方法不属于Child
var r Robot = Man{} //Rebot继承了其他两个接口,拥有了他们的方法定义,所以方法都可以调用
r.cry()
r.speak()
r.charge()
8.4,空接口的使用
interface{} 是一个空的 interface 类型, 空的 interface 没有方法,所以可以认为所有的类型都实现了 interface{}。如果定义一个函数参数是 interface{} 类型, 这个函数应该可以接受任何类型作为它的参数。
空接口可以作为通用类型,不确定具体什么类型;作为方法接收者,实现泛型;用于channel传递不确定类型的值。
//interface{}可以代表任何类型
m := make(map[string]interface{}) //定义一个map,key字符串,值是任何类型
m["name"] = "John"
m["age"] = 30
func printValue(v interface{}) { //参数可以是任何类型,实现类似泛型的功能
fmt.Println(v)
}
var data []interface{} //实现泛型切面,可以放入任何类型元素
data = append(data, "Hello")
data = append(data, 123)
泛型类型的转换需要用到断言,格式 ret, ok := interface.(type)
a := 1
v, ok := interface{}(a).(int) // 将整数a转换为接口类型
//Go 中的 interface 类型是不能直接转换成其他类型的,需要使用到断言。
var itf interface{} = 1 //接口类型
j, ok := itf.(int) //通过断言,转换为int类型