1. 程式人生 > >GO語言泛型程式設計實踐

GO語言泛型程式設計實踐

緊接著上次說到的RDB檔案解析功能,資料解析步驟完成後,下一個問題就是如何儲存解析出來的資料,Redis有多種資料型別,string、hash、list、zset、set,一開始想到的方案是為每一種資料定義一種資料結構,根據不同的資料型別,將資料儲存到不同的資料結構,但是這樣的做法帶來了比較多的冗餘程式碼,以string和hash為例,一開始的程式碼是這樣的:

type Rdb struct {
    … // 其他屬性
    strObj     	map[string]string
    hashObj 	map[string]map[string]string
    …// 其他結構體定義
}

// 儲存string的函式
func (r *Rdb) saveStrObj(redisKey string, redisVal string) {
    r.strObj[redisKey] = redisVal
}

// 儲存hash的函式
func (r *Rdb) saveHashObj(redisKey string, hashField string, hashVal string) {
    item, ok := r.hashObj[redisKey]

    if !ok {
        item = make(map[string]string)
        r.hashObj[redisKey] = item
    }

    item[hashField] = hashVal
}
複製程式碼

這種方式有比較多的冗餘程式碼,比如儲存字串和儲存雜湊結構需要編寫兩套相似程式碼了,且在初始化Rdb結構體的時候,還需要初始化所有結構體之後,再傳遞到Rdb的初始化函式中,比如:

strObj := make(map[string]string)
hashObj := make(map[string]map[string]string)
rdb := &Rdb{…, strObj, hashObj}
複製程式碼

這樣的程式碼寫起來比較繁瑣,且不好維護,如果在更多資料型別的專案中,這樣的程式碼看起來簡直令人髮指。比如在這次的實踐中,redis的資料都是鍵值對,鍵的型別是固定的-字串,但是值的型別就有map、string等等各種型別,於是乎就想到是否有泛型這種技術可以協助實現想要的功能。

泛型程式設計

泛型程式設計(generic programming)是程式設計語言的一種風格或正規化。泛型允許程式設計師在強型別程式設計語言中編寫程式碼時使用一些以後才指定的型別,在例項化時作為引數指明這些型別。(摘自維基百科)

簡單地理解,泛型程式設計指的是不針對某一種特定的型別進行程式設計,一個方法不是針對了某幾種特定的資料型別,而是對大部分資料型別都有效。

比如開發一個加法功能,不只是支援整型做加法,浮點型、字串、陣列等等型別的加法,都可以實現。

在開始介紹Go語言的泛型程式設計實現之前,我想先聊一聊C語言的泛型實現,還是那句話,最喜歡C語言。

C語言的泛型實現

以交換變數的函式為例子,在C語言的實現,是通過無型別指標void *來實現,看下面的程式碼:

// 交換函式,泛型實現版本
void swap(void *p1, void *p2)
{
        size_t size = (sizeof(p1) == sizeof(p2)) ? sizeof(p1) : -1;
        char temp[size];
        memcpy(temp, p1, sizeof(p1));
        memcpy(p1, p2, sizeof(p2));
        memcpy(p2, temp, sizeof(temp));
}
複製程式碼

那麼,有了泛型版本的交換函式後,通過執行整型、浮點數和字串的交換驗證一下:

int main()
{
    int a = 1;
    int b = 42767;
    swap(&a, &b);
    
    float f1 = 1.234;
    float f2 = 2.345;
    swap(&f1, &f2);

    char str1[6] = "hello";
    char str2[10] = "world ooo";
    swap(str1, str2);

    printf("a: %d, b: %d\n", a, b);
    printf("f1: %f, f2: %f\n", f1, f2);
    printf("str1: %s, str2: %s\n", str1, str2);
}
複製程式碼

編譯執行後結果如下:

泛型版本的交換函式實現的關鍵是void *和memcpy函式,是拷貝記憶體的操作,因為資料在記憶體中都是儲存二進位制,只要操作交換的型別是一致的,那麼通過memcpy會拷貝型別佔用位元組大小的資料,從而實現同類型的資料交換。需要注意一點的是,C語言下的泛型程式設計是不安全的,比如在這個交換函式中,如果操作了不同型別資料的交換,比如short和int的交換:

short a = 1;
int b = 5;
swap(&a, &b);
複製程式碼

這個呼叫時不會報錯,且可執行的,但是交換的結果依賴於系統的位元組序,這種交換是沒有意義的,需要程式設計師去做更多的檢查和特殊判斷。

