天天看點

Golang 新手教程:入門速成指南

原文連結
Golang 新手教程:入門速成指南

讓我們從 Go(或 Golang)的一個小介紹開始。 Go 由 Google 工程師 Robert Griesemer,Rob Pike 和 Ken Thompson 設計。 它是一種靜态類型的編譯語言。 第一個版本于 2012 年 3 月作為開源釋出。

“Go 是一種開源程式設計語言,可以輕松建構的簡單,可靠,高效的軟體”。

--- 關于 go

在許多語言中,有許多方法可以解決某些給定的問題。是以 程式員可以花很多時間思考解決問題的最佳方法。

然而,Go 卻是隻有一種正确的方法來解決問題的語言。

這節省了開發人員的時間,并使大型代碼庫易于維護。 Go 中沒有地圖和過濾器等 “富有表現力” 的功能。

“如果你有增加表現力的功能,通常會增加費用”

--- Rob Pike

入門

Go 是由包組成的。 main 包告訴 Go 編譯器該程式可以被編譯成可執行檔案,而不是一個共享的庫。它是應用程式的入口。main 包被定義為如下格式:

package main           

接下來,讓我們通過在 Go 工作區中建立一個檔案 main.go 來編寫一個簡單的 hello world 示例。

go的工作區

Go 中的工作空間由環境變量「GOPATH」定義。你寫的任何代碼都将寫在工作區内。Go 将搜尋 GOPATH 目錄中的任何包,或者在安裝 Go 時預設設定的 GOROOT 目錄。 GOROOT 是安裝 go 的路徑。

将 GOPATH 設定為你想要的目錄。 現在,讓我們将它添加到檔案夾〜/ workspace 中。

#寫入 env
export GOPATH=~/workspace

#cd 到工作區目錄\
cd ~/workspace
           

使用我們剛剛建立的工作空間檔案夾中的以下代碼建立檔案 main.go。

Hello World!

package main

import (
 "fmt"
)

func main(){
  fmt.Println("Hello World!")
}
           

在上面的 demo 中, fmt 是 Go 中的内置包,它實作了格式化 I / O 的功能。

在 Go 中我們導入一個包使用 import 關鍵字func main 是代碼執行的入口。Println 是 fmt 包中的一個函數,它為我們列印 “hello world”。

讓我們看一下運作這個檔案。 我們可以通過兩種方式運作 Go 指令。 我們知道,Go 是一種編譯語言,是以我們首先需要在執行之前編譯它。

> go build main.go
           

這會建立一個二進制可執行檔案 main,現在我們可以運作它:

> ./main
 # Hello World!
           

還有另一種更簡單的方法來運作程式。 go run 指令有助于抽象編譯步驟。 您隻需運作以下指令即可執行該程式。

go run main.go
 # Hello World!
           

您可以使用

https://play.golang.org

來運作本文提到的代碼。

變量

變量在 Go 語言中是一個很明确的定義。 Go 是一種靜态類型的語言。這意味着在聲明變量時我們就需要明确變量的類型。一般一個變量的定義如下:

var a int
           

上面的執行個體中,我們定義了一個 int 類型的變量 a ,預設會被指派成 0 。使用以下文法可以初始化改變變量的值:

var a = 1
           

這裡我們沒有制定變量 a 的類型,在我們給它初始化為 1 時,它就自動被定義成了 int 類型的變量。

我們也可以使用一種更簡短的文法來定義它:

message := "hello world"
           

我們也可以在同一行聲明多個同類型變量:

var b, c int = 2, 3
           

資料類型

跟任何其他程式設計語言一樣,Go 語言支援各種不同的資料結構。 讓我們探讨一下:

Number, String, and Boolean

一些受支援的 number 存儲類型是:int, int8, int16, int32, int64,uint, uint8, uint16, uint32, uint64, uintptr...

string 類型存儲一些列的位元組。 它用關鍵字string 表示和聲明。

