Go 語言簡介

週末天氣很差,只能宅在家裏,因而就順便看了一下Go語言,以爲比較有意思,因此寫篇文章介紹一下。我想寫一篇你能夠在乘坐地鐵或公交車上下班時就能夠初步瞭解一門語言的文章。因此,下面的文章主要是以代碼和註釋爲主。只須要你對C語言,Unix,Python有一點基礎,我相信你會在30分鐘左右讀完並對Go語言有一些初步瞭解的。javascript

Hello World

package main //聲明本文件的package名

import "fmt" //import語言的fmt庫——用於輸出

func main() {
    fmt.Println("hello world")
}

運行

你能夠有兩種運行方式,css

$go run hello.go
hello world
$go build hello.go

$ls
hello hello.go

$./hello
hello world

本身的package

你可使用GOPATH環境變量,或是使用相對路徑來import你本身的package。html

Go的規約是這樣的:java

1)在import中,你可使用相對路徑,如 ./或 ../ 來引用你的packagemysql

2)若是沒有使用相對路徑,那麼,go會去找$GOPATH/src/目錄。程序員

import "./haoel"  //import當前目錄裏haoel子目錄裏的全部的go文件
import "haoel"  //import 環境變量 $GOPATH/src/haoel子目錄裏的全部的go文件

fmt輸出格式

fmt包和libc裏的那堆使用printf, scanf,fprintf,fscanf 很類似。下面的東西對於C程序員不會陌生。golang

注意:Println不支持,Printf才支持%式的輸出:sql

package main

import "fmt"
import "math"

func main() {
    fmt.Println("hello world")

    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", "hello world")
}

固然,也可使用如\n\t\r這樣的和C語言同樣的控制字符shell

變量和常量

變量的聲明很像 javascript,使用 var關鍵字。注意:go是靜態類型的語言,下面是代碼:編程

//聲明初始化一個變量
var  x int = 100
var str string = "hello world"</pre>
//聲明初始化多個變量
var  i, j, k int = 1, 2, 3

//不用指明類型,經過初始化值來推導
var b = true //bool型

還有一種定義變量的方式(這讓我想到了Pascal語言,但徹底不同)

x := 100 //等價於 var x int = 100;

常量很簡單,使用const關鍵字:

const s string = "hello world"
const pi float32 = 3.1415926

數組

直接看代碼(注意其中的for語句,和C很類似吧,就是沒有括號了)

func main() {
    var a [5]int
    fmt.Println("array a:", a)

    a[1] = 10
    a[3] = 30
    fmt.Println("assign:", a)

    fmt.Println("len:", len(a))

    b := [5]int{1, 2, 3, 4, 5}
    fmt.Println("init:", 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("2d: ", c)
}

運行結果:

array a: [0 0 0 0 0]
assign: [0 10 0 30 0]
len: 5
init: [1 2 3 4 5]
2d:  [[0 1 2] [1 2 3]]

數組的切片操做

這個很Python了。

a := [5]int{1, 2, 3, 4, 5}

b := a[2:4] // a[2] 和 a[3],但不包括a[4]
fmt.Println(b)

b = a[:4] // 從 a[0]到a[4],但不包括a[4]
fmt.Println(b)

b = a[2:] // 從 a[2]到a[4],且包括a[2]
fmt.Println(b)

分支循環語句

if語句

注意:if 語句沒有圓括號,而必須要有花括號

//if 語句
if x % 2 == 0 {
    //...
}
//if - else
if x % 2 == 0 {
    //偶數...
} else {
    //奇數...
}

//多分支
if num < 0 {
    //負數
} else if num == 0 {
    //零
} else {
    //正數
}

switch 語句

注意:switch語句沒有break,還可使用逗號case多個值

switch i {
    case 1:
        fmt.Println("one")
    case 2:
        fmt.Println("two")
    case 3:
        fmt.Println("three")
    case 4,5,6:
        fmt.Println("four, five, six")
    default:
        fmt.Println("invalid value!")
}

for 語句

前面你已見過了,下面再來看看for的三種形式:(注意:Go語言中沒有while)

//經典的for語句 init; condition; post
for i := 0; i<10; i++{
     fmt.Println(i)
}

//精簡的for語句 condition
i := 1
for i<10 {
    fmt.Println(i)
    i++
}

//死循環的for語句 至關於for(;;)
i :=1
for {
    if i>10 {
        break
    }
    i++
}

關於分號

從上面的代碼咱們能夠看到代碼裏沒有分號。其實,和C同樣,Go的正式的語法使用分號來終止語句。和C不一樣的是,這些分號由詞法分析器在掃描源代碼過程當中使用簡單的規則自動插入分號,所以輸入源代碼多數時候就不須要分號了

規則是這樣的:若是在一個新行前方的最後一個標記是一個標識符(包括像intfloat64這樣的單詞)、一個基本的如數值這樣的文字、或如下標記中的一個時,會自動插入分號:

break continue fallthrough return ++ -- ) }

