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]
 * 用指针修改结构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的语言就要看有哪些抽象层次更高的特性等等。还是那句话,基础语言只能说我们会用,而能够区别一门语言的高级特性才是它的根本和灵魂,也是我们要着重学习和领悟的地方。

(0)

相关推荐

  • 12种最常用的网页编程语言简介(值得收藏)

    如今,随着网站的越来越普及,与Web相关的开发技术持续热门,从前端到后端,从标记语言到开发语言,各种技术交相辉映,沉沉浮浮,从开始简单的html到复杂的web开发语言asp.asp.net.php.jsp等等,在此,我就借助SEO马龙博客的平台跟大家简单的介绍一下常见的12种网页编程语言 1.PHP PHP是一个嵌套的缩写名称,是英文"超级文本预处理语言"(PHP:Hypertext Preprocessor)的缩写.PHP是一种HTML内嵌式的语言,与微软的ASP颇有几分相似,都是一

  • C语言编程中的联合体union入门学习教程

    联合体(union)在C语言中是一个特殊的数据类型,能够存储不同类型的数据在同一个内存位置.可以定义一个联合体使用许多成员,但只有一个部件可以包含在任何时候给定的值.联合体会提供使用相同的存储器位置供多用途的有效方式. 定义联合体 要定义联合体,必须使用union语句很相似于定义结构.联合体声明中定义了一个新的数据类型,程序不止一个成员.联合体声明的格式如下: union [union tag] { member definition; member definition; ... member

  • 12种实现301网页重定向方法的代码实例(含Web编程语言和Web服务器)

    为什么需要使用301重定向: 1. 保留搜索引擎的排名: 301 重定向是最有效的方法,不会影响到搜索引擎对页面的排名. 2. 保留访客和流量: 如果你将页面链接到大量方法可以访问过的地址,如果不是用重定向的话你就会失去这些用户(不解)原文:If you move your popular page to which a lot of visitors have already linked, you may lose them if you don't used redirect method

  • C语言编程入门之程序头文件的简要解析

    头文件是扩展名为.h的文件,其中包含C函数的声明和宏定义,也可以多个源文件之间共享.有两种类型的头文件:程序员编写的文件,和编译器中附带的文件. 要求使用头文件的程序,包括通过它,使用C语言预处理指令#include就像所看到的包含stdio.h头文件,它随着编译器自带. 包括一个头文件等于复制头文件的内容,但我们不这样做,因为这很容易出错,一个好主意是我们不复制头文件的内容,特别是包括多个程序的源文件. 在C或C++程序的简单做法是,我们把所有的常量,宏全系统全局变量和函数原型在头文件,其中包

  • Linux下C语言实现C/S模式编程

    由标题可知,这篇文章主要讲如何用C语言实现一个C/S模式的程序. 主要功能:时间回送. 客户机发出请求,服务器响应时间,并返回服务器时间,与客户机进行同步. 废话不多说,下面直接贴出源代码. 代码如下: #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <errno.h> #include <time.h> #

  • 浅谈C语言编程中程序的一些基本的编写优化技巧

    大概所有学习C语言的初学者,都被前辈说过,C语言是世界上接近最速的编程语言,当然这并不是吹牛,也并不是贬低其他语言,诚然非C语言能写出高速度的代码,但是C语言更容易写出高速的程序(高速不代表高效),然而再好的工具,在外行人手中也只能是黯淡没落. 对于现代编译器,现代CPU而言,我们要尽量迎合CPU的设计(比如架构和处理指令的方式等),虽然编译器是为程序员服务,并且在尽它最大的能力来优化程序员写出的代码,但是毕竟它还没有脱离电子的范畴,如果我们的代码不能让编译器理解,编译器无法帮我们优化代码,那么

  • 解析C语言基于UDP协议进行Socket编程的要点

    两种协议 TCP 和 UDP 前者可以理解为有保证的连接,后者是追求快速的连接. 当然最后一点有些 太过绝对 ,但是现在不需熬考虑太多,因为初入套接字编程,一切从简. 稍微试想便能够大致理解, TCP 追求的是可靠的传输数据, UDP 追求的则是快速的传输数据. 前者有繁琐的连接过程,后者则是根本不建立可靠连接(不是绝对),只是将数据发送而不考虑是否到达. 以下例子以 *nix 平台的便准为例,因为 Windows平台需要考虑额外的加载问题,稍作添加就能在 Windows 平台上运行UDP. U

  • Python语言的面相对象编程方式初步学习

    词语练习 class:告诉python创造一个新的东西 object:两个意思:最基本的东西和任何实例化的东西. instance:创建一个类得到的东西. def:在类中创建一个函数. self:在类里面的函数中使用,是实例和object能访问的变量. inheritance:继承,一个类可以继承另一个类,像你和你的父母. composition:一个类可以包含另外一个类,就像汽车包含轮胎. attribute:一个属性类,通常包括变量. is-a:表示继承关系 has-a:包含关系 通过卡片记

  • Go语言编程入门超级指南

    1.序言 Golang作为一门出身名门望族的编程语言新星,像豆瓣的Redis平台Codis.类Evernote的云笔记leanote等. 1.1 为什么要学习 如果有人说X语言比Y语言好,两方的支持者经常会激烈地争吵.如果你是某种语言老手,你就是那门语言的"传道者",下意识地会保护它.无论承认与否,你都已被困在一个隧道里,你看到的完全是局限的.<肖申克的救赎>对此有很好的注脚: [Red] These walls are funny. First you hate 'em,

  • 易语言编程入门第一个程序

    目录 易语言的优点: 最早接触易语言是三年前的事情了,那时候是因为DNF这个游戏我才知道了易语言这个编程语言,当时对他就非常的憧憬.只不过那时候易语言的学习资源比较少,而且自身的学业比较重就没有仔细的了解了. 最近几日再回归DNF的时候突然想到了易语言,所以决定抽点空闲时间学习一下,先定一个小目标:做一个DNF的辅助工具!(也许最终都无法完成也说不定) 这是第一天学习的内容 易语言的优点: 1.     代码是中文的,降低了学习的门槛 2.     全可视化编程,即输即画减少了代码出错的可能 3

  • C语言编程入门必背的示例代码整理大全

    目录 一.C语言必背代码前言 二.一部分C语言必背代码 一.C语言必背代码前言 对于c语言来说,要记得东西其实不多,基本就是几个常用语句加一些关键字而已.你所看到的那些几千甚至上万行的代码,都是用这些语句和关键词来重复编写的.只是他们逻辑功能不一样,那如何快速的上手C语言代码,建议多看多写,下面是小编整理的C语言必背代码. 二.一部分C语言必背代码 1.输出9*9成法口诀,共9行9列,i控制行,j控制列. #include "stdio.h" main() {int i,j,resul

  • 使用Browserify配合jQuery进行编程的超级指南

    引言 1. manually 以前,我新开一个网页项目,然后想到要用jQuery,我会打开浏览器,然后找到jQuery的官方网站,点击那个醒目的"Download jQuery"按钮,下载到.js文件,然后把它丢在项目目录里.在需要用到它的地方,这样用<script>引入它: <script src="path/to/jquery.js"></script> 2. Bower 后来,我开始用Bower这样的包管理工具.所以这个过程

  • C语言堆栈入门指南

    C语言堆栈入门指南 在计算机领域,堆栈是一个不容忽视的概念,我们编写的C语言程序基本上都要用到.但对于很多的初学着来说,堆栈是一个很模糊的概念.堆栈:一种数据结构.一个在程序运行时用于存放的地方,这可能是很多初学者的认识,因为我曾经就是这么想的和汇编语言中的堆栈一词混为一谈.我身边的一些编程的朋友以及在网上看帖遇到的朋友中有好多也说不清堆栈,所以我想有必要给大家分享一下我对堆栈的看法,有说的不对的地方请朋友们不吝赐教,这对于大家学习会有很大帮助. 首先在数据结构上要知道堆栈,尽管我们这么称呼它,

  • ColdFusionMX 编程指南 ColdFusionMX编程入门

    第三期:ColdFusionMX编程入门 序言 上一期我们讲解了ColdFusionMX的基本管理操作,并且熟悉了ColdFusionMX的管理界面布局,而且上一期最后我们演示了两个非常短小的coldfusion程序,这一期会详细讲解coldfusion的入门编程,其中包括在asp中对于初学者而言非常令人头疼的数据库操作. 在每次开始进入正题之前,每一期的序言内容都会为大家介绍一些关于ColdFusion发展或者其他一些具有价值的小知识,第一期为大家介绍了Macromedia MX产品的策略和c

  • ASP编程入门进阶(十三):Ad & Content Rotator

    ASP的强大不仅仅局限于接受和显示的交互,更多的是运用ActiveX 组件进行更强大的Web应用. 那究竟ActiveX组件为何物?它又是如何运作的呢?其实ActiveX Server Components(ActiveX 服务器组件)是一个存在于 WEB 服务器上的文件,该文件包含执行某项或一组任务的代码,组件可以执行公用任务,这样就不必自己去创建执行这些任务的代码.很形象的一句话:运用组件直接采用别人经典的功能强大的程序.只不过这程序已被封装了的. 那具体ActiveX组件是如何产生.如何得

  • Python入门学习指南分享

    对于初学者,入门至关重要,这关系到初学者是从入门到精通还是从入门到放弃.以下是结合Python的学习经验,整理出的一条学习路径,主要有四个阶段 NO.1 新手入门阶段,学习基础知识 总体来讲,找一本靠谱的书,由浅入深,边看边练. 网上的学习教程有很多,多到不知道如何选择.所有教程在基础知识介绍方面都差不多,区别在于讲的是否足够细(例如运行原理)以及是否有足够的练习.目前推荐大家看书<Python编程 从入门到实践> ,作者是美国教师,内容从基础知识开始,循序渐进,层层深入,适合零基础者.课程内

  • AngularJS 2.0入门权威指南

    学习 Angular 2 当越来越多的 web app 使用 Angular 1构建的时候,更快更强大的 Angular 2 将会很快成为新的标准. Angular的新约定使得它更容易去学习.更快的去开发 app.通过本教程学习更快速.更强大的 Angular 版本. Angular 一个跨移动和桌面的框架 快速开始 本指南指导你如何构建一个简单 Angular app. 可以使用typescript/ JavaScript / Dart任意一种语言来编写Angular app,本教程采用Jav

随机推荐