协程
go协程的特点:
1.有独立的栈空间
2.共享程序堆空间
3.调度由用户控制
4.协程是轻量级的线程
堆和栈的区别:
1.申请方式和回收方式不同
堆和栈的第一个区别就是申请方式不同:
(1)栈(英文名称是 stack)是系统自动分配空间的,例如我们定义一个char a;系统会自动在栈上为其开辟空间。
(2)堆(英文名称是 heap)则是程序员根据需要自己申请的空间,例如:malloc(10) 开辟十个字节的空间。由于栈上的空间是自动分配自动回收的,所以栈上的数据的生存周期只是在函数的运行过程中,运行后就释放掉,不可以再访问。而堆上的数据只要程序员不释放空间,就一直可以访问到,不过缺点是一旦忘记释放会造成内存泄露。
2.申请后系统的响应不同
(1)栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。
(2)堆:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的 delete语句才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。 也就是说堆会在申请后还要做一些后续的工作这就会引出申请效率的问题。
3.申请效率的比较
根据第 1 点和第 2 点可知。
(1)栈:由系统自动分配,速度较快。但程序员是无法控制的。
(2)堆:是由 new 分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便。
4.申请大小的限制
(1)栈:在 Windows 下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。如果申请的空间超过栈的剩余空间时,将提示 overflow。因此,能从栈获得的空间较小。
(2)堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。
5.堆和栈中的存储内容
由于栈的大小有限,所以用子函数还是有物理意义的,而不仅仅是逻辑意义。
(1)栈: 在函数调用时,第一个进栈的是主函数中函数调用后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,然后是函数中的局部变量。注意静态变量是不入栈的。当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。
(2)堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容有程序员安排。
GPM模式调度:
1)M:操作系统的主线程(是物理线程)
2)P:协程执行需要的上下文
3)G:协程
管道
内部结构
type hchan struct {
//channel分为无缓冲和有缓冲两种。
//对于有缓冲的channel存储数据,借助的是如下循环数组的结构
qcount uint // 循环数组中的元素数量
dataqsiz uint // 循环数组的长度
buf unsafe.Pointer // 指向底层循环数组的指针
elemsize uint16 //能够收发元素的大小
closed uint32 //channel是否关闭的标志
elemtype *_type //channel中的元素类型
//有缓冲channel内的缓冲数组会被作为一个“环型”来使用。
//当下标超过数组容量后会回到第一个位置,所以需要有两个字段记录当前读和写的下标位置
sendx uint // 下一次发送数据的下标位置
recvx uint // 下一次读取数据的下标位置
//当循环数组中没有数据时,收到了接收请求,那么接收数据的变量地址将会写入读等待队列
//当循环数组中数据已满时,收到了发送请求,那么发送数据的变量地址将写入写等待队列
recvq waitq // 读等待队列
sendq waitq // 写等待队列
lock mutex //互斥锁,保证读写channel时不存在并发竞争问题
}
为什么需要channel:
channel阻塞:
channel 比如写入很快,读很慢,即使channel写入满后,也不会爆死锁的错误。默认有消费者的时候,chan会阻塞等待(必须得有生产消费者存在)
channel只读只写:
var ch <-ch int 只读
var ch ch<- int 只写
// channel 只读只写的操作, 目的是防止出现channel的误操作
func test1(ch <-chan int) {
for i := range ch {
fmt.Println(i)
}
}
func test2(ch chan<- int) {
for i := 0; i < 3; i++ {
ch <- i
}
close(ch)
}
func demo4() {
ch := make(chan int, 3)
test2(ch)
test1(ch)
}
select阻塞:
// select 实现channel阻塞
func demo5() {
intCh := make(chan int, 10)
for i := 1; i < 10; i++ {
intCh <- i
}
strCh := make(chan string, 5)
for i := 0; i < 5; i++ {
strCh <- fmt.Sprintf("hello, %v", i)
}
for {
select {
case v := <-intCh:
fmt.Println(v)
case v := <-strCh:
fmt.Println(v)
default:
continue
}
}
}
go协程中出现错误,怎么处理?
// 协程中出现错误,怎么处理
func sayHello(wg *sync.WaitGroup) {
defer func() {
wg.Done()
}()
for i := 0; i < 5; i++ {
fmt.Println("say hello", i)
time.Sleep(time.Second)
}
}
func sayHi(wg *sync.WaitGroup) {
defer func() {
wg.Done()
if err := recover(); err != nil {
fmt.Println("sayHi() error is", err)
}
}()
var myMap map[int]int
myMap[0] = 1 // 这里会出现一个panic
}
func demo6() {
var wg sync.WaitGroup
wg.Add(1)
go sayHello(&wg)
wg.Add(1)
go sayHi(&wg)
wg.Wait()
}
反射
应用场景:写框架、序列化数据、写适配器模式的等
基本介绍:
利用反射遍历结构体的字段、调用结构体的方法
type Monster struct {
Name string `json:"name"`
Age int `json:"monster_age"`
Score float32
Sex string
}
func (s Monster) Print() {
fmt.Println("-----start-----")
fmt.Println(s)
fmt.Println("-----end-----")
}
func (s Monster) GetSum(n1, n2 int) int {
return n1 + n2
}
func (s Monster) Set(name string, age int, score float32, sex string) {
s.Name = name
s.Age = age
s.Score = score
s.Sex = sex
}
func TestStruct(a interface{}) {
typ := reflect.TypeOf(a)
val := reflect.ValueOf(a)
kd := val.Kind()
if kd != reflect.Struct {
fmt.Println("expect struct")
return
}
// 获得该结构体,有几个字段
num := val.NumField()
fmt.Printf("struct has %d fields\n", num)
// 遍历结构体的所有字段
for i := 0; i < num; i++ {
fmt.Printf("field %d, value is %v\n", i, val.Field(i))
// 获取tag
tagVal := typ.Field(i).Tag.Get("json")
if tagVal != "" {
fmt.Printf("field %d, tag is %v \n", i, tagVal)
}
}
// 获取结构体有多少方法
numOfMethod := val.NumMethod()
fmt.Printf("struct has %d methods\n", numOfMethod)
// 反射获取的方法,默认是方法名来排序的
val.Method(1).Call(nil)
// 带参数
var params []reflect.Value
params = append(params, reflect.ValueOf(10))
params = append(params, reflect.ValueOf(40))
res := val.Method(0).Call(params)
fmt.Println("res = ", res[0].Int())
}
func demo4() {
a := Monster{
Name: "黄鼠狼金",
Age: 400,
Score: 30.8,
}
TestStruct(a)
}
面向对象
数据结构绑定方法时候,使用指针和数值的区别
/*
结构体绑定方法时候,使用指针和不使用的区别
使用指针:在方法内部对结构体的value发生修改时,外部也会发生修改
不使用指针:在方法内修改的内容,不会影响到外部
*/
type Stu struct {
Name string
Age int
}
func (s Stu) SetNameValue() {
s.Name = "xyt"
}
func (s *Stu) SetNamePtr() {
s.Name = "xyt"
}
func main() {
a := Stu{
Name: "wjf",
Age: 18,
}
a.SetNameValue()
fmt.Println(a.Name) // wjf
a.SetNamePtr()
fmt.Println(a.Name) // xyt
}
切片的扩容机制
切片的扩容机制,如果原切片ls满后,底层会重新创建一个新数组容量是原来的2倍,并将旧数组在值复制到新数组,然后ls指向新的数组。扩容到达1024后,只会增加原来的1/4去扩容。
5.1 切片类型
切片是引用类型,第一个元素是值的地址,底层是一个struct