go 學習筆記

這篇文章就總結一下go 的細節
具體參考:
http://c.biancheng.net/golang/
但是里面有寫 收費章節(其實他也是抄的別的,暫時沒找到源頭)

這個是 gitbook 的文檔,但是好像有點歲月的痕跡
https://wizardforcel.gitbooks.io/gopl-zh/preface.html
這也是gitbook 文檔,稍微美觀一點的
https://learnku.com/docs/the-way-to-go/an-actual-example-of-128-using-the-interface-fmtfprintf/3668
簡書 Go接口類型的使用
go 中文網 接口的作用
github go 做web

這個就比較全, awesome go 文檔

Go 語言問題集(Go Questions)

  • 數組聲明
var 數組變量名 [元素數量]Type

數組的元素數量,可以是一個表達式,但最終通過編譯期計算的結果必須是整型數值,元素數量不能含有到運行時才能確認大小的數值

func get_array_length() int {
        return 3
}

func main() {
    length := get_array_length()  
    fmt.Println(length)  
    var a [len("sffddd")] int        // 定義三個整數的數組   但是內置
函數 比如 len 就不會報錯
    // var a [get_array_length()]int   // 數組長度必須在編譯的時候
就搞定,否
則編譯報錯   這個就包錯:non-constant array bound length  
    // 現在算是理解了一點,解釋語言和編譯語言的區別,編譯語
言關于內存
分配的初始化都要,在編譯的時候就要搞定, 
    b := make([]int, get_array_length())   // 這種作為函數參數的
還是可以 使用
    fmt.Printf("%v\n", b)
    fmt.Println(a[0])        // 打印第一個元素
    fmt.Println(a[len(a)-1]) // 打印最后一個元素
}

現在算是理解了一點,解釋語言和編譯語言的區別,編譯語言關于內存分配的初始化都要在編譯的時候就要搞定,不能間接間接計算獲得(除了一些內置函數,如上面的len) 這個以后要多練習,才能理解了

  • go 多維數組

記住,go 數組是 值類型, 多維數組也是會按照 類型的初始值來填充的

var array_name [size1][size2]...[sizen] array_type

從前到后的 size 分別是外層到內層的

// 聲明一個二維整型數組,兩個維度的長度分別是 4 和 2
    var array [4][2]int
    array = [4][2]int{{10, 11}, {20, 21}, {30, 31}, {40, 41}}
    // 寫了索引就按照索引賦值,默認是按照順序的
    array = [4][2]int{1: {20, 21}, {40, 41}}   
// [[0 0] [20 21] [40 41] [0 0]]
    array = [4][2]int{1: { 20, 21}, 3:{ 40, 41} }  
// [[0 0] [20 21] [0 0] [40 41]]

如果指定了索引,初始化,后面不指定索引的話,就按照前面的索引+1 來賦值了

  • 切片(slice)

是對數組的一個連續片段的引用,所以切片是一個引用類型

a := [3] int {1,2,3}
b:= a[:]
a[0] = 10
fmt.Println(b)  // [10 2 3]
數組直接切片就是返回的就是切片類型, 他是原來數組的一個引
用,如果原來數組改變,切片的值也是跟著變的

而且切片是引用類型,索引當做函數參數,在函數內部的改變也是會影響外面的值的。比如下面:

var a = [5] int {0,1,2,3,4}
b := a[2:3]
b = append(b, 33)
fmt.Println(cap(b), a)
cap(b) 結果是 3  a 的結果是  [0 1 2 33 4]

為啥上面的 cap(b) 而不是 1, 因為如果是用數組進行切片的,cap 默認是當前切片索引的第一個數字到數組結束的位置,所以進行 append 之后, a 的第四數字 3 被 33覆蓋,如果,append(...) 導致 b cap 超出,他會切換地址,就不會影響 a 的值了,有意思。

一般 [ num ] 或者 [...] 這種 有長度的是數組 ,而沒有長度的是 切片
切片可以從數組或切片生成新的切片, 他們都是原來對象的引用

