Golang 基礎之基礎語法梳理 (三)

帽兒山的槍手發表於2022-03-20

大家好,今天將梳理出的 Go語言基礎語法內容,分享給大家。 請多多指教,謝謝。

本次《Go語言基礎語法內容》共分為三個章節,本文為第三章節

本章節內容

  • 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
  ...
}

注意

  1. 介面名:使用type將介面定義為自定義的型別名。Go語言的介面在命名時,一般會在單詞後面新增er,如有寫操作的介面叫Writer,有字串功能的介面叫Stringer等。介面名最好要能突出該介面的型別含義。

  2. 方法名:當方法名首字母是大寫且這個介面型別名首字母也是大寫時,這個方法可以被介面所在的包(package)之外的程式碼訪問。

  3. 引數列表、返回值列表:引數列表和返回值列表中的引數變數名可以省略。

例子

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 官方地址

https://pkg.go.dev/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是由所有可比較型別(布林、數字、字串、指標、通道、可比較型別的陣列、欄位均為可比較型別的結構)實現的介面。可比較介面只能用作型別引數約束,不能用作變數的型別。

https://pkg.go.dev/builtin@master#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

技術文章持續更新,請大家多多關注呀~~

搜尋微信公眾號【 帽兒山的槍手 】,關注我