[轉] 一篇文章上手Go語言(排版調整)

導讀:

Go語言是近年來最熱門的編程語言,是Google開發的一種靜態強類型、編譯型、併發型,並具備垃圾回收功能的編程語言。Go語言同時具有開發效率高和執行效率高兩大特色,被譽爲雲計算時代的C語言。本文做者經過一篇文章帶你學會Go語言。git

Go 語言是一門開源語言,可以輕鬆的構建簡單,可靠,高效的軟件。
—— Golang

在不少語言中,解決給定的問題一般有多種方式。工程師須要花費大量的時間思考什麼纔是解決問題的最優解法。而在Golang中,問題的解法一般只有一種。github

這一特性大大節約了工程師的時間,並且使得維護大型代碼庫變得更容易。在Golang中沒有maps和filter這樣"高消耗"的特性。golang

語言的特性帶來更好的表現力也帶來代價。
——Rob Pike

1.開始動手

Golang由包組成。Golang編譯器將main包編譯爲可執行文件,而非共享庫。main包是應用的入口,一般被定義以下:web

package main
下面看一個hello world 的例子,在Golang 的工做空間建立main.go文件。編程

1.1 工做空間

在Go語言中,工做空間由環境變量GOPATH定義。全部編寫的代碼須要在工做空間中。Go語言會在GOPATH和GOROOT的路徑中搜索包。GoROOT是在安裝的時候肯定的安裝路徑。json

下面來設置GOPATH,咱們將~/workspace 加入工做空間。數組

# export env
export GOPATH=~/workspace
# go inside the workspace
cd ~/workspace

1.2 HELLO WORLD!

咱們在剛纔的工做空間下建立main.go 文件,代碼以下:服務器

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

上面的例子中,fmt是Go內置的格式化I/O函數。多線程

咱們在Go語言中使用import關鍵字導入包,func main 是入口函數。Println是fmt包中函數,用於打印 "Hello World!"。架構

讓咱們開始運行該文件。衆所周知Go是編譯型語言,咱們在運行以前先進行編譯。

go build main.go

這將會建立一個二進制運行文件main,咱們如今來運行它:

./main
// Hello World!

另外一種簡單的方式是使用go run 命令:

go run main.go
// Hello World!

2.變量

Go中的變量類型是顯式指定的。Go語言是強類型語言,這意味着在變量聲明的時候會檢查變量類型。

變量定義以下所示:

var a int
在這個例子中,a的初始值被設置爲0。用下面的方式能夠定義並初始化變量。

var a = 1
這裏的變量被編譯器推斷爲int。更簡單的變量定義以下所示:

message := "hello world"
咱們也能夠在同一行聲明多個變量:

var b, c int = 2, 3

3.數據類型

3.1 NUMBER,STRING, BOOLEAN

int 的類型有 int, int8, int16, int32, int64, unit, unit8, unit16, unit 32, unit64, unitptr...
[u開頭表示無符號;uintptr 是一種無符號的整數類型,沒有指定具體的bit大小可是足以容納指針。 uintptr類型只有在底層編程是才須要,特別是Go語言和C語言函數庫或操做系統接口相交互的地方。]

String類型使用byte序列存儲數據,用關鍵字string來聲明變量。
bool 關鍵字表示布爾類型。
Golang 也支持複數,用conplex64和complex128表示。

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

3.2 ARRAYS,SLICES,MAPS

Array 是同類型元素的數組。Array在聲明的時候會指定長度且不能改變。一個數組的定義以下:

var a[5] int
也有多維數組,定義以下

var multiD [2][3]int
Slices 是能隨時擴容的同類型元素的序列 。Slice的聲明方式以下:

var b []int
這將會建立一個容量爲0,長度爲0的Slice。Slice也能夠定義容量和長度,格式以下:

numbers := make([]int, 5, 10)
這個Slice初始長度爲5,容量爲10。

Slice是數組的封裝,其內部實現是數組,slice有三個元素,容量,長度和指向內部數組的指針。
selice圖片
Slice的容量能夠經過append 或者 copy函數增長。Append函數也能在數組的末尾添加元素,在容量不足的狀況下會對slice擴容。

numbers = append(numbers, 1, 2, 3, 4)
另外一種增長slice容量的方式是使用copy函數。Copy函數的原理是建立一個新的大容量的slice,並把原有的slice拷貝到新的slice中。

// 建立新的slice
number2 := make([]int, 15)
// 複製原有的slice到新的slice
copy(number2, number)

咱們也能夠建立slice的子slice。例子以下:

package main

 import (
     "fmt"
 )

 func main() {
     // 初始化slice
     number2 := []int{1, 2, 3, 4}
     fmt.Println(number2) // -> [1 2 3 4]
     // 建立子slice
     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]
 }

Go語言中的Map是鍵值對,定義以下:

