一文掌握GO语言实战技能(三)

【云原生 37】Docker快速部署编程语言Golang_docker go运行环境 🏅云计算领域优质创作者🏅。 阅读详情
  • Go 接口

  • Go 反射详解

  • Go 并发编程

  • Go Channel介绍

  • Go Worker池的实现

  • Go Select语义介绍和使用

  • Go 互斥锁介绍

  • Go 读写锁介绍

  • Go 读写锁和互斥锁性能比较

  • Go 原子操作

GO 接口

接口的介绍和定义

  • 接口定义了一个对象的行为规范

    a. 只定义规范,不实现

    b. 具体的对象需要实现规范的细节

  • Go 中接口定义

    a. type 接口名字 interface

    b. 接口里面是一组方法签名的集合,后面的调用参数和返回值都要和接口中的方法一模一样

    c. 接口是引用类型(指针);注意:函数传递接口类型参数时,一定不要加*,因为接口本身就是引用类型,如果加*就报错了

type Animal interface { 
  Talk()
  Eat() int
  Run() 
}
  • 接口的实现

a. 一个类只要实现了接口要求的所有函数,我们就说这个类实现了该接口

b. 接口类型的变量可以保存实现该接口的任何具体类型的实例。

package main

import (
 "fmt"
)

type Animal interface { //定义了动物的规范(接口定义的一组方法)
 Talk()
 Eat()
 Name() string
}

type Dog struct { //狗如果能够满足了动物的规范(接口方法),那其就是动物
}

func (d Dog) Talk() { //目前狗还不是动物,因为其只实现了Eat,还需要实现Eat和Name,其才算是动物
 fmt.Println("汪汪汪")
}

func (d Dog) Eat() {
 fmt.Println("我在吃骨头")
}

func (d Dog) Name() string {
 fmt.Println("我的名字叫旺财")
 return "旺财"
}

type Pig struct {
}

func (d Pig) Talk() {
 fmt.Println("坑坑坑")
}

func (d Pig) Eat() {
 fmt.Println("我在吃草")
}

func (d Pig) Name() string {
 fmt.Println("我的名字叫猪八戒")
 return "猪八戒"
}

func testInterface1() {
 var d Dog
 var a Animal
 a = &d //dog满足了接口所有方法,所以我们可以直接将其复制给Animal,对应的理论就是接口类型的变量可以保存实现该接口的任何具体类型的实例。

 // fmt.Printf("a:%v dog:%v\n", a, dog) //接口底层就是指针,指向的就是一个空对象,如果字节调用就会panic
 a.Eat()
 a.Talk()
 a.Name()

 var pig Pig
 a = &pig
 a.Eat()
 a.Talk()
 a.Name()

 fmt.Printf("%T %v\n", a, a)
}
func main() {
 testInterface1()

 //testInterface2()
}
// 我在吃骨头
// 汪汪汪
// 我的名字叫旺财
// 我在吃草
// 坑坑坑
// 我的名字叫猪八戒
// *main.Pig &{}
  • 空接口

a. 空接口没有定义任何方法

b. 任何类型都实现了空接口, 空接口可以存储 任何类型,就是一个容器

package main

import (
 "fmt"
)

func describe(a interface{}) {
 fmt.Printf("%T %v\n", a, a)
}

func testInterface1() {
 var a interface{}
 var b int = 100
 a = b

 fmt.Printf("%T %v\n", a, a)

 var c string = "hello"
 a = c
 fmt.Printf("%T %v\n", a, a)

 var d map[string]int = make(map[string]int, 100)
 d["abc"] = 1000
 d["eke"] = 30

 a = d
 fmt.Printf("%T %v\n", a, a)
}

type Student struct {
 Name string
 Sex  int
}

func main() {

 a := 65
 describe(a)

 str := "hello"
 describe(str)

 var stu Student = Student{
  Name: "user01",
  Sex:  1,
 }

 describe(stu)
  // testInterface1
}
// int 65
// string hello
// main.Student {user01 1}
  • 类型断言

a. 如何获取接口类型里面存储的具体的值呢?

  1. 使用a.(类型的值),ok用来判断是否正确,引入 ok判断机制! v, ok := i.(T)。如果没有ok,程序会挂掉。

package main

import (
 "fmt"
)

func test(a interface{}) {

 // s := a.(int)
 s, ok := a.(int)
 if ok {
  fmt.Println(s)
  return
 }

 str, ok := a.(string)
 if ok {
  fmt.Println(str)
  return
 }

 f, ok := a.(float32)
 if ok {
  fmt.Println(f)
  return
 }

 fmt.Println("can not define the type of a")
}

func testInterface1() {
 var a int = 100
 test(a)

 var b string = "hello"
 test(b)
}
  1. type switch。转换为2次,解决办法是 v:=a.(type)

func testSwitch(a interface{}) {
 switch a.(type) {
 case string:
  fmt.Printf("a is string, value:%v\n", a.(string))
 case int:
  fmt.Printf("a is int, value:%v\n", a.(int))
 case int32:
  fmt.Printf("a is int, value:%v\n", a.(int))
 default:
  fmt.Println("not support type\n")
 }
}

func testSwitch2(a interface{}) {
 switch v := a.(type) {
 case string:
  fmt.Printf("a is string, value:%v\n", v)
 case int:
  fmt.Printf("a is int, value:%v\n", v)
 case int32:
  fmt.Printf("a is int, value:%v\n", v)
 default:
  fmt.Println("not support type\n")
 }
}

func testInterface2() {
 var a int = 100
 testSwitch(a)
 var b string = "hello"
 testSwitch(b)
}

func testInterface3() {
 var a int = 100
 testSwitch2(a)
 var b string = "hello"
 testSwitch2(b)
}

func main() {
 testInterface1()
 //testInterface2()
}
  • 指针接收

package main

import "fmt"

type coder interface {
 code()
 debug()
}

type Gopher struct {
 language string
}

//值接收
func (p Gopher) code() {
 fmt.Printf("I am coding %s language\n", p.language)
}

//指针接收
func (p *Gopher) debug() {
 fmt.Printf("I am debuging %s language\n", p.language)
}

func main() {
 // var c coder = &Gopher{"Go"} //这种写法没有问题
 var c coder = Gopher{"Go"} //这种写法会报错,因为Gopher没有全部实现coder接口。表面上看, *Gopher类型也没有实现code方法,但是因为Gopher类型实现了code方法,所以让*Gopher类型自动拥有了code方法
 c.code()
 // c.debug()
}

