new与make
二者都是用于内存分配,当声明的变量是引用类型时,不能给该变量赋值,因为没有分配空间。
我们可以用new和make对其进行内存分配。
- 首先说说new
new函数定义
func new(Type) *Type
传入一个类型,返回一个指向分配好该类型的地址的指针,并将变量赋零值。
在敲代码时并不常用。
- 再来说说make
make也是用于内存分配的,但是和new不同。
make有明确的分工,它只能用于slice、map、channel。因为三者都是引用类型,所以并不需要返回指针,而且不会置零值。
make与new都是分配堆空间。
数据定义
1、当给返回值命名时,如果已经默认了返回值名称,则所有返回值都要有默认名。如
myFunc(x,y int)(sum int,error)
是不被允许的,编译会报错,如果一个返回值有名称则也需要用()括起来。
2、结构体比较
一般来说结构体是可以使用“==”比较的,只有当结构体定义时属性顺序不同,或者结构体中有不可比较的类型,如map和slice。
带有map和slice的结构体可以使用reflect库下的
DeepEqual(x, y any) bool
进行比较。
不同结构体实例化出来的对象比较结果必然也会不同。
3、string与nil类型
nil 可以用作 interface、function、pointer、map、slice 和 channel 的“空值”,但是不能作为string类型的空值。
所以nil不能作为string类型的返回值。
4、常量
变量在程序运行中分配内存,而常量在编译器预处理阶段就开始分配。
内存四区:
- 栈区
空间较小,要求数据读写性能高,数据存放时间较短暂。由编译器自动分配和释放,存放函数的参数值、函数的调用流程方法地址、局部变量等(局部变量如果产生逃逸现象,可能会挂在在堆区)
- 堆区
空间充裕,数据存放时间较久。一般由开发者分配及释放(但是Golang中会根据变量的逃逸现象来选择是否分配到栈上或堆上),启动Golang的GC由GC清除机制自动回收。
-
全局区
- 全局变量的开辟是在程序在
main
之前就已经放在内存中。而且对外完全可见。即作用域在全部代码中,任何同包代码均可随时使用,在变量会搞混淆,而且在局部函数中如果同名称变量使用:=
赋值会出现编译错误。 - 全局变量最终在进程退出时,由操作系统回收。我们在开发的时候,尽量减少使用全局变量的设计
- 常量区也归属于全局区,常量为存放数值字面值单位,即不可修改。或者说的有的常量是直接挂钩字面值的。
- 全局变量的开辟是在程序在
-
代码区
存放要运行的代码片段。
程序运行前的准备工作:
1、操作系统把物理硬盘代码load到内存
2、操作系统把c代码分成四个区
3、操作系统找到main函数入口执行
在golang中,常量是无法取出地址的,因为字面量符号并没有地址而言。
数组与切片
- 切片的底层也是数组
- 切片是可动态扩增长度的数组
- 初始化方式不同,数组声明时需要先确定容量len=cap,并且会使用0值填充,切片初始化时使用一般使用make,len和cap可以自定义不同的值。
定义切片的几种方式:
var (
a []int // nil切片
b = []int{} // 空切片
c = []int{1, 2, 3} //三个元素,len=3,cap=3
d = c[:2] //两个元素,len=2,cap=3
e = c[:2:cap(c)] //两个元素,len=2,cap=3
f = c[:0] //没有元素,len=0,cap=3
g = make([]int, 3) //三个元素,len=3,cap=3
h = make([]int, 2, 3) //两个元素,len=2,cap=3
)
。
拼接
两个slice在append的时候,记住需要进行将第二个slice进行...
打散再拼接。
使用new定义的slice返回的是切片指针,不能直接进行append,需要使用解引用后使用,不如用make。
map
1、赋值问题
Go map如果value是结构体,那么赋值的时候不允许使用map直接给结构体赋值。
原因:说法多种多样,可以分为以下几个点:
1)map作为一个封装好的数据结构,由于它底层可能会由于数据扩张而进行迁移,所以拒绝直接寻址,避免产生野指针;
2)map中的key在不存在的时候,赋值语句其实会进行新的k-v值的插入,所以拒绝直接寻址结构体内的字段,以防结构体不存在的时候可能造成的错误;
3)这可能和map的并发不安全性相关
- x = y 这种赋值的方式,你必须知道 x的地址,然后才能把值 y 赋给 x。
- 但 go 中的 map 的 value 本身是不可寻址的,因为 map 的扩容的时候,可能要做 key/val pair迁移
- value 本身地址是会改变的
- 不支持寻址的话又怎么能赋值呢
解决办法:
- 二次赋值
- 使用结构体指针
2、遍历赋值问题
如果map的value是指针类型,那么不要使用foreach,因为foreach中指针会始终指在最后一个元素的位置。
原因:
go foreach 中的指针变量在每次迭代中都会被重复使用。如果迭代变量在“for”语句之外声明,则执行后它们的值将是最后一次迭代的值.
解决办法:
使用for而不是foreach。
interface
1、赋值问题
比如说:
type People interface {
Speak(string) string
}
type Stduent struct{}
func (stu *Stduent) Speak(think string) (talk string) {
if think == "love" {
talk = "You are a good boy"
} else {
talk = "hi"
}
return
}
那么能不能这么赋值呢?
var peo People = Stduent{}
答案是不行
Stduent does not implement People
可以这么理解,实现接口的是结构体的引用不是结构体本身。
接口分为两种:空接口和非空接口。
- 先来看空接口
就是没有定义任何方法的接口。
如:
type EmptyInterface interface {
}
空接口结构体在Go语言中的定义为
type eface struct { //空接口
_type *_type //类型信息
data unsafe.Pointer //指向数据的指针(go语言中特殊的指针类型unsafe.Pointer类似于c语言中的void*)
}
根据这个定义来看看这个问题:
var stu *Stduent
var peo People = stu
想一想这个peo会不会是nil?
答案是不是,因为eface定义中data是nil但是_type不是nil,所以peo不是nil。
- 再来看看非空接口
Go中的定义为:
type iface struct {
tab *itab
data unsafe.Pointer
}
itab里面包含了interface的一些关键信息,比如method的具体实现。
2、inteface{}与*interface{}的区别
type S struct {
}
func f(x interface{}) {
}
func g(x *interface{}) {
}
func main() {
s := S{}
p := &s
f(s) //A
g(s) //B 运行出错
f(p) //C
g(p) //D 运行出错
}
Go语言是强类型语言,interface{}作为参数可以传入指针和结构体,*interface{}作为参数只能传入 *interface{}。
channel
特性
go 1.21.6 windows/amd64
- 给一个nil channel发送数据,会造成死锁
- 从一个nil channel取出数据,也会造成死锁
- 往一个已经close的channel发送数据,会报panic
- 但是从一个已经close的channel取出,不会报panic,如果缓冲区中为空,则返回一个零值
- 无缓冲的channel是同步的,而有缓冲的channel是非同步的
口诀是:“空读写死锁,写关闭异常,读关闭空零”。
WaitGroup
WaitGroup与goroutine的竞速问题:
问题描述:
const N = 10
var wg = &sync.WaitGroup{}
func main() {
for i:= 0; i< N; i++ {
go func(i int) {
wg.Add(1)
println(i)
defer wg.Done()
}(i)
}
wg.Wait()
}
结果不唯一,代码存在风险, 所有go未必都能执行到。
就是还没等到Add函数执行,goroutine已经完成了。
解决办法就是将Add函数放在goroutine外,wg.Done在goroutine 内。
大部分内容来自:语雀-刘丹冰Aceld-Go修养之路