2021-10-20

go 课件拾遗

break,continue

标签

如果不加标签的话,默认退出当前最近的一次循环。而标签可以退出自己想退出的循环。

循环结构例题

打出日历表

package main

import "fmt"

func main() {
   /*
      万年历
      思路:year:=2018,month:=8
      求出2018年7月31日,距离1900年1月1日的总天数,除以7取余数,就是空格的数量
      星期一星期二星期三星期四星期五星期六星期日
   */

   //1.给定年份和月份,可以键盘输入:
   var year int
   fmt.Print("请输入年份:")
   fmt.Scanf("%d",&year)
   var month int
   fmt.Print("请输入月份:")
   fmt.Scanf("%d",&month)
   //2.求出2018年7月31日到1900年1月1日的总天数
   sum := 0 //总天数
   //2.1先累加整年的天数
   for i := 1900; i < year; i++ {
      if i%4 == 0 && i%100 != 0 || i%400 == 0 { //判断i是否是闰年
         sum += 366
      } else {
         sum += 365
      }
   }
   //2.2求2018年1月到7月的总天数
   day := 0 //每个月的天数
   for i := 1; i < month; i++ {
      switch i {
      case 1, 3, 5, 7, 8, 10, 12:
         day = 31
      case 4, 6, 9, 11:
         day = 30
      case 2:
         if year%4 == 0 && year%100 != 0 || year%400 == 0 {
            day = 29
         } else {
            day = 28
         }
      }
      sum += day
   }
   //3.计算空格的数量
   kong := sum % 7
   //4.求month月的天数
   day2 :=0//month月的天数
   switch month {
   case 1,3,5,7,8,10,12:
      day2=31
   case 4,6,9,11:
      day2=30
   case 2:
      if year %4==0&&year % 100!=0||year % 400 ==0{
         day2 = 29
      }else {
         day2=28
      }
   }
   //打印
   fmt.Println("星期一\t星期二\t星期三\t星期四\t星期五\t星期六\t星期日")
   //打印空格
   for i:=0;i<kong;i++{
      fmt.Print("\t\t")
   }
   //打印数字
   for i:=1;i<=day2;i++{
      fmt.Print(i,"\t\t")
      //换行
      if (i+kong) % 7 ==0{
         fmt.Println()
      }
   }
    fmt.Println(sum)
}

map

因为返回的是指针,map作为参数的时候,函数内部能修改map。

我们知道slice 也可以使用make初始化,make slice返回的是结构体,slice作为参数的时候,函数内部修改可能会影响slice


for range 函数的使用

package main

import "fmt"

func main() {
   x := min(1, 3, 2, 0)
   fmt.Printf("The minimum is: %d\n", x)
   slice := []int{7,9,223,5,1}
   x = max(slice...)
   fmt.Printf("The minimum in the slice is: %d", x)
}

func min(s ...int) int {
   if len(s)==0 {
      return 0
   }
   min := s[0]
   for _, v := range s {
      if v < min {
         min = v
      }
   }
   return min
}
func max(b ...int )int {
   if len(b) == 0 {
      return 0
   }
   max := b[0]
   for _, v := range b {
      if v > max {
         max = v
      }
   }
   return max
}

传递边长参数时

package main

import "fmt"

func main() {
   x := min(1, 3, 2, 0)
   fmt.Printf("The minimum is: %d\n", x)
   slice := []int{7,9,223,5,1}
   x = max(slice...)
   fmt.Printf("The maxmum in the slice is: %d", x)
}

func min(s ...int) int {
   if len(s)==0 {
      return 0
   }
   min := s[0]
   for _, v := range s {
      if v < min {
         min = v
      }
   }
   return min
}
func max(b ...int )int {
   if len(b) == 0 {
      return 0
   }
   max := b[0]
   for _, v := range b {
      if v > max {
         max = v
      }
   }
   return max
}

defer

defer是在函数中最后执行,也可以类栈 后进先出

    for i := 0; i < 5; i++ {
        defer fmt.Printf("%d ", i)
    }
}

结果是 4 3 2 1 0

-[…]

defer 函数实参是通过值拷贝进去所以a++不影响defer的函数值

package main
func f() int {
   a:=1
   defer  func(i int){
      print(i)
   }(a)

   a++
 return a
}
func main(){
   f()
}

defer在注册(return)后是无法执行的

package main
func main(){
   defer print("hello world 1")
   return
   defer  print("hello world 2")
}

调用os.Exit(int)时就算defer提前注册也不会再被执行

package main

