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

大家好,今天將梳理出的 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 官方地址

//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是由所有可比較類型(布爾、數字、字元串、指針、通道、可比較類型的數組、欄位均為可比較類型的結構)實現的介面。可比較介面只能用作類型參數約束,不能用作變數的類型。

//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

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

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