Go 編程模式:Go Generation

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

本文是全系列中第6 / 9篇:Go編程模式

« 上一篇文章 下一篇文章 »

現實中的類比

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

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

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來解決的。

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微信公衆賬號和微信小程序

(轉載本站文章請註明作者和出處 酷 殼 – CoolShell ,請勿用於任何商業用途)

——=== 訪問 酷殼404頁面 尋找遺失兒童。 ===——
好爛啊 有點差 湊合看看 還不錯 很精彩 ( 1 人打了分,平均分: 5.00 )

Loading...
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章