一般Go程序僅在for循環語句中使用分號,以此來分開初始化器、條件和增量單元。若是你在一行中寫多個語句,也須要用分號分開。

注意不管任什麼時候候,你都不該該將一個控制結構((ifforswitchselect)的左大括號放在下一行。若是這樣作,將會在大括號的前方插入一個分號,這可能致使出現不想要的結果

map

map在別的語言裏可能叫哈希表或叫dict,下面是和map的相關操做的代碼,代碼很容易懂

func main(){
    m := make(map[string]int) //使用make建立一個空的map

    m["one"] = 1
    m["two"] = 2
    m["three"] = 3

    fmt.Println(m) //輸出 map[three:3 two:2 one:1] (順序在運行時可能不同)
    fmt.Println(len(m)) //輸出 3

    v := m["two"] //從map裏取值
    fmt.Println(v) // 輸出 2

    delete(m, "two")
    fmt.Println(m) //輸出 map[three:3 one:1]

    m1 := map[string]int{"one": 1, "two": 2, "three": 3}
    fmt.Println(m1) //輸出 map[two:2 three:3 one:1] (順序在運行時可能不同)

    for key, val := range m1{
        fmt.Printf("%s => %d \n", key, val)
        /*輸出:(順序在運行時可能不同)
            three => 3
            one => 1
            two => 2*/
    }
}

指針

Go語言同樣有指針,看代碼

var i int = 1
var pInt *int = &i
//輸出:i=1     pInt=0xf8400371b0       *pInt=1
fmt.Printf("i=%d\tpInt=%p\t*pInt=%d\n", i, pInt, *pInt)

*pInt = 2
//輸出:i=2     pInt=0xf8400371b0       *pInt=2
fmt.Printf("i=%d\tpInt=%p\t*pInt=%d\n", i, pInt, *pInt)

i = 3
//輸出:i=3     pInt=0xf8400371b0       *pInt=3
fmt.Printf("i=%d\tpInt=%p\t*pInt=%d\n", i, pInt, *pInt)

Go具備兩個分配內存的機制,分別是內建的函數new和make。他們所作的事不一樣,所應用到的類型也不一樣,這可能引發混淆,但規則卻很簡單。

內存分配

new 是一個分配內存的內建函數,但不一樣於其餘語言中同名的new所做的工做,它只是將內存清零,而不是初始化內存。new(T)爲一個類型爲T的新項目分配了值爲零的存儲空間並返回其地址,也就是一個類型爲*T的值。用Go的術語來講,就是它返回了一個指向新分配的類型爲T的零值的指針

make(T, args)函數的目的與new(T)不一樣。它僅用於建立切片、map和chan(消息管道),並返回類型T(不是*T)的一個被初始化了的(不是)實例。這種差異的出現是因爲這三種類型實質上是對在使用前必須進行初始化的數據結構的引用。例如,切片是一個具備三項內容的描述符,包括指向數據(在一個數組內部)的指針、長度以及容量,在這三項內容被初始化以前,切片值爲nil。對於切片、映射和信道,make初始化了其內部的數據結構並準備了將要使用的值。如:

下面的代碼分配了一個整型數組,長度爲10,容量爲100,並返回前10個數組的切片

make([]int, 10, 100)

如下示例說明了newmake的不一樣。

var p *[]int = new([]int)   // 爲切片結構分配內存;*p == nil;不多使用
var v  []int = make([]int, 10) // 切片v如今是對一個新的有10個整數的數組的引用

// 沒必要要地使問題複雜化:
var p *[]int = new([]int)
fmt.Println(p) //輸出:&[]
*p = make([]int, 10, 10)
fmt.Println(p) //輸出:&[0 0 0 0 0 0 0 0 0 0]
fmt.Println((*p)[2]) //輸出: 0

// 習慣用法:
v := make([]int, 10)
fmt.Println(v) //輸出:[0 0 0 0 0 0 0 0 0 0]

函數

老實說,我對Go語言這種反過來聲明變量類型和函數返回值的作法有點不滿(保持和C同樣的不能夠嗎? 呵呵)

package main
import "fmt"

func max(a int, b int) int { //注意參數和返回值是怎麼聲明的

    if a > b {
        return a
    }
    return b
}

func main(){
    fmt.Println(max(4, 5))
}

函數返回多個值

Go中不少Package 都會返回兩個值,一個是正常值,一個是錯誤,以下所示:

package main
import "fmt"

func main(){
    v, e := multi_ret("one")
    fmt.Println(v,e) //輸出 1 true

    v, e = multi_ret("four")
    fmt.Println(v,e) //輸出 0 false

    //一般的用法(注意分號後有e)
    if v, e = multi_ret("four"); e {
    	// 正常返回
    }else{
    	// 出錯返回
    }
}

func multi_ret(key string) (int, bool){
    m := map[string]int{"one": 1, "two": 2, "three": 3}

    var err bool
    var val int

    val, err = m[key]

    return val, err
}

函數不定參數

例子很清楚了,我就很少說了

func sum(nums ...int) {
    fmt.Print(nums, " ")  //輸出如 [1, 2, 3] 之類的數組
    total := 0
    for _, num := range nums { //要的是值而不是下標
        total += num
    }
    fmt.Println(total)
}
func main() {
    sum(1, 2)
    sum(1, 2, 3)

    //傳數組
    nums := []int{1, 2, 3, 4}
    sum(nums...)
}

函數閉包

nextNum這個函數返回了一個匿名函數,這個匿名函數記住了nextNum中i+j的值,並改變了i,j的值,因而造成了一個閉包的用法

func nextNum() func() int {
    i,j := 1,1
    return func() int {
        var tmp = i+j
        i, j = j, tmp
        return tmp
    }
}
//main函數中是對nextNum的調用,其主要是打出下一個斐波拉契數
func main(){
    nextNumFunc := nextNum()
    for i:=0; i<10; i++ {
    	fmt.Println(nextNumFunc())
    }
}

函數的遞歸

和c基本是同樣的

func fact(n int) int {
    if n == 0 {
        return 1
    }
    return n * fact(n-1)
}

func main() {
    fmt.Println(fact(7))
}

結構體

Go的結構體和C的基本上同樣,不過在初始化時有些不同,Go支持帶名字的初始化。

type Person struct {
    name string
    age  int
    email string
}

func main() {
    //初始化
    person := Person{"Tom", 30, "tom@gmail.com"}
    person = Person{name:"Tom", age: 30, email:"tom@gmail.com"}

    fmt.Println(person) //輸出 {Tom 30 tom@gmail.com}

    pPerson := &person

    fmt.Println(pPerson) //輸出 &{Tom 30 tom@gmail.com}

    pPerson.age = 40
    person.name = "Jerry"
    fmt.Println(person) //輸出 {Jerry 40 tom@gmail.com}
}

結構體方法

很少說了,看代碼吧。

注意:Go語言中沒有public, protected, private的關鍵字,因此,若是你想讓一個方法能夠被別的包訪問的話,你須要把這個方法的第一個字母大寫。這是一種約定

type rect struct {
    width, height int
}

func (r *rect) area() int { //求面積
    return r.width * r.height
}

func (r *rect) perimeter() int{ //求周長
    return 2*(r.width + r.height)
}

func main() {
    r := rect{width: 10, height: 15}

    fmt.Println("面積: ", r.area())
    fmt.Println("周長: ", r.perimeter())

    rp := &r
    fmt.Println("面積: ", rp.area())
    fmt.Println("周長: ", rp.perimeter())
}

接口和多態

接口意味着多態,下面是一個經典的例子,不用多說了,本身看代碼吧。

//---------- 接 口 --------//
type shape interface {
	area() float64 //計算面積
	perimeter() float64 //計算周長
}

//--------- 長方形 ----------//
type rect struct {
    width, height float64
}

func (r *rect) area() float64 { //面積
	return r.width * r.height
}

func (r *rect) perimeter() float64 { //周長
	return 2*(r.width + r.height)
}

//----------- 圓  形 ----------//
type circle struct {
	radius float64
}

func (c *circle) area() float64 { //面積
	return math.Pi * c.radius * c.radius
}

func (c *circle) perimeter() float64 { //周長
	return 2 * math.Pi * c.radius
}

// ----------- 接口的使用 -----------//
func interface_test() {
    r := rect {width:2.9, height:4.8}
    c := circle {radius:4.3}

    s := []shape{&r, &c} //經過指針實現

    for _, sh := range s {
        fmt.Println(sh)
    	fmt.Println(sh.area())
    	fmt.Println(sh.perimeter())
    }
}

錯誤處理 – Error接口

函數錯誤返回多是C/C++時最讓人糾結的東西的,Go的多值返回可讓咱們更容易的返回錯誤,其能夠在返回一個常規的返回值以外,還能輕易地返回一個詳細的錯誤描述。一般狀況下,錯誤的類型是error,它有一個內建的接口。

type error interface {
    Error() string
}

仍是看個示例吧:

package main

import "fmt"
import "errors"

//自定義的出錯結構
type myError struct {
    arg  int
    errMsg string
}
//實現Error接口
func (e *myError) Error() string {
    return fmt.Sprintf("%d - %s", e.arg, e.errMsg)
}

//兩種出錯
func error_test(arg int) (int, error) {
    if arg < 0  {
         return -1, errors.New("Bad Arguments - negtive!")
     }else if arg >256 {
        return -1, &myError{arg, "Bad Arguments - too large!"}
    }
    return arg*arg, nil
}

//相關的測試
func main() {
    for _, i := range []int{-1, 4, 1000} {
        if r, e := error_test(i); e != nil {
            fmt.Println("failed:", e)
        } else {
            fmt.Println("success:", r)
        }
    }
}

程序運行後輸出:

failed: Bad Arguments - negtive!
success: 16
failed: 1000 - Bad Arguments - too large!

錯誤處理 – Defer

下面的程序對於每個熟悉C語言的人來講都不陌生(有資源泄露的問題),C++使用RAII來解決這種問題。

func CopyFile(dstName, srcName string) (written int64, err error) {
    src, err := os.Open(srcName)
    if err != nil {
        return
    }

    dst, err := os.Create(dstName)
    if err != nil {
        return
    }

    written, err = io.Copy(dst, src)
    dst.Close()
    src.Close()
    return
}

Go語言引入了Defer來確保那些被打開的文件能被關閉。以下所示:(這種解決方式仍是比較優雅的)

func CopyFile(dstName, srcName string) (written int64, err error) {
    src, err := os.Open(srcName)
    if err != nil {
        return
    }
    defer src.Close()

    dst, err := os.Create(dstName)
    if err != nil {
        return
    }
    defer dst.Close()

    return io.Copy(dst, src)
}

Go的defer語句預設一個函數調用(延期的函數),該調用在函數執行defer返回時馬上運行。該方法顯得不一樣常規,但倒是處理上述狀況頗有效,不管函數怎樣返回,都必須進行資源釋放。

咱們再來看一個defer函數的示例:

for i := 0; i < 5; i++ {
    defer fmt.Printf("%d ", i)
}

被延期的函數之後進先出(LIFO)的順行執行,所以以上代碼在返回時將打印4 3 2 1 0。

總之,我我的以爲defer的函數行爲有點怪異,我如今尚未徹底搞清楚。

錯誤處理 – Panic/Recover

對於不可恢復的錯誤,Go提供了一個內建的panic函數,它將建立一個運行時錯誤並使程序中止(至關暴力)。該函數接收一個任意類型(每每是字符串)做爲程序死亡時要打印的東西。當編譯器在函數的結尾處檢查到一個panic時,就會中止進行常規的return語句檢查。

下面的僅僅是一個示例。實際的庫函數應避免panic。若是問題能夠容忍,最好是讓事情繼續下去而不是終止整個程序。

var user = os.Getenv("USER")

func init() {
    if user == "" {
        panic("no value for $USER")
    }
}

當panic被調用時,它將當即中止當前函數的執行並開始逐級解開函數堆棧,同時運行全部被defer的函數。若是這種解開達到堆棧的頂端,程序就 死亡了。可是,也可使用內建的recover函數來從新得到Go程的控制權並恢復正常的執行。 對recover的調用會通知解開堆棧並返回傳遞到panic的參量。因爲僅在解開期間運行的代碼處在被defer的函數以內,recover僅在被延期 的函數內部纔是有用的。

你能夠簡單地理解爲recover就是用來捕捉Painc的,防止程序一會兒就掛掉了。

下面是一個例程,很簡單了,不解釋了

func g(i int) {
    if i>1 {
        fmt.Println("Panic!")
        panic(fmt.Sprintf("%v", i))
    }

}

func f() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered in f", r)
        }
    }()

    for i := 0; i < 4; i++ {
        fmt.Println("Calling g with ", i)
        g(i)
        fmt.Println("Returned normally from g.")
     }
}

