golang 类型的初始化和寻址问题

一、类型初始化

1、基本数据类型

在这里插入图片描述

2、函数类型:初始值为nil

var test func() //初始值为nil

函数字面量类型的语法表达格式是 func (InputTypeList) OutputTypeList, “有名函数”和“匿名函数(没有函数名类似闭包)”的类型都属于函数字面量类型,有名函数的定义相当于初始化一个函数字面量类型后将其赋值给一个函数名变量,“匿名函数”的定义也是直接初始化一个函数字面量类型,只是没有绑定到一个具体函数名变量上。从 Go 类型系统的角度来看,“有名函数”和“匿名函数”都是函数字面量类型的实例。可以使用 type NewType OldType 语法定义一种新类型,这种类型都是命名类型,同理可以使用该方法定义一种新类型——函数命名类型,简称函数类型,例如:type NewFuncType FuncLiteral 依据Go语言类型系统的概念,NewFuncType 为新定义的函数命名类型,FuncLiteral 为函数字面量类型,FuncLiteral 为函数类型 NewFuneType 的底层类型。

示例: 如下声明了一个 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: 2ch声明后为nil,在g1中被初始化为缓冲区大小为1的通道,g1向ch写数据后退出;通过参数把ch传递给g2时,ch还是nil,所以在g2内部ch为nil,从nil的通道读数据会阻塞,所以g2无法退出;另外Main协程不会退出,会持续遍历通道c,定时器的通道并不统计在NumGoroutine中,所以会打印存在2个goroutine。

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的实现:

扫描二维码关注公众号,回复: 12329442 查看本文章
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数组需要扩容时,它会开辟一倍的内存空间,并且会渐进式的把原数组拷贝,即用到旧数组的时候就拷贝到新数组。

//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 语句

猜你喜欢

转载自blog.csdn.net/u014618114/article/details/107828329