import (
   "fmt"
   "os"
)

func main() {
   defer fmt.Println("hello word 1")
   fmt.Println("hello world 2")
   os.Exit(1)
}

匿名函数

第一种用法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yJx8T9g9-1634737066022)(C:\Users\17250\AppData\Roaming\Typora\typora-user-images\image-20211006160056033.png)]

第二种用法

将函数赋值给变量,通过a 来进行调用。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xJNn5cyz-1634737066024)(C:\Users\17250\AppData\Roaming\Typora\typora-user-images\image-20211006160259932.png)]

第三种方法

全局匿名函数。

**ps:**函数开头要大写。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Vg0WHATF-1634737066025)(C:\Users\17250\AppData\Roaming\Typora\typora-user-images\image-20211006160700066.png)]

package main

import "fmt"

var (
 Fun1 =func (n1 ,n2 int)int{
   return n1-n2
}
)//全局匿名函数,开头字母要大写

func main() {

   func() {
      fmt.Println("hello word")
   }() //立即执行匿名函数
   
   var add = func(a, b int) int {
      return a + b
   }(10, 20)
   fmt.Println(add)
   
   
   Fun1(5,6)
   fmt.Println(Fun1(5, 6))
}

defer+recover捕获panic

可以提高程序的健壮性

package main

import (
   "fmt"
)

func main()  {
   text()
   fmt.Println("测试结束")
}
func text()  {
   defer func() {
      err:=recover()
      if err !=0{
         fmt.Println("错误已捕获",err)
      }
   }()
   sum1:=10
   sum2:=0
   result:=sum1/sum2
   fmt.Println(result)

}
自定义错误
package main

import (
   "errors"
   "fmt"
)

func main() {
   err := text()
   if err != nil {
      fmt.Println("错误为", err)
   } else {
      fmt.Println("测试结束,上述程序正常")
   }

}
func text() (err error) {
   sum1 := 10
   sum2 := 5
   if sum2 == 0 {
      return errors.New("除数不能为0")
   } else {

      result := sum1 / sum2
      fmt.Println(result)
      return nil
   }

}

sort 排序使用

package main

import (
   "fmt"
   "sort"
)

func main() {
   var a = [...]int{3, 7, 8, 9, 1}
   b := a[:]
    sort.Ints(b)//Ints 根据类型来进行操作
   fmt.Println(b)
   sort.Sort(sort.Reverse(sort.IntSlice(b)))//反向排序
   fmt.Println(b)
}

自定义函数进行多函数单一操作

package main

import "fmt"

func add(a, b int) int {
   return a + b
}
func sub(a, b int) int {
   return a - b
}
func mul(a, b, c int) int {
   return a * b * c
}

func do(f Op, a, b int) int {
   return f(a, b)
}

func work(l Ct, a, b, c int) int {
   return l(a, b, c)
}

type Op func(int, int) int //自定义一个Op来选择你要进行的函数
type Ct func(int, int, int) int

func main() {
   a := do(sub, 545, 2)
   fmt.Println(a)
   b := do(add, 5, 6)
   fmt.Println(b)
   c := work(mul, 5, 6, 3)
   fmt.Println(c)

}

接口

接口是一种抽象类型

package main

import "fmt"

type dog struct {}
type cat struct {}
type people struct {}

func (d dog) say() {
   fmt.Println("汪汪汪")
}


func (c cat) say() {
   fmt.Println("喵喵喵")
}

func (p people)say(){
   fmt.Println("啊啊啊")
}

type Sayer interface {
   say()
}//定义一个接口类型Sayer
func do(ary Sayer){
   ary.say()
}//定义一个函数 来同此函数完成say的方法
func main(){
   a:=cat{}//实例化一个cat
   do(a)
    
   b:=people{}//实例化一个people
   do(b)
    
   c:=dog{}
   do(c)

}

使用值接受者和使用指针接收者实现接口的区别

package main

import "fmt"

type dog struct{}
type cat struct{}
type people struct{}

func (d dog) say() {
   fmt.Println("汪汪汪")
}

func (c cat) say() {
   fmt.Println("喵喵喵")
}

func (p people) say() {
   fmt.Println("啊啊啊")
}

type Sayer interface {
   say()
} //定义一个接口类型Sayer

func do(ary Sayer) {
   ary.say()
} //定义一个函数 来同此函数完成say的方法

type Mover interface {
   move()
}

