Golang從入門到深刻

爲何學習Golang語言

Go語言爲併發而生

Go語言的併發是基於 goroutine 的,goroutine 相似於線程,但並不是線程。能夠將 goroutine 理解爲一種虛擬線程。Go 語言運行時會參與調度 goroutine,並將 goroutine 合理地分配到每一個 CPU 中,最大限度地使用CPU性能。開啓一個goroutine的消耗很是小(大約2KB的內存),你能夠輕鬆建立數百萬個goroutine。
goroutine的特色:html

  • goroutine具備可增加的分段堆棧。這意味着它們只在須要時纔會使用更多內存。
  • goroutine的啓動時間比線程快。
  • goroutine原生支持利用channel安全地進行通訊。
  • goroutine共享數據結構時無需使用互斥鎖。

Go性能強悍

15511709770708.jpg
數據來源:https://benchmarksgame-team.p...linux

Go語言簡單易學

  • 語法簡潔
  • 代碼風格統一
  • 開發效率高

發展前景

大公司都在用,跟着主流走沒錯。
image-20190203135218456.pnggit

環境搭建

下載地址

Go官網下載地址:https://golang.org/dl/
Go官方鏡像站(推薦):https://golang.google.cn/dl/程序員

安裝Golang

windwos/mac安裝

安裝方式基本都是下一步下一步傻瓜式安裝,很少介紹github

centos安裝

下載壓縮包golang

wget  https://dl.google.com/go/go1.13.4.linux-amd64.tar.gz

下載好的文件解壓到/usr/local目錄下:web

mkdir -p /usr/local/go  # 建立目錄
tar -C /usr/lcoal/go zxvf go1.13.4.linux-amd64.tar.gz

若是提示沒有權限,加上sudo以root用戶的身份再運行。執行完就能夠在/usr/local/下看到go目錄
配置環境變量: Linux下有兩個文件能夠配置環境變量,其中/etc/profile是對全部用戶生效的,添加以下兩行代碼,保存退出sql

export GOROOT=/usr/local/go
export PATH=$PATH:$GOROOT/bin

使用source命令加載/etc/profile文件便可生效。 檢查:編程

> source /etc/profile
> go version

配置GOPATH

GOPATH是一個環境變量,用來代表你寫的go項目的存放路徑(工做目錄)。
GOPATH路徑最好只設置一個,全部的項目代碼都放到GOPATH的src目錄下。
Go1.11版本以後,開啓go mod模式以後就再也不強制須要配置GOPATH了。
Linux和Mac平臺就參照上面配置環境變量的方式將本身的工做目錄添加到環境變量中便可
GOPATH在不一樣操做系統平臺上的默認值json

在Windows 平臺中GOPATH默認值是%USERPROFILE%/go,例如:C:Users用戶名go
在Unix 平臺中GOPATH默認值是$HOME/go,例如:/home/用戶名/go

Go項目結構

在進行Go語言開發的時候,咱們的代碼老是會保存在$GOPATH/src目錄下。在工程通過go build、go install或go get等指令後,會將下載的第三方包源代碼文件放在$GOPATH/src目錄下, 產生的二進制可執行文件放在 $GOPATH/bin目錄下,生成的中間緩存文件會被保存在 $GOPATH/pkg 下。
1550805044488.png

go mod 包管理極力推薦

go module是Go1.11版本以後官方推出的版本管理工具,而且從Go1.13版本開始,go module將是Go語言默認的依賴管理工具。
具體參考:https://github.com/golang/go/...

GO111MODULE

要啓用go module支持首先要設置環境變量GO111MODULE,經過它能夠開啓或關閉模塊支持,它有三個可選值:offonauto,默認值是auto

  1. GO111MODULE=off禁用模塊支持,編譯時會從GOPATHvendor文件夾中查找包。
  2. GO111MODULE=on啓用模塊支持,編譯時會忽略GOPATHvendor文件夾,只根據go.mod下載依賴。
  3. GO111MODULE=auto,當項目在$GOPATH/src外且項目根目錄有go.mod文件時,開啓模塊支持。

簡單來講,設置GO111MODULE=on以後就可使用go module了,之後就沒有必要在GOPATH中建立項目了,而且還可以很好的管理項目依賴的第三方包信息。

使用 go module 管理依賴後會在項目根目錄下生成兩個文件go.modgo.sum

Go1.11以後設置GOPROXY命令爲:

export GOPROXY=https://proxy.golang.org
阿里雲配置以下:
export GOPROXY= https://mirrors.aliyun.com/go...

nexus社區提供配置以下:
export GOPROXY=https://gonexus.dev

goproxy.io的配置以下:
export GOPROXY=https://goproxy.io/

基於athens的公共服務配置以下:
export GOPROXY=https://athens.azurefd.net

官方提供的(jfrog,golang)
export GOPROXY=https://gocenter.io
export GOPROXY=https://proxy.golang.org

七牛雲贊助支持的
export GOPROXY=https://goproxy.cn

go mod命令

go mod download    下載依賴的module到本地cache(默認爲$GOPATH/pkg/mod目錄)
go mod edit        編輯go.mod文件
go mod graph       打印模塊依賴圖
go mod init        初始化當前文件夾, 建立go.mod文件
go mod tidy        增長缺乏的module,刪除無用的module
go mod vendor      將依賴複製到vendor下
go mod verify      校驗依賴
go mod why         解釋爲何須要依賴

Golang變量和常量

變量和常量是編程中必不可少的部分,也是很好理解的一部分。

標識符

在編程語言中標識符就是程序員定義的具備特殊意義的詞,好比變量名、常量名、函數名等等。 Go語言中標識符由字母數字和_(下劃線)組成,而且只能以字母和_開頭。 舉幾個例子:abc, _, _123, a123。

關鍵字

關鍵字是指編程語言中預先定義好的具備特殊含義的標識符。 關鍵字和保留字都不建議用做變量名。
Go語言中有25個關鍵字:

break         //退出循環
default     //選擇結構默認項(switch、select)
func         //定義函數
interface    //定義接口
select        //channel
case         //選擇結構標籤
chan         //定義channel
const         //常量
continue     //跳過本次循環
defer         //延遲執行內容(收尾工做)
go         //併發執行
map         //map類型
struct        //定義結構體
else         //選擇結構
goto         //跳轉語句
package     //包
switch        //選擇結構
fallthrough     //??
if         //選擇結構
range         //從slice、map等結構中取元素
type        //定義類型
for         //循環
import         //導入包
return         //返回
var        //定義變量

Go語言中還有37個保留字

break     default     func     interface    select
case      defer       go       map          struct
chan      else        goto     package      switch
const     fallthrough if       range        type
continue  for         import   return       var

變量

變量的來歷

程序運行過程當中的數據都是保存在內存中,咱們想要在代碼中操做某個數據時就須要去內存上找到這個變量,可是若是咱們直接在代碼中經過內存地址去操做變量的話,代碼的可讀性會很是差並且還容易出錯,因此咱們就利用變量將這個數據的內存地址保存起來,之後直接經過這個變量就能找到內存上對應的數據了。

變量類型

變量(Variable)的功能是存儲數據。不一樣的變量保存的數據類型可能會不同。通過半個多世紀的發展,編程語言已經基本造成了一套固定的類型,常見變量的數據類型有:整型、浮點型、布爾型等。
Go語言中的每個變量都有本身的類型,而且變量必須通過聲明才能開始使用。

變量聲明

聲明格式

var 變量名 變量類型

變量聲明以關鍵字var開頭,變量類型放在變量的後面,行尾無需分號。 舉個例子:

var name string
var age int
var sex bool

每聲明一個變量就須要寫var關鍵字會比較繁瑣,go語言中還支持批量變量聲明:

var (
    a string
    b int
    c bool
    d float32
)

變量的初始化

Go語言在聲明變量的時候,會自動對變量對應的內存區域進行初始化操做。每一個變量會被初始化成其類型的默認值,例如: 整型和浮點型變量的默認值爲0。 字符串變量的默認值爲空字符串。 布爾型變量默認爲false。 切片、函數、指針變量的默認爲nil。
變量初始化的標準格式以下:

var 變量名 類型 = 表達式

舉例

var name string = "Q1mi"
var age int = 18

或
var name, age = "Q1mi", 20

可使用更簡略的 := 方式聲明並初始化變量

n := 10

在使用多重賦值時,若是想要忽略某個值,可使用匿名變量(anonymous variable)。 匿名變量用一個下劃線_表示,例如:

