希望你看到这篇文章的时候还是在公交车和地铁上正在上下班的时间,我希望我的这篇文章可以让你利用这段时间了解一门语言。当然,希望你不会因为看我的文章而错过站。呵呵。

如果你还不了解Go语言的语法,还请你移步先看一下上篇——《Go语言简介(上):语法
GO 语言简介(下)— 特性 - 图2

goroutine

GoRoutine 主要是使用 go 关键字来调用函数,你还可以使用匿名函数,如下所示:

  1. package main
  2. import "fmt"
  3. func f(msg string) {
  4. fmt.Println(msg)
  5. }
  6. func goroutineTest() {
  7. msg := "goroutine"
  8. go f(msg)
  9. go func(msg string) {
  10. fmt.Println(msg)
  11. }(msg)
  12. //让主进程停住,不然主进程退了,goroutine也就退了
  13. var input string
  14. fmt.Scanln(&input)
  15. fmt.Println("done")
  16. }
  17. func main(){
  18. goroutineTest()
  19. }

image.png
我们再来看一个示例,下面的代码中包括很多内容,包括时间处理,随机数处理,还有 goroutine 的代码。如果你熟悉 C 语言,你应该会很容易理解下面的代码。

你可以简单的把 go 关键字调用的函数想像成 **pthread_create**。下面的代码使用 **for** 循环创建了 **3** 个线程,每个线程使用一个随机的 **Sleep** 时间,然后在 routine() 函数中会输出一些线程执行的时间信息。

  1. package main
  2. import "fmt"
  3. import "time"
  4. import "math/rand"
  5. func routine(name string, delay time.Duration) {
  6. t0 := time.Now()
  7. fmt.Println(name, " start at ", t0)
  8. time.Sleep(delay)
  9. t1 := time.Now()
  10. fmt.Println(name, " end at ", t1)
  11. fmt.Println(name, " lasted ", t1.Sub(t0))
  12. }
  13. func main() {
  14. //生成随机种子
  15. rand.Seed(time.Now().Unix())
  16. var name string
  17. for i:=0; i<3; i++{
  18. name = fmt.Sprintf("go_%02d", i) //生成ID
  19. //生成随机等待时间,从0-4秒
  20. go routine(name, time.Duration(rand.Intn(5)) * time.Second)
  21. }
  22. //让主进程停住,不然主进程退了,goroutine也就退了
  23. var input string
  24. fmt.Scanln(&input)
  25. fmt.Println("done")
  26. }

运行的结果可能是:

  1. go_00 start at 2012-11-04 19:46:35.8974894 +0800 +0800
  2. go_01 start at 2012-11-04 19:46:35.8974894 +0800 +0800
  3. go_02 start at 2012-11-04 19:46:35.8974894 +0800 +0800
  4. go_01 end at 2012-11-04 19:46:36.8975894 +0800 +0800
  5. go_01 lasted 1.0001s
  6. go_02 end at 2012-11-04 19:46:38.8987895 +0800 +0800
  7. go_02 lasted 3.0013001s
  8. go_00 end at 2012-11-04 19:46:39.8978894 +0800 +0800
  9. go_00 lasted 4.0004s

goroutine的并发安全性

关于goroutine,我试了一下,无论是Windows还是Linux,基本上来说是用操作系统的线程来实现的。不过,goroutine有个特性,也就是说,如果一个goroutine没有被阻塞,那么别的goroutine就不会得到执行。这并不是真正的并发,如果你要真正的并发,你需要在你的main函数的第一行加上下面的这段代码:

  1. import "runtime"
  2. ...
  3. runtime.GOMAXPROCS(4)