func main() {
    f()
    fmt.Println("Returned normally from f.")
}

運行結果以下:(咱們能夠看到Painc後的for循環就沒有往下執行了,可是main的程序還在往下走)

Calling g with  0
Returned normally from g.
Calling g with  1
Returned normally from g.
Calling g with  2
Panic!
Recovered in f 2
Returned normally from f.

你習慣這種編程方式嗎?我以爲有點詭異。呵呵。

好了,上面是是一Go語言相關的編程語法的介紹,我沒有事無鉅細,只是讓你瞭解一下Go語言是長什麼樣的。固然,這還沒完,請期待下篇——Go語言的特性

但願你看到這篇文章的時候仍是在公交車和地鐵上正在上下班的時間,我但願個人這篇文章可讓你利用這段時間瞭解一門語言。固然,但願你不會由於看個人文章而錯過站。呵呵。

若是你還不瞭解Go語言的語法,還請你移步先看一下上篇——《Go語言簡介(上):語法

goroutine

GoRoutine主要是使用go關鍵字來調用函數,你還可使用匿名函數,以下所示:

package main
import "fmt"

func f(msg string) {
    fmt.Println(msg)
}

func main(){
    go f("goroutine")

    go func(msg string) {
        fmt.Println(msg)
    }("going")
}

