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传递消息。
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!
未完待续