国外官网:https://golang.org/dl/

Golang安装包国内镜像网站:https://golang.google.cn/dl/

中文网站:https://studygolang.com/dl

go的优势:

  • 极简单的部署方式:
    • 可直接编译机器码
    • 不依赖其他库
    • 直接运行即可部署
  • 静态类型语言
    • 编译时候即可检查出来大多数问题
  • 语言层面上的并发
    • 天生支持并发
    • 切换成本低
    • 能够充分利用多核Cpu利用率
  • 强大的标准库:
    • runtime系统调度机制
    • 高效的GC垃圾回收
    • 丰富的标准库
  • 简单易学
    • 关键字少,仅25个
    • 有面向对象特征(封装,继承,多态)
    • 跨平台语言

Golang基础

1.1 变量的声明

变量声明的四种方式:

  1. var a int //a默认初始值为0
  2. var b =10
  3. var c int=20
  4. d:=30 (常用)

其中:=声明方式只能用在函数体内部,所以第四种不能用在全局声明

1.2 常量与iota

const修饰的量称为常量

  • 定义常量时必须赋初值
    1. const a =10
    2. const b int //报错
  • 常量不允许被修改
    1. const a =10
    2. a=20 //报错 cannot assign to a 不能修改a的数值

const定义枚举类型:

iota只能与const一起使用

  1. //const与iota一起定义枚举类型时,每行iota都会加1,第一行默认值为0
  2. const(
  3. BeiJing=iota // BeiJing=0
  4. HuBei // HuBei=1
  5. FuJian // FuJian=2
  6. )
  1. //使用表达式跟前者一样
  2. const(
  3. a,b=iota+1,iota+2 //此时iota=0 所以a=1,b=2
  4. c,d //此时iota=1 所以c=2,d=3
  5. e,f //此时iota=2 所以e=3,f=4
  6. g,h=iota*2,iota*3 //此时iota=3 所以g=6,h=9
  7. j,k //此时iota=4 所以j=8,k=12
  8. )

1.3 init函数与import导包

  1. `main`函数在导包时,首先会执行`init`函数,即是`init`函数会优先于`main`函数先被执行。因此可以在`init`函数执行一些初始化操作,比如加载配置文件,环境变量初始化等。

1654958002056.png

  1. testinit
  2. ├─lib1
  3. └─lib1.go
  4. ├─lib2
  5. └─lib2.go
  6. └─main.go

main.go代码:

  1. import (
  2. "awustjq/go-codingtrave/testinit/lib1"
  3. "awustjq/go-codingtrave/testinit/lib2"
  4. )
  5. func main() {
  6. lib1.Lib1Test()
  7. lib2.Lib2Test()
  8. }

lib1.go代码:

  1. func Lib1Test(){
  2. fmt.Println("lib1Test is working.....")
  3. }
  4. func init() {
  5. fmt.Println("lib1 init.....")
  6. }

lib2.go代码:

  1. func Lib2Test(){
  2. fmt.Println("lib2Test is working.....")
  3. }
  4. func init() {
  5. fmt.Println("lib2 init.....")
  6. }

执行结果:可以看出main.go执行前会先去执行导包中init函数

  1. lib1 init.....
  2. lib2 init.....
  3. lib1Test is working.....
  4. lib2Test is working.....

import导包

  1. import _ "aa" //给aa包起一个匿名,无法使用当前包里的方法,但是还是会执行此包的init函数;
  2. import bb "fmt" //给fmt包起一个别名,可以使用bb.Println()进行打印;
  1. import aa "fmt"
  2. func main() {
  3. aa.Println("hello world")
  4. fmt.Println("hello world") //报错
  5. }

1.4 defer语句

类似于C++中的析构函数,程序声明周期结束前执行的命令;当有个多个defer语句,**defer**语句采用的是**压栈**顺序执行,即先进后出执行顺序

  1. //打印顺序 3 4 2 1 说明defer是压栈执行 且程序结束前执行
  2. func main() {
  3. defer fmt.Println("1 is working....")
  4. defer fmt.Println("2 is working....")
  5. fmt.Println("3 is working....")
  6. fmt.Println("4 is working....")
  7. }