切片復位

a := []int{1, 2, 3}
slice[0:0]  // 兩者同時為 0 時,等效于空切片

切片分配地址
聲明一個切片的時候:
a := [] int // 此時a 為 nil 0x0

a := [] int {} // 此時 a 為 [] 但不是 nil 已經分配了地址
a := make([] int , length, cap) // a 為 [ ] 也已經分配了地址
使用 make 的好處是 可以指定 切片初始 的大小和容量

(使用make 一定分配了內存,但是直接 [a:b] 只是把切片指向了已經存在的內存區域, 而沒有重新分配內存)

  • 切片 append()
    append 給切片動態的添加元素
    尾部追加
var a []int
a = append(a, 1) // 追加1個元素
a = append(a, 1, 2, 3) // 追加多個元素, 手寫解包方式
a = append(a, []int{1,2,3}...) // 追加一個切片, 切片需要解包

開頭添加(一般導致內存重新分配,已有元素復制一份)

var a = []int{1,2,3}
a = append([]int{0}, a...) // 在開頭添加1個元素
a = append([]int{-3,-2,-1}, a...) // 在開頭添加1個切片

append 原地址空間不足就會擴容 和 py 類似都是 二倍空間 遞增

var numbers []int
for i := 0; i < 10; i++ {
    numbers = append(numbers, i)
    fmt.Printf("len: %d  cap: %d pointer: %p\n", len(numbers), 
cap(numbers), numbers)
}

> len: 1  cap: 1 pointer: 0xc00004a080
len: 2  cap: 2 pointer: 0xc00004a0c0
len: 3  cap: 4 pointer: 0xc0000480e0
len: 4  cap: 4 pointer: 0xc000040e0
len: 5  cap: 8 pointer: 0xc000078080
len: 6  cap: 8 pointer: 0xc000078080
len: 7  cap: 8 pointer: 0xc000078080
len: 8  cap: 8 pointer: 0xc000078080
len: 9  cap: 16 pointer: 0xc00007a080
len: 10  cap: 16 pointer: 0xc00007a080

append 鏈式調用

var a []int
a = append(a[:i], append([]int{x}, a[i:]...)...) // 在第i個位置插入x
a = append(a[:i], append([]int{1,2,3}, a[i:]...)...) // 在第i個位置插入切片

每個添加操作中的第二個 append 調用都會創建一個臨時切片,并將 a[i:] 的內容復制到新創建的切片中,然后將臨時創建的切片再追加到 a[:i] 中

切片擴容之后,會更換內存地址,這樣就和原來的沒關系了:

a := [3] int {1,2,3}
b:= a[:]  
b = append(b, 5,6,7)
b[0] = 10  // a  [1 2 3]  b [10 2 3 5 6 7]

所以擴容更換地址,就不是引用原來對象了,而是直接復制了一個 新的。

a := make([] int, 3, 5)
a = []int{1,2,3}    //注意,這里  cap(a)=3并且和一開始的make分配的地址不同了,
因為這里直接把a指向了另一個切片地址了
b := append(a, 4)
a[0] = 10
fmt.Printf("b 是 %v, a 是 %v \n", b, a) 
fmt.Printf("b 地址是 %p, a 地址是 %p \n", b, a)
>>
b 是 [1 2 3 4], a 是 [10 2 3] 
b 地址是 0xc000070060, a 地址是 0xc0000480c0

(發現 新變量接受append 返回值,都是新的地址(舊變量不擴容,就還是原來的地址)) 這個結論是錯的哦
無論用那個變量接受append,地址都是取決于有沒有擴容 沒有的話還是原來的地址。

  • copy() 復制切片

copy( destSlice, srcSlice []T) int
// 將 srcSlice 復制到 destSlice

copy 函數直接操作 原切片 并返回替換的元素個數