file, _ := os.Open("test.log")

匿名變量不佔用命名空間,不會分配內存,因此匿名變量之間不存在重複聲明。 (在Lua等編程語言裏,匿名變量也被叫作啞元變量。)
注意事項:

  1. 函數外的每一個語句都必須以關鍵字開始(var、const、func等)
  2. :=不能使用在函數外。
  3. _多用於佔位,表示忽略值。

常量

相對於變量,常量是恆定不變的值,多用於定義程序運行期間不會改變的那些值。 常量的聲明和變量聲明很是相似,只是把var換成了const,常量在定義的時候必須賦值。

const pi = 3.1415
const e = 2.7182
或
const (
    pi = 3.1415
    e = 2.7182
)

iota

iota是go語言的常量計數器,只能在常量的表達式中使用。

iota在const關鍵字出現時將被重置爲0。const中每新增一行常量聲明將使iota計數一次(iota可理解爲const語句塊中的行索引)。 使用iota能簡化定義,在定義枚舉時頗有用。

例如

const (
        n1 = iota //0
        n2        //1
        n3        //2
        n4        //3
    )
    
const (
        a, b = iota + 1, iota + 2 //1,2
        c, d                      //2,3
        e, f                      //3,4
)

定義數量級 (這裏的<<表示左移操做,1<<10表示將1的二進制表示向左移10位,也就是由1變成了10000000000,也就是十進制的1024。同理2<<2表示將2的二進制表示向左移2位,也就是由10變成了1000,也就是十進制的8。)

const (
        _  = iota
        KB = 1 << (10 * iota)
        MB = 1 << (10 * iota)
        GB = 1 << (10 * iota)
        TB = 1 << (10 * iota)
        PB = 1 << (10 * iota)
    )

基本數據類型

Go語言中有豐富的數據類型,除了基本的整型、浮點型、布爾型、字符串外,還有數組、切片、結構體、函數、map、通道(channel)等。Go 語言的基本類型和其餘語言大同小異。

整型

整型分爲如下兩個大類: 按長度分爲:int八、int1六、int3二、int64 對應的無符號整型:uint八、uint1六、uint3二、uint64

其中,uint8就是咱們熟知的byte型,int16對應C語言中的short型,int64對應C語言中的long型。
image.png

特殊整型

image.png
注意:在使用intuint類型時,不能假定它是32位或64位的整型,而是考慮intuint可能在不一樣平臺上的差別。

注意事項:獲取對象的長度的內建len()函數返回的長度能夠根據不一樣平臺的字節長度進行變化。實際使用中,切片或 map 的元素數量等均可以用int來表示。在涉及到二進制傳輸、讀寫文件的結構描述時,爲了保持文件的結構不會受到不一樣編譯目標平臺字節長度的影響,不要使用intuint

數字字面量語法(Number literals syntax)

Go1.13版本以後引入了數字字面量語法,這樣便於開發者以二進制、八進制或十六進制浮點數的格式定義數字,例如:
v := 0b00101101, 表明二進制的 101101,至關於十進制的 45。 v := 0o377,表明八進制的 377,至關於十進制的 255。 v := 0x1p-2,表明十六進制的 1 除以 2²,也就是 0.25。 並且還容許咱們用 _ 來分隔數字,好比說:

v := 123_456  // 123456。

浮點型

Go語言支持兩種浮點型數:float32和float64。這兩種浮點型數據格式遵循IEEE 754標準: float32 的浮點數的最大範圍約爲 3.4e38,可使用常量定義:math.MaxFloat32。 float64 的浮點數的最大範圍約爲 1.8e308,可使用一個常量定義:math.MaxFloat64。

打印浮點數時,可使用fmt包配合動詞%f,代碼以下:

fmt.Printf("%f\n", math.Pi)
fmt.Printf("%.2f\n", math.Pi)

複數

complex64和complex128

var c1 complex64
c1 = 1 + 2i
var c2 complex128
c2 = 2 + 3i
fmt.Println(c1)
fmt.Println(c2)

複數有實部和虛部,complex64的實部和虛部爲32位,complex128的實部和虛部爲64位。

布爾值

Go語言中以bool類型進行聲明布爾型數據,布爾型數據只有true(真)false(假)兩個值。
注意:

  1. 布爾類型變量的默認值爲false
  2. Go 語言中不容許將整型強制轉換爲布爾型.
  3. 布爾型沒法參與數值運算,也沒法與其餘類型進行轉換。

字符串

