一、类型初始化
1、基本数据类型
2、函数类型:初始值为nil
var test func() //初始值为nil
func (InputTypeList) OutputTypeListtype NewType OldTypetype NewFuncType FuncLiteral
示例: 如下声明了一个 CalculateType 函数类型,并实现 Serve() 方法,并将拥有相同参数的 add 和 mul 强制转换成 CalculateType 函数类型,同时这两个函数都拥有了 CalculateType 函数类型的 Serve() 方法。
package main
import "fmt"
type CalculateType func(int, int) // 声明了一个函数类型
// 该函数类型实现了一个方法
func (c *CalculateType) Serve() {
fmt.Println("我是一个函数类型")
}
// 加法函数
func add(a, b int) {
fmt.Println(a + b)
}
// 乘法函数
func mul(a, b int) {
fmt.Println(a * b)
}
func main() {
a := CalculateType(add) // 将add函数强制转换成CalculateType类型
b := CalculateType(mul) // 将mul函数强制转换成CalculateType类型
a(2, 3)
b(2, 3)
a.Serve()
b.Serve()
}
// 5
// 6
// 我是一个函数类型
// 我是一个函数类型
3、通道类型:初始值为nil,用make完成初始化
nil通道上的读写永远阻塞。当case上读一个通道时,如果这个通道是nil,则该case永远阻塞。nil channel会阻塞对该channel的所有读、写。所以可以将某个channel设置为nil,进行强制阻塞,对于select分支来说,就是强制禁用此分支。这个功能有1个妙用,select通常处理的是多个通道,当某个读通道关闭了,但不想select再继续关注此case,继续处理其他case,把该通道设置为nil即可。
select {
case x, open := <-inCh1:
if !open {
inCh1 = nil
break
}
out<-x
case x, open := <-inCh2:
if !open {
inCh2 = nil
break
}
out<-x
}
// 当ch1和ch2都关闭是才退出
if inCh1 == nil && inCh2 == nil {
break
}
}
初始化示例:
// var ch chan int //ch为nil
package main
import (
"fmt"
"runtime"
"time"
)
func main() {
var ch chan int
// g1
go func() {
ch = make(chan int, 1)
ch <- 1
}()
//g2
go func(ch chan int) {
time.Sleep(time.Second)
<-ch
}(ch)
c := time.Tick(1 * time.Second)
for range c {
fmt.Printf("#goroutines: %d\n", runtime.NumGoroutine())
}
}
#goroutines: 2chnilnilcNumGoroutine
4、切片类型:初始值为nil,用make完成初始化。
切片的底层数据结构:切片的结构体由3部分构成,Pointer 是指向一个数组的指针,len 代表当前切片的长度,cap 是当前切片的容量。cap 总是大于等于 len 的。
type slice struct {
array unsafe.Pointer
len int
cap int
}
初始化示例:
var s []int // len(s) == 0, s == nil
s = nil // len(s) == 0, s == nil
s = []int(nil) // len(s) == 0, s == nil
s = []int{} // len(s) == 0, s != nil初始化成功
s = make([]int,size,capacity) //初始化完成
append的注意事项: 可以直接往nil切片中追加元素。
var names []string
for key, _ := range urls {
names = append(names, key)
}
append的实现:
func Append(slice, data []byte) []byte {
// append的实现原理
// 直接使用比切片长度还要大的下标时,会报错内存溢出
// 所以要先make一个新的切片
lengthNewSlice := len(slice) + len(data)
// make新的切片时需注意总容量的大小,如果大于原切片,需扩充
capNewSlice := cap(slice)
if lengthNewSlice > cap(slice) {
capNewSlice = lengthNewSlice
}
// 经测试,数据类型不能作为变量传递进来,所以应该用switch来实现,此处不再赘述
newSlice := make([]byte, lengthNewSlice, capNewSlice)
// 接下来赋值
for sliceKey, sliceItem := range slice {//若slice为nil,则不会进行迭代
newSlice[sliceKey] = sliceItem
}
for dataKey, item := range data {
newSlice[dataKey + len(slice)] = item
}
// 赋值操作也可以用copy函数
return slice
}
5、map类型: 初始值为nil,用make完成初始化
map的hash寻址: 哈希函数会将传入的key值进行哈希运算,得到一个唯一的值。go语言把生成的哈希值一分为二,比如一个key经过哈希函数,生成的哈希值为:8423452987653321,go语言会这它拆分为84234529,和87653321。那么,前半部分就叫做高位哈希值,后半部分就叫做低位哈希值。后面会说高位哈希值和低位哈希值是做什么用的。
- 高位哈希值:是用来确定当前的bucket(桶)有没有所存储的数据的。
- 低位哈希值:是用来确定,当前的数据存在了哪个bucket(桶)
map底层的数据结构: hmap是map的最外层的一个数据结构,包括了map的各种基础信息、如大小、bucket。首先说一下,buckets这个参数,它存储的是指向buckets数组的一个指针,当bucket(桶为0时)为nil。我们可以理解为,hmap指向了一个空bucket数组,并且当bucket数组需要扩容时,它会开辟一倍的内存空间,并且会渐进式的把原数组拷贝,即用到旧数组的时候就拷贝到新数组。当往 map 中存储一个 kv 对时,通过 k 获取 hash 值,hash 值的低八位和 bucket 数组长度取余,定位到在数组中的那个下标,hash 值的高八位存储在 bucket 中的 tophash 中,用来快速判断 key 是否存在,key 和 value 的具体值则通过指针运算存储,当一个 bucket 满时,通过 overfolw 指针链接到下一个 bucket。
。
//Go map的一个header结构
type hmap struct {
count int //map的大小. len()函数就取的这个值
flags uint8 //map状态标识
B uint8 // 可以最多容纳 6.5 * 2 ^ B 个元素,6.5为装载因子即:map长度=6.5*2^B
//B可以理解为buckets已扩容的次数
noverflow uint16 // 溢出buckets的数量
hash0 uint32 // hash 种子
buckets unsafe.Pointer //指向最大2^B个Buckets数组的指针. count==0时为nil.
oldbuckets unsafe.Pointer //指向扩容之前的buckets数组,并且容量是现在一半.不增长就为nil
nevacuate uintptr // 搬迁进度,小于nevacuate的已经搬迁
extra *mapextra // 可选字段,额外信息
}
bucket(桶),每一个bucket最多放8个key和value,最后由一个overflow字段指向下一个bmap,注
意key、value、overflow字段都不显示定义,而是通过maptype计算偏移获取的。
// Go map 的 buckets结构
type bmap struct {
// 每个元素hash值的高8位,如果tophash[0] < minTopHash,表示这个桶的搬迁状态
tophash [bucketCnt]uint8
// 第二个是8个key、8个value,但是我们不能直接看到;为了优化对齐,go采用了key放在一起,value放在一起
// 第三个是溢出时,下一个溢出桶的地址
}
初始化示例:
// 先声明map
var m1 map[string]string // nil map不能赋值
// 再使用make函数创建一个非nil的map,nil map不能赋值
m1 = make(map[string]string)
// 最后给已声明的map赋值
m1["a"] = "aa"
m1["b"] = "bb"
// 直接创建
m2 := make(map[string]string)
// 然后赋值
m2["a"] = "aa"
m2["b"] = "bb"
// 初始化 + 赋值一体化
m3 := map[string]string{
"a": "aa",
"b": "bb",
}
// ==========================================
// 查找键值是否存在
if v, ok := m1["a"]; ok {
fmt.Println(v)
} else {
fmt.Println("Key Not Found")
}
// 遍历map
for k, v := range m1 {
fmt.Println(k, v)
}
6、new与make的区别
-
make用于内建类型的引用类型的初始化。(map、slice 和channel)的内存分配。new用于各种类型的内存分配。
-
内建函数new本质上说跟其它语言中的同名函数功能一样:new(T)分配了零值填充的T类型的内存空间,并且返回其地址,即一个*T类型的值。用Go的术语说,它返回了一个指针,指向新分配的类型T的零值。有一点非常重要:new返回指针。
-
内建函数make(T, args)与new(T)有着不同的功能,make只能创建slice、map和channel,并且返回一个有初始值(非零)的T类型,而不是*T。
p := new([]int) //p == nil; with len and cap 0,被置零的slice结构体的指针,即指向值为nil的slice的指针
fmt.Println(p)
v := make([]int, 10, 50) // v is initialed with len 10, cap 50
fmt.Println(v)
//输出结果:
//&[]
//[0 0 0 0 0 0 0 0 0 0]
二、类型寻址
什么数据类型是可以正常取址的?
假设 T 类型的方法上接收器既有 T 类型的,又有 *T 指针类型的,那么就不可以在不能寻址的 T 值上调用 *T 接收器的方法
- &B{} 是指针,可寻址
- B{} 是值,不可寻址
- b := B{} b是变量,可寻址
总结:
对于指针类型为 *T 的操作数 x,间接指针 *x 表示类型为 T 的值指向 x。若 x 为 nil,尝试求值 *x 将会引发运行时恐慌。
1、以下几种是可寻址的:
- 一个变量: &x
- 指针引用(pointer indirection): &*x
- slice 索引操作(不管 slice 是否可寻址): &s[1]
- 可寻址 struct 的字段: &point.X
- 可寻址数组的索引操作: &a[0]
- composite literal 类型: &struct{ X int }{1}
2、下列情况 x 是不可以寻址的,不能使用 &x 取得指针:
- 字符串中的字节
- map 对象中的元素
- 接口对象的动态值(通过 type assertions 获得)
- 常数
- literal 值(非 composite literal)
- package 级别的函数
- 方法 method(用作函数值)
- 中间值(intermediate value):
- 函数调用
- 显式类型转换
- 各种类型的操作(除了指针引用 pointer dereference 操作 *x):
channel receive operations
sub-string operations
sub-slice operations
加减乘除等运算符
解释:
1、常数为什么不可以寻址?
如果可以寻址的话,我们可以通过指针修改常数的值,破坏了常数的定义。
2、map 的元素为什么不可以寻址?
两个原因,如果对象不存在,则返回零值,零值是不可变对象,所以不能寻址,如果对象存在,因为 Go 中 map 实现中元素的地址是变化的,这意味着寻址的结果是无意义的。
3、为什么 slice 不管是否可寻址,它的元素读是可以寻址的?
因为 slice 底层实现了一个数组,它是可以寻址的。
4、为什么字符串中的字符/字节又不能寻址呢?
因为字符串是不可变的。
5、规范中还有几处提到了 addressable:
调用一个接收者为指针类型的方法时,使用一个可寻址的值将自动获取这个值的指针。++、-- 语句的操作对象必须可寻址或者是 map 的索引操作。赋值语句 = 的左边对象必须可寻址,或者是 map 的索引操作,或者是 _。上条同样使用 for … range 语句