slice1 := []int{1, 2, 3, 4, 5}
slice2 := []int{5, 4, 3}
copy(slice2, slice1) // 只會復制slice1的前3個元素到slice2中
> slice2  //  [1,2,3]
copy(slice1, slice2) // 只會復制slice2的3個元素到slice1的前3個位置
> slice1 // [1 2 3 4 5]

append 返回新的切片 取決于地址是不是改變(是不是擴容了), copy 直接在目的切片直接修改, 復制 第二個參數切片。(返回替換的元素數量)

  • Go語言從切片中刪除元素

這個比較有意思
開頭刪除
普通做法(移動數據指針)

a = []int{1, 2, 3}
a = a[1:] // 刪除開頭1個元素
a = a[N:] // 刪除開頭N個元素

append實現 (后面的數據向開頭移動, 不會導致內存空間結構的變化)

a = []int{1, 2, 3}
a = append(a[:0], a[1:]...) // 刪除開頭1個元素
a = append(a[:0], a[N:]...) // 刪除開頭N個元素

copy 實現

a = []int{1, 2, 3}
a = a[:copy(a, a[1:])] // 刪除開頭1個元素
a = a[:copy(a, a[N:])] // 刪除開頭N個元素

中間刪除(對剩余的元素進行一次整體挪動)

a = []int{1, 2, 3, ...}
a = append(a[:i], a[i+1:]...) // 刪除中間1個元素
a = append(a[:i], a[i+N:]...) // 刪除中間N個元素
a = a[:i+copy(a[i:], a[i+1:])] // 刪除中間1個元素
a = a[:i+copy(a[i:], a[i+N:])] // 刪除中間N個元素

尾部刪除

a = []int{1, 2, 3}
a = a[:len(a)-1] // 刪除尾部1個元素
a = a[:len(a)-N] // 刪除尾部N個元素

Go語言中刪除切片元素的本質是,以被刪除元素為分界點,將前后兩個部分的內存重新連接起來

  • map

map 是引用類型:

var mapname map[keytype]valuetype

[keytype] 和 valuetype 之間允許有空格
使用和 slice 差不多
聲明 可以直接初始化,如果只是聲明,那就要用make 來分配地址

map 容量:

make(map[keytype]valuetype, cap)

如果提前知道map 大小,最好寫一下 cap, 優化性能

map 遍歷:
使用 for range 即可
只使用 值

for _, v := range scene {}

只是用 key

for k := range scene { }

同時 key 和值

for k, v := range scene { }

map元素的刪除和清空
刪除某個 key

delete(map, 鍵)

清空 map
沒有清空map 的函數,還是重新 make分配內存吧

不用擔心垃圾回收的效率,Go語言中的并行垃圾回收效率比寫一個清空函數要高效的多
sync.Map
在并發環境中使用的map
pass

  • list(列表)

列表使用 container/list 包來實現,內部的實現原理是雙鏈表,列表能夠高效地進行任意位置的元素插入和刪除操作
這個以前沒看過,需要看一下吧
這個是要是 list 包的使用
列表與切片和 map 不同的是,列表并沒有具體元素類型的限制,因此,列表的元素可以是任意類型
初始化列表:

變量名 := list.New()
或:
var 變量名 list.List

大概使用 參考:
http://c.biancheng.net/view/35.html

  • nil:空值/零值

指針、切片、映射、通道、函數和接口的零值則是 nil
數類型為 0
字符串為 ""
布爾為 false

nil 不是關鍵字或保留字, 你可以 var nil = xx(但是不規范)

兩個為 nil 的值不能使用 == 比較, 只能單獨和 nil 比較

nil 大小不同,默認是當前類型的大小

  • go 流程控制

pass

  • go 函數

挑幾個重點講一下吧

1、go 閉包
函數 + 引用環境 = 閉包
閉包就和 py 一樣的道理,直接返回的函數,可以引用到他上級的東西。
閉包具有記憶性,他可以記得外面的東西(會跟隨閉包生命期一直存在)