咱們再來看一個示例,下面的代碼中包括不少內容,包括時間處理,隨機數處理,還有goroutine的代碼。若是你熟悉C語言,你應該會很容易理解下面的代碼。

你能夠簡單的把go關鍵字調用的函數想像成pthread_create。下面的代碼使用for循環建立了3個線程,每一個線程使用一個隨機的Sleep時間,而後在routine()函數中會輸出一些線程執行的時間信息。

package main

import "fmt"
import "time"
import "math/rand"

func routine(name string, delay time.Duration) {

    t0 := time.Now()
    fmt.Println(name, " start at ", t0)

    time.Sleep(delay)

    t1 := time.Now()
    fmt.Println(name, " end at ", t1)

    fmt.Println(name, " lasted ", t1.Sub(t0))
}

func main() {

    //生成隨機種子
    rand.Seed(time.Now().Unix())

    var name string
    for i:=0; i<3; i++{
        name = fmt.Sprintf("go_%02d", i) //生成ID
        //生成隨機等待時間,從0-4秒
        go routine(name, time.Duration(rand.Intn(5)) * time.Second)
    }

    //讓主進程停住,否則主進程退了,goroutine也就退了
    var input string
    fmt.Scanln(&input)
    fmt.Println("done")
}

運行的結果多是:

go_00  start at  2012-11-04 19:46:35.8974894 +0800 +0800
go_01  start at  2012-11-04 19:46:35.8974894 +0800 +0800
go_02  start at  2012-11-04 19:46:35.8974894 +0800 +0800
go_01  end at  2012-11-04 19:46:36.8975894 +0800 +0800
go_01  lasted  1.0001s
go_02  end at  2012-11-04 19:46:38.8987895 +0800 +0800
go_02  lasted  3.0013001s
go_00  end at  2012-11-04 19:46:39.8978894 +0800 +0800
go_00  lasted  4.0004s

goroutine的併發安全性

關於goroutine,我試了一下,不管是Windows仍是Linux,基本上來講是用操做系統的線程來實現的。不過,goroutine有個特性,也就是說,若是一個goroutine沒有被阻塞,那麼別的goroutine就不會獲得執行。這並非真正的併發,若是你要真正的併發,你須要在你的main函數的第一行加上下面的這段代碼:

import "runtime"
...
runtime.GOMAXPROCS(4)

仍是讓咱們來看一個有併發安全性問題的示例(注意:我使用了C的方式來寫這段Go的程序)

這是一個常常出如今教科書裏賣票的例子,我啓了5個goroutine來賣票,賣票的函數sell_tickets很簡單,就是隨機的sleep一下,而後對全局變量total_tickets做減一操做。

package main

import "fmt"
import "time"
import "math/rand"
import "runtime"

var total_tickets int32 = 10;

func sell_tickets(i int){
    for{
        if total_tickets > 0 { //若是有票就賣
            time.Sleep( time.Duration(rand.Intn(5)) * time.Millisecond)
            total_tickets-- //賣一張票
            fmt.Println("id:", i, "  ticket:", total_tickets)
        }else{
            break
        }
    }
}

func main() {
    runtime.GOMAXPROCS(4) //個人電腦是4核處理器,因此我設置了4
    rand.Seed(time.Now().Unix()) //生成隨機種子

    for i := 0; i < 5; i++ { //併發5個goroutine來賣票
         go sell_tickets(i)
    }
    //等待線程執行完
    var input string
    fmt.Scanln(&input)
    fmt.Println(total_tickets, "done") //退出時打印還有多少票
}

這個程序毋庸置疑有併發安全性問題,因此執行起來你會看到下面的結果:

$go run sell_tickets.go
id: 0   ticket: 9  
id: 0   ticket: 8  
id: 4   ticket: 7  
id: 1   ticket: 6  
id: 3   ticket: 5  
id: 0   ticket: 4  
id: 3   ticket: 3  
id: 2   ticket: 2  
id: 0   ticket: 1  
id: 3   ticket: 0  
id: 1   ticket: -1  
id: 4   ticket: -2  
id: 2   ticket: -3  
id: 0   ticket: -4  
-4 done

可見,咱們須要使用上鎖,咱們可使用互斥量來解決這個問題。下面的代碼,我只列出了修改過的內容:

 package main
import "fmt"
import "time"
import "math/rand"
import "sync"
import "runtime"

var total_tickets int32 = 10;
var mutex = &sync.Mutex{} //可簡寫成:var mutex sync.Mutex

func sell_tickets(i int){
    for total_tickets>0 {
        mutex.Lock()
        if total_tickets > 0 {
            time.Sleep( time.Duration(rand.Intn(5)) * time.Millisecond)
            total_tickets--
            fmt.Println(i, total_tickets)
        }
        mutex.Unlock()
    }
}
.......
......

原子操做

說到併發就須要說說原子操做,相信你們還記得我寫的那篇《無鎖隊列的實現》一文,裏面說到了一些CAS – CompareAndSwap的操做。Go語言也支持。你能夠看一下至關的文檔

我在這裏就舉一個很簡單的示例:下面的程序有10個goroutine,每一個會對cnt變量累加20次,因此,最後的cnt應該是200。若是沒有atomic的原子操做,那麼cnt將有可能獲得一個小於200的數。

下面使用了atomic操做,因此是安全的。

package main

import "fmt"
import "time"
import "sync/atomic"

