欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

golang 杂思

程序员文章站 2022-05-29 12:02:01
正文 这里给大家总结一些 Go player 开发小技巧. 欢迎批评和交流, 望大家喜欢. 1. 配置管理 推荐一种简单粗暴的配置管理方式 [配置 映射 内部结构]. 例如有个配置文件 config.online.yaml 我们可以在代码直接写映射规则. Go var C = struct { PI ......

正文

这里给大家总结一些 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.

如果换用 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. 

后记

-

golang 杂思