func Accumulate(value int) func() int {
    // 返回一個閉包
    return func() int {
        // 累加
        value++
        // 返回一個累加值
        return value
    }
}

func main() {
    accumulator := Accumulate(1)
    // 累加1并打印
    fmt.Println(accumulator())  // 2
    fmt.Println(accumulator())  // 3
    // 打印累加器的函數地址
    fmt.Printf("%p\n", &accumulator)  // 0xc000072018 這里用 &取閉包實例變量的地址 
    // 創建一個累加器, 初始值為1
    accumulator2 := Accumulate(10)  // 11
    // 累加1并打印
    fmt.Println(accumulator2())
    // 打印累加器的函數地址
    fmt.Printf("%p\n", &accumulator2) //0xc000072028

    // (不加&的話是去閉包函數的地址,所以下面結果是一樣的)
    fmt.Printf("%p\n%p\n", accumulator, accumulator2)  // 0x498030  0x490830
}

2、可變參數
這 py 可變參數一樣的,只不過 go * 表示指針,所以用 ... 來表示 可變參數
同樣 args ...type 也必是函數最后一個參數才可以,是一個語法糖,在函數內部args 已經封裝為 一個 切片了(py 里面是 封裝成元祖)

聲明參數:

args ...type

type 表示 args 的參數只能是 type 類型的。如果你想支持任意類型,請把type 改為 interface{}

args ...interface{}  // 這樣 args  就可以是任意類型了 

interface{} 傳遞任意類型數據是Go語言的慣例用法
舉個例子:

func MyPrintf(args ...interface{}) {
    for _, arg := range args {
        switch arg.(type) {  // 這里用 Go 類型斷言 .(type) 取出類型
            case int:
                fmt.Println(arg, "is an int value.")
            case string:
                fmt.Println(arg, "is a string value.")
            case int64:
                fmt.Println(arg, "is an int64 value.")
            default:
                fmt.Println(arg, "is an unknown type.")
        }
    }
}
func main(){
    var v1 int = 1
    var v2 int64 = 234
    var v3 string = "hello"
    var v4 float32 = 1.234
    MyPrintf(v1, v2, v3, v4)

}

上面用到了,Go 類型斷言 參考:
https://studygolang.com/articles/12355

3、傳遞可變參數
就是和py 也類似的, 這里使用 args ... 解包
比如:

append(a, []int{1,2,3}...) // 追加一個切片, 切片需要解包
等價與
append(a, 1,2,3)

4、Go語言defer(延遲執行語句)

多個 defer 行為被注冊時,它們會以逆序執行(類似棧,即后進先出)
也就是說,先被 defer 的語句最后被執行,最后被 defer 的語句,最先被執行
類似 finally 語句

func main() {
    fmt.Println("defer begin")
    // 將defer放入延遲調用棧
    defer fmt.Println(1)
    defer fmt.Println(2)
    // 最后一個放入, 位于棧頂, 最先調用
    defer fmt.Println(3)
    fmt.Println("defer end")
}
>> 
defer begin
defer end
3
2
1

注意 defer return 和返回值的順序問題。
如果是 沒返回值 名的聲明,那么 defer 不會改變 return 的值(指針除外),有返回值名,那就可能會改變 return 的值
return xx > defer xxx >函數結束

命名返回值變量, defer 會修改他的值
匿名返回值變量, defer 不會修改他的值

5、遞歸
斐波那契數列

package main
import "fmt"
func main() {
    result := 0
    for i := 1; i <= 10; i++ {
        result = fibonacci(i)
        fmt.Printf("fibonacci(%d) is: %d\n", i, result)
    }
}
func fibonacci(n int) (res int) {   // 指定返回的變量名,可以不用return了
    if n <= 2 {
        res = 1
    } else {
        res = fibonacci(n-1) + fibonacci(n-2)
    }
    return   // 返回值已經制定了,所以這里直接return ,后面不需要寫參數了,當然寫也沒問題
}