**return****defer**在同一个函数,**return**先被执行。

  1. //"return func called..."先被打印 "defer func called..."后被打印 说明return先于defer执行
  2. func deferfunc()int{
  3. fmt.Println("defer func called...")
  4. return 0
  5. }
  6. func returnfunc()int{
  7. fmt.Println("return func called...")
  8. return 0
  9. }
  10. func testdeferAndreturn()int{
  11. defer deferfunc()
  12. return returnfunc()
  13. }

1.5 slice切片

数组定义方式:

  1. var Array1 [10]int //默认全是0
  2. Array2:=[10]int{1,2,3,4,5} //不够的补0
  3. Array3:=[...]int{1,2,3,4} //不写数组具体数字,自动推导数字个数

切片的本质:切片变量名是指向底层数组首地址的指针,维护了指向底层数组的指针,长度和容量。

切片定义方式:

  1. //方式1 没有分配空间时通过索引访问就会报错
  2. var slice1 []int{}
  3. //此时仅声明slice1是切片,并没分配空间,两种插入元素会报错。
  4. slice1[0]=0 / slice1=append(slice1,0) //越界报错
  5. //方式2:
  6. //slice2的长度和容量均为3
  7. slice2:=[]int{1,2,3}
  8. //方式3:
  9. //slice3=[]int{0,0,0} len(slice3)=3,cap(slice3)=3 长度和容量都为3,所以切片数用0填充
  10. slice3:=make([]int,3) 等价于 slice3:=make([]int,3,3)
  11. //slice4=[]int{} len(slice3)=0,cap(slice3)=3 长度为0,容量为3
  12. slice4:=make([]int,0,3)

两种声明方式比较

  1. dic1:=make([]int,3)
  2. dic2:=make([]int,0,3)
  3. fmt.Println("len(dic1)=",len(dic1),"cap(dic1)=",cap(dic1),"dic1= ",dic1)
  4. fmt.Println("len(dic2)=",len(dic2),"cap(dic2)=",cap(dic2),"dic2= ",dic2)
  5. dic1=append(dic1,0)
  6. dic2=append(dic2,0)
  7. fmt.Println("len(dic1)=",len(dic1),"cap(dic1)=",cap(dic1),"dic1= ",dic1)
  8. fmt.Println("len(dic2)=",len(dic2),"cap(dic2)=",cap(dic2),"dic2= ",dic2)
  9. //输出:
  10. len(dic1)= 3 cap(dic1)= 3 dic1= [0 0 0]
  11. len(dic2)= 0 cap(dic2)= 3 dic2= []
  12. len(dic1)= 4 cap(dic1)= 6 dic1= [0 0 0 0]
  13. len(dic2)= 1 cap(dic2)= 3 dic2= [0]

切片的长度表示左指针和右指针之间的距离;

切片的容量表示左指针到底层数组末尾的距离;

1655025400516.png

切片和数组比较:

  • 数组长度是固定的,不便于修改。
  • 数组作为参数传参的时候,数组长度和传参数组长度必须一致。
  • 数组采用值传递,切片采用引用传递。
  • 数组需要遍历求解数组的长度,而切片底层包含len字段,可以直接计算切片长度。

切片中扩容的原理:

  • 如果旧切片的容量小于1024,则扩容后的容量是原来的两倍;
  • 如果旧切片的容量大于1024,则扩容后的容量在原基础上增加1/4;
  • 如果指定容量超过旧切片容量的两倍,则扩容至指定容量;

深浅拷贝问题:

切片本质是指向底层数组的指针s2s1底层公用一个数组,所以一方修改了底层数组数值,另一方数值也会被修改,即是浅拷贝

s3借助copy函数实现深拷贝也即是**copy**找了一块新内存将底层数组也进行拷贝,所以对原底层数组修改,对s3底层并无影响。

  1. func main() {
  2. s1:=[]int{1,2,3}
  3. s2:=s1[:]
  4. //s3的容量要与s1一致
  5. s3:=make([]int,len(s1))
  6. copy(s3,s1)
  7. s2[0]=999
  8. fmt.Println("s1=",s1,"s2=",s2,"s3=",s3)
  9. fmt.Println("&s1[0]=",&s1[0],"&s2[0]=",&s2[0],"&s3[0]=",&s3[0])
  10. }
  11. //s1= [999 2 3] s2= [999 2 3] s3= [1 2 3]
  12. //&s1[0]= 0xc0000ae090 &s2[0]= 0xc0000ae090 &s3[0]= 0xc0000ae0a8

