赞
踩
目录
一、var、new和make的区别?
二十一、Printf(),Sprintf(),FprintF()都是格式化输出,有什么不同?
变量初始化一般包括两步:变量声明和内存分配。var关键字是用来声明变量的,new和make函数是用来分配内存的。
(一)使用var初始化有两种情况
- package main
-
- import "fmt"
-
- func main() {
- var i int
- fmt.Println(i)
- }
-
- // 输出
- 0
- package main
-
- import "fmt"
-
- func main() {
- var i *int
- *i = 0
- fmt.Println(*i)
- }
-
- // 输出
- panic: runtime error: invalid memory address or nil pointer dereference
- [signal SIGSEGV: segmentation violation code=0x2 addr=0x0 pc=0x104edf930]
-
- goroutine 1 [running]:
- main.main()
- /Users/Jason/go/src/test.go:7 +0x20
- exit status 2
此时想要使用需要分配内存
- package main
-
- import "fmt"
-
- func main() {
- var i *int
- i = new(int)
- *i = 1
- fmt.Println(*i)
- }
-
- // 输出
- 1
(二)new和make都是Golang中分配内存的内建函数,在堆上分配内存,有以下区别:
channel是go中的一种数据类型,多个协程可以通过channel进行交互。
channel分为无缓冲区和有缓冲区两种。
channel底层是基于环形队列实现的,
defer底层为压栈,执行顺序为“先进后出”。
return的操作并非原子性的,分为赋值和返回两步。
retrun先将结果写入返回值,defer执行defer后的函数,函数携带返回值退出。
方法一:使用一个无缓存channel
- package main
-
- import (
- "fmt"
- "sync"
- )
-
- func main() {
- ch := make(chan int)
- wg := sync.WaitGroup{}
- // 这里设置计数器为1的原因是在i=10时,两个协程的for循环都结束
- // 此时已经打印完成,因此只需要执行一次wg.Done()
- wg.Add(1)
- go func() {
- defer wg.Done()
- for i := 1; i < 11; i++ {
- ch <- 1
- if i%2 == 0 {
- fmt.Println("偶数:", i)
- }
- }
- }()
- go func() {
- defer wg.Done()
- for i := 1; i < 11; i++ {
- <-ch
- if i%2 != 0 {
- fmt.Println("奇数:", i)
- }
- }
- }()
- wg.Wait()
- }
由于变量ch是一个无缓冲的channel,所以只有读写同时就绪时才不会阻塞。所以两个goroutine会同时进入各自的if语句(此时i是相同的),但是此时只能有一个 if 是成立的,不管哪个go协程快,都会由于读channel或写channel导致阻塞,因此程序会交替打印1-10且有顺序。
方法二:使用两个channel,其中一个有缓存
- package main
-
- import (
- "fmt"
- "sync"
- )
-
- func main() {
- // 设置缓存的原因是当i=10时,打印奇数的协程1会先退出,打印偶数的协程2打印出10
- // 然后会往ch1中写数据,如果ch1不设置缓存,会发生堵塞,协程2会一直无法退出
- ch1 := make(chan struct{}, 1)
- ch0 := make(chan struct{})
- wg := sync.WaitGroup{}
- wg.Add(2)
- go func() {
- defer wg.Done()
- for i := 1; i < 11; i++ { // 10
- ch1 <- struct{}{}
- if i%2 != 0 {
- fmt.Println("第一个协程,奇数:", i)
- }
-
- ch0 <- struct{}{}
- }
- }()
- go func() {
- defer wg.Done()
- for i := 1; i < 11; i++ { // 10
- <-ch0
-
- if i%2 == 0 {
- fmt.Println("第二个协程,偶数:", i)
- }
- <-ch1
- }
- }()
- wg.Wait()
- }
(一)信号通知(无缓冲的通道)
当信息收集完成,通知下游开始计算数据:
- package main
-
- import (
- "log"
- "time"
- )
-
- func collectMsg(isOver chan struct{}) {
- log.Println("开始采集")
- time.Sleep(3000 * time.Millisecond)
- log.Println("完成采集")
- isOver <- struct{}{}
- }
-
- func calculateMsg() {
- log.Println("开始进行数据分析")
- }
-
- func main() {
- isOver := make(chan struct{})
- go func() {
- collectMsg(isOver)
- }()
- <-isOver
- calculateMsg()
- }
输出结果
- 2022/04/13 15:19:17 开始采集
- 2022/04/13 15:19:20 完成采集
- 2022/04/13 15:19:20 开始进行数据分析
如果只是单纯的使用通知操作,那么最好将空结构体放入通道。因为空结构体在go中是不占用内存空间的。
res := struct{}{} fmt.Println(unsafe.Sizeof(res)) // 结果为0
(二)执行任务超时控制(通过select机制)
使用select
和time.After
,看操作和定时器哪个先返回,处理先完成的,就达到了超时控制的效果。
- package main
-
- import (
- "log"
- "time"
- )
-
- func doWork() <-chan struct{} {
- ch := make(chan struct{})
- go func() {
- // 处理耗时任务
- log.Println("开始处理任务")
- time.Sleep(2 * time.Second)
- ch <- struct{}{}
- }()
- return ch
- }
-
- func main() {
- select {
- case <-doWork():
- log.Println("任务在规定时间内结束")
- case <-time.After(1 * time.Second):
- log.Println("任务处理超时")
- }
- }
输出结果
- 2022/04/13 17:24:17 开始处理任务
- 2022/04/13 17:24:18 任务处理超时
(三)控制并发数(通过设置缓冲区大小)
经常会写一些脚本,在凌晨的时候对内或者对外拉取数据,但是如果不对并发请求加以控制,往往会导致 groutine 泛滥,进而占满CPU资源。往往不能控制的东西意味着不好的事情将要发生。对于我们来说,可以通过channel来控制并发数。
- package main
-
- import (
- "fmt"
- "time"
- )
-
- func job(index int) {
- // 耗时任务
- time.Sleep(2 * time.Second)
- fmt.Printf("任务:%d 已完成\n", index)
- }
-
- func main() {
- limit := make(chan struct{}, 10)
- jobCount := 100
- for i := 0; i < jobCount; i++ {
- go func(index int) {
- limit <- struct{}{}
- job(index)
- <-limit
- }(i)
- }
- time.Sleep(30 * time.Second)
- }
以上程序最多同时开启10个协程。
channel可以声明为只读或者只写,默认是读写都可用。
使用channel完成后记得注意关闭,不关闭阻塞会导致deadlock。
当需要不断从channel读取数据时,最好使用for-range
读取channel,这样既安全又便利,当channel关闭时,for循环会自动退出,无需主动监测channel是否关闭,可以防止读取已经关闭的channel,造成读到数据为通道所存储的数据类型的零值。
重复关闭通道、关闭一个nil的通道、往一个关闭的通道发送数据。
1. 重复关闭channel
- package main
-
- func main() {
- ch := make(chan struct{})
- close(ch)
- close(ch)
- }
输出结果
- panic: close of closed channel
-
- goroutine 1 [running]:
- main.main()
- /home/jason/go/src/test.go:6 +0x39
- exit status 2
2. 关闭一个nil值的channel
- package main
-
- func main() {
- var ch chan struct{}
- close(ch)
- }
输出结果
- panic: close of nil channel
-
- goroutine 1 [running]:
- main.main()
- /home/jason/go/src/test.go:5 +0x1b
- exit status 2
3. 往一个已经关闭的channel写数据
- package main
-
- func main() {
- ch := make(chan struct{})
- close(ch)
- ch <- struct{}{}
- }
输出结果
- panic: send on closed channel
-
- goroutine 1 [running]:
- main.main()
- /home/jason/go/src/test.go:6 +0x3e
- exit status 2
go从语言层面实现并发编程。很多语言支持多进程、多线程,但实现和控制比较麻烦。go语言天生支持并发和协程,可以轻松启动成千上万个协程。
Go语言的数据类型可以分为:基本数据类型和派生数据类型。
基本数据类型包括数值型(整形、浮点型、其他类型)、字符串、字符型、布尔型。
派生数据类型包括结构体、数组、指针、映射、切片、通道、函数、接口、联合体。
包的本质是创建不同的文件夹,来存放程序文件。
Go支持显式类型转换以满足其严格的类型要求。
Goroutine是用户态的一种更轻量级的线程,其调度由用户(程序)控制而不是由内核控制。
如果A和B有想同的方法列表,那么接口A和B就是等价的,可以相互赋值;如果接口A的方法是接口B的子集,那么接口B可以赋值给接口A,因为B实现了A接口的所有方法。
从类型、模式和状态总结channel的特点。
类型:channel分为有缓冲和无缓冲两种类型。
模式:只读模式、只写模式和读写模式。
- ch1 := make(chan<- int) // 只读chan
- ch2 := make(<-chan int) // 只写chan
- ch3 := make(chan int) // 读写chan
状态:未初始化、正常和关闭
未初始化 | 正常 | 关闭 | |
关闭 | panic | 正常关闭 | panic |
发送 | 永远堵塞导致deadlock | 堵塞或成功 | panic |
接收 | 永远堵塞导致deadlock | 堵塞或成功 | 缓冲区为空接收类型零值,否则正常 |
注意点:
无缓冲的Channel是同步的,有缓冲的Channel是非同步的。
数组、切片和通道。
切片有三个域:数组指针、大小和容量。
defer常用在成对的操作上,如打开、关闭、连接、断开连接、加锁、解锁。
切片的底层是通过数组实现的,基于数组实现使得底层的内存是连续分配的。
切片对象非常小,是因为它是只有3个字段的数据结构: 指向底层数组的指针、切片的长度、切片的容量。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。