6、Go語言處理運行時錯誤
go 語言中錯誤處理思想在于,對于可能出現錯誤的函數,需要在返回值加一個 錯誤的error 類型,如果是成功的 error 值為 nil, 失敗則返回具體的錯誤信息。(當然也可以,用defer 等關鍵字間接在函數內部實現錯誤處理,但是這種會增加程序復雜度)
自定義一個錯誤
使用 errors 包來聲明一個錯誤類型(這是最基本的錯誤字符串)

var err = errors.New("this is an error")

簡單除零錯誤

import (
    "errors"
    "fmt"
)
// 定義除數為0的錯誤
var errDivisionByZero = errors.New("傻逼錯誤,666")
func div(dividend, divisor int) (int, error) {
    // 判斷除數為0的情況并返回
    if divisor == 0 {
        return 0, errDivisionByZero
    }
    // 正常計算,返回空錯誤
    return dividend / divisor, nil
}
func main() {
    fmt.Println(div(1, 0))
}

>>
0 傻逼錯誤,666

自定義錯誤類型:
(這個同 py Exception 定義自己的錯誤類型)

package main
import (
    "fmt"
)
// 聲明一個解析錯誤
type ParseError struct {
    Filename string // 文件名
    Line     int    // 行號
}
// 實現error接口,返回錯誤描述
func (e *ParseError) Error() string {
    return fmt.Sprintf("%s:%d", e.Filename, e.Line)
}
// 創建一些解析錯誤
func newParseError(filename string, line int) error {
    return &ParseError{filename, line}
}
func main() {
    var e error
    // 創建一個錯誤實例,包含文件名和行號
    e = newParseError("main.go", 1)
    // 通過error接口查看錯誤描述
    fmt.Println(e.Error())
    // 根據錯誤接口具體的類型,獲取詳細錯誤信息
    switch detail := e.(type) {
    case *ParseError: // 這是一個解析錯誤
        fmt.Printf("Filename: %s Line: %d\n", detail.Filename, detail.Line)
    default: // 其他類型的錯誤
        fmt.Println("other error")
    }
}

有時候程序需要打印出異常堆棧
https://github.com/pkg/errors
http://www.lxweimin.com/p/1fd51b1706e2
使用 github.com/pkg/errors 給 error 變量添加堆棧,然后打印,如下:

if err := recover(); err != nil{
            fmt.Printf("%+v", errors.WithStack(err.(error)))   // %+v 為輸出堆棧格式, 這里 err  為 接口類型,所以轉為 WithStack 需要的 error 類型,
// 如果主動 拋出panic 錯誤,那么請 這樣 panic(errors.New("some desc"))  而不是 panic("some desc")   (ps: 這種panic  recover 捕獲 是字符串 接口類型了,err.(error) 會報錯)
}
>> 
runtime error: invalid memory address or nil pointer dereference
check_server/user.Register.func1
    D:/bbk/無人機項目/user/register.go:146
...
    D:/bbk/無人機項目/user/register.go:161
github.com/gin-gonic/gin.(*Context).Next

這個暫時不太懂啊
7、Go語言宕機(panic)
(就是類似 py raise )
最簡單的使用:

package main
func main() {
    panic("主動泡出錯誤")
}
>>
panic: 主動拋出錯誤
routine 1 [running]:
main.panic_error(...)...

panic 內置函數的定義:

func panic(v interface{})    // panic() 的參數可以是任意類型的。

所以 panic 參數不止是字符串
使用 數組參數:

panic([3]int {1,2,3} )
>>panic: ([3]int) (0x48d460,0xc0000480c0)

上面說了defer 語句的作用,就是為了做一些 延遲處理,這里配上 panic 會非常好用

package main
import "fmt"
func main() {
    defer fmt.Println("宕機后要做的事情1")
    defer fmt.Println("宕機后要做的事情2")
    panic("宕機")
}
>>
宕機后要做的事情2
宕機后要做的事情1
panic: 宕機

