一、需求背景
在 go 语言开发中,经常会遇到类似下面需要定义多个布尔类型参数的场景:
- 日志打印方式配置参数,包含是否打印文件名、是否打印事件、是否打印函数名等
- 打开文件的方式参数,包含是否只读、是否只写、是否创建、是否追加等
实现上述需求,常规的做法是定义多个布尔类型的参数,如:
Go
// 文件操作配置类
type Config struct {
IsReadOnly bool
IsWriteOnly bool
IsCreate bool
// ... 每个标志都需要一个字段
}
上述方案的缺点:
- 占用内存多
- 不便于传参
二、实现方案
2.1 方案思路
在 go 语言中,一种更高效的实现方案是通过定义常量并使用位运算组合多个布尔选项,每个选项对应一个二进制位(bit),即一个布尔标志
Go
// 文件操作类型常量,按 2 的幂次赋值,每个类型占用一个二进制中的位
const (
O_RDONLY = 1 << iota // 1 (0b0001)
O_WRONLY // 2 (0b0010)
O_CREAT // 4 (0b0100)
)
// 文件操作配置类
type Config struct {
Flags int // 文件操作类型标识位
}
// 设置多个标志
config.Flags = O_CREAT | O_WRONLY
// 清除标识
config.Flags &^= O_CREAT // 清除 O_CREAT 标志
func Process(flags int) {
// 检查多个标志
if flags & (O_CREAT | O_WRONLY) != 0 {
// 处理逻辑...
}
// ...
}
2.2 工作原理
假设 Flags = 3(二进制 00000011),则 Flags & O_WRONLY 的结果如下:
00000011 (Flags = 3)
& 00000010 (O_WRONLY = 2)
00000010 (结果 = 2)
结果不为 0, 即 Flags & O_WRONLY != 0,说明标识位中含有 O_WRONLY
2.3 方案优点
①高效存储:使用一个整数(如uint8, uint16, uint32等)的各个二进制位来存储多个布尔标志,可以节省内存。例如,一个32位整数可以存储32个不同的标志,而如果每个标志都用一个bool变量(通常占1个字节)存储,则需要32字节。
②快速组合和检查:可以同时设置和检查多个标志。
Go
// 同时设置多个标志
config.Flags = O_CREAT | O_WRONLY
③性能高:位运算CPU原生支持的操作,因此检查标志位的速度很快,比哈希表查找、字符串比较等快得多。
④便于参数传递:由于所有标志都存储在一个整数中,因此可以方便地作为函数参数传递、存储在结构体中或序列化到文件中。
三、实际案例
3.1 GO 源码中文件操作配置的相关源码
Go
// 源码位置:src/os/file.go
// Flags to OpenFile wrapping those of the underlying system. Not all
// flags may be implemented on a given system.
const (
// Exactly one of O_RDONLY, O_WRONLY, or O_RDWR must be specified.
O_RDONLY int = syscall.O_RDONLY // open the file read-only.
O_WRONLY int = syscall.O_WRONLY // open the file write-only.
O_RDWR int = syscall.O_RDWR // open the file read-write.
// The remaining values may be or'ed in to control behavior.
O_APPEND int = syscall.O_APPEND // append data to the file when writing.
O_CREATE int = syscall.O_CREAT // create a new file if none exists.
O_EXCL int = syscall.O_EXCL // used with O_CREATE, file must not exist.
O_SYNC int = syscall.O_SYNC // open for synchronous I/O.
O_TRUNC int = syscall.O_TRUNC // truncate regular writable file when opened.
)
const (
// Invented values to support what package os expects.
O_RDONLY = 0x00000
O_WRONLY = 0x00001
O_RDWR = 0x00002
O_CREAT = 0x00040
O_EXCL = 0x00080
O_NOCTTY = 0x00100
O_TRUNC = 0x00200
O_NONBLOCK = 0x00800
O_APPEND = 0x00400
O_SYNC = 0x01000
O_ASYNC = 0x02000
O_CLOEXEC = 0x80000
o_DIRECTORY = 0x100000 // used by internal/syscall/windows
o_NOFOLLOW_ANY = 0x20000000 // used by internal/syscall/windows
o_OPEN_REPARSE = 0x40000000 // used by internal/syscall/windows
)
// 源码位置:src/syscall/syscall_windows.go
func Open(name string, flag int, perm uint32) (fd Handle, err error) {
if len(name) == 0 {
return InvalidHandle, ERROR_FILE_NOT_FOUND
}
namep, err := UTF16PtrFromString(name)
if err != nil {
return InvalidHandle, err
}
var access uint32
switch flag & (O_RDONLY | O_WRONLY | O_RDWR) {
case O_RDONLY:
access = GENERIC_READ
case O_WRONLY:
access = GENERIC_WRITE
case O_RDWR:
access = GENERIC_READ | GENERIC_WRITE
}
if flag&O_CREAT != 0 {
access |= GENERIC_WRITE
}
// ...
return h, nil
}
3.2 goframe 框架中记录日志内容的配置项相关源码
Go
// 源码位置:pkg/mod/github.com/gogf/gf@v1.16.9/os/glog/glog_logger.go
// 设置日志打印选项常量值
const (
F_ASYNC = 1 << iota // Print logging content asynchronously。
F_FILE_LONG // Print full file name and line number: /a/b/c/d.go:23.
F_FILE_SHORT // Print final file name element and line number: d.go:23. overrides F_FILE_LONG.
F_TIME_DATE // Print the date in the local time zone: 2009-01-23.
F_TIME_TIME // Print the time in the local time zone: 01:23:23.
F_TIME_MILLI // Print the time with milliseconds in the local time zone: 01:23:23.675.
F_CALLER_FN // Print Caller function name and package: main.main
F_TIME_STD = F_TIME_DATE | F_TIME_MILLI
)
// 源码位置:pkg/mod/github.com/gogf/gf@v1.16.9/os/glog/glog_logger_config.go
// Config is the configuration object for logger.
type Config struct {
Handlers []Handler `json:"-"` // Logger handlers which implement feature similar as middleware.
Writer io.Writer `json:"-"` // Customized io.Writer.
// 日志打印选项标识位
Flags int `json:"flags"` // Extra flags for logging output features.
// ...
}
func DefaultConfig() Config {
c := Config{
File: defaultFileFormat,
Flags: F_TIME_STD, // 打印内容默认设置为 标准时间格式
Level: LEVEL_ALL,
CtxKeys: []interface{}{gctx.CtxKey},
// ...
}
// ...
return c
}
// SetAsync enables/disables async logging output feature.
func (l *Logger) SetAsync(enabled bool) {
if enabled {
l.config.Flags = l.config.Flags | F_ASYNC // 设置 F_ASYNC 标识位
} else {
l.config.Flags = l.config.Flags & ^F_ASYNC // 清除 F_ASYNC 标识位
}
}
// 源码位置:pkg/mod/github.com/gogf/gf@v1.16.9/os/glog/glog_logger.go
// print prints `s` to defined writer, logging file or passed `std`.
func (l *Logger) print(ctx context.Context, level int, values ...interface{}) {
// ...
if l.config.HeaderPrint {
// Time.
timeFormat := ""
if l.config.Flags&F_TIME_DATE > 0 { // 判断日志打印选项标识位中是否含有 F_TIME_DATE
timeFormat += "2006-01-02"
}
if l.config.Flags&F_TIME_TIME > 0 {
if timeFormat != "" {
timeFormat += " "
}
timeFormat += "15:04:05"
}
if l.config.Flags&F_TIME_MILLI > 0 {
if timeFormat != "" {
timeFormat += " "
}
timeFormat += "15:04:05.000"
}
// ...
}
// ...
}