func (d *dog) move() {
   fmt.Println("狗会动")
}
func main() {
   var S Mover
   var a = &dog{}
   S = a
   S.move()

   b := dog{}
   S = b//这里会进行报错因为  move是指针接收者如果换成&bJ就成功
   S.move()
   // b:=dog{}
   // S.move()

}

接口关系

一个类型可以同时实现多个接口,而接口间彼此独立,不知道对方的实现。 例如,狗可以叫,也可以动。我们就分别定义Sayer接口和Mover接口,如下: Mover接口。

// Sayer 接口
type Sayer interface {
	say()
}

// Mover 接口
type Mover interface {
	move()
}

dog 既可以实现Sayer接口,也可以实现Mover接口

type dog struct {
	name string
}

// 实现Sayer接口
func (d dog) say() {
	fmt.Printf("%s会叫汪汪汪\n", d.name)
}

// 实现Mover接口
func (d dog) move() {
	fmt.Printf("%s会动\n", d.name)
}

func main() {
	var x Sayer
	var y Mover

	var a = dog{name: "旺财"}
	x = a
	y = a
	x.say()
	y.move()
}

多类型实现同一接口

Go语言中不同的类型还可以实现同一接口 首先我们定义一个Mover接口,它要求必须由一个move方法。

// Mover 接口
type Mover interface {
	move()
}

例如狗可以动,汽车也可以动,可以使用如下代码实现这个关系:

type dog struct {
	name string
}

type car struct {
	brand string
}

// dog类型实现Mover接口
func (d dog) move() {
	fmt.Printf("%s会跑\n", d.name)
}

// car类型实现Mover接口
func (c car) move() {
	fmt.Printf("%s速度70迈\n", c.brand)
}

这个时候我们在代码中就可以把狗和汽车当成一个会动的物体来处理了,不再需要关注它们具体是什么,只需要调用它们的move方法就可以了。

func main() {
	var x Mover
	var a = dog{name: "旺财"}
	var b = car{brand: "保时捷"}
	x = a
	x.move()
	x = b
	x.move()
}

上面的代码执行结果如下:

旺财会跑
保时捷速度70

并且一个接口的方法,不一定需要由一个类型完全实现,接口的方法可以通过在类型中嵌入其他类型或者结构体来实现。

// WashingMachine 洗衣机
type WashingMachine interface {
	wash()
	dry()
}

// 甩干器
type dryer struct{}

// 实现WashingMachine接口的dry()方法
func (d dryer) dry() {
	fmt.Println("甩一甩")
}

// 海尔洗衣机
type haier struct {
	dryer //嵌入甩干器
}

// 实现WashingMachine接口的wash()方法
func (h haier) wash() {
	fmt.Println("洗刷刷")
}

接口嵌套

接口与接口通过嵌套创造出新的接口

// Sayer 接口
type Sayer interface {
	say()
}

// Mover 接口
type Mover interface {
	move()
}

// 接口嵌套
type animal interface {
	Sayer
	Mover
}

嵌套得到的接口的使用与普通接口一样,这里我们让cat实现animal接口:

type cat struct {
	name string
}

func (c cat) say() {
	fmt.Println("喵喵喵")
}

func (c cat) move() {
	fmt.Println("猫会动")
}

func main() {
	var x animal
	x = cat{name: "花花"}
	x.move()
	x.say()
}

空接口

空接口是值没有定义任何方法的接口。因此任何类型都实现了空接口。空接口类型的变量可以接收任意类型的变量

func main() {
	// 定义一个空接口x
	var x interface{}
	s := "Hello 沙河"
	x = s
	fmt.Printf("type:%T value:%v\n", x, x)
	i := 100
	x = i
	fmt.Printf("type:%T value:%v\n", x, x)
	b := true
	x = b
	fmt.Printf("type:%T value:%v\n", x, x)
}

空接口的意义

空接口作为函数的参数

使用空接口可以实现接收任意类型的函数参数。

// 空接口作为函数参数
func show(a interface{}) {
	fmt.Printf("type:%T value:%v\n", a, a)
}

空接口作为map的值

使用空接口实现可以保存任意值的字典。

// 空接口作为map值
	var studentInfo = make(map[string]interface{})
	studentInfo["name"] = "沙河娜扎"
	studentInfo["age"] = 18
	studentInfo["married"] = false
studentInfo["hoppy"]=[]string{"basktball","ping-pong"}
	fmt.Println(studentInfo)
package main

import (
   "fmt"
)

func main(){
   var x interface{}
   x="hello"
   fmt.Println(x)
   var m=make(map[interface{}]interface{})
   m["城市"]=15515
   m[1212]="北京"
   fmt.Println(m)
}