还是让我们来看一个有并发安全性问题的示例(注意:我使用了C的方式来写这段Go的程序)
这是一个经常出现在教科书里卖票的例子,我启了5个goroutine来卖票,卖票的函数sell_tickets很简单,就是随机的sleep一下,然后对全局变量total_tickets作减一操作。

  1. package main
  2. import "fmt"
  3. import "time"
  4. import "math/rand"
  5. import "runtime"
  6. func main() {
  7. sellTicketsTest()
  8. }
  9. var totalTickets int32 = 10
  10. func sellTicketsTest() {
  11. runtime.GOMAXPROCS(3) //我的电脑是3核处理器,所以我设置了3
  12. rand.Seed(time.Now().Unix()) //生成随机种子
  13. for i := 0; i < 5; i++ { //并发5个goroutine来卖票
  14. go sellTickets(i)
  15. }
  16. //等待线程执行完
  17. var input string
  18. fmt.Scanln(&input)
  19. fmt.Println(totalTickets, "done") //退出时打印还有多少票
  20. }
  21. func sellTickets(i int) {
  22. for {
  23. if totalTickets > 0 { //如果有票就卖
  24. time.Sleep(time.Duration(rand.Intn(5)) * time.Millisecond)
  25. totalTickets-- //卖一张票
  26. //window平台获取线程ID方法: windows.GetCurrentThreadId()
  27. fmt.Println("id:", i, " ticket:", totalTickets, "ThreadId:", windows.GetCurrentThreadId())
  28. } else {
  29. break
  30. }
  31. }
  32. }

这个程序毋庸置疑有并发安全性问题,所以执行起来你会看到下面的结果:

  1. PS D:\Projects\Github\NoobWu\github.com\samples> go run simple.go
  2. id: 2 ticket: 8 ThreadId: 11392
  3. id: 0 ticket: 8 ThreadId: 2520
  4. id: 4 ticket: 7 ThreadId: 2520
  5. id: 2 ticket: 6 ThreadId: 11392
  6. id: 4 ticket: 5 ThreadId: 3152
  7. id: 3 ticket: 4 ThreadId: 2520
  8. id: 1 ticket: 3 ThreadId: 11392
  9. id: 4 ticket: 2 ThreadId: 2520
  10. id: 3 ticket: 1 ThreadId: 11392
  11. id: 2 ticket: 0 ThreadId: 2520
  12. id: 4 ticket: -1 ThreadId: 11392
  13. id: 0 ticket: -2 ThreadId: 11392
  14. id: 1 ticket: -3 ThreadId: 11392
  15. id: 3 ticket: -4 ThreadId: 3152

image.png
可见,我们需要使用上锁,我们可以使用互斥量来解决这个问题。下面的代码,我只列出了修改过的内容:

  1. package main
  2. import "fmt"
  3. import "time"
  4. import "math/rand"
  5. import "sync"
  6. import "runtime"
  7. var totalTickets int32 = 10
  8. var mutex = &sync.Mutex{} //可简写成:var mutex sync.Mutex
  9. func sellTicketsAndLockTest() {
  10. runtime.GOMAXPROCS(3) //我的电脑是3核处理器,所以我设置了3
  11. rand.Seed(time.Now().Unix()) //生成随机种子
  12. for i := 0; i < 5; i++ { //并发5个goroutine来卖票
  13. go sellTicketsAndLock(i)
  14. }
  15. //等待线程执行完
  16. var input string
  17. fmt.Scanln(&input)
  18. fmt.Println(totalTickets, "done") //退出时打印还有多少票
  19. }
  20. func sellTicketsAndLock(i int) {
  21. for totalTickets > 0 {
  22. mutex.Lock()
  23. if totalTickets > 0 {
  24. time.Sleep(time.Duration(rand.Intn(5)) * time.Millisecond)
  25. totalTickets--
  26. //window平台获取线程ID方法: windows.GetCurrentThreadId()
  27. fmt.Println("id:", i, " ticket:", totalTickets, "ThreadId:", windows.GetCurrentThreadId())
  28. }
  29. mutex.Unlock()
  30. }
  31. }

image.png

原子操作

说到并发就需要说说原子操作,相信大家还记得我写的那篇《无锁队列的实现》一文,里面说到了一些CAS – CompareAndSwap的操作。Go语言也支持。你可以看一下相当的文档
我在这里就举一个很简单的示例:下面的程序有10个goroutine,每个会对cnt变量累加20次,所以,最后的cnt应该是200。如果没有atomic的原子操作,那么cnt将有可能得到一个小于200的数。
下面使用了atomic操作,所以是安全的。

  1. package main
  2. import "fmt"
  3. import "time"
  4. import "sync/atomic"
  5. func main() {
  6. var cnt uint32 = 0
  7. for i := 0; i < 10; i++ {
  8. go func() {
  9. for i:=0; i<20; i++ {
  10. time.Sleep(time.Millisecond)
  11. atomic.AddUint32(&cnt, 1)
  12. }
  13. }()
  14. }
  15. time.Sleep(time.Second)//等一秒钟等goroutine完成
  16. cntFinal := atomic.LoadUint32(&cnt)//取数据
  17. fmt.Println("cnt:", cntFinal)
  18. }