當執行 panic 的時候,后面的語句就不會執行了,但是panic 之前的語句會被執行,如果需要延遲處理的使用 defer 即可(其實感覺和正常執行差不多,無非 defer 順序是先進后出的)

8、宕機恢復(recover)
recover 就是類似 py try :except 結構
讓程序在出現異常的時候,能夠繼續執行

type panicContext struct {
    function string // 所在函數
}
// 保護方式允許一個函數
func ProtectRun(entry func()) {
    // 延遲處理的函數
    defer func() {
        // 發生宕機時,獲取panic傳遞的上下文并打印
        err := recover()
        switch err.(type) {
        case runtime.Error: // 運行時錯誤
            fmt.Println("runtime error:", err)
        case nil:
            fmt.Println("meiYOUCUOWU")
        default: // 非運行時錯誤
            fmt.Println("error:", err)
        }
    }()
    entry()
}
func main() {
    
    ProtectRun(func(){
        fmt.Println("沒有異常出現  ")
    })
}
>>
沒有異常出現  
meiYOUCUOWU
  • go 語言結構體

這里簡單說一下吧,因為以前看過很多里
結構體的字段名不能重復的
結構體定義的是一個內存結構,只有實例化才能分配內存
1、實例化有兩種形式:
直接初始化:

var ins T  ( 此時 ins 已經有了 地址,只不過他里面的 屬性都是零值, 數值類型零值一般不是 nil ) 
ins.xx = xx  // 屬性賦值 

先分配內存:
new() 或 & 即可 返回的是 結構體指針類型

ins := &T{} // 對結構體進行&取地址操作時,視為類型進行一次 new 的實例化操對該作
ins := new(T)

ins 現在為 *T 類型
結構體可以不用new 來初始化,那就是普通值類型,new 之后是為引用類型了
2、初始化結構體的成員變量
使用“鍵值對”初始化結構體
鍵值對就是 {key:value}, 忽略的字段自動使用 類型零 值

(結構體成員中只能包含該結構體的指針類型,包含非指針類型會引起編譯錯誤。)
直接忽略鍵

ins := 結構體類型名{
    字段1的值,
    字段2的值,
    …
}

這種形式需要注意,必須初始化所有值,位置也要對應好。

  • Go語言構造函數

1、這個構造函數就是自己寫的一個初始化函數罷了;

type Cat struct {
    Color string
    Name  string
}
func NewCatByName(name string) *Cat {
    return &Cat{
        Name: name,
    }
}
func NewCatByColor(color string) *Cat {
    return &Cat{
        Color: color,
    }
}

上面定義了兩個函數,對 貓對象進行初始化。
2、 類似繼承

type Cat struct {
    Color string
    Name  string
}
type BlackCat struct {
    Cat  // 嵌入Cat, 類似于派生  定義 BlackCat 結構,并嵌入了 Cat 結構
// 體,BlackCat 擁有 Cat 的所有成員,實例化后可以自由訪問 Cat 的所有成員。
}
// “構造基類”
func NewCat(name string) *Cat {
    return &Cat{
        Name: name,
    }
}
// “構造子類”
func NewBlackCat(color string) *BlackCat {
    cat := &BlackCat{}  // 實例化 BlackCat 結構,此時 Cat 也同時被實例化
    cat.Color = color
    return cat
}

// Cat 結構體類似于面向對象中的“基類”

  • 類型內嵌和結構體內嵌

結構體的字段有些事可以省略 字段名的,只有類型

type innerS struct {
    in1 int
    in2 int
}
type outerS struct {
    b int
    c float32
    int // anonymous field
    innerS //anonymous field
}
func main() {
    outer := new(outerS)
    outer.b = 6
    outer.c = 7.5
    outer.int = 60  // 匿名字段的類型智能有一個哦,因為他想相當于字段名(不重復)
    outer.in1 = 5   // 可以使用 嵌套的字段(相當于繼承的)
    outer.in2 = 10
    fmt.Printf("outer.b is: %d\n", outer.b)
    fmt.Printf("outer.int is: %d\n", outer.int)
    fmt.Printf("outer.in2 is: %d\n", outer.in2)  
}

