Golang 基礎之基礎語法梳理 (三)
- 2022 年 3 月 20 日
- 筆記
- Golang從小白到大神
大家好,今天將梳理出的 Go語言基礎語法內容,分享給大家。 請多多指教,謝謝。
本次《Go語言基礎語法內容》共分為三個章節,本文為第三章節
- Golang 基礎之基礎語法梳理 (一)
- Golang 基礎之基礎語法梳理 (二)
- Golang 基礎之基礎語法梳理 (三)
本章節內容
- interface
- 反射
- 泛型
interface
介紹
在Go語言中介面 (interface) 是一種類型, 一種抽象的類型。
介面 (interface) 定義了一個對象的行為規範, 只定義規範不實現,由具體的對象來實現規範的細節。
介面做的事情就像是定義一個協議(規則)。
Interface 是一組method的集合, 是duck-type programming 的一種體現。
介面的定義
- 介面是一個或多個方法簽名的集合
- 介面只有方法聲明,沒有實現,沒有數據欄位
- 介面可以匿名嵌入其他介面,或嵌入到結構中
- 介面調用不會做receiver的自動轉換
- 介面同樣支援匿名欄位方法
- 介面也可實現類似OOP中的多態
- 任何類型的方法集中只要擁有該介面’對應的全部方法’簽名
- 只有當介面存儲的類型和對象都為nil時,介面才等於nil
- 用 interface{} 傳遞任意類型數據是Go語言的慣例用法,而且 interface{} 是類型安全的
- 空介面可以作為任何類型數據的容器
- 一個類型可實現多個介面
- 介面命名習慣以 er 結尾
使用
每個介面由數個方法組成,介面的定義如下
type 介面類型 interface {
方法名1 (參數列表1) 返回值列表1
方法名2 (參數列表2) 返回值列表2
...
}
注意
介面名:使用type將介面定義為自定義的類型名。Go語言的介面在命名時,一般會在單詞後面添加er,如有寫操作的介面叫Writer,有字元串功能的介面叫Stringer等。介面名最好要能突出該介面的類型含義。
方法名:當方法名首字母是大寫且這個介面類型名首字母也是大寫時,這個方法可以被介面所在的包(package)之外的程式碼訪問。
參數列表、返回值列表:參數列表和返回值列表中的參數變數名可以省略。
例子
type writer interface {
Write([]byte) error
}
值接收者和指針接收介面
type Mover interface {
move()
}
type dog struct {}
func (d dog) move() {
fmt.Println("狗狗")
}
func main() {
var x Mover
var wangcai = dog{}
x = wangcai // x 可以接收dog類型
var fugui = &dog{} // fugui是 *dog 類型
x = fugui // x可以接收*dog類型 指針接收
x.move()
}
多個類型實現同一介面
// Mover 介面
type Mover interface {
move()
}
type dog struct {
name string
}
type car struct {
brand string
}
// dog 類型實現 Mover 介面
func (d dog) move() {
fmt.Printf("%s: mmmm", d.name)
}
// car 類型實現 Mover 介面
func (c car) move() {
fmt.Printf("%s: mmmm", c.brand)
}
func main() {
var x Mover
var a = dog{name: "旺財"}
var b = car{brand: "蝦米"}
x = a
x.move()
x = b
x.move()
}
一個介面的方法,不一定需要由一個類型完全實現,介面的方法可以通過在類型中嵌入其他類型或者結構體來實現。
介面嵌套
介面與介面間可以通過嵌套創造出新的介面。
type Sayer interface {
say()
}
type Mover interface {
move()
}
// 介面嵌套
type animal interface {
Sayer
Mover
}
// 嵌套得到的介面的使用與普通介面一樣
type cat struct {
name string
}
func (c cat) say() {
fmt.Println("ssss")
}
func (c cat) move() {
fmt.Println("mmmm")
}
func main() {
var x animal
x = cat{name: "花花"}
x.move()
x.say()
}
空介面
空介面是指沒有定義任何方法的介面,因此任何類型都實現了空介面。
空介面類型的變數可以存儲任意類型的變數。
func main() {
// 定義一個空介面 x
var x interface{}
s := "test data"
x = s
fmt.Printf("type:%T value: %v\n", x, x)
i := 100
x = i
fmt.Printf("type:%T value: %v\n", x, x)
b := true
x = b
fmt.Printf("type:%T value: %v\n", x, x)
}
空介面作為函數的參數
使用空介面實現可以接收任意類型的函數對象。
func show(a interface{}){
fmt.Printf("type:%T value: %v\n", a, a)
}
空介面作為map的參數
使用空介面實現可以保存任意值的字典
var Info = make(map[string]interface{})
Info["id"] = 1
Info["name"] = "帽兒山的槍手"
fmt.Println(Info)
獲取空介面值
判斷空介面中值,可以使用類型斷言,語法如下
x.(T)
x
表示類型為 interface{} 的變數
T
表示斷言 x 可能是的類型
該語法返回兩個參數,第一個參數是 x 轉化為 T 類型後的變數, 第二個值是一個布爾值, 若為 true 則表示斷言成功, false 則表示失敗。
func main() {
var x interface{}
x = "data"
v, ok := x.(string)
if ok {
fmt.Println(v)
} else {
fmt.Println("類型斷言失敗")
}
}
如果要斷言多次,可以寫 if
判斷, 也可以用 switch
語句實現。
反射
介紹
什麼是反射?
例如:有時候我們需要知道某個值是什麼類型,才能用對等邏輯去處理它。
以下是常用的處理方法:
// 偽程式碼
switch value := value.(type){
case string:
// 處理操作
case int:
// 處理操作
...
}
這樣處理,會寫的非常長,而且還可能存在自定的類型,也就是說這個判斷日後可能還要一直改,因為無法知道未知值到底屬於什麼類型。
如果使用反射來處理,使用標準庫 reflect
中的 TypeOf 和 ValueOf 函數從介面中獲取目標對象的資訊,就可以輕鬆處理這個問題。
更多介紹,可參考reflect 官方地址
Go語言提供了一種機制,在編譯時不知道類型的情況下,可更新變數、運行時查看值、調用方法以及直接對他們的布局進行操作的機制,稱為反射。
使用
使用反射查看對象類型
package main
import (
"fmt"
"reflect"
)
func main() {
var name string = "帽兒山的槍手"
nameType := reflect.TypeOf(name)
nameValue := reflect.ValueOf(name)
fmt.Println("name type: ", nameType)
fmt.Println("name value: ", nameValue)
}
輸出
name type: string
name value: 帽兒山的槍手
struct 類型反射用法
package main
import (
"fmt"
"reflect"
)
type Info struct {
Name string
Desc string
}
func (i Info) Detail() {
fmt.Println("detail info")
}
func main() {
i := Info{Name: "帽兒山的槍手", Desc: "技術分享"}
t := reflect.TypeOf(i) // 獲取目標對象
v := reflect.ValueOf(i) // 獲取value值
for i := 0; i < v.NumField(); i++ { // NumField()獲取欄位總數
key := t.Field(i) // 根據下標,獲取包含的key
value := v.Field(i).Interface() // 獲取key對應的值
fmt.Printf("key=%s value=%v type=%v\n", key.Name, value, key.Type)
}
// 獲取Info的方法
for i := 0; i < t.NumMethod(); i++ {
m := t.Method(i)
fmt.Printf("方法 Name=%s Type=%v\n", m.Name, m.Type)
}
}
輸出
key=Name value=帽兒山的槍手 type=string
key=Desc value=技術分享 type=string
方法 Name=Detail Type=func(main.Info)
通過反射判斷類型用法
package main
import (
"fmt"
"reflect"
)
type Info struct {
Name string
Desc string
}
func main() {
i := Info{Name: "帽兒山的槍手", Desc: "技術分享"}
t := reflect.TypeOf(i)
// Kind()函數判斷值的類型
if k := t.Kind(); k == reflect.Struct {
fmt.Println("struct type")
}
num := 100
switch v := reflect.ValueOf(num); v.Kind() {
case reflect.String:
fmt.Println("string type")
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
fmt.Println("int type")
default:
fmt.Printf("unhandled kind %s", v.Kind())
}
}
輸出
struct type
int type
通過反射修改內容
package main
import (
"fmt"
"reflect"
)
type Info struct {
Name string
Desc string
}
func main() {
i := &Info{Name: "帽兒山的槍手", Desc: "技術分享"}
v := reflect.ValueOf(i)
// 修改值必須是指針類型
if v.Kind() != reflect.Ptr {
fmt.Println("不是指針類型")
return
}
v = v.Elem() // 獲取指針指向的元素
name := v.FieldByName("Desc") // 獲取目標key的值
name.SetString("好好工作")
fmt.Printf("修改後數據: %v\n", *i)
}
輸出
修改後數據: {帽兒山的槍手 好好工作}
通過反射調用方法
package main
import (
"fmt"
"reflect"
)
type Info struct {
Name string
Desc string
}
func (i Info) Detail() {
fmt.Println("detail info")
}
func main() {
i := Info{Name: "帽兒山的槍手", Desc: "技術分享"}
v := reflect.ValueOf(i)
// 獲取方法控制權
mv := v.MethodByName("Detail")
mv.Call([]reflect.Value{}) // 這裡是無調用參數 []reflect.Value{}
}
輸出
detail info
泛型
介紹
泛型的概念,可以從多態看起,多態是同一形式表現出不同行為的一種特性,在程式語言中被分為兩類,臨時性多態和參數化多態。
根據實參生成不同的版本,支援任意數量的調用,即泛型,簡言之,就是把元素類型變成了參數。
golang版本需要在 1.17版本或以上,才支援泛型使用。
(1.17版本泛型是golang推出的嘗鮮版,1.18是正式版本)
舉例:
func Add(a, b int) int{}
func AddFloat(a, b float64) float64{}
在泛型的幫助下,上面程式碼就可以簡化成為:
func Add[T any](a, b T) T
Add後面的[T any],T表示類型的標識,any表示T可以是任意類型。
a、b和返回值的類型T和前面的T是同一個類型。
為什麼用[],而不是其他語言中的<>,官方有過解釋,大概就是<>會有歧義。曾經計劃使用() ,因為太容易混淆,最後使用了[]。
泛型3大概念
- 類型參數
- 類型約束
- 類型推導
特性
- 函數可以通過type關鍵字引入額外的類型參數
(type parameters)列表:func F(type T)(p T) { ... }
- 這些類型參數可以像一般的參數一樣在函數體中使用
- 類型也可以擁有類型參數列表:
type M(type T) []T
- 每個類型參數可以擁有一個約束:
func F(type T Constraint)(p T) { ... }
- 使用interface來描述類型的約束
- 被用作類型約束的interface可以擁有一個預聲明類型列表,限制了實現此介面的類型的基礎類型
- 使用泛型函數或類型時需要傳入類型實參
- 類型推斷允許用戶在調用泛型函數時省略類型實參
- 泛型函數只允許進行類型約束所規定的操作
使用
對泛型進行輸出
如果Go當前版本是1.17版本,運行時需要加參數 -gcflags=-G=3
# 完整命令
go run -gcflags=-G=3 example.go
示例
package main
import (
"fmt"
)
func print[T any](s []T) {
for _, v := range s {
fmt.Printf("%v ", v)
}
fmt.Printf("\n")
}
func main() {
print[int]([]int{1,2,3,4})
print[float64]([]float64{1.01, 2.02, 3.03, 4.04})
print[string]([]string{"a", "b", "c", "d"})
}
輸出
1 2 3 4
1.01 2.02 3.03 4.04
a b c d
Go1.18 中,any
是 interface{} 的別名
使用泛型約束,控制類型的使用範圍
原先的語法中,類型約束會用逗號分隔的方式來展示
type int, int8, int16, int32, int64
在新語法中,結合定義為 union element(聯合元素),寫成一系列由豎線 」|「 分隔的類型或近似元素。
int | int8 | int16 | int32 | int64
示例
package main
import (
"fmt"
)
type CustomType interface {
int | int8 | int16 | int32 | int64 | string
}
func add[T CustomType] (a, b T) T{
return a + b
}
func main() {
fmt.Println(add(1, 2))
fmt.Println(add("帽兒山的槍手", "技術分享"))
}
輸出
3
帽兒山的槍手技術分享
上述 CustomType
介面類型也可以寫成以下格式
type CustomType interface {
~int | ~string
}
上述聲明的類型集是 ~int,也就是所有類型為 int 的類型(如:int、int8、int16、int32、int64)都能夠滿足這個類型約束的條件。
泛型中自帶 comparable 約束
因為不是所有的類型都可以==比較,所以Golang內置提供了一個comparable約束,表示可比較的。
官方說明
comparable是由所有可比較類型(布爾、數字、字元串、指針、通道、可比較類型的數組、欄位均為可比較類型的結構)實現的介面。可比較介面只能用作類型參數約束,不能用作變數的類型。
package main
import (
"fmt"
)
func diff[T comparable](a []T, v T) {
for _, e := range a {
if e == v {
fmt.Println(e)
}
}
}
func main() {
diff([]int{1, 2, 3, 4}, 3)
}
輸出
3
泛型中操作指針
package main
import (
"fmt"
)
func pointerOf[T any](v T) *T {
return &v
}
func main() {
name := pointerOf("帽兒山的槍手")
fmt.Println(*name)
id := pointerOf(100)
fmt.Println(*id)
}
輸出
帽兒山的槍手
100
技術文章持續更新,請大家多多關注呀~~
搜索微信公眾號【 帽兒山的槍手 】,關注我