代码如下

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-e7WHzG2g-1634737066027)(C:\Users\17250\AppData\Roaming\Typora\typora-user-images\image-20211008190554686.png)]

类型断言

空接口可以存储任意类型的值,那我们如何获取其存储的具体数据呢?

想要判断空接口中的值这个时候就可以使用类型断言,其语法格式:

x.(T)

其中:

  • x:表示类型为interface{}的变量
  • T:表示断言x可能是的类型。

该语法返回两个参数,第一个参数是x转化为T类型后的变量,第二个值是一个布尔值,若为true则表示断言成功,为false则表示断言失败。

举个例子:

func main() {
	var x interface{}
	x = "Hello 沙河"
	v, ok := x.(string)
	if ok {
		fmt.Println(v)
	} else {
		fmt.Println("类型断言失败")
	}
}

上面的示例中如果要断言多次就需要写多个if判断,这个时候我们可以使用switch语句来实现:

func justifyType(x interface{}) {
	switch v := x.(type) {
	case string:
		fmt.Printf("x is a string,value is %v\n", v)
	case int:
		fmt.Printf("x is a int is %v\n", v)
	case bool:
		fmt.Printf("x is a bool is %v\n", v)
	default:
		fmt.Println("unsupport type!")
	}
}

因为空接口可以存储任意类型值的特点,所以空接口在Go语言中的使用十分广泛。

关于接口需要注意的是,只有当有两个或两个以上的具体类型必须以相同的方式进行处理时才需要定义接口。不要为了接口而写接口,那样只会增加不必要的抽象,导致不必要的运行时损耗。

package main

import (
	"fmt"
)

func main(){
	var x interface{}
	x="hello"
	ret,ok :=x.(string)//x 是接口类型的
	if !ok{
		fmt.Println("不是字符串类型")
	}else {
		fmt.Printf("%v是字符串类型\n",ret)
	}
    
    //switchd
	switch t:=x.(type) {
	case string:
		fmt.Printf("%v是字符串类型",t)

	}
}

并发

  • 并发:同一时间段内执行多个任务(你在用微信和两个女朋友聊天)。

  • 并行:同一时刻执行多个任务(你和你朋友都在用微信和女朋友聊天)。

  • Go语言的并发通过goroutine实现。goroutine类似于线程,属于用户态的线程,我们可以根据需要创建成千上万个goroutine并发工作。goroutine是由Go语言的运行时(runtime)调度完成,而线程是由操作系统调度完成。

  • Go语言还提供channel在多个goroutine间进行通信。goroutinechannel是 Go 语言秉承的 CSP(Communicating Sequential Process)并发模式的重要实现基础。

单个或多个goroutine的使用

func hello() {
  fmt.Println("Hello Goroutine!")
}
func main() {
  hello()
  fmt.Println("main goroutine done!")
}

