Go 語言簡介(上)— 語法

jopen 12年前發布 | 37K 次閱讀 Go語言 Google Go/Golang開發

周末天氣不好,只能宅在家里,于是就順便看了一下Go語言,覺得比較有意思,所以寫篇文章介紹一下。我想寫一篇你可以在乘坐地鐵或公交車上下班時就可以初步了解一門語言的文章。所以,下面的文章主要是以代碼和注釋為主。只需要你對C語言,Unix,Python有一點基礎,我相信你會在30分鐘左右讀完并對Go語言有一些初步了解的。

Go 語言簡介(上)— 語法

Hello World

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

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

func main() { fmt.Println("hello world") }</pre>

運行

你可以有兩種運行方式,

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

$ls hello hello.go

$./hello hello world</pre>

自己的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("%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")

}</pre>

當然,也可以使用如\n\t\r這樣的和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)

}</pre>

運行結果:

 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)</pre>

分支循環語句

if語句

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

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

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

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++ }</pre>

關于分號

從上面的代碼我們可以看到代碼里沒有分號。其實,和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*/
}

}</pre>

指針

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

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

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

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

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]</pre>

函數

老實說,我對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)) }</pre>

函數返回多個值

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

}</pre>

函數不定參數

例子很清楚了,我就不多說了

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...)

}</pre>

函數閉包

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)) }</pre>

結構體

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}

}</pre>

結構體方法

不多說了,看代碼吧。

注意: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())

}</pre>

接口和多態

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

//---------- 接 口 --------//
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())
}

}</pre>

錯誤處理 – 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) } } }</pre>

程序運行后輸出:

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

}</pre>

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)

}</pre>

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") } }</pre>

當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.") }</pre>

運行結果如下:(我們可以看到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語言的特性


 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!