这样的函数还有很多,参看go的atomic包文档(被墙)

Channel 信道

Channal是什么?Channal就是用来通信的,就像Unix下的管道一样,在Go中是这样使用Channel的。
下面的程序演示了一个goroutine和主程序通信的例程。这个程序足够简单了。

  1. package main
  2. import "fmt"
  3. func main() {
  4. //创建一个string类型的channel
  5. channel := make(chan string)
  6. //创建一个goroutine向channel里发一个字符串
  7. go func() { channel <- "hello" }()
  8. msg := <- channel
  9. fmt.Println(msg)
  10. }

指定channel的buffer
指定buffer的大小很简单,看下面的程序:

  1. package main
  2. import "fmt"
  3. func main() {
  4. channel := make(chan string, 2)
  5. go func() {
  6. channel <- "hello"
  7. channel <- "World"
  8. }()
  9. msg1 := <-channel
  10. msg2 := <-channel
  11. fmt.Println(msg1, msg2)
  12. }

Channel的阻塞
注意,channel默认上是阻塞的,也就是说,如果Channel满了,就阻塞写,如果Channel空了,就阻塞读。于是,我们就可以使用这种特性来同步我们的发送和接收端。
下面这个例程说明了这一点,代码有点乱,不过我觉得不难理解。

  1. package main
  2. import "fmt"
  3. import "time"
  4. func main() {
  5. channel := make(chan string) //注意: buffer为1
  6. go func() {
  7. channel <- "hello"
  8. fmt.Println("write \"hello\" done!")
  9. channel <- "World" //Reader在Sleep,这里在阻塞
  10. fmt.Println("write \"World\" done!")
  11. fmt.Println("Write go sleep...")
  12. time.Sleep(3*time.Second)
  13. channel <- "channel"
  14. fmt.Println("write \"channel\" done!")
  15. }()
  16. time.Sleep(2*time.Second)
  17. fmt.Println("Reader Wake up...")
  18. msg := <-channel
  19. fmt.Println("Reader: ", msg)
  20. msg = <-channel
  21. fmt.Println("Reader: ", msg)
  22. msg = <-channel //Writer在Sleep,这里在阻塞
  23. fmt.Println("Reader: ", msg)
  24. }

上面的代码输出的结果如下:

  1. Reader Wake up...
  2. Reader: hello
  3. write "hello" done!
  4. write "World" done!
  5. Write go sleep...
  6. Reader: World
  7. write "channel" done!
  8. Reader: channel

Channel阻塞的这个特性还有一个好处是,可以让我们的goroutine在运行的一开始就阻塞在从某个channel领任务,这样就可以作成一个类似于线程池一样的东西。关于这个程序我就不写了。我相信你可以自己实现的。
多个Channel的select

  1. package main
  2. import "time"
  3. import "fmt"
  4. func main() {
  5. //创建两个channel - c1 c2
  6. c1 := make(chan string)
  7. c2 := make(chan string)
  8. //创建两个goruntine来分别向这两个channel发送数据
  9. go func() {
  10. time.Sleep(time.Second * 1)
  11. c1 <- "Hello"
  12. }()
  13. go func() {
  14. time.Sleep(time.Second * 1)
  15. c2 <- "World"
  16. }()
  17. //使用select来侦听两个channel
  18. for i := 0; i < 2; i++ {
  19. select {
  20. case msg1 := <-c1:
  21. fmt.Println("received", msg1)
  22. case msg2 := <-c2:
  23. fmt.Println("received", msg2)
  24. }
  25. }
  26. }

注意:上面的select是阻塞的,所以,才搞出ugly的for i <2这种东西**。**
Channel select阻塞的Timeout
解决上述那个for循环的问题,一般有两种方法:一种是阻塞但有timeout,一种是无阻塞。我们来看看如果给select设置上timeout的。

  1. for {
  2. timeout_cnt := 0
  3. select {
  4. case msg1 := <-c1:
  5. fmt.Println("msg1 received", msg1)
  6. case msg2 := <-c2:
  7. fmt.Println("msg2 received", msg2)
  8. case <-time.After(time.Second * 30):
  9. fmt.Println("Time Out")
  10. timout_cnt++
  11. }
  12. if time_cnt > 3 {
  13. break
  14. }
  15. }


