这一周,利用每天晚上下班回来后的一小时,学习了Google开发的Go语言,算是对其有了个基本的了解。确实是门漂亮的语言。
首先,从它的设计目标是设计一种高效的、静态编译的、易于编写的语言。它涉足的是系统级的编程,试图与C/C++抗衡。
详细来说,它的设计目标有如下几点(来自wikipedia和golang FAQ):
- 安全:类型安全与内存安全。没有继承,无需处理类型的依赖关系,弱化类型的使用;变量默认初始化,简化设计负担。
- 并发和通信的支持。内建的并发机制使得多线程编程变得非常简单;内建的chan(channel)类型简化了线程间通讯。
- 完全的内存垃圾回收机制。
- 高速编译。没有头文件、Makefile等复杂的工程依赖关系,使得编译速度更快,工程更容易组织。
而在我看来,通过一周的学习,给我留下最深印象的,是如下几个方面:
- 更符合自然语言的语法。类型的声明放在变量后面,实战发现确实比放在前面更易读。
- 方便的内建类型。string、map、数组等,这些复杂的类型都内建于语言中。
- 内建的并发机制。对于多线程程序的编写支持非常好。
- 没有类、只有结构和接口。只是很不习惯,目前还没有发现这样做的好处。
- 文档。从基本的语法、包的文档,到教程、设计建议等。对于理解Go,写好Go非常有帮助。
下面我将以我在教程中写的一些代码为例,说说我对上面几点的理解。
自然的语法
go语言的语法,非常符合英语语法的习惯(英语语法较汉语更具有逻辑性,更能清楚的解释问题)。
比如
定义一个变量:x int,用英语来读就是x of type int;
定义一个函数:func add(x, y int) int,读出来就是:a function named add, with parameter x & y of type int, that returns int。非常自然。
当然,要显示出这种定义的自然性,我们可以看一个复杂的例子,函数指针:
首先看看C语言的定义方式:
int (*fp)(int (*ff)(int x, int y), int b)它定义了一个函数指针fp,指向一个以函数指针ff和b为参数,并返回int的函数。其中ff指向一个以x,y为参数,并返回int的函数。x!真复杂
如果用Go呢?
fp func(ff func(x, y int) int, b int) int
是不是刚好与上面的描述符合?
这种符合自然语言语法的定义方式,简化了代码理解的步骤,也不容易出错。
关于此部分更详细的内容,可以参考Go's Declaration Syntax。
下面给出一个hello world程序,一睹为快:
// 类似Java,用包名来组织代码 package main import "fmt" // 程序的“入口”,main函数。 func main() { fmt.Println("Hello, 世界") // 没有return语句 }
方便的内建类型
这里我以tour.golang.org中的一个练习为例子,介绍go语言中的map和string。
这个练习要求:
实现WordCount函数。此函数输入一句英文语句,并返回一个map类型,存储每个单词对应的重复次数。主函数以及写好,包含一个wc.Test函数,用于测试WordCount函数的正确性。
提示:strings.Fields可能会很有帮助。
下面是我的实现:
package main import ( "tour/wc" "strings" ) func WordCount(s string) map[string]int { // 创建一个键为string,值为int的map // make可以用来创建任何类型的变量。 // 比如make([]int, 3)是创建3个元素的int数组 m := make(map[string]int) // 变量的使用可以不用显示的指明类型 // 这里,words的类型即Fields的返回值类型,是个字符串数组 words := strings.Fields(s) // Go语言没有while、do-while // for 条件 { 执行体 } 即相当于while // for { 执行体 } 即无限循环 // 这里,使用for的range特性,取words的索引和值 // 分别给_和word,下划线_相当于一个占位符,不赋值给具体的变量 // 同样,还可以使用:i, _ := range words,表示只需要其索引 // 甚至可以使用:_, _ := range words,表示只需要循环相应次数即可 for _, word := range words { // 根据键取map中的值,并修改 // Go是内存安全的语言,如果m中不存在word键 // 将会自动创建一个word,并初始化其值为0 m[word]++; } return m } func main() { wc.Test(WordCount) }
由这个例子,我们可以了解到Go语言的很多特性,比如_, word := range words这样的多个赋值同时进行(也可用于函数返回值),比如内建的string、map类型,比如简化的循环体(没有括号,去掉while,do-while,支持多种循环条件的定义),还有代码的组织方式等。
结构体和接口
Go语言没有类的概念,没有构造、析构函数,更没有继承。只有结构体和接口。
下面以Exercise: Images为例,介绍Go语言的结构体和接口的使用:
package main import ( "image" "image/color" "tour/pic" ) // 定义Image类型 // 类似的定义还可以这样:type MyInt int // 相当于typedef type Image struct{ content [][]uint32 // 二维数组,存储图片内容 // 包含每个像素点的RGBA值。 width, height int // 图片宽度和高度 } // 自定义的像素点函数,返回给定点的RGBA值 func valueOfPointer(x, y int) uint32 { return uint32(0xfffff*x^(0xfffff*y + 0xff)) } // 自定义的图片生成函数,用于使用给定的像素点函数生成一幅图片 func makePic(w, h int, f func(int,int) uint32) *Image { img := new(Image) img.width = w img.height = h // 此处先申请一个长度为w,类型为[]uint32的数组 img.content = make([][]uint32, w) for x := 0; x < w; x++ { // 再为每个数组的元素申请h长度的uint32型数组 // 由此而创建出一块 w x h 的二维数组 img.content[x] = make([]uint32, h) // 使用f函数为每个像素赋值 for y := 0; y < h; y++ { img.content[x][y] = f(x, y) } } return img } //////////////////////////////////////////// // 接下来的几个函数是接口image.Image的函数实现 // Go语言中,无需显示的申明实现接口 // 只需要实现接口的所有函数,即实现了接口 // Bounds 函数返回图片的可用区域 // 在 func 和函数名之间加上类型,表示此函数是该类型的成员函数 // 注意,此处的类型不仅限于结构体,比如浮点数、整数都可以。 func (img *Image) Bounds() image.Rectangle { return image.Rect(0, 0, img.width, img.height) } // ColorModel 函数指明图片使用的颜色模式 // 这里,我们选用RGBA模式 func (img *Image) ColorModel() color.Model { return color.RGBAModel } // At 函数,返回指定像素点的颜色属性 func (img *Image) At(x, y int) color.Color { // 根据练习的说明设置超出范围的点的颜色 if x >= img.width || y >= img.height { return color.RGBA{uint8(x), uint8(y), 0xff, 0xff} } // 根据存储的二维数组,生成RGBA模式的颜色并返回 var c = img.content[x][y] return color.RGBA{ uint8((c >> 24) & 0xff), uint8((c >> 16) & 0xff), uint8((c >> 8) & 0xff), uint8(c & 0xff) } } func main() { m := makePic(200, 100, valueOfPointer) // 调用pic类的ShowImage来显示生成的图片 pic.ShowImage(m) }
可能是我还未理解Go语言的精髓,暂时没有发现这种没有类、甚至没有显示继承关系的设计有怎样的优势。如果有知道的朋友一定要告诉我,非常感谢!
内建的并发机制
Go语言内建了并发机制,无需第三方库的支持就可以方便的创建线程。并且,Go语言包含一个chan类型用于线程间的变量传递,降低了使用共享内存传递的风险,有些类似于unix里的管道。
下面先以一个Equivalent Binary Trees的例子,来介绍并发以及chan的使用,并进一步熟悉Go语言:
package main import ( "fmt" "tour/tree" "sort" ) // Walk 遍历t,将其所有的内容由ch发送出去 // 我使用递归的方式实现了它 // 注意,Go语言的channel是有类型的 func Walk(t *tree.Tree, ch chan int) { if t.Left != nil { Walk(t.Left, ch) } if t.Right != nil { Walk(t.Right, ch) } // 使用 <- 符号将变量值发送到chan ch <- t.Value } // Same 决定t1、t2是否是具有相同内容的两棵树 func Same(t1, t2 *tree.Tree) bool { // 创建两个具有缓存的管道 // 管理发送的线程将不停的发送,直至缓存溢出, // 等到管理接收的线程取出值以后,才能继续发送 // 这相当于一个固定大小的队列。 ch1 := make(chan int, 10) ch2 := make(chan int, 10) // 使用两个数组来存储树的内容 a1 := make([]int, 10) a2 := make([]int, 10) // 新建两个线程,同时开始遍历 go Walk(t1, ch1) go Walk(t2, ch2) // 主线程将不停的接收另外两个线程传来的数据 for i := 0; i < 10; i++ { a1[i] = <- ch1 a2[i] = <- ch2 } // 排序以便于检查其内容是否一致 sort.Ints(a1) sort.Ints(a2) for i := 0; i < 10; i++ { if a1[i] != a2[i] { return false } } return true } func main() { // tree.New(k)可以创建内容包含k, 2k, 3k, ..., nk的树 t1 := tree.New(1) t2 := tree.New(2) ch := make(chan int, 10) // 打印t1树 fmt.Print("t1: ") go Walk(t1, ch) for i := 0; i < 10; i++ { fmt.Printf("%2d, ", <- ch) } fmt.Println() // 打印t2树 fmt.Print("t2: ") go Walk(t2, ch) for i := 0; i < 10; i++ { fmt.Printf("%2d, ", <- ch) } fmt.Println() // 测试Same函数 fmt.Printf("Is %v that t1 equal t1.\n", Same(t1, t1)) fmt.Printf("Is %v that t1 equal t2.\n", Same(t1, t2)) }
由例子可以看到管道的使用方式: <- 。发送可以用 channel <- value,接收可以用 variable := <- channel。非常形象。
Go语言中的并发是基于函数的。使用 go function() 即可使此函数在新的线程中运行,父线程将继续运行,不会等待函数结束。
并发更复杂更灵活的运用,请看练习:Web Crawler。
OK,关于Go的简单介绍就到这里,更详细的文档请参阅Go语言官方网站:http://golang.org/。
本专题(Go语言学习)所涉及的代码已经同步到GitHub上,便于分享,下面是链接:
https://github.com/tankery/study-go
注:从今天起,我将以每星期至少一篇的频率写这份博客。
至于每周一篇的频率,是由我常年加班的工作性质决定的。每周一篇,法定节日休息。。。
有疑问加站长微信联系(非本文作者)