func main() {
    var cnt uint32 = 0
    for i := 0; i < 10; i++ {
        go func() {
            for i:=0; i<20; i++ {
                time.Sleep(time.Millisecond)
                atomic.AddUint32(&cnt, 1)
            }
        }()
    }
    time.Sleep(time.Second)//等一秒鐘等goroutine完成
    cntFinal := atomic.LoadUint32(&cnt)//取數據
    fmt.Println("cnt:", cntFinal)
}

這樣的函數還有不少,參看go的atomic包文檔(被牆)

Channel 信道

Channal是什麼?Channal就是用來通訊的,就像Unix下的管道同樣,在Go中是這樣使用Channel的。

下面的程序演示了一個goroutine和主程序通訊的例程。這個程序足夠簡單了。

package main

import "fmt"

func main() {
    //建立一個string類型的channel
    channel := make(chan string)

    //建立一個goroutine向channel裏發一個字符串
    go func() { channel <- "hello" }()

    msg := <- channel
    fmt.Println(msg)
}

指定channel的buffer

指定buffer的大小很簡單,看下面的程序:

package main
import "fmt"

func main() {
    channel := make(chan string, 2)

    go func() {
        channel <- "hello"
        channel <- "World"
    }()

    msg1 := <-channel
    msg2 := <-channel
    fmt.Println(msg1, msg2)
}

Channel的阻塞

注意,channel默認上是阻塞的,也就是說,若是Channel滿了,就阻塞寫,若是Channel空了,就阻塞讀。因而,咱們就可使用這種特性來同步咱們的發送和接收端。

下面這個例程說明了這一點,代碼有點亂,不過我以爲不難理解。

package main

import "fmt"
import "time"

func main() {

    channel := make(chan string) //注意: buffer爲1

    go func() {
        channel <- "hello"
        fmt.Println("write \"hello\" done!")

        channel <- "World" //Reader在Sleep,這裏在阻塞
        fmt.Println("write \"World\" done!")

        fmt.Println("Write go sleep...")
        time.Sleep(3*time.Second)
        channel <- "channel"
        fmt.Println("write \"channel\" done!")
    }()

    time.Sleep(2*time.Second)
    fmt.Println("Reader Wake up...")

    msg := <-channel
    fmt.Println("Reader: ", msg)

    msg = <-channel
    fmt.Println("Reader: ", msg)

    msg = <-channel //Writer在Sleep,這裏在阻塞
    fmt.Println("Reader: ", msg)
}

上面的代碼輸出的結果以下:

Reader Wake up...
Reader:  hello
write "hello" done!
write "World" done!
Write go sleep...
Reader:  World
write "channel" done!
Reader:  channel

Channel阻塞的這個特性還有一個好處是,可讓咱們的goroutine在運行的一開始就阻塞在從某個channel領任務,這樣就能夠做成一個相似於線程池同樣的東西。關於這個程序我就不寫了。我相信你能夠本身實現的。

多個Channel的select

package main
import "time"
import "fmt"

func main() {
    //建立兩個channel - c1 c2
    c1 := make(chan string)
    c2 := make(chan string)

    //建立兩個goruntine來分別向這兩個channel發送數據
    go func() {
        time.Sleep(time.Second * 1)
        c1 <- "Hello"
    }()
    go func() {
        time.Sleep(time.Second * 1)
        c2 <- "World"
    }()

    //使用select來偵聽兩個channel
    for i := 0; i < 2; i++ {
        select {
        case msg1 := <-c1:
            fmt.Println("received", msg1)
        case msg2 := <-c2:
            fmt.Println("received", msg2)
        }
    }
}

注意:上面的select是阻塞的,因此,才搞出ugly的for i <2這種東西

Channel select阻塞的Timeout

解決上述那個for循環的問題,通常有兩種方法:一種是阻塞但有timeout,一種是無阻塞。咱們來看看若是給select設置上timeout的。

    for {
        timeout_cnt := 0
        select {
        case msg1 := <-c1:
            fmt.Println("msg1 received", msg1)
        case msg2 := <-c2:
            fmt.Println("msg2 received", msg2)
        case  <-time.After(time.Second * 30):
            fmt.Println("Time Out")
            timout_cnt++
        }
        if time_cnt > 3 {
            break
        }
    }

上面代碼中高亮的代碼主要是用來讓select返回的,注意 case中的time.After事件。

Channel的無阻塞

好,咱們再來看看無阻塞的channel,其實也很簡單,就是在select中加入default,以下所示:

    for {
        select {
        case msg1 := <-c1:
            fmt.Println("received", msg1)
        case msg2 := <-c2:
            fmt.Println("received", msg2)
        default: //default會致使無阻塞
            fmt.Println("nothing received!")
            time.Sleep(time.Second)
        }
    }

Channel的關閉

關閉Channel能夠通知對方內容發送完了,不用再等了。參看下面的例程:

package main

import "fmt"
import "time"
import "math/rand"