var m map[string]int
m是定義的變量名,鍵的類型是string,值的類型是integers。Map中添加鍵值對的例子以下:

package main

 import (
     "fmt"
 )

 func main() {
     m := make(map[string]int)
     // 添加鍵值對
     m["clearity"] = 2
     m["simplicity"] = 3
     // 打印值
     fmt.Println(m["clearity"])   // -> 2
     fmt.Println(m["simplicity"]) // -> 3
 }

4.類型轉換

使用類型轉換可以改變數據類型,例子以下:

package main

 import (
     "fmt"
 )

 func increment(i *int) {
     *i++
 }

 func main() {
     a := 1.1
     b := int(a)
     fmt.Println(b)
     //-> 1
 }

5.條件表達式

5.1 IF ELSE

If else 的例子以下,須要注意的是花括號和條件表達式位於同一行。

package main

 import (
     "fmt"
 )

 func increment(i *int) {
     *i++
 }

 func main() {
     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")
     }
 }

5.2 SWITCH CASE

Switch case 能組織多條件表達式,例子以下:

package main

 import (
     "fmt"
 )

 func increment(i *int) {
     *i++
 }

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

5.3 循環

Golang中只有一個循環表達的關鍵字,不一樣形式的循環表達式以下:

package main

 import (
     "fmt"
 )

 func increment(i *int) {
     *i++
 }

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

上面的例子和C語言中的while循環相似,更爲正式的循環表達形式以下:

package main

 import (
     "fmt"
 )

 func increment(i *int) {
     *i++
 }

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

Go 語言中的死循環定義以下:

for {
}

6.指針

Go 語言可使用指針,指針存儲變量的地址,指針用*來定義。指針的定義和所指數據的類型相關:

var ap *int
這裏的ap是指向整型數據的指針,&用於獲取所變量的地址。

a :=12
ap = &a
* 用於獲取指針所指的地址的值。

fmt.Println(*ap)
// => 12
指針一般用於將結構體作爲參數傳遞。

傳值一般意味着拷貝,意味着須要更多的內存。
使用指針傳遞時,在函數中改變的變量會傳遞給調用的方法或函數。

package main

 import (
     "fmt"
 )

 func increment(i *int) {
     *i++
 }

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

7.函數

main 包中main函數是golang 程序的入口。咱們能夠定義多個函數並調用。例如:

package main

 import (
     "fmt"
 )

 func add(a int, b int) int {
     c := a + b
     return c
 }

 func main() {
     fmt.Println(add(2, 1)) //=> 3
 }

從上面的例子中咱們能夠看出,Golang 中的函數用func關鍵字加上函數名, 後面是附帶數據類型的參數,最後是函數的返回類型。

函數的返回值能夠被預先定義,例子以下:

package main

 import (
     "fmt"
 )

 func add(a int, b int) (c int) {
     c = a + b
     return
 }

 func main() {
     fmt.Println(add(2, 1)) //=> 3
 }

這裏c定義爲返回值,所以變量c將會被自動返回,無需在函數最後的return中聲明。
你也能夠定義一個多個返回值的函數,使用,進行分割。

package main

 import (
     "fmt"
 )

 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) //=> successfully added
     fmt.Println(sum) //=> 3
 }

8.方法,結構體,接口

Golang 不是徹底的面嚮對象語言,可是支持不少面向對象的特性,例若有結構體,接口,方法等。

8.1 結構體

結構體是有類型,不一樣變量的集合。例如咱們想定義Person類型,其中包含姓名,年齡,性別。例如:

type person struct {
    name String
    age int
    gender string
}

定義好了person結構體後,咱們如今來使用它:

//方式 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

8.2 方法

方法是一種帶有接受器的特殊函數。接收器能夠是值或者指針。例子以下:

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的接受器不是指針。

8.3 接口

Golang中的接口是方法的集合,接口有助於將同類型的屬性組合起來,讓咱們一塊兒來看一個anminal的接口。

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。咱們把 snake和cat類型賦值給animal,使用Println 輸出a.description。

咱們在cat和snake中使用不一樣的方式實現了describe方法,咱們獲得了不一樣類型的輸出。

9.包

在Golang中,咱們的代碼在某個包下。main包是程序執行的入口。在Go中有不少內置的包,例如咱們以前用過的fmt包。

Go 的包機制是大型軟件的基礎,可以將大型的工程分解成小部分。
—— Robert Griesemer

9.1 安裝一個包

go get

// 例子
go get github.com/satori/go.uuid

安裝的包保存在GOPATH的環境中,你能夠在 $GOPATH/pkg 路徑下看到安裝的包。

9.2 建立一個自定義包

首先建立一個文件夾 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,須要注意的是在person包中函數secretName不能被訪問,這是由於Go中小寫字母開頭的函數是私有函數。

9.3 生成包文檔

