Go 語法速覽與實踐清單(上-V0.5)

上篇某互聯網上市公司基於 Golang 的運維基礎框架視頻
鏈接:
https://pan.baidu.com/s/1KRWibJP8i-s9SJtjKXq_tQ
密碼:fcx3

Go 語法速覽與實踐清單(V0.5)

Go CheatSheet 是對於 Go 學習/實踐過程中的語法與技巧進行盤點,其屬於 Awesome CheatSheet 系列,致力於提升學習速度與研發效能,即可以將其當做速查手冊,也可以作爲輕量級的入門學習資料。 本文參考了許多優秀的文章與代碼示範,統一聲明在了 Go Links;如果希望深入瞭解某方面的內容,可以繼續閱讀 Go 開發:語法基礎與工程實踐請添加鏈接描述,或者前往 coding-snippets/go請添加鏈接描述查看使用 Go 解決常見的數據結構與算法、設計模式、業務功能方面的代碼實現。

環境配置與語法基礎

可以前往這裏下載 Go SDK 安裝包,或者使用 brew 等包管理器安裝。go 命令依賴於 $GOPATH 環境變量進行代碼組織,多項目情況下也可以使用 ln 進行目錄映射以方便進行項目管理。GOPATH 允許設置多個目錄,每個目錄都會包含三個子目錄:src 用於存放源代碼,pkg 用於存放編譯後生成的文件,bin 用於存放編譯後生成的可執行文件。

環境配置完畢後,可以使用 go get 獲取依賴,go run 運行程序,go build 來編譯項目生成與包名(文件夾名)一致的可執行文件。Golang 1.8 之後支持 dep 依賴管理工具,對於空的項目使用 dep init 初始化依賴配置,其會生成 Gopkg.toml Gopkg.lock vendor/ 這三個文件(夾)。

我們可以使用 dep ensure -add github.com/pkg/errors 添加依賴,運行之後,其會在 toml 文件中添加如下鎖:

[[constraint]]
  name = "github.com/pkg/errors"
  version = "0.8.0"

簡單的 Go 中 Hello World 代碼如下:

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

也可以使用 Beego 實現簡單的 HTTP 服務器:

package main
import "github.com/astaxie/beego"
func main() {
   beego.Run()
}

Go 並沒有相對路徑引入,而是以文件夾爲單位定義模塊,譬如我們新建名爲 math 的文件夾,然後使用 package math 來聲明該文件中函數所屬的模塊。

import (
       mongo "mywebapp/libs/mongodb/db" // 對引入的模塊重命名
       _ "mywebapp/libs/mysql/db" // 使用空白下劃線表示僅調用其初始化函數

)

外部引用該模塊是需要使用工作區間或者 vendor 相對目錄,其目錄索引情況如下:

cannot find package "sub/math" in any of:
   ${PROJECTROOT}/vendor/sub/math (vendor tree)
   /usr/local/Cellar/go/1.10/libexec/src/sub/math (from $GOROOT)
   ${GOPATH}/src/sub/math (from $GOPATH)

Go 規定每個源文件的首部需要進行包聲明,可執行文件默認放在 main 包中;而各個包中默認首字母大寫的函數作爲其他包可見的導出函數,而小寫函數則默認外部不可見的私有函數。

表達式與控制流

變量聲明與賦值

作爲強類型靜態語言,Go 允許我們在變量之後標識數據類型,也爲我們提供了自動類型推導的功能。

// 聲明三個變量,皆爲 bool 類型
var c, python, java bool

// 聲明不同類型的變量,並且賦值
var i bool, j int = true, 2

// 複雜變量聲明
var (
   ToBe   bool       = false
   MaxInt uint64     = 1<<64 - 1
   z      complex128 = cmplx.Sqrt(-5 + 12i)
)

// 短聲明變量
c, python, java := true, false, "no!"

// 聲明常量
const constant = "This is a constant"