使用指针作的理由:

  1. 方法能够修改指向的值。

  2. 避免在每次调用方法时复制该值,在值的类型为大型结构体时,这样做会更加高效。

是使用值接收还是指针接收,不是由该方法是否修改了调用者(也就是接收者)来决定,而是应该基于该类型的本质。

如果类型具备“原始的本质”,也就是说它的成员都是由 Go 语言里内置的原始类型,如字符串,整型值等,那就定义值接收者类型的方法。像内置的引用类型,如 slice,map,interface,channel,这些类型比较特殊,声明他们的时候,实际上是创建了一个 header, 对于他们也是直接定义值接收者类型的方法。这样,调用函数时,是直接 copy 了这些类型的header,而header本身就是为复制设计的。

如果类型具备非原始的本质,不能被安全地复制,这种类型总是应该被共享,那就定义指针接收者的方法。比如 go 源码里的文件结构体(struct File)就不应该被复制,应该只有一份实体。

// 方式一: 值类型实现
// 值类型实现了Dog, (d Dog) Eat() 形式,那么我们既可以把Dog以值类型( var d Dog  a = d a.Eat())给Animal,也可以以指针类型给 d,那么这时候在调用时候接口做了转化(*(&Dog).Eat(),&Dog为指针类型,加*转换为值类型)
package main

import (
 "fmt"
)

type Animal interface {
 Talk()
 Eat()
 Name() string
}

type Dog struct {
}
// 值类型实现
func (d Dog) Talk() {
 fmt.Println("汪汪汪")
}
// 值类型实现
func (d Dog) Eat() {
 fmt.Println("我在吃骨头")
}
// 值类型实现
func (d Dog) Name() string {
 fmt.Println("我的名字叫旺财")
 return "旺财"
}

func main() {
 var a Animal
 var d Dog

 //如果一个变量存储在接口类型的变量中之后,那么不能获取这个变量的地址
 a = d
 a.Eat()
 // 值类型实现了一个接口,指针类型可以存储进去
 // 指针类型实现了一个接口,值类型存储存储不进去

 fmt.Printf("%T %v\n", a, a)

 var d1 *Dog = &Dog{}
 a = d1
 //*(&Dog).Eat() // 指针类型存入调用方法 会做这种转换
 a.Eat()
 fmt.Printf("*Dog %T %v\n", a, a)
}
// 方式一: 指针类型实现
// 如果指针类型实现了接口,a存的是值类型的Dog,那么调用a.Eat(),首先取&Dog.Eat(),但是一个变量存储在接口变量,那么就不能获取这个变量地址,所以就调取不到这个接口,所以,此时我们如果是指针类型实现了这个接口,我们只能存指针的Dog

// 值类型实现了一个接口,指针类型可以存储进去
// 指针类型实现了一个接口,值类型存储存储不进去
package main

import (
 "fmt"
)

type Animal interface {
 Talk()
 Eat()
 Name() string
}

type Dog struct {
}

func (d *Dog) Talk() {
 fmt.Println("汪汪汪")
}

func (d *Dog) Eat() {
 fmt.Println("我在吃骨头")
}

func (d *Dog) Name() string {
 fmt.Println("我的名字叫旺财")
 return "旺财"
}

func main() {
 var a Animal
 var d Dog
 a = &d // 存的指针类型的Dog
 a.Eat()
 // 值类型实现了一个接口,指针类型可以存储进去
 // 指针类型实现了一个接口,值类型存储存储不进去

 fmt.Printf("%T %v\n", a, a)

 var d1 *Dog = &Dog{}
 a = d1
 //*(&Dog).Eat() 使用的是指针类型那么定义时候使用
 a.Eat()
 fmt.Printf("*Dog %T %v\n", a, a)
}

  • 同一类型实现多接口,Animal 和PuruDongWu 都可以实现d,Dog即实现可taisheng的接口,也实现animal接口

package main

import (
 "fmt"
)

type Animal interface {
 Talk()
 Eat()
 Name() string
}

type PuruDongWu interface {
 TaiSheng()
}

type Dog struct {
}

func (d Dog) Talk() {
 fmt.Println("汪汪汪")
}

func (d Dog) Eat() {
 fmt.Println("我在吃骨头")
}

func (d Dog) Name() string {
 fmt.Println("我的名字叫旺财")
 return "旺财"
}

func (d Dog) TaiSheng() {
 fmt.Println("狗是胎生的")
}

func main() {
 var d Dog
 var a Animal
  // 开始时 是nil
 fmt.Println("%v %T %p", a, a, a)   // nil nil nil

 if a == nil {
  //a.Eat()
  fmt.Println("a is nil")
 }

 a = d
 a.Eat()

 var b PuruDongWu
 b = d
 b.TaiSheng()
}
  • 接口嵌套,和结构体嵌套类似

package main

import "fmt"

type Animal interface {
 Eat()
 Talk()
 Name() string
}

type Describle interface {
 Describle() string
}

type AdvanceAnimal interface {
 Animal
 Describle
}

type Dog struct {
}

func (d Dog) Eat() {
 fmt.Println("dog is eating")
}

func (d Dog) Talk() {
 fmt.Println("dog is talking")
}

func (d Dog) Name() string {
 fmt.Println("my name is dog")
 return "dog"
}

func (d Dog) Describle() string {
 fmt.Println("dog is a dog")
 return "dog is a dog"
}

func main() {
 var d Dog
 var a AdvanceAnimal

 a = d
 a.Describle()
 a.Eat()
 a.Talk()
 a.Name()
}

Go 反射详解

反射: 动态获取运行时信息

  • 变量的介绍

  1. 变量的内在机制

a. 类型信息,这部分是元信息,是预先定义好的

b. 值类型,这部分是程序运行过程中,动态改变的

var arr [10]int 
arr[0] = 10 
arr[1] = 20 
arr[2] = 30 
arr[3] = 40 
arr[4] = 50

type Animal struct { 
  Name string
  age int 
}
var a Animal
  1. 反射与空接口

a. 空接口可以存储任何类型的变量

b. 那么给你一个空接口,怎么判断存储的是什么东西? 类型断言判断

c. 在运行时动态的获取一个变量的类型信息和值信息,就叫反射

  1. 怎么分析?

a. 内置包 reflect

b. 获取类型信息: reflect.TypeOf

c. 获取值信息: reflect.ValueOf

