六、GO 程式設計模式:GO GENERATION

zhaocrazy發表於2022-02-08

在本篇文章中,我們將要學習一下Go語言的程式碼生成的玩法。Go語言程式碼生成主要還是用來解決程式設計泛型的問題,泛型程式設計主要解決的問題是因為靜態型別語言有型別,所以,相關的演算法或是對資料處理的程式會因為型別不同而需要複製一份,這樣導致資料型別和演算法功能耦合的問題。泛型程式設計可以解決這樣的問題,就是說,在寫程式碼的時候,不用關心處理資料的型別,只需要關心相當處理邏輯。泛型程式設計是靜態語言中非常非常重要的特徵,如果沒有泛型,我們很難做到多型,也很難完成抽象,會導致我們的程式碼冗餘量很大。

現實中的類比

舉個現實當中的例子,用螺絲刀來做具比方,螺絲刀本來就是一個擰螺絲的動作,但是因為螺絲的型別太多,有平口的,有十字口的,有六角的……螺絲還有大小尺寸,導致我們的螺絲刀為了要適配各種千奇百怪的螺絲型別(樣式和尺寸),導致要做出各種各樣的螺絲刀。

六、GO 程式設計模式:GO GENERATION

而真正的抽象是螺絲刀不應該關心螺絲的型別,只要關注好自己的功能是否完備,並讓自己可以適配於不同型別的螺絲,如下所示,這就是所謂的泛型程式設計要解決的實際問題。

六、GO 程式設計模式:GO GENERATION

Go語言的型別檢查

因為Go語言目前並不支援真正的泛型,所以,只能用 interface{} 這樣的類似於 void* 這種過度泛型來玩這就導致了我們在實際過程中就需要進行型別檢查。Go語言的型別檢查有兩種技術,一種是 Type Assert,一種是Reflection。

Type Assert

這種技術,一般是對某個變數進行 .(type)的轉型操作,其會返回兩個值, variable, error,第一個返回值是被轉換好的型別,第二個是如果不能轉換型別,則會報錯。

比如下面的示例,我們有一個通用型別的容器,可以進行 Put(val)Get(),注意,其使用了 interface{}作泛型

//Container is a generic container, accepting anything.
type Container []interface{}

//Put adds an element to the container.
func (c *Container) Put(elem interface{}) {
    *c = append(*c, elem)
}
//Get gets an element from the container.
func (c *Container) Get() interface{} {
    elem := (*c)[0]
    *c = (*c)[1:]
    return elem
}

在使用中,我們可以這樣使用

intContainer := &Container{}
intContainer.Put(7)
intContainer.Put(42)

但是,在把資料取出來時,因為型別是 interface{} ,所以,你還要做一個轉型,如果轉型成功能才能進行後續操作(因為 interface{}太泛了,泛到什麼型別都可以放)下在是一個Type Assert的示例:

// assert that the actual type is int
elem, ok := intContainer.Get().(int)
if !ok {
    fmt.Println("Unable to read an int from intContainer")
}

fmt.Printf("assertExample: %d (%T)\n", elem, elem)
Reflection

對於反射,我們需要把上面的程式碼修改如下:

type Container struct {
    s reflect.Value
}
func NewContainer(t reflect.Type, size int) *Container {
    if size <=0  { size=64 }
    return &Container{
        s: reflect.MakeSlice(reflect.SliceOf(t), 0, size), 
    }
}
func (c *Container) Put(val interface{})  error {
    if reflect.ValueOf(val).Type() != c.s.Type().Elem() {
        return fmt.Errorf(“Put: cannot put a %T into a slice of %s", 
            val, c.s.Type().Elem()))
    }
    c.s = reflect.Append(c.s, reflect.ValueOf(val))
    return nil
}
func (c *Container) Get(refval interface{}) error {
    if reflect.ValueOf(refval).Kind() != reflect.Ptr ||
        reflect.ValueOf(refval).Elem().Type() != c.s.Type().Elem() {
        return fmt.Errorf("Get: needs *%s but got %T", c.s.Type().Elem(), refval)
    }
    reflect.ValueOf(refval).Elem().Set( c.s.Index(0) )
    c.s = c.s.Slice(1, c.s.Len())
    return nil
}