在 Go 中,如果我們需要比較兩個複雜對象的相似性,可以使用 reflect.DeepEqual 方法:

m1 := map[string]int{
   "a":1,
   "b":2,
}
m2 := map[string]int{
   "a":1,
   "b":2,
}
fmt.Println(reflect.DeepEqual(m1, m2))

條件判斷

Go 提供了增強型的 if 語句進行條件判斷:

// 基礎形式
if x > 0 {
   return x
} else {
   return -x
}

// 條件判斷之前添加自定義語句
if a := b + c; a < 42 {
   return a
} else {
   return a - 42
}

// 常用的類型判斷
var val interface{}
val = "foo"
if str, ok := val.(string); ok {
   fmt.Println(str)
}

Go 也支持使用 Switch 語句:

// 基礎格式
switch operatingSystem {
case "darwin":
   fmt.Println("Mac OS Hipster")
   // 默認 break,不需要顯式聲明
case "linux":
   fmt.Println("Linux Geek")
default:
   // Windows, BSD, ...
   fmt.Println("Other")
}

// 類似於 if,可以在條件之前添加自定義語句
switch os := runtime.GOOS; os {
case "darwin": ...
}

// 使用 switch 語句進行類型判斷:
switch v := anything.(type) {
 case string:
   fmt.Println(v)
 case int32, int64:
   ...
 default:
   fmt.Println("unknown")
}

Switch 中也支持進行比較:

number := 42
switch {
   case number < 42:
       fmt.Println("Smaller")
   case number == 42:
       fmt.Println("Equal")
   case number > 42:
       fmt.Println("Greater")
}

或者進行多條件匹配:

var char byte = '?'
switch char {
   case ' ', '?', '&', '=', '#', '+', '%':
       fmt.Println("Should escape")
}

循環

Go 支持使用 for 語句進行循環,不存在 while 或者 until:

for i := 1; i < 10; i++ {
}

// while - loop
for ; i < 10;  {
}

// 單條件情況下可以忽略分號
for i < 10  {
}

// ~ while (true)
for {
}

我們也可以使用 range 函數,對於 Arrays 與 Slices 進行遍歷:

// loop over an array/a slice
for i, e := range a {
   // i 表示下標,e 表示元素
}

// 僅需要元素
for _, e := range a {
   // e is the element
}

// 或者僅需要下標
for i := range a {
}

// 定時執行
for range time.Tick(time.Second) {
   // do it once a sec
}

Function: 函數

定義,參數與返回值

// 簡單函數定義
func functionName() {}

// 含參函數定義
func functionName(param1 string, param2 int) {}

// 多個相同類型參數的函數定義
func functionName(param1, param2 int) {}

// 函數表達式定義
add := func(a, b int) int {
   return a + b
}

Go 支持函數的最後一個參數使用 ... 設置爲不定參數,即可以傳入一個或多個參數值:

func adder(args ...int) int {
   total := 0
   for _, v := range args { // Iterates over the arguments whatever the number.
       total += v
   }
   return total
}

adder(1, 2, 3) // 6
adder(9, 9) // 18

nums := []int{10, 20, 30}
adder(nums...) // 60

我們也可以使用 Function Stub 作爲函數參數傳入,以實現回調函數的功能:

func Filter(s []int, fn func(int) bool) []int {
   var p []int // == nil
   for _, v := range s {
       if fn(v) {
           p = append(p, v)
       }
   }
   return p
}

雖然 Go 不是函數式語言,但是也可以用其實現柯里函數(Currying Function):

func add(x, y int) int {
   return x+ y
}

func adder(x int) (func(int) int) {
   return func(y int) int {
       return add(x, y)
   }
}

func main() {
   add3 := adder(3)
   fmt.Println(add3(4))    // 7
}

Go 支持多個返回值:

// 返回單個值
func functionName() int {
   return 42
}

// 返回多個值
func returnMulti() (int, string) {
   return 42, "foobar"
}
var x, str = returnMulti()

