# Go 語言入門指南: 環境搭建、基礎語法和常用特性解析 | 青訓營 ## 從零開始 ### Go 語言簡介 。在不修改類型定義情況下,可以為其添加介面,這在Java、C++下是不可思議的。go的介面滿足鴨子模型,所謂鴨子類型:只要走起路來像鴨子、叫起來也像鴨子,那麼就可以把它當作鴨子。
二、基本使用
介面定義,描述一堆方法的集合,給出方法聲明即可,不能有預設實現,也不能有變數
type User interface { Say() GetName() string }
定了一個user
介面,包含兩個方法
任何類型都可以實現這兩個方法,不需要顯示使用implements
關鍵字。滿足兩個條件,與介面方法簽名完全一致,是介面方法的超集。即可判定類型實現了介面。
type Sales struct { // 定義類型 name string } func (p *Sales) GetName() string { // 介面方法一 return p.name } func (p *Sales) Say() { // 介面方法二 fmt.Println("Hi, I'm", p.name) } func (p *Sales) peddle() { fmt.Printf("%s peddle", p.name) }
Sales
類型滿足兩個條件,可判斷實現了User
介面。從代碼上看兩者沒有直接關聯,這就是隱式實現。
按照上面的兩個條件,Sales
也實現瞭如下介面
type Person interface { GetName() string }
可以看到非常鬆散,就是這麼簡潔。再次強調只要滿足兩個條件:與介面方法簽名一致,是介面方法的超集,即可判定類型實現了介面。從類型自身角度看,完全不知道自己實現了哪些介面。
通過實例調用方法
var sales Sales = Sales{name: "tom"} sales.Say()
通過介面調用方法,只要類型實現了介面,就可以賦值給介面變數,並使用介面調用方法
var user User = &Sales{name: "tom"} // 賦值給介面變數,註意是地址 user.Say() // 通過介面調用方法 fmt.Printf("%T\n", user) // *main.Sales
介面是引用類型,和指針一樣,只能指向實例的地址。
介面主要目標是解耦,通常稱為面向介面編程,主流使用方式是函數形參是介面類型,調用時候傳遞介面變數,這也是介面存在的意義。
func PrintName(user User) { // 形參是User介面類型 fmt.Println("姓名:", user.GetName()) } var sales User = &Sales{name: "tom"} PrintName(sales) // 傳入user介面變數
形參是介面類型,可傳入所有實現了該介面的實例,不在依賴具體類型。
在標準庫中大量使用介面。比如排序是普片需求,標準庫提供了排序函數,形參是介面類型,任何實現了該介面的類型,都可直接使用排序函數
type Interface interface { // 排序介面 Len() int Less(i, j int) bool Swap(i, j int) } func Sort(data Interface) { // 標準庫排序函數 ... }
和結構體一樣,介面也支持繼承特性
type User interface { Say() GetName() string } type Admin interface { User // 繼承User介面 TeamName string // 自有屬性 }
需要實現包括繼承的所有方法,才判定實現了該介面
並非只能使用結構體實現介面,其他自定義類型也可以實現介面,如下X
類型實現了Plus
介面
type Plus interface { incr() } type X int func (x *X) incr() { *x += 1 fmt.Println(*x) }
三、介面斷言
在介面變數上操作,用於檢查介面類型變數是否實現了期望的介面或者具體的類型。使用介面的本質,就是實例類型和介面類型之間轉換,而是否允許轉換就依賴斷言
value, ok := x.(T)
x
表示介面的類型,T
表示具體類型(也可以是介面),可根據該布爾值判斷 x 是否為 T 類型。
- 如果 T 是實例類型,類型斷言會檢查 x 的動態類型是否滿足 T。如果成功返回 x 的動態值,其類型是 T。
- 如果 T 是介面類型,類型斷言會檢查 x 的動態類型是否滿足 T。如果成功返回 值是 T 的介面值。
- 無論 T 是什麼類型,如果 x 是 nil 介面值,類型斷言都會失敗。
使用上面案例進行斷言
var user User = &Sales{name: "tony"} if value, ok := user.(User); ok { // true, 介面斷言 value.Say() } _, ok = user.(*Sales) // true, 具體類型斷言, 註意這裡使用了指針類型
註意,如果不接收第二個返回值(也就是 ok),斷言失敗時會 panic
,對nil
斷言同樣也會 panic。
admin := user.(Admin) // Admin是管理員介面,斷言失敗panic
具體類型實例如何斷言,可以先轉為為介面,然後再進行斷言
user1 := Sales{"tom"} var data interface{} = user1 // 轉換為空介面 if _, ok := data.(Sales); ok { // 再進行斷言 fmt.Println("yes") }
斷言常見使用場景,異常捕獲時判定錯誤類型
func ProtectRun(entry func()) { defer func() { err := recover() // 獲取錯誤類型 switch err.(type) { // 斷言錯誤類型, 不同類型的錯誤採取不同的處理方式 case runtime.Error: fmt.Println("runtime error:", err) default: fmt.Println("error:", err) } }() ... }
四、介面轉換
go
語言基本數據類型轉換比較嚴格,所有基礎類型不支持隱式轉換,如下案例都不支持
s := "a" + 1 // err // 不同長度的整型, 也支持自動轉換 var i int = 10 var n int8 = 20 m := i+n // err
go
只能顯示轉換
s := "a" + string(1) // a1 var i int = 10 var n int8 = 20 m := i + int(n) // 30
使用介面的本質就是類型轉換,賦值時轉換為介面變數,執行時候轉換為實例。 go 語言對於介面類型的轉換則非常的靈活,對象和介面之間的轉換、介面和介面之間的轉換都可能是隱式的轉換
var f *os.File var a io.ReadCloser = f // 隱式轉換, *os.File 滿足 io.ReadCloser 介面 var b io.Reader = a // 隱式轉換, io.ReadCloser 滿足 io.Reader 介面 var c io.Closer = a // 隱式轉換, io.ReadCloser 滿足 io.Closer 介面 var d io.Reader = a.(io.Reader) // 顯式轉換, io.Closer 不滿足 io.Reader 介面
有時候對象和介面之間太靈活了,導致需要人為地限制這種無意之間的適配。常見的做法是定義一個含特殊方法來區分介面。比如 runtime 包中的 Error 介面就定義了一個特有的 RuntimeError 方法,用於避免其它類型無意中適配了該介面
type runtime.Error interface { error RuntimeError() }
不過這種做法只是君子協定,如果有人刻意偽造介面也是很容易的。再嚴格一點的做法是給介面定義一個私有方法。只有滿足了這個私有方法的對象才可能滿足這個介面,而私有方法的名字是包含包的絕對路徑名的,因此只能在包內部實現這個私有方法才能滿足這個介面。測試包中的 testing.TB 介面就是採用類似的技術
type testing.TB interface { Error(args ...interface{}) Errorf(format string, args ...interface{}) ... private() // 私有方法 }
不過這種通過私有方法禁止外部對象實現介面的做法也是有代價的,首先是這個介面只能包內部使用,外部包正常情況下是無法直接創建滿足該介面對象的;其次,這種防護措施也不是絕對的,惡意的用戶依然可以繞過這種保護機制。通過嵌入匿名的 testing.TB 介面來偽造私有的 private 方法,因為介面方法是延遲綁定,編譯時 private 方法是否真的存在並不重要。
type TB struct { testing.TB } func (p *TB) Fatal(args ...interface{}) { fmt.Println("TB.Fatal disabled!") } func main() { var tb testing.TB = new(TB) tb.Fatal("Hello, playground") }
在自己的 TB 結構體類型中重新實現了 Fatal 方法,然後通過將對象隱式轉換為 testing.TB 介面類型(因為內嵌了匿名的 testing.TB 對象,因此是滿足 testing.TB 介面的),然後通過 testing.TB 介面來調用我們自己的 Fatal 方法。
五、空介面
介面定義沒有聲明任何方法,稱為空介面,按照go
規範任何類型都實現了空介面,因為都滿足了兩個實現條件。這就比較有意思了,空介面可以等於任何值,類似Java中的Object對象。
var data interface{} // 定義空介面變數 data = 1 fmt.Printf("type=%T, value=%v\n", data, data) data = "hello" fmt.Printf("type=%T, value=%v\n", data, data
輸出如下
type=int, value=1 type=string, value=hello
函數形參是空介面類型,就表示可接收任何類型,然後再在斷言,不同的類型,採用不同的邏輯,在開發框架層時經常使用
func assertion(T interface{}) { switch T.(type) { case User: fmt.Println("user") case Admin: fmt.Println("admin") default: fmt.Println("default") } }
T.(type)
語法只能在switch
中使用,可以理解定製語法糖,否則需要使用if
逐個類型斷言
空介面在標準庫空也有普遍使用,比如panic函數終止程式時,可傳遞空介面類型的參數,捕獲錯誤時可獲取
type any = interface{} func panic(v any)