但是我们发现程序并没有打印Hello Goroutine!这是因为main()函数已经结束了,并没有等待gorouine的函数,提前退出main()。为了解决这个问题,可以有两种结局方案。

  • 利用time.Sleep来使main函数等待goroutine

    func main() {
    	go hello() // 启动另外一个goroutine去执行hello函数
    	fmt.Println("main goroutine done!")
        time.Sleep(time.Second)//time.Sleep(time.XXXX*number)
    
  • 利用sync的wait.WaitGoup来解决并发问题(多个goroutie的使用)

    在代码中生硬的使用time.Sleep肯定是不合适的,Go语言中可以使用sync.WaitGroup来实现并发任务的同步。 sync.WaitGroup有以下几个方法:

    方法名功能
    (wg * WaitGroup) Add(delta int)计数器+delta
    (wg *WaitGroup) Done()计数器-1
    (wg *WaitGroup) Wait()阻塞直到计数器变为0

    sync.WaitGroup内部维护着一个计数器,计数器的值可以增加和减少。例如当我们启动了N 个并发任务时,就将计数器值增加N。每个任务完成时通过调用Done()方法将计数器减1。通过调用Wait()来等待并发任务执行完,当计数器值为0时,表示所有并发任务已经完成。

var wg sync.WaitGroup

func hello(i int) {
	defer wg.Done() // goroutine结束就登记-1
	fmt.Println("Hello Goroutine!", i)
}
func main() {

	for i := 0; i < 10; i++ {
		wg.Add(1) // 启动一个goroutine就登记+1
		go hello(i)
	}
	wg.Wait() // 等待所有登记的goroutine都结束
}

goroutine与线程

  • Go运行时的调度器使用GOMAXPROCS参数来确定需要使用多少个OS线程来同时执行Go代码。默认值是机器上的CPU核心数。例如在一个8核心的机器上,调度器会把Go代码同时调度到8个OS线程上(GOMAXPROCS是m:n调度中的n)。

  • Go语言中可以通过runtime.GOMAXPROCS()函数设置当前程序并发时占用的CPU逻辑核心数。

  • Go1.5版本之前,默认使用的是单核心执行。Go1.5版本之后,默认使用全部的CPU逻辑核心数。

​ 我们可以通过将任务分配到不同的CPU逻辑核心上实现并行的效果,这里举个例子:

func a() {
	for i := 1; i < 10; i++ {
		fmt.Println("A:", i)
	}
}

func b() {
	for i := 1; i < 10; i++ {
		fmt.Println("B:", i)
	}
}

func main() {
	runtime.GOMAXPROCS(1)
	go a()
	go b()
	time.Sleep(time.Second)
}

两给核心数只有一个逻辑核心,此时是做完一个任务再继续另一个任务。

func a() {
	for i := 1; i < 10; i++ {
		fmt.Println("A:", i)
	}
}

func b() {
	for i := 1; i < 10; i++ {
		fmt.Println("B:", i)
	}
}

func main() {
	runtime.GOMAXPROCS(2)
	go a()
	go b()
	time.Sleep(time.Second)
}
package main

import (
   "fmt"
   "runtime"
   "sync"
)

func a() {
   for i := 0; i < 10; i++ {
      fmt.Println("A", i)
   }
   wg.Done()
}

var wg sync.WaitGroup//

func b() {
   for i := 0; i < 10; i++ {
      fmt.Println("B", i)
   }
   wg.Done()
}

func main() {
   runtime.GOMAXPROCS(5)
   wg.Add(2)
   go a()
   go b()
   wg.Wait()
}

channel通道

  • 如果说goroutine是Go程序并发的执行体,channel就是它们之间的连接。channel是可以让一个goroutine发送特定值到另一个goroutine的通信机制。

  • Go 语言中的通道(channel)是一种特殊的类型。通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。每一个通道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型。

channel类型

channel是一种类型,一种引用类型。声明通道类型的格式如下:

var 变量 chan 元素类型

举几个例子:

var ch1 chan int   // 声明一个传递整型的通道
var ch2 chan bool  // 声明一个传递布尔型的通道
var ch3 chan []int // 声明一个传递int切片的通道

创建channel

通道是引用类型其空值为nil

channel 也需要通过内置函数make来进行初始化(例如其他的slice map 也需要make进行初始化)

var ch chan int
fmt.Println(ch) // <nil>

创建channel的格式如下:

make(chan 元素类型, [缓冲大小])

channel的缓冲大小是可选的。

举几个例子:

ch4 := make(chan int)
ch5 := make(chan bool)
ch6 := make(chan []int)

channel操作

通道有发送(send)、接收(receive)和关闭(close)三种操作。

发送和接收都使用<-符号。

现在我们先使用以下语句定义一个通道:

ch := make(chan int)
发送

将一个值发送到通道中。

ch <- 10 // 把10发送到ch中
接收

从一个通道中接收值。

x := <- ch // 从ch中接收值并赋值给变量x
<-ch       // 从ch中接收值,忽略结果
关闭

我们通过调用内置的close函数来关闭通道。

close(ch)

关于关闭通道需要注意的事情是,只有在通知接收方goroutine所有的数据都发送完毕的时候才需要关闭通道。通道是可以被垃圾回收机制回收的,它和关闭文件是不一样的,在结束操作之后关闭文件是必须要做的,但关闭通道不是必须的。

关闭后的通道有以下特点:

  1. 对一个关闭的通道再发送值就会导致panic。
  2. 对一个关闭的通道进行接收会一直获取值直到通道为空。
  3. 对一个关闭的并且没有值的通道执行接收操作会得到对应类型的零值。
  4. 关闭一个已经关闭的通道会导致panic。

无缓冲的通道

无缓冲的通道又称为阻塞的通道。我们来看一下下面的代码:

func main() {
	ch := make(chan int)
	ch <- 10
	fmt.Println("发送成功")
}

上面这段代码能够通过编译,但是执行的时候会出现以下错误:

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.main()
        .../src/github.com/Q1mi/studygo/day06/channel02/main.go:8 +0x54

为什么会出现deadlock错误呢?

因为我们使用ch := make(chan int)创建的是无缓冲的通道,无缓冲的通道只有在有人接收值的时候才能发送值。就像你住的小区没有快递柜和代收点,快递员给你打电话必须要把这个物品送到你的手中,简单来说就是无缓冲的通道必须有接收才能发送。

上面的代码会阻塞在ch <- 10这一行代码形成死锁,那如何解决这个问题呢?

一种方法是启用一个goroutine去接收值,例如:

func recv(c chan int) {
	ret := <-c
	fmt.Println("接收成功", ret)
}
func main() {
	ch := make(chan int)
	go recv(ch) // 启用goroutine从通道接收值
	ch <- 10
	fmt.Println("发送成功")
}

无缓冲通道上的发送操作会阻塞,直到另一个goroutine在该通道上执行接收操作,这时值才能发送成功,两个goroutine将继续执行。相反,如果接收操作先执行,接收方的goroutine将阻塞,直到另一个goroutine在该通道上发送一个值。

使用无缓冲通道进行通信将导致发送和接收的goroutine同步化。因此,无缓冲通道也被称为同步通道

有缓冲的通道

解决上面问题的方法还有一种就是使用有缓冲区的通道。我们可以在使用make函数初始化通道的时候为其指定通道的容量,例如:

func main() {
	ch := make(chan int, 1) // 创建一个容量为1的有缓冲区通道
	ch <- 10
	fmt.Println("发送成功")
}

只要通道的容量大于零,那么该通道就是有缓冲的通道,通道的容量表示通道中能存放元素的数量。就像你小区的快递柜只有那么个多格子,格子满了就装不下了,就阻塞了,等到别人取走一个快递员就能往里面放一个。

我们可以使用内置的len函数获取通道内元素的数量,使用cap函数获取通道的容量,虽然我们很少会这么做。

for range从通道循环取值

当向通道中发送完数据时,我们可以通过close函数来关闭通道。

当通道被关闭时,再往该通道发送值会引发panic,从该通道取值的操作会先取完通道中的值,再然后取到的值一直都是对应类型的零值。那如何判断一个通道是否被关闭了呢?

我们来看下面这个例子:

// channel 练习
func main() {
	ch1 := make(chan int)
	ch2 := make(chan int)
	// 开启goroutine将0~100的数发送到ch1中
	go func() {
		for i := 0; i < 100; i++ {
			ch1 <- i
		}
		close(ch1)
	}()
	// 开启goroutine从ch1中接收值,并将该值的平方发送到ch2中
	go func() {
		for {
			i, ok := <-ch1 // 通道关闭后再取值ok=false
			if !ok {
				break
			}
			ch2 <- i * i
		}
		close(ch2)
	}()
	// 在主goroutine中从ch2中接收值打印
	for i := range ch2 { // 通道关闭后会退出for range循环
		fmt.Println(i)
	}
}
package main

import (
   "fmt"
   "runtime"
)

func main() {
   runtime.GOMAXPROCS(2)
   ch1 :=make(chan int,100)
   ch2 :=make(chan int ,200)

   go func () {
      for i := 0; i < 10; i++ {
         ch1<-i
      }
      close(ch1)
   }()

   go func () {
      for{
         tmp,ok:=<-ch1
         if !ok{
            break
         }
         ch2<-tmp*tmp
      }
      close(ch2)
   }()


   for value :=range ch2{
      fmt.Println(value)
   }
}

单向通道

有的时候我们会将通道作为参数在多个任务函数间传递,很多时候我们在不同的任务函数中使用通道都会对其进行限制,比如限制通道在函数中只能发送或只能接收。

Go语言中提供了单向通道来处理这种情况。例如,我们把上面的例子改造如下:

func counter(out chan<- int) {
	for i := 0; i < 100; i++ {
		out <- i
	}
	close(out)
}

func squarer(out chan<- int, in <-chan int) {
	for i := range in {
		out <- i * i
	}
	close(out)
}
func printer(in <-chan int) {
	for i := range in {
		fmt.Println(i)
	}
}

func main() {
	ch1 := make(chan int)
	ch2 := make(chan int)
	go counter(ch1)
	go squarer(ch2, ch1)
	printer(ch2)
}

其中,

  • chan<- int是一个只写单向通道(只能对其写入int类型值),可以对其执行发送操作但是不能执行接收操作;
  • <-chan int是一个只读单向通道(只能从其读取int类型值),可以对其执行接收操作但是不能执行发送操作。

在函数传参及任何赋值操作中可以将双向通道转换为单向通道,但反过来是不可以的。

自打单向通道

package main

import (
   "fmt"
   "time"
)

func worker(id int, jobs <-chan int, results chan<- int) {
   for job := range jobs {
      fmt.Printf("worker;%dstart job %d\n", id, job)
      results <- job * 2
      time.Sleep(time.Millisecond * 500)
      fmt.Printf("worker:%d stop jpb%d\n", id, job)
   }

}
func main() {

   jobs := make(chan int, 100)
   results := make(chan int, 200)
   for i := 0; i < 3; i++ {
      go worker(i, jobs, results)
   }
   for j := 0; j < 5; j++ {
      jobs <- j
   }
   close(jobs)
   for i:=0;i<5;i++ {
      ret:= <-results
      fmt.Println(ret)
   }
}

通道总结

channel常见的异常总结,如下图:channel异常总结

关闭已经关闭的channel也会引发panic

记住应该在生产者的地方关闭channel,而不是消费的地方去关闭它,这样容易引起panic

worker pool(goroutine池)

在工作中我们通常会使用可以指定启动的goroutine数量–worker pool模式,控制goroutine的数量,防止goroutine泄漏和暴涨。

一个简易的work pool示例代码如下:

func worker(id int, jobs <-chan int, results chan<- int) {
	for j := range jobs {
		fmt.Printf("worker:%d start job:%d\n", id, j)
		time.Sleep(time.Second)
		fmt.Printf("worker:%d end job:%d\n", id, j)
		results <- j * 2
	}
}


func main() {
	jobs := make(chan int, 100)
	results := make(chan int, 100)
	// 开启3个goroutine
	for w := 1; w <= 3; w++ {
		go worker(w, jobs, results)
	}
	// 5个任务
	for j := 1; j <= 5; j++ {
		jobs <- j
	}
	close(jobs)
	// 输出结果
	for a := 1; a <= 5; a++ {
		<-results
	}
}

select多路复用

select的使用:

func main() {
	ch := make(chan int, 1)
	for i := 0; i < 10; i++ {
		select {
		case x := <-ch:
			fmt.Println(x)
		case ch <- i:
		}
	}
  package main

import (
   "fmt"
)

func main() {

   ch1 := make(chan int, 100)
   for i := 0; i < 10; i++ {
      select {//随机选择以下进行操作
      case x := <-ch1:
         fmt.Println(x)
      case ch1 <- i:
      default:
         fmt.Println("啥也没干")

      }
   }

}

使用select语句能提高代码的可读性。

  • 可处理一个或多个channel的发送/接收操作。

  • 如果多个case同时满足,select会随机选择一个。

  • 对于没有caseselect{}会一直等待,可用于阻塞main函数。

并发安全和锁

互斥锁

package main

import (
   "fmt"
   "sync"

)

var (
   wg sync.WaitGroup
   x  int64
   lock sync.Mutex
)

func add() {

   for i := 0; i < 5000000; i++ {
      lock.Lock()
      x = x + 1
      lock.Unlock()
   }
   wg.Done()
}

func main() {
   wg.Add(2)
   go add()
   go add()
   wg.Wait()
   fmt.Println(x)
}

读写锁

package main

import (
	"fmt"
	"sync"
	"time"
)

var (
	wg sync.WaitGroup
	x  int64
	lock sync.RWMutex
)

func read() {
	lock.RLock()
	time.Sleep(time.Millisecond)
	lock.RUnlock()
	wg.Done()
}
func  write()  {
	lock.Lock()
	x=x+1
	time.Sleep(time.Millisecond*10)
	lock.Unlock()
	wg.Done()
}

func main() {
	start :=time.Now()
	for i:=0;i<100;i++{
		wg.Add(1)
		go read()
	}
	for i:=0;i<10;i++{
		wg.Add(1)
		go write()
	}
	wg.Wait()
	end :=time.Now()
	fmt.Println(end.Sub(start))
}

sync.Map

Go语言中内置的map不是并发安全的。请看下面的示例:

var m = make(map[string]int)

func get(key string) int {
	return m[key]
}

func set(key string, value int) {
	m[key] = value
}

func main() {
	wg := sync.WaitGroup{}
	for i := 0; i < 20; i++ {
		wg.Add(1)
		go func(n int) {
			key := strconv.Itoa(n)
			set(key, n)
			fmt.Printf("k=:%v,v:=%v\n", key, get(key))
			wg.Done()
		}(i)
	}
	wg.Wait()
}

上面的代码开启少量几个goroutine的时候可能没什么问题,当并发多了之后执行上面的代码就会报fatal error: concurrent map writes错误。

像这种场景下就需要为map加锁来保证并发的安全性了,Go语言的sync包中提供了一个开箱即用的并发安全版map–sync.Map。开箱即用表示不用像内置的map一样使用make函数初始化就能直接使用。同时sync.Map内置了诸如StoreLoadLoadOrStoreDeleteRange等操作方法。

var m = sync.Map{}//进行初始化

func main() {
	wg := sync.WaitGroup{}
	for i := 0; i < 20; i++ {
		wg.Add(1)
		go func(n int) {
			m.Store(i, n)
			value, _ := m.Load(i)
			fmt.Printf("k=:%v,v:=%v\n",i, value)
			wg.Done()
		}(i)
	}
	wg.Wait()
}

atomic包

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kyWAtvX5-1634737066029)(C:\Users\17250\AppData\Roaming\Typora\typora-user-images\image-20211010094604244.png)]

