Go 中几种常见的编程模式

date
Jun 27, 2023
slug
golang-programming-patterns
status
Published
tags
Go
summary
Go 常见的编程模式
type
Post
模式可以理解成最佳实践,或者是约定俗成的规范或套路,熟悉一些常见模式可以方便理解项目代码。本文是参考左耳朵耗子的专栏文章做的笔记,另外也缅怀一下耗子叔。

slice 切片的数据共享与扩容迁移

切片的数据共享
切片就像是在数组上开的窗口,透过切片窗口可以看到和修改底层数组。
这段代码中,foo 和 bar 都是来源于一个底层数组的切片,在 foo 或 bar 任意一个切片做修改,都会影响到另一个切片,或者说在另一个切片也能看到其他切片做出的修改。
左图是未修改时的示例,右图是修改后的示例。
notion image
 
append 覆盖后续切片现象
给一个切片 append 元素时,如果 append 后超出原有切片的容量则会发生扩容,确切来说应该是重分配内存把原有元素迁移到新地址,而如果 append 后没超过原有容量则不发生扩容迁移。
下面代码是一个原空间不足,而发生扩容迁移例子
notion image
往 a 中 append 元素,a 原本已经是满格 32 容量了,append 之后发生扩容迁移,与 b 不再共享数据,之后在 a 上修改反映不到 b 上。
下面代码表示的是 append 后容量足够,在原位修改且因为数据共享导致覆盖掉了后面的切片
notion image
要避免这个问题发生可以在生成切片 dir1 时,指定其容量就等于初始化长度,使得一旦后续再往里 append 元素必然导致扩容迁移。 dir1 := path[:sepIndex:sepIndex]
 

使用 reflect.DeepEqual() 深度比较

深度比较可以深入复杂类型内部逐个属性比较
 

Functional Options 高阶函数实现可选配置

初始化某实例时,有多种可选的初始化参数,可以使用单独 Config 对象,也可以使用 Builder 模式。这里介绍一种通过函数式编程来初始化的做法。
Server 就是要实例化的对象
首先定义一种函数类型,接收 Server 的指针,目的是修改 Server 内部的属性
定义一组返回 Option 的方法,接收定制参数,将参数写入到 Option 中,实现高阶函数的效果。
最后定义初始化函数,函数除了必选参数外,其他可选参数通过变长参数传入,通过 for 循环启动每个 option 方法设置 server 实例,实现可选参数初始化。
实际上就是利用了高阶函数来初始化变量,间接实现保存状态的效果。
 

通过嵌入组合的方式实现控制反转 IoC

在业务逻辑代码中耦合进控制逻辑,会导致在编写业务逻辑时需要处理业务之外的事,而且控制逻辑耦合进业务中,只能适用于当前业务逻辑,无法被复用。
控制反转是一种解耦思想,将原本耦合在业务逻辑中的控制逻辑单独拆出来实现,不再让业务逻辑在处理业务的同时还要去实现控制逻辑,而是专注处理业务。
假设现在有一个 IntSet 结构,用来处理整型数据:
现在要为其增加 undo 功能
一种做法是在 IntSet 基础上完整实现一遍 undo 功能,即 IntSet 自己维护一个 do functions 切片,自己维护入栈出栈操作。
上面代码这就是所谓的控制逻辑依赖业务逻辑,或者说控制逻辑被耦合进了业务逻辑之中。undo 是控制逻辑,IntSet 是业务逻辑,IntSet 在处理原本整数数据相关的事,现在却要关心函数调用次序的事,而 undo 控制逻辑嵌入到了 IntSet 中,只能服务于 IntSet,而 undo 原本是一个通用逻辑,可能别的数据结构也会用到 undo 功能。
能否将控制逻辑抽出来,单独实现,然后大家共用一套协议,面对协议开发就好了?
现声明一个 Undo 类型并给 Undo 类型声明几个方法,专门处理 undo 操作,它实际上就是一组 function 的切片
IntSet 通过嵌入一个 Undo 结构,间接实现 undo 功能,这样控制逻辑封装在 Undo 中,业务逻辑 IntSet 只关心处理整型数据就好了。
本质还是面向接口编程和组合优于继承的思想,一方面是制定交互协议,另一方面是根据自身需求选择合适组件组合到一起。
 

通过反射实现泛型版的 Map Reduce Filter

泛型函数就是接收任意类型的 slice,处理后返回相应类型的结果。
最直接的不做类型判断的泛型 Map
加入类型判断的 Map
加入类型判断的 Reduce
加入类型判断的 Filter
总的来说就是验证 slice 类型,验证 function 类型和参数数量,验证 slice 元素类型与函数参数类型等等一切你能想到的正常代码中会报错的地方。
 

通过代码生成的方式生成出泛型代码

go genarate 命令可以识别代码中的特定格式的注释,执行命令并传递参数,可以利用这个特点预先编写命令生成代码,避免在运行时使用反射或断言增加性能消耗,还可以保持代码的简洁易读。
写一个生成通用容器的模板
针对模版做替换的脚本
在实际代码中添加注释,注释格式为 go:generate command args,然后执行 go generate,会扫描 go 代码中的注释并执行。
最后生成两个容器代码
 

高阶函数之装饰器

 

利用 channel 实现 pipeline 模式

或者单独一个 pipeline 函数,看着更合适
 

© 菜皮 2020 - 2023