boolean 使用關鍵字 bool 存儲布爾值。

Go 還支援複數類型資料類型,可以使用complex64 和 complex128 聲明。

var a bool = true
var b int = 1
var c string = 'hello world'
var d float32 = 1.222
var x complex128 = cmplx.Sqrt(-5 + 12i)
           

數組,切片,以及 Maps

數組是相同資料類型的元素序列。 數組在聲明中定義要指定長度,是以不能進行擴充。 數組聲明為:

var a [5]int
           

數組也可以是多元的。 我們可以使用以下格式建立它們:

var multiD [2][3]int
           

當數組的值在運作時不能進行更改。 數組也不提供擷取子數組的能力。 為此,Go 有一個名為切片的資料類型。

切片存儲一系列元素,可以随時擴充。 切片聲明類似于數組聲明 --- 沒有定義容量:

var b []int
           

這将建立一個零容量和零長度的切片。 切片也可以定義容量和長度。 我們可以使用以下文法:

numbers := make([]int,5,10)
           

這裡,切片的初始長度為 5,容量為 10。

切片是數組的抽象。 切片使用數組作為底層結構。 切片包含三個元件:容量,長度和指向底層數組的指針,如下圖所示:

Golang 新手教程:入門速成指南

圖檔位址:

https://blog.golang.org/go-slices-usage-an...

通過使用 append 或 copy 函數可以增加切片的容量。 append 函數可以為數組的末尾增加值,并在需要時增加容量。

numbers = append(numbers, 1, 2, 3, 4)
           

增加切片容量的另一種方法是使用複制功能。 隻需建立另一個具有更大容量的切片,并将原始切片複制到新建立的切片:

// 建立切片
number2 := make([]int, 15)
// 将原始切片複制到新切片
copy(number2, numbers)
           

我們可以建立切片的子切片。 這可以使用以下指令完成:

// 初始化長度為 4,以及指派
number2 := []int{1,2,3,4}
fmt.Println(numbers) // -> [1 2 3 4]
// 建立子切片
slice1 := number2[2:]
fmt.Println(slice1) // -> [3 4]
slice2 := number2[:3]
fmt.Println(slice2) // -> [1 2 3]
slice3 := number2[1:4]
fmt.Println(slice3) // -> [2 3 4]
           

map 是 go 的一種 Key-Value 類型的資料結構,我們可以通過下面的指令聲明一個 map :

m := make(map[string]int)
           

m 是 一個 Key 類型為 string、Value 類型為 int 的 map 類型的變量。我們可以很容易地添加鍵值對到 map 中:

// adding key/value
m["clearity"] = 2
m["simplicity"] = 3
// printing the values
fmt.Println(m["clearity"]) // -> 2
fmt.Println(m["simplicity"]) // -> 3
           

類型轉化

通過類型轉化,能将一種類型轉為另一種類型。讓我們來看一個簡單的例子:

a := 1.1
b := int(a)
fmt.Println(b)
//-> 1
           

并不是所有類型都可以轉為另一種類型。需要確定資料類型是可以轉化的。

流程控制

if else

對于流程控制,我們可以使用 if-else 語句,如下例所示。 確定花括号與條件位于同一行。

if num := 9; num < 0 {
 fmt.Println(num, "is negative")
} else if num < 10 {
 fmt.Println(num, "has 1 digit")
} else {
 fmt.Println(num, "has multiple digits")
}
           

switch case

Switch cases 有助于組織多個條件語句。 以下示例顯示了一個簡單的 switch case 語句:

i := 2
switch i {
case 1:
 fmt.Println("one")
case 2:
 fmt.Println("two")
default:
 fmt.Println("none")
}
           

循環

Go 為循環設定了一個關鍵字。 單個 for 循環指令有助于實作不同類型的循環:

i := 0
sum := 0
for i < 10 {
 sum += 1
  i++
}
fmt.Println(sum)
           

