目录
目录
1、基本类型
2、方法相关
3、接口
4、recover
5、类的创建
私有类
6、进程状态
7、进程的调度
- 线程是cpu调度的基本单位,进程是资源占有的基本单位。
- 因为线程中的代码是在用户态运行,而线程的调度是在内核态,所以线程切换会触发用户态和内核态的切换。
- 线程上下文切换的代价是高昂的:上下文切换的延迟取决于不同的因素,大概是50到100 ns左右,考虑到硬件平均在每个核心上每ns执行12条指令,那么一次上下文切换可能会花费600到1200条指令的延迟时间。
cpu上下文
CPU 寄存器,是CPU内置的容量小、但速度极快的内存。而程序计数器,则是用来存储 CPU 正在执行的指令位置、或者即将执行的下一条指令位置。它们都是 CPU 在运行任何任务前,必须的依赖环境,因此叫做CPU上下文
它是指,先把前一个任务的CPU上下文(也就是CPU寄存器和程序计数器)保存起来,然后加载新任务的上下文到这些寄存器和程序计数器,最后再跳转到程序计数器所指的新位置,运行新任务。
多线程和协程
虽然I/O线程数量增多,会造成非常频繁的上下文切换,进而影响效率。但在互联网应用中,它却是一个优秀的解决方案。
更优秀的解决方式也有,那就是使用协程。协程是用户态的线程,是对普通线程更细粒度的划分。它是在用户态运行的,由用户自行调度,所以也就避免了频繁的上下文切换问题。
但协程在Java中还不成熟,它依然是Golang语言的诱人特性。使用Golang开发的Web服务,可以采用更少的线程来支持大量I/O密集型的请求。
三者的区别:
线程 | 进程 | 协程 | |
---|---|---|---|
资源分配 | 在进程内共享资源 | 在进程间独立分配资源 | 在单个进程内共享资源 |
执行方式 | 并发执行 | 抢占性执行 | 并发执行 |
系统开销 | 较小 | 较大 | 较小 |
Go协程的调度是由Go语言运行时(runtime)自动管理的,而不是由操作系统内核调度的。这意味着,Go协程可以在不切换内核线程的情况下切换,从而避免了线程切换带来的开销。
其次,Go协程是协作式多任务,而不是抢占式多任务。这意味着,在Go语言中,协程之间是通过让出时间片的方式来协作的,而不是通过抢占处理器的方式。这使得Go协程在多个协程之间切换时,更加高效。
最后,Go协程是非常轻量级的。在Go语言中,创建一个新的协程只需要2kb左右的的内存空间,而传统的线程通常需要几兆的内存空间。这使得Go协程可以创建成千上万个,而不会对系统带来太大的负担。
但是,Go协程也有一些缺点,由于Go协程是由Go语言运行时自动调度的,因此它们之间并没有固定的执行关系,也就是说,不能保证某个协程在另一个协程之前执行。因此,在使用Go协程时,需要注意避免出现竞态条件的情况。
8、go的多线程模型
9、channel
不同于其他编程语言,Go使用通信通道(channel)来传递数据。这使得只有一个协程(goroutine)能够访问数据,避免了竞态条件的出现。
发送接收
10、select
11、for -select 的相关配置
12、timer 包
func main() {
//timer 和 channel
timer := time.NewTimer(5 * time.Second)
fmt.Printf("time is %v", time.Now().Unix())
expireTime := <-timer.C
fmt.Printf("expireTime is %v \n", expireTime)
fmt.Printf("expireTime is %v", timer.Stop())
timer2 := <-time.After(5 * time.Second)
fmt.Printf("timer2 is %v \n", timer2)
//简单的定时器
intChan := make(chan int, 1)
go func() {
time.Sleep(1 * time.Second)
intChan <- 1
}()
select {
case <-intChan:
fmt.Printf("1s Done")
case <-time.NewTimer(3 * time.Second).C:
fmt.Println("time out")
}
}
//每间隔5s发送一个随机数
intChan2 := make(chan int, 1)
timeTicker := time.NewTicker(5 * time.Second)
go func() {
for _ = range timeTicker.C {
select {
case intChan2 <- 1:
case intChan2 <- 2:
case intChan2 <- 3:
}
}
}()
var sum int
//遍历这个channel
for e := range intChan2 {
fmt.Println("reveice ", e)
sum += e
if sum > 10 {
fmt.Println("超过10 任务结束")
//结束定时timer
timeTicker.Stop()
break
}
}
13、锁
读写锁
sync只执行一次
sync waitGroup
14、切片相关
首先强调几点关于切片的注意事项
- Golang 中的函数参数,都是传值,不是传地址
- 对于切片自身的底层数据结构,我们可以通过索引的方式拿到底层数组的地址,并修改其地址上的值,例如
sli[2] = "hello"
,这是可以直接修改
-
如果传入的切片,期望实参也能够被改变的话,那么就需要想办法修改切片的底层数组
- 通过传切片的地址,也就是传指针的方式
- 在函数中,去索引切片的底层数组地址,进行修改数据
x := []int{1, 2, 3}
func(arr *[]int) {
(*arr)[0] = 7
fmt.Println(arr)
}(&x)
fmt.Println(x)
map和slice,在golang中,当map作为形参时,虽然是值传递,但是由于make()返回的是一个指针类型,所以我们可以在函数哪修改map的数值并影响到函数外
严格来说golang没有引用传递,下面示例汇总虽然在exchangeSlice内对切片的修改反馈到了main函数中,但是内部和外部的slice的地址其实是不一样的。两个slice的地址不一致,但是他们指向了同一个底层数组,在exchangeSlice函数内的变更最后反馈到了底层数组上。
package main
import "fmt"
func main() {
slice := []int{1,2,3,4,5}
fmt.Println(slice)
exchangeSlice(slice)
fmt.Println(slice)
}
func exchangeSlice(slice []int) {
for k, v := range slice {
slice[k] = v * 2
}
}
但是需要注意
这里需要格外注意,在内部exchangeSlice函数内如果发生了底层数组的扩容,那么exchangeSlice内的slice则会申请新的数组,在扩容后的slice做的变更也将不会再反馈到外部main函数的slice中
slice在append的时候,如果底层数组的大小(cap)不够了,就会发生扩容。发生扩容的时候,slice结构体的指针会指向一个新的底层数组,然后把原来数组中的元素拷贝到新数组中,最后添加上append的新元素,就完成了扩容。 所以在这个时候,函数内部slice的改变是不会影响到函数外部slice的。因为此时,两个结构体中的指针指向的底层数组已经不相同了。
package main
import "fmt"
func main() {
slice := []int{1,2,3}
fmt.Println(slice)
exchangeSlice(slice)
fmt.Println(slice)
}
func exchangeSlice(slice []int) {
slice = append(slice, 4, 5, 6); // 原始数组大小为4,超过4会扩容
for k, v := range slice {
slice[k] = v * 2
}
fmt.Println(slice)
}
slice扩容