上面代码中高亮的代码主要是用来让select返回的,注意 case中的time.After事件。
Channel的无阻塞
好,我们再来看看无阻塞的channel,其实也很简单,就是在select中加入default,如下所示:

  1. for {
  2. select {
  3. case msg1 := <-c1:
  4. fmt.Println("received", msg1)
  5. case msg2 := <-c2:
  6. fmt.Println("received", msg2)
  7. default: //default会导致无阻塞
  8. fmt.Println("nothing received!")
  9. time.Sleep(time.Second)
  10. }
  11. }


Channel的关闭
关闭Channel可以通知对方内容发送完了,不用再等了。参看下面的例程:

  1. package main
  2. import "fmt"
  3. import "time"
  4. import "math/rand"
  5. func main() {
  6. channel := make(chan string)
  7. rand.Seed(time.Now().Unix())
  8. //向channel发送随机个数的message
  9. go func () {
  10. cnt := rand.Intn(10)
  11. fmt.Println("message cnt :", cnt)
  12. for i:=0; i<cnt; i++{
  13. channel <- fmt.Sprintf("message-%2d", i)
  14. }
  15. close(channel) //关闭Channel
  16. }()
  17. var more bool = true
  18. var msg string
  19. for more {
  20. select{
  21. //channel会返回两个值,一个是内容,一个是还有没有内容
  22. case msg, more = <- channel:
  23. if more {
  24. fmt.Println(msg)
  25. }else{
  26. fmt.Println("channel closed!")
  27. }
  28. }
  29. }
  30. }

定时器

Go语言中可以使用time.NewTimer或time.NewTicker来设置一个定时器,这个定时器会绑定在你的当前channel中,通过channel的阻塞通知机器来通知你的程序。
下面是一个timer的示例。

  1. package main
  2. import "time"
  3. import "fmt"
  4. func main() {
  5. timer := time.NewTimer(2*time.Second)
  6. <- timer.C
  7. fmt.Println("timer expired!")
  8. }

上面的例程看起来像一个Sleep,是的,不过Timer是可以Stop的。你需要注意Timer只通知一次。如果你要像C中的Timer能持续通知的话,你需要使用Ticker。下面是Ticker的例程:

  1. package main
  2. import "time"
  3. import "fmt"
  4. func main() {
  5. ticker := time.NewTicker(time.Second)
  6. for t := range ticker.C {
  7. fmt.Println("Tick at", t)
  8. }
  9. }

上面的这个ticker会让你程序进入死循环,我们应该放其放在一个goroutine中。下面这个程序结合了timer和ticker

  1. package main
  2. import "time"
  3. import "fmt"
  4. func main() {
  5. ticker := time.NewTicker(time.Second)
  6. go func () {
  7. for t := range ticker.C {
  8. fmt.Println(t)
  9. }
  10. }()
  11. //设置一个timer,10钞后停掉ticker
  12. timer := time.NewTimer(10*time.Second)
  13. <- timer.C
  14. ticker.Stop()
  15. fmt.Println("timer expired!")
  16. }


Socket编程

下面是我尝试的一个Echo Server的Socket代码,感觉还是挺简单的。
Server端

  1. package main
  2. import (
  3. "net"
  4. "fmt"
  5. "io"
  6. )
  7. const RECV_BUF_LEN = 1024
  8. func main() {
  9. listener, err := net.Listen("tcp", "0.0.0.0:6666")//侦听在6666端口
  10. if err != nil {
  11. panic("error listening:"+err.Error())
  12. }
  13. fmt.Println("Starting the server")
  14. for {
  15. conn, err := listener.Accept() //接受连接
  16. if err != nil {
  17. panic("Error accept:"+err.Error())
  18. }
  19. fmt.Println("Accepted the Connection :", conn.RemoteAddr())
  20. go EchoServer(conn)
  21. }
  22. }
  23. func EchoServer(conn net.Conn) {
  24. buf := make([]byte, RECV_BUF_LEN)
  25. defer conn.Close()
  26. for {
  27. n, err := conn.Read(buf);
  28. switch err {
  29. case nil:
  30. conn.Write( buf[0:n] )
  31. case io.EOF:
  32. fmt.Printf("Warning: End of data: %s \n", err);
  33. return
  34. default:
  35. fmt.Printf("Error: Reading data : %s \n", err);
  36. return
  37. }
  38. }
  39. }