上面的示例類似于 C 中的 while 循環。對于 for 循環,可以使用相同的 for 語句

sum := 0
for i := 0; i < 10; i++ {
  sum += i
}
fmt.Println(sum)
           

Go 中的無限循環:

for {
}
           

指針

Go 支援指針。指針是儲存值的位址的地方。 一個指針用 * 定義 。根據資料類型定義指針。 例:

var ap *int
           

上面的 ap 是指向整數類型的指針。& 運算符可用于擷取變量的位址。

a := 12
ap = &a
           

可以使用 * 運算符通路指針指向的值:

fmt.Println(*ap)
// => 12
           

在将結構體作為參數傳遞或者為已定義類型聲明方法時,通常首選指針。

傳遞值時,實際複制的值意味着更多的記憶體

傳遞指針後,函數更改的值将反映在方法 / 函數調用者中。

例子:

func increment(i *int) {
  *i++
}
func main() {
  i := 10
  increment(&i)
  fmt.Println(i)
}
//=> 11
           

Note: 當你在部落格中嘗試示例代碼時,不要忘記将其包含在 main 包中,并在需要時導入 fmt 或其他包,如上面第一個 main.go 示例所示。

函數

main 函數 定義在 main 包中,是程式執行的入口。可以定義和使用更多功能。 讓我們看一個簡單的例子:

func add(a int, b int) int {
  c := a + b
  return c
}
func main() {
  fmt.Println(add(2, 1))
}
//=> 3
           

上面的例子中可以看到,使用 func 關鍵字後面跟函數名定義 Go 的函數

函數的傳回值也可以在函數中預先定義:

func add(a int, b int) (c int) {
  c = a + b
  return
}
func main() {
  fmt.Println(add(2, 1))
}
//=> 3
           

這裡 c 被定義為傳回變量。 是以,定義的變量 c 将自動傳回,而無需在結尾的 return 語句中再次定義。

你還可以從單個函數傳回多個傳回值,将傳回值與逗号分隔開。

func add(a int, b int) (int, string) {
  c := a + b
  return c, "successfully added"
}
func main() {
  sum, message := add(2, 1)
  fmt.Println(message)
  fmt.Println(sum)
}
           

方法,結構體,以及接口

Go 不是絕對的面向對象的語言, 但是使用結構體,接口和方法,它有很多面向對象的風格以及對面向對象的支援。

結構體

結構體是不同字段的類型集合。 結構用于将資料分組在一起。 例如,如果我們想要對 Person 類型的資料進行分組,我們會定義一個 person 的屬性,其中可能包括姓名,年齡,性别。 可以使用以下文法定義結構:

type person struct {
  name string
  age int
  gender string
}
           

在定義了 person 結構體的情況下,現在讓我們建立一個 person 執行個體 p:

//方式1:指定屬性和值
p := person{name: "Bob", age: 42, gender: "Male"}
//方式2:指定值
person{"Bob", 42, "Male"}
           

我們可以用英文的點号(.)輕松通路這些資料

p.name
//=> Bob
p.age
//=> 42
p.gender
//=> Male
           

你還可以使用其指針直接通路結構體裡面的屬性:

pp = &person{name: "Bob", age: 42, gender: "Male"}
pp.name
//=> Bob
           

方法

方法是一個特殊類型的帶有傳回值的函數。傳回值既可以是值,也可以是指針。讓我們建立一個名為 describe 的方法,它具有我們在上面的例子中建立的 person 結構體類型的傳回值:

package main
import "fmt"

//定義結構體
type person struct {
  name   string
  age    int
  gender string
}

// 方法定義
func (p *person) describe() {
  fmt.Printf("%v is %v years old.", p.name, p.age)
}
func (p *person) setAge(age int) {
  p.age = age
}

func (p person) setName(name string) {
  p.name = name
}

