本文从设计、规范、陷阱到相关实现以例证说明并结合自己思考,详细解释了该如何写golang好代码。发表前作者已经详细校对,如本文有遗漏的错误请指出,带来的不便请谅解。
本章节按照设计模式中的核心设计原则介绍在Go语言中的实现。
类的设计尽量做到只有一个原因引起变化。 在交易的场景中,我们需要做一些交易存储、验证,我们可以声明交易的结构体,这个结构体是为了存储每笔交易。但是验证的功能我们可以拆开,这样代码更具有维护性、测试的编写也更简单方便。
type Trade struct {
TradeID int
Symbol string
Quantity float64
Price float64
}
type TradeRepository struct {
db *sql.DB
}
func (tr *TradeRepository) Save(trade *Trade) error {
_, err := tr.db.Exec("INSERT INTO trades (trade_id, symbol, quantity, price) VALUES (?, ?, ?, ?)", trade.TradeID, trade.Symbol, trade.Quantity, trade.Price)
if err != nil {
return err
}
return nil
}
type TradeValidator struct {}
func (tv *TradeValidator) Validate(trade *Trade) error {
if trade.Quantity <= 0 {
return errors.New("Trade quantity must be greater than zero")
}
if trade.Price <= 0 {
return errors.New("Trade price must be greater than zero")
}
return nil
}
对扩展开放,对修改关闭。实现常见的方法是,通过接口或者多态继承。 当我们的系统要增加期权交易的功能时,我们可以扩展接口实现,声明TradeProcessor,而不是在声明一个统一的处理器中,在里面写各种的兼容逻辑。
type TradeProcessor interface {
Process(trade *Trade) error
}
type FutureTradeProcessor struct {}
func (ftp *FutureTradeProcessor) Process(trade *Trade) error {
// process future trade
return nil
}
type OptionTradeProcessor struct {}
func (otp *OptionTradeProcessor) Process(trade *Trade) error {
// process option trade
return nil
}
所有引用父类的地方必须能透明地使用其子类的对象。 里氏替换可以简单的理解为开闭原则的一种拓展,目的是通过父子类继承部分实现子类替换父类,为了更好实现代码可扩展性。
Golang没有明确的继承机制,但是可以通过Trade接口当做面向对象对象的父类,FutureTrade是具体的实现,通过这样的机制可以实现里氏替换。当其它函数需要调用Trade时,可以完全替换为FutureTrade是完全没有任何问题的。
type Trade interface {
Process() error
}
type FutureTrade struct {
Trade
}
func (ft *FutureTrade) Process() error {
// process future trade
return nil
}
建立单一接口,不要建立臃肿庞大的接口;即接口要尽量细化,同时接口中的方法要尽量少。 Go中接口方法越少越好,这样有利于封装、隔离。
示例中,定义Trade接口,OptionTrade接口,只有当我们进行期权交易时可以实现隐含波动率。这样做到了接口的隔离,如果我们在Trade接口中定义了CalculateImpliedVolatility方法,这样无关的期货交易也需要实现CalculateImpliedVolatility方法。
type Trade interface {
Process() error
}
type OptionTrade interface {
CalculateImpliedVolatility() error
}
type FutureTrade struct {
Trade
}
func (ft *FutureTrade) Process() error {
// process future trade
return nil
}
type OptionTrade struct {
Trade
}
func (ot *OptionTrade) Process() error {
// process option trade
return nil
}
func (ot *OptionTrade) CalculateImpliedVolatility() error {
// calculate implied volatility
return nil
}
依赖接口不依赖实例。 当我们进行处理交易需要将交易信息存储时,我们只需要指定我们实际存储的操作结构实现TradeService接口,这样我们的TradeProcessor结构体可以根据实际需要指定我们存储的数据库类型。
type TradeService interface {
Save(trade *Trade) error
}
type TradeProcessor struct {
tradeService TradeService
}
func (tp *TradeProcessor) Process(trade *Trade) error {
err := tp.tradeService.Save(trade)
if err != nil {
return err
}
// process trade
return nil
}
type SqlServerTradeRepository struct {
db *sql.DB
}
func (str *SqlServerTradeRepository) Save(trade *Trade) error {
_, err := str.db.Exec("INSERT INTO trades (trade_id, symbol, quantity, price) VALUES (?, ?, ?, ?)", trade.TradeID, trade.Symbol, trade.Quantity, trade.Price)
if err != nil {
return err
}
return nil
}
type MongoDbTradeRepository struct {
session *mgo.Session
}
func (mdtr *MongoDbTradeRepository) Save(trade *Trade) error {
collection := mdtr.session.DB("trades").C("trade")
err := collection.Insert(trade)
if err != nil {
return err
}
return nil
}
Golang不是按照面具有向对象思想的语言去设计,但是面向对象中的一些设计模式的思想也可以在Golang中实现。本章参考设计模式之禅书籍中的常用设计模式,举例说明Golang中实现方式。
全局只存在一个单例,new创建的单例只存在一个。 类图(摘自设计模式之禅):
应用场景: 全局只能存在一个对象,用于生成全局的序列号、IO资源访问、全局配置信息等等。 golang实现: 并发场景下需要注意正确的实现方式:
var once sync.Once
var instance interface{}
func GetInstance() *singleton {
once.Do(func() {
instance = &amp;singleton{}
})
return instance
}
有限多列模式作为单例模式扩展,全局只存在固定的数量的模式,这种有限的多例模式。一般这种模式使用的比较多,也可以配合下文所提到的工厂模式构建,例如采用了多个链接的数据库连接池等等。 相关详细介绍: Golang 多例模式与单例模式
定义一个用于创建对象的接口,让子类决定实例化哪一个类。
类图:
示例:
package main
// Factory interface
type simpleInterest struct {
principal int
rateOfInterest int
time int
}
type compoundInterest struct {
principal int
rateOfInterest int
time int
}
// Interface
type InterestCalculator interface {
Calculate()
}
func (si *simpleInterest) Calculate() {
// logic to calculate simple interest
}
func (si *compoundInterest) Calculate() {
// logic to calculate compound interest
}
func NewCalculator(kind string) InterestCalculator {
if kind == "simple" {
return &amp;simpleInterest{}
}
return &amp;compoundInterest{}
}
func Factory_Interface() {
siCalculator := NewCalculator("simple")
siCalculator.Calculate() // Invokes simple interest calculation logic
ciCalculator := NewCalculator("compound")
ciCalculator.Calculate() // Invokes compound interest calculation logic
}
工厂模式是典型的解耦框架。高层模块只需要知道产品的抽象类。其他的实现都不用关心,符合迪米特法则,符合依赖倒置原则只依赖产品的抽象,符合里氏替换原则,使用产品子类替换产品的父类。
其他对象提供一种代理以控制对这个对象的访问。
类图:
示例:
// zkClient backend request struct.
type zkClient struct {
ServiceName string
Client client.Client
opts []client.Option
}
// NewClientProxy create new zookeeper backend request proxy,
// required parameter zookeeper name service: trpc.zookeeper.xxx.xxx.
func NewClientProxy(name string, opts ...client.Option) Client {
c := &amp;zkClient{
ServiceName: name,
Client: client.DefaultClient,
opts: opts,
}
c.opts = append(c.opts, client.WithProtocol("zookeeper"), client.WithDisableServiceRouter())
return c
}
// Get execute zookeeper get command.
func (c *zkClient) Get(ctx context.Context, path string) ([]byte, *zk.Stat, error) {
req := &amp;Request{
Path: path,
Op: OpGet{},
}
rsp := &amp;Response{}
ctx, msg := codec.WithCloneMessage(ctx)
defer codec.PutBackMessage(msg)
msg.WithClientRPCName(fmt.Sprintf("/%s/Get", c.ServiceName))
msg.WithCalleeServiceName(c.ServiceName)
msg.WithSerializationType(-1) // non-serialization
msg.WithClientReqHead(req)
msg.WithClientRspHead(rsp)
if err := c.Client.Invoke(ctx, req, rsp, c.opts...); err != nil {
return nil, nil, err
}
return rsp.Data, rsp.Stat, nil
}
代理的目的是在目标对象方法的基础上做增强。这种增强本质通常就是对目标对象方法进行拦截和过滤。
对象间一种一对多的依赖关系,使得每当一个对象改变状态,则所有依赖于它的对象都会得到通知并被自动更新。
类图:
示例:
package main
import "fmt"
type Item struct {
observerList []Observer
name string
inStock bool
}
func newItem(name string) *Item {
return &amp;Item{
name: name,
}
}
func (i *Item) updateAvailability() {
fmt.Printf("Item %s is now in stock\n", i.name)
i.inStock = true
i.notifyAll()
}
func (i *Item) register(o Observer) {
i.observerList = append(i.observerList, o)
}
func (i *Item) notifyAll() {
for _, observer := range i.observerList {
observer.update(i.name)
}
}
使用场景,事件多级触发,关联行为,跨系统消息的交换场景,级联通知情况下,运行效率和开发效率可能会有问题。
本章的规范是按照腾讯Golang代码规范标准梳理出的一些关键且容易疏忽的规范。
err := r.updateByAttaIDs(fMd5OneTime, sMd5OneTime)
if err != nil {
值拷贝是Go采取参数传值策略,因此涉及到传值时需要注意。
package main
import (
"fmt"
)
func main() {
x := [3]int{1, 2, 3}
func(arr [3]int) {
arr[0] = 7
fmt.Println(arr)
}(x)
fmt.Println(x) // 1 2 3
}
有人可能会问,我记得我传map、slice怎么不会有类似的问题?底层实现本质是指针指向了存储区域,变量代表了这个指针。
管道操作,谨记口诀:"读关闭空值,读写空阻塞,写关闭异常,关闭空、关闭已关闭异常"。个人建议管道除非在一些异步处理的场景建议使用外,其它场景不建议过多使用,有可能会影响代码的可读性。 检测管道关闭示例:
func IsClosed(ch <-chan T) bool {
select {
case <-ch:
return true
default:
}
return false
}
关闭channel的原则:我们只应该在发送方关闭,当channel只有一个发送方时。
匿名函数捕获的数据是变量的引用,在一些开发的场景中,异步调用函数的输出不符合预期的场景。
type A struct {
id int
}
func main() {
channel := make(chan A, 5)
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
for a := range channel {
wg.Add(1)
go func() {
defer wg.Done()
fmt.Println(a.id) // 输出的数字是无法确定的,输出依赖具体的调度时机。
// go vet 提示 loop variable a captured by func literal
}()
}
}()
for i := 0; i < 10; i++ {
channel <- A{id:i}
}
close(channel)
wg.Wait()
}
defer执行流程,第一步return执行将结果写入返回值,第二步执行defer会被按照先进后出的顺序执行,第三步返回当前结果。
示例1:这里返回引用,我们达到了defer修改返回值的目的,如果我们这里不是以引用返回会产生什么结果呢?这里需要留意之前说的Go里是值拷贝,如果不是引用返回这里返回的是0。
package main
import (
"fmt"
)
func main() {
fmt.Println("c return:", *(c())) // 打印结果为 c return: 2
}
func c() *int {
var i int
defer func() {
i++
fmt.Println("c defer2:", i) // 打印结果为 c defer: 2
}()
defer func() {
i++
fmt.Println("c defer1:", i) // 打印结果为 c defer: 1
}()
return i
}
示例2 :实际返回的为1,原因是我们采用了命名返回变量,返回时值的空间已预分配好了
package main
import "fmt"
func main() {
fmt.Println(test())
}
func test() (result int) {
defer func() {
result++
}()
return 0 // result = 0
// result++
}
recover函数在defer捕获异常时必须在defer函数里调用,否则是无效调用。
// 无效
func main() {
recover()
panic(1)
}
// 无效
func main() {
defer recover()
panic(1)
}
// 无效
func main() {
defer func() {
func() { recover() }()
}()
panic(1)
}
// 有效
func main() {
defer func() {
recover()
}()
panic(1)
}
sync.Mutex的拷贝,导致锁失效引发race condition。传参时我们需要通过指针进行传递。
示例:
package main
import (
"fmt"
"sync"
"time"
)
type Container struct {
sync.Mutex // <-- Added a mutex
counters map[string]int
}
func (c Container) inc(name string) {
c.Lock() // <-- Added locking of the mutex
defer c.Unlock()
c.counters[name]++
}
func main() {
c := Container{counters: map[string]int{"a": 0, "b": 0}}
doIncrement := func(name string, n int) {
for i := 0; i < n; i++ {
c.inc(name)
}
}
go doIncrement("a", 100000)
go doIncrement("a", 100000)
// Wait a bit for the goroutines to finish
time.Sleep(300 * time.Millisecond)
fmt.Println(c.counters)
}
编码工具很关键,建议集成到发布流水线里,对代码进行静态检查、代码优化。以防将有问题的代码发布至正式环境导致故障。
vet 检查go 的源码并报告可以的问题,我们可以在提交代码前、或者是在流水线配置Go代码的强制检验。
asmdecl report mismatches between assembly files and Go declarations
assign check for useless assignments
atomic check for common mistakes using the sync/atomic package
bools check for common mistakes involving boolean operators
buildtag check that +build tags are well-formed and correctly located
cgocall detect some violations of the cgo pointer passing rules
composites check for unkeyed composite literals
copylocks check for locks erroneously passed by value
httpresponse check for mistakes using HTTP responses
loopclosure check references to loop variables from within nested functions
lostcancel check cancel func returned by context.WithCancel is called
nilfunc check for useless comparisons between functions and nil
printf check consistency of Printf format strings and arguments
shift check for shifts that equal or exceed the width of the integer
slog check for incorrect arguments to log/slog functions
stdmethods check signature of methods of well-known interfaces
structtag check that struct field tags conform to reflect.StructTag.Get
tests check for common mistaken usages of tests and examples
unmarshal report passing non-pointer or non-interface values to unmarshal
unreachable check for unreachable code
unsafeptr check for invalid conversions of uintptr to unsafe.Pointer
unusedresult check for unused results of calls to some functions
goimports可以合理的整合包的分组,也可以将其纳入到项目流水线当中。
大部分的格式问题可以通过 gofmt 解决, gofmt 自动格式化代码,保证所有的 go 代码与官方推荐的格式保持一致。
CR的目的是让我们的代码更具有规范、排查出错误、代码设计的统一,从而降低不好代码所带来的误解、重复、错误等问题。无论是Contributor或者是Code Reviewer,都有职责去执行好CR的每个环节,这样我们才能写出更好更优秀的代码。
受限于篇幅原因本文还有许多内容没有给大家展开做详细介绍,文末的参考文献中引用了许多优秀的文章,可以通过这些链接进行进一步学习。最后希望读者能从本篇文章有所收货,知易行难,与君共勉。
参考文献
本文由微信公众号腾讯技术工程原创,哈喽比特收录。
文章来源:https://mp.weixin.qq.com/s/OIHqmgK4V7Y26uYoFjsCyA
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。