1.6 map

  1. `map`中并没有`cap`容量的概念,如果插入数据超过`map`容量,`map`不会像切片一样存在容量倍数增长,而是你插入多少容量就增长多少。

定义形式:

  1. //map并没有容量这概念
  2. var m1 map[string]string //并没有分配内存空间,赋值报错
  3. m2:=make(map[int]int,5)
  4. m3:=map[string]string{ //声明时直接赋初值
  5. "one":"php",
  6. "two":"go",
  7. }
  1. map底层是通过哈希表实现的,有一个buckets指针和oldbuckets指针,当不进行扩容时候,使用的是buckets,而oldbuckets为空,当进行扩容的时候,oldbuckets不为空,buckets大小变为oldbuckets两倍。 buckets储存是将key放在一起,value放在一起,而没有将keyvalue放一起,这样可以避免字节对齐的问题,避免浪费多余储存空间。
  1. delete(citymap,"China") //只能以key值进行删除
  1. func ChangeValueslice(s1 []int){
  2. s1=append(s1,6)
  3. fmt.Println("func里面s地址为:",&s1[0])
  4. }
  5. func ChangeValuemap(m1 map[string]string){
  6. m1["three"]="c++"
  7. }
  8. func main() {
  9. m1:=make(map[string]string)
  10. m1["one"]="php"
  11. m1["two"]="Go"
  12. s1:=[]int{1,2,3,4,5}
  13. fmt.Println("main里面s地址为:",&s1[0])
  14. fmt.Println("第一次打印:","m1=",m1,"s1=",s1)
  15. ChangeValueslice(s1)
  16. ChangeValuemap(m1)
  17. fmt.Println("第二次打印:","m1=",m1,"s1=",s1)
  18. }
  19. //main里面s地址为: 0xc000010480
  20. //第一次打印: m1= map[one:php two:Go] s1= [1 2 3 4 5]
  21. //func里面s地址为: 0xc00000e2d0
  22. //第二次打印: m1= map[one:php three:c++ two:Go] s1= [1 2 3 4 5]
  23. //main里面s地址为: 0xc000010480

比较main函数和func函数中切片地址(地址不同,说明传参是拷贝了一个副本)和数值(数值没有被修改,说明底层数组不同),实际上切片给函数传参本质还是值传递

1.7 结构体struct

  1. 如果说类的属性首字母大写,表示该属性是对外可以访问的,否则的话只能再类的内部访问 ,**用大小写表示类中属性或者方法是否对其他包开放(类名,属性,方法等都是这样)**。在本类中大小写都可以访问 但是必须大写才能被外包和模块访问。

给一个类型起别名

  1. type myint int //相当于给int类型起一个别名myint
  2. func main() {
  3. var a myint=10
  4. fmt.Println("a=",a)
  5. fmt.Printf("type of a=%T\n",a)
  6. }

封装

实现方式:

给结构体绑定一个方法,带由接受者的函数称为方法;

  1. type Book struct{
  2. Name string
  3. Auth string
  4. }
  5. //此即为封装,给结构体Book绑定一个GetName的方法
  6. func (b *Book)GetName()string{ //(b *Book)一般传指针 可以用来进行写操作
  7. }

继承:

实现方式:

  • 通过匿名字段实现继承

定义父类结构体Human和对应方法

  1. type Human struct{
  2. Name string
  3. Age int
  4. }
  5. func (h *Human)Eat(){
  6. fmt.Println("human eat....")
  7. }
  8. func (h *Human)Walk(){
  9. fmt.Println("human walk....")
  10. }

结构体SuperMan通过匿名字段继承结构体Human

  1. type SuperMan struct{
  2. Human //通过父类匿名字段实现继承
  3. Id int
  4. }
  5. //重写父类的walk方法
  6. func (sm *SuperMan)Walk(){
  7. fmt.Println("SuperMan walk....")
  8. }
  9. //定义子类独有的方法
  10. func (sm *SuperMan)Fly(){
  11. fmt.Println("SuperMan fly....")
  12. }

定义子类对象,实现继承

  1. func main() {
  2. sm:=&SuperMan{
  3. Id: 1,
  4. Human:Human{"姜庆",12},
  5. }
  6. // var s SuperMan 定义子类对象
  7. //s.Id=... s.Name=... s.Age=...
  8. sm.Eat() //human eat.... 子类没有的方法 从父类继承
  9. sm.Walk() //SuperMan walk.... 子类独有的方法,实现子类自己的方法
  10. sm.Fly() //SuperMan fly.... 子类重写的方法,实现子类自己的方法
  11. }
  • 子类将父类所有方法全部重写

