20个Golang片段让我不再健忘 | 京东云技术团队

前言

本文使用代码片段的形式来解释在 go 语言开发中经常遇到的小功能点,由于本人主要使用 java 开发,因此会与其作比较,希望对大家有所帮助。

1. hello world

新手村的第一课,毋庸置疑。

1package main 2 3import "fmt" 4 5func main() { 6 fmt.Printf("hello world") 7} 8

2. 隐形初始化

1package main 2 3import "fmt" 4 5func main() { 6 load() 7} 8 9func load() { 10 fmt.Printf("初始化..手动%s 不错\n", "1") 11} 12 13func init() { 14 fmt.Printf("隐形初始化。。\n") 15} 16

在 go 中定义 init 函数,程序在运行时会自动执行。类似使 junit 的 [@before](https://my.oschina.net/u/3870904) 注解。

3. 多模块的访问

java 中 package 包的概念,go 是通过文件夹 + package 关键字来定义的。

一般而言,我们会通过go init来创建项目,生成的go.mod文件位于根目录。

常见的实践是,创建文件夹并且保持 package 名称与文件夹保持一致。这样 import 的永远是文件夹,遵循以上规则则意味着文件夹的名称即为模块名。

同一个 package 可以创建多个 .go 文件,虽然分布在不同的文件中。但是他们中的方法名称不能相同。需要注意,这里与 java中不同类中方法可以重名不同。

此外,也没有诸如private、protected、public等包访问权限关键字。只要定义的函数首字母为大写。则可以被外部成功调用。

来看一下示例:

1go-tour 2└── ch3 3 ├── model 4 │ └── test 5 │ │ ├── testNest.go 6 │ └── helper.go 7 │ └── helper2.go 8 │ 9 └── main.go 10 └── go.mod 11

此处,ch3、model、test 均为文件夹,也可以说是 package。helper.go 位于 model 下,它的代码如下:

1package model 2 3import "fmt" 4 5var AppName = "bot" 6var appVersion = "1.0.0" 7 8func Say() { 9 fmt.Printf("%s", "hello") 10} 11 12func init() { 13 fmt.Printf("%s,%s", AppName, appVersion) 14} 15

再来看看 main.go

1package main 2 3import ( 4 "ch3/model" 5 "ch3/model/test" 6) 7 8func main() { 9 model.Say() 10} 11

显然它的调用是通过 packageName.MethodName() 来使用的。需要注意的是,一个 go.mod 下只能有一个 main 包。

4. 引用外部库

和 java 的 maven 类似,go 几经波折也提供了官方仓库。如下,通过 go get github.com/satori/go.uuid 命令即可安装 uuid 库,未指定版本,因此下载的为最新版本。

使用时是这样的:

1package main 2 3import ( 4 "fmt" 5 uuid "github.com/satori/go.uuid" 6) 7 8func main() { 9 10 uuid := uuid.NewV4() 11 fmt.Printf("%s", uuid) 12} 13

5. 数组字典和循环

直接看代码就是了。

1package main 2 3import "fmt" 4 5var item []int 6var m = map[int]int{ 7 100: 1000, 8} 9var m2 = make(map[int]int) 10 11func main() { 12 13 for i := 0; i < 10; i++ { 14 item = append(item, i) 15 m[i] = i 16 m2[i] = i 17 } 18 19 for i := range item { 20 fmt.Printf("item vlaue=%d\n", i) 21 } 22 23 for key, value := range m { 24 fmt.Printf("m:key=%d,value=%d\n", key, value) 25 } 26 27 for _, value := range m2 { 28 fmt.Printf("m2:value=%d\n", value) 29 } 30} 31
  • := 的形式只能在方法内
  • 全局的只能用 var x=..
  • map输出没有顺序

6. 结构体和JSON

go 中通过 struct 来定义结构体,你可以把它简单理解为对象。一般长这样。

1type App struct { 2 AppName string 3 AppVersion string `json:"app_version"` 4 appAuthor string "pleuvoir" 5 DefaultD string "default" 6} 7

我们经常在 java 程序中使用 fastjson 来输出 JSON字符串。 go 中自带了这样的类库。

1package main 2 3import ( 4 app2 "app/app" //可以定义别名 5 "encoding/json" 6 "fmt" 7) 8 9func main() { 10 11 a := app2.App{} 12 fmt.Printf("%s\n", a) 13 14 app := app2.App{AppName: "bot", AppVersion: "1.0.1"} 15 16 json, _ := json.Marshal(app) //转换为字符串 17 18 fmt.Printf("json is %s\n", json) 19} 20
  • 结构体中 JSON 序列化不会转变大小写,可以指定它输出的 key名称通过 json:xxx 的描述标签。
  • 结构体中的默认值赋值了也不展示

7. 异常处理

作为一个有经验的程序员:),go 的异常处理涉及的很简单,也往往为人所诟病。比如满屏幕的 err 使用。

1package main 2 3import ( 4 "fmt" 5 "os" 6) 7 8func _readFile() (int, error) { 9 file, err := os.ReadFile("test.txt") 10 if err != nil { 11 fmt.Printf("error is = %s\n", err) 12 return 0, err 13 } 14 fmt.Printf("file = %s \n", file) 15 return len(file), err 16} 17 18func readFile() (int, error) { 19 fileLength, err := _readFile() 20 if err != nil { 21 fmt.Printf("异常,存在错误 %s\n", err) 22 } 23 return fileLength, err 24} 25 26func main() { 27 fileLength, _ := readFile() 28 fmt.Printf("%d\n", fileLength) 29 30} 31

和 java 不同,它支持多返回值,为我们的使用带来了很多便利。如果不需要处理这个异常,可以使用 _ 忽略。

8. 异步

千呼万唤始出来,令人兴奋的异步。

1package main 2 3import ( 4 "bufio" 5 "fmt" 6 "os" 7) 8 9func worker() { 10 for i := 0; i < 10; i++ { 11 fmt.Printf("i=%d\n", i) 12 } 13} 14func main() { 15 16 go worker() 17 go worker() 18 19 //阻塞 获取控制台的输出 20 reader := bufio.NewReader(os.Stdin) 21 read, err := reader.ReadBytes('\n') //注意是单引号 回车后结束控制台输出 22 if err != nil { 23 fmt.Printf("err is =%s\n", err) 24 return 25 } 26 fmt.Printf("read is %s \n", read) 27} 28

如此的优雅,如此的简单。只需要一个关键字 go 便可以启动一个协程。我们在 java 中经常使用的是线程池,而在 go 中也存在协程池。据我观察,部分协程池 benchmark 的性能确实比官方语言关键字高很多。

9. 异步等待

这里就类似 java 中使用 countdownLatch 等关键字空值并发编程中程序的等待问题。

1package main 2 3import ( 4 "fmt" 5 "sync" 6 "time" 7) 8 9func upload(waitGroup *sync.WaitGroup) { 10 for i := 0; i < 5; i++ { 11 fmt.Printf("正在上传 i=%d \n", i) 12 } 13 time.Sleep(5 * time.Second) 14 waitGroup.Done() 15} 16 17func saveToDb() { 18 fmt.Printf("保存到数据库中\n") 19 time.Sleep(3 * time.Second) 20} 21 22func main() { 23 24 begin := time.Now() 25 fmt.Printf("程序开始 %s \n", begin.Format(time.RFC850)) 26 27 waitGroup := sync.WaitGroup{} 28 waitGroup.Add(1) 29 30 go upload(&waitGroup) 31 go saveToDb() 32 waitGroup.Wait() 33 34 fmt.Printf("程序结束 耗时 %d ms ", time.Now().UnixMilli()-begin.UnixMilli()) 35} 36

sync 包类似于 J.U.C 包,里面可以找到很多并发编程的工具类。sync.WaitGroup 便可以简简单单认为是 countdownLatch 吧。也不能多次调用变为负数,否则会报错。

注意,这里需要传入指针,因为它不是一个引用类型。一定要通过指针传值,不然进程会进入死锁状态。

10. 管道

1package main 2 3import ( 4 "fmt" 5 "sync" 6) 7 8var ch = make(chan int) 9var sum = 0 //是线程安全的 10 11func consumer(wg *sync.WaitGroup) { 12 for { 13 select { 14 case num, ok := <-ch: 15 if !ok { 16 wg.Done() 17 return 18 } 19 sum = sum + num 20 } 21 } 22} 23 24func producer() { 25 for i := 0; i < 10_0000; i++ { 26 ch <- i 27 } 28 close(ch) //如果不关闭则会死锁 29} 30 31func main() { 32 33 wg := sync.WaitGroup{} 34 wg.Add(1) 35 go producer() 36 go consumer(&wg) 37 38 wg.Wait() 39 fmt.Printf("sum = %d \n", sum) 40} 41

这里演示的是什么呢?管道类似一个队列,进行线程间数据的传递。当关闭时消费端也退出,如果没关闭管道,运行时会报死锁。可以看出全局变量在线程间是安全的。

可以衍生出一种固定写法:

1//固定写法 2func consumer(wg *sync.WaitGroup) { 3 for { 4 select { 5 case num, ok := <-ch: 6 if !ok { 7 wg.Done() 8 return 9 } 10 sum = sum + num 11 } 12 } 13} 14

11. 接口

1package main 2 3import "fmt" 4 5type Person interface { 6 Say() 7 SetName(name string) 8} 9 10type ZhangSan struct { 11 Value string 12} 13 14func (z *ZhangSan) Say() { 15 fmt.Printf("name=%s", z.Value) 16} 17 18func (z *ZhangSan) SetName(name string) { 19 z.Value = name + ":hehe" 20} 21 22func main() { 23 zhangSan := ZhangSan{} 24 zhangSan.SetName("pleuvoir") 25 zhangSan.Say() 26} 27

如上的程序演示了接口的使用。

  • go的接口没有强依赖
  • 通过结构体 + 方法的形式实现,注意方法传入的可以是引用也可以是值

12. 锁

1package main 2 3import ( 4 "fmt" 5 "sync" 6) 7 8type Number struct { 9 Value int 10 mutex sync.Mutex //加锁 11} 12 13func (receiver *Number) Add() { 14 receiver.mutex.Lock() 15 defer receiver.mutex.Unlock() //退出时会执行 16 receiver.Value = receiver.Value + 1 17 //fmt.Printf("add\n") 18} 19 20func (receiver *Number) Get() int { 21 receiver.mutex.Lock() 22 defer receiver.mutex.Unlock() 23 return receiver.Value 24} 25 26func main() { 27 number := Number{Value: 0} 28 29 wg := sync.WaitGroup{} 30 31 n := 100_0000 32 wg.Add(n) 33 34 for i := 0; i < n; i++ { 35 go func(wg *sync.WaitGroup) { 36 number.Add() 37 wg.Done() 38 }(&wg) 39 } 40 41 wg.Wait() 42 fmt.Printf("count=%d", number.Get()) 43} 44

这里是什么?显然就像是显示锁的 ReentrantLock 的使用,相信大家都能看懂。这里出现了新关键字 defer,我暂且是理解为 finally。不知道你怎么看?

13. 读写配置文件

这也是一个很常规的功能,看看怎么实现。

1package main 2 3import ( 4 "encoding/json" 5 "fmt" 6 "os" 7) 8 9type Preferences struct { 10 Name string `json:"name"` 11 Version float64 `json:"version"` 12} 13 14const configPath = "config.json" 15 16func main() { 17 18 preferences := Preferences{Name: "app", Version: 100.01} 19 marshal, err := json.Marshal(preferences) 20 21 err = os.WriteFile(configPath, marshal, 777) 22 if err != nil { 23 fmt.Printf("写入配置文件错误,%s\n", err) 24 return 25 } 26 27 //读取配置文件 28 file, err := os.ReadFile(configPath) 29 if err != nil { 30 fmt.Printf("读取文件错误,%s\n", err) 31 return 32 } 33 fmt.Printf("%s\n", file) //{"name":"app","version":100.01} 34 35 //构建一个对象用来序列化 36 readConfig := Preferences{} 37 38 //反序列化 39 err = json.Unmarshal(file, &readConfig) 40 if err != nil { 41 fmt.Printf("配置文件转换为JSON错误,%s\n", err) 42 } 43 44 fmt.Printf("%v", readConfig) //{app 100.01} 45

这里挺没意思的,写入 JSON 字符串,然后读取回来在加载到内存中。不过,简单的示例也够说明问题了。

14. 宕机处理

这是类似于一种最上层异常捕获的机制,在程序的入口处捕获所有的异常。

1package main 2 3import ( 4 "fmt" 5 "time" 6) 7 8func worker() { 9 //defer func() { //不能写在主函数,最外层catch没啥用 10 // if err := recover(); err != nil { 11 // fmt.Printf("%s", err) 12 // } 13 //}() 14 defer recovery() 15 panic("严重错误") 16} 17 18func recovery() { 19 if err := recover(); err != nil { 20 fmt.Printf("死机了。%s\n", err) 21 } 22} 23 24func main() { 25 for true { 26 worker() 27 time.Sleep(1 * time.Second) 28 } 29} 30

注释写的很清楚,聪明的你一看就懂。

15. 单元测试

与 java 不同,go 建议单元测试文件尽可能的离源代码文件近一些。比如这样:

1go-tour 2 └── main.go 3 └── main_test.go 4

并且它的命名也是这样简单粗暴:

1package main 2 3import ( 4 "testing" 5) 6 7func TestInit(t *testing.T) { 8 t.Log("heh") 9 10 helper := PersonHelper{} 11 helper.init("pleuvoir") 12 t.Log(helper.Name) 13} 14

以大写的 Test 开头,文件名称以 _test 结尾,很清爽的感觉。

16. 启动传参

这也是一个很常用的知识点。这里有两种方式:

  • 直接传
  • 使用 flag
1package main 2 3import ( 4 "encoding/json" 5 "flag" 6 "fmt" 7 "os" 8) 9 10func main() { 11 12 //第一种方式 13 args := os.Args 14 15 for i, arg := range args { 16 println(i, arg) 17 } 18 19 //第二种方式 20 config := struct { 21 Debug bool 22 Port int 23 }{} 24 25 flag.BoolVar(&config.Debug, "debug", true, "是否开启debug模式") 26 flag.IntVar(&config.Port, "port", 80, "端口") 27 28 flag.Parse() 29 30 json, _ := json.Marshal(config) 31 32 fmt.Printf("json is %s\n", json) 33} 34

我建议使用第二种,更便捷自带类型转换,还可以给默认值,非常好。

17. 优雅退出

1 2 3package main 4 5import ( 6 "fmt" 7 "os" 8 "os/signal" 9 "syscall" 10) 11 12func quit() { 13 println("执行一些清理工作。。") 14} 15 16//正常的退出 17//终端 CTRL+C退出 18//异常退出 19 20func main() { 21 22 defer quit() 23 println("进来了") 24 25 //读取信号,没有一直会阻塞住 26 exitChan := make(chan os.Signal) 27 28 //监听信号 29 signals := make(chan os.Signal) 30 signal.Notify(signals, syscall.SIGINT, syscall.SIGQUIT) 31 32 go func() { 33 //有可能一次接收到多个 34 for s := range signals { 35 switch s { 36 case syscall.SIGINT, syscall.SIGQUIT: 37 println("\n监听到操作系统信号。。") 38 quit() //如果监听到这个信号没处理,那么程序就不会退出了 39 if i, ok := s.(syscall.Signal); ok { 40 value := int(i) 41 fmt.Printf("是信号类型,准备退出 %d", value) 42 } else { 43 println("不知道是啥,0退出") 44 os.Exit(0) 45 } 46 // os.Exit(value) 47 exitChan <- s 48 } 49 } 50 }() 51 52 println("\n程序在这里被阻塞了。") 53 <-exitChan 54 //panic("heh") 55 println("\n阻塞被终止了。") 56} 57

这其实是在监听操作系统的信号,java 中也有类似的回调的接口(我忘了名字)。

18. 反射

作为一门高级语言,反射肯定是有的。还是使用 reflect 包。

1package main 2 3import ( 4 "fmt" 5 "reflect" 6) 7 8type Person struct { 9 Name string `json:"name"` 10} 11 12func (p *Person) SetName(name string) { 13 p.Name = name 14} 15 16func (p *Person) GetName() (string, string) { 17 return p.Name, "1.0.1" 18} 19 20func worker1() { 21 p := Person{} 22 p.SetName("pleuvoir") 23 name, _ := p.GetName() 24 fmt.Printf(name) 25} 26 27// 获取方法 28func worker2() { 29 p := Person{} 30 rv := reflect.ValueOf(&p) 31 value := []reflect.Value{reflect.ValueOf("peluvoir")} 32 rv.MethodByName("SetName").Call(value) 33 values := rv.MethodByName("GetName").Call(nil) 34 for i, v := range values { 35 fmt.Printf("\ni=%d,value=%s\n", i, v) 36 } 37} 38 39func worker3() { 40 s := Person{} 41 rt := reflect.TypeOf(s) 42 if field, ok := rt.FieldByName("Name"); ok { 43 tag := field.Tag.Get("json") 44 fmt.Printf("tag is %s \n", tag) 45 } 46} 47 48func main() { 49 //正常获取 50 worker1() 51 //获取方法 52 worker2() 53 //获取标签 54 worker3() 55} 56

没什么好说的,写代码全靠猜。

19. atomic

类似 java 中的 atomic 原子变量。

1package main 2 3import ( 4 "fmt" 5 "sync" 6 "sync/atomic" 7) 8 9func main() { 10 11 workers := 1000 12 13 wg := sync.WaitGroup{} 14 wg.Add(workers) 15 for i := 0; i < workers; i++ { 16 go worker2(&wg) 17 } 18 wg.Wait() 19 20 fmt.Printf("count = %d", count) 21} 22 23var count int64 = 0 24 25func worker1(wg *sync.WaitGroup) { 26 count++ 27 wg.Done() 28} 29 30func worker2(wg *sync.WaitGroup) { 31 atomic.AddInt64(&count, 1) //特别简单 32 wg.Done() 33} 34

真的是特别简单。

20. 线程安全的Map

类似于ConcurrentHashMap,与普通的 api 有所不同。

1var sessions = sync.Map{} 2sessions.Store(uuid, uuid) 3load, ok := sessions.Load(value.Token) 4 if ok { 5 // 做你想做的事情 6 } 7

21. return func

这里就是函数式变成的例子了。函数是一等公民可以作为参数随意传递。java 什么时候能支持呢?

1 2package main 3 4import "fmt" 5 6func main() { 7 engine := Engine{} 8 engine.Function = regular() 9 10 function := engine.Function 11 12 for i := 0; i < 3; i++ { 13 s := function("pleuvoir") 14 fmt.Printf("s is %s\n", s) 15 } 16 17} 18 19type Engine struct { 20 Function func(name string) string 21} 22 23func regular() (ret func(name string) string) { 24 fmt.Printf("初始化一些东西。\n") 25 return func(name string) string { 26 fmt.Printf("我是worker。name is %s\n", name) 27 return "我是匿名函数的返回值" 28 } 29} 30

比如这里,如果要初始化日志什么。最后需要让框架在哪里打印日志,就需要将这个初始化的日志实例传递过去。总而言之,言而总之。会需要让代码各种传递。

这种方式在于第一次调用的时候会执行上面的代码片段,后面只是保存了这个函数的句柄,然后可以一直调用这个匿名函数。

22. context

1package main 2 3import ( 4 "context" 5 "fmt" 6 "time" 7) 8 9func main() { 10 worker1() 11} 12 13func worker1() { 14 15 //总共2秒超时 16 value := context.WithValue(context.Background(), "token", "pleuvoir") 17 timeout, cancelFunc := context.WithTimeout(value, 5*time.Second) 18 defer cancelFunc() 19 20 //模拟任务 21 fmt.Println("开始任务") 22 deep := 10 23 go handler(timeout, deep) 24 25 fmt.Println("开始阻塞", time.Now()) 26 //等待主线程超时,阻塞操作 27 select { 28 case <-timeout.Done(): 29 fmt.Println("阻塞结束", timeout.Err(), time.Now()) 30 } 31 32} 33 34// 模拟任务处理,循环下载图片等 35func handler(timeout context.Context, deep int) { 36 37 if deep > 0 { 38 fmt.Printf("[begin]token is %s %s deep=%d\n", timeout.Value("token"), time.Now(), deep) 39 time.Sleep(1 * time.Second) 40 go handler(timeout, deep-1) 41 } 42 43 //下面的哪个先返回 先执行哪个 44 //如果整体超时 或者 当前方法超过2秒 就结束 45 select { 46 47 //等待超时会返回 48 case <-timeout.Done(): 49 fmt.Println("超时了。", timeout.Err()) 50 //等待这么久 然后会返回 这个函数可不是比较时间,这里其实是在模拟处理任务,固定执行一秒 和休息一秒效果一样 51 //但是休息一秒的话就不会实时返回了,所以这里实际应用可以是一个带超时的回调? 52 case <-time.After(time.Second): 53 fmt.Printf("[ end ]执行完成耗时一秒 %s %d\n", time.Now(), deep) 54 } 55} 56

作用:在不同的协程中传递上下文。

  • 传值 类似于threadLocal
  • 可以使用超时机制,无论往下传递了多少协程,只要最上层时间到了 后面的都不执行
  • 俄罗斯套娃一次一层包装

23. 字符串处理

这是最高频率的操作了,使用任何语言都无法错过。

1package main 2 3import ( 4 "fmt" 5 "strings" 6) 7 8func main() { 9 10 str := " pleuvoir " 11 12 trimSpace := strings.TrimSpace(str) 13 14 fmt.Printf("去除空格 %s\n", trimSpace) 15 16 subString := trimSpace[4:len(trimSpace)] 17 fmt.Printf("subString after is %s\n", subString) 18 19 prefix := strings.HasPrefix(subString, "vo") 20 fmt.Printf("是否有前缀 vo : %v\n", prefix) 21 22 suffix := strings.HasSuffix(subString, "ir") 23 fmt.Printf("是否有后缀 ir : %v\n", suffix) 24 25 builder := strings.Builder{} 26 builder.WriteString("hello") 27 builder.WriteString(" ") 28 builder.WriteString("world") 29 30 fmt.Printf("stringBuilder append is %s\n", builder.String()) 31 32 eles := []string{"1", "2"} 33 34 join := strings.Join(eles, "@") 35 fmt.Printf("join after is %s\n", join) 36 37 //拼接格式化字符串,并且能返回 38 sprintf := fmt.Sprintf("%s@%s", "1", "20") 39 fmt.Printf("Sprintf after is %s\n", sprintf) 40 41 //打印一个对象 比较清晰的方式 42 person := struct { 43 Name string 44 Age int 45 }{"pleuvoir", 18} 46 fmt.Printf("%v", person) // 输出 {Name:pleuvoir Age:18} 47} 48

主要是使用 fmt 包。

24. 任务投递

如果说使用 go 最激动人心的是什么?是大量的协程。如果在下载任务中,我们可以启动很多协程进行分片下载。如下,即展示使用多路复用高速下载。

1package main 2 3import ( 4 "fmt" 5 "sync" 6 "time" 7) 8 9func main() { 10 11 chunks := 10 //文件分成n份 12 workers := 5 //个线程处理 13 14 wg := sync.WaitGroup{} 15 wg.Add(chunks) 16 17 jobs := make(chan int, chunks) //带缓冲的管道 等于任务数 18 19 for i := 0; i < workers; i++ { 20 go handler1(i, jobs, &wg) 21 } 22 23 //将任务全部投递给worker 24 scheduler(jobs, chunks) 25 26 wg.Wait() 27 28 fmt.Println("download finished .") 29} 30 31// 分成 chunks 份任务 里分发 32// 将 n 份下载任务都到管道中去,这里管道数量等于 任务数量n 管道不会阻塞 33func scheduler(jobs chan int, chunks int) { 34 for i := 0; i < chunks; i++ { 35 //time.Sleep(time.Duration(rand.Intn(3)) * time.Second) 36 jobs <- i 37 } 38} 39 40// 写法2 41// 注意这里的是直接接受管道,这也是一种固定写法,下面的 range jobs 可以认为是阻塞去抢这个任务,多个线程都在抢任务 42func handler2(workerId int, jobs <-chan int, wg *sync.WaitGroup) { 43 for job := range jobs { 44 // fmt.Printf("workerId[%d] job[%d] start download .\n", workerId, job) 45 time.Sleep(1 * time.Second) 46 fmt.Printf("workerId[%d] job[%d] download ok.\n", workerId, job) 47 wg.Done() //这里不要break,这样执行完当前的线程就能继续抢了 48 } 49} 50 51// 写法1,select case 多路复用 52func handler1(workerId int, jobs chan int, wg *sync.WaitGroup) { 53 for { 54 select { 55 case job, _ := <-jobs: 56 // fmt.Printf("workerId[%d] job[%d] start download .\n", workerId, job) 57 time.Sleep(3 * time.Second) 58 fmt.Printf("workerId[%d] job[%d] download ok.\n", workerId, job) 59 wg.Done() //这里不要break,这样执行完当前的线程就能继续抢了 60 } 61 } 62} 63

后语

以上都是一个新手 Gopher 的经验总结,文中难免有错误,恳请指正。

作者:京东零售 付伟

来源:京东与开发者社区

点赞
收藏

评论区

加载中...

相关推荐

MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1

文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

MySQL部分从库上面因为大量的临时表tmp_table造成慢查询

背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_

2020年前端实用代码段,为你的工作保驾护航

有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )

mysql中like用法

like的通配符有两种%(百分号):代表零个、一个或者多个字符。\(下划线):代表一个数字或者字符。1\.name以"李"开头wherenamelike'李%'2\.name中包含"云",“云”可以在任何位置wherenamelike'%云%'3\.第二个和第三个字符是0的值wheresalarylike'\00%'4\

VBox 启动虚拟机失败

在Vbox(5.0.8版本)启动Ubuntu的虚拟机时,遇到错误信息:NtCreateFile(\\Device\\VBoxDrvStub)failed:0xc000000034STATUS\_OBJECT\_NAME\_NOT\_FOUND(0retries) (rc101)Makesurethekern