天天看點

golang int64轉string_手把手教你學之golang反射(1)

github 位址​github.com

作為靜态語言,golang 稍顯笨拙,還好 go 的标準包

reflect

(反射)包彌補了這點不足,它提供了一系列強大的 API,能夠根據執行過程中對象的類型來改變程式控制流。本文将通過設計并實作一個簡易的 mysql orm 來學習它,要求讀者了解

mysql

基本知識,并且跟我一樣至少已經接觸 golang 兩到三個月。

orm 這個概念相信同學們都非常熟悉,尤其是寫過

rails

的同學,對

active_record

的強大肯定深有體會(得益于的

method_missing

define_method

方法,少寫了海量代碼),是以對 orm 我就不過多介紹了。本文要實作的 orm 隻提供基本的

CRUD

(增删改查)和

transaction

(事務)功能,核心代碼控制在 300 行左右。 如果想手把手照着寫,需要先做一些準備工作。

準備工作

在本地 mysql 裡

create database orm_db

,然後再

create

一張

user

表,結構如下:

CREATE TABLE `user` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主鍵',
  `age` smallint(10) unsigned NOT NULL DEFAULT 0 COMMENT '年齡',
  `first_name` varchar(45) NOT NULL DEFAULT '' COMMENT '姓',
  `last_name` varchar(45) NOT NULL DEFAULT '' COMMENT '名',
  `email` varchar(45) NOT NULL DEFAULT '' COMMENT '郵箱位址',
  `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '建立時間',
  `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新時間',
  PRIMARY KEY (`id`),
  KEY `idx_email` (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='使用者表';
           

同時,golang 代碼裡定義一個與之對應的

struct

type User struct {
    ID        int64     `json:"id"`         // 自增主鍵
    Age       int64     `json:"age"`        // 年齡
    FirstName string    `json:"first_name"` // 姓
    LastName  string    `json:"last_name"`  // 名
    Email     string    `json:"email"`      // 郵箱位址
    CreatedAt time.Time `json:"created_at"` // 建立時間
    UpdatedAt time.Time `json:"updated_at"` // 更新時間
}
           

與 mysql 互動需要用到一個 go 标準包和一個驅動,代碼

import

如下:

package orm

import (
    "database/sql"

    //register driver
    _ "github.com/go-sql-driver/mysql"
)
           

首先按照

database

次元建立連接配接,寫一個可以傳回 mysql 連接配接的函數:

//Connect db by dsn e.g. "user:[email protected](127.0.0.1:3306)/dbname"
func Connect(dsn string) (*sql.DB, error) {
    conn, err := sql.Open("mysql", dsn)
    if err != nil {
        return nil, err
    }
    //設定連接配接池
    conn.SetMaxOpenConns(100)
    conn.SetMaxIdleConns(10)
    conn.SetConnMaxLifetime(10 * time.Minute)
    return conn, conn.Ping()
}
           

設計一個

struct

用于實作 orm(go 不是面向對象的語言,沒有

class

):

//Query will build a sql
type Query struct {
    db      *sql.DB
    table   string
}
           

最後将通過

Query

拼接出 sql 語句與 mysql 互動,是以寫一個綁定函數:

//Table bind db and table
func Table(db *sql.DB, tableName string) func() *Query {
    return func() *Query {
        return &Query{
            db:    db,
            table: tableName,
        }
    }
}
           

傳回值是一個閉包函數,這樣使用時直接調用這個閉包函數就可以擷取一個綁定好的 database 和 table 的

Query

,比如現在有資料庫

orm_db

user

表:

//全局變量ormDB和users
ormDB, _ := Connect("user:[email protected](127.0.0.1:3306)/orm_db")
users := Table(ormDB, "user")
//調用
users().Insert(...)
           

準備工作到此完成,下面進入正題。

Insert 方法

首先分析一下标準

insert

語句:

insert into user (first_name, last_name) values ('Tom', 'Cat'), ('Tom', 'Cruise')
           

把 sql 語句中變化的部分抽象出來,其實就是

key

(字段)和

value

(值),那麼 orm 裡的

Insert

方法原型就有了,如下,參數是 struct 或者 map,因為它們都能提供鍵值對:

//Insert in can be *User, []*User, map[string]interface{}
func (q *Query) Insert(in interface{}) (int64, error) {
    var keys, values []string
    v := reflect.ValueOf(in)
    //剝離指針
    for v.Kind() == reflect.Ptr {
        v = v.Elem()
    }
    switch v.Kind() {
    case reflect.Struct:
        keys, values = sKV(v)
    case reflect.Map:
        keys, values = mKV(v)
    case reflect.Slice:
        for i := 0; i < v.Len(); i++ {
            //Kind是切片時,可以用Index()方法周遊
            sv := v.Index(i)
            for sv.Kind() == reflect.Ptr || sv.Kind() == reflect.Interface {
                sv = sv.Elem()
            }
            //切片元素不是struct或者指針,報錯
            if sv.Kind() != reflect.Struct {
                return 0, errors.New("method Insert error: in slice is not structs")
            }
            //keys隻儲存一次就行,因為後面的都一樣了
            if len(keys) == 0 {
                keys, values = sKV(sv)
                continue
            }
            _, val := sKV(sv)
            values = append(values, val...)
        }
    default:
        return 0, errors.New("method Insert error: type error")
    }
    //todo
    //...
}
           

參數

in

可以是一個

User

(前文定義好的結構體)執行個體的指針(或者指針集合),也可以是一個 map,這兩個結構都可以提供鍵值對,我們通過反射來分析它的

類型

,然後根據類型執行相應的邏輯。 reflect 包裡的有兩個重要結構

Type

Value

,Type 是一個接口,定義了所有類型相關的 api,reflect 裡的

*rtype

實作了這個接口,通過 reflect.TypeOf 函數可以擷取任何傳入值的

*rtype

。Value 是一個 struct,通過 reflect.ValueOf 函數擷取,它在

*rtype

的基礎上又封裝了傳入值的 unsafe.Pointer 類型的

位址

以及這個值的

中繼資料

。 在 Type 和 Value 之上還有一個

Kind

,它代表傳入值的

原始類型

,比如:

type myInt int
var i myInt
t := reflect.TypeOf(i)
k := t.Kind()
           

t 是 myInt,而 k 是 int,Type 和 Kind 是不同的,這一點要注意區分。 如果 Type 的 Kind 是指針、接口、切片、map 等複合類型,可以調用 Elem()方法擷取基類型。 如果 Value 的 Kind 是指針、接口,可以調用 Elem()方法擷取實際值。 Value 上還定義了一個

Interface()

方法,它是 ValueOf()方法的反操作。 有了上面這些反射方法,我們可以封裝一個

sKV()

函數,它專門處理 struct 類型的值,擷取 key(取 json tag)和 value:

func sKV(v reflect.Value) ([]string, []string) {
    var keys, values []string
    t := v.Type()
    for n := 0; n < t.NumField(); n++ {
        tf := t.Field(n)
        vf := v.Field(n)
        //忽略非導出字段
        if tf.Anonymous {
            continue
        }
        //忽略無效、零值字段
        if !vf.IsValid() || reflect.DeepEqual(vf.Interface(), reflect.Zero(vf.Type()).Interface()) {
            continue
        }
        for vf.Type().Kind() == reflect.Ptr {
            vf = vf.Elem()
        }
        //有時候根據需求會組合struct,這裡處理下,支援擷取嵌套的struct tag和value
        //如果字段值是time類型之外的struct,遞歸擷取keys和values
        if vf.Kind() == reflect.Struct && tf.Type.Name() != "Time" {
            cKeys, cValues := sKV(vf)
            keys = append(keys, cKeys...)
            values = append(values, cValues...)
            continue
        }
        //根據字段的json tag擷取key,忽略無tag字段
        key := strings.Split(tf.Tag.Get("json"), ",")[0]
        if key == "" {
            continue
        }
        value := format(vf)
        if value != "" {
            keys = append(keys, key)
            values = append(values, value)
        }
    }
    return keys, values
}
           

sKV()

函數裡需要格式化字元串,那麼定義一個

format()

函數。

time.Time

類型怎麼轉化成各種資料庫的時間類型我有點拿不準,是以需要對比時間類型的值時,一律用 unxi 時間戳,感覺比較省事不會出錯:

func format(v reflect.Value) string {
    //斷言出time類型直接轉unix時間戳
    if t, ok := v.Interface().(time.Time); ok {
        return fmt.Sprintf("FROM_UNIXTIME(%d)", t.Unix())
    }
    switch v.Kind() {
    case reflect.String:
        return fmt.Sprintf(`'%s'`, v.Interface())
    case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int:
        return fmt.Sprintf(`%d`, v.Interface())
    case reflect.Float32, reflect.Float64:
        return fmt.Sprintf(`%f`, v.Interface())
    //如果是切片類型,周遊元素,遞歸格式化成"(, , , )"形式
    case reflect.Slice:
        var values []string
        for i := 0; i < v.Len(); i++ {
            values = append(values, format(v.Index(i)))
        }
        return fmt.Sprintf(`(%s)`, strings.Join(values, ","))
    //接口類型剝一層遞歸
    case reflect.Interface:
        return format(v.Elem())
    }
    return ""
}
           

map 類型處理起來和 struct 不同,是以我們再定義一個

mKV()

函數,目的和 sKV()一樣,都是擷取鍵值對:

func mKV(v reflect.Value) ([]string, []string) {
    var keys, values []string
    //擷取map的key組成的切片
    mapKeys := v.MapKeys()
    for _, key := range mapKeys {
        value := format(v.MapIndex(key))
        if value != "" {
            values = append(values, value)
            keys = append(keys, key.Interface().(string))
        }
    }
    return keys, values
}
           

利用 sKV()和 mKV()函數取到鍵值對後,就得到了 insert 語句中的變化部分,補全 Insert()方法的

todo

部分:

//Insert in can be User, *User, []User, []*User, map[string]interface{}
func (q *Query) Insert(in interface{}) (int64, error) {
    //already done
    kl := len(keys)
    vl := len(values)
    if kl == 0 || vl == 0 {
        return 0, errors.New("method Insert error: no data")
    }
    var insertValue string
    //插入多條記錄時需要用","拼接一下values
    if kl < vl {
        var tmpValues []string
        for kl <= vl {
            if kl%(len(keys)) == 0 {
                tmpValues = append(tmpValues, fmt.Sprintf("(%s)", strings.Join(values[kl-len(keys):kl], ",")))
            }
            kl++
        }
        insertValue = strings.Join(tmpValues, ",")
    } else {
        insertValue = fmt.Sprintf("(%s)", strings.Join(values, ","))
    }
    query := fmt.Sprintf(`insert into %s (%s) values %s`, q.table, strings.Join(keys, ","), insertValue)
    log.Printf("insert sql: %s", query)
    st, err := q.DB.Prepare(query)
    if err != nil {
        return 0, err
    }
    result, err := st.Exec()
    if err != nil {
        return 0, err
    }
    return result.LastInsertId()
}
           

原理很簡單,利用反射分析參數,取鍵值對,然後拼接 sql 語句,再通過 mysql 驅動入庫。 調用示例:

user1 := &User{
    Age:       30,
    FirstName: "Tom",
    LastName:  "Cat",
}
user2 := User{
    Age:       30,
    FirstName: "Tom",
    LastName:  "Curise",
}
user3 := User{
    Age:       30,
    FirstName: "Tom",
    LastName:  "Hanks",
}
user4 := map[string]interface{}{
    "age":        30,
    "first_name": "Tom",
    "last_name":  "Zzy",
}
users().Insert([]interface{}{user1, user2})
users().Insert(user3)
users().Insert(user4)
           

增删改查的

部分到此完成,因為查詢語句非常複雜多變,是以有了資料後,先進行

西二旗搬磚仔:手把手教你學之golang反射(2)​zhuanlan.zhihu.com

golang int64轉string_手把手教你學之golang反射(1)

繼續閱讀