func main() {

    channel := make(chan string)
    rand.Seed(time.Now().Unix())

    //向channel發送隨機個數的message
    go func () {
        cnt := rand.Intn(10)
        fmt.Println("message cnt :", cnt)
        for i:=0; i<cnt; i++{
            channel <- fmt.Sprintf("message-%2d", i)
        }
        close(channel) //關閉Channel
    }()

    var more bool = true
    var msg string
    for more {
        select{
        //channel會返回兩個值,一個是內容,一個是還有沒有內容
        case msg, more = <- channel:
            if more {
                fmt.Println(msg)
            }else{
                fmt.Println("channel closed!")
            }
        }
    }
}

定時器

Go語言中可使用time.NewTimer或time.NewTicker來設置一個定時器,這個定時器會綁定在你的當前channel中,經過channel的阻塞通知機器來通知你的程序。

下面是一個timer的示例。

package main

import "time"
import "fmt"

func main() {
    timer := time.NewTimer(2*time.Second)

    <- timer.C
    fmt.Println("timer expired!")
}

上面的例程看起來像一個Sleep,是的,不過Timer是能夠Stop的。你須要注意Timer只通知一次。若是你要像C中的Timer能持續通知的話,你須要使用Ticker。下面是Ticker的例程:

package main

import "time"
import "fmt"

func main() {
    ticker := time.NewTicker(time.Second)

    for t := range ticker.C {
        fmt.Println("Tick at", t)
    }
}

上面的這個ticker會讓你程序進入死循環,咱們應該放其放在一個goroutine中。下面這個程序結合了timer和ticker

package main

import "time"
import "fmt"

func main() {

    ticker := time.NewTicker(time.Second)

    go func () {
        for t := range ticker.C {
            fmt.Println(t)
        }
    }()

    //設置一個timer,10鈔後停掉ticker
    timer := time.NewTimer(10*time.Second)
    <- timer.C

    ticker.Stop()
    fmt.Println("timer expired!")
}

Socket編程

下面是我嘗試的一個Echo Server的Socket代碼,感受仍是挺簡單的。

package main

import (
    "net"
    "fmt"
    "io"
)

const RECV_BUF_LEN = 1024

func main() {
    listener, err := net.Listen("tcp", "0.0.0.0:6666")//偵聽在6666端口
    if err != nil {
        panic("error listening:"+err.Error())
    }
    fmt.Println("Starting the server")

    for {
        conn, err := listener.Accept() //接受鏈接
        if err != nil {
            panic("Error accept:"+err.Error())
        }
        fmt.Println("Accepted the Connection :", conn.RemoteAddr())
        go EchoServer(conn)
    }
}

func EchoServer(conn net.Conn) {
    buf := make([]byte, RECV_BUF_LEN)
    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: Reading data : %s \n", err);
                return
        }
     }
}
package main

import (
    "fmt"
    "time"
    "net"
)

const RECV_BUF_LEN = 1024

func main() {
    conn,err := net.Dial("tcp", "127.0.0.1:6666")
    if err != nil {
        panic(err.Error())
    }
    defer conn.Close()

    buf := make([]byte, RECV_BUF_LEN)

    for i := 0; i < 5; i++ {
        //準備要發送的字符串
        msg := fmt.Sprintf("Hello World, %03d", i)
        n, err := conn.Write([]byte(msg))
        if err != nil {
            println("Write Buffer Error:", err.Error())
            break
        }
        fmt.Println(msg)

        //從服務器端收字符串
        n, err = conn.Read(buf)
        if err !=nil {
            println("Read Buffer Error:", err.Error())
            break
        }
        fmt.Println(string(buf[0:n]))

        //等一秒鐘
        time.Sleep(time.Second)
    }
}

系統調用

Go語言那麼C,因此,必定會有一些系統調用。Go語言主要是經過兩個包完成的。一個是os包,一個是syscall包。(注意,連接被牆)

這兩個包裏提供都是Unix-Like的系統調用,

  • syscall裏提供了什麼Chroot/Chmod/Chmod/Chdir…,Getenv/Getgid/Getpid/Getgroups/Getpid/Getppid…,還有不少如Inotify/Ptrace/Epoll/Socket/…的系統調用。

  • os包裏提供的東西很少,主要是一個跨平臺的調用。它有三個子包,Exec(運行別的命令), Signal(捕捉信號)和User(經過uid查name之類的)

syscall包的東西我不舉例了,你們能夠看看《Unix高級環境編程》一書。

os裏的取幾個例:

環境變量

package main

import "os"
import "strings"


func main() {
    os.Setenv("WEB", "http://coolshell.cn") //設置環境變量
    println(os.Getenv("WEB")) //讀出來

    for _, env := range os.Environ() { //窮舉環境變量
        e := strings.Split(env, "=")
        println(e[0], "=", e[1])
    }
}

執行命令行

下面是一個比較簡單的示例

package main
import "os/exec"
import "fmt"
func main() {
    cmd := exec.Command("ping", "127.0.0.1")
    out, err := cmd.Output()
    if err!=nil {
        println("Command Error!", err.Error())
        return
    }
    fmt.Println(string(out))
}