上面的程式碼並不難讀,這是完全使用 reflection的玩法,其中

  • NewContainer()會根據引數的型別初始化一個Slice
  • Put()時候,會檢查 val 是否和Slice的型別一致。
  • Get()時,我們需要用一個入參的方式,因為我們沒有辦法返回 reflect.Value 或是 interface{},不然還要做Type Assert
  • 但是有型別檢查,所以,必然會有檢查不對的道理 ,因此,需要返回 error

於是在使用上面這段程式碼的時候,會是下面這個樣子:

f1 := 3.1415926
f2 := 1.41421356237

c := NewMyContainer(reflect.TypeOf(f1), 16)

if err := c.Put(f1); err != nil {
  panic(err)
}
if err := c.Put(f2); err != nil {
  panic(err)
}

g := 0.0

if err := c.Get(&g); err != nil {
  panic(err)
}
fmt.Printf("%v (%T)\n", g, g) //3.1415926 (float64)
fmt.Println(c.s.Index(0)) //1.4142135623

我們可以看到,Type Assert是不用了,但是用反射寫出來的程式碼還是有點複雜的。那麼有沒有什麼好的方法?

它山之石

對於泛型程式設計最牛的語言 C++ 來說,這類的問題都是使用 Template來解決的。

//用<class T>來描述泛型
template <class T> 
T GetMax (T a, T b)  { 
    T result; 
    result = (a>b)? a : b; 
    return (result); 
} 
int i=5, j=6, k; 
//生成int型別的函式
k=GetMax<int>(i,j);

long l=10, m=5, n; 
//生成long型別的函式
n=GetMax<long>(l,m); 

C++的編譯器會在編譯時分析程式碼,根據不同的變數型別來自動化的生成相關型別的函式或類。C++叫模板的具體化。

這個技術是編譯時的問題,所以,不需要我們在執行時進行任何的執行的型別識別,我們的程式也會變得比較的乾淨。

那麼,我們是否可以在Go中使用C++的這種技術呢?答案是肯定的,只是Go的編譯器不幫你幹,你需要自己動手。

Go Generator

要玩 Go的程式碼生成,你需要三件事:

  1. 一個函式模板,其中設定好相應的佔位符。
  2. 一個指令碼,用於按規則來替換文字並生成新的程式碼。
  3. 一行註釋程式碼。
函式模板

我們把我們之前的示例改成模板。取名為 container.tmp.go 放在 ./template/

package PACKAGE_NAME
type GENERIC_NAMEContainer struct {
    s []GENERIC_TYPE
}
func NewGENERIC_NAMEContainer() *GENERIC_NAMEContainer {
    return &GENERIC_NAMEContainer{s: []GENERIC_TYPE{}}
}
func (c *GENERIC_NAMEContainer) Put(val GENERIC_TYPE) {
    c.s = append(c.s, val)
}
func (c *GENERIC_NAMEContainer) Get() GENERIC_TYPE {
    r := c.s[0]
    c.s = c.s[1:]
    return r
}

我們可以看到函式模板中我們有如下的佔位符:

  • PACKAGE_NAME – 包名
  • GENERIC_NAME – 名字
  • GENERIC_TYPE – 實際的型別

其它的程式碼都是一樣的。

函式生成指令碼

然後,我們有一個叫gen.sh的生成指令碼,如下所示:

#!/bin/bash

set -e

SRC_FILE=${1}
PACKAGE=${2}
TYPE=${3}
DES=${4}
#uppcase the first char
PREFIX="$(tr '[:lower:]' '[:upper:]' <<< ${TYPE:0:1})${TYPE:1}"

DES_FILE=$(echo ${TYPE}| tr '[:upper:]' '[:lower:]')_${DES}.go

sed 's/PACKAGE_NAME/'"${PACKAGE}"'/g' ${SRC_FILE} | \
    sed 's/GENERIC_TYPE/'"${TYPE}"'/g' | \
    sed 's/GENERIC_NAME/'"${PREFIX}"'/g' > ${DES_FILE}

其需要4個引數:

  • 模板原始檔
  • 包名
  • 實際需要具體化的型別
  • 用於構造目標檔名的字尾