d. 获取变量的类型: Type.Kind()

  • 通过反射设置变量的值

  • 获取结构体类型相关信息

package main

import (
  "fmt"
  "reflect"
)
type S struct {
  A int
  B string
}
func main() {
  s := S{23, "skidoo"}
  v := reflect.ValueOf(s)
  t := v.Type()
  for i := 0; i < v.NumField(); i++ {
      f := v.Field(i)
      fmt.Printf("%d: %s %s = %v\n", i,
      t.Field(i).Name, f.Type(), f.Interface())
} }

// 0: A int = 23
// 1: B string = skidoo
  • 设置结构体相关字段的值

v.Elem() 获取的是结构体

v.Elem().Field(0) 获取的就是第一个值 A

v.FieldByName("SetA") 也可以获取对应的值

package main 

import (
  "fmt"
  "reflect"
)
type S struct {
  A int
  B string
}
func main() {
  s := S{23, "skidoo"}
  v := reflect.ValueOf(&s)
  t := v.Type()
  v.Elem().Field(0).SetInt(100)
  // 等价 v.Elem().FieldByName("A").SetInt(100)
  for i := 0; i < v.Elem().NumField(); i++ {
  f := v.Elem().Field(i)
  fmt.Printf("%d: %s %s = %v\n", i,t.Elem().Field(i).Name, f.Type(), f.Interface())
} }
  • 获取结构体的方法信息

package main

import (
  "fmt"
  "reflect"
)
type S struct {
  A int
  B string
}
func (s *S) Test() {
   fmt.Println("this is a test") 
}
func main() {
  s := S{23, "skidoo"}
  v := reflect.ValueOf(&s)
  t := v.Type() 
  v.Elem().Field(0).SetInt(100) 
  fmt.Println("method num:", v.NumMethod()) 
  for i := 0; i < v.NumMethod(); i++ {
      // t获取结构体方法信息,如果调用就是v.method
      f := t.Method(i)
      fmt.Printf("%d method, name:%v, type:%v\n", i, f.Name, f.Type) 
  }
}
  • 获取结构体中tag信息

package main

import (
 "fmt"
 "reflect"
)

type Student struct {
 Name  string `json:"name" db:"name"`
 Sex   int
 Age   int
 Score float32
 //xxx   int
}

func (s *Student) SetName(name string) {
 s.Name = name
}

func (s *Student) Print() {
 fmt.Printf("通过反射进行调用:%#v\n", s)
}

func main() {
 var s Student
 s.SetName("xxx")
 //SetName(&s, "xxx")
 v := reflect.ValueOf(&s)
 t := v.Type()
 //t := reflect.TypeOf(s)

 field0 := t.Elem().Field(0)
 fmt.Printf("tag json=%s\n", field0.Tag.Get("json"))
 fmt.Printf("tag db=%s\n", field0.Tag.Get("db"))

 //json.UnMa
 var s string
}
  • 反射总结: 反射是在运行时动态的获取一个变量的类型信息和值信息

  • 应用场景

a. 序列化和反序列化,比如json, protobuf等各种数据协议

b. 各种数据库的ORM, 比如gorm,sqlx等数据库中间件

c. 配置文件解析相关的库, 比如yaml、ini等

总之一句话,在一些场景,实现都不知道数据的具体类型,用反射.

Go 并发编程

  • 并发与并行

a. 并发:同一时间段内执行多个操作。

b. 并行:同一时刻执行多个操作。

  • 多线程

a. 线程是由操作系统进行管理,也就是处于内核态。

b. 线程之间进行切换,需要发生用户态到内核态的切换。

c. 当系统中运行大量线程,系统会变的非常慢。

d. 用户态的线程,支持大量线程创建。也叫协程或goroutine。

  • 创建goroutine

package main

import (
 "fmt"
 "time"
)

func hello() {
 fmt.Println("hello goroutine")
}

func main() {
 go hello()
 fmt.Println("main thread terminate")
 time.Sleep(time.Second)
}
这个例子中很少会打印出 fmt.Println("hello goroutine"),因为 hello 是单个线程,开了单个线程
所以是和 main 打印一起进行的

修复后:

package main

import (
 "fmt"
 "time"
)

func hello(i int) {
 fmt.Println("hello goroutine", i)
}

func main() {
 for i := 0; i < 10; i++ {
  go hello(i)
 }
 time.Sleep(time.Second)
}

// hello goroutine 8
// hello goroutine 7
// hello goroutine 9
// hello goroutine 0
// hello goroutine 3
// hello goroutine 1
// hello goroutine 6
// hello goroutine 4
// hello goroutine 2
// hello goroutine 5
// 每次打印出来都不一样,说明多线程执行是不一致的
  • 启动多个goroutine

主线程退出,子线程也会相应的退出

package main

import (
 "fmt"
 "time"
)

func numbers() {
 for i := 1; i <= 5; i++ {
  time.Sleep(250 * time.Millisecond)
  fmt.Printf("%d ", i)
 }
}
func alphabets() {
 for i := 'a'; i <= 'e'; i++ {
  time.Sleep(400 * time.Millisecond)
  fmt.Printf("%c ", i)
 }
}
func main() {
 go numbers()
 go alphabets()
 time.Sleep(3000 * time.Millisecond)
 fmt.Println("main terminated")
}

// 1 a 2 3 b 4 c 5 d e main terminated
  • 多核控制

a. 通过runtime包进行多核设置

b. GOMAXPROCS设置当前程序运行时占用的cpu核数

c. NumCPU获取当前系统的cpu核数

  • Goroutine原理浅析

a. 一个操作系统线程对应用户态多个goroutine

b. 同时使用多个操作系统线程

c. 操作系统线程对goroutine是多对多关系,即M:N

  • 模型抽象

a. 操作系统线程: M

b. 用户态线程(goroutine): G

c. 上下文对象:P

  • goroutine调度

  • 系统调用怎么处理

Go channel介绍

a. 本质上就是一个队列,是一个容器

b. 因此定义的时候,需要只定容器中元素的类型

c. var 变量名 chan 数据类型

  • goroutine调度 之间的通信通过channel进行通信, 定义完chan要进行初始化,不然会是nil,初始化后就可以进行插入元素了

package main

import (
 "fmt"
)