正規一點的用來處理標準輸入和輸出的示例以下:

package main

import (
	"strings"
	"bytes"
	"fmt"
	"log"
	"os/exec"
)

func main() {
	cmd := exec.Command("tr", "a-z", "A-Z")
	cmd.Stdin = strings.NewReader("some input")
	var out bytes.Buffer
	cmd.Stdout = &out
	err := cmd.Run()
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("in all caps: %q\n", out.String())
}

命令行參數

Go語言中處理命令行參數很簡單:(使用os的Args就能夠了)

func main() {
    args := os.Args
    fmt.Println(args) //帶執行文件的
    fmt.Println(args[1:]) //不帶執行文件的
}

在Windows下,若是運行結果以下:

C:\Projects\Go>go run args.go aaa bbb ccc ddd
[C:\Users\haoel\AppData\Local\Temp\go-build742679827\command-line-arguments\_
obj\a.out.exe aaa bbb ccc ddd]
[aaa bbb ccc ddd]

那麼,若是咱們要搞出一些像 mysql -uRoot -hLocalhost -pPwd 或是像 cc -O3 -Wall -o a a.c 這樣的命令行參數咱們怎麼辦?Go提供了一個package叫flag能夠容易地作到這一點

package main
import "flag"
import "fmt"

func main() {

    //第一個參數是「參數名」,第二個是「默認值」,第三個是「說明」。返回的是指針
    host := flag.String("host", "coolshell.cn", "a host name ")
    port := flag.Int("port", 80, "a port number")
    debug := flag.Bool("d", false, "enable/disable debug mode")

    //正式開始Parse命令行參數
    flag.Parse()

    fmt.Println("host:", *host)
    fmt.Println("port:", *port)
    fmt.Println("debug:", *debug)
}

執行起來會是這個樣子:

#若是沒有指定參數名,則使用默認值
$ go run flagtest.go
host: coolshell.cn
port: 80
debug: false

#指定了參數名後的狀況
$ go run flagtest.go -host=localhost -port=22 -d
host: localhost
port: 22
debug: true

#用法出錯了(如:使用了不支持的參數,參數沒有=)
$ go build flagtest.go
$ ./flagtest -debug -host localhost -port=22
flag provided but not defined: -debug
Usage of flagtest:
  -d=false: enable/disable debug mode
  -host="coolshell.cn": a host name
  -port=80: a port number
exit status 2

感受仍是挺不錯的吧。

一個簡單的HTTP Server

代碼賽過千言萬語。呵呵。這個小程序讓我又找回之前用C寫CGI的時光了。(Go的官方文檔是《Writing Web Applications》)

package main

import (
    "fmt"
    "net/http"
    "io/ioutil"
    "path/filepath"
)

const http_root = "/home/haoel/coolshell.cn/"

func main() {
    http.HandleFunc("/", rootHandler)
    http.HandleFunc("/view/", viewHandler)
    http.HandleFunc("/html/", htmlHandler)

    http.ListenAndServe(":8080", nil)
}

//讀取一些HTTP的頭
func rootHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "rootHandler: %s\n", r.URL.Path)
    fmt.Fprintf(w, "URL: %s\n", r.URL)
    fmt.Fprintf(w, "Method: %s\n", r.Method)
    fmt.Fprintf(w, "RequestURI: %s\n", r.RequestURI )
    fmt.Fprintf(w, "Proto: %s\n", r.Proto)
    fmt.Fprintf(w, "HOST: %s\n", r.Host) 
}

//特別的URL處理
func viewHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "viewHandler: %s", r.URL.Path)
}

//一個靜態網頁的服務示例。(在http_root的html目錄下)
func htmlHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Printf("htmlHandler: %s\n", r.URL.Path)
    
    filename := http_root + r.URL.Path
    fileext := filepath.Ext(filename)

    content, err := ioutil.ReadFile(filename)
    if err != nil {
        fmt.Printf("   404 Not Found!\n")
        w.WriteHeader(http.StatusNotFound)
        return
    }
    
    var contype string
    switch fileext {
        case ".html", "htm":
            contype = "text/html"
        case ".css":
            contype = "text/css"
        case ".js":
            contype = "application/javascript"
        case ".png":
            contype = "image/png"
        case ".jpg", ".jpeg":
            contype = "image/jpeg"
        case ".gif":
            contype = "image/gif"
        default: 
            contype = "text/plain"
    }
    fmt.Printf("ext %s, ct = %s\n", fileext, contype)
    
    w.Header().Set("Content-Type", contype)
    fmt.Fprintf(w, "%s", content)
    
}

Go的功能庫有不少,你們本身慢慢看吧。我再吐個槽——Go的文檔真很差讀。例子太少了

先說這麼多吧。這是我週末兩天學Go語言學到的東西,寫得太倉促了,並且還有一些東西理解不到位,還你們請指正!

(全文完)

相關文章
相關標籤/搜索