這篇文章就總結一下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
- 數組聲明
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
在 GOPATH 指定的工作目錄下,代碼總是會保存在
GOPATH/bin 目錄下,生成的中間緩存文件會被保存在
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 包目錄即可