func main() {
 var c chan int
 fmt.Printf("c=%v\n", c)

 c = make(chan int, 1)
 fmt.Printf("c=%v\n", c)
 c <- 100

 /*
  data := <-c
  fmt.Printf("data:%v\n", data)
 */
 <-c
}
c=<nil>
c=0xc00005a070
  • 元素入队和出队

var a chan int

a. 入队操作,a <- 100

b. 出队操作:data := <- a

初始化时候要进行制定chan的元素,不然会出现死锁的情况

package main

import (
 "fmt"
)

func main() {
 var c chan int
 fmt.Printf("c=%v\n", c)

 c = make(chan int, 1)
 fmt.Printf("c=%v\n", c)
 c <- 100


  data := <-c
  fmt.Printf("data:%v\n", data)
 
 // <-c
}

不带缓存区的chan队列,入队时候就死锁了,只有消费者存在时候,才可以入队

package main

import (
 "fmt"
 "time"
)

func produce(c chan int) {
 c <- 1000
 fmt.Println("produce finished")
}

func consume(c chan int) {
 data := <-c
 fmt.Println(data)
}

func main() {
 var c chan int
 fmt.Printf("c=%v\n", c)

 c = make(chan int)
 go produce(c)
 go consume(c)
 time.Sleep(time.Second * 5)
}
// c=<nil>
// 1000
// produce finished
  • 使用chan来进行goroutine同步  替代sleep函数

package main

import (
 "fmt"
 "time"
)

func hello(c chan bool) {
 time.Sleep(5 * time.Second)
 fmt.Println("hello goroutine")

 c <- true
}

func main() {
 var exitChan chan bool
 exitChan = make(chan bool)
 go hello(exitChan)
 fmt.Println("main thread terminate")
 <-exitChan
}

<-exitChan起到了额阻塞主线程的功能,只有在hello线程存入之后才可以会进行取出,

如果chan没有带缓存,存的时候会阻塞,还有,加入没有存,取得时候也会阻塞

  • 单向chan

package main

import "fmt"
// 只可以存数据
func sendData(sendch chan<- int) {
 sendch <- 10
 //<-sendch
}
// 只可以取数据
func readData(sendch <-chan int) {
 //sendch <- 10
 data := <-sendch
 fmt.Println(data)
}

func main() {
 chnl := make(chan int)
 go sendData(chnl)
 readData(chnl)
}
  • chan关闭

package main

import (
 "fmt"
)

func producer(chnl chan int) {
 for i := 0; i < 10; i++ {
  chnl <- i
 }
 close(chnl)
}

func main() {
 ch := make(chan int)
 go producer(ch)
 for {
  v, ok := <-ch
  if ok == false {
   fmt.Println("chan is closed")
   break
  }
  fmt.Println("Received ", v)
 }
}
  • for range操作 程序关闭自动就会关闭,如果管道没有元素,自动阻塞

package main

import (
 "fmt"
 "time"
)

func producer(chnl chan int) {
 for i := 0; i < 10; i++ {
  chnl <- i
  time.Sleep(time.Second)
 }
 close(chnl)
}

func main() {
 ch := make(chan int)
 go producer(ch)
 for v := range ch {
  fmt.Println("receive:", v)
 }
}

  • 带缓冲区的chanel

Ch := make(chan type, capacity)

package main

import "fmt"

func main() {
 ch := make(chan string, 2)
 // var s string
 // 如果空的chan 取出时候也会阻塞,死锁
 // s = <-ch
 ch <- "hello"
 ch <- "world"
 ch <- "!"
 // 如果插入的超过缓存空间,那么久会阻塞
 
 //ch <- "test"
 s1 := <-ch
 s2 := <-ch
 // 先进先出
 fmt.Println( s1, s2)
}

  • channel的长度和容量

Ch := make(chan type, capacity)

package main

import (
 "fmt"
)

func main() {
 ch := make(chan string, 3)
 ch <- "naveen"
 ch <- "paul"
 fmt.Println("capacity is", cap(ch))
 fmt.Println("length is", len(ch))
 fmt.Println("read value", <-ch)
 fmt.Println("new length is", len(ch))
}
//  capacity is 3
//  length is 2
// read value naveen
 // new length is 1
  • 如何等待一组goroutine结束?

a. 使用不带缓冲区的channel实现

package main
import (
"fmt"
"time" )
func process(i int, ch chan bool) {
    fmt.Println("started Goroutine ", i)
    time.Sleep(2 * time.Second)
    fmt.Printf("Goroutine %d ended\n", i)
    ch <- true
}
func main() {
no := 3
exitChan := make(chan bool, no) for i := 0; i < no; i++ {
        go process(i, exitChan)
    }
    for i := 0; I < no;i++{
        <-exitChan
}
fmt.Println("All go routines finished executing") }
 

b. 使用sync.WaitGroup实现

wg *sync.WaitGroup 要传地址,如果传副本,只会修改副本的

package main

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

