golang学习笔记04——如何真正写好Golang代码?

前言

前面讲了一些golang微服务中常用的技术,这个章节单独拎出来,主要讨论关于如何写好golang代码。本文从设计、规范、陷阱到相关实现以例证说明并结合自己思考,详细解释如何写golang好代码。由于作者技术水平有限,如本文有遗漏的错误请指出,带来的不便请谅解。

废话不多说,我们开始进入正题。

1.Golang 实现SOLID 设计原则

每个语言都有自己的“设计模式”,golang也不例外。其中主要有以下几点需要注意:

1.1 单一职责原则

类的设计尽量做到只有一个原因引起变化。比如在交易的场景中,我们需要做一些交易存储、验证,我们可以声明交易的结构体,这个结构体是为了存储每笔交易。但是验证的功能我们可以拆开,这样代码更具有维护性、测试的编写也更简单方便。

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
}

1.2 开闭原则

对扩展开放,对修改关闭。实现常见的方法是,通过接口或者多态继承。 当我们的系统要增加交易的功能时,我们可以扩展接口实现,声明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
}

1.3 里氏替换原则

所有引用父类的地方必须能透明地使用其子类的对象。 里氏替换可以简单的理解为开闭原则的一种拓展,目的是通过父子类继承部分实现子类替换父类,为了更好实现代码可扩展性。

Golang没有明确的继承机制,但是可以通过Trade接口当做面向对象对象的父类,FutureTrade是具体的实现,通过这样的机制可以实现里氏替换。当其它函数需要调用Trade时,可以完全替换为FutureTrade是完全没有任何问题的。

type Trade interface {
    Process() error
}

type FutureTrade struct {
    Trade
}

func (ft *FutureTrade) Process() error {
    // process future trade
    return nil
}

1.4 接口隔离原则

建立单一接口,不要建立臃肿庞大的接口;即接口要尽量细化,同时接口中的方法要尽量少。 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
}

1.5 依赖倒置原则

依赖接口不依赖实例。 当我们进行处理交易需要将交易信息存储时,我们只需要指定我们实际存储的操作结构实现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
}

2.Golang实现常见设计模式

Golang不是按照面具有向对象思想的语言去设计,但是面向对象中的一些设计模式的思想也可以在Golang中实现。

2.1 单例设计模式

全局只存在一个单例,new创建的单例只存在一个。 类图:
在这里插入图片描述

  • 应用场景: 全局只能存在一个对象,用于生成全局的序列号、IO资源访问、全局配置信息等等。 golang实现: 并发场景下需要注意正确的实现方式:
var once sync.Once
var instance interface{}
func GetInstance() *singleton {
  once.Do(func() {
    instance = &amp;amp;singleton{}
  })
  return instance
}

有限多列模式作为单例模式扩展,全局只存在固定的数量的模式,这种有限的多例模式。一般这种模式使用的比较多,也可以配合下文所提到的工厂模式构建,例如采用了多个链接的数据库连接池等等。 相关详细介绍: Golang 多例模式与单例模式

2.2 工厂模式

定义一个用于创建对象的接口,让子类决定实例化哪一个类。类图:
在这里插入图片描述

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;amp;simpleInterest{}
}
return &amp;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
}

工厂模式是典型的解耦框架。高层模块只需要知道产品的抽象类。其他的实现都不用关心,符合迪米特法则,符合依赖倒置原则只依赖产品的抽象,符合里氏替换原则,使用产品子类替换产品的父类。

2.3 代理模式

其他对象提供一种代理以控制对这个对象的访问。类图:
在这里插入图片描述

type zkClient struct {
	ServiceName string
	Client      client.Client
	opts        []client.Option
}

// NewClientProxy create new zookeeper backend request proxy,
func NewClientProxy(name string, opts ...client.Option) Client {
	c := &amp
	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
	amp
	Request{
		Path: path,
		Op:   OpGet{},
	}
	rsp := &amp
	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
}

代理的目的是在目标对象方法的基础上做增强。这种增强本质通常就是对目标对象方法进行拦截和过滤。

2.4 观察者模式