Client端

  1. package main
  2. import (
  3. "fmt"
  4. "time"
  5. "net"
  6. )
  7. const RECV_BUF_LEN = 1024
  8. func main() {
  9. conn,err := net.Dial("tcp", "127.0.0.1:6666")
  10. if err != nil {
  11. panic(err.Error())
  12. }
  13. defer conn.Close()
  14. buf := make([]byte, RECV_BUF_LEN)
  15. for i := 0; i < 5; i++ {
  16. //准备要发送的字符串
  17. msg := fmt.Sprintf("Hello World, %03d", i)
  18. n, err := conn.Write([]byte(msg))
  19. if err != nil {
  20. println("Write Buffer Error:", err.Error())
  21. break
  22. }
  23. fmt.Println(msg)
  24. //从服务器端收字符串
  25. n, err = conn.Read(buf)
  26. if err !=nil {
  27. println("Read Buffer Error:", err.Error())
  28. break
  29. }
  30. fmt.Println(string(buf[0:n]))
  31. //等一秒钟
  32. time.Sleep(time.Second)
  33. }
  34. }

系统调用

Go语言那么C,所以,一定会有一些系统调用。Go语言主要是通过两个包完成的。一个是os包,一个是syscall包。(注意,链接被墙)
这两个包里提供都是Unix-Like的系统调用,

  • syscall里提供了什么Chroot/Chmod/Chmod/Chdir…,Getenv/Getgid/Getpid/Getgroups/Getpid/Getppid…,还有很多如Inotify/Ptrace/Epoll/Socket/…的系统调用。
  • os包里提供的东西不多,主要是一个跨平台的调用。它有三个子包,Exec(运行别的命令), Signal(捕捉信号)和User(通过uid查name之类的)

syscall包的东西我不举例了,大家可以看看《Unix高级环境编程》一书。
os里的取几个例:
环境变量

  1. package main
  2. import "os"
  3. import "strings"
  4. func main() {
  5. os.Setenv("WEB", "https://coolshell.cn") //设置环境变量
  6. println(os.Getenv("WEB")) //读出来
  7. for _, env := range os.Environ() { //穷举环境变量
  8. e := strings.Split(env, "=")
  9. println(e[0], "=", e[1])
  10. }
  11. }


执行命令行

下面是一个比较简单的示例

  1. package main
  2. import "os/exec"
  3. import "fmt"
  4. func main() {
  5. cmd := exec.Command("ping", "127.0.0.1")
  6. out, err := cmd.Output()
  7. if err!=nil {
  8. println("Command Error!", err.Error())
  9. return
  10. }
  11. fmt.Println(string(out))
  12. }

正规一点的用来处理标准输入和输出的示例如下:

  1. package main
  2. import (
  3. "strings"
  4. "bytes"
  5. "fmt"
  6. "log"
  7. "os/exec"
  8. )
  9. func main() {
  10. cmd := exec.Command("tr", "a-z", "A-Z")
  11. cmd.Stdin = strings.NewReader("some input")
  12. var out bytes.Buffer
  13. cmd.Stdout = &out
  14. err := cmd.Run()
  15. if err != nil {
  16. log.Fatal(err)
  17. }
  18. fmt.Printf("in all caps: %q\n", out.String())
  19. }


命令行参数

Go语言中处理命令行参数很简单:(使用os的Args就可以了)

  1. func main() {
  2. args := os.Args
  3. fmt.Println(args) //带执行文件的
  4. fmt.Println(args[1:]) //不带执行文件的
  5. }

在Windows下,如果运行结果如下:
C:\Projects\Go>go run args.go aaa bbb ccc ddd<br />[C:\Users\haoel\AppData\Local\Temp\go-build742679827\command-line-arguments_<br />obj\a.out.exe aaa bbb ccc ddd]<br />[aaa bbb ccc ddd]
那么,如果我们要搞出一些像 mysql -uRoot -hLocalhost -pPwd 或是像 cc -O3 -Wall -o a a.c 这样的命令行参数我们怎么办?Go提供了一个package叫flag可以容易地做到这一点

  1. package main
  2. import "flag"
  3. import "fmt"
  4. func main() {
  5. //第一个参数是“参数名”,第二个是“默认值”,第三个是“说明”。返回的是指针
  6. host := flag.String("host", "coolshell.cn", "a host name ")
  7. port := flag.Int("port", 80, "a port number")
  8. debug := flag.Bool("d", false, "enable/disable debug mode")
  9. //正式开始Parse命令行参数
  10. flag.Parse()
  11. fmt.Println("host:", *host)
  12. fmt.Println("port:", *port)
  13. fmt.Println("debug:", *debug)
  14. }

