Go语言编程入门超级指南
1.序言
golang作为一门出身名门望族的编程语言新星,像豆瓣的redis平台codis、类evernote的云笔记leanote等。
1.1 为什么要学习
如果有人说x语言比y语言好,两方的支持者经常会激烈地争吵。如果你是某种语言老手,你就是那门语言的“传道者”,下意识地会保护它。无论承认与否,你都已被困在一个隧道里,你看到的完全是局限的。《肖申克的救赎》对此有很好的注脚:
[red] these walls are funny. first you hate ‘em, then you get used to ‘em. enough time passes, you get so you depend on them. that's institutionalized.
这些墙很有趣。起初你恨它们,之后你习惯了它们。随着时间流逝,你开始以来它们。这就是*。
在你还没有被完全“*化”时,为何不多学些语言,哪怕只是浅尝辄止,潜移默化中也许你的思维壁垒就松动了。不管是golang还是ruby还是其他语言,当看到一些语法习惯与之前熟悉的c和java不同时,的确潜意识里就会产生抵触情绪,觉得这不好,还是自己习惯的那套好。长此以往,如果不能冲破自己的心理,“坐以待毙”,被时间淘汰恐怕只是早晚的事儿。所以这里的关键也 不是非要学习golang,而是要不断地学!
1.2 用什么工具来开发
golang也有专门的ide,但由于最近迷上了sublime text神器,所以这里还是用st来学习golang。配置步骤与在st中使用其他语言开发都类似:
安装智能提示插件gosublime
创建编译配置脚本
点preferences -> package settings -> gosublime -> user settings中写入(感觉保存时自动格式化出来的缩进、空格等风格有些“讨厌”,所以就禁掉了):
{
"fmt_enabled": false,
"env": {
"path":"d:\\program files (x86)\\go\bin"
}
}
点新建build system产生go.sublime-build中写入:
{
"path": "d:\\program files (x86)\\go\\bin",
"cmd": ["go", "run", "${file}"],
"selector": "source.go"
}
2.你好,世界
golang版的helloworld来了!一眼望去,package和import的声明方式与java如出一辙,比较明显的区别是:func关键字、每行末尾没有分号、println()大写的函数名。这个例子虽小,却“五脏俱全”,后面会逐一分析这个小例子中碰到的golang语法点。
package main
import "fmt"
func main() {
fmt.println("你好,世界!")
}
2.1 运行方式
golang提供了go run“解释”执行和go build编译执行两种运行方式,所谓的“解释”执行其实也是编译出了可执行文件后才执行的。
$ go run helloworld.go
你好,世界!
$ go build helloworld.go
$ ls
helloworld helloworld.go
$ ./helloworld
你好,世界!
2.2 package管理
上面例子中我们使用的就是fmt包下的println()函数。golang约定:我们可以用./或../相对路径来引自己的package;如果不是相对路径,那么go会去$gopath/src下查找。
2.3 格式化输出
类似c、java等语言,golang的fmt包提供了格式化输出功能,而且像%d、%s等占位符和\t、\r、\n转义也几乎完全一致。但golang的println不支持格式化,只有printf支持,所以我们经常会在后面加入\n换行。此外,golang加入了%t打印值的类型,%v打印数组等集合的所有元素。
package main
import "fmt"
import "math"
/**
* this is printer!
* 布尔值:false
* 二进制:11111111
* 八进制:377
* 十六进制:ff
* 十进制:255
* 浮点数:3.141593
* 字符串:printer
*
* 对象类型:int,string,bool,float64
* 集合:[1 2 3 4 5]
*/
func main() {
fmt.println("this is printer!")
fmt.printf("布尔值:%t\n", 1 == 2)
fmt.printf("二进制:%b\n", 255)
fmt.printf("八进制:%o\n", 255)
fmt.printf("十六进制:%x\n", 255)
fmt.printf("十进制:%d\n", 255)
fmt.printf("浮点数:%f\n", math.pi)
fmt.printf("字符串:%s\n", "printer")
fmt.printf("对象类型:%t,%t,%t,%t\n", 1, "hello", true, math.e)
fmt.printf("集合:%v\n", [5]int{1, 2, 3, 4, 5})
}
3.语法基础
3.1 变量和常量
虽然golang是静态类型语言,却用类似javascript中的var关键字声明变量。而且像同样是静态语言的scala一样,支持类型自动推断。有一点很重要的不同是:如果明确指明变量类型的话,类型要放在变量名后面。这有点别扭吧?!后面会看到函数的入参和返回值的类型也要这样声明。
package main
import "fmt"
/**
* 单变量声明:num[100], word[hello]
* 多变量声明:i[1], i[2], k[3]
* 推导类型:b1[true], b2[false]
* 常量:age[20], pi[3.141593]
*/
func main() {
var num int = 100
var word string = "hello"
fmt.printf("单变量声明:num[%d], word[%s]\n", num, word)
var i, j, k int = 1, 2, 3
fmt.printf("多变量声明:i[%d], i[%d], k[%d]\n", i, j, k)
var b1 = true
b2 := false
fmt.printf("推导类型:b1[%t], b2[%t]\n", b1, b2)
const age int = 20
const pi float32 = 3.1415926
fmt.printf("常量:age[%d], pi[%f]\n", age, pi)
}
3.2 控制语句
作为最基本的语法要素,golang的各种控制语句也是特点鲜明。在对c继承发扬的同时,也有自己的想法融入其中:
if/switch/for的条件部分都没有圆括号,但必须有花括号。
switch的case中不需要break。《c专家编程》里也“控诉”了c的fall-through问题。既然90%以上的情况都要break,为何不将break作为case的默认行为?而且编程语言后来者也鲜有纠正这一问题的。
switch的case条件可以是多个值。
golang中没有while。
package main
import "fmt"
/**
* testif: x[2] is even
* testif: x[3] is odd
*
* testswitch: one
* testswitch: two
* testswitch: three, four, five [3]
* testswitch: three, four, five [4]
* testswitch: three, four, five [5]
*
* 标准模式:[0] [1] [2] [3] [4] [5] [6]
* while模式:[0] [1] [2] [3] [4] [5] [6]
* 死循环模式:[0] [1] [2] [3] [4] [5] [6]
*/
func main() {
testif(2)
testif(3)
testswitch(1)
testswitch(2)
testswitch(3)
testswitch(4)
testswitch(5)
testfor(7)
}
func testif(x int) {
if x % 2 == 0 {
fmt.printf("testif: x[%d] is even\n", x)
} else {
fmt.printf("testif: x[%d] is odd\n", x)
}
}
func testswitch(i int) {
switch i {
case 1:
fmt.println("testswitch: one")
case 2:
fmt.println("testswitch: two")
case 3, 4, 5:
fmt.printf("testswitch: three, four, five [%d]\n", i)
default:
fmt.printf("testswitch: invalid value[%d]\n", i)
}
}
func testfor(upper int) {
fmt.print("标准模式:")
for i := 0; i < upper; i++ {
fmt.printf("[%d] ", i)
}
fmt.println()
fmt.print("while模式:")
j := 0
for j < upper {
fmt.printf("[%d] ", j)
j++
}
fmt.println()
fmt.print("死循环模式:")
k := 0
for {
if (k >= upper) {
break
}
fmt.printf("[%d] ", k)
k++
}
fmt.println()
}
分号和花括号
分号由词法分析器在扫描源代码过程自动插入的,分析器使用简单的规则:如果在一个新行前方的最后一个标记是一个标识符(包括像int和float64这样的单词)、一个基本的如数值这样的文字、或break continue fallthrough return ++ – ) }中的一个时,它就会自动插入分号。
分号的自动插入规则产生了“蝴蝶效应”:所有控制结构的左花括号不都能放在下一行。因为按照上面的规则,这样做会导致分析器在左花括号的前方插入一个分号,从而引起难以预料的结果。所以golang中是不能随便换行的。
3.3 函数
函数有几点不同:
func关键字。
最大的不同就是“倒序”的类型声明。
不需要函数原型,引用的函数可以后定义。这一点很好,真不喜欢c语言里要么将“最底层抽象”的函数放在最前面定义,要么写一堆函数原型声明在最前面。
3.4 集合
golang提供了数组和map作为基本数据结构:
数组中的元素会自动初始化,例如int数组元素初始化为0
切片(借鉴python)的区间跟主流语言一样,都是 “左闭右开”
用 range()遍历数组和map
package main
import "fmt"
/**
* array未初始化: [0 0 0 0 0]
* array赋值: [0 10 0 20 0]
* array初始化: [0 1 2 3 4 5]
* array二维: [[0 1 2] [1 2 3]]
* array切片: [2 3] [0 1 2 3] [2 3 4 5]
*
* map哈希表:map[one:1 two:2 three:3],长度[3]
* map删除元素后:map[one:1 three:3],长度[2]
* map打印:
* one => 1
* four => 4
* three => 3
* five => 5
*/
func main() {
testarray()
testmap()
}
func testarray() {
var a [5]int
fmt.println("array未初始化: ", a)
a[1] = 10
a[3] = 20
fmt.println("array赋值: ", a)
b := []int{0, 1, 2, 3, 4, 5}
fmt.println("array初始化: ", b)
var c [2][3]int
for i := 0; i < 2; i++ {
for j := 0; j < 3; j++ {
c[i][j] = i + j
}
}
fmt.println("array二维: ", c)
d := b[2:4] // b[3,4]
e := b[:4] // b[1,2,3,4]
f := b[2:] // b[3,4,5]
fmt.println("array切片:", d, e, f)
}
func testmap() {
m := make(map[string]int)
m["one"] = 1
m["two"] = 2
m["three"] = 3
fmt.printf("map哈希表:%v,长度[%d]\n", m, len(m))
delete(m, "two")
fmt.printf("map删除元素后:%v,长度[%d]\n", m, len(m))
m["four"] = 4
m["five"] = 5
fmt.println("map打印:")
for key, val := range m {
fmt.printf("\t%s => %d\n", key, val)
}
fmt.println()
}
3.5 指针和内存分配
golang中可以使用指针,并提供了两种内存分配机制:
new:分配长度为0的空白内存,返回类型t*。
make:仅用于 切片、map、chan消息管道,返回类型t而不是指针。
package main
import "fmt"
/**
* 整数i=[10],指针pint=[0x184000c0],指针指向*pint=[10]
* 整数i=[3],指针pint=[0x184000c0],指针指向*pint=[3]
* 整数i=[5],指针pint=[0x184000c0],指针指向*pint=[5]
*
* wild的数组指针: <nil>
* wild的数组指针==nil[true]
*
* new分配的数组指针: &[]
* new分配的数组指针[0x18443010],长度[0]
* new分配的数组指针==nil[false]
* new分配的数组指针make后: &[0 0 0 0 0 0 0 0 0 0]
* new分配的数组元素[3]: 23
*
* make分配的数组引用: [0 0 0 0 0 0 0 0 0 0]
*/
func main() {
testpointer()
testmemallocate()
}
func testpointer() {
var i int = 10;
var pint *int = &i;
fmt.printf("整数i=[%d],指针pint=[%p],指针指向*pint=[%d]\n",
i, pint, *pint)
*pint = 3
fmt.printf("整数i=[%d],指针pint=[%p],指针指向*pint=[%d]\n",
i, pint, *pint)
i = 5
fmt.printf("整数i=[%d],指针pint=[%p],指针指向*pint=[%d]\n",
i, pint, *pint)
}
func testmemallocate() {
var pnil *[]int
fmt.println("wild的数组指针:", pnil)
fmt.printf("wild的数组指针==nil[%t]\n", pnil == nil)
var p *[]int = new([]int)
fmt.println("new分配的数组指针:", p)
fmt.printf("new分配的数组指针[%p],长度[%d]\n", p, len(*p))
fmt.printf("new分配的数组指针==nil[%t]\n", p == nil)
//error occurred
//(*p)[3] = 23
*p = make([]int, 10)
fmt.println("new分配的数组指针make后:", p)
(*p)[3] = 23
fmt.println("new分配的数组元素[3]:", (*p)[3])
var v []int = make([]int, 10)
fmt.println("make分配的数组引用:", v)
}
3.6 面向对象编程
golang的结构体跟c有几点不同:
结构体可以有方法,其实也就相当于oop中的类了。
支持带名称的初始化。
用指针访问结构中的属性也用”.”而不是”->”,指针就像java中的引用一样。
没有public,protected,private等访问权限控制。c也没有protected,c中默认是public的,private需要加static关键字限定。golang中方法名大写就是public的,小写就是private的。
同时,golang支持接口和多态,而且接口有别于java中继承和实现的方式,而是采取了类似ruby中更为新潮的duck type。只要struct与interface有相同的方法,就认为struct实现了这个接口。就好比只要能像鸭子那样叫,我们就认为它是一只鸭子一样。
package main
import (
"fmt"
"math"
)
// -----------------
// struct
// -----------------
type person struct {
name string
age int
email string
}
func (p *person) getname() string {
return p.name
}
// -------------------
// interface
// -------------------
type shape interface {
area() float64
}
type rect struct {
width float64
height float64
}
func (r *rect) area() float64 {
return r.width * r.height
}
type circle struct {
radius float64
}
func (c *circle) area() float64 {
return math.pi * c.radius * c.radius
}
// -----------------
// test
// -----------------
/**
* 结构person[{cdai 30 cdai@gmail.com}],姓名[cdai]
* 结构person指针[&{cdai 30 cdai@gmail.com}],姓名[cdai]
* 用指针修改结构person为[{carter 40 cdai@gmail.com}]
*
* shape[0]周长为[13.920000]
* shape[1]周长为[58.088048]
*/
func main() {
teststruct()
testinterface()
}
func teststruct() {
p1 := person{"cdai", 30, "cdai@gmail.com"}
p1 = person{name: "cdai", age: 30, email: "cdai@gmail.com"}
fmt.printf("结构person[%v],姓名[%s]\n", p1, p1.getname())
ptr1 := &p1
fmt.printf("结构person指针[%v],姓名[%s]\n", ptr1, ptr1.getname())
ptr1.age = 40
ptr1.name = "carter"
fmt.printf("用指针修改结构person为[%v]\n", p1)
}
func testinterface() {
r := rect { width: 2.9, height: 4.8 }
c := circle { radius: 4.3 }
s := []shape{ &r, &c }
for i, sh := range s {
fmt.printf("shape[%d]周长为[%f]\n", i, sh.area())
}
}
3.7 异常处理
golang中异常的使用比较简单,可以用errors.new创建,也可以实现error接口的方法来自定义异常类型,同时利用函数的多返回值特性可以返回异常类。比较复杂的是defer和recover关键字的使用。golang没有采取try-catch“包住”可能出错代码的这种方式,而是用 延迟处理 的方式。
用defer调用的函数会以后进先出(lifo)的方式,在当前函数结束后依次顺行执行。defer的这一特点正好可以用来处理panic。当panic被调用时,它将立即停止当前函数的执行并开始逐级解开函数堆栈,同时运行所有被defer的函数。如果这种解开达到堆栈的顶端,程序就死亡了。但是,也可以使用内建的recover函数来重新获得go程的控制权并恢复正常的执行。由于仅在解开期间运行的代码处在被defer的函数之内,recover仅在被延期的函数内部才是有用的。
package main
import (
"fmt"
"errors"
"os"
)
/**
* 自定义error类型,实现内建error接口
* type error interface {
* error() string
* }
*/
type myerror struct {
arg int
msg string
}
func (e *myerror) error() string {
return fmt.sprintf("%d - %s", e.arg, e.msg)
}
/**
* failed[*errors.errorstring]: bad arguments - negative!
* success: 16
* failed[*main.myerror]: 1000 - bad arguments - too large!
*
* recovered! panic message[cannot find specific file]
* 4 3 2 1 0
*/
func main() {
// 1.test error
args := []int{-1, 4, 1000}
for _, i := range args {
if r, e := testerror(i); e != nil {
fmt.printf("failed[%t]: %v\n", e, e)
} else {
fmt.println("success: ", r)
}
}
// 2.test defer
src, err := os.open("control.go")
if (err != nil) {
fmt.printf("打开文件错误[%v]\n", err)
return
}
defer src.close()
// use src...
for i := 0; i < 5; i++ {
defer fmt.printf("%d ", i)
}
// 3.test panic/recover
defer func() {
if r := recover(); r != nil {
fmt.printf("recovered! panic message[%s]\n", r)
}
}()
_, err2 := os.open("test.go")
if (err2 != nil) {
panic("cannot find specific file")
}
}
func testerror(arg int) (int, error) {
if arg < 0 {
return -1, errors.new("bad arguments - negative!")
} else if arg > 256 {
return -1, &myerror{ arg, "bad arguments - too large!" }
} else {
return arg * arg, nil
}
}
4.高级特性
上面介绍的只是golang的基本语法和特性,尽管像控制语句的条件不用圆括号、函数多返回值、switch-case默认break、函数闭包、集合切片等特性相比java的确提高了开发效率,但这些在其他语言中也都有,并不是golang能真正吸引人的地方。不仅是golang,我们学习任何语言当然都是从基本语法特性着手,但学习时要不断地问自己:使这门语言区别于其他语言的”独到之处“在哪?这种独到之处往往反映了语言的设计思想、出发点、要解决的”痛点“,这才是一门语言或任何技术的立足之本。
4.1 goroutine
goroutine使用go关键字来调用函数,也可以使用匿名函数。可以简单的把go关键字调用的函数想像成pthread_create。如果一个goroutine没有被阻塞,那么别的goroutine就不会得到执行。也就是说goroutine阻塞时,golang会切换到其他goroutine执行,这是非常好的特性!java对类似goroutine这种的协程没有原生支持,像akka最害怕的就是阻塞。因为协程不等同于线程,操作系统不会帮我们完成“现场”保存和恢复,所以要实现goroutine这种特性,就要模拟操作系统的行为,保存方法或函数在协程“上下文切换”时的context,当阻塞结束时才能正确地切换回来。像kilim等协程库利用字节码生成,能够胜任,而akka完全是运行时的。
注意:如果你要真正的并发,需要调用runtime.gomaxprocs(cpu_num)设置。
package main
import "fmt"
func main() {
go f("goroutine")
go func(msg string) {
fmt.println(msg)
}("going")
// block main thread
var input string
fmt.scanln(&input)
fmt.println("done")
}
func f(msg string) {
fmt.println(msg)
}
4.2 原子操作
像java一样,golang支持很多cas操作。运行结果是unsaftcnt可能小于200,因为unsafecnt++在机器指令层面上不是一条指令,而可能是从内存加载数据到寄存器、执行自增运算、保存寄存器中计算结果到内存这三部分,所以不进行保护的话有些更新是会丢失的。
package main
import (
"fmt"
"time"
"sync/atomic"
"runtime"
)
func main() {
// important!!!
runtime.gomaxprocs(4)
// thread-unsafe
var unsafecnt int32 = 0
for i := 0; i < 10; i++ {
go func() {
for i := 0; i < 20; i++ {
time.sleep(time.millisecond)
unsafecnt++
}
}()
}
time.sleep(time.second)
fmt.println("cnt: ", unsafecnt)
// cas toolkit
var cnt int32 = 0
for i := 0; i < 10; i++ {
go func() {
for i := 0; i < 20; i++ {
time.sleep(time.millisecond)
atomic.addint32(&cnt, 1)
}
}()
}
time.sleep(time.second)
cntfinal := atomic.loadint32(&cnt)
fmt.println("cnt: ", cntfinal)
}
神奇cas的原理
golang的addint32()类似于java中atomicinteger.incrementandget(),其伪代码可以表示如下。二者的基本思想是一致的,本质上是 乐观锁:首先,从内存位置m加载要修改的数据到寄存器a中;然后,修改数据并保存到另一寄存器b;最终,利用cpu提供的cas指令(java通过jni调用到)用一条指令完成:1)a值与m处的原值比较;2)若相同则将b值覆盖到m处。
若不相同,则cas指令会失败,说明从内存加载到执行cas指令这一小段时间内,发生了上下文切换,执行了其他线程的代码修改了m处的变量值。那么重新执行前面几个步骤再次尝试。
aba问题:即另一线程修改了m位置的数据,但是从原值改为c,又从c改回原值。这样上下文切换回来,cas指令发现m处的值“未改变”(实际是改了两次,最后改回来了),所以cas指令正常执行,不会失败。这种问题在java中可以用atomicstampedreference/atomicmarkablereference解决。
public final int incrementandget() {
for (;;) {
int current = get();
int next = current + 1;
if (compareandset(current, next))
return next;
}
}
4.3 channel管道
通过前面可以看到,尽管goroutine很方便很高效,但如果滥用的话很可能会导致并发安全问题。而channel就是用来解决这个问题的,它是goroutine之间通信的桥梁,类似actor模型中每个actor的mailbox。多个goroutine要修改一个状态时,可以将请求都发送到一个channel里,然后由一个goroutine负责顺序地修改状态。
channel默认是阻塞的,也就是说select时如果没有事件,那么当前goroutine会发生读阻塞。同理,channel是有大小的,当channel满了时,发送方会发生写阻塞。channel这种阻塞的特性加上goroutine可以很容易就能实现生产者-消费者模式。
用case可以给channel设置阻塞的超时时间,避免一直阻塞。而default则使select进入无阻塞模式。
package main
import (
"fmt"
"time"
)
/**
* output:
* received message: hello
* received message: world
*
* received from channel-1: hello
* received from channel-2: world
*
* received message: hello
* time out!
*
* nothing received!
* received message: hello
* nothing received!
* nothing received!
* nothing received!
* nothing received!
* nothing received!
* nothing received!
* nothing received!
* nothing received!
* nothing received!
* received message: world
* nothing received!
* nothing received!
* nothing received!
*/
func main() {
listenonchannel()
selecttwochannels()
blockchannelwithtimeout()
unblockchannel()
}
func listenonchannel() {
// specify channel type and buffer size
channel := make(chan string, 5)
go func() {
channel <- "hello"
channel <- "world"
}()
for i := 0; i < 2; i++ {
msg := <- channel
fmt.println("received message: " + msg)
}
}
func selecttwochannels() {
c1 := make(chan string)
c2 := make(chan string)
go func() {
time.sleep(time.second)
c1 <- "hello"
}()
go func() {
time.sleep(time.second)
c2 <- "world"
}()
for i := 0; i < 2; i++ {
select {
case msg1 := <- c1:
fmt.println("received from channel-1: " + msg1)
case msg2 := <- c2:
fmt.println("received from channel-2: " + msg2)
}
}
}
func blockchannelwithtimeout() {
channel := make(chan string, 5)
go func() {
channel <- "hello"
// sleep 10 sec
time.sleep(time.second * 10)
channel <- "world"
}()
for i := 0; i < 2; i++ {
select {
case msg := <- channel:
fmt.println("received message: " + msg)
// set timeout 5 sec
case <- time.after(time.second * 5):
fmt.println("time out!")
}
}
}
func unblockchannel() {
channel := make(chan string, 5)
go func() {
channel <- "hello"
time.sleep(time.second * 10)
channel <- "world"
}()
for i := 0; i < 15; i++ {
select {
case msg := <- channel:
fmt.println("received message: " + msg)
default:
fmt.println("nothing received!")
time.sleep(time.second)
}
}
}
4.4 缓冲流
golang的bufio包提供了方便的缓冲流操作,通过strings或网络io得到流后,用bufio.newreader/writer()包装:
缓冲区:peek()或read时,数据会从底层进入到缓冲区。缓冲区默认大小为4096字节。
切片和拷贝:peek()和readslice()得到的都是切片(缓冲区数据的引用)而不是拷贝,所以更加节约空间。但是当缓冲区数据变化时,切片也会随之变化。而readbytes/string()得到的都是数据的拷贝,可以放心使用。
unicode支持:readrune()可以直接读取unicode字符。有意思的是golang中unicode字符也要用单引号,这点与java不同。
分隔符:readslice/bytes/string()得到的包含分隔符,bufio不会自动去掉。
writer:对应地,writer提供了writebytes/string/rune。
undo方法:可以将读出的字节再放回到缓冲区,就像什么都没发生一样。
package main
import (
"fmt"
"strings"
"bytes"
"bufio"
)
/**
* buffered: 0
* buffered after peek: 7
* abcde
* axcde
*
* abcdefghijklmnopqrst 20 <nil>
* uvwxyz1234567890 16 <nil>
* 0 eof
*
* "abc "
* "def "
* "ghi"
*
* "abc "
* "def "
* "ghi"
*
* read unicode=[你], size=[3]
* read unicode=[好], size=[3]
* read(after undo) unicode=[好], size=[3]
*
* available: 4096
* buffered: 0
* available after write: 4088
* buffered after write: 8
* buffer after write: ""
* available after flush: 4096
* buffered after flush: 0
* buffer after flush: "abcdefgh"
*
* hello,世界!
*/
func main() {
testpeek()
testread()
testreadslice()
testreadbytes()
testreadunicode()
testwrite()
testwritebyte()
}
func testpeek() {
r := strings.newreader("abcdefg")
br := bufio.newreader(r)
fmt.printf("buffered: %d\n", br.buffered())
p, _ := br.peek(5)
fmt.printf("buffered after peek: %d\n", br.buffered())
fmt.printf("%s\n", p)
p[1] = 'x'
p, _ = br.peek(5)
fmt.printf("%s\n", p)
}
func testread() {
r := strings.newreader("abcdefghijklmnopqrstuvwxyz1234567890")
br := bufio.newreader(r)
b := make([]byte, 20)
n, err := br.read(b)
fmt.printf("%-20s %-2v %v\n", b[:n], n, err)
n, err = br.read(b)
fmt.printf("%-20s %-2v %v\n", b[:n], n, err)
n, err = br.read(b)
fmt.printf("%-20s %-2v %v\n", b[:n], n, err)
}
func testreadslice() {
r := strings.newreader("abc def ghi")
br := bufio.newreader(r)
w, _ := br.readslice(' ')
fmt.printf("%q\n", w)
w, _ = br.readslice(' ')
fmt.printf("%q\n", w)
w, _ = br.readslice(' ')
fmt.printf("%q\n", w)
}
func testreadbytes() {
r := strings.newreader("abc def ghi")
br := bufio.newreader(r)
w, _ := br.readbytes(' ')
fmt.printf("%q\n", w)
w, _ = br.readslice(' ')
fmt.printf("%q\n", w)
s, _ := br.readstring(' ')
fmt.printf("%q\n", s)
}
func testreadunicode() {
r := strings.newreader("你好,世界!")
br := bufio.newreader(r)
c, size, _ := br.readrune()
fmt.printf("read unicode=[%c], size=[%v]\n", c, size)
c, size, _ = br.readrune()
fmt.printf("read unicode=[%c], size=[%v]\n", c, size)
br.unreadrune()
c, size, _ = br.readrune()
fmt.printf("read(after undo) unicode=[%c], size=[%v]\n", c, size)
}
func testwrite() {
b := bytes.newbuffer(make([]byte, 0))
bw := bufio.newwriter(b)
fmt.printf("available: %d\n", bw.available())
fmt.printf("buffered: %d\n", bw.buffered())
bw.writestring("abcdefgh")
fmt.printf("available after write: %d\n", bw.available())
fmt.printf("buffered after write: %d\n", bw.buffered())
fmt.printf("buffer after write: %q\n", b)
bw.flush()
fmt.printf("available after flush: %d\n", bw.available())
fmt.printf("buffered after flush: %d\n", bw.buffered())
fmt.printf("buffer after flush: %q\n", b)
}
func testwritebyte() {
b := bytes.newbuffer(make([]byte, 0))
bw := bufio.newwriter(b)
bw.writebyte('h')
bw.writebyte('e')
bw.writebyte('l')
bw.writebyte('l')
bw.writebyte('o')
bw.writestring(",")
bw.writerune('世')
bw.writerune('界')
bw.writerune('!')
bw.flush()
fmt.println(b)
}
4.5 并发控制
sync包中的waitgroup是个很有用的类,类似信号量。wg.add()和done()能够加减waitgroup(信号量)的值,而wait()会挂起当前线程直到信号量变为0。下面的例子用waitgroup的值表示正在运行的goroutine数量。在goroutine中,用defer done()确保goroutine正常或异常退出时,waitgroup都能减一。
package main
import (
"fmt"
"sync"
)
/**
* i'm waiting all goroutines on wg done
* i'm done=[0]
* i'm done=[1]
* i'm done=[2]
* i'm done=[3]
* i'm done=[4]
* i'm done=[5]
* i'm done=[6]
* i'm done=[7]
* i'm done=[8]
* i'm done=[9]
*/
func main() {
var wg sync.waitgroup
for i := 0; i < 10; i++ {
wg.add(1)
go func(id int) {
defer wg.done()
fmt.printf("i'm done=[%d]\n", id)
}(i)
}
fmt.println("i'm waiting all goroutines on wg done")
wg.wait()
}
4.6 网络编程
golang的net包的抽象层次还是挺高的,用不了几行代码就能实现一个简单的tcp或http服务端了。
4.6.1 socket编程
package main
import (
"net"
"fmt"
"io"
)
/**
* starting the server
* accept the connection: 127.0.0.1:14071
* warning: end of data eof
*/
func main() {
listener, err := net.listen("tcp", "127.0.0.1:12345")
if err != nil {
panic("error listen: " + err.error())
}
fmt.println("starting the server")
for {
conn, err := listener.accept()
if err != nil {
panic("error accept: " + err.error())
}
fmt.println("accept the connection: ", conn.remoteaddr())
go echoserver(conn)
}
}
func echoserver(conn net.conn) {
buf := make([]byte, 1024)
defer conn.close()
for {
n, err := conn.read(buf)
switch err {
case nil:
conn.write(buf[0:n])
case io.eof:
fmt.printf("warning: end of data %s\n", err)
return
default:
fmt.printf("error: read data %s\n", err)
return
}
}
}
4.6.2 http服务器
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
http.handlefunc("/hello", handlehello)
fmt.println("serving on http://localhost:7777/hello")
log.fatal(http.listenandserve("localhost:7777", nil))
}
func handlehello(w http.responsewriter, req *http.request) {
log.println("serving", req.url)
fmt.fprintln(w, "hello, world!")
}
5.结束语
5.1 golang初体验
golang的某些语法的确很简洁,像行尾无分号、条件语句无括号、类型推断、函数多返回值、异常处理、原生协程支持、ducktype继承等,尽管很多并不是golang首创,但结合到一起写起来还是很舒服的。
当然golang也有让人“不爽”的地方。像变量和函数中的类型声明写在后面简直是“反人类”!同样是颠覆,switch的case默认会break就很实用。另外,因为golang主要还是想替代c做系统开发,所以像类啊、包啊还是能看到c的影子,例如类声明只有成员变量而不会包含方法实现等,支持全局函数等,所以有时看到aaa.bbb()还是有点迷糊,不知道aaa是包名还是实例名。
5.2 如何学习一门语言
当我们谈到学习英语时,想到的可能是背单词、学语法、练习听说读写。对于编程语言来说,背单词(关键字)、学语法(语法规则)少不了,可听说读写只剩下了“写”,因为我们说话的对象是“冷冰冰”的计算机。所以唯一的捷径就是“写”,不断地练习!
此外,学的语言多了也能总结出一些规律。首先是基础语法,包括了变量和常量、控制语句、函数、集合、oop、异常处理、控制台输入输出、包管理等。然后是高级特性就差别比较大了。专注高并发的语言就要看并发方面的特性,专注oop的语言就要看有哪些抽象层次更高的特性等等。还是那句话,基础语言只能说我们会用,而能够区别一门语言的高级特性才是它的根本和灵魂,也是我们要着重学习和领悟的地方。