緊接着上次說到的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的初始化函數中,好比:redis
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語言的泛型實現spa
以交換變量的函數爲例子,在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進行類型轉換的技術稱之爲類型斷言,是一種類型之間轉換的技術,與類型轉換不一樣的是,類型斷言是在接口間進行。
語法
<目標類型的值>,<布爾參數> := <表達式>.( 目標類型 ) // 安全類型斷言
<目標類型的值> := <表達式>.( 目標類型 )  //非安全類型斷言
複製代碼
若是斷言失敗,會致使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語言中的一個亮點,同時對計算機底層操做數據的本質也有所瞭解,程序的數據是在底層是一堆二進制,解析數據不是去識別數據的類型,而是程序根據變量的類型讀取對應的字節,而後採起不一樣的方式去解析它。所謂類型, 只是讀取內存的方式不一樣罷了。
原創文章,文筆有限,才疏學淺,文中如有不正之處,萬望告知。
若是本文對你有幫助,請點個贊吧,謝謝^_^
更多精彩內容,請關注我的公衆號。