func main() {
  pp := &person{name: "Bob", age: 42, gender: "Male"}
  pp.describe()
  // => Bob is 42 years old
  pp.setAge(45)
  fmt.Println(pp.age)
  //=> 45
  pp.setName("Hari")
  fmt.Println(pp.name)
  //=> Bob
}           

從上面的例子中可以看到, 現在可以使用點運算符 調用該方法,就像作為 pp.describe 這樣。請注意,傳回值是指針類型。使用指針,我們傳遞對值的引用,是以如果我們對方法進行任何更改,它将反映在傳回值 pp 中。指針類型的傳回值也不會建立對象的新副本,進而節省了記憶體。

請注意,在上面的示例中,age 的值已更改,而 name 的值不會改變。因為方法 setName 是傳回值是值類型,而 setAge 方法的傳回值是類型指針。

接口

Go 的接口是一系列方法的集合。接口有助于将類型的屬性組合在一起。下面,我們以接口 animal 為例:

type animal interface {
  description() string
}
           

這裡的 animal 是一個接口。現在,我們用兩個不同的執行個體來實作 animal 這個接口:

package main

import (
  "fmt"
)

type animal interface {
  description() string
}

type cat struct {
  Type  string
  Sound string
}

type snake struct {
  Type      string
  Poisonous bool
}

func (s snake) description() string {
  return fmt.Sprintf("Poisonous: %v", s.Poisonous)
}

func (c cat) description() string {
  return fmt.Sprintf("Sound: %v", c.Sound)
}

func main() {
  var a animal
  a = snake{Poisonous: true}
  fmt.Println(a.description())
  a = cat{Sound: "Meow!!!"}
  fmt.Println(a.description())
}

//=> Poisonous: true
//=> Sound: Meow!!!           

在 main 函數中, 我們建立了一個 animal 接口類型的變量 a。我們為 animal 接口指定了 snake 和 cat 兩個執行個體對象,并使用 Println 方法列印 a.description 。

我們所有用 go 語言寫的代碼都是在包含在對應的包中。 main 包是程式執行的入口。Go 中有很多内置包。 我們使用的一個最常見的包是 fmt 包

「Go 的包主要是用來進行大規模程式設計,并且可以将大型項目分成更小的部分。」

--- Robert Griesemer

包的安裝

