Go語言6-接口、反射
接著上次的繼續講接口,先回顧一下接口的用法:
package main import "fmt" // 定義接口 type Car interface { GetName() string Run() } // 定義結構體 type Tesla struct { Name string } // 實現接口的GetName()方法 func (t *Tesla) GetName() string { return t.Name } // 實現接口的Run()方法 func (t *Tesla) Run() { fmt.Printf("%s is running\n", t.Name) } func main() { var c Car var t Tesla = Tesla{"Tesla Model S"} c = &t // 上面是用指針*Tesla實現了接口的方法,這裏要傳地址 /* 或者在定義的時候,就定義結構體指針 var t *Tesla = &Tesla{"Tesla Model X"} c = t */ fmt.Println(c.GetName()) c.Run() }
強調一下:interface 類型默認是一個指針
空接口
沒有定義任何方法的接口,就是空接口:
type Empty interface{} // 定義了一個接口類型 Empty,裏面沒有任何方法
var e1 Empty // e1 就是一個空接口
var e2 interface{} // e2 也是空接口,這裏跳過了接口類型的定義,在定義接口的同時把接口類型一起做了
由於空接口裏沒有定義任何方法,任何類型都實現了空接口。也就是空接口可以被任何類型實現,空接口能夠容納任何類型。
package main import "fmt" func main(){ var e interface{} // 定義一個空接口 var n int e = n // n可以給e賦值,因為n實現了e。這樣接口就能存儲它具體的實現類 //n = e // 反過來就不行, fmt.Printf("%T %T\n", n, e) // 通過接口也能獲取到它的實現類 }
之前一直使用的 fmt.Println()
,什麽類型都可以往裏傳。這個函數接收的參數是這樣的:
func(a ...interface{}) (n int, err error)
這裏單看參數類型,就是空接口,任何類型都實現了空接口,所以任何類型都能作為參數。
類型轉換
空接口也是個類型,類型轉換的用法是一樣的,不要遇到了大括號就看不懂了:
var i int // 定義一個int類型
j := int32(i) // 轉成int32
k := interface{}(i) // 轉成空接口類型
對自定義結構體排序
排序使用 sort 包。包裏提供了 Sort 方法可以對接口進行排序。
func Sort(data Interface)
Sort 對 data 進行排序。它調用一次 data.Len 來決定排序的長度 n,調用 data.Less 和 data.Swap 的開銷為 O(n*log(n))。此排序為不穩定排序。
這裏是對接口進行排序,所以傳入的 data 參數需要實現接口裏的方法,接口的定義如下:
type Interface interface {
// Len is the number of elements in the collection.
// Len 為集合內元素的總數
Len() int
// Less reports whether the element with
// index i should sort before the element with index j.
//
// Less 返回索引為 i 的元素是否應排在索引為 j 的元素之前。
Less(i, j int) bool
// Swap swaps the elements with indexes i and j.
// Swap 交換索引為 i 和 j 的元素
Swap(i, j int)
}
所以要對你的自定義結構體進行排序,首先定義一個該結構體類型的切片類型,然後實現上面的3個方法。之後就可以插入數據然後進行排序了:
package main
import (
"fmt"
"sort"
)
// 自定義的結構體
type Animal struct {
Type string
Weitht int
}
// 新定義一個切片的類型,下面對這個類型實現interface要求的3個方法
// 直接用 []Animal Go不認,這裏應該是起了個別名
type AnimalSlice []Animal
// 對自定義的切片類型實現Sort的接口要求的3個方法
func (a AnimalSlice) Len() int {
return len(a)
}
func (a AnimalSlice) Less(i, j int) bool {
return a[i].Weitht < a[j].Weitht
}
func (a AnimalSlice) Swap(i, j int) {
a[i], a[j] = a[j], a[i]
}
func main() {
var tiger Animal = Animal{"Tiger", 200}
var dog Animal = Animal{"Dog", 20}
var cat Animal = Animal{"Cat", 15}
var elephant Animal = Animal{"Elephant", 4000}
// 這裏的切片要用自定義的類型,別名被認為是兩個不同的類型,只有這個實現了接口的方法
var data AnimalSlice
data = append(data, tiger)
data = append(data, dog)
data = append(data, cat)
data = append(data, elephant)
fmt.Println(data)
sort.Sort(data)
fmt.Println(data)
}
接口嵌套
一個接口可以嵌套另外的接口:
type ReadWrite interface {
Read(b Buffer) bool
Write(b Buffer) bool
}
type Lock interface {
Lock()
Unlock
}
type File interface {
ReadWrite
Lock
Close
}
嵌套的用法類似結構體的繼承。這樣如果已經有了一些接口,只要把這些接口組合一下,就又產生了一些新的接口了,不用去重復定義。
再來寫個例子,主要是熟悉對接口編程的思路,順便用到了接口的嵌套:
package main
import "fmt"
// 定義一個接口
type Reader interface {
Read()
}
// 再定義一個接口
type Writer interface {
Write(s string)
}
// 定義第三個接口,嵌套上面的兩個接口
type ReadWriter interface {
Reader
Writer
}
// 定義一個結構體
type file struct {
content string
}
func (f *file) Read(){
fmt.Println(f.content)
}
func (f *file) Write(s string){
f.content = s
fmt.Println("寫入數據:", s)
}
// 這個函數是對接口進行操作,上面的file類型實現了接口的所有方法
func CheckChange(rw ReadWriter, s string) {
rw.Read()
rw.Write(s)
rw.Read()
}
func main() {
var f file = file{"Hello"}
CheckChange(&f, "How are you")
}
上面寫的 CheckChange() 方法,只有是實現了 ReadWriter 這個接口的任何類型,都可以用這個函數來調用。
類型斷言
類型斷言,由於接口是一般類型,不知道具體的類型。
之前的例子裏的函數,定義的入參是接口。而實際傳入的是某個實現了接口類型的具體類型。比如上面的例子 CheckChange() 方法接收的參數主要是實現了 ReadWriter 接口的任何類型都可以。可以是例子裏的自定義類型 file。也可以是別的類型比如再自定義一個 message。這樣在函數接收參數後,是不知道這個參數的具體類型的。有些場景,你需要知道這個接口指向的具體類型是什麽。
可以把接口類型轉成具體類型,如果要轉成具體類型,可以采用以下方法進行轉換:
package main
import "fmt"
func main() {
var i int = 10 // 這個是int
var j interface{} // 這個是空接口
fmt.Printf("%T %v\n", j, j) // 還沒給j賦值,現在j只是一個空指針,默認類型和默認值都是nil
j = i // 任何類型都可以給空接口賦值,如果i是參數傳入的,現在並不知道i的類型
fmt.Printf("%T %v\n", j, j) // 可以打印查看現在的j的類型和值都是和i一樣的,但是代碼層面還是不知道具體類型
res := j.(int) // 轉成int類型,如果不是int類型會報錯
//res := j.(int32) // 轉成int32,由於類型不對,會報錯
fmt.Printf("%T %v\n", res, res)
}
上面在 res := j.(int)
這句做類型轉換之前,打印 j 的類型的時候已經看到類型是 int 了,但是其實 j 的類型在代碼層面還不知道。需要執行這句類型轉換把類型轉成 int 。下面的函數接收空接口,但是內部要做加法,只有將參數轉成數值類型後,才能做加法:
func add(a interface{}){
b := a.(int) // 只有做了類型轉換,才能做下面的加法
b++
c := a
fmt.Printf("%T %v\n", c, c) // 雖然能打印出類型,但是代碼層面這個的類型還是interface{}
//c++ // 這句還不能執行,現在c的類型是interface{},只有數值類型能做加法
}
上面是不帶檢查的,如果類型轉換不成功,會報錯。下面是帶檢查的類型斷言:
package main
import "fmt"
type Num struct {
n int
}
func main() {
var i Num = Num{1}
var j interface{}
j = i
res, ok := j.(int) // 帶檢查的類型斷言
fmt.Println(res, ok) // ok是false,類型不對,res的值就是轉換類型的默認值
var k interface{}
k = i
res2, ok := k.(Num) // 這次類型是對的
fmt.Println(res2, ok) // ok是true
}
判斷類型
除了類型斷言,還有這個方法可以判斷類型。
下面的函數可以判斷傳入參數的類型:
package main
import "fmt"
func classifier(items ...interface{}) {
for i, v := range items {
switch v.(type) {
case bool:
fmt.Println("bool", i)
case float64:
fmt.Println("float64", i)
case int:
fmt.Println("int", i)
case nil:
fmt.Println("nil", i)
case string:
fmt.Println("string", i)
default:
fmt.Println("unknow", i)
}
}
}
func main() {
classifier(1, "", nil, 1.234, true, int32(5))
}
/* 執行結果
PS H:\Go\src\go_dev\day6\interface\classifier> go run main.go
int 0
string 1
nil 2
float64 3
bool 4
unknow 5
PS H:\Go\src\go_dev\day6\interface\classifier>
*/
這裏用到了 v.(type) ,這個必須與 switch case 聯合使用,如果寫在 switch 外面,編譯器會報錯。
判斷是否實現了指定接口
語法如下:
v, ok := interface{}(實例).(接口名)
先要把類型轉成空接口,然後再判斷是否實現了指定的接口。
示例:
package main
import "fmt"
// 定義一個結構體
type Example struct{
Name string
}
// 這是一個接口
type IF1 interface{
Hello()
}
// 這是另一個接口
type IF2 interface{
Hi()
}
// 實現了接口 IF1 的方法
func (e Example) Hello(){
fmt.Println("Hello")
}
func main(){
var e Example = Example{"TEST"} // 這裏可以不做初始化的,不初始化也是有默認值的,srting型就是空
v, ok := interface{}(e).(IF1)
fmt.Println(v, ok)
v2, ok := interface{}(e).(IF2)
fmt.Println(v2, ok)
}
/* 執行結果
PS H:\Go\src\go_dev\day6\interface\is_if> go run main.go
{TEST} true
<nil> false
PS H:\Go\src\go_dev\day6\interface\is_if>
*/
接口示例
實現一個通用的鏈表類
重點要實現尾插法,頭插法的當前節點不用移動,始終是頭節點就行了。而尾插法要有一個當前節點的指針始終指向最後的一個節點。示例:
// go_dev\day6\interface\link\link\link.go
package link
import (
"fmt"
)
type Link struct{
Data interface{} // 數據是空接口,所以是通用類型
Next *Link
}
// 頭插法,p需要傳指針,因為方法裏需要改變p的值
// 但是p本身也是個指針,所以接收的類型是指針的指針
func (l *Link) AddNodeHead(data interface{}, p **Link){
var node Link
node.Data = data
node.Next = (*p).Next
(*p).Next = &node
}
// 尾插法
func (l *Link) AddNodeTail(data interface{}, p **Link){
var node Link
node.Data = data
(*p).Next = &node
(*p) = &node
}
// 遍歷鏈表的方法,打印當前節點以及之後的所有的節點
func (l *Link) Trans(){
for l != nil {
fmt.Println(*l)
l = l.Next
}
}
// go_dev\day6\interface\link\main\main.go
package main
import (
"../link"
)
func main(){
var intLink link.Link // 別名,後面都用intLink
head := intLink // head是頭節點
p := &head // p是指向當前節點的指針,註意結構體是值類型
// 插入節點
for i := 0; i < 10; i++ {
node := intLink
node.Data = i
// 插入節點的方法,需改改變p本真的值,這裏就要把p的地址傳進去
// 由於p本身已經是個指針了,再傳指針的地址,那個變量就是指針的指針
//intLink.AddNodeHead(node, &p) // 頭插法
intLink.AddNodeTail(node, &p) // 尾插法
}
head.Trans() // 從頭節點遍歷鏈表
}
這個例子用了指針的指針。因為結構體是值類型,指向當前節點的變量p需要是一個指針類型。然而在添加節點的方法裏(主要是尾插法),需要改變p的值,將p重新指向新插入的節點。這就要求必須把p的地址傳進來,這樣就是指針的指針了。
其實也可以不用那麽做,不在方法裏改變p的值,而是給方法添加一個返回值,返回最新的當前節點。這樣就需要在調用方法的時候獲取返回值然後賦值給p,就是在方法外改變p的值,這樣就可以傳p的副本給方法處理了。
實現一個負載均衡的調度算法,支持隨機、輪訓等算法
(略...)
反射
反射,可以在運行時動態的獲取到變量的相關信息。需要 reflect 包:
import "reflect"
基本用法
主要是下面這2個函數:
func TypeOf(i interface{}) Type
: 獲取變量的類型,返回 reflect.Type 類型func ValueOf(i interface{}) Value
: 獲取變量的值,返回 reflect.Value 類型
package main
import (
"fmt"
"reflect"
)
func test(a interface{}){
t := reflect.TypeOf(a)
fmt.Println(t)
v := reflect.ValueOf(a)
fmt.Println(v)
}
func main(){
n := 100
test(n)
}
/* 執行結果
PS H:\Go\src\go_dev\day6\reflect\beginning> go run main.go
int
100
PS H:\Go\src\go_dev\day6\reflect\beginning>
*/
在 reflect.Value 裏提供了很多方法。大多數情況下,都是要先獲取到 reflect.Value 類型,然後再調用對應的方法來實現。
獲取類別(kind)
類型(type)和類別(kind),原生的類型兩個的名字應該是一樣了。不過自定義類型比如結構體,type就是我們自定義的名字,而kind就是struct。
要獲取kind,首先是用上面的方法獲取到 reflect.Value 類型,然後調用 Kind 方法,返回 reflect.Kind 類型:
func (v Value) Kind() Kind
具體用法:
package main
import (
"fmt"
"reflect"
)
type Example struct{} // 自定義結構體,看下類型和類別
func main(){
a1 := 10
t1 := reflect.TypeOf(a1)
v1 := reflect.ValueOf(a1)
k1 := v1.Kind()
fmt.Println(t1, k1) // 原生類型的類別看不出來
a2 := Example{}
t2 := reflect.TypeOf(a2)
v2 := reflect.ValueOf(a2)
k2 := v2.Kind()
fmt.Println(t2, k2) // 自定義結構體的類型是自定義的名字,類別是struct
}
/* 執行結果
PS H:\Go\src\go_dev\day6\reflect\kind> go run main.go
int int
main.Example struct
reflect.Kind string
*/
示例中我們最後看到的是打印輸出的效果。上面的兩個 Kind() 方法的返回值的類型是 reflect.Kind ,這是包裏定義的常量。如果要進行比較的話,這樣比較:
k1 == reflect.Struct
k2 == reflect.String
另外,返回的類型並不是字符串類型。返回的是包裏定義的常量上面已經講過了。如果要獲取類型的字符串名稱,可以用 reflect.Kind 類型的 String() 方法:
func (k Kind) String() string
轉成空接口
用法:
func (v Value) Interface() (i interface{})
示例:
package main
import (
"fmt"
"reflect"
)
type Student struct{
Name string
Age int
}
func main(){
var s Student = Student{"Adam", 18}
t := reflect.ValueOf(s)
tif := t.Interface() // 調用Interface()方法,返回空接口類型
// 類型斷言,必須要用空接口調用
if stu, ok := tif.(Student); ok{
fmt.Printf("%T %v\n", stu, stu)
}
}
獲取、設置變量
通過反射獲取變量的值:
func (v Value) Float() float64
func (v Value) Int() int64
func (v Value) Bool() bool
func (v Value) String() string
通過反射設置變量的值:
func (v Value) SetFloat(x float64)
func (v Value) SetInt(x int64)
func (v Value) SetBool(x bool)
func (v Value) SetString(x string)
如果要設置的是一個值類型,那麽肯定是要傳地址的。但是傳地址之後,轉成了Value類型後就無法再用星號取到指針指向的內容了。這裏提供下面的 Elem() 方法。
取指針指向的值:
func (v Value) Elem() Value
示例:
package main
import (
"fmt"
"reflect"
)
func get(x interface{}){
v := reflect.ValueOf(x)
res := v.Int()
fmt.Printf("%T %v\n", res, res)
}
func set(x interface{}){
v := reflect.ValueOf(x) // x如果是個指針,*x是可以用的
// 但是通過ValueOf()方法獲得的v就不是指針了,沒法用*v
// 所以有了下面的Elem()方法,效果就是我們想要的*v的效果
v.Elem().SetInt(2) // 先要用Elem獲取到指針指向的內容,然後才能Set
}
func main(){
var n int = 1
get(n)
set(&n) // 這裏肯定是要地址的
get(n)
}
操作結構體
返回結構體裏字段、方法的數量:
func (v Value) NumField() int
func (v Value) NumMethod() int
示例:
package main
import (
"fmt"
"reflect"
)
type Student struct{
Name string
Age int
Score float32
}
func TestStruct(x interface{}){
v := reflect.ValueOf(x)
if k := v.Kind(); k != reflect.Struct {
fmt.Println(v, "不是結構體")
return
}
fmt.Println(v, "是結構體")
numOfField := v.NumField()
fmt.Println("結構體裏的字段數量:",numOfField)
numOfMethod := v.NumMethod()
fmt.Println("結構體裏的方法數量:",numOfMethod)
}
func main() {
TestStruct(1) // 傳個非結構體測試一下效果
var a Student = Student{"Adam", 17, 92.5}
TestStruct(a)
}
獲取對應的字段、方法
通過下標獲取:
func (v Value) Field(i int) Value
func (v Value) Method(i int) Value
還有通過名字獲取:
func (v Value) FieldByName(name string) Value
func (v Value) FieldByNameFunc(match func(string) bool) Value
func (v Value) MethodByName(name string) Value
調用方法:
用上面的方法獲取到方法後,再 .Call(nil) 就可以執行了。沒有參數的話傳 nil 就好了。Call只接收1個參數,把方法需要的所有參數都轉成 Value 類型然後放在一個切片裏傳給 Call 執行。返回值也是切片,裏面所有的值都是 Value 類型:
func (v Value) Call(in []Value) []Value
上面2句可以寫一行裏,比如下面這樣,調用第一個方法,沒有參數,不要返回值:
v.Method(0).Call(nil)
Type 接口的操作
這裏用的是TypeOf() 方法,不要和上面的搞混了。返回值是 reflect.Type 類型,這是一個接口類型:
type Type interface {}
接口裏的方法比較多,具體去官網看吧:https://go-zh.org/pkg/reflect/#Type
獲取字段的Tag對應的內容
json序列化是用Tag替換字段名的實現,利用的也是這裏的反射。
通過接口的 Field(i int) StructField
方法,傳入下標獲取到的是一個 StructField 結構體:
type StructField struct {
// Name is the field name.
// PkgPath is the package path that qualifies a lower case (unexported)
// field name. It is empty for upper case (exported) field names.
// See http://golang.org/ref/spec#Uniqueness_of_identifiers
Name string
PkgPath string
Type Type // field type
Tag StructTag // field tag string
Offset uintptr // offset within struct, in bytes
Index []int // index sequence for Type.FieldByIndex
Anonymous bool // is an embedded field
}
結構體裏有一個字段是 Tag ,類型是 StructTag 。這是一個字符串類型的別名,不過裏面實現了一些方法。調用 StructTag 的 Get 方法,傳入Tag的key,就能返回Tag裏對應的value:
func (tag StructTag) Get(key string) string
完整的代碼,抄官網的示例( https://go-zh.org/pkg/reflect/#example_StructTag ):
package main
import (
"fmt"
"reflect"
)
func main() {
type S struct {
F string `species:"gopher" color:"blue"`
}
s := S{}
st := reflect.TypeOf(s) // 註意這裏是TypeOf,返回值是 Type 接口
field := st.Field(0) // Type 接口裏的方法,返回 StructField 結構體。
// StructField結構體裏面的Tag字段是 StructTag 一個 string 類型的別名
// StructTag裏實現了Get方法,下面就是調用該方法通過key獲取到value
fmt.Println(field.Tag.Get("color"), field.Tag.Get("species"))
}
json序列化操作的時候,就是利用了反射的方法,獲取到tag裏json這個key對應的value,替換原本的字段名。
課後作業
實現一個圖書管理系統v2,增加以下功能:
- 增加用戶登錄、註冊功能
- 增加借書過期的圖書界面
- 增加顯示熱門圖書的功能,被借次數最多的Top10
- 增加查看某人的借書記錄的功能
Go語言6-接口、反射