一、类型初始化

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的区别

  1. make用于内建类型的引用类型的初始化。(map、slice 和channel)的内存分配。new用于各种类型的内存分配。

  2. 内建函数new本质上说跟其它语言中的同名函数功能一样:new(T)分配了零值填充的T类型的内存空间,并且返回其地址,即一个*T类型的值。用Go的术语说,它返回了一个指针,指向新分配的类型T的零值。有一点非常重要:new返回指针。

  3. 内建函数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):
  1. 函数调用
  2. 显式类型转换
  3. 各种类型的操作(除了指针引用 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 语句