A 基本数据类型
1 布尔类型 bool
1)Go 对于值之间的比较有非常严格的限制,只有两个类型相同的值才可以进行比较,如果值的类型是接口(interface),它们也必须都实现了相同的接口。如果其中一个值是常量,那么另外一个值的类型必须和该常量类型相兼容的。
2)对于布尔值的好的命名能够很好地提升代码的可读性,例如以 is 或者 Is 开头的isSorted 、 isFinished 、 isVisivle 。
3)在格式化输出时,你可以使用 %t 来表示你要输出的值为布尔型。
4)布尔型的值只可以是常量 true 或者 false。
2 数字类型
##有符号整数
int8(-128 -> 127)
int16(-32768 -> 32767)
int32(-2,147,483,648 -> 2,147,483,647)
int64(-9,223,372,036,854,775,808 -> 9,223,372,036,854,775,807)
int( 32 位操作系统上32 位,64 位操作系统64 位)
##无符号整数
uint8(0 -> 255)
uint16(0 -> 65,535)
uint32(0 -> 4,294,967,295)
uint64(0 -> 18,446,744,073,709,551,615)
uint ( 32 位操作系统上32 位,64 位操作系统64 位)
##浮点型
float32(+- 1e-45 -> +- 3.4 * 1e38)
float64(+- 5 1e-324 -> 107 1e308)
#无float类型
#复数
Go 拥有以下复数类型:
complex64 (32 位实数和虚数)
complex128 (64 位实数和虚数)
复数使用 re+imI 来表示,其中 re 代表实数部分, im 代表虚数部分,I 代表根号负 1
var c1 complex64 = 5 + 10i
fmt.Printf("The value is: %v", c1) // 输出: 5 + 10i
1)整型的零值为 0,浮点型的零值为 0.0。
2)int 和 uint 在 32 位操作系统上,它们均使用 32 位(4 个字节) ,在 64 位操作系统上,它们均使用 64 位(8 个字节)。
3)uintptr 的长度被设定为足够存放一个指针即可。
4)整型的零值为 0,浮点型的零值为 0.0。
5)int 型是计算最快的一种类型。
- 尽可能地使用 float64,因为 math 包中所有有关数学运算的函数都会要求接收这个类型。
- Go 是强类型语言,不会进行隐式转换.。所以Go 中不允许不同类型之间的混合使用,但允许常量之间的混合使用。
package main
func main() {
var a int
var b int32
a = 15
b = a + a // 编译错误
b = b + 5 // 因为 5 是常量,所以可以通过编译
}
3 字符类型
字符不是 Go 语言的一个类型,字符只是整数的特殊用例:
1)byte 类型是 uint8的别名:下面三个表达式等价:
var ch byte = 'A'
var ch byte = 65
var ch byte = '\x41' // \x 总是紧跟着长度为 2 的 16 进制数
2) rune 是 int32 的别名。
Unicode 至少占用 2 个字节,所以GO使用 int16 或者 int 类型来表示。如果需要使用到 4 字节,则会加上 \U 前缀;前缀 \u 则总是紧跟着长度为 4 的 16 进制数,前缀 \U紧跟着长度为 8 的 16 进制数。
var ch int = '\u0041'
var ch2 int = '\u03B2'
var ch3 int = '\U00101234'
fmt.Printf("%d - %d - %d\n", ch, ch2, ch3) // integer
fmt.Printf("%c - %c - %c\n", ch, ch2, ch3) // character
fmt.Printf("%X - %X - %X\n", ch, ch2, ch3) // UTF-8 bytes
fmt.Printf("%U - %U - %U", ch, ch2, ch3) // UTF-8 code point
B 字符串
// $GOROOT/src/pkg/runtime/runtime.h
struct String
{
byte* str;
intgo len;
};
1)字符串是 UTF-8 字符的一个序列(当字符为 ASCII 码时则占用 1 个字节,其它字符根据需要占用 2-4 个字节)
2)字符串在Go语言内存模型中用一个2字长的数据结构表示。它包含一个指向字符串存储数据的指针和一个长度数据。
3)字符串是一种值类型,且值不可变。所以多字符串共享同一个存储数据是安全的。
4)切分操作 str[i:j] 会得到一个新的2字长结构,一个可能不同的但仍指向同一个字节序列(即上文说的存储数据)的指针和长度数据。字符串切分不涉及内存分配。
5)string 类型的零值为ptr为nil,len为0的字符串,即空字符串 ""
6)获取字符串中某个字节的地址的行为是非法的,例如: &str[i]
C slice
// $GOROOT/src/pkg/runtime/runtime.h
struct Slice
{ // must not move anything
byte* array; // actual data
uintgo len; // number of elements
uintgo cap; // allocated number of elements
};
1)一个slice是一个数组某个部分的引用。在内存中,它是一个包含3个域的结构体:指向slice中第一个元素的指针,slice的长度,以及slice的容量。
2)切片 s 来说该不等式永远成立: 0 <= len(s)<= cap(s)
3)在对slice进行append等操作时,可能会造成slice的自动扩容:
首先判断,如果新申请容量(cap)大于2倍的旧容量(old.cap),最终容量(newcap)就是新申请的容量(cap)
否则判断,如果旧切片的长度小于1024,则最终容量(newcap)就是旧容量(old.cap)的两倍,即(newcap=doublecap)
否则判断,如果旧切片长度大于等于1024,则最终容量(newcap)从旧容量(old.cap)开始循环增加原来的 1/4,即(newcap=old.cap,for {newcap += newcap/4})直到最终容量(newcap)大于等于新申请的容量(cap),即(newcap >= cap)
func growslice(et *_type, old slice, cap int) slice {
if raceenabled {
callerpc := getcallerpc(unsafe.Pointer(&et))
racereadrangepc(old.array, uintptr(old.len*int(et.size)), callerpc, funcPC(growslice))
}
if msanenabled {
msanread(old.array, uintptr(old.len*int(et.size)))
}
if et.size == 0 {
// 如果新要扩容的容量比原来的容量还要小,这代表要缩容了,那么可以直接报panic了。
if cap < old.cap {
panic(errorString("growslice: cap out of range"))
}
// 如果当前切片的大小为0,还调用了扩容方法,那么就新生成一个新的容量的切片返回。
return slice{unsafe.Pointer(&zerobase), old.len, cap}
}
// 这里就是扩容的策略
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
newcap = cap
} else {
if old.len < 1024 {
newcap = doublecap
} else {
// Check 0 < newcap to detect overflow
// and prevent an infinite loop.
for 0 < newcap && newcap < cap {
newcap += newcap / 4
}
// Set newcap to the requested cap when
// the newcap calculation overflowed.
if newcap <= 0 {
newcap = cap
}
}
}
// 计算新的切片的容量,长度。
var lenmem, newlenmem, capmem uintptr
const ptrSize = unsafe.Sizeof((*byte)(nil))
switch et.size {
case 1:
lenmem = uintptr(old.len)
newlenmem = uintptr(cap)
capmem = roundupsize(uintptr(newcap))
newcap = int(capmem)
case ptrSize:
lenmem = uintptr(old.len) * ptrSize
newlenmem = uintptr(cap) * ptrSize
capmem = roundupsize(uintptr(newcap) * ptrSize)
newcap = int(capmem / ptrSize)
default:
lenmem = uintptr(old.len) * et.size
newlenmem = uintptr(cap) * et.size
capmem = roundupsize(uintptr(newcap) * et.size)
newcap = int(capmem / et.size)
}
// 判断非法的值,保证容量是在增加,并且容量不超过最大容量
if cap < old.cap || uintptr(newcap) > maxSliceCap(et.size) {
panic(errorString("growslice: cap out of range"))
}
var p unsafe.Pointer
if et.kind&kindNoPointers != 0 {
// 在老的切片后面继续扩充容量
p = mallocgc(capmem, nil, false)
// 将 lenmem 这个多个 bytes 从 old.array地址 拷贝到 p 的地址处
memmove(p, old.array, lenmem)
// 先将 P 地址加上新的容量得到新切片容量的地址,然后将新切片容量地址后面的 capmem-newlenmem 个 bytes 这块内存初始化。为之后继续 append() 操作腾出空间。
memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem)
} else {
// 重新申请新的数组给新切片
// 重新申请 capmen 这个大的内存地址,并且初始化为0值
p = mallocgc(capmem, et, true)
if !writeBarrier.enabled {
// 如果还不能打开写锁,那么只能把 lenmem 大小的 bytes 字节从 old.array 拷贝到 p 的地址处
memmove(p, old.array, lenmem)
} else {
// 循环拷贝老的切片的值
for i := uintptr(0); i < lenmem; i += et.size {
typedmemmove(et, add(p, i), add(old.array, i))
}
}
}
// 返回最终新切片,容量更新为最新扩容之后的容量
return slice{p, old.len, newcap}
}
make和new
Go有两个数据结构创建函数:new和make。
new返回一个指向已清零内存的指针,而make返回一个复杂的结构。
基本的区别是 new(T) 返回一个 *T ,返回的这个指针可以被隐式地消除引用(图中的黑色箭头) 。而 make(T, args) 返回一个普通的T.
注意:空切片和 nil 切片是不同的,空切片指向的地址不是nil,指向的是一个内存地址,但是它没有分配任何内存空间,即底层元素包含0个元素。
不管是使用 nil 切片还是空切片,对其调用内置函数 append,len 和 cap 的效果都是一样的。
D map
map在底层是用哈希表实现的。
struct Hmap
{
uint8 B; // 可以容纳2^B个项
uint16 bucketsize; // 每个桶的大小
byte *buckets; // 2^B个Buckets的数组
byte *oldbuckets; // 前一个buckets,只有当正在扩容时才不为空
};
struct Bucket
{
uint8 tophash[BUCKETSIZE]; // hash值的高8位....低位从bucket的array定位到bucket
Bucket *overflow; // 溢出桶链表,如果有
byte data[1]; // BUCKETSIZE keys followed by BUCKETSIZE values
};
// BUCKETSIZE是用宏定义的8,每个bucket中存放最多8个key/value对, 如果多于8个,那么会申请一个新的bucket,overflow指向它
1)Bucket中key/value的放置顺序,是将keys放在一起,values放在一起。
2)扩容使用的是增量扩容:扩容会建立一个大小是原来2倍的新的表,将旧的bucket搬到新的表中之后,并不会将旧的bucket从oldbucket中删除,而是加上一个已删除的标记。当hash表扩容之后,需要将那些旧的pair重新哈希到新的table上,这个工作是逐步的完成(在insert和remove时每次搬移1-2个pair)
查找过程
- 根据key计算出hash值。
- 如果存在old table, 首先在old table中查找,如果找到的bucket已经evacuated,转到步骤3。 反之,返回其对应的value。
- 在new table中查找对应的value。
插入过程分析
- 根据key算出hash值,进而得出对应的bucket。
- 如果bucket在old table中,将其重新散列到new table中。
- 在bucket中,查找空闲的位置,如果已经存在需要插入的key,更新其对应的value。
- 根据table中元素的个数,判断是否grow table。
- 如果对应的bucket已经full,重新申请新的bucket作为overbucket。
- 将key/value pair插入到bucket中。
F nil
Go任何类型在未初始化时都对应一个零值:布尔类型是false,整型是0,字符串是"",而指针,函数,interface,slice,channel和map的零值都是nil。