执行起来会是这个样子:

  1. #如果没有指定参数名,则使用默认值
  2. $ go run flagtest.go
  3. host: coolshell.cn
  4. port: 80
  5. debug: false
  6. #指定了参数名后的情况
  7. $ go run flagtest.go -host=localhost -port=22 -d
  8. host: localhost
  9. port: 22
  10. debug: true
  11. #用法出错了(如:使用了不支持的参数,参数没有=)
  12. $ go build flagtest.go
  13. $ ./flagtest -debug -host localhost -port=22
  14. flag provided but not defined: -debug
  15. Usage of flagtest:
  16. -d=false: enable/disable debug mode
  17. -host="coolshell.cn": a host name
  18. -port=80: a port number
  19. exit status 2

感觉还是挺不错的吧。

一个简单的HTTP Server

代码胜过千言万语。呵呵。这个小程序让我又找回以前用C写CGI的时光了。(Go的官方文档是《Writing Web Applications》)

  1. package main
  2. import (
  3. "fmt"
  4. "net/http"
  5. "io/ioutil"
  6. "path/filepath"
  7. )
  8. const http_root = "/home/haoel/coolshell.cn/"
  9. func main() {
  10. http.HandleFunc("/", rootHandler)
  11. http.HandleFunc("/view/", viewHandler)
  12. http.HandleFunc("/html/", htmlHandler)
  13. http.ListenAndServe(":8080", nil)
  14. }
  15. //读取一些HTTP的头
  16. func rootHandler(w http.ResponseWriter, r *http.Request) {
  17. fmt.Fprintf(w, "rootHandler: %s\n", r.URL.Path)
  18. fmt.Fprintf(w, "URL: %s\n", r.URL)
  19. fmt.Fprintf(w, "Method: %s\n", r.Method)
  20. fmt.Fprintf(w, "RequestURI: %s\n", r.RequestURI )
  21. fmt.Fprintf(w, "Proto: %s\n", r.Proto)
  22. fmt.Fprintf(w, "HOST: %s\n", r.Host)
  23. }
  24. //特别的URL处理
  25. func viewHandler(w http.ResponseWriter, r *http.Request) {
  26. fmt.Fprintf(w, "viewHandler: %s", r.URL.Path)
  27. }
  28. //一个静态网页的服务示例。(在http_root的html目录下)
  29. func htmlHandler(w http.ResponseWriter, r *http.Request) {
  30. fmt.Printf("htmlHandler: %s\n", r.URL.Path)
  31. filename := http_root + r.URL.Path
  32. fileext := filepath.Ext(filename)
  33. content, err := ioutil.ReadFile(filename)
  34. if err != nil {
  35. fmt.Printf(" 404 Not Found!\n")
  36. w.WriteHeader(http.StatusNotFound)
  37. return
  38. }
  39. var contype string
  40. switch fileext {
  41. case ".html", "htm":
  42. contype = "text/html"
  43. case ".css":
  44. contype = "text/css"
  45. case ".js":
  46. contype = "application/javascript"
  47. case ".png":
  48. contype = "image/png"
  49. case ".jpg", ".jpeg":
  50. contype = "image/jpeg"
  51. case ".gif":
  52. contype = "image/gif"
  53. default:
  54. contype = "text/plain"
  55. }
  56. fmt.Printf("ext %s, ct = %s\n", fileext, contype)
  57. w.Header().Set("Content-Type", contype)
  58. fmt.Fprintf(w, "%s", content)
  59. }

Go的功能库有很多,大家自己慢慢看吧。我再吐个槽——Go的文档真不好读。例子太少了
先说这么多吧。这是我周末两天学Go语言学到的东西,写得太仓促了,而且还有一些东西理解不到位,还大家请指正!
(全文完)
GO 语言简介(下)— 特性 - 图6 GO 语言简介(下)— 特性 - 图7
关注CoolShell微信公众账号和微信小程序
https://coolshell.cn/articles/8489.html