子类将父类所有方法全部重写,就可认为是实现继承;

多态:

实现方式:

  • 父类是一个interface接口类型;
  • 子类必须全部重写父类的接口方法;
  • 父类指向子类的指针对象;

定义一个父类的抽象类并定义两种方法

  1. type AnimalIF interface{ //interface本质是指针
  2. Eat()
  3. Sleep()
  4. }

定义具体的类,并分别实现这两种方法

  1. //此时即实现前述所说的继承,子类全部重写父类的方法,只不过此父类是一个抽象类
  2. type Cat struct {}
  3. func (c *Cat)Eat(){
  4. fmt.Println("Cat is eatting")
  5. }
  6. func (c *Cat)Sleep(){
  7. fmt.Println("Cat is Sleepping")
  8. }
  9. type Dog struct {}
  10. func (d *Dog)Eat(){
  11. fmt.Println("Dog is eating")
  12. }
  13. func (d *Dog)Sleep(){
  14. fmt.Println("Dog is Sleepping")
  15. }

定义不同对象实现多态 传指针因为AnimalIF是指针类型

  1. func main(){
  2. var animal AnimalIF //定义父类对象
  3. animal=&Cat{} //父类指向子类的指针对象
  4. animal.Eat() //Cat is eatting
  5. animal.Sleep() //Cat is Sleepping
  6. animal=&Dog{}
  7. animal.Eat() //Dog is eatting
  8. animal.Sleep() //Dog is Sleepping
  9. }

或者定义一个多态的方法,再实现

  1. func ShowAnimal(animal AnimalIF){
  2. animal.Sleep()
  3. animal.Eat()
  4. }
  5. func main() {
  6. var dog Dog
  7. ShowAnimal(&dog) //父类指向子类的指针对象
  8. var cat Cat
  9. ShowAnimal(&cat) //父类指向子类的指针对象
  10. }

1.8 万能类型interface{}

interface{}是万能类型

  1. func TestInter(arg interface{}){
  2. fmt.Printf("type of arg is %T, arg=%v\n",arg,arg)
  3. }
  4. func main() {
  5. a,str,b:=1,"abc",3.14
  6. TestInter(a) //type of arg is int, arg=1
  7. TestInter(str) //type of arg is string, arg=abc
  8. TestInter(b) //type of arg is float64, arg=3.14
  9. }

interface{}提供的类型断言机制,只有空接口类型才有

  1. func TestInter(arg interface{}){
  2. //fmt.Printf("type of arg is %T, arg=%v\n",arg,arg)
  3. //如果是断言类型,value就位对应数值,ok为true 否则value为nil,ok为false
  4. value,ok:= arg.(string)
  5. if !ok{
  6. fmt.Println("arg is not string type")
  7. }else{
  8. fmt.Println("arg is string type, value=",value)
  9. }
  10. }
  11. func main() {
  12. a,str,b:=1,"abc",3.14
  13. TestInter(a) //arg is not string type
  14. TestInter(str) //arg is string type, value= abc
  15. TestInter(b) //arg is not string type
  16. }

断言机制成功原因:

  1. 变量的类型与变量值实现一个`pair`对,赋值时,始终会保持这个`pair`
  1. func main() {
  2. var a interface{}
  3. //pair<type:string value:"JiangQing">
  4. str:="JiangQing"
  5. //赋值后,保证pair对不会被改变,pair<type:string value:"JiangQing">
  6. a=str
  7. fmt.Printf("type of a is %T ,a=%v\n",a,a)
  8. }

1.9 反射机制

  1. reflect包里
  2. reflect.TypeOf(arg) //求变量类型
  3. reflect.ValueOf(arg) //求变量值

结构体转jsonjson转结构体

定义对应结构体

  1. type Movie struct {
  2. Title string `json:"电影名"`
  3. Year int `json:"上映年份"`
  4. Price int `json:"票价"`
  5. Actors []string `json:"主演"`
  6. }

