golang 雜思

正文

這裏給你們總結一些 Go player 開發小技巧. 歡迎批評和交流, 望你們喜歡.

1. 配置管理

推薦一種簡單粗暴的配置管理方式 [配置 映射 內部結構]. 
例若有個配置文件 config.online.yaml
# 常量
pi: 3.14159265358

# 即表示網址屬性值
uri: https://www.google.com

# 即表示 server.host 屬性的值
server:
    host: http://www.youtube.com

# 數組, 即表示 server 爲 [a, b, c]
host:
    - 172.217.161.132
    - 216.58.220.206
    - 8.8.8.8
咱們能夠在代碼直接寫映射規則.
var C = struct {
    PI float64 `yaml:"pi"`
    URL `yaml:"uri"`
    Server struct {
        Host `yaml:"host"`
    } `yaml:"server"`
    Host []string `yaml:"host"`
}{}
程序啓動時候, 經過 func init() {} 初始化. 使用時只須要使用 config.C.PI, 
是否是很方便. 再補充一個更好的配置文件協議 toml.

tomlhtml

若是換用 toml 配置(config.online.toml)的內容更好理解
pi  = 3.14159265358
uri = https://www.google.com

[server]
host = http://www.youtube.com

host = [
    "172.217.161.132",
    "216.58.220.206",
    "8.8.8.8"
]
真的, 看見 toml 的第一眼就喜歡上了. 好舒服 ~ 讓人以爲好舒服, 就應該這樣的雕琢.

2. fmt.Sprintf

有時候咱們看見這樣的代碼片斷
if len(v) > 0 {
        errMessage = fmt.Sprintf(t, v...)
    } else {
        errMessage = t
    }
其實對於 fmt.Sprintf 是多此一舉, 能夠直接
errMessage = fmt.Sprintf(t, v...)

3. 乒乓結構

(說的很輕巧, 推薦有所思考) 普通的讀寫操做代碼有
var lastMd5sLock   = sync.RWMutex{}
var lastMd5s map[string]map[string]string

func ClearCache() {
    lastMd5sLock.Lock()
    defer lastMd5sLock.Unlock()
    lastMd5s = make(map[string]map[string]string)
}
這裏分享個幹掉 RWMutex 的無鎖技巧. 運用新舊兩份配置, 使用空間換時間技巧.
var nowIndex uint32
var dataConf [2]map[string]map[string]string

// ClearCache conf map clear
func ClearCache() {
    lastConf := make(map[string]map[string]string)
    lastIndex := 1 - atomic.LoadUint32(&nowIndex)
    dataConf[lastIndex] = lastConf
    atomic.StoreUint32(&nowIndex, lastIndex)
}
咱們來說解代碼, 原先的 ClearCache 那段代碼加了寫鎖. 寫鎖可以作到兩件事情
1' 臨界狀況有人在單條讀取, 清除會讓其等待
2' 臨界狀況有人在單條寫入, 清除會讓其等待

假如咱們不對 ClearCache 加寫鎖, 採用原子交換技巧.

因爲此刻內存中存在 dataConf[1] new 和 dataConf[0] old 兩個配置對象.
臨界狀況指讀取和寫入都在進行, 但此刻觸發清除操做
1' 臨界狀況有人在單條讀取, 寫方將 nowIndex 指向了 1, 但讀取的仍然是 dataConf[0] old
2' 臨界狀況有人在單條寫入, 寫入的仍是 dataConf[0] old

上面行爲和加鎖後產出結果同樣. 於是清除函數, 能夠用原子技巧替代鎖.

經過這個原理, 咱們作配置更新或者同步時候能夠採用下面步驟獲取最優性能
1' 解析配置, 生成一個新的配置對象 map 填充到 dataConf[lastIndex]
2' 新的配置對象讀取索引原子賦值給當前的讀取索引 lastIndex = lastIndex

爲何說這麼多呢. 由於鎖是一個咱們須要慎重對待的點.

而對於那些不加鎖, 也沒有原子操做的乒乓結構, 能夠自行利用 go -race 分析. 
其讀寫一致性沒法保證(讀寫撕裂, 髒讀), 並且沒法保證編譯器不作優化. 有時候那種寫法線上竟然
不出問題, 可是一旦出了問題就是莫名其妙, 很難追查. 這裏就不表那種錯誤的乒乓寫法, 來污染同
行代碼.

4. 配置庫解析

提及配置庫, 我看有的同窗經過這樣代碼作配置文件內容提取和分割.
content, err := ioutil.ReadFile(file)
if err != nil {
    // ...
}

for _, line := range strings.Split(string(content), "\n") {
    // ...
}
上面代碼存在兩個潛在問題
1' 大文件內存會炸
2' 不一樣平臺換行符不統一 mac \r linux \n windows \r\n

一個穩健漂亮代碼模板推薦用下面
fin, err := os.Open(path)
    if err != nil {
        // Error ...
    }
    defer fin.Close()

    // create a Reader
    var buf bytes.Buffer
    reader := bufio.NewReader(fin)
    for {
        line, isPrefix, err := reader.ReadLine()
        if len(line) > 0 {
            buf.Write(line)
            if !isPrefix {
                // 完整的行而且不帶 \r\n, 運行獨立的業務代碼 ~
                lins := string(buf.Bytes())

                buf.Reset()
            }
        }

        if err != nil {
            break
        }
    }
強烈推薦!! 各位保存這個套路模板.

5. Go MD5