func process(i int, wg *sync.WaitGroup) {
   fmt.Println("started Goroutine ", i)
   time.Sleep(2 * time.Second)
   fmt.Printf("Goroutine %d ended\n", i)
   wg.Done()
}
func main() {
   no := 3
   var wg sync.WaitGroup
   wg.Wait()
   fmt.Println("wait return")
   for i := 0; i < no; i++ {
    wg.Add(1)
    go process(i, &wg)
   }
   wg.Wait()
   

Go Worker池的实现

a. 生产者、消费者模型,简单有效

b. 控制goroutine的数量,防止goroutine泄露和暴涨

c. 基于goroutine和chan,构建workerpool非常简单

计算一个数字的各个位数之和,比如123,和等于1+2+3=6 B. 需要计算的数字使用随机算法生成

方案介绍

A. 任务抽象成一个个job

B. 使用job队列和result队列

C. 开一组goroutine进行实际任务计算,并把结果放回result队列

package main

import (
 "fmt"
 "math/rand"
)

type Job struct {
 Number int
 Id     int
}

type Result struct {
 job *Job
 sum int
}

func calc(job *Job, result chan *Result) {
 var sum int
 number := job.Number
 for number != 0 {
  tmp := number % 10
  sum += tmp
  number /= 10
 }

 r := &Result{
  job: job,
  sum: sum,
 }

 result <- r
}

func Worker(jobChan chan *Job, resultChan chan *Result) {

 for job := range jobChan {
  calc(job, resultChan)
 }
}

func startWorkerPool(num int, jobChan chan *Job, resultChan chan *Result) {

 for i := 0; i < num; i++ {
  go Worker(jobChan, resultChan)
 }
}

func printResult(resultChan chan *Result) {
 for result := range resultChan {
  fmt.Printf("job id:%v number:%v result:%d\n", result.job.Id, result.job.Number, result.sum)
 }
}

func main() {

 jobChan := make(chan *Job, 1000)
 resultChan := make(chan *Result, 1000)

 startWorkerPool(128, jobChan, resultChan)

 go printResult(resultChan)
 var id int
 for {
  id++
  number := rand.Int()
  job := &Job{
   Id:     id,
   Number: number,
  }

  jobChan <- job
 }
}
// job id:1836371 number:1062421083552683812 result:67
// job id:1836372 number:6439169726753483267 result:98
// job id:1836373 number:4936444777724757493 result:103
// job id:1836374 number:596995741200900799 result:91
// job id:1836375 number:2319236362663578637 result:88
// job id:1836376 number:3956317946286456241 result:91
// job id:1836377 number:1555755986829607843 result:103
// job id:1836378 number:6569228944483682098 result:103
// job id:1836379 number:7038879116070115551 result:75
// job id:1836380 number:3382855905395583671 result:95
// job id:1836381 number:482521455470510672 result:68
// job id:1836382 number:6859849441952049848 result:107

Go select语义介绍和使用

  • 多channel场景

多个channel同时需要读取或写入,怎么办?

串行操作? NONONO

package main

import (
 "fmt"
 "time"
)

func server1(ch chan string) {
 time.Sleep(time.Second * 6)
 ch <- "response from server1"
}

func server2(ch chan string) {
 time.Sleep(time.Second * 3)
 ch <- "response from server2"
}

func main() {
 output1 := make(chan string)
 output2 := make(chan string)

 go server1(output1)
 go server2(output2)
 
  s1 := <-output1
  fmt.Println("s1:", s1)

  s2 := <-output2
  fmt.Println("s2:", s2)
 

 // select {
 // case s1 := <-output1:
 //  fmt.Println("s1:", s1)
 // case s2 := <-output2:
 //  fmt.Println("s2:", s2)
 // default:
 //  fmt.Println("run default")
 // }
}

s1 和s2 会在6s后同时输出

package main

import (
 "fmt"
 "time"
)

func server1(ch chan string) {
 time.Sleep(time.Second * 6)
 ch <- "response from server1"
}

func server2(ch chan string) {
 time.Sleep(time.Second * 3)
 ch <- "response from server2"
}

func main() {
 output1 := make(chan string)
 output2 := make(chan string)

 go server1(output1)
 go server2(output2)
 
  // s1 := <-output1
  // fmt.Println("s1:", s1)

  // s2 := <-output2
  // fmt.Println("s2:", s2)
 

 select {
 case s1 := <-output1:
  fmt.Println("s1:", s1)
 case s2 := <-output2:
  fmt.Println("s2:", s2)
 default:
  fmt.Println("run default")
 }
}
如果两个chanel 同时唤起,那么两个chanel就是随机的策略
  • select登场

A. 同时监听一个或多个channel,直到其中一个channel ready

B. 如果其中多个channel同时ready,随机选择一个进行操作。

C. 语法和switch case有点类似,代码可读性更好。

select {
    case s1 := <-output1:
        fmt.Println(s1)
    case s2 := <-output2:
}
fmt.Println(s2)
  • default分支,当case分支的channel都没有ready的话,执行default

A. 用来判断channel是否满了

B. 用来判断channel是否是空的

package main

import (
 "fmt"
 "time"
)

func write(ch chan string) {
 for {
  select {
  case ch <- "hello":
   fmt.Println("write succ")
  default:
   fmt.Println("channel is full")
  }
  time.Sleep(time.Millisecond * 500)
 }
}

func main() {
 //select {}

 output1 := make(chan string, 10)

 go write(output1)
 for s := range output1 {
  fmt.Println("recv:", s)
  time.Sleep(time.Second)
 }
}
  • empty select

package main
func main() {
    select {
} }
  • 现实例子

A. 多个goroutine同时操作一个资源,这个资源又叫临界区

B. 现实生活中的十字路口,通过红路灯实现线程安全

C. 火车上的厕所,通过互斥锁来实现线程安全

  • 实际例子, x = x +1

A. 先从内存中取出x的值

B. CPU进行计算,x+1

C. 然后把x+1的结果存储在内存中

Go 互斥锁介绍

A. 同时有且只有一个线程进入临界区,其他的线程则在等待锁

B. 当互斥锁释放之后,等待锁的线程才可以获取锁进入临界区

C. 多个线程同时等待同一个锁,唤醒的策略是随机的

读写锁介绍

A. 读写锁使用场景: 读多写少的场景

B. 分为两种角色,读锁和写锁

C. 当一个goroutine获取写锁之后,其他的goroutine获取写锁或读锁都会等待

D. 当一个goroutine获取读锁之后,其他的goroutine获取写锁都会等待, 但其他 goroutine获取读锁时,都会继续获得锁.

package main

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

var rwlock sync.RWMutex
var x int
var wg sync.WaitGroup

func write() {
   rwlock.Lock()
   fmt.Println("write lock")
   x = x + 1
   time.Sleep(10 * time.Second)
   fmt.Println("write unlock")
   rwlock.Unlock()
   wg.Done()
}

func read(i int) {
   fmt.Println("wait for rlock")
   rwlock.RLock()
   fmt.Printf("goroutine:%d x=%d\n", i, x)
   time.Sleep(time.Second)
   rwlock.RUnlock()
   wg.Done()
}

func main() {

   wg.Add(1)
   go write()
   time.Sleep(time.Millisecond * 5)
   for i := 0; i < 10; i++ {
    wg.Add(1)
    go read(i)
   }

   wg.Wait()

}

// write lock
// wait for rlock
// wait for rlock
// wait for rlock
// wait for rlock
// wait for rlock
// wait for rlock
// wait for rlock
// wait for rlock
// wait for rlock
// wait for rlock
// write unlock
// goroutine:0 x=1
// goroutine:4 x=1
// goroutine:7 x=1
// goroutine:8 x=1
// goroutine:5 x=1
// goroutine:3 x=1
// goroutine:9 x=1
// goroutine:2 x=1
// goroutine:6 x=1
// goroutine:1 x=1

  • Go 读写锁和互斥锁性能比较

互斥锁:

package main

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

var rwlock sync.RWMutex
var x int
var wg sync.WaitGroup
var mutex sync.Mutex

func write() {
   for i := 0; i < 100; i++ {
    //rwlock.Lock()
    mutex.Lock()
    x = x + 1
    time.Sleep(10 * time.Millisecond)
    mutex.Unlock()
    //rwlock.Unlock()
   }
   wg.Done()
}

func read(i int) {
   for i := 0; i < 100; i++ {
    //rwlock.RLock()
    mutex.Lock()
    time.Sleep(time.Millisecond)
    mutex.Unlock()
    //rwlock.RUnlock()
   }
   wg.Done()
}

func main() {

   start := time.Now().UnixNano()
   wg.Add(1)
   go write()

   for i := 0; i < 100; i++ {
    wg.Add(1)
    go read(i)
   }

   wg.Wait()
   end := time.Now().UnixNano()
   cost := (end - start) / 1000 / 1000
   fmt.Println("cost:", cost, "ms")
}

// [Running] go run "/Users/zll/Desktop/gostudy/listen21/rw_vs_mutex/main.go"
// cost: 13627 ms

// [Done] exited with code=0 in 14.249 seconds

// [Running] go run "/Users/zll/Desktop/gostudy/listen21/rw_vs_mutex/main.go"
// cost: 13568 ms

//  [Done] exited with code=0 in 14.03 seconds

读写锁:

package main

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

var rwlock sync.RWMutex
var x int
var wg sync.WaitGroup
var mutex sync.Mutex

func write() {
   for i := 0; i < 100; i++ {
    rwlock.Lock()
    // mutex.Lock()
    x = x + 1
    time.Sleep(10 * time.Millisecond)
    // mutex.Unlock()
    rwlock.Unlock()
   }
   wg.Done()
}

func read(i int) {
   for i := 0; i < 100; i++ {
    rwlock.RLock()
    // mutex.Lock()
    time.Sleep(time.Millisecond)
    // mutex.Unlock()
    rwlock.RUnlock()
   }
   wg.Done()
}

func main() {

   start := time.Now().UnixNano()
   wg.Add(1)
   go write()

   for i := 0; i < 100; i++ {
    wg.Add(1)
    go read(i)
   }

   wg.Wait()
   end := time.Now().UnixNano()
   cost := (end - start) / 1000 / 1000
   fmt.Println("cost:", cost, "ms")
}

// [Running] go run "/Users/zll/Desktop/gostudy/listen21/rw_vs_mutex/main.go"
// cost: 1251 ms

// [Done] exited with code=0 in 1.518 seconds

// [Running] go run "/Users/zll/Desktop/gostudy/listen21/rw_vs_mutex/main.go"
// cost: 1273 ms

// [Done] exited with code=0 in 1.477 seconds

Go 原子操作

A. 加锁代价比较耗时,需要上下文切换

B. 针对基本数据类型,可以使用原子操作保证线程安全

C. 原子操作在用户态就可以完成,因此性能比互斥锁要高

package main

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

var x int32
var wg sync.WaitGroup

var mutex sync.Mutex

func addMutex() {
   for i := 0; i < 500; i++ {
    mutex.Lock()
    x = x + 1
    mutex.Unlock()
   }
   wg.Done()
}

func add() {
   for i := 0; i < 500; i++ {
    //mutex.Lock()
    //x = x +1
    atomic.AddInt32(&x, 1)
    //mutex.Unlock()
   }
   wg.Done()
}

func main() {

   start := time.Now().UnixNano()
   for i := 0; i < 10000; i++ {
    wg.Add(1)
    go add()
    //go addMutex()
   }

   wg.Wait()
   end := time.Now().UnixNano()
   cost := (end - start) / 1000 / 1000
   fmt.Println("x:", x, "cost:", cost, "ms")
}

推荐阅读

(点击标题可跳转阅读)

RxJS入门

一文掌握Webpack编译流程

一文深度剖析Axios源码

Javascript条件逻辑设计重构
Promise知识点自测

你不知道的React Diff
你不知道的GIT神操作
程序中代码坏味道(上)

程序中代码坏味道(下)

学习Less,看这篇就够了

一文掌握GO语言实战技能(一)

一文掌握GO语言实战技能(二)

一文掌握Linux实战技能-系统管理篇

一文掌握Linux实战技能-系统操作篇

一文达到Mysql实战水平

一文达到Mysql实战水平-习题答案

从表单抽象到表单中台

vue源码分析(1)- new Vue

实战LeetCode 系列(一) (题目+解析)

一文掌握Javascript函数式编程重点

实战LeetCode - 前端面试必备二叉树算法

一文读懂 React16.0-16.6 新特性(实践 思考)

阿里、网易、滴滴、今日头条、有赞.....等20家面试真题

30分钟学会 snabbdom 源码,实现精简的 Virtual DOM 库


觉得本文对你有帮助?请分享给更多人

关注「React中文社区」加星标,每天进步

golang devops项目实战 - 为什么选择go语言 先来聊聊go有趣的历史 在google中,大部分的项目都是采用C C++开发,少量地用了java,其次 才是python;在2007年的某一天,google的首席工程师在编译一个C++项目 过程中,尽管在google早就实现了分布式编译系统,但是Rob Pike和Robert Griesemer实在受够了那个漫长的编译等待时间,于是突发灵感与坐在旁边的 Ken Thompson一起讨论实在有必要发... 阅读详情

相关推荐

【Golang】关于Go语言中的包

Go语言中包的本质:文件夹,不同的文件夹可以存放不同的功能代码。 在Go语言中,每个源代码文件都必须属于一个包。包通常由一个或多个以.go为后缀的源文件组成,这些源文件通常存放在同一个目录下。 包的名称应该具有描述性,能够清楚地表达其功能。包名一般使用小写字母,并且和包所在的文件夹名保持一致。

景天科技苑 3万+

go+react开发简单的web系统

go练手项目

一个真实、有温度的无名小卒 1万+

Go语言实战:工具和包管理之Golang标准库

1.背景介绍 1. 背景介绍 Go语言,也被称为Golang,是一种静态类型、编译式、多平台的编程语言。它于2009年由Google的工程师Robert Griesemer、Rob Pike和Ken Thompson开发。Go语言的设计目标是简单、高效、可靠和易于扩展。它的核心特点是强大的并发支持、简洁的语法和高性能。 Golang标准库是Go语言的核心组成部分,提供了大量的功能和工具,包括...

AI天才研究院 1245

go语言实战_一文掌握GO语言实战技能(三)

Go 接口Go 反射详解Go 并发编程Go Channel介绍Go Worker池的实现Go Select语义介绍和使用Go 互斥锁介绍Go 读写锁介绍Go 读写锁和互斥锁性能比较Go 原子操作GO 接口接口的介绍和定义接口定义了一个对象的行为规范a. 只定义规范,不实现b. 具体的对象需要实现规范的细节Go 中接口定义a. type 接口名字 interfaceb. 接口里面是一组方法...

weixin_39600328的博客 211

STM32F103C8T6心电采集实战:从PCB设计到上位机开发的完整指南(附TI杯开源资料)

本文详细介绍了基于STM32F103C8T6的心电采集系统开发全流程,从PCB设计到上位机开发,特别适合大学生电子设计大赛参赛者参考。内容涵盖硬件设计精要、软件算法优化及上位机开发技巧,并附TI杯开源资料,帮助开发者低成本实现医疗级心电信号采集。

kafka6courier的博客 194

BeckHoff <--> ABB 机器手 Profinet 通讯

本文介绍了BeckHoff PLC与ABB机器人通过PROFINET通信的实现方案。主要内容包括:1)通信接口的详细变量定义,涵盖输入/输出信号、设备状态监控、夹爪控制等关键功能位;2)全局数据结构和变量配置说明;3)机器人任务控制程序的实现方法,包括参数校验、任务指令下发和状态监控机制;4)重点分析了任务调度功能块的代码逻辑,涉及慢速模式处理、多参数配置和执行状态判断。该方案通过结构化变量定义和模块化程序设计,实现了PLC对机器人作业的精确控制,为工业自动化系统集成提供了可靠通信范例。