编码过程:结构体转json

  1. func main() {
  2. movie:=&Movie{
  3. Title: "喜剧之王",
  4. Year: 1999,
  5. Price: 20,
  6. Actors: []string{"周星驰","张柏芝"},
  7. }
  8. //结构体转json 返回值是[]byte{}
  9. jsonstr,_:=json.Marshal(movie)
  10. fmt.Println(string(jsonstr))
  11. }
  12. //{"电影名":"喜剧之王","上映年份":1999,"票价":20,"主演":["周星驰","张柏芝"]}
  13. {
  14. "电影名":"喜剧之王",
  15. "上映年份":1999,
  16. "票价":20,
  17. "主演":[
  18. "周星驰",
  19. "张柏芝"
  20. ]
  21. }

解码过程:将json转结构体

  1. var m Movie
  2. json.Unmarshal(jsonstr,&m) //这里切记传指针,因为你要修改结构体变量
  3. fmt.Printf("m=%#v\n",m)
  4. //m=main.Movie{Title:"喜剧之王", Year:1999, Price:20, Actors:[]string{"周星驰", "张柏芝"}}

1.10 GMP模型

1.11 groutine

非匿名

  1. func Print(){
  2. i:=0
  3. for{
  4. time.Sleep(1*time.Second)
  5. fmt.Println("子go程:",i)
  6. i++
  7. }
  8. }
  9. func main() {
  10. go Print()
  11. i:=0
  12. for{
  13. time.Sleep(1*time.Second)
  14. fmt.Println("主go程:",i)
  15. i++
  16. }
  17. }

无参go程 匿名

  1. func main() {
  2. go func() {
  3. defer fmt.Println("A defer")
  4. func(){
  5. defer fmt.Println("B defer")
  6. fmt.Println("B")
  7. }()
  8. fmt.Println("A")
  9. }()
  10. time.Sleep(3*time.Second)
  11. }
  12. // B ; B defer; A ;A defer;

有参go程

  1. func main() {
  2. //想要获得子go程返回值要借助channel
  3. go func(a int,b int)bool {
  4. fmt.Println("a=",a,"b=",b)
  5. return true
  6. }(10,20)
  7. time.Sleep(3*time.Second)
  8. }

1.12 channel

**chan**的结构体有个读**goroutine**队列,写**goroutine**队列,互斥锁**mutex**,环形队列作为缓冲区 实现**groutine**之间通信,可以让**groutine**之间按照顺序执行

无缓冲的channel:两个go程中,任意一个先到达都会阻塞等待对方到达才会执行传输。

  1. eg:如果写go程先到达,他会阻塞等待读go程到达将管道数据读走才会执行写go程的后续操作,读go程也会继续后续操作;反过来 go程先到达也会阻塞等待写go程写入数据

有缓冲的channel:当写go程写满管道容量,就无法继续写 进而阻塞

  1. 当读go程从管道持续读数据 没有数据就会阻塞。关闭channel后,无法向channe发送数据,但可以从中读取数据。
  1. func main() {
  2. c:=make(chan int)
  3. go func() {
  4. defer fmt.Println("子groutine结束....")
  5. fmt.Println("子groutine运行....")
  6. c<-666
  7. }()
  8. time.Sleep(1*time.Second)
  9. if num,ok:=<-c;ok{
  10. fmt.Println("num=",num)
  11. }
  12. }
  13. //先打印"子groutine运行...."
  14. //而 "子groutine结束...." 与num数值打印两者之间没有先后顺序,甚至可能主groutine结束,导致 "子groutine结束...." 没有被打印出来

channelrange联合使用 会持续向channel c中读取数据 如果没有数据就会阻塞

  1. for data:=range c{
  2. fmt.Println(data)
  3. }

channelselect联合使用

  1. for {
  2. select {
  3. case <-chan1://持续监测chan1 从chan1中读取数据 如果chan1中有数据就会触发
  4. case chan1<-1: //持续监测chan2 向chan2中写数据 如果chan2为空就会触发
  5. }
  6. }
  1. # 版本替换问题
  2. go mod edit -replace =zinx@v0.1=znix@v0.2
  3. # 当设置了GOPRIVATE后,导包就会先去GOPRIVATE进行导包