go get <package-url-github>
// 例子
go get [github.com/satori/go.uuid](https://github.com/satori/go.uuid)
           

我們安裝的包儲存在環境變量 env 的 GOPATH 目錄下,這是我們的工作目錄。 你可以通過我們的工作目錄 cd $GOPATH/pkg 中的 pkg 檔案夾檢視到下載下傳的包。

建立自定義包

我們從建立 custom_package 檔案夾開始:

> mkdir custom_package
> cd custom_package
           

要建立自定義包,首先我們需要建立一個和包名一樣的檔案夾。假設我們要建立一個 person 包,那麼我們得在 custom_package 檔案夾裡建立一個名為 person 的檔案夾。

> mkdir person
> cd person
           

現在我們在該檔案夾中,建立一個 person.go 檔案。

package person
func Description(name string) string {
  return "The person name is: " + name
}
func secretName(name string) string {
  return "Do not share"
}
           

我們現在需要安裝這個包,這樣它才可被引入和使用。我們安裝一下:

> go install
           

現在,我們回到 custom_package 檔案夾中,建立 main.go 檔案。

package main
import(
  "custom_package/person"
  "fmt"
)
func main(){ 
  p := person.Description("Milap")
  fmt.Println(p)
}
// => The person name is: Milap
           

至此,我們已經可以引入建立的 person 包了,并且使用包中的 Description 方法。注意,我們在包中建立的 secretName 方法是無法被通路的。在 Go 語言中,方法名稱為非大寫字母開頭的,即為私有方法。

封包檔

Go 擁有内建的封包檔支援功能。運作如下指令生成文檔。

godoc person Description
           

它将會為我們的 person 包内部的 Description 函數生成文檔。檢視文檔的話隻需要使用如下指令啟動一個 web 伺服器就可以:

godoc -http=":8080"
           

現在去通路這個URL

http://localhost:8080/pkg/

然後你就可以看到我們剛建立的封包檔了。

Go 中部分常見的内置包

fmt

fmt 包實作了格式化 I/O 的功能。我們可以使用這個包來列印到标準輸出。

json

Go 中另外一個有用的常見的包就是 json 包,用來編碼和解碼 json 資料。 接下來,讓我們舉一個例子來編碼和解碼一個 json:

編碼

package main

import (
  "fmt"
  "encoding/json"
)

func main(){
  mapA := map[string]int{"apple": 5, "lettuce": 7}
  mapB, _ := json.Marshal(mapA)
  fmt.Println(string(mapB))
}
           

解碼

package main

import (
  "fmt"
  "encoding/json"
)

type response struct {
  PageNumber int `json:"page"`
  Fruits []string `json:"fruits"`
}

func main(){
  str := `{"page": 1, "fruits": ["apple", "peach"]}`
  res := response{}
  json.Unmarshal([]byte(str), &res)
  fmt.Println(res.PageNumber)
}
//=> 1
           

在使用 unmarshal 函數解碼 json 位元組時,第一個參數是 json 位元組,第二個參數是我們希望 json 映射到的響應類型 struct 的位址。 請注意,json:"page" 将 page 的鍵映射到結構體中的 PageNumber 的鍵。

錯誤處理

錯誤是程式不希望出現的意外結果。假設我們正在對一個外部服務的 API 進行調用。 當然,API 調用可能會成功或者失敗。當出現錯的時候,Go 語言可以識别程式中的錯誤。 我們來看看這個例子:

resp, err := http.Get("http://example.com/")
           

這裡的 API 調用傳回的錯誤對象可能存在或者不存在。 我們可以檢查錯誤是否為 nil 值,并相應地處理響應:

package main

import (
  "fmt"
  "net/http"
)

func main(){
  resp, err := http.Get("http://example.com/")
  if err != nil {
    fmt.Println(err)
    return
  }
  fmt.Println(resp)
}
           

從函數傳回自定義錯誤

當我們寫一個自己的函數時, 在有些情況下存在錯誤要處理,我們利用 error 對象傳回這些錯誤:

func Increment(n int) (int, error) {
  if n < 0 {
    // 傳回一個 error 對象
    return nil, errors.New("math: cannot process negative number")
  }
  return (n + 1), nil
}
func main() {
  num := 5

  if inc, err := Increment(num); err != nil {
    fmt.Printf("Failed Number: %v, error message: %v", num, err)
  }else {
    fmt.Printf("Incremented Number: %v", inc)
  }
}
           

在 Go 中内置的包或我們使用的外部的包都有一個錯誤處理機制。因為我們調用的任何函數都可能存在錯誤。而且這些錯誤永遠不應該被忽略,并且總是在我們稱之為函數的地方優雅地處理,就像我們在上面的例子中所做的那樣。

Panic

panic 是在程式執行期間突然遇到,未經處理的異常。 在 Go 中,panc 不是處理程式中異常的理想方式。 建議使用 error 對象。 發生 panic 時,程式執行停止。 panic 之後要繼續執行的程式就使用 defer。

Defer

Defer 總是在函數結束時執行。

//Go
package main

import "fmt"

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

func f() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered in f", r)
        }
    }()
    fmt.Println("Calling g.")
    g(0)
    fmt.Println("Returned normally from g.")
}

func g(i int) {
    if i > 3 {
        fmt.Println("Panicking!")
        panic(fmt.Sprintf("%v", i))
    }
    defer fmt.Println("Defer in g", i)
    fmt.Println("Printing in g", i)
    g(i + 1)
}           