// 命名返回多個值
func returnMulti2() (n int, s string) {
   n = 42
   s = "foobar"
   // n and s will be returned
   return
}
var x, str = returnMulti2()

閉包: Closure

Go 同樣支持詞法作用域與變量保留,因此我們可以使用閉包來訪問函數定義處外層的變量:

func scope() func() int{
   outer_var := 2
   foo := func() int { return outer_var}
   return foo
}

閉包中並不能夠直接修改外層變量,而是會自動重定義新的變量值:

func outer() (func() int, int) {
   outer_var := 2
   inner := func() int {
       outer_var += 99
       return outer_var // => 101 (but outer_var is a newly redefined
   }
   return inner, outer_var // => 101, 2 (outer_var is still 2, not mutated by inner!)
}

函數執行

Go 中提供了 defer 關鍵字,允許將某個語句的執行推遲到函數返回語句之前:

func read(...) (...) {
 f, err := os.Open(file)
 ...
 defer f.Close()
 ...
 return .. // f will be closed

異常處理

Go 語言中並不存在 try-catch 等異常處理的關鍵字,對於那些可能返回異常的函數,只需要在函數返回值中添加額外的 Error 類型的返回值:

type error interface {
   Error() string
}

某個可能返回異常的函數調用方式如下:

import (
   "fmt"
   "errors"
)

func main() {
   result, err:= Divide(2,0)

   if err != nil {
           fmt.Println(err)
   }else {
           fmt.Println(result)
   }
}

func Divide(value1 int,value2 int)(int, error) {
   if(value2 == 0){
       return 0, errors.New("value2 mustn't be zero")
   }
   return value1/value2  , nil
}

Go 還爲我們提供了 panic 函數,所謂 panic,即是未獲得預期結果,常用於拋出異常結果。譬如當我們獲得了某個函數返回的異常,卻不知道如何處理或者不需要處理時,可以直接通過 panic 函數中斷當前運行,打印出錯誤信息、Goroutine 追蹤信息,並且返回非零的狀態碼:

_, err := os.Create("/tmp/file")
if err != nil {
   panic(err)
}

數據類型與結構

類型綁定與初始化

Go 中的 type 關鍵字能夠對某個類型進行重命名:

// IntSlice 並不等價於 []int,但是可以利用類型轉換進行轉換
type IntSlice []int
a := IntSlice{1, 2}

可以使用 T(v) 或者 obj.(T) 進行類型轉換,obj.(T) 僅針對 interface{} 類型起作用:

t := obj.(T) // if obj is not T, error
t, ok := obj.(T) // if obj is not T, ok = false

// 類型轉換與判斷
str, ok := val.(string);

基本數據類型

interface {} // ~ java Object
bool // true/false
string
int8  int16  int32  int64
int // =int32 on 32-bit, =int64 if 64-bit OS
uint8 uint16 uint32 uint64 uintptr
uint
byte // alias for uint8
rune // alias for int32, represents a Unicode code point
float32 float64

字符串

// 多行字符串聲明
hellomsg := `
"Hello" in Chinese is 你好 ('Ni Hao')
"Hello" in Hindi is नमस्ते ('Namaste')

格式化字符串:

fmt.Println("Hello, 你好, नमस्ते, Привет, ᎣᏏᏲ") // basic print, plus newline
p := struct { X, Y int }{ 17, 2 }
fmt.Println( "My point:", p, "x coord=", p.X ) // print structs, ints, etc
s := fmt.Sprintln( "My point:", p, "x coord=", p.X ) // print to string variable

fmt.Printf("%d hex:%x bin:%b fp:%f sci:%e",17,17,17,17.0,17.0) // c-ish format
s2 := fmt.Sprintf( "%d %f", 17, 17.0 ) // formatted print to string variable

序列類型

Array 與 Slice 都可以用來表示序列數據,二者也有着一定的關聯。

Array

其中 Array 用於表示固定長度的,相同類型的序列對象,可以使用如下形式創建:

[N]Type
[N]Type{value1, value2, ..., valueN}

// 由編譯器自動計算數目
[...]Type{value1, value2, ..., valueN}

其具體使用方式爲:

// 數組聲明
var a [10]int

// 賦值
a[3] = 42

// 讀取
i := a[3]

// 聲明與初始化
var a = [2]int{1, 2}
a := [2]int{1, 2}
a := [...]int{1, 2}

Go 內置了 len 與 cap 函數,用於獲取數組的尺寸與容量:

var arr = [3]int{1, 2, 3}
arr := [...]int{1, 2, 3}

len(arr) // 3
cap(arr) // 3

不同於 C/C++ 中的指針(Pointer)或者 Java 中的對象引用(Object Reference),Go 中的 Array 只是值(Value)。這也就意味着,當進行數組拷貝,或者函數調用中的參數傳值時,會複製所有的元素副本,而非僅僅傳遞指針或者引用。顯而易見,這種複製的代價會較爲昂貴。

Slice

Slice 爲我們提供了更爲靈活且輕量級地序列類型操作,可以使用如下方式創建 Slice:

// 使用內置函數創建
make([]Type, length, capacity)
make([]Type, length)

// 聲明爲不定長度數組
[]Type{}
[]Type{value1, value2, ..., valueN}

// 對現有數組進行切片轉換
array[:]
array[:2]
array[2:]
array[2:3]

不同於 Array,Slice 可以看做更爲靈活的引用類型(Reference Type),它並不真實地存放數組值,而是包含數組指針(ptr),len,cap 三個屬性的結構體。換言之,Slice 可以看做對於數組中某個段的描述,包含了指向數組的指針,段長度,以及段的最大潛在長度,其結構如下圖所示:
Go 語法速覽與實踐清單(上-V0.5)

// 創建 len 爲 5,cap 爲 5 的 Slice
s := make([]byte, 5)

// 對 Slice 進行二次切片,此時 len 爲 2,cap 爲 3
s = s[2:4]

// 恢復 Slice 的長度
s = s[:cap(s)]

需要注意的是, 切片操作並不會真實地複製 Slice 中值,只是會創建新的指向原數組的指針,這就保證了切片操作和操作數組下標有着相同的高效率。不過如果我們修改 Slice 中的值,那麼其會 真實修改底層數組中的值,也就會體現到原有的數組中:

d := []byte{'r', 'o', 'a', 'd'}
e := d[2:]
// e == []byte{'a', 'd'}
e[1] = 'm'
// e == []byte{'a', 'm'}
// d == []byte{'r', 'o', 'a', 'm'}

Go 提供了內置的 append 函數,來動態爲 Slice 添加數據,該函數會返回新的切片對象,包含了原始的 Slice 中值以及新增的值。如果原有的 Slice 的容量不足以存放新增的序列,那麼會自動分配新的內存:

// len=0 cap=0 []
var s []int

// len=1 cap=2 [0]
s = append(s, 0)

// len=2 cap=2 [0 1]
s = append(s, 1)

// len=5 cap=8 [0 1 2 3 4]
s = append(s, 2, 3, 4)

// 使用 ... 來自動展開數組
a := []string{"John", "Paul"}
b := []string{"George", "Ringo", "Pete"}
a = append(a, b...) // equivalent to "append(a, b[0], b[1], b[2])"
// a == []string{"John", "Paul", "George", "Ringo", "Pete"}

我們也可以使用內置的 copy 函數,進行 Slice 的複製,該函數支持對於不同長度的 Slice 進行復制,其會自動使用最小的元素數目。同時,copy 函數還能夠自動處理使用了相同的底層數組之間的 Slice 複製,以避免額外的空間浪費。

func copy(dst, src []T) int

// 申請較大的空間容量
t := make([]byte, len(s), (cap(s)+1)*2)
copy(t, s)
s = t

映射類型

var m map[string]int
m = make(map[string]int)
m["key"] = 42

// 刪除某個鍵
delete(m, "key")

// 測試該鍵對應的值是否存在
elem, has_value := m["key"]

// map literal
var m = map[string]Vertex{
   "Bell Labs": {40.68433, -74.39967},
   "Google":    {37.42202, -122.08408},
}

Struct & Interface: 結構體與接口

Struct: 結構體

Go 語言中並不存在類的概念,只有結構體,結構體可以看做屬性的集合,同時可以爲其定義方法。

// 聲明結構體
type Vertex struct {
   // 結構體的屬性,同樣遵循大寫導出,小寫私有的原則
   X, Y int
   z bool
}

// 也可以聲明隱式結構體
point := struct {
   X, Y int
}{1, 2}

// 創建結構體實例
var v = Vertex{1, 2}

// 讀取或者設置屬性
v.X = 4;

// 顯示聲明鍵
var v = Vertex{X: 1, Y: 2}

// 聲明數組
var v = []Vertex{{1,2},{5,2},{5,5}}

方法的聲明也非常簡潔,只需要在 func 關鍵字與函數名之間聲明結構體指針即可,該結構體會在不同的方法間進行復制:

func (v Vertex) Abs() float64 {
   return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

// Call method
v.Abs()

對於那些需要修改當前結構體對象的方法,則需要傳入指針:

func (v *Vertex) add(n float64) {
   v.X += n
   v.Y += n
}

var p *Person = new(Person) // pointer of type Person

Pointer: 指針

// p 是 Vertex 類型
p := Vertex{1, 2}  

// q 是指向 Vertex 的指針
q := &p

// r 同樣是指向 Vertex 對象的指針
r := &Vertex{1, 2}

// 指向 Vertex 結構體對象的指針類型爲 *Vertex
var s *Vertex = new(Vertex)

Interface: 接口

Go 允許我們通過定義接口的方式來實現多態性:

// 接口聲明
type Awesomizer interface {
   Awesomize() string
}

// 結構體並不需要顯式實現接口
type Foo struct {}

// 而是通過實現所有接口規定的方法的方式,來實現接口
func (foo Foo) Awesomize() string {
   return "Awesome!"
}
type Shape interface {
  area() float64
}

func getArea(shape Shape) float64 {
  return shape.area()
}

type Circle struct {
  x,y,radius float64
}

type Rectangle struct {
  width, height float64
}

func(circle Circle) area() float64 {
  return math.Pi * circle.radius * circle.radius
}

func(rect Rectangle) area() float64 {
  return rect.width * rect.height
}

func main() {
  circle := Circle{x:0,y:0,radius:5}
  rectangle := Rectangle {width:10, height:5}

  fmt.Printf("Circle area: %f\n",getArea(circle))
  fmt.Printf("Rectangle area: %f\n",getArea(rectangle))
}
//Circle area: 78.539816
//Rectangle area: 50.000000

慣用的思路是先定義接口,再定義實現,最後定義使用的方法:

package animals

type Animal interface {
   Speaks() string
}

// implementation of Animal
type Dog struct{}
func (a Dog) Speaks() string { return "woof" }

/** 在需要的地方直接引用 **/

package circus

import "animals"

func Perform(a animal.Animal) { return a.Speaks() }

Go 也爲我們提供了另一種接口的實現方案,我們可以不在具體的實現處定義接口,而是在需要用到該接口的地方,該模式爲:

func funcName(a INTERFACETYPE) CONCRETETYPE

定義接口:

package animals

type Dog struct{}
func (a Dog) Speaks() string { return "woof" }

/** 在需要使用實現的地方定義接口 **/
package circus

type Speaker interface {
   Speaks() string
}

func Perform(a Speaker) { return a.Speaks() }

未完待續......

轉載|Segmentfault
感謝作者:王下邀月熊_Chevalier
查看原文

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章