ackage main

import (
	"fmt"
	"sync"
	"sync/atomic"
	"time"
)

type Counter interface {
	Inc()
	Load() int64
}

// 普通版
type CommonCounter struct {
	counter int64
}

func (c CommonCounter) Inc() {
	c.counter++
}

func (c CommonCounter) Load() int64 {
	return c.counter
}

// 互斥锁版
type MutexCounter struct {
	counter int64
	lock    sync.Mutex
}

func (m *MutexCounter) Inc() {
	m.lock.Lock()
	defer m.lock.Unlock()
	m.counter++
}

func (m *MutexCounter) Load() int64 {
	m.lock.Lock()
	defer m.lock.Unlock()
	return m.counter
}

// 原子操作版
type AtomicCounter struct {
	counter int64
}

func (a *AtomicCounter) Inc() {
	atomic.AddInt64(&a.counter, 1)
}

func (a *AtomicCounter) Load() int64 {
	return atomic.LoadInt64(&a.counter)
}

func test(c Counter) {
	var wg sync.WaitGroup
	start := time.Now()
	for i := 0; i < 1000; i++ {
		wg.Add(1)
		go func() {
			c.Inc()
			wg.Done()
		}()
	}
	wg.Wait()
	end := time.Now()
	fmt.Println(c.Load(), end.Sub(start))
}

