抽时间看看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。
有疑问加站长微信联系(非本文作者)