這種高頻出現代碼片斷, 強烈建議統一封裝. 保證出口統一. 這裏帶你們封裝兩個.
// MD5String md5 hash
func MD5String(str string) string {
    data := md5.Sum([]byte(str))
    return fmt.Sprintf("%x", data)
}
// MD5File 文件 MD5
func MD5File(path string) (string, error) {
    fin, err := os.Open(path)
    if err != nil {
        return "", err
    }
    defer fin.Close()

    m := md5.New()

    // 文件讀取解析, 並設置緩衝緩衝大小
    const blockSize = 4096
    buf := make([]byte, blockSize)
    for {
        n, err := fin.Read(buf)
        if err != nil {
            return "", err
        }

        // buf[:0] == []
        m.Write(buf[:n])

        if n < blockSize {
            break
        }
    }

    return fmt.Sprintf("%x", m.Sum(nil)), nil
}
不要問爲何那麼麻煩, 由於那叫專業. 小點遊戲包片斷 4G, 你來個 md5 試試

6. github.com/spf13/cast

不要用這個庫, 性能全是呵呵呵.
Go 中類型轉換代碼其實很健全(實在沒辦法能夠自行寫反射), 舉例以下
// ParseBool returns the boolean value represented by the string.
// It accepts 1, t, T, TRUE, true, True, 0, f, F, FALSE, false, False.
// Any other value returns an error.
func ParseBool(str string) (bool, error)

// ParseFloat converts the string s to a floating-point number
// with the precision specified by bitSize: 32 for float32, or 64 for float64.
// When bitSize=32, the result still has type float64, but it will be
// convertible to float32 without changing its value.
func ParseFloat(s string, bitSize int) (float64, error)

// ParseInt interprets a string s in the given base (0, 2 to 36) and
// bit size (0 to 64) and returns the corresponding value i.
func ParseInt(s string, base int, bitSize int) (i int64, err error)
能夠看看 github.com/spf13/cast 源碼設計水平線 ~
// ToBoolE casts an empty interface to a bool.
func ToBoolE(i interface{}) (bool, error) {

    i = indirect(i)

    switch b := i.(type) {
    case bool:
        return b, nil
    case nil:
        return false, nil
    case int:
        if i.(int) != 0 {
            return true, nil
        }
        return false, nil
    case string:
        return strconv.ParseBool(i.(string))
    default:
        return false, fmt.Errorf("Unable to Cast %#v to bool", i)
    }
}
首先看到的是 b := i.(type) 斷言, 觸發一次反射. 
隨後可能到 case int 分支 i.(int) or case string 分支 i.(string) 觸發二次反射. 
很是浪費. 由於 b 就是反射後的值了. 猜想做者當時喝了點酒.

其實做者寫的函數還有個商榷地方在於調用 indirect 函數找到指針指向的原始類型.
// From html/template/content.go
// Copyright 2011 The Go Authors. All rights reserved.
// indirect returns the value, after dereferencing as many times
// as necessary to reach the base type (or nil).
func indirect(a interface{}) interface{} {
    if a == nil {
        return nil
    }
    if t := reflect.TypeOf(a); t.Kind() != reflect.Ptr {
        // Avoid creating a reflect.Value if it's not a pointer.
        return a
    }
    v := reflect.ValueOf(a)
    for v.Kind() == reflect.Ptr && !v.IsNil() {
        v = v.Elem()
    }
    return v.Interface()
}
這個函數引自 Go 標準庫 html/template/content.go 中. 
用於將非 nil 指針轉成指向類型. 提升代碼兼容性. 
這是隱藏的反射. 我的以爲用在這裏很浪費 ~

Go 開發中反射是低效的保證. 反射性能損耗在
    1' 運行時安全檢查
    2' 調用底層的類型轉換函數
不到非用不可, 請不要用反射. 和鎖同樣都須要慎重

外部庫太多容易形成版本管理複雜, 並且生產力和效率也不必定提高. 例如上面的包 ~

... ...

其實咱們的協議層, 是太愛客戶端了. int, number, string 全都兼容. 
把本來 json 協議要作的事情, 拋給了運行時問題. 這方面, 強烈推薦 json 協議語義明確. 
方便咱們後端作參數健壯性過濾. 避免部分 CC 攻擊.

7. MySQL 相關討論

在數據業務設計時. 順帶同你們交流下 MySQL 設計過程當中小技巧(模板)
create table [table_nane] (
    id bigint unsigned NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT '物理主鍵',
    update_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新時間',
    create_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '建立時間',
    [delete_time timestamp DEFAULT NULL COMMENT '刪除時間']

    [template]

) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
問題 1: 物理主鍵 id 爲何是 unsigned ?
回答  : 
    1' 性能更好, unsigned 不涉及 反碼和補碼 轉碼消耗
    2' 表示物理主鍵更廣 [-2^63, 2^63-1] -> [0, 2^64-1]
    3' mysql 優化會更好. select * from * where id < 250;
        原先是 select * from * where -2^63 <= id and id < 250;
        如今是 select * from * where 0 <= id and id < 250;

問題 2: 爲何用 timestamp 表示時間?
回答  :
    1' timestamp 和 int 同樣都是 4字節. 用它表示時間戳更友好.
    2' 業務再也不關心時間的建立和更新相關業務代碼. 省心, 省代碼

問題 3: 爲何是 utf8mb4 而不是 utf8? 
回答  : 
    mysql 的 utf8 不是標準的 utf8. unicode 編碼定義是使用 1-6 字節表示一個字符. 
    但 mysql utf8 只使用了 1-3 字節表示一個字符, 那麼遇到 4字節編碼以上的字符(表情符號)
    會發生意外. 因此 mysql 在 5.5 以後版本推出了 utf8mb4 編碼, 徹底兼容之前的 utf8.

後記

渴望光榮 - https://music.163.com/#/song?id=31421394mysql

Golang

相關文章
相關標籤/搜索