Go語言的泛型

在Go語言裡面,沒有真正的泛型,它的泛型是通過利用interface的特性來實現,因為interface也是一種型別, 只要實現了interface裡面的方法就可以歸屬為同一種類型,空的interface沒有任何方法,那麼任何型別都可以作為同一類(這一點有點類似Java的Object,所有類的超類)。

interface

interface是Go語言的一種型別,可以類比理解為Java的介面型別,在Go語言裡,interface定義了一個方法集合,只要實現了interface裡面的方法集,那就可以說是實現了該介面。Go語言的interface型別是一種靜態的資料型別,在編譯時會檢查,但是它也算是一種動態的資料型別,因為它可以用來儲存多種型別的資料。

Go語言的interface提供了一種鴨子型別(duck typing)的用法,用起來就好像是PHP中的動態資料型別一樣,但是如果企圖使用一個有其他方法宣告的interface來儲存int,編譯器還是會報錯的。

以開頭的程式碼為例,改為使用interface後,程式碼是怎麼樣呢?

定義保持Redis物件的RedisObject結構體,儲存物件的型別、佔用長度,物件值,值使用了空interface型別:

type RedisObject struct {
    objType int
    objLen  int
    objVal  interface{}
}
複製程式碼

當儲存值時,只需要將值直接賦值給RedisObject即可:

func (r *Rdb) saveStrObj(redisKey string, strVal string) {
    redisObj := NewRedisObject(RDB_TYPE_STRING, r.loadingLen, strVal)
    r.mapObj[redisKey] = redisObj
}

func (r *Rdb) saveHash(hashKey string, hashField string, hashValue string) {
    item, ok := r.mapObj[hashKey]
    if !ok {
        tmpMap := make(map[string]string)
        item = NewRedisObject(RDB_TYPE_HASH, 0, tmpMap)
        r.mapObj[hashKey] = item
    }
    
	item.objVal.(map[string]string)[hashField] = hashValue
}
複製程式碼

對於字串型別而言,它的值就是簡單的字串,使用語句r.mapObj[redisKey] = redisObj賦值即可,而雜湊物件相對複雜一些,首先檢查儲存鍵hashKey的是否為有效物件,如果不是,則需要新建一個雜湊物件,在儲存時,需要講objVal(interface型別)解析為鍵值對物件,然後再進行賦值,具體程式碼是objVal.(map[string]string),意思是將型別為interface的objVal解析為map[string][string]型別的值。

型別斷言

上面對objVal進行型別轉換的技術稱之為型別斷言,是一種型別之間轉換的技術,與型別轉換不同的是,型別斷言是在介面間進行。

語法

<目標型別的值>,<布林引數> := <表示式>.( 目標型別 ) // 安全型別斷言
<目標型別的值> := <表示式>.( 目標型別 )&emsp;&emsp;//非安全型別斷言
複製程式碼

如果斷言失敗,會導致panic的發生,為了防止過多的panic,需要在斷言之前進行一定的判斷,這就是安全與非安全斷言的區別,安全型別斷言可以獲得布林值來判斷斷言是否成功。

另外,也可以通過t.(type)得到變數的具體型別。

var t interface{}
t = functionOfSomeType()
switch t := t.(type) {
    default:
        fmt.Printf("unexpected type %T", t)       // %T prints whatever type t has
    case bool:
        fmt.Printf("boolean %t\n", t)             // t has type bool
    case int:
        fmt.Printf("integer %d\n", t)             // t has type int
    case *bool:
        fmt.Printf("pointer to boolean %t\n", *t) // t has type *bool
    case *int:
        fmt.Printf("pointer to integer %d\n", *t) // t has type *int
}
複製程式碼

總結

通過這次的小實踐,除了對泛型程式設計有了更多的瞭解,學習到了Go語言的泛型程式設計原理,認識到interface也算是Go語言中的一個亮點,同時對計算機底層操作資料的本質也有所瞭解,程式的資料是在底層是一堆二進位制,解析資料不是去識別資料的型別,而是程式根據變數的型別讀取對應的位元組,然後採取不同的方式去解析它。所謂型別, 只是讀取記憶體的方式不同罷了。

原創文章,文筆有限,才疏學淺,文中若有不正之處,萬望告知。

如果本文對你有幫助,請點個贊吧,謝謝^_^

更多精彩內容,請關注個人公眾號。