工控智联驿站 1032

[GO语言基础] 一.为什么我要学习Golang以及GO语言入门普及

作为网络安全初学者,会遇到采用Go语言开发的恶意样本。因此从今天开始从零讲解Golang编程语言,一方面是督促自己不断前行且学习新知识;另一方面是分享与读者,希望大家一起进步。这系列文章入门部分将参考“尚硅谷”韩顺平老师的视频和书籍《GO高级编程》,详见参考文献,并结合作者多年的编程经验进行学习和丰富,且看且珍惜吧!后续会结合网络安全进行GO语言实战深入,驾驭~ 这些年我学过各种编程语言,从最早的C语言到C++,再到C#、PHP、JAVA,再到IOS开发、Python,到最新的GO语言,学得是真的杂。有时

杨秀璋的专栏 12万+

网络安全面试题

在当今社会网络安全行业越来越发达,也有越来越多的人去学习,为了更好地进行工作,除了学好知识外还要应对企业的面试。 所以在这里我归总了一些网络安全方面的常见面试题,希望对大家有所帮助。 windows常见 1:描述tcp/udp的区别及优劣,及其发展前景 TCP—传输控制协议,提供的是面向连接、可靠的字节流服务。当客户和服务器彼此交换数据前,必须先在双方之间建立一个TCP连接,之后才能...