对象间一种一对多的依赖关系,使得每当一个对象改变状态,则所有依赖于它的对象都会得到通知并被自动更新。类图:
在这里插入图片描述

type Item struct {
    observerList []Observer
    name         string
    inStock      bool
}

func newItem(name string) *Item {
    return &amp;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)
    }
}

使用场景,事件多级触发,关联行为,跨系统消息的交换场景,级联通知情况下,运行效率和开发效率可能会有问题。

3.Golang 易疏忽规范

3.1 声明

  • 错误使用util命名的包,不容易正常识别功能的用途,导致util包越来越臃肿。
  • slice的创建使用var arr []int,初始化切片使用 var s []string 而不是 s := make([]string),初始化,如果确定大小建议使用make初始化。
  • import . 只能用于测试文件,且必须是为了解决循环依赖,才能使用。

3.2 函数定义

  • 不要通过参数返回数据,如果需要多参数返回,建议使用struct结构化。
  • 尽量用error表示执行是否成功,而不是用bool或者int。
  • 多使用指针接收器,尽量避免使用值接收器。

3.3 函数实现

  • 除0、1、“”不要使用字面量。
  • if else 通常可以简写为 if return。
  • 尽量将 if 和变量定义应该放在一行。 错误的示例:
err := getOne(userNo)
if err != nil {
  • 不要添加没必要的空行。
  • 使用 == “” 判断字符串是否为空。
  • 通过%v打印错误信息,%v建议加:。
  • Fail Fast原则,如果出现失败应该立即返回error,如果继续处理,则属于特殊情况需要添加注释。

3.4 命名规范

  • array 和 map 的变量命名时,添加后缀 s。
  • _, xxx for xxxs 一般要求 xxx 相同。
  • 正则表达式变量名以RE结尾。
  • 不要用注释删除代码。
  • TODO格式: TODO(rtx_name): 什么时间/什么时机,如何解决。 19.导出的函数/变量的职责必须与包&文件职责高度一致。

3.5 基本类型

  • 时间类型尽量使用内置定义,如,time.Second,不要使用 int。
  • 建议所有不对外开源的工程的 module name 使用 xxxxxx/group/repo ,方便他人直接引用。
  • 应用服务接口建议有 README.md。

3.6 安全问题

  • 代码中是否存在token 密码是否加密。
  • 日志中是否输出用户敏感信息。
  • PB是否开启validation。
  • 字符串占位符,如果输入数据来自外部,建议使用%q进行安全转义。

4.Golang 编码陷阱

4.1 值拷贝

值拷贝是Go采取参数传值策略,因此涉及到传值时需要注意。

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怎么不会有类似的问题?底层实现本质是指针指向了存储区域,变量代表了这个指针。
在这里插入图片描述

4.2 管道操作

  • 管道操作,谨记口诀:“读不能空,否则阻塞;写不能空,否则错误”。
    • 个人建议管道除非在一些异步处理的场景建议使用外,其它场景不建议过多使用,有可能会影响代码的可读性。 检测管道关闭示例:
  • 关闭channel的原则:我们只应该在发送方关闭,当channel只有一个发送方时。

4.3 匿名函数变量捕获

匿名函数捕获的数据是变量的引用,在一些开发的场景中,异步调用函数的输出不符合预期的场景。

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()
}

4.4 defer执行流程

defer执行流程,第一步return执行将结果写入返回值,第二步执行defer会被按照先进后出的顺序执行,第三步返回当前结果。

示例1:这里返回引用,我们达到了defer修改返回值的目的,如果我们这里不是以引用返回会产生什么结果呢?这里需要留意之前说的Go里是值拷贝,如果不是引用返回这里返回的是0。

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,原因是我们采用了命名返回变量,返回时值的空间已预分配好了

func main() {
    fmt.Println(test())
}

func test() (result int) {
    defer func() {
        result++
    }()

    return 0 // result = 0
    // result++
}

4.5 recover正确执行方式

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)
}

4.6 sync.Mutex错误传递

sync.Mutex的拷贝,导致锁失效引发race condition。传参时我们需要通过指针进行传递。

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)
}
  • 16
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

GoppViper

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值