Golang中有內置的功能支持包文檔。運行下面的命令將生成文檔:

godoc person Description

這將會爲Description 函數生成文檔,想要在web服務器上查看文檔須要運行下面的命令:

godoc -http=":8080"

如今打開連接 http://localhost:8080/pkg/ 將會看到咱們剛纔看到的文檔。

9.4 Go 中內置的包

9.4.1 fmt
fmt包實現可標準的I/O函數,咱們在以前的包中用過其中的打印輸出函數。

9.4.2 json
Golang中另外一個內置的重要包的是json,它可以對JSON進行編解碼。

編碼

package main

 import (
     "encoding/json"
     "fmt"
 )

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

解碼

package main

 import (
     "encoding/json"
     "fmt"
 )

 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中的「page」對應的是結構體中的PageNumber。

10.錯誤處理

錯誤是程序中不該該出現的結果。假設咱們編寫一個API調用外部的服務。這個API可能成功也可能失敗。當存在錯誤是,Golang程序可以識別:

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

對API的 調用可能成功也可能失敗,咱們能夠經過檢查錯誤是否爲空來選擇處理方式。

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

10.1 從函數中返回自定義錯誤

當咱們在自定義函數是,某些狀況下會產生錯誤。咱們可使用error 對象返回這些錯誤:

package main

 import (
     "errors"
     "fmt"
 )

 func Increment(n int) (int, error) {
     if n < 0 {
         // return error object
         return 0, 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)
     }
 }

 // => The person name is: Milap

Go 內置的包,外部的包都有處理錯誤的機制。所以咱們調用的函數都有可能產生錯誤。這些錯誤不該該忽略而是應該向上面的例子那樣被優雅的處理。

10.2 Panic

Panic是程序運行中忽然產生未經處理的異常。在Go中,panic不是合理處理異常的方式,推薦使用error對象代替。當Panic產生時,程序將會暫停運行。當panic被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)
 }

10.3 Defer

Defer 在函數結尾必定會執行。在上面的函數中,使用panic() 暫停程序的運行,defer語句使程序執行結束時使用改行。Defer也能夠用做咱們想要在函數的結尾執行的語句,例如關閉文件。

11.併發

Golong使用輕量級線程Go routies支持併發。

11.1 Go routine

Go routine 是可以並行運行的函數。建立Go routine 很是簡單,只須要在函數前添加關鍵字go,這樣函數就可以並行運行了。Go routines 是輕量級的,咱們可以建立上千個Go routines。例如:

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,可以並行運行。咱們想要在多線程中共享資源,可是Golang並不支持。由於這會致使死鎖和資源等待。Go 提供了另外一種共享資源的方式:channel。

11.2 Channels

咱們可使用Channel在兩個Go routine之間傳遞數據。建立channel以前須要制定接受的數據類型。例如咱們建立了一個接受string類型的channel。

c := make(chan string)

有了這個channel以後,咱們能夠經過這個channel發送和接收string類型的數據。

package main

 import "fmt"

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

接收的channel一直等待發送的channel發送數據。

11.3 One way channel

有些狀況下,咱們但願Go routine 經過channel接收數據,但不發送數據,反之亦然。這時候咱們能夠建立一個one-way channel。例如:

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只能給channel發送數據而不能接受數據。

12.使用selecct優化多個channel

有這樣一種狀況,一個函數等待多個channel,這時候咱們可使用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函數等待兩個channel,c1和c2。使用select語句,先從channel中收到的數據會被打印出來。

12.1 Buffered channel

在Golang中能夠建立buffered channel,當buffer滿的時候,發送數據給channel將會被阻塞。例如:

package main

 import "fmt"

 func main() {
     ch := make(chan string, 2)
     ch <- "hello"
     ch <- "world"
     ch <- "!" // extra message in buffer
     fmt.Println(<-ch)
 }

 // => fatal error: all goroutines are asleep - deadlock!
Golang爲何如此成功?
簡單。。。
—— Rob-pike

13.總結

咱們學習Golang以 下的主要模塊和特性:

  • 列表項目
  • 變量,數據類型
  • Array,Slices和Map
  • 函數
  • 循環和條件語句
  • 指針
  • 方法,結構體和接口
  • 錯誤處理
  • 併發——Go routines和channels

恭喜你,你已經對Go有了很好的理解。

One of my most productive days was throwing away 1,000 lines of code.
—— Ken Thompson

不要停下腳步,繼續前進。思考一個小應用程序並開始動手。

原文連接:
原創: Milap Neupane 高可用架構
https://mp.weixin.qq.com/s?__...
https://milapneupane.com.np/2...

本文做者Milap Neupane,由何朋朋翻譯。轉載本文請註明出處,歡迎更多小夥伴加入翻譯及投稿文章的行列,詳情請戳公衆號菜單「聯繫咱們」。

相關文章
相關標籤/搜索