fzx_hsaj的博客 6万+

GO开篇:手握Java走进Golang的世界

Go简介、Go和Java的对比

健身变秃,coding变强 1万+

OceanBase数据库:与MySQL和Oracle的函数差异分析

OceanBase数据库兼容性解析:MySQL与Oracle模式深度比较 摘要:OceanBase通过独特的双模式架构,实现了对MySQL和Oracle的高度兼容。文章首先分析了OceanBase的兼容性架构,包括SQL解析层、优化器层和执行引擎层的差异化设计。随后对比了两种模式在数据类型、SQL语法、函数支持等方面的兼容性差异,其中MySQL模式兼容5.7版本大部分功能,Oracle模式则支持155个内置函数和PL/SQL特性。最后提出了模式选择的六大考量因素,包括现有系统基础、功能需求和迁移成本等,为数

本博客聚焦 YOLOv11 全流程落地,涵盖架构优化、数据集处理、训练技巧与多场景部署,兼及国产数据库、Java 开发与 AI 模型应用,内容兼顾理论与工程实践,为开发者提供系统干货,助力高效提升技术能力。 1227

医疗AI知识库更新慢?提示工程架构师用Agentic AI自动更新的策略

我们的目标是用Agentic AI替代传统更新流程中的“重复劳动”,让医疗知识库(以知识图谱为例)能自动跟踪最新医学进展,把“更新周期从数月缩短到数天”。从PubMed/指南数据库自动抓取新文献;用提示工程从文本中提取结构化医学知识;自动校验新知识与旧知识的冲突;将通过校验的知识写入知识图谱。拆问题:先讲医疗知识库更新慢的3大根源;给方案:用Agentic AI设计4个“小助手”解决问题;讲原理:每个小助手的工作逻辑+提示工程技巧;做实战。

AI 原生应用开发的博客 1061

Go 和 Golang 有什么关系?

