1. 程式人生 > 其它 >Go 語言簡介(上)— 語法

Go 語言簡介(上)— 語法

Hello World

package main //宣告本檔案的package名

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

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

執行

你可以有兩種執行方式,

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

$ls
hello hello.go

$./hello
hello world

自己的package

你可以使用GOPATH環境變數,或是使用相對路徑來import你自己的package。

Go的規約是這樣的:

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

2)如果沒有使用相對路徑,那麼,go會去找$GOPATH/src/目錄。

 import "./haoel"  //import當前目錄裡haoel子目錄裡的所有的go檔案
 import "haoel"  //import 環境變數 $GOPATH/src/haoel子目錄裡的所有的go檔案

fmt輸出格式

fmt包和libc裡的那堆使用printf, scanf,fprintf,fscanf 很相似。下面的東西對於C程式設計師不會陌生。

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

package main

import "fmt"
import "math"

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

    fmt.Printf("%tn", 1==2)
    fmt.Printf("二進位制:%bn", 255)
    fmt.Printf("八進位制:%on", 255)
    fmt.Printf("十六進位制:%Xn", 255)
    fmt.Printf("十進位制:%dn", 255)
    fmt.Printf("浮點數:%fn", math.Pi)
    fmt.Printf("字串:%sn", "hello world")
}

當然,也可以使用如ntr這樣的和C語言一樣的控制字元

變數和常量

變數的宣告很像 javascript,使用 var關鍵字。注意:go是靜態型別的語言,下面是程式碼:

//宣告初始化一個變數
var  x int = 100
var str string = "hello world"

//宣告初始化多個變數 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 == 10 {
    //零
} 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=%dtpInt=%pt*pInt=%dn", i, pInt, *pInt)

*pInt = 2
//輸出:i=2     pInt=0xf8400371b0       *pInt=2
fmt.Printf("i=%dtpInt=%pt*pInt=%dn", i, pInt, *pInt)

i = 3
//輸出:i=3     pInt=0xf8400371b0       *pInt=3
fmt.Printf("i=%dtpInt=%pt*pInt=%dn", 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, "[email protected]"}
    person = Person{name:"Tom", age: 30, email:"[email protected]"}

    fmt.Println(person) //輸出 {Tom 30 [email protected]}

    pPerson := &person

    fmt.Println(pPerson) //輸出 &{Tom 30 [email protected]}

    pPerson.age = 40
    person.name = "Jerry"
    fmt.Println(person) //輸出 {Jerry 40 [email protected]}
}

結構體方法

不多說了,看程式碼吧。

注意: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.