这里给你们总结一些 Go player 开发小技巧. 欢迎批评和交流, 望你们喜欢.
推荐一种简单粗暴的配置管理方式 [配置 映射 内部结构]. 例若有个配置文件 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 的第一眼就喜欢上了. 好舒服 ~ 让人以为好舒服, 就应该这样的雕琢.
有时候咱们看见这样的代码片断
if len(v) > 0 { errMessage = fmt.Sprintf(t, v...) } else { errMessage = t }
其实对于 fmt.Sprintf 是多此一举, 能够直接
errMessage = fmt.Sprintf(t, v...)
(说的很轻巧, 推荐有所思考) 普通的读写操做代码有
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 分析. 其读写一致性没法保证(读写撕裂, 脏读), 并且没法保证编译器不作优化. 有时候那种写法线上竟然 不出问题, 可是一旦出了问题就是莫名其妙, 很难追查. 这里就不表那种错误的乒乓写法, 来污染同 行代码.
提及配置库, 我看有的同窗经过这样代码作配置文件内容提取和分割.
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 } }
强烈推荐!! 各位保存这个套路模板.
这种高频出现代码片断, 强烈建议统一封装. 保证出口统一. 这里带你们封装两个.
// 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 试试
不要用这个库, 性能全是呵呵呵. 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 攻击.
在数据业务设计时. 顺带同你们交流下 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