func main() {
	c1 := CommonCounter{} // 非并发安全
	test(c1)
	c2 := MutexCounter{} // 使用互斥锁实现并发安全
	test(&c2)
	c3 := AtomicCounter{} // 并发安全且比互斥锁效率更高
	test(&c3)
}

atomic包提供了底层的原子级内存操作,对于同步算法的实现很有用。这些函数必须谨慎地保证正确使用。除了某些特殊的底层应用,使用通道或者sync包的函数/类型实现同步更好。

网络编程

osi七层模型

tcp通信

TCP协议

TCP/IP(Transmission Control Protocol/Internet Protocol) 即传输控制协议/网间协议,是一种面向连接(连接导向)的、可靠的、基于字节流的传输层(Transport layer)通信协议,因为是面向连接的协议,数据像水流一样传输,会存在黏包问题。

TCP服务端

一个TCP服务端可以同时连接很多个客户端,例如世界各地的用户使用自己电脑上的浏览器访问淘宝网。因为Go语言中创建多个goroutine实现并发非常方便和高效,所以我们可以每建立一次链接就创建一个goroutine去处理。

TCP服务端程序的处理流程:

  1. 监听端口
  2. 接收客户端请求建立链接
  3. 创建goroutine处理链接。

我们使用Go语言的net包实现的TCP服务端代码如下:

// tcp/server/main.go

// TCP server端

// 处理函数
func process(conn net.Conn) {
	defer conn.Close() // 关闭连接
	for {
		reader := bufio.NewReader(conn)
		var buf [128]byte
		n, err := reader.Read(buf[:]) // 读取数据
		if err != nil {
			fmt.Println("read from client failed, err:", err)
			break
		}
		recvStr := string(buf[:n])
		fmt.Println("收到client端发来的数据:", recvStr)
		conn.Write([]byte(recvStr)) // 发送数据
	}
}

func main() {
	listen, err := net.Listen("tcp", "127.0.0.1:20000")
	if err != nil {
		fmt.Println("listen failed, err:", err)
		return
	}
	for {
		conn, err := listen.Accept() // 建立连接
		if err != nil {
			fmt.Println("accept failed, err:", err)
			continue
		}
		go process(conn) // 启动一个goroutine处理连接
	}
}

将上面的代码保存之后编译成serverserver.exe可执行文件。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值