大家好,我是煎鱼。最近天气也冷了,掐指一算是招聘季了,无论是校招、社招、HR、面试官们都蠢蠢欲动。这不,我有一个朋友的 HR 朋友都有起名困难了,一看 Go 语言的工作说明(Job Des...

EDDYCJY的博客 1万+

CAD二次开发c# .net加载cui\cuix菜单文件

public static void LoadMyCui(string cuiFile) { Document doc = Application.DocumentManager.MdiActiveDocument; object oldCmdEcho = Application.GetSystemVariable("CMDECHO"); object oldFileDia = Application.GetSys..

2127

【Golang】三分钟让你快速了解Go语言&为什么我们需要Go语言?

Go编译速度快的几个原因使用import引用管理方式没有模板编译负担1.5版本后编译器的优化语言本身关键字很少。

Yaoyao2024的博客 3689

CLASSB类移植(满足IEC 60730-1和60355-1标准)

针对软件认证需要满足IEC 60730-1和60335-1标准,ST公司专门提供的软件扩展包X-CUBE-CLASSB,它包含自测库和文档,支持多种STM32系列芯片,通过预集成测试库和标准化流程,帮助开发者快速满足国际安全标准,适用于家电、工业控制等高可靠性场景。ClassB软件包程序检测内容分为两个主要部分:启动时的自检和运行时的周期自检,MCU在上电的时候会进行复位启动,在启动完成以后会进行运行自检。

m0_61973119的博客 1402

D16: OpenClaw + Notion API:知识管理新玩法

本文介绍了如何利用OpenClaw连接Notion API构建自动化知识管理系统。文章首先分析了传统知识管理的痛点,包括信息分散、更新不及时等问题,并对比了Notion与其他工具在API完整性、结构化程度等方面的优势。接着详细说明了Notion API的获取和授权流程,并设计了包含项目知识库、技术决策库和会议纪要库的三层架构系统。最后通过会议纪要自动归档的实战案例,展示了如何通过YAML配置实现从飞书会议到Notion的自动化知识沉淀流程。该方案能有效解决团队知识管理中的信息碎片化问题,提升协作效率。

刘一说的IT专栏 274

一文带你GO语言入门

Go语言(又称Golang)是Google开发的一种静态强类型、编译型、并发型,并具有垃圾回收功能的编程语言。Go语言的主要特点包括:- 简洁和简单 - 语法简单明快,易于学习和使用GoLand是JetBrains公司推出的Go语言集成开发环境(IDE)。可定制构建工具和任务。GoLand为Go语言提供了类似IntelliJ IDEA的开发体验,可以大大提高Go语言的开发效率。它支持完整的开发生命周期,是目前最好的Go语言IDE之一。许多Go语言开发者都选择了GoLand来提升生产力。

Venus's Blog 1万+

复分析 阿尔福斯(Ahlfors) 第三版答案 英文

答案分为十章,有部分习题没有答案,全英文复分析阿尔福斯(Ahlfors) 第三版答案

100天精通Golang(基础入门篇)——第1天:学习Go语言基本概念

100天精通Golang(基础入门篇)——第1天:学习Go语言基本概念,欢迎来到100天精通Golang的基础入门篇!在这个系列教程中,我们将带领你逐步掌握Go语言的核心概念和语法。本篇博文将着重介绍Go语言的基本概念,包括其定义、用途和优势。通过阅读本文,你将对Go语言有一个清晰的了解。在第1天的学习中,我们将探索Go语言的基本概念。我们将介绍Go语言是什么,它可以用来做什么,以及它相较于其他编程语言的优势。

猫头虎技术团队:授渔优于赠鱼,兴趣引领智慧,探索之乐尤显珍贵。商务合作+:Libin9iOak ,万粉变现+:优快云WF,猫头虎承诺每年免费为100名C站创作者做账号流量诊断服务!全网搜:猫头虎技术团队,点击文章底部名片或直接私信我一切皆可谈,快找虎哥! 6万+

TMS_WebGMaps_1.9.8.0_Full_Source

TMS_WebGMaps_1.9.8.0_Full_Source 全源码 delphi cbuilder

efm32 入门.pdf

本PDF共计22页,介绍的是操作驱动API,下载与否自己斟酌。

go语言实战_一文掌握GO语言实战技能(一) 万字长文

GO基本介绍GO 基本概念GO 语言特性GO 程序的基本结构GO 标识符 关键字 变量 常量GO 数据类型GO 操作符 与 日期GO 流程控制GO 函数介绍GO 闭包的介绍Go 基本介绍Go 语言诞生于谷歌,由计算机领域的三位宗师级大牛 Rob Pike、Ken Thompson 和 Robert Griesemer 写成。由于出身名门,Go 在诞生之初就吸引了大批开发者的关注。诞生十...

weixin_39744316的博客 993

IDEA的使用指南

IDEA基于java的使用手册,可以最快速的上手,在使用中不断的丰富自己的使用技巧.

【小tips】这门语言是叫 Go 还是 Golang?

到底是Go语言还是Golang语言?来看看官方解释吧

m0_60511809的博客 3024

视频教程-Golang全栈工程师入门到实战-Go语言

Golang全栈工程师入门到实战 贵州三口木网络科技有限公司创始人,CEO兼...

weixin_27067581的博客 1074

为什么劝你要学习Golang以及GO语言(Go语言知识普及)

为什么劝你要学习Golang以及GO语言

m0_69234258的博客 4973

Golang Go 语言简介

1. Go语言是什么 “Go 是一种开源的程序设计语言,它意在使得人们能够方便地构建简单、可靠、高效率的软件”(来自 go 官网 golang.org) Go 语言是 Google 开发的开源编程语言,于 2006年1月2日15点4分5秒 诞生,2009年11月 开源,2012年发布 go 稳定版。 go 的主要目标是“兼具 Python 等动态语言的开发速度和 C/C++ 等编译型语言的性能与安全性”。 我们程序员在开发程序,开发软件时都会选择一门编程语言,那么我们应该怎样进行选择呢?可能有同学会说,我

李培冠的博客 1万+

熬夜整理,最全的Go语言教程来了,快来收藏!

随着国内越来越多的企业开始使用Go语言,Go语言一度变得火热,成为不少程序员朋友的首选语言。Go语言最早诞生于谷歌,出自谷歌的三位大牛之手,自2009年发布以来,Go语言已经度过了第12个年头,相比于其它语言,可谓是语言界的新生儿。如果想成为一名Go语言开发者,需要怎么入手呢?本文将会为您推荐一些关于Go语言学习的书籍、视频以及网站资源。1).Go语言实战如果想快速使用Go语言进行开发实战,这本书是一个很好的选择。这本书没有对基础语法进行详细介绍,而是更偏实战应用一些。这本书的目标读者是已经有一定其他编程语

fangyuan621的专栏 1万+

一文掌握GO语言实战技能(一) - 万字长文

GO基本介绍GO 基本概念GO 语言特性GO 程序的基本结构GO 标识符 关键字 变量 常量GO 数据类型GO 操作符 与 日期GO 流程控制GO 函数介绍GO 闭包的介绍Go 基本介...

React_Community的博客 761

从零到一学习golang语言_初识go语言(一)

从本篇文章起,从零到一学习go语言。 一、go和golang? 到底是go语言还是golang语言,其实官网已经表态过为go语言。官网的域名使用https://golang.org/,是因为go.org已经被注册过了,需要花几千万购买,但是没这个必要。第二个原因使用golang是因为,在搜索引擎中查找相关go的资料时使用golang能能更大限度匹配,如果仅仅是输入go很可能得不到你想要的。 二、go语言简介 go语言是一个开源的项目,为了让程序开发者更高产,提高程序员的开发生产力。 go语言富有表

Sun_ltyy的博客 1355
上一篇: 虚幻引擎5官方视频
下一篇: V8是如何执行一段JS代码的?
React 中文社区
博客等级 码龄6年 119粉丝 41原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值