通过加锁解决数据竞争问题

  1. package main
  2. import (
  3. "fmt"
  4. "sync"
  5. )
  6. var (
  7. x int64
  8. wg sync.WaitGroup // 等待组
  9. m sync.Mutex
  10. )
  11. // add 对全局变量x执行5000次加1操作
  12. func add() {
  13. for i := 0; i < 5000; i++ {
  14. m.Lock()
  15. x = x + 1
  16. m.Unlock()
  17. }
  18. wg.Done()
  19. }
  20. func main() {
  21. wg.Add(2)
  22. go add()
  23. go add()
  24. wg.Wait()
  25. fmt.Println(x)
  26. }

三个协程交替打印

  1. package main
  2. import (
  3. "fmt"
  4. "sync"
  5. )
  6. func main() {
  7. var dogChan, catChan, fishChan = make(chan struct{}), make(chan struct{}), make(chan struct{})
  8. wg := sync.WaitGroup{}
  9. wg.Add(3)
  10. go func(s string) {
  11. defer wg.Done()
  12. for i := 0; i < 10; i++ {
  13. <-dogChan
  14. fmt.Println(s)
  15. catChan <- struct{}{}
  16. }
  17. <-dogChan
  18. }("dog")
  19. go func(s string) {
  20. defer wg.Done()
  21. for i := 0; i < 10; i++ {
  22. <-catChan
  23. fmt.Println(s)
  24. fishChan <- struct{}{}
  25. }
  26. }("cat")
  27. go func(s string) {
  28. defer wg.Done()
  29. for i := 0; i < 10; i++ {
  30. <-fishChan
  31. fmt.Println(s)
  32. dogChan <- struct{}{}
  33. }
  34. }("fish")
  35. dogChan <- struct{}{}
  36. wg.Wait()
  37. }

交替打印奇数和偶数

  1. package main
  2. import (
  3. "fmt"
  4. "sync"
  5. )
  6. // 两个携程集体打印数
  7. func main() {
  8. jiChan, ouChan := make(chan struct{}), make(chan struct{})
  9. wg := sync.WaitGroup{}
  10. a := 0
  11. wg.Add(1)
  12. go func(a *int) {
  13. for i := 0; i < 10; i++ {
  14. <-jiChan
  15. fmt.Println(*a)
  16. *a++
  17. ouChan <- struct{}{}
  18. }
  19. <-jiChan
  20. defer wg.Done()
  21. }(&a)
  22. go func(a *int) {
  23. for i := 0; i < 10; i++ {
  24. <-ouChan
  25. fmt.Println(*a)
  26. *a++
  27. jiChan <- struct{}{}
  28. }
  29. }(&a)
  30. jiChan <- struct{}{}
  31. wg.Wait()
  32. }

交替打印奇数偶数

  1. package main
  2. import (
  3. "fmt"
  4. "sync"
  5. )
  6. // 两个携程集体打印数
  7. func main() {
  8. jiChan, ouChan := make(chan *int), make(chan *int)
  9. wg := sync.WaitGroup{}
  10. wg.Add(1)
  11. go func() {
  12. for i := 0; i < 10; i++ {
  13. a, _ := <-jiChan
  14. fmt.Println(*a)
  15. *a++
  16. ouChan <- a
  17. }
  18. <-jiChan
  19. defer wg.Done()
  20. }()
  21. go func() {
  22. for i := 0; i < 10; i++ {
  23. a, _ := <-ouChan
  24. fmt.Println(*a)
  25. *a++
  26. jiChan <- a
  27. }
  28. }()
  29. a := 0
  30. jiChan <- &a
  31. wg.Wait()
  32. }

三个携程交替打印abc

  1. package main
  2. import (
  3. "fmt"
  4. "sync"
  5. )
  6. func main() {
  7. wg := sync.WaitGroup{}
  8. aChan, bChan, cChan := make(chan struct{}), make(chan struct{}), make(chan struct{})
  9. wg.Add(1)
  10. go func() {
  11. for i := 0; i < 10; i++ {
  12. <-aChan
  13. fmt.Print("A")
  14. bChan <- struct{}{}
  15. }
  16. defer wg.Done()
  17. <-aChan
  18. }()
  19. go func() {
  20. for i := 0; i < 10; i++ {
  21. <-bChan
  22. fmt.Print("B")
  23. cChan <- struct{}{}
  24. }
  25. }()
  26. go func() {
  27. for i := 0; i < 10; i++ {
  28. <-cChan
  29. fmt.Print("C")
  30. fmt.Println()
  31. aChan <- struct{}{}
  32. }
  33. }()
  34. aChan <- struct{}{}
  35. wg.Wait()
  36. }