Go语言的那些事儿(第一篇)

Golang的特点

说明:本文大量借鉴了文章,在此表示感谢。

  • 部署简单

Golang编译生成的是一个静态可执行文件,除了 glibc 外没有其他外部依赖,完全不需要操心应用所需的各种包、库的依赖关系,大大减轻了维护的负担。

  • 并发性好

Goroutine和Channel机制使得编写高并发的服务端软件变得相当容易,很多情况下完全不需要考虑锁机制以及由此带来的问题。单个Go应用也能有效的利用多个CPU核,并行执行的性能好。

  • 性能优良

Golang占用的CPU资源更少,通常比一般语言(Java和Python)更节省资源。【这里存在争议】

GO程序员的五个进化阶段:

第一个阶段(菜逼): 刚刚学习了这门语言。 已经通过一些教程或者培训班了解基本的语法,可以写短的代码片段。

第二个阶段 (探索者): 可以写一个完整的程序,但不懂一些更高级的语言特征,比如“channels”。还没有使用GO写一个大项目。

第三个阶段(大手): 你能熟练的使用Go, 能够用GO去解决,生产环境中一个具体和完整的问题。已经形成了一套自己的惯用法和常用代码库。在你的编码方案中Go是一个非常好用的工具。

第四阶段 (大神): 绝逼清楚Go语言的设计选择和背后的动机。能理解的简洁和可组合性哲学。

布道师: 积极地与他人分享关于Go语言知识和你对Go语言的理解。在各种合适的场所发出自己的声音, 参与邮件列表、建立QQ群、做专题报告。成为一个布道者不见得是一个完全独立的阶段,这个角色可以在上述的任何一个阶段中。

并发支持

  • goroutine

goroutine非常类似线程。通过 go f()的方式使用,开启一个新的goroutine去完成相应任务。

  • chennel

goroutine开始执行,但是如何知道执行结束呢?这就需要使用channel传递消息。

Goroutine

channel的缓冲区

有缓冲的channel

var a string
var c = make(chan int, 1)

func f() {
    a = "hello, world"
    c <- 0
}

func main() {
    go f()
    <-c
    print(a)
}

无缓冲的channel

var a string
var c = make(chan int)

func f() {
    a = "hello, world"
    <-c
}

func main() {
    go f()
    c <- 0
    print(a)
}

Go语言的并发

从语言层面支持并发是Go语言最大的特色。为什么很多人形容Go是为云计算而生的语言呢?主要原因就是在分布式系统中,并发是必须考虑的一个问题,Go语言能够快速高效地处理这个问题,受到广大云计算开发者的青睐。我们尝试从原理上理解下Go语言是如何处理高并发的。

我们观察下面的程序:

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

func main() {
    loop()
    loop()
}

我们得到的输出结果:

0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9

如果我们把一个loop放到子线程(goroutine)中去跑,我们会得到如下结果:

0 1 2 3 4 5 6 7 8 9

奇怪的是,为什么只输出了一次呢?主线程一次,子routine一次,应该是两次才是啊。原来,在goroutine还没准备跑的时候,主函数已经退出了。main函数退出太快了,我们要想办法阻止它过早地退出,一个办法是让main等待一下:

func main() {
    go loop()
    loop()
    time.Sleep(time.Second) // 停顿一秒
}

这次确实输出了两次,目的达到了。可是采用等待的办法并不好,首先我们并不知道go routine要跑多久。其次这里有硬编码。这里,在GO里面有标准的方式之一是waitgroup方法。

我们看看使用WaitGroup的例子

package main

import (
    "fmt"
    "sync"
)

func main() {
    var wg sync.WaitGroup

    wg.Add(1)

    go func() {
        defer wg.Done()
        for i := 0; i < 10; i++ {
            fmt.Printf("i=%v\n", i)
        }
    }()

    for i := 0; i < 10; i++ {
        fmt.Printf("i=%v\n", i)
    }

    wg.Wait()
}

waitgroup会等待goroutine退出。

除了使用waitgroup之外,我们还可以使用channel,我们看看channel怎么使用:

package main

import (
    "fmt"
)

func main() {
    var messages chan string = make(chan string)
    go func(message string) {
        messages <- message // 存消息
    }("Ping!")

    fmt.Println(<-messages) // 取消息
}

这里需要说明的是,默认信道的存消息和取消息都是阻塞的。也就是说, 无缓冲的信道在取消息和存消息的时候都会挂起当前的goroutine,除非另一端已经准备好。比如以下的main函数和foo函数:

var ch chan int = make(chan int)

func foo() {
    ch <- 0  // 向ch中加数据,如果没有其他goroutine来取走这个数据,那么挂起foo, 直到main函数把0这个数据拿走
}

func main() {
    go foo()
    <- ch // 从ch取数据,如果ch中还没放数据,那就挂起main线,直到foo函数中放数据为止
}

那既然信道可以阻塞当前的goroutine, 那么回到上一部分「goroutine」所遇到的问题「如何让goroutine告诉主线我执行完毕了」 的问题来, 使用一个信道来告诉主线即可:

package main

import (
    "fmt"
)

var complete chan int = make(chan int)

func loop() {
    for i := 0; i < 10; i++ {
        fmt.Printf("%d ", i)
    }

    complete <- 0 // 执行完毕了,发个消息
}

func main() {
    go loop()
    <-complete // 直到线程跑完, 取到消息. main在此阻塞住
}

如果不用信道来阻塞主线的话,主线就会过早跑完,loop线都没有机会执行。

其实,无缓冲的信道永远不会存储数据,只负责数据的流通,为什么这么讲呢?
* 从无缓冲信道取数据,必须要有数据流进来才可以,否则当前线阻塞
* 数据流入无缓冲信道, 如果没有其他goroutine来拿走这个数据,那么当前线阻塞

所以,你可以测试下,无论如何,我们测试到的无缓冲信道的大小都是0 (len(channel))

如果信道正有数据在流动,我们还要加入数据,或者信道枯竭,我们一直向无数据流入的空信道取数据呢? 就会引起死锁

死锁

一个死锁的例子:

func main() {
    ch := make(chan int)
    <- ch // 阻塞main goroutine, 信道c被锁
}

执行这个程序你会看到Go报这样的错误:

fatal error: all goroutines are asleep - deadlock!

未完待续

猜你喜欢

转载自blog.csdn.net/afandaafandaafanda/article/details/67640307