然後其會用 sed 命令去替換我們的上面的函式模板,並生成到目標檔案中。(關於sed命令請參看本站的《sed 簡明教程》)

生成程式碼

接下來,我們只需要在程式碼中打一個特殊的註釋:

//go:generate ./gen.sh ./template/container.tmp.go gen uint32 container
func generateUint32Example() {
    var u uint32 = 42
    c := NewUint32Container()
    c.Put(u)
    v := c.Get()
    fmt.Printf("generateExample: %d (%T)\n", v, v)
}

//go:generate ./gen.sh ./template/container.tmp.go gen string container
func generateStringExample() {
    var s string = "Hello"
    c := NewStringContainer()
    c.Put(s)
    v := c.Get()
    fmt.Printf("generateExample: %s (%T)\n", v, v)
}

其中,

  • 第一個註釋是生成包名為 gen 型別為 uint32 目標檔名以 container 為字尾
  • 第二個註釋是生成包名為 gen 型別為 string 目標檔名以 container 為字尾

然後,在工程目錄中直接執行 go generate 命令,就會生成如下兩份程式碼,

一份檔名為uint32_container.go

package gen

type Uint32Container struct {
    s []uint32
}
func NewUint32Container() *Uint32Container {
    return &Uint32Container{s: []uint32{}}
}
func (c *Uint32Container) Put(val uint32) {
    c.s = append(c.s, val)
}
func (c *Uint32Container) Get() uint32 {
    r := c.s[0]
    c.s = c.s[1:]
    return r
}

另一份的檔名為 string_container.go

package gen

type StringContainer struct {
    s []string
}
func NewStringContainer() *StringContainer {
    return &StringContainer{s: []string{}}
}
func (c *StringContainer) Put(val string) {
    c.s = append(c.s, val)
}
func (c *StringContainer) Get() string {
    r := c.s[0]
    c.s = c.s[1:]
    return r
}

這兩份程式碼可以讓我們的程式碼完全編譯通過,所付出的代價就是需要多執行一步 go generate 命令。

新版Filter

現在我們再回頭看看我們之前《Go程式設計模式:Map-Reduce》中的那些個用反射整出來的例子,有了這樣的技術,我就不必在程式碼裡用那些晦澀難懂的反射來做執行時的型別檢查了。我們可以寫下很乾淨的程式碼,讓編譯器在編譯時檢查型別對不對。下面是一個Fitler的模板檔案 filter.tmp.go

package PACKAGE_NAME

type GENERIC_NAMEList []GENERIC_TYPE

type GENERIC_NAMEToBool func(*GENERIC_TYPE) bool

func (al GENERIC_NAMEList) Filter(f GENERIC_NAMEToBool) GENERIC_NAMEList {
    var ret GENERIC_NAMEList
    for _, a := range al {
        if f(&a) {
            ret = append(ret, a)
        }
    }
    return ret
}

於是我們可在需要使用這個的地方,加上相關的 go generate 的註釋

type Employee struct {
  Name     string
  Age      int
  Vacation int
  Salary   int
}

//go:generate ./gen.sh ./template/filter.tmp.go gen Employee filter
func filterEmployeeExample() {

  var list = EmployeeList{
    {"Hao", 44, 0, 8000},
    {"Bob", 34, 10, 5000},
    {"Alice", 23, 5, 9000},
    {"Jack", 26, 0, 4000},
    {"Tom", 48, 9, 7500},
  }

  var filter EmployeeList
  filter = list.Filter(func(e *Employee) bool {
    return e.Age > 40
  })

  fmt.Println("----- Employee.Age > 40 ------")
  for _, e := range filter {
    fmt.Println(e)
  }

  filter = list.Filter(func(e *Employee) bool {
    return e.Salary <= 5000
  })

  fmt.Println("----- Employee.Salary <= 5000 ------")
  for _, e := range filter {
    fmt.Println(e)
  }
}

第三方工具

我們並不需要自己手寫 gen.sh 這樣的工具類,已經有很多第三方的已經寫好的可以使用。下面是一個列表:

(全文完)本文非本人所作,轉載左耳朵耗子部落格和出處 酷 殼 – CoolShell

本作品採用《CC 協議》,轉載必須註明作者和本文連結
滴水穿石,石破天驚----馬乂

相關文章