Go語言中的字符串以原生數據類型出現,使用字符串就像使用其餘原生數據類型(int、bool、float3二、float64 等)同樣。 Go 語言裏的字符串的內部實現使用UTF-8編碼。 字符串的值爲雙引號(")中的內容,能夠在Go語言的源碼中直接添加非ASCII碼字符,例如:

s1 := "hello"
s2 := "你好"

字符串轉義符

Go 語言的字符串常見轉義符包含回車、換行、單雙引號、製表符等,以下表所示。
image.png

多行字符串

Go語言中要定義一個多行字符串時,就必須使用反引號字符:

s1 := `第一行
第二行
第三行
`
fmt.Println(s1)

byte和rune類型

組成每一個字符串的元素叫作「字符」,能夠經過遍歷或者單個獲取字符串元素得到字符。 字符用單引號(’)包裹起來,如:

var a := '中'
var b := 'x'

Go 語言的字符有如下兩種:

  1. uint8類型,或者叫 byte 型,表明了ASCII碼的一個字符。
  2. rune類型,表明一個UTF-8字符

類型轉換

Go語言中只有強制類型轉換,沒有隱式類型轉換。該語法只能在兩個類型之間支持相互轉換的時候使用。
好比:計算直角三角形的斜邊長時使用math包的Sqrt()函數,該函數接收的是float64類型的參數,而變量a和b都是int類型的,這個時候就須要將a和b強制類型轉換爲float64類型。

func sqrtDemo() {
    var a, b = 3, 4
    var c int
    // math.Sqrt()接收的參數是float64類型,須要強制轉換
    c = int(math.Sqrt(float64(a*a + b*b)))
    fmt.Println(c)
}

運算符

Go 語言內置的運算符有:算術運算符,關係運算符,邏輯運算符,位運算符,賦值運算符.

算術運算符

加+ 減 - 乘 * 除 / 求餘 % 自增 ++ 自減 –
代碼以下:

package main

import (
    "fmt"
)

func main()  {
    var a int = 21
    var b int = 10
    var c int

    c = a + b
    fmt.Printf("第一行 - c 的值爲 %d\n", c )
    c = a - b
    fmt.Printf("第二行 - c 的值爲 %d\n", c )
    c = a * b
    fmt.Printf("第三行 - c 的值爲 %d\n", c )
    c = a / b
    fmt.Printf("第四行 - c 的值爲 %d\n", c )
    c = a % b
    fmt.Printf("第五行 - c 的值爲 %d\n", c )
    a++
    fmt.Printf("第六行 - a 的值爲 %d\n", a )
    a=21   // 爲了方便測試,a 這裏從新賦值爲 21
    a--
    fmt.Printf("第七行 - a 的值爲 %d\n", a )

}

結果輸出:

第一行 - c 的值爲 31
第二行 - c 的值爲 11
第三行 - c 的值爲 210
第四行 - c 的值爲 2
第五行 - c 的值爲 1
第六行 - a 的值爲 22
第七行 - a 的值爲 20

關係運算符

== != > < >= <=
代碼以下:

package main

import (
    "fmt"
)

func main()  {
    var a int = 21
    var b int = 10

    if( a == b ) {
        fmt.Printf("第一行 - a 等於 b\n" )
    } else {
        fmt.Printf("第一行 - a 不等於 b\n" )
    }
    if ( a < b ) {
        fmt.Printf("第二行 - a 小於 b\n" )
    } else {
        fmt.Printf("第二行 - a 不小於 b\n" )
    }

    if ( a > b ) {
        fmt.Printf("第三行 - a 大於 b\n" )
    } else {
        fmt.Printf("第三行 - a 不大於 b\n" )
    }
    /* Lets change value of a and b */
    a = 5
    b = 20
    if ( a <= b ) {
        fmt.Printf("第四行 - a 小於等於 b\n" )
    }
    if ( b >= a ) {
        fmt.Printf("第五行 - b 大於等於 a\n" )
    }
}

結果輸出:

第一行 - a 不等於 b
第二行 - a 不小於 b
第三行 - a 大於 b
第四行 - a 小於等於 b
第五行 - b 大於等於 a

邏輯運算符

&& || !
代碼以下:

package main

import (
    "fmt"
)

func main()  {
    var a bool = true
    var b bool = false
    if ( a && b ) {
        fmt.Printf("第一行 - 條件爲 true\n" )
    }
    if ( a || b ) {
        fmt.Printf("第二行 - 條件爲 true\n" )
    }
    /* 修改 a 和 b 的值 */
    a = false
    b = true
    if ( a && b ) {
        fmt.Printf("第三行 - 條件爲 true\n" )
    } else {
        fmt.Printf("第三行 - 條件爲 false\n" )
    }
    if ( !(a && b) ) {
        fmt.Printf("第四行 - 條件爲 true\n" )
    }
}

結果輸出:

第二行 - 條件爲 true
第三行 - 條件爲 false
第四行 - 條件爲 true

位運算符

代碼以下:

package main

import (
    "fmt"
)

func main()  {
    var a uint = 60    /* 60 = 0011 1100 */
    var b uint = 13    /* 13 = 0000 1101 */
    var c uint = 0

    c = a & b       /* 12 = 0000 1100 */
    fmt.Printf("第一行 - c 的值爲 %d\n", c )

    c = a | b       /* 61 = 0011 1101 */
    fmt.Printf("第二行 - c 的值爲 %d\n", c )

    c = a ^ b       /* 49 = 0011 0001 */
    fmt.Printf("第三行 - c 的值爲 %d\n", c )

    c = a << 2     /* 240 = 1111 0000 */
    fmt.Printf("第四行 - c 的值爲 %d\n", c )

    c = a >> 2     /* 15 = 0000 1111 */
    fmt.Printf("第五行 - c 的值爲 %d\n", c )

}

結果輸出:

第一行 - c 的值爲 12
第二行 - c 的值爲 61
第三行 - c 的值爲 49
第四行 - c 的值爲 240
第五行 - c 的值爲 15

賦值運算符

= += -= *= /= %= <<= >>= &= ^= |=
代碼以下:

package main

import (
    "fmt"
)

func main()  {
    var a int = 21
    var c int

    c =  a
    fmt.Printf("第 1 行 - =  運算符實例,c 值爲 = %d\n", c )

    c +=  a
    fmt.Printf("第 2 行 - += 運算符實例,c 值爲 = %d\n", c )

    c -=  a
    fmt.Printf("第 3 行 - -= 運算符實例,c 值爲 = %d\n", c )

    c *=  a
    fmt.Printf("第 4 行 - *= 運算符實例,c 值爲 = %d\n", c )

    c /=  a
    fmt.Printf("第 5 行 - /= 運算符實例,c 值爲 = %d\n", c )

    c  = 200;

    c <<=  2
    fmt.Printf("第 6行  - <<= 運算符實例,c 值爲 = %d\n", c )

    c >>=  2
    fmt.Printf("第 7 行 - >>= 運算符實例,c 值爲 = %d\n", c )

    c &=  2
    fmt.Printf("第 8 行 - &= 運算符實例,c 值爲 = %d\n", c )

    c ^=  2
    fmt.Printf("第 9 行 - ^= 運算符實例,c 值爲 = %d\n", c )

    c |=  2
    fmt.Printf("第 10 行 - |= 運算符實例,c 值爲 = %d\n", c )
}

結果輸出:

第 1 行 - =  運算符實例,c 值爲 = 21
第 2 行 - += 運算符實例,c 值爲 = 42
第 3 行 - -= 運算符實例,c 值爲 = 21
第 4 行 - *= 運算符實例,c 值爲 = 441
第 5 行 - /= 運算符實例,c 值爲 = 21
第 6行  - <<= 運算符實例,c 值爲 = 800
第 7 行 - >>= 運算符實例,c 值爲 = 200
第 8 行 - &= 運算符實例,c 值爲 = 0
第 9 行 - ^= 運算符實例,c 值爲 = 2
第 10 行 - |= 運算符實例,c 值爲 = 2

其餘運算符

& *
代碼以下:

package main

import "fmt"

func main()  {
    var a int = 4
    var b int32
    var c float32
    var ptr *int

    /* 運算符實例 */
    fmt.Printf("第 1 行 - a 變量類型爲 = %T\n", a );
    fmt.Printf("第 2 行 - b 變量類型爲 = %T\n", b );
    fmt.Printf("第 3 行 - c 變量類型爲 = %T\n", c );

    /*  & 和 * 運算符實例 */
    ptr = &a    /* 'ptr' 包含了 'a' 變量的地址 */
    fmt.Printf("a 的值爲  %d\n", a);
    fmt.Printf("*ptr 爲 %d\n", *ptr);

}

結果輸出:

第 1 行 - a 變量類型爲 = int
第 2 行 - b 變量類型爲 = int32
第 3 行 - c 變量類型爲 = float32
a 的值爲  4
*ptr 爲 4

流程控制

流程控制是每種編程語言控制邏輯走向和執行次序的重要部分,流程控制能夠說是一門語言的「經脈」。

Go語言中最經常使用的流程控制有if和for,而switch和goto主要是爲了簡化代碼、下降重複代碼而生的結構,屬於擴展類的流程控制。

if else(分支結構)

Go語言中if條件判斷的格式以下:

if 表達式1 {
    分支1
} else if 表達式2 {
    分支2
} else{
    分支3
}

案例:

func Score(score int)  {
    if score >= 90 {
        fmt.Println("A")
    } else if score > 75 {
        fmt.Println("B")
    } else {
        fmt.Println("C")
    }
}

//能夠在 if 表達式以前添加一個執行語句,再根據變量值進行判斷
func Score2(score int)  {
    if score := 65; score >= 90 {
        fmt.Println("A")
    } else if score > 75 {
        fmt.Println("B")
    } else {
        fmt.Println("C")
    }
}

for(循環結構)

Go 語言中的全部循環類型都可以使用for關鍵字來完成。
for循環的基本格式以下:

for 初始語句;條件表達式;結束語句{
    循環體語句
}

條件表達式返回true時循環體不停地進行循環,直到條件表達式返回false時自動退出循環。
案例:

func forDemo() {
    for i := 0; i < 10; i++ {
        fmt.Println(i)
    }
}
//for循環的初始語句能夠被忽略,可是初始語句後的分號必需要寫
func forDemo2() {
    i := 0
    for ; i < 10; i++ {
        fmt.Println(i)
    }
}
//for循環的初始語句和結束語句均可以省略
func forDemo3() {
    i := 0
    for i < 10 {
        fmt.Println(i)
        i++
    }
}

無限循環

for {
    循環體語句
}

for循環能夠經過breakgotoreturnpanic語句強制退出循環。

for range(鍵值循環)

Go語言中可使用for range遍歷數組、切片、字符串、map 及通道(channel)。 經過for range遍歷的返回值有如下規律:

  1. 數組、切片、字符串返回索引和值。
  2. map返回鍵和值。
  3. 通道(channel)只返回通道內的值。

switch case

使用switch語句可方便地對大量的值進行條件判斷。

func switchFinger(finger int) {
    switch finger {
    case 1:
        fmt.Println("大拇指")
    case 2:
        fmt.Println("食指")
    case 3:
        fmt.Println("中指")
    case 4:
        fmt.Println("無名指")
    case 5:
        fmt.Println("小拇指")
    default:
        fmt.Println("無效的輸入!")
    }
}

//一個分支能夠有多個值,多個case值中間使用英文逗號分隔
func jiou(n int) {
    switch n {
    case 1, 3, 5, 7, 9:
        fmt.Println("奇數")
    case 2, 4, 6, 8:
        fmt.Println("偶數")
    default:
        fmt.Println(n)
    }
}

//分支還可使用表達式,這時候switch語句後面不須要再跟判斷變量
func getage(age int) {
    switch {
    case age < 25:
        fmt.Println("好好學習吧")
    case age > 25 && age < 35:
        fmt.Println("好好工做吧")
    case age > 60:
        fmt.Println("好好享受吧")
    default:
        fmt.Println("活着真好")
    }
}

//fallthrough語法能夠執行知足條件的case的下一個case,是爲了兼容C語言中的case設計的。
func stringprint(s string) {
    switch {
    case s == "a":
        fmt.Println("a")
        fallthrough
    case s == "b":
        fmt.Println("b")
    case s == "c":
        fmt.Println("c")
    default:
        fmt.Println("...")
    }
}

goto(跳轉到指定標籤)

goto語句經過標籤進行代碼間的無條件跳轉。goto語句能夠在快速跳出循環、避免重複退出上有必定的幫助。Go語言中使用goto語句能簡化一些代碼的實現過程。 例如雙層嵌套的for循環要退出時:

package main

import "fmt"

func main()  {
    var breakAgain bool
    // 外循環
    for x := 0; x < 10; x++ {
        // 內循環
        for y := 0; y < 10; y++ {
            // 知足某個條件時, 退出循環
            if y == 2 {
                // 設置退出標記
                breakAgain = true
                // 退出本次循環
                break
            }
        }
        // 根據標記, 還須要退出一次循環
        if breakAgain {
            break
        }
    }
    fmt.Println("done")
}

代碼說明以下:
第 10 行,構建外循環。
第 13 行,構建內循環。
第 16 行,當 y==2 時須要退出全部的 for 循環。
第 19 行,默認狀況下循環只能一層一層退出,爲此就須要設置一個狀態變量 breakAgain,須要退出時,設置這個變量爲 true。
第 22 行,使用 break 退出當前循環,執行後,代碼調轉到第 28 行。
第 28 行,退出一層循環後,根據 breakAgain 變量判斷是否須要再次退出外層循環。
第 34 行,退出全部循環後,打印 done。

將上面的代碼使用Go語言的 goto 語句進行優化:

package main

import "fmt"

func main()  {

    for x := 0; x < 10; x++ {
        for y := 0; y < 10; y++ {
            if y == 2 {
                // 跳轉到標籤
                goto breakHere
            }
        }
    }
    // 手動返回, 避免執行進入標籤
    return
    // 標籤
breakHere:
    fmt.Println("done")
}

break(跳出循環)

break語句能夠結束for、switch和select的代碼塊。
break語句還能夠在語句後面添加標籤,表示退出某個標籤對應的代碼塊,標籤要求必須定義在對應的for、switch和 select的代碼塊上。

continue(繼續下次循環)

continue語句能夠結束當前循環,開始下一次的循環迭代過程,僅限在for循環內使用。
在 continue語句後添加標籤時,表示開始標籤對應的循環。

數組(Array)

數組是同一種數據類型元素的集合。 在Go語言中,數組從聲明時就肯定,使用時能夠修改數組成員,可是數組大小不可變化。 基本語法:

// 定義一個長度爲3元素類型爲int的數組a
var a [3]int

數組的初始化

初始化數組時可使用初始化列表來設置數組元素的值。

//數組會初始化爲int類型的零值
var testArray [3]int  
//使用指定的初始值完成初始化
var numArray = [3]int{1, 2}       
//使用指定的初始值完成初始化
var cityArray = [3]string{"北京", "上海", "深圳"}

按照上面的方法每次都要確保提供的初始值和數組長度一致,通常狀況下咱們可讓編譯器根據初始值的個數自行推斷數組的長度,例如:

var testArray [3]int
var numArray = [...]int{1, 2}
var cityArray = [...]string{"北京", "上海", "深圳"}

數組的遍歷

package main

import "fmt"

func main()  {

    var a = [...]string{"北京", "上海", "杭州","鄭州"}
    // 方法1:for循環遍歷
    for i := 0; i < len(a); i++ {
        fmt.Println(a[i])
    }

    // 方法2:for range遍歷
    for index, value := range a {
        fmt.Println(index, value)
    }
}

多維數組

Go語言是支持多維數組的,咱們這裏以二維數組爲例(數組中又嵌套數組)。

package main
import "fmt"
func main()  {
    a := [3][2]string{
        {"北京", "上海"},
        {"廣州", "深圳"},
        {"成都", "重慶"},
    }
    
    //二維數組的遍歷
    for _, v1 := range a {
        for _, v2 := range v1 {
            fmt.Printf("%s\t", v2)
        }
        fmt.Println()
    }
    
    //支持的寫法
    a := [...][2]string{
        {"北京", "上海"},
        {"廣州", "深圳"},
        {"成都", "重慶"},
    }
    //不支持多維數組的內層使用...
    b := [3][...]string{
        {"北京", "上海"},
        {"廣州", "深圳"},
        {"成都", "重慶"},
    }
}

切片(slice)

切片(Slice)是一個擁有相同類型元素的可變長度的序列。它是基於數組類型作的一層封裝。它很是靈活,支持自動擴容。

切片是一個引用類型,它的內部結構包含地址長度容量。切片通常用於快速地操做一塊數據集合。

切片的定義

聲明切片類型的基本語法以下:

var name []T

其中,
name:表示變量名
T:表示切片中的元素類型

切片的長度和容量

切片擁有本身的長度和容量,咱們能夠經過使用內置的len()函數求長度,使用內置的cap()函數求切片的容量。

基於數組定義切片

因爲切片的底層就是一個數組,因此咱們能夠基於數組定義切片。

// 基於數組定義切片
a := [5]int{55, 56, 57, 58, 59}
//基於數組a建立切片,包括元素a[1],a[2],a[3]
b := a[1:4] 

c := a[1:] //[56 57 58 59]
d := a[:4] //[55 56 57]
e := a[:]  //[55 56 57 58 59]

切片再切片

除了基於數組獲得切片,咱們還能夠經過切片來獲得切片。

package main

import "fmt"

func main()  {
    //切片再切片
    a := [...]string{"北京", "上海", "廣州", "深圳", "成都", "重慶"}
    fmt.Printf("a:%v type:%T len:%d  cap:%d\n", a, a, len(a), cap(a))
    b := a[1:3]
    fmt.Printf("b:%v type:%T len:%d  cap:%d\n", b, b, len(b), cap(b))
    c := b[1:5]
    fmt.Printf("c:%v type:%T len:%d  cap:%d\n", c, c, len(c), cap(c))
}

輸出:

a:[北京 上海 廣州 深圳 成都 重慶] type:[6]string len:6  cap:6
b:[上海 廣州] type:[]string len:2  cap:5
c:[廣州 深圳 成都 重慶] type:[]string len:4  cap:4

注意:對切片進行再切片時,索引不能超過原數組的長度,不然會出現索引越界的錯誤。

使用make()函數構造切片

動態的建立一個切片,咱們就須要使用內置的make()函數,格式以下:

make([]T, size, cap)

其中:

  • T:切片的元素類型
  • size:切片中元素的數量
  • cap:切片的容量

舉個例子:

a := make([]int, 2, 10)
fmt.Println(a)      //[0 0]
fmt.Println(len(a)) //2
fmt.Println(cap(a)) //10

上面代碼中a的內部存儲空間已經分配了10個,但實際上只用了2個。 容量並不會影響當前元素的個數,因此len(a)返回2,cap(a)則返回該切片的容量。

切片的基本操做

切片不能直接比較

切片之間是不能比較的,咱們不能使用==操做符來判斷兩個切片是否含有所有相等元素。 切片惟一合法的比較操做是和nil比較。 一個nil值的切片並無底層數組,一個nil值的切片的長度和容量都是0。可是咱們不能說一個長度和容量都是0的切片必定是nil,例以下面的示例:

var s1 []int         //len(s1)=0;cap(s1)=0;s1==nil
s2 := []int{}        //len(s2)=0;cap(s2)=0;s2!=nil
s3 := make([]int, 0) //len(s3)=0;cap(s3)=0;s3!=nil

因此要判斷一個切片是不是空的,要是用len(s) == 0來判斷,不該該使用s == nil來判斷。

切片的賦值拷貝

拷貝先後兩個變量共享底層數組,對一個切片的修改會影響另外一個切片的內容

s1 := make([]int, 3) //[0 0 0]
s2 := s1             //將s1直接賦值給s2,s1和s2共用一個底層數組
s2[0] = 100
fmt.Println(s1) //[100 0 0]
fmt.Println(s2) //[100 0 0]

切片遍歷

切片的遍歷方式和數組是一致的,支持索引遍歷和for range遍歷。

s := []int{1, 3, 5}

for i := 0; i < len(s); i++ {
    fmt.Println(i, s[i])
}

for index, value := range s {
    fmt.Println(index, value)
}

append()方法爲切片添加元素

Go語言的內建函數append()能夠爲切片動態添加元素。 每一個切片會指向一個底層數組,這個數組能容納必定數量的元素。當底層數組不能容納新增的元素時,切片就會自動按照必定的策略進行「擴容」,此時該切片指向的底層數組就會更換。「擴容」操做每每發生在append()函數調用時。 舉個例子:

func main() {
    //append()添加元素和切片擴容
    var numSlice []int
    for i := 0; i < 10; i++ {
        numSlice = append(numSlice, i)
        fmt.Printf("%v  len:%d  cap:%d  ptr:%p\n", numSlice, len(numSlice), cap(numSlice), numSlice)
    }
}
  1. append()函數將元素追加到切片的最後並返回該切片。
  2. 切片numSlice的容量按照1,2,4,8,16這樣的規則自動進行擴容,每次擴容後都是擴容前的2倍。

append()函數還支持一次性追加多個元素。 例如:

var citySlice []string
// 追加一個元素
citySlice = append(citySlice, "北京")
// 追加多個元素
citySlice = append(citySlice, "上海", "廣州", "深圳")
// 追加切片
a := []string{"成都", "重慶"}
citySlice = append(citySlice, a...)
fmt.Println(citySlice) //[北京 上海 廣州 深圳 成都 重慶]

使用copy()函數複製切片

Go語言內建的copy()函數能夠迅速地將一個切片的數據複製到另一個切片空間中,copy()函數的使用格式以下:

copy(destSlice, srcSlice []T)

其中:

  • srcSlice: 數據來源切片
  • destSlice: 目標切片

從切片中刪除元素

Go語言中並無刪除切片元素的專用方法,咱們可使用切片自己的特性來刪除元素。 代碼以下:

func main() {
    // 從切片中刪除元素
    a := []int{30, 31, 32, 33, 34, 35, 36, 37}
    // 要刪除索引爲2的元素
    a = append(a[:2], a[3:]...)
    fmt.Println(a) //[30 31 33 34 35 36 37]
}

總結一下就是:要從切片a中刪除索引爲index的元素,操做方法是a = append(a[:index], a[index+1:]...)

字典(Map)

map是一種無序的基於key-value的數據結構,Go語言中的map是引用類型,必須初始化才能使用。

map定義

map[KeyType]ValueType

其中:

  • KeyType:表示鍵的類型。
  • ValueType:表示鍵對應的值的類型。

map類型的變量默認初始值爲nil,須要使用make()函數來分配內存。語法爲:

make(map[KeyType]ValueType, [cap])

其中cap表示map的容量,該參數雖然不是必須的,可是咱們應該在初始化map的時候就爲其指定一個合適的容量。

map基本使用

scoreMap := make(map[string]int, 8)
scoreMap["張三"] = 90
scoreMap["小明"] = 100

userInfo := map[string]string{
    "username": "張三",
    "password": "123456",
}

判斷某個鍵是否存在

Go語言中有個判斷map中鍵是否存在的特殊寫法,格式以下:

value, ok := map[key]

map的遍歷

Go語言中使用for range遍歷map。

for k, v := range scoreMap {
    fmt.Println(k, v)
}

使用delete()函數刪除鍵值對

使用delete()內建函數從map中刪除一組鍵值對,delete()函數的格式以下:

delete(map, key)

其中:

  • map:表示要刪除鍵值對的map
  • key:表示要刪除的鍵值對的鍵

按照指定順序遍歷map

package main

import (
    "fmt"
    "math/rand"
    "sort"
    "time"
)

func main()  {
    rand.Seed(time.Now().UnixNano()) //初始化隨機數種子

    var scoreMap = make(map[string]int, 200)

    for i := 0; i < 100; i++ {
        key := fmt.Sprintf("stu%02d", i) //生成stu開頭的字符串
        value := rand.Intn(100)          //生成0~99的隨機整數
        scoreMap[key] = value
    }
    //取出map中的全部key存入切片keys
    var keys = make([]string, 0, 200)
    for key := range scoreMap {
        keys = append(keys, key)
    }
    //對切片進行排序
    sort.Strings(keys)
    //按照排序後的key遍歷map
    for _, key := range keys {
        fmt.Println(key, scoreMap[key])
    }
}

指針

區別於C/C++中的指針,Go語言中的指針不能進行偏移和運算,是安全指針。

要搞明白Go語言中的指針須要先知道3個概念:指針地址、指針類型和指針取值。
任何程序數據載入內存後,在內存都有他們的地址,這就是指針。而爲了保存一個數據在內存中的地址,咱們就須要指針變量。

好比,「永遠不要高估本身」這句話是個人座右銘,我想把它寫入程序中,程序一啓動這句話是要加載到內存(假設內存地址0x123456),我在程序中把這段話賦值給變量A,把內存地址賦值給變量B。這時候變量B就是一個指針變量。經過變量A和變量B都能找到個人座右銘。

Go語言中的指針不能進行偏移和運算,所以Go語言中的指針操做很是簡單,咱們只須要記住兩個符號:&(取地址)和*(根據地址取值)。

指針地址和指針類型

每一個變量在運行時都擁有一個地址,這個地址表明變量在內存中的位置。Go語言中使用&字符放在變量前面對變量進行「取地址」操做。 Go語言中的值類型(int、float、bool、string、array、struct)都有對應的指針類型,如:*int*int64*string等。
取變量指針的語法以下:

ptr := &v    // v的類型爲T

其中:

  • v:表明被取地址的變量,類型爲T
  • ptr:用於接收地址的變量,ptr的類型就爲T,稱作T的指針類型。表明指針。

指針取值

在對普通變量使用&操做符取地址後會得到這個變量的指針,而後能夠對指針使用*操做,也就是指針取值,代碼以下。

//指針取值
a := 10
// 取變量a的地址,將指針保存到b中
b := &a 
fmt.Printf("type of b:%T\n", b)
// 指針取值(根據指針去內存取值)
c := *b
fmt.Printf("type of c:%T\n", c)
fmt.Printf("value of c:%v\n", c)

總結:取地址操做符&和取值操做符*是一對互補操做符,&取出地址,*根據地址取出地址指向的值。

變量、指針地址、指針變量、取地址、取值的相互關係和特性以下:

  • 對變量進行取地址(&)操做,能夠得到這個變量的指針變量。
  • 指針變量的值是指針地址。
  • 對指針變量進行取值(*)操做,能夠得到指針變量指向的原變量的值。

函數

Go語言中支持函數、匿名函數和閉包,而且函數在Go語言中屬於「一等公民」。

函數定義

Go語言中定義函數使用func關鍵字,具體格式以下:

func 函數名(參數)(返回值){
    函數體
}
  • 函數名:由字母、數字、下劃線組成。但函數名的第一個字母不能是數字。在同一個包內,函數名也稱不能重名(包的概念詳見後文)。
  • 參數:參數由參數變量和參數變量的類型組成,多個參數之間使用,分隔。
  • 返回值:返回值由返回值變量和其變量類型組成,也能夠只寫返回值的類型,多個返回值必須用()包裹,並用,分隔。
  • 函數體:實現指定功能的代碼塊。

標準案例

package main

import "fmt"

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

func intSum2(x ...int) int {
    fmt.Println(x) //x是一個切片
    sum := 0
    for _, v := range x {
        sum = sum + v
    }
    return sum
}

func calc(x, y int) (int, int) {
    sum := x + y
    sub := x - y
    return sum, sub
}

func main(){
    ret := intSum(10, 20)
    fmt.Println(ret)
    ret1 := intSum2()
    ret2 := intSum2(10)
    ret3 := intSum2(10, 20)
    ret4 := intSum2(10, 20, 30)
    fmt.Println(ret1, ret2, ret3, ret4) //0 10 30 60
}
intSum函數有兩個參數,這兩個參數的類型均爲int,所以能夠省略x的類型,由於y後面有類型說明,x參數也是該類型。
可變參數是指函數的參數數量不固定。Go語言中的可變參數經過在參數名後加...來標識。
Go語言中函數支持多返回值,函數若是有多個返回值時必須用()將全部返回值包裹起來。

函數進階

變量做用域

全局變量是定義在函數外部的變量,它在程序整個運行週期內都有效。 在函數中能夠訪問到全局變量。
局部變量又分爲兩種: 函數內定義的變量沒法在該函數外使用,例以下面的示例代碼main函數中沒法使用testLocalVar函數中定義的變量x
for循環語句中定義的變量一樣也屬於局部變量,外部沒法使用

package main

import "fmt"

//定義全局變量num
var num int64 = 10

func testGlobalVar() {
    fmt.Printf("num=%d\n", num) //函數中能夠訪問全局變量num
}
func testLocalVar() {
    //定義一個函數局部變量x,僅在該函數內生效
    var x int64 = 100
    fmt.Printf("x=%d\n", x)
}
func main() {
    testGlobalVar() //num=10
    testLocalVar() //x=100
    //fmt.Println(x) //undefined: x
    func testLocalVar3() {
    
    for i := 0; i < 10; i++ {
        fmt.Println(i) //變量i只在當前for語句塊中生效
    }
    //fmt.Println(i) //此處沒法使用變量i
}
}

函數類型與變量

定義函數類型
咱們可使用type關鍵字來定義一個函數類型,具體格式以下:

type calculation func(int, int) int

上面語句定義了一個calculation類型,它是一種函數類型,這種函數接收兩個int類型的參數而且返回一個int類型的返回值。

簡單來講,凡是知足這個條件的函數都是calculation類型的函數,例以下面的add和sub是calculation類型。

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

func sub(x, y int) int {
    return x - y
}

add和sub都能賦值給calculation類型的變量。

var c calculation
c = add

高階函數

高階函數分爲函數做爲參數和函數做爲返回值兩部分。

package main

import (
    "errors"
    "fmt"
)

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

func do(s string) (func(int, int) int, error) {
    switch s {
    case "+":
        return add, nil
    case "-":
        return sub, nil
    default:
        err := errors.New("沒法識別的操做符")
        return nil, err
    }
}

func main() {
    ret2 := calc(10, 20, add)
    fmt.Println(ret2) //30
}

匿名函數和閉包

函數固然還能夠做爲返回值,可是在Go語言中函數內部不能再像以前那樣定義函數了,只能定義匿名函數。匿名函數就是沒有函數名的函數,匿名函數的定義格式以下:

func(參數)(返回值){
    函數體
}

匿名函數由於沒有函數名,因此沒辦法像普通函數那樣調用,因此匿名函數須要保存到某個變量或者做爲當即執行函數:

func main() {
    // 將匿名函數保存到變量
    add := func(x, y int) {
        fmt.Println(x + y)
    }
    add(10, 20) // 經過變量調用匿名函數

    //自執行函數:匿名函數定義完加()直接執行
    func(x, y int) {
        fmt.Println(x + y)
    }(10, 20)
}

閉包

閉包指的是一個函數和與其相關的引用環境組合而成的實體。簡單來講,閉包=函數+引用環境。 首先咱們來看一個例子:

func adder() func(int) int {
    var x int
    return func(y int) int {
        x += y
        return x
    }
}
func main() {
    var f = adder()
    fmt.Println(f(10)) //10
    fmt.Println(f(20)) //30
    fmt.Println(f(30)) //60

    f1 := adder()
    fmt.Println(f1(40)) //40
    fmt.Println(f1(50)) //90
}

變量f是一個函數而且它引用了其外部做用域中的x變量,此時f就是一個閉包。 在f的生命週期內,變量x也一直有效

defer語句

Go語言中的defer語句會將其後面跟隨的語句進行延遲處理。在defer歸屬的函數即將返回時,將延遲處理的語句按defer定義的逆序進行執行,也就是說,先被defer的語句最後被執行,最後被defer的語句,最早被執行。

舉個例子

func main() {
    fmt.Println("start")
    defer fmt.Println(1)
    defer fmt.Println(2)
    defer fmt.Println(3)
    fmt.Println("end")
}

panic/recover

Go語言中目前(Go1.12)是沒有異常機制,可是使用panic/recover模式來處理錯誤。panic能夠在任何地方引起,但recover只有在defer調用的函數中有效。 首先來看一個例子:

func funcA() {
    fmt.Println("func A")
}

func funcB() {
    panic("panic in B")
}

func funcC() {
    fmt.Println("func C")
}
func main() {
    funcA()
    funcB()
    funcC()
}

程序運行期間funcB中引起了panic致使程序崩潰,異常退出了。這個時候咱們就能夠經過recover將程序恢復回來,繼續日後執行。

func funcA() {
    fmt.Println("func A")
}

func funcB() {
    defer func() {
        err := recover()
        //若是程序出出現了panic錯誤,能夠經過recover恢復過來
        if err != nil {
            fmt.Println("recover in B")
        }
    }()
    panic("panic in B")
}

func funcC() {
    fmt.Println("func C")
}
func main() {
    funcA()
    funcB()
    funcC()
}

注意:

  1. recover()必須搭配defer使用。
  2. defer必定要在可能引起panic的語句以前定義。

結構體

golang中並無明確的面向對象的說法,實在要扯上的話,能夠將struct比做其它語言中的class。

結構體的定義

使用typestruct關鍵字來定義結構體,具體代碼格式以下:

type 類型名 struct {
    字段名 字段類型
    字段名 字段類型
    …
}
  • 類型名:標識自定義結構體的名稱,在同一個包內不能重複。
  • 字段名:表示結構體字段名。結構體中的字段名必須惟一。
  • 字段類型:表示結構體字段的具體類型。

舉個例子,咱們定義一個Person(人)結構體,代碼以下:

type person struct {
    name string
    city string
    age  int8
}

//或
type person struct {
    name, city string
    age        int8
}

結構體實例化

只有當結構體實例化時,纔會真正地分配內存。也就是必須實例化後才能使用結構體的字段。

結構體自己也是一種類型,咱們能夠像聲明內置類型同樣使用var關鍵字聲明結構體類型。

var 結構體實例 結構體類型

基本實例化

var p1 person
    p1.name = "zhiqiang"
    p1.city = "ZhengZhou"
    p1.age = 24
    fmt.Printf("p1=%v\n", p1)  //p1={zhiqiang ZhengZhou 24}
    fmt.Printf("p1=%#v\n", p1) //p1=main.person{name:"zhiqiang", city:"ZhengZhou", age:24}

咱們經過.來訪問結構體的字段(成員變量),例如p1.namep1.age等。

指針類型結構體

經過使用new關鍵字對結構體進行實例化,獲得的是結構體的地址。

p := new(person)
    p.name="zhiqiang"
    p.city="ZhengZhou"
    p.age=24
    fmt.Printf("p=%#v\n", p) //p=&main.person{name:"zhiqiang", city:"ZhengZhou", age:24}

取結構體的地址實例化

使用&對結構體進行取地址操做至關於對該結構體類型進行了一次new實例化操做。

p := &person{}
    fmt.Printf("p=%#v\n", p) //p=&main.person{name:"", city:"", age:0}
    p.name="zhiqiang"
    p.city="ZhengZhou"
    p.age=24
    fmt.Printf("p=%#v\n", p) //p=&main.person{name:"zhiqiang", city:"ZhengZhou", age:24}

使用鍵值對初始化

p := person{name:"zhiqiang",city:"ZhengZhou",age:24}
fmt.Printf("p=%#v\n", p) //p=main.person{name:"zhiqiang", city:"ZhengZhou", age:24}

p := &person{
    name: "zhiqiang",
}
fmt.Printf("p=%#v\n", p) //p=&main.person{name:"zhiqiang", city:"", age:0}

或
p := &person{
    name: "zhiqiang",
    city: "ZhengZhou",
    age:  24,
}
fmt.Printf("p=%#v\n", p) //p=&main.person{name:"zhiqiang", city:"ZhengZhou", age:24}

匿名結構體

臨時數據結構等場景下還可使用匿名結構體

var user struct{Name string; Age int}
    user.Name = "zhiqiang"
    user.Age = 24
    fmt.Printf("%#v\n", user)

構造函數

Go語言的結構體沒有構造函數

實現了一個person的構造函數。 由於struct是值類型,若是結構體比較複雜的話,值拷貝性能開銷會比較大,因此該構造函數返回的是結構體指針類型。

func newPerson(name, city string, age int8) *person {
    return &person{
        name: name,
        city: city,
        age:  age,
    }
}

調用構造函數

p := newPerson("zhiqiang", "ZhengZhou", 18)
fmt.Printf("%#v\n", p) //&main.person{name:"zhiqiang", city:"ZhengZhou", age:18}

方法和接收者

Go語言中的方法(Method)是一種做用於特定類型變量的函數。這種特定類型變量叫作接收者(Receiver)。接收者的概念就相似於其餘語言中的this或者 self

方法的定義格式以下:

func (接收者變量 接收者類型) 方法名(參數列表) (返回參數) {
    函數體
}

其中,

  • 接收者變量:接收者中的參數變量名在命名時,官方建議使用接收者類型名的第一個小寫字母,而不是selfthis之類的命名。例如,Person類型的接收者變量應該命名爲 pConnector類型的接收者變量應該命名爲c等。
  • 接收者類型:接收者類型和參數相似,能夠是指針類型和非指針類型。
  • 方法名、參數列表、返回參數:具體格式與函數定義相同。
package  main

import "fmt"

type person struct {
    name, city string
    age        int8
}
func newPerson(name, city string, age int8) *person {
    return &person{
        name: name,
        city: city,
        age:  age,
    }
}
func (p person) Play()  {
    fmt.Printf("%s正在玩耍!\n", p.name)

}
func (p *person) SetAge(newage int8){
    p.age = newage
}
func (p *person) GetAge(){
    fmt.Printf("%s的年齡%d歲", p.name,p.age)

}
func main()  {
    p := newPerson("zhiqiang", "ZhengZhou", 18)
    p.GetAge()
}

指針類型的接收者:指針類型的接收者由一個結構體的指針組成,因爲指針的特性,調用方法時修改接收者指針的任意成員變量,在方法結束後,修改都是有效的。這種方式就十分接近於其餘語言中面向對象中的this或者self。 例如咱們爲Person添加一個SetAge方法,來修改實例變量的年齡。

值類型的接收者:當方法做用於值類型接收者時,Go語言會在代碼運行時將接收者的值複製一份。在值類型接收者的方法中能夠獲取接收者的成員值,但修改操做只是針對副本,沒法修改接收者變量自己。

何時應該使用指針類型接收者

  1. 須要修改接收者中的值
  2. 接收者是拷貝代價比較大的大對象
  3. 保證一致性,若是有某個方法使用了指針接收者,那麼其餘的方法也應該使用指針接收者。

結構體的匿名字段

結構體容許其成員字段在聲明時沒有字段名而只有類型,這種沒有名字的字段就稱爲匿名字段。

匿名字段默認採用類型名做爲字段名,結構體要求字段名稱必須惟一,所以一個結構體中同種類型的匿名字段只能有一個。

package  main

import "fmt"
//Person 結構體Person類型
type Person struct {
    string
    int
}

func main() {
    p1 := Person{
        "github",
        18,
    }
    fmt.Printf("%#v\n", p1)        //main.Person{string:"北京", int:18}
    fmt.Println(p1.string, p1.int) //北京 18
}

嵌套結構體

package main

import "fmt"

//Address 地址結構體
type Address struct {
    Province   string
    City       string
    CreateTime string
}

//Email 郵箱結構體
type Email struct {
    Account    string
    CreateTime string
}

//User 用戶結構體
type User struct {
    Name   string
    Address
    Email
}


func main() {
    var u User
    u.Name = "zhiqiang"
    //嵌套結構體內部可能存在相同的字段名。這個時候爲了不歧義須要指定具體的內嵌結構體的字段。
    u.Address.CreateTime = "2000" //指定Address結構體中的CreateTime
    u.Email.CreateTime = "2000"   //指定Email結構體中的CreateTime
    fmt.Printf("u=%#v\n", u)
}

結構體的「繼承」

Go語言中使用結構體也能夠實現其餘編程語言中面向對象的繼承。

package main

import "fmt"

//Animal 動物
type Animal struct {
    name string
}

func (a *Animal) move() {
    fmt.Printf("%s會動!\n", a.name)
}

//Dog 狗
type Dog struct {
    Feet    int8
    *Animal //經過嵌套匿名結構體實現繼承
}

func (d *Dog) wang() {
    fmt.Printf("%s會汪汪汪~\n", d.name)
}

func main() {
    d1 := &Dog{
        Feet: 4,
        Animal: &Animal{ //注意嵌套的是結構體指針
            name: "樂樂",
        },
    }
    d1.wang() //樂樂會汪汪汪~
    d1.move() //樂樂會動!
}

結構體字段的可見性

結構體中字段大寫開頭表示可公開訪問,小寫表示私有(僅在定義當前結構體的包中可訪問)。

結構體標籤(Tag)

Tag是結構體的元信息,能夠在運行的時候經過反射的機制讀取出來。 Tag在結構體字段的後方定義,由一對反引號包裹起來,具體的格式以下:

`key1:"value1" key2:"value2"`

結構體標籤由一個或多個鍵值對組成。鍵與值使用冒號分隔,值用雙引號括起來。鍵值對之間使用一個空格分隔。 注意事項: 爲結構體編寫Tag時,必須嚴格遵照鍵值對的規則。結構體標籤的解析代碼的容錯能力不好,一旦格式寫錯,編譯和運行時都不會提示任何錯誤,經過反射也沒法正確取值。例如不要在key和value之間添加空格。

例如gorm模型定義:

type User struct {
    gorm.Model
    Birthday     time.Time
    Age          int
    Name         string  `gorm:"size:255"`       // string默認長度爲255, 使用這種tag重設。
    Num          int     `gorm:"AUTO_INCREMENT"` // 自增

    CreditCard        CreditCard      // One-To-One (擁有一個 - CreditCard表的UserID做外鍵)
    Emails            []Email         // One-To-Many (擁有多個 - Email表的UserID做外鍵)

    BillingAddress    Address         // One-To-One (屬於 - 本表的BillingAddressID做外鍵)
    BillingAddressID  sql.NullInt64

    ShippingAddress   Address         // One-To-One (屬於 - 本表的ShippingAddressID做外鍵)
    ShippingAddressID int

    IgnoreMe          int `gorm:"-"`   // 忽略這個字段
    Languages         []Language `gorm:"many2many:user_languages;"` // Many-To-Many , 'user_languages'是鏈接表
}

接口( interface )

是對其餘類型行爲的歸納和抽象。

interface是一組method的集合,是duck-type programming的一種體現。接口作的事情就像是定義一個協議(規則)

接口的定義

每一個接口由數個方法組成,接口的定義格式以下:

type 接口類型名 interface{
    方法名1( 參數列表1 ) 返回值列表1
    方法名2( 參數列表2 ) 返回值列表2
    …
}

其中:

  • 接口名:使用type將接口定義爲自定義的類型名。Go語言的接口在命名時,通常會在單詞後面添加er,若有寫操做的接口叫Writer,有字符串功能的接口叫Stringer等。接口名最好要能突出該接口的類型含義。
  • 方法名:當方法名首字母是大寫且這個接口類型名首字母也是大寫時,這個方法能夠被接口所在的包(package)以外的代碼訪問。
  • 參數列表、返回值列表:參數列表和返回值列表中的參數變量名能夠省略。

接口實現案例

一個對象只要所有實現了接口中的方法,那麼就實現了這個接口。換句話說,接口就是一個須要實現的方法列表

值接收者方式的dog

指針接收者實現接口的cat

package main

import "fmt"

// Sayer 接口
type Sayer interface {
    say()
}
type dog struct {}

func (d *dog) say() {
    fmt.Println("狗會動")
}
type cat struct {}
// cat實現了Sayer接口
func (c cat) say() {
    fmt.Println("喵喵喵")
}
func main() {
    var x Sayer // 聲明一個Sayer類型的變量x
    a := cat{}  // 實例化一個cat
    b := dog{}  // 實例化一個dog
    x = a       // 能夠把cat實例直接賦值給x
    x.say()     // 喵喵喵
    x = &b       // 能夠把dog實例指針直接賦值給x
    x.say()     // 汪汪汪
}

接口嵌套

接口與接口間能夠經過嵌套創造出新的接口。

// Sayer 接口
type Sayer interface {
    say()
}

// Mover 接口
type Mover interface {
    move()
}

// 接口嵌套
type animal interface {
    Sayer
    Mover
}

空接口

空接口的定義

空接口是指沒有定義任何方法的接口。所以任何類型都實現了空接口。

空接口類型的變量能夠存儲任意類型的變量。

// 定義一個空接口x
    var x interface{}

空接口的應用

使用空接口實現能夠接收任意類型的函數參數。

使用空接口實現能夠保存任意值的字典。

// 空接口做爲函數參數
func show(a interface{}) {
    fmt.Printf("type:%T value:%v\n", a, a)
}

// 空接口做爲map值
var studentInfo = make(map[string]interface{})

接口值

一個接口的值(簡稱接口值)是由一個具體類型具體類型的值兩部分組成的。這兩部分分別稱爲接口的動態類型動態值

var w io.Writer
w = os.Stdout
w = new(bytes.Buffer)
w = nil

想要判斷空接口中的值這個時候就可使用類型斷言,其語法格式:

x.(T)

其中:

  • x:表示類型爲interface{}的變量
  • T:表示斷言x多是的類型。

該語法返回兩個參數,第一個參數是x轉化爲T類型後的變量,第二個值是一個布爾值,若爲true則表示斷言成功,爲false則表示斷言失敗。

var x interface{}
    x = "Hello Golang"
    v, ok := x.(string)
    if ok {
        fmt.Println(v)
    } else {
        fmt.Println("類型斷言失敗")
    }

關於接口須要注意的是,只有當有兩個或兩個以上的具體類型必須以相同的方式進行處理時才須要定義接口。不要爲了接口而寫接口,那樣只會增長沒必要要的抽象,致使沒必要要的運行時損耗。

反射

類型太多,然而類型斷言又猜不全,使用反射可以直接拿到接口的動態類型和動態值

反射的應用

web框架,配置文件解析,orm框架

雙刃劍

反射是一個強大並富有表現力的工具,能讓咱們寫出更靈活的代碼。可是反射不該該被濫用,緣由有如下三個。

  1. 基於反射的代碼是極其脆弱的,反射中的類型錯誤會在真正運行的時候纔會引起panic,那極可能是在代碼寫完的很長時間以後。
  2. 大量使用反射的代碼一般難以理解。
  3. 反射的性能低下,基於反射實現的代碼一般比正常代碼運行速度慢一到兩個數量級。

reflect包

在Go語言的反射機制中,任何接口值都由是一個具體類型具體類型的值兩部分組成的(咱們在上一篇接口的博客中有介紹相關概念)。 在Go語言中反射的相關功能由內置的reflect包提供,任意接口值在反射中均可以理解爲由reflect.Typereflect.Value兩部分組成,而且reflect包提供了reflect.TypeOfreflect.ValueOf兩個函數來獲取任意對象的Value和Type。

案例說明

package main

import (
    "fmt"
    "reflect"
)

func reflectType(x interface{}) {
    t := reflect.TypeOf(x)
    fmt.Printf("name:%v kid:%v\n", t.Name(),t.Kind())
}
func reflectValue(x interface{}) {
    v := reflect.ValueOf(x)
    k := v.Kind()
    switch k {
    case reflect.Int64:
        // v.Int()從反射中獲取整型的原始值,而後經過int64()強制類型轉換
        fmt.Printf("type is int64, value is %d\n", int64(v.Int()))
    case reflect.Float32:
        // v.Float()從反射中獲取浮點型的原始值,而後經過float32()強制類型轉換
        fmt.Printf("type is float32, value is %f\n", float32(v.Float()))
    case reflect.Float64:
        // v.Float()從反射中獲取浮點型的原始值,而後經過float64()強制類型轉換
        fmt.Printf("type is float64, value is %f\n", float64(v.Float()))
    }
}

func reflectSetValue1(x interface{}) {
    v := reflect.ValueOf(x)
    if v.Kind() == reflect.Int64 {
        v.SetInt(200) //修改的是副本,reflect包會引起panic
    }
}

func reflectSetValue2(x interface{}) {
    v := reflect.ValueOf(x)
    // 反射中使用 Elem()方法獲取指針對應的值
    if v.Elem().Kind() == reflect.Int64 {
        v.Elem().SetInt(200)
    }
}

func main() {
    var a float32 = 3.14
    reflectType(a) // name:float32 kid:float32
    reflectValue(a) //type is float32, value is 3.140000

    var b int64 = 100
    reflectType(b) // name:int64 kid:int64

    c := reflect.ValueOf(b)
    fmt.Printf("type c :%T\n", c) // type c :reflect.Value

    type book struct{ title string }
    var e =book{title:"Golang"}
    reflectType(e) //name:book kid:struct


    var f int64 = 100
    // reflectSetValue1(a) //panic: reflect: reflect.Value.SetInt using unaddressable value
    reflectSetValue2(&f)
    fmt.Println(f) //200

    // *int類型空指針
    var g *int
    fmt.Println("var a *int IsNil:", reflect.ValueOf(g).IsNil()) //var a *int IsNil: true

    // nil值
    fmt.Println("nil IsValid:", reflect.ValueOf(nil).IsValid()) //nil IsValid: false

    // 實例化一個匿名結構體
    // 嘗試從結構體中查找"abc"字段
    fmt.Println("不存在的結構體成員:", reflect.ValueOf(struct {}{}).FieldByName("abc").IsValid()) //不存在的結構體成員: false

    // 嘗試從結構體中查找"abc"方法
    fmt.Println("不存在的結構體方法:", reflect.ValueOf(b).MethodByName("abc").IsValid()) //不存在的結構體方法: false

    // 嘗試從map中查找一個不存在的鍵
    fmt.Println("map中不存在的鍵:", reflect.ValueOf(map[string]int64{}).MapIndex(reflect.ValueOf("hehe")).IsValid()) //map中不存在的鍵: false

}

Go語言的反射中像數組、切片、Map、指針等類型的變量,它們的.Name()都是返回

reflect.ValueOf()返回的是reflect.Value類型,其中包含了原始值的值信息。reflect.Value與原始值之間能夠互相轉換。

reflect.Value類型提供的獲取原始值的方法以下:

方法 說明
Interface() interface {} 將值以 interface{} 類型返回,能夠經過類型斷言轉換爲指定類型
Int() int64 將值以 int 類型返回,全部有符號整型都可以此方式返回
Uint() uint64 將值以 uint 類型返回,全部無符號整型都可以此方式返回
Float() float64 將值以雙精度(float64)類型返回,全部浮點數(float3二、float64)都可以此方式返回
Bool() bool 將值以 bool 類型返回
Bytes() []bytes 將值以字節數組 []bytes 類型返回
String() string 將值以字符串類型返回

IsNil()報告v持有的值是否爲nil。v持有的值的分類必須是通道、函數、接口、映射、指針、切片之一;不然IsNil函數會致使panic。

IsValid()返回v是否持有一個值。若是v是Value零值會返回假,此時v除了IsValid、String、Kind以外的方法都會致使panic。

結構體反射

任意值經過reflect.TypeOf()得到反射對象信息後,若是它的類型是結構體,能夠經過反射值對象(reflect.Type)的NumField()Field()方法得到結構體成員的詳細信息。

reflect.Type中與獲取結構體成員相關的的方法以下表所示。

方法 說明
Field(i int) StructField 根據索引,返回索引對應的結構體字段的信息。
NumField() int 返回結構體成員字段數量。
FieldByName(name string) (StructField, bool) 根據給定字符串返回字符串對應的結構體字段的信息。
FieldByIndex(index []int) StructField 多層成員訪問時,根據 []int 提供的每一個結構體的字段索引,返回字段的信息。
FieldByNameFunc(match func(string) bool) (StructField,bool) 根據傳入的匹配函數匹配須要的字段。
NumMethod() int 返回該類型的方法集中方法的數目
Method(int) Method 返回該類型方法集中的第i個方法
MethodByName(string)(Method, bool) 根據方法名返回該類型方法集中的方法

結構體反射示例

package main

import (
    "fmt"
    "reflect"
)

type student struct {
    Name  string `json:"name"`
    Score int    `json:"score"`
}
// 給student添加兩個方法 Study和Sleep(注意首字母大寫)
func (s student) Study() string {
    msg := "好好學習,每天向上。"
    fmt.Println(msg)
    return msg
}

func (s student) Sleep() string {
    msg := "好好睡覺,快快長大。"
    fmt.Println(msg)
    return msg
}
func main() {
    stu := student{
        Name:  "張三",
        Score: 90,
    }

    t := reflect.TypeOf(stu)

    v := reflect.ValueOf(stu)



    fmt.Println(t.Name(), t.Kind()) // student struct
    // 經過for循環遍歷結構體的全部字段信息
    for i := 0; i < t.NumField(); i++ {
        field := t.Field(i)
        fmt.Printf("name:%s index:%d type:%v json tag:%v\n", field.Name, field.Index, field.Type, field.Tag.Get("json"))
    }

    // 經過字段名獲取指定結構體字段信息
    if scoreField, ok := t.FieldByName("Score"); ok {
        fmt.Printf("name:%s index:%d type:%v json tag:%v\n", scoreField.Name, scoreField.Index, scoreField.Type, scoreField.Tag.Get("json"))
    }


    fmt.Println(t.NumMethod())
    for i := 0; i < v.NumMethod(); i++ {
        methodType := v.Method(i).Type()
        fmt.Printf("method name:%s\n", t.Method(i).Name)
        fmt.Printf("method:%s\n", methodType)
        // 經過反射調用方法傳遞的參數必須是 []reflect.Value 類型
        var args = []reflect.Value{}
        v.Method(i).Call(args)
    }
}
相關文章
相關標籤/搜索