在上面的例子中,我們使用 panic()來執行程式。 正如你所注意到的一樣,有一個延遲語句,它将使程式在程式執行結束時執行該行。 當我們需要在函數結束時執行某些操作時,也可以使用 Defer,例如關閉檔案。

并發

Go 是建立在并發的基礎上的。Go 中的并發可以通過輕量級線程的 Go routine 來實作。

Go routine

Go routine 是可以與另一個函數并行或并發的函數。 建立 Go routine 非常簡單。 隻需在函數前面添加關鍵字 Go,我們就可以使它并行執行。 Go routine 非常簡單非常輕量級,是以我們可以建立數千個例程。 讓我們看一個簡單的例子:

package main
import (
  "fmt"
  "time"
)
func main() {
  go c()
  fmt.Println("I am main")
  time.Sleep(time.Second * 2)
}
func c() {
  time.Sleep(time.Second * 2)
  fmt.Println("I am concurrent")
}
//=> I am main
//=> I am concurrent
           

就像你在上面的示例中所看到的,函數 c 是一個 Go routine,它與 Go 程式的主線程并行執行。 有時我們希望在多個線程之間共享資源。 Go 不是将一個線程的變量與另一個線程共享,因為這會增加死鎖和資源等待的可能性。 還有另一種在 Go routine 之間共享資源的方法:通過 Go 語言的通道。

通道

我們可以使用通道在兩個 Go routine 之間傳遞資料。 在建立通道時,必須指定通道接收的資料類型。 讓我們建立一個 string 類型的簡單通道,如下所示:

c := make(chan string)
           

有了這個通道,我們可以發送 string 類型資料。 我們都可以在此通道中發送和接收資料:

package main

import "fmt"

func main(){
  c := make(chan string)
  go func(){ c <- "hello" }()
  msg := <-c
  fmt.Println(msg)
}
//=>"hello"
           

接收方通道将會一直等待發送方向通道發送資料。

單向通道

在某些情況下,我們希望 Go routine 通過通道接收資料但不發送資料,反之亦然。 為此,我們還可以建立單向通道。 讓我們看一個簡單的例子:

package main

import (
 "fmt"
)

func main() {
 ch := make(chan string)

 go sc(ch)
 fmt.Println(<-ch)
}

func sc(ch chan<- string) {
 ch <- "hello"
}
           

在上面的例子中,sc 是一個 Go routine,它隻能向通道發送消息但不能接收消息。

使用 select 為 Go routine 處理多個通道

一個程序裡面可能有多個通道正在等待。為此,我們可以使用 select 語句。 讓我們看一個更清晰的例子:

package main

import (
 "fmt"
 "time"
)

func main() {
 c1 := make(chan string)
 c2 := make(chan string)
 go speed1(c1)
 go speed2(c2)
 fmt.Println("The first to arrive is:")
 select {
 case s1 := <-c1:
  fmt.Println(s1)
 case s2 := <-c2:
  fmt.Println(s2)
 }
}

func speed1(ch chan string) {
 time.Sleep(2 * time.Second)
 ch <- "speed 1"
}

func speed2(ch chan string) {
 time.Sleep(1 * time.Second)
 ch <- "speed 2"
}           

在上面的示例中,main 方法正在等待讀取 c1 和 c2 通道的資料。 使用 select case 語句列印出結果,消息會通過通道發送過來,會列印出先發送過來的消息。

緩沖通道

有些情況下,我們需要像一個通道發送多個資料。 你可以為此建立一個緩沖通道。使用緩沖通道, 在緩沖區滿之前接受方不會收到任何消息。 讓我們看一下這個例子:

package main

import "fmt"

func main(){
  ch := make(chan string, 2)
  ch <- "hello"
  ch <- "world"
  fmt.Println(<-ch)
}           

恭喜你!!! 你現在對 Go 有了不錯的認識。

不要止步于此,繼續前進, 考慮一個小的應用程式并開始建構它吧!!!

繼續閱讀