在一個結構體中對于每一種數據類型只能有一個匿名字段

  • go 接口

接口就是對外暴露的使用特性
內部使用結構體內嵌組合對象應該具有的特性
接口就是一組方法特征的提取,綁定各種比如結構體的對象,已實現在對象上層進行抽象的東西
聲明:

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

注意 方法名和接口名首字母都大寫,這個方法就可以被其他包直接調用了

接口內的方法參數和返回值 ,可以不寫變量,只寫類型即可

type writer interface{
    Write([]byte) error
}

( 有個地方需要注意 type xxx string, interface{}(xxx).(type) 值是xxx 了 而不是 string)

  • Go語言實現接口的條件

一個任意類型T的方法集實現了接口A的 全部方法(>=),那么T 就實現了 A 的接口類型。
接口的作用就是 提取,解耦,同一,規范

// 定義一個數據寫入器
type DataWriter interface {
    WriteData(data interface{}) error
}
// 定義文件結構,用于實現DataWriter
type file struct {
}
// 實現DataWriter接口的WriteData方法
func (d *file) WriteData(data interface{}) error {
    // 模擬寫入數據
    fmt.Println("WriteData:", data)
    return nil
}
func main() {
    // 實例化file
    // var f file
    // 聲明一個DataWriter的接口
    var writer DataWriter
    // 將接口賦值f,也就是*file類型
    writer = new(file) // 這邊要是指針類型,如果是結構體直接調用其方法,則不需要寫&xx這種類型,吊用的時候會自動轉化的,但如果傳給接口變量,則要寫&或用new返回地址
    // 使用DataWriter接口進行數據寫入
    writer.WriteData("data")
    //var f file
   //  writer = f //如果是 file 類型,那么報錯:file does not implement 
//DataWriter (WriteData method has pointer receiver)
    // writer.WriteData("data")
}
>>
WriteData: data

要使用接口,必須實現接口內的所有方法,否則都是編譯報錯(實現一部分,是亂的,沒法整齊劃一,錯誤也很難檢查了)

  • 類型與接口的關系

類型可以有多個接口的,因為功能分類不一樣,所以實現的接口也可以不一樣的,這也體現了解耦

  • go 類型斷言

其實上面已經講過了
1、使用在接口值上的操作

返回 x 的值(也就是 value)和一個布爾值(也就是 ok)
判斷失敗,將會把 ok 置為 false,t 置為 T 類型的 0 值

value, ok := x.(T)   

或者只返回 轉化后的值(注意錯誤判斷)


    var a interface{}
    a = float32(0.1)
    v := a.(float32)
    fmt.Println(v)
>0.1

x 表示一個接口的類型,T 表示一個具體的類型(也可為接口類型)

如果 T 是具體的類型,并且 x 屬于 T 類型, value 則會轉化為T類型的值
如果T 是接口類型, x 屬于他, x 轉化為T 的接口值
2、通過 switch 進行判斷
pass

  • 包的使用

1、包的一般常識
一般小寫,和目錄名一樣(也可以不一樣)
一個目錄下屬于同一個包,一個包的文件不能放在不同的文件夾下

2、包的導入 (現在我使用go module,所以這塊不需要看了)
分全路徑和相對路徑的導入
全路徑:

包名是從GOPATH/src/ 或 GOROOT/src/后開始計算的,使用/ 進行路徑分隔.GOPATH/src/ 下面是自己的包,GOROOT/src/ 下面是 標準包

import "lab/test"  // 自己的包
import "database/sql/driver"   // 內置包
import "database/sql"   // 內置包

上面演示了導入形式
test 包是自定義的包,其源碼位于GOPATH/src/lab/test 目錄下;
driver 包的源碼位于GOROOT/src/database/sql/driver 目錄下;
sql 包的源碼位于GOROOT/src/database/sql 目錄下。
相對路徑:
標準包只可以卻對路徑導入,自己的包(gopath/src 下面的)可以使用相對路徑導入(
和 py 一樣的

兩個包:
GOPATH/src/lab/a
GOPATH/src/lab/b
b 中 導入a 包:

import ../a
或 
import  lab/a

3、包的引用格式
四中格式
標準:

import "xx"
// xx . 使用

自定義別名:

import F "fmt"
F.  使用

全導入:

import . "fmt"
相當于把 fmt 包直接合并到當前程序中
直接 Println()  而不用加 fmt. 了,因為已經合并了包到當前包,同包直接用的

匿名引用格式

import _ "fmt"
相當于只是初始化 這個包 init 函數

導入包注意點:

  • 一個包可以有多個 init 函數,包加載時會執行全部的 init 函數,但并不能保證執行順序,所以不建議在一個包中放入多個 init 函數,將需要初始化的邏輯放到一個 init 函數里面
  • 包不能出現環形引用的情況,比如包 a 引用了包 b,包 b 引用了包 c,如果包 c 又引用了包 a,則編譯不能通過。
  • 包的重復引用是允許的,比如包 a 引用了包 b 和包 c,包 b 和包 c 都引用了包 d。這種場景相當于重復引用了 d,這種情況是允許的,并且 Go 編譯器保證包 d 的 init 函數只會執行一次

4、gopath
go 安裝后,會設置一個默認地 gopath


image.png

在 GOPATH 指定的工作目錄下,代碼總是會保存在 GOPATH/src 目錄下。在工程經過 go build、go install 或 go get 等指令后,會將產生的二進制可執行文件放在GOPATH/bin 目錄下,生成的中間緩存文件會被保存在 GOPATH/pkg 下。 添加 git 等工具,只需要添加GOPATH/src 目錄的源碼即可。bin 和 pkg 目錄的內容都可以由 src 目錄生成。
設置一個 gopath

export GOPATH=xxx 

執行 go install 等命令編譯的時候 會自動找到當前gopath 的目錄進行操作。
包括引用的包等,也會去 gopath 里面找

最好每個項目設置自己的gopath, 以免混亂
5、自定義包
自定義的包在GOPATH 的 src 目錄下, 或者 src 子目錄都可以的,只要編譯地時候指定從 gopath/src 下的相對路徑就好了
同一包必須在同一個文件夾下,比如目錄a 下有 b.go c.go d目錄 那么 b, c 就是 屬于同一個包。d 也許是a下面一個 單獨的包.

導入包需要注意:

如果項目的目錄不在 GOPATH 環境變量中,則需要把項目移到 GOPATH 所在的目錄中,或者將項目所在的目錄設置到 GOPATH 環境變量中,否則無法完成編譯;
使用 import 語句導入包時,使用的是包所屬文件夾的名稱;
包中的函數名第一個字母要大寫,否則無法在外部調用;
自定義包的包名不必與其所在文件夾的名稱保持一致,但為了便于維護,建議保持一致;
調用自定義包時使用 包名 . 函數名 的方式,如上例:demo.PrintStr()

一個目錄下的同級文件歸屬一個包。
包名可以與其目錄不同名。
包名為 main 的包為應用程序的入口包,編譯源碼沒有 main 包時,將無法編譯輸出可執行的文件。

6、讓外部訪問包的類型和值
正常的首字母大寫就ok了。
還有一個情況,就是結構提里面的屬性,首字母也要大寫,否則在包外,無法調用到的(字段要包外使用,她本身大寫,整個結構體名也首大寫才行)

在被導出的結構體或接口中,如果它們的字段或方法首字母是大寫,外部可以訪問這些字段和方法

編譯直接 寫 main 包目錄即可

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。