抽时间看看Google的GO语言到底有什么特点。Go说得是不错,自从C依赖,N年没有一个经典的编程语言了,计算机发展了几十年,语言还是C的那一套,是该有所作为了,做起来真的不容易啊。看看GO到底有哪些地方做的很好。
编译打包
python很好,只是依赖于python环境,譬如CentOS5.5上是Python2.5,还没有json。。。
如果在CentOS6上开发的.py,直接放到CentOS5.5,有可能是跑不起来的,这个对于商业化部署还是很头疼的。
一种方式是把Python2.6虚拟机编译出来,还可以用cxfreeze和pyinstaller打包成一个binary,不再依赖于python环境。
一般都是选择后一种了,一般编译出来的文件几兆左右,和用c/c++编译出来的程序没有什么区别。
额,来看看GO,GO其实不是解释性的语言,而是静态语言。所以是可以编译的:
// hello.go
package main
import "fmt"
func main() {
fmt.Printf("hello, world!\n")
}
编译一下:
[winlin@dev6 go-rtmp]$ go build hello.go
[winlin@dev6 go-rtmp]$ ls -lh
-rwxrwxr-x 1 winlin winlin 2.2M Jan 13 22:31 hello
查看依赖,只依赖于libc:
[winlin@dev6 bin]$ ldd gotour
linux-vdso.so.1 => (0x00007fff263ff000)
libpthread.so.0 => /lib64/libpthread.so.0 (0x0000003856600000)
libc.so.6 => /lib64/libc.so.6 (0x0000003855a00000)
/lib64/ld-linux-x86-64.so.2 (0x0000003855200000)
关于GOPATH,其实类似于python的site-packages,譬如:
go get code.google.com/p/go-tour/gotour
这条命令会在$GOPATH下载go-tour以及依赖的包,然后编译出gotour执行文件,直接$GOPATH/bin/gotour执行就可以。
go get相当于执行下面的命令:
这个命令会执行:
mkdir -p $GOPATH/src && cd $GOPATH/src
mkdir -p code.google.com/p && cd code.google.com/p
hg clone https://code.google.com/p/go-tour
然后下载依赖的项目:
cd $GOPATH/src/code.google.com/p
hg clone https://code.google.com/p/go.tools
hg clone https://code.google.com/p/go.net
然后开始编译:
mkdir -p $GOPATH/bin && cd $GOPATH/bin
go build code.google.com/p/go-tour/gotour
其实go get最后一步调用的不是build,而是install:
mkdir -p $GOPATH/bin && cd $GOPATH/bin
go install code.google.com/p/go-tour/gotour
install就会生成pkg。GOPATH就是用来指定这个dir的,可以在任何目录调用go install,会生成到GOPATH这个目录。
go install安装某个package时,要求package的目录结构有规则,可以查看go help gopath。
一般而言,可以用两个GOPATH,一个用来装哪些个依赖包,一个是自己的包。参考:https://code.google.com/p/go-wiki/wiki/GOPATH#Repository_Integration_and_Creating_
譬如,在/etc/profile中设置如下:
# for google go.
export GOROOT=/usr/local/go
export GOPATH=/home/winlin/git/google-go:/home/winlin/git/go-rtmp
export PATH=$PATH:$GOROOT/bin
执行:go get code.google.com/p/go-tour/gotour
会生成如下项目:
[winlin@centos6x86 ~]$ ls /home/winlin/git/google-go/src/code.google.com/p/
go.net go.tools go-tour
外部依赖库就安装到了第一个GOPATH所在的目录了。
在自己的目录下建立package,譬如:mkdir -p /home/winlin/git/go-rtmp/src/hello
然后:vim /home/winlin/git/go-rtmp/src/hello/hello.go
输入以下内容:
package main
import "fmt"
import "math"
func main() {
fmt.Println(math.Pi)
}
在任意位置都都可以编译这个package:
[winlin@centos6x86 ~]$ go build hello
[winlin@centos6x86 ~]$ pwd
/home/winlin
[winlin@centos6x86 ~]$ ls -lh hello
-rwxrwxr-x. 1 winlin winlin 1.8M Jan 14 21:50 hello
[winlin@centos6x86 ~]$ ./hello
3.141592653589793
实际上如果编译一个错误的package,会显示go查找的目录位置:
[winlin@centos6x86 ~]$ go build hells
can't load package: package hells: cannot find package "hells" in any of:
/usr/local/go/src/pkg/hells (from $GOROOT)
/home/winlin/git/google-go/src/hells (from $GOPATH)
/home/winlin/git/go-rtmp/src/hells
可见是先去GOROOT找,然后去所有的GOPATH找。
总之,GO在编译打包上没有问题。
Package依赖关系
GO的核心目标是大规模编程,所以在处理依赖方面必须要很强悍。即用户不需要处理任何编译的依赖关系,go自动处理,只需要遵守语言的package规范即可。
这一点还是真的很赞,要知道编译一个ffmpeg真的不容易,依赖巨多,版本居多,编译错误后可能得找出错的那个库的依赖,以此类推,确实是一件不容易的事情。
GO如何处理这个问题?看看如何编译SRS,分别是c++的和GO的两个版本。
参考:http://dev:6060/doc/code.html
C++版本,参考:https://github.com/winlinvip/simple-rtmp-server
git clone https://github.com/winlinvip/simple-rtmp-server
cd simple-rtmp-server/trunk
./configure --with-ssl --with-hls --with-ffmpeg --with-http
make
其实还好?其实不然,如果在CentOS6下面编译,基本上没有问题,如果换个环境呢?肯定编译失败。原因是configrure做了很多事情,需要安装gcc/g++/make等工具,需要编译nginx/ffmpeg,编译nginx需要安装pcre,安装ffmpeg时需要libaacplus/liblame/libx264,以此类推,真的是不容易的一个脚本。
具体的依赖项目,得用一个wiki才能搞定:https://github.com/winlinvip/simple-rtmp-server/wiki/Build
GO版本,参考:https://github.com/winlinvip/go.srs
export GOPATH=~/mygo
go get github.com/winlinvip/go.srs/go_srs
这样就可以?是的,这样就可以了。
C++的srs编译在:./objs/srs
GO的srs编译在:$GOPATH/bin/go_srs
其实go做了很多事情,因为go.srs依赖的其他package都是按照go的规范写的,所以go get命令可以自动下载需要的依赖包,并且进行编译。
查看GOPATH就知道它做的事情:
[winlin@centos6x86 ~]$ tree $GOPATH
/home/winlin/mygo
├── bin
│ └── go_srs
├── pkg
│ └── linux_386
│ └── github.com
│ └── winlinvip
│ └── go.rtmp
│ └── rtmp.a
└── src
└── github.com
└── winlinvip
├── go.rtmp
│ ├── LICENSE
│ ├── README.md
│ └── rtmp
│ └── version.go
└── go.srs
├── go_srs
│ └── srs.go
├── LICENSE
├── README.md
└── research
└── demo-func
└── func_declare.go
除了go.srs,连go.srs依赖的go.rtmp也自动下载下来并且编译了。
go get等价于下载和安装:
go get -d github.com/winlinvip/go.srs/go_srs
go install github.com/winlinvip/go.srs/go_srs
代价就是package会比较长,好处是一个命令,搞定所有的事情,这个很赞~
并发和并行计算
无疑go的设计目标就是大规模程序,并发和并行计算是很重要也是很大的一个特点。用时髦的词,go为云计算而生。从领域角度讲,go是为写服务器/服务而设计的。
不管用什么词语,云/服务都有一个重要的特点:系统为多人同时提供服务,也就是并发和并行计算。并发只同时支持多人的能力,并行计算指利用多CPU和多机器的计算系统。单进程也可以支持并发,利用linux的epoll和非阻塞异步socket就可以做到,nginx就是典型。只是服务器基本上都是多CPU,所以支持多进程也会有很大的优势,nginx也是典型。
多进程编程可以参考:http://blog.csdn.net/win_lin/article/details/7755773
异步非阻塞能带来最高性能,麻烦的地方就是状态机很复杂;因此对于复杂的状态机,譬如RTMP协议,状态变换巨多,用协程(协程/轻量级线程/用户态线程)等技术就能在异步的基础上使用同步,参考:http://blog.csdn.net/win_lin/article/details/8242653
C/C++并未提供语言级别的协程支持,而是有一些库提供支持(python提供了yield关键字,但支持的不是很完善,有eventlet库支持);go重要的特点就是在语言级别提供支持。
C/C++的库一般只提供了协程的支持,对多进程的支持有限;go同时支持协程和多进程,go的运行时本身是多线程的。
在EffectiveGo中解释得很详细:http://dev:6060/doc/effective_go.html#concurrency
下面开启了两个协程goroutine,不断进行累加运算:
package main
import (
"fmt"
"time"
)
func main() {
var fun = func (id int) {
count := 0
for {
if (count % 1500000000) == 0 {
fmt.Printf("[%v] id=%v, count=%v\n", time.Now().Format("2006-1-06 15:04:05"), id, count)
}
count++
}
}
go fun(101)
go fun(102)
time.Sleep(300 * time.Second)
}
计算结果如下:
C:/Go/bin/go.exe run R:/mygo/go.srs/research/demo/tour/go_concurrency.go
[2014-2-14 11:08:02] id=101, count=0
[2014-2-14 11:08:02] id=102, count=0
[2014-2-14 11:08:07] id=101, count=1500000000
[2014-2-14 11:08:09] id=102, count=1500000000
[2014-2-14 11:08:11] id=101, count=3000000000
[2014-2-14 11:08:14] id=102, count=3000000000
[2014-2-14 11:08:16] id=101, count=4500000000
[2014-2-14 11:08:19] id=102, count=4500000000
[2014-2-14 11:08:21] id=101, count=6000000000
[2014-2-14 11:08:23] id=102, count=6000000000
[2014-2-14 11:08:26] id=101, count=7500000000
[2014-2-14 11:08:28] id=102, count=7500000000
[2014-2-14 11:08:30] id=101, count=9000000000
[2014-2-14 11:08:33] id=102, count=9000000000
[2014-2-14 11:08:35] id=101, count=10500000000
可见这两个goroutine是交替执行的,go的运行时会调度它们。查看CPU,4CPU用到了25%也就是1CPU。
只需要设置一句,就可以利用多CPU多进程并行计算:
runtime.GOMAXPROCS(2)
将使用两个CPU计算,代码如下:
package main
import (
"fmt"
"time"
"runtime"
)
func main() {
var fun = func (id int) {
count := 0
for {
if (count % 1500000000) == 0 {
fmt.Printf("[%v] id=%v, count=%v\n", time.Now().Format("2006-1-06 15:04:05"), id, count)
}
count++
}
}
if runtime.NumCPU() > 1 {
runtime.GOMAXPROCS(2)
}
go fun(101)
go fun(102)
time.Sleep(300 * time.Second)
}
运算结果如下:
C:/Go/bin/go.exe run R:/mygo/go.srs/research/demo/tour/go_parallelization.go
[2014-2-14 11:12:22] id=101, count=0
[2014-2-14 11:12:22] id=102, count=0
[2014-2-14 11:12:25] id=102, count=1500000000
[2014-2-14 11:12:25] id=101, count=1500000000
[2014-2-14 11:12:28] id=102, count=3000000000
[2014-2-14 11:12:28] id=101, count=3000000000
[2014-2-14 11:12:31] id=102, count=4500000000
[2014-2-14 11:12:31] id=101, count=4500000000
[2014-2-14 11:12:34] id=102, count=6000000000
[2014-2-14 11:12:34] id=101, count=6000000000
[2014-2-14 11:12:38] id=102, count=7500000000
[2014-2-14 11:12:38] id=101, count=7500000000
[2014-2-14 11:12:41] id=102, count=9000000000
[2014-2-14 11:12:41] id=101, count=9000000000
[2014-2-14 11:12:44] id=102, count=10500000000
[2014-2-14 11:12:44] id=101, count=10500000000
这两个协程是并行运算的,4CP占用50%即2CPU在工作。
若使用C/C++呢?需要使用库,譬如state-threads,然后多进程需要fork,若需要通信的话,还需要用进程间通信技术,着实很麻烦。
go呢?一个go关键字,即可支持协程和多进程,通信用channel即可。简单~
Reflect反射
反射是元编程概念,参考"The Laws of Reflection":http://dev:6060/blog/laws-of-reflection
简单来讲,reflect的基本类型是Type和Value,即变量的类型信息和值信息。
Type.Elem是获取元素类型,譬如Type为**MyClass,Type.Elem是*MyClass,Type.Elem().Elem()是MyClass。或者说,就是类似于C/C++中*的作用,取指针的值。
Value.Elem和Type.Elem是对应的,是对值进行操作。
Value.CanSet和Value.Set是对变量进行设置操作,和C/C++一样,只有指针才能被设置。
package main
import (
"fmt"
"reflect"
)
type BlackWinlin struct {
id int
}
type RedWinlin struct {
name string
}
func main() {
bw := BlackWinlin{id:10}
var rtmp_pkt *RedWinlin = nil
fmt.Println("rtmp==========================")
rtmp_pkt = nil
if my_rtmp_expect(&bw, &rtmp_pkt) {
fmt.Println("discoveryed pkt from black:", rtmp_pkt)
}
fmt.Println()
rtmp_pkt = nil
if my_rtmp_expect(&RedWinlin{}, &rtmp_pkt) {
fmt.Println("discoveryed pkt from red:", rtmp_pkt)
}
fmt.Println()
fmt.Println("rtmp==========================")
var src_black_pkt *BlackWinlin = &bw
var src_red_pkt *RedWinlin = &RedWinlin{name: "hello"}
rtmp_pkt = nil
if my_rtmp_expect(&src_black_pkt, &rtmp_pkt) {
fmt.Println("discoveryed pkt from black:", rtmp_pkt)
}
fmt.Println()
rtmp_pkt = nil
if my_rtmp_expect(&src_red_pkt, &rtmp_pkt) {
fmt.Println("discoveryed pkt from red:", rtmp_pkt)
}
fmt.Println()
fmt.Println("rtmp==========================")
// set the value which is ptr to ptr
var prtmp_pkt **RedWinlin = nil
if my_rtmp_expect(&src_red_pkt, prtmp_pkt) {
fmt.Println("discoveryed pkt from red(ptr):", prtmp_pkt)
fmt.Println("discoveryed pkt from red(value):", *prtmp_pkt)
}
prtmp_pkt = &rtmp_pkt
if my_rtmp_expect(&src_red_pkt, prtmp_pkt) {
fmt.Println("discoveryed pkt from red(ptr):", prtmp_pkt)
fmt.Println("discoveryed pkt from red(value):", *prtmp_pkt)
}
}
func my_rtmp_expect(pkt interface {}, v interface {}) (ok bool){
/*
func my_rtmp_expect(pkt interface {}, v interface {}){
rt := reflect.TypeOf(v)
rv := reflect.ValueOf(v)
// check the convertible and convert to the value or ptr value.
// for example, the v like the c++ code: Msg**v
pkt_rt := reflect.TypeOf(pkt)
if pkt_rt.ConvertibleTo(rt){
// directly match, the pkt is like c++: Msg**pkt
// set the v by: *v = *pkt
rv.Elem().Set(reflect.ValueOf(pkt).Elem())
return
}
if pkt_rt.ConvertibleTo(rt.Elem()) {
// ptr match, the pkt is like c++: Msg*pkt
// set the v by: *v = pkt
rv.Elem().Set(reflect.ValueOf(pkt))
return
}
}
*/
ok = false
pkt_rt := reflect.TypeOf(pkt)
pkt_rv := reflect.ValueOf(pkt)
pkt_ptr_rt := reflect.PtrTo(pkt_rt)
rt := reflect.TypeOf(v)
rv := reflect.ValueOf(v)
if rv.Kind() != reflect.Ptr || rv.IsNil() {
fmt.Println("expect must be ptr and not nil")
return
}
fmt.Println("type info, src:", pkt_rt, "ptr(src):", pkt_ptr_rt, ", expect:", rt)
fmt.Println("value info, src:", pkt_rv, ", src.Elem():", pkt_rv.Elem(), ", expect:", rv, ", expect.Elem():", rv.Elem())
fmt.Println("convertible src=>expect:", pkt_rt.ConvertibleTo(rt))
fmt.Println("ptr convertible ptr(src)=>expect:", pkt_ptr_rt.ConvertibleTo(rt))
fmt.Println("elem convertible src=>expect.Elem()", pkt_rt.ConvertibleTo(rt.Elem()))
fmt.Println("settable src:", pkt_rv.CanSet(), ", expect:", rv.CanSet())
fmt.Println("elem settable src:", pkt_rv.Elem().CanSet(), ", expect:", rv.Elem().CanSet())
// check the convertible and convert to the value or ptr value.
// for example, the v like the c++ code: Msg**v
if rv.Elem().CanSet() {
if pkt_rt.ConvertibleTo(rt){
// directly match, the pkt is like c++: Msg**pkt
// set the v by: *v = *pkt
fmt.Println("directly match, src=>expect")
rv.Elem().Set(pkt_rv.Elem())
ok = true
return
}
if pkt_rt.ConvertibleTo(rt.Elem()) {
// ptr match, the pkt is like c++: Msg*pkt
// set the v by: *v = pkt
fmt.Println("pointer match, src=>*expect")
rv.Elem().Set(pkt_rv)
ok = true
return
}
fmt.Println("not match, donot set expect")
} else {
fmt.Println("expect cannot set")
}
return
}
GO语言的问题
GO语言RTMP服务器:https://github.com/winlinvip/go.srs
C++语言RTMP服务器:https://github.com/winlinvip/simple-rtmp-server
GO只有C++性能的1/30,在50个连接时,GO占用CPU50%,C++只占用2%。当然,应该是代码写的有问题。至少目前还没有办法转向GO。