Interface 合理性验证
在编译时验证接口的符合性。这包括:
- 将实现特定接口的导出类型作为接口API 的一部分进行检查
- 实现同一接口的(导出和非导出)类型属于实现类型的集合
- 任何违反接口合理性检查的场景,都会终止编译,并通知给用户
补充:上面3条是编译器对接口的检查机制, 大体意思是错误使用接口会在编译期报错. 所以可以利用这个机制让部分问题在编译期暴露.
如果 *Handler
与 http.Handler
的接口不匹配, 那么语句 var _ http.Handler = (*Handler)(nil)
将无法编译通过.
Bad
// 如果Handler没有实现http.Handler,会在运行时报错
type Handler struct {
// ...
}
func (h *Handler) ServeHTTP(
w http.ResponseWriter,
r *http.Request,
) {
...
}
Good
type Handler struct {
// ...
}
// 用于触发编译期的接口的合理性检查机制
// 如果Handler没有实现http.Handler,会在编译期报错
var _ http.Handler = (*Handler)(nil)
func (h *Handler) ServeHTTP(
w http.ResponseWriter,
r *http.Request,
) {
// ...
}
赋值的右边应该是断言类型的零值。 对于指针类型(如 *Handler
)、切片和映射,这是 nil
; 对于结构类型,这是空结构。
type LogHandler struct {
h http.Handler
log *zap.Logger
}
var _ http.Handler = LogHandler{}
func (h LogHandler) ServeHTTP(
w http.ResponseWriter,
r *http.Request,
) {
// ...
}
Mutex 的使用方法
非导出,嵌入 mutex
type smap struct {
sync.Mutex // only for unexported types(仅适用于非导出类型)
data map[string]string
}
func newSMap() *smap {
return &smap{
data: make(map[string]string),
}
}
func (m *smap) Get(k string) string {
m.Lock()
defer m.Unlock()
return m.data[k]
}
对于导出的类型,使用专用字段
type SMap struct {
mu sync.Mutex // 对于导出类型,请使用私有锁
data map[string]string
}
func NewSMap() *SMap {
return &SMap{
data: make(map[string]string),
}
}
func (m *SMap) Get(k string) string {
m.mu.Lock()
defer m.mu.Unlock()
return m.data[k]
}
在危险的边缘拷贝在 Slices 和 Maps
slices 和 maps 包含了指向底层数据的指针,因此在需要复制它们时要特别注意。
接收 Slices 和 Maps
Bad
func (d *Driver) SetTrips(trips []Trip) {
d.trips = trips
}
trips := ...
d1.SetTrips(trips)
// 你是要修改 d1.trips 吗?
trips[0] = ...
Good
func (d *Driver) SetTrips(trips []Trip) {
d.trips = make([]Trip, len(trips))
copy(d.trips, trips)
}
trips := ...
d1.SetTrips(trips)
// 这里我们修改 trips[0],但不会影响到 d1.trips
trips[0] = ...
返回 slices 或 maps
Bad
type Stats struct {
mu sync.Mutex
counters map[string]int
}
// Snapshot 返回当前状态。
func (s *Stats) Snapshot() map[string]int {
s.mu.Lock()
defer s.mu.Unlock()
return s.counters
}
// snapshot 不再受互斥锁保护
// 因此对 snapshot 的任何访问都将受到数据竞争的影响
// 影响 stats.counters
snapshot := stats.Snapshot()
Good
type Stats struct {
mu sync.Mutex
counters map[string]int
}
func (s *Stats) Snapshot() map[string]int {
s.mu.Lock()
defer s.mu.Unlock()
result := make(map[string]int, len(s.counters))
for k, v := range s.counters {
result[k] = v
}
return result
}
// snapshot 现在是一个拷贝
snapshot := stats.Snapshot()
慎重创建 size > 1 的 Channel
默认情况下,channel 是无缓冲的,其 size 为 0。
channel 通常 size 应为 1 或是 0。
Bad
// 不推荐
c := make(chan int, 64)
一般来说,不推荐创建 size > 1 的 Channel。
当需要创建 size > 1 的 Channel ****时,需要考虑:
是什么阻止了 channel 在高负载下和阻塞写时的写入,以及当这种情况发生时系统逻辑有哪些变化。
Good
// 大小:1
c := make(chan int, 1) // 或者
// 无缓冲 channel,大小为 0
c := make(chan int)
避免在公共结构中使用类型嵌入
Go 允许 类型嵌入 作为继承和组合之间的折衷。
外部类型获取嵌入类型的方法的隐式副本、获得与类型同名的字段。
如果嵌入的类型是 public,那么外部类型获得的字段也是 public。为了保持向后兼容性,外部类型的每个未来版本都必须保留嵌入类型。
无论是使用嵌入式结构还是使用嵌入式接口,嵌入式类型都会限制类型的演化:
- 向嵌入式接口添加方法是一个破坏性的改变。
- 删除嵌入类型是一个破坏性的改变。
- 即使使用满足相同接口的替代方法替换嵌入类型,也是一个破坏性的改变。
Bad
// ConcreteList 是一个实体列表。
type ConcreteList struct {
*AbstractList
}
// AbstractList 是各种实体列表的通用实现。
type AbstractList interface {
Add(Entity)
Remove(Entity)
}
// ConcreteList 是一个实体列表。
type ConcreteList struct {
AbstractList
}
Good
// ConcreteList 是一个实体列表。
type ConcreteList struct {
list AbstractList
}
// 添加将实体添加到列表中。
func (l *ConcreteList) Add(e Entity) {
l.list.Add(e)
}
// 移除从列表中移除实体。
func (l *ConcreteList) Remove(e Entity) {
l.list.Remove(e)
}
业务中很少需要嵌入类型。 这是一种方便,可以避免编写冗长的委托方法。
尽管编写这些委托方法是乏味的,但是额外的工作隐藏了实现细节,留下了更多的更改机会,还消除了在文档中发现完整列表接口的间接性操作。
使用 init() 时的注意事项
虽然init()
顺序是明确的,但代码可以更改, 因此init()
函数之间的关系可能会使代码变得脆弱和容易出错。
应避免因依赖于其他init()
函数顺序而造成的问题。
同时应该避免一些动态操作,以免在如go test
之类的环境造成不必要的动态操作:
- 避免访问或操作全局或环境状态,如机器信息、环境变量、工作目录等
- 避免I/O,包括文件系统、数据库、网络和系统调用等
但在某些情况下,init()
可能更可取或是必要的,可能包括:
- 不能表示为单个赋值的复杂表达式。
- 可插入的钩子,如
database/sql
、编码类型注册表等。