Golang中常用的代码优化点

大家好,我是轩脉刃。

这篇想和大家聊一聊golang的常用代码写法。在golang中,如果大家不断在一线写代码,一定多多少少会有一些些代码的套路和经验。这些经验是代表你对一些问题,或者一类问题的思考和解决。处理一个问题的方法有很多,如果频繁遇到同样的场景和问题,我们会自己思考有没有更好的方式来解决,所以也就有了一些代码套路了。这里,我想和大家分享一下我个人在开发过程中看到和使用到的一些常用的代码写法。

使用pkg/error而不是官方error库

其实我们可以思考一下,我们在一个项目中使用错误机制,最核心的几个需求是什么?

1 附加信息:我们希望错误出现的时候能附带一些描述性的错误信息,甚至于这些信息是可以嵌套的。

2 附加堆栈:我们希望错误不仅仅打印出错误信息,也能打印出这个错误的堆栈信息,让我们可以知道错误的信息。

在Go的语言演进过程中,error传递的信息太少一直是被诟病的一点。我推荐在应用层使用 github.com/pkg/errors 来替换官方的error库。

假设我们有一个项目叫errdemo,他有sub1,sub2两个子包。sub1和sub2两个包都有Diff和IoDiff两个函数。

// sub2.go
package sub2
import (
"errors"
"io/ioutil"
)
func Diff(foo int, bar int) error {
return errors.New("diff error")
} // sub1.go
package sub1 import (
"errdemo/sub1/sub2"
"fmt"
"errors"
)
func Diff(foo int, bar int) error {
if foo < 0 {
return errors.New("diff error")
}
if err := sub2.Diff(foo, bar); err != nil {
return err
}
return nil
} // main.go
package main import (
"errdemo/sub1"
"fmt"
)
func main() {
err := sub1.Diff(1, 2)
fmt.Println(err)
}

在上述三段代码中,我们很不幸地将sub1.go中的Diff返回的error和sub2.go中Diff返回的error都定义为同样的字符串“diff error”。这个时候,在main.go中,我们返回的error,是无论如何也判断不出这个error是从sub1 还是 sub2 中抛出的。调试的时候会带来很大的困扰。

而使用 github.com/pkg/errors ,我们所有的代码都不需要进行修改,只需要将import地方进行对应的修改即可。

在main.go中使用fmt.Printf("%+v", err) 就能除了打印error的信息,也能将堆栈打印出来了。

// sub2.go
package sub2
import (
"github.com/pkg/errors"
"io/ioutil"
)
func Diff(foo int, bar int) error {
return errors.New("diff error")
} // sub1.go
package sub1 import (
"errdemo/sub1/sub2"
"fmt"
"github.com/pkg/errors"
)
func Diff(foo int, bar int) error {
if foo < 0 {
return errors.New("diff error")
}
if err := sub2.Diff(foo, bar); err != nil {
return err
}
return nil
} // main.go
package main import (
"errdemo/sub1"
"fmt"
)
func main() {
err := sub1.Diff(1, 2)
fmt.Printf("%+v", err)
}

看到,除了"diff error" 的错误信息之外,还将堆栈大衣拿出来了,我们能明确看到是sub2.go中第7行抛出的错误。

其实 github.com/pkg/errors 的原理也是非常简单,它利用了fmt包的一个特性:

其中在打印error之前会判断当前打印的对象是否实现了Formatter接口,这个formatter接口只有一个format方法

所以在 github.com/pkg/errors 中提供的各种初始化error方法(包括errors.New)就是封装了一个fundamental 结构,这个结构中带着error的信息和堆栈信息

它实现了Format方法。

在初始化slice的时候尽量补全cap

当我们要创建一个slice结构,并且往slice中append元素的时候,我们可能有两种写法来初始化这个slice。

方法1:

package main

import "fmt"

func main() {
arr := []int{}
arr = append(arr, 1,2,3,4, 5)
fmt.Println(arr)
}

方法2:

package main

import "fmt"

func main() {
arr := make([]int, 0, 5)
arr = append(arr, 1,2,3,4, 5)
fmt.Println(arr)
}

方法2相较于方法1,就只有一个区别:在初始化[]int slice的时候在make中设置了cap的长度,就是slice的大小。

这两种方法对应的功能和输出结果是没有任何差别的,但是实际运行的时候,方法2会比少运行了一个growslice的命令。

这个我们可以通过打印汇编码进行查看:

方法1:

方法2:

我们看到方法1中使用了growsslice方法,而方法2中是没有调用这个方法的。

这个growslice的作用就是扩充slice的容量大小。就好比是原先我们没有定制容量,系统给了我们一个能装两个鞋子的盒子,但是当我们装到第三个鞋子的时候,这个盒子就不够了,我们就要换一个盒子,而换这个盒子,我们势必还需要将原先的盒子里面的鞋子也拿出来放到新的盒子里面。所以这个growsslice的操作是一个比较复杂的操作,它的表现和复杂度会高于最基本的初始化make方法。对追求性能的程序来说,应该能避免尽量避免。

具体对growsslice函数具体实现同学有兴趣的可以参考源码src的 runtime/slice.go 。

当然,我们并不是每次都能在slice初始化的时候就能准确预估到最终的使用容量的。所以这里使用了一个“尽量”。明白是否设置slice容量的区别,我们在能预估容量的时候,请尽量使用方法2那种预估容量后的slice初始化方式。

初始化一个类的时候,如果类的构造参数较多,尽量使用Option写法

我们一定遇到需要初始化一个类的时候,大部分的时候,初始化一个类我们会使用类似下列的New方法。

package newdemo

type Foo struct {
name string
id int
age int db interface{}
} func NewFoo(name string, id int, age int, db interface{}) *Foo {
return &Foo{
name: name,
id: id,
age: age,
db: db,
}
}

我们定义一个NewFoo方法,其中存放初始化Foo结构所需要的各种字段属性。

这个写法乍看之下是没啥问题的,但是一旦Foo结构内部的字段进行了变化,增加或者减少了,那么这个初始化函数NewFoo就怎么看怎么别扭了。参数继续增加?那么所有调用方的地方也都需要进行修改了,且按照代码整洁的逻辑,参数多于5个,这个函数就很难使用了。而且,如果这5个参数都是可有可无的参数,就是有的参数可以允许不填写,有默认值,比如age这个字段,如果不填写,在后续的业务逻辑中可能没有很多影响,那么我在实际调用NewFoo的时候,age这个字段还需要传递0值。

foo := NewFoo("jianfengye", 1, 0, nil)

这种语意逻辑就不对了。

这里其实有一种更好的写法:使用Option写法来进行改造。Option写法顾命思议,将所有可选的参数作为一个可选方式,一般我们会一定一个“函数类型”来代表这个Option,然后配套将所有可选字段设计一个这个函数类型的具体实现。而在具体的使用的时候,使用可变字段的方式来控制有多少个函数类型会被执行。比如上述的代码,我们会改造为:

type Foo struct {
name string
id int
age int db interface{}
} // FooOption 代表可选参数
type FooOption func(foo *Foo) // WithName 代表Name为可选参数
func WithName(name string) FooOption {
return func(foo *Foo) {
foo.name = name
}
} // WithAge 代表age为可选参数
func WithAge(age int) FooOption {
return func(foo *Foo) {
foo.age = age
}
} // WithDB 代表db为可选参数
func WithDB(db interface{}) FooOption {
return func(foo *Foo) {
foo.db = db
}
} // NewFoo 代表初始化
func NewFoo(id int, options ...FooOption) *Foo {
foo := &Foo{
name: "default",
id: id,
age: 10,
db: nil,
}
for _, option := range options {
option(foo)
}
return foo
}

解释下上面的这段代码,我们创建了一个FooOption的函数类型,这个函数类型代表的函数结构是 func(foo *Foo) ,很简单,将foo指针传递进去,能让内部函数进行修改。

然后我们定义了三个返回了FooOption的函数:

  • WithName
  • WithAge
  • WithDB

以WithName为例,这个函数参数为string,返回值为FooOption。在返回值的FooOption中,根据参数修改了Foo指针。

// WithName 代表Name为可选参数
func WithName(name string) FooOption {
return func(foo *Foo) {
foo.name = name
}
}

顺便说一下,这种函数我们一般都以With开头,表示我这次初始化“带着”这个字段。

而最后NewFoo函数,参数我们就改造为两个部分,一个部分是“非Option”字段,就是必填字段,假设我们的Foo结构实际上只有一个必填字段id,而其他字段皆是选填的。而其他所有选填字段,我们使用一个可变参数 options 替换。

NewFoo(id int, options ...FooOption)

在具体的实现中,也变化成2个步骤:

  • 按照默认值初始化一个foo对象
  • 遍历options改造这个foo对象

按照这样改造之后,我们具体使用Foo结构的函数就变为如下样子:

// 具体使用NewFoo的函数
func Bar() {
foo := NewFoo(1, WithAge(15), WithName("foo"))
fmt.Println(foo)
}

可读性是否高了很多?New一个Foo结构,id为1,并且带着指定age为15,指定name为“foo”。

后续如果Foo多了一个可变属性,那么只需要多一个WithXXX的方法,而NewFoo函数不需要任何变化,调用方只有需要指定这个可变属性的地方增加WithXXX即可。扩展性非常好。

这种Option的写法在很多著名的库中都有使用到,gorm, go-redis等。所以我们要把这种方式熟悉起来,一旦我们在需要对一个比较复杂的类进行初始化的时候,这种方法应该是最优的方式了。

巧用大括号控制变量作用域

在golang写的过程中,你一定有过为 := 和 = 烦恼的时刻。一个变量,到写的时候,我还要记得前面是否已经定义过了,如果没有定义过,使用 := ,如果已经定义过,使用 =。

当然很多时候可能你不会犯这种错误,变量命名的比较好的话,我们是很容易记得是否前面有定义过的。但是更多时候,对于err这种通用的变量名字,你可能就不一定记得了。

这个时候,巧妙使用大括号,就能很好避免这个问题。

我举一个我之前写一个命令行工具的例子,大家知道写命令行工具,对传递的参数的解析是需要有一些逻辑的,“如果参数中有某个字段,那么解析并存储到变量中,如果没有,记录error”,这里我就使用了大括号,将每个参数的解析和处理错误的逻辑都封装起来。

代码大致如下:

var name string
var folder string
var mod string
...
{
prompt := &survey.Input{
Message: "请输入目录名称:",
}
err := survey.AskOne(prompt, &name)
if err != nil {
return err
} ...
}
{
prompt := &survey.Input{
Message: "请输入模块名称(go.mod中的module, 默认为文件夹名称):",
}
err := survey.AskOne(prompt, &mod)
if err != nil {
return err
}
...
}
{
// 获取hade的版本
client := github.NewClient(nil)
prompt := &survey.Input{
Message: "请输入版本名称(参考 https://github.com/gohade/hade/releases,默认为最新版本):",
}
err := survey.AskOne(prompt, &version)
if err != nil {
return err
}
...
}

首先我将最终解析出来的最终变量在最开始做定义,然后使用三个大括号,分别将 name, mod, version 三个变量的解析逻辑封装在里面。而在每个大括号里面,err变量的作用域就完全局限在括号中了,每次都可以直接使用 := 来创建一个新的 err并处理它,不需要额外思考这个err 变量是否前面已经创建过了。

如果你自己观察,大括号在代码语义上还有一个好处,就是归类和展示。归类的意思是,这个大括号里面的变量和逻辑是一个完整的部分,他们内部创建的变量不会泄漏到外部。这个等于等于告诉后续的阅读者,你在阅读的时候,如果对这个逻辑不感兴趣,不阅读里面的内容,而如果你感兴趣的话,可以进入里面进行阅读。基本上所有IDE都支持对大括号封装的内容进行压缩,我使用Goland,压缩后,我的命令行的主体逻辑就更清晰了。

所以使用大括号,结合IDE,你的代码的可读性能得到很大的提升。

总结

文章中总结了四个golang中常用的写法

  • 使用pkg/error而不是官方error库
  • 在初始化slice的时候尽量补全cap
  • 初始化一个类的时候,如果类的构造参数较多,尽量使用Option写法
  • 巧用大括号控制变量作用域

这几种写法和注意事项是在工作过程和阅读开源项目中的一些总结和经验,每个经验都是对应为了解决不同的问题。

虽然说golang已经对代码做了不少的规范和优化,但是好的代码和不那么好的代码是有一些差距的,这些写法优化点就是其中一部分。本文列出的只是四个点,当然还有很多类似的golang写法优化点,相信大家在工作生活中也能遇到不少,只要大家平时能多思考多总结多动手,也能积攒出属于自己的一本小小的优化手册的。

Golang中常用的代码优化点的更多相关文章

  1. golang中type常用用法

    golang中,type是非常重要的关键字,一般常见用法就是定义结构,接口等,但是type还有很多其它的用法,在学习中遇到了以下几种,这点简单总结记录下 定义结构 type Person struct ...

  2. golang中的字符串拼接

    go语言中支持的字符串拼接的方法有很多种,这里就来罗列一下 常用的字符串拼接方法 1.最常用的方法肯定是 + 连接两个字符串.这与python类似,不过由于golang中的字符串是不可变的类型,因此用 ...

  3. golang中接口interface和struct结构类的分析

    再golang中,我们要充分理解interface和struct这两种数据类型.为此,我们需要优先理解type的作用. type是golang语言中定义数据类型的唯一关键字.对于type中的匿名成员和 ...

  4. Golang 中操作 Mongo Update 的方法

    Golang 和 MongoDB 中的 ISODate 时间交互问题 2018年02月27日 11:28:43 独一无二的小个性 阅读数:357 标签: GolangMongoDB时间交互时间转换 更 ...

  5. HTTPS相关知识以及在golang中的应用

    最近简单学习了HTTPS,并在golang中实践了一下,现在把学到的知识记录下来,方便以后查看,如果有幸能帮到有需要的人就更好了,如果有错误欢迎留言指出. 一些简单的概念,可以自行百度百科 HTTPS ...

  6. 六、golang中的结构体和方法、接口

    结构体: 1.用来自定义复杂数据结构 2.struct里面可以包含多个字段(属性) 3.struct类型可以定义方法,注意和函数的区分 4.strucr类型是值类型 5.struct类型可以嵌套 6. ...

  7. 在Golang中使用C语言代码实例

    转自:http://www.jb51.net/article/56720.htm cgo 使得在 Golang 中可以使用 C 代码. Hello World 为了有一个较为直观的了解,我们来看一个简 ...

  8. golang中,slice的几个易混淆点

    slice在golang中是最常用的类型,一般可以把它作为数组使用,但是比数组要高效呀.不过,我感觉这个东西用的不好坑太多了.还是需要了解下他底层的实现 slice的结构定义 type slice s ...

  9. golang实现常用集合原理介绍

    golang本身对常用集合的封装还是比较少的,主要有数组(切片).双向链表.堆等.在工作中可能用到其他常用的集合,于是我自己对常用的集合进行了封装,并对原理做了简单介绍,代码库地址:https://g ...

随机推荐

  1. SpringBoot中请求参数 @MatrixVariable 矩阵变量

    一.矩阵变量请求格式 /users;id=1,uname=jack 二.SpringBoot开启矩阵请求 首先查看springboot源码关于矩阵部分的内容 在 WebMvcAutoConfigura ...

  2. ansible批量修改服务器密码

    看了一下网上代码大多数是ansible-playbook实现的,需要写一个脚本,或者手动传递变量进去. 以前用python tcp模块写过客户端主动上报修改密码脚本 今天写一个ansible主控客户端 ...

  3. vmstat监视内存的使用情况

    vmstat是Virtual Meomory Statistics(虚拟内存统计)的缩写,可实时动态监视操作系统的虚拟内存.进程.CPU活动. vmstat的语法 vmstat [-V] [-n] [ ...

  4. python-利用json模块处理json数据几个函数总结

    1.前言 json是一种轻量级的数据交换格式,它是JavaScript的子集,易于人阅读和编写. 前端和后端进行数据交互,其实就是JS和Python进行数据交互. 接口间或者前后端间的语言不一致,不同 ...

  5. 「BUAA OO Pre」 Pre 2总结回顾概览

    「BUAA OO Pre」 Pre 2总结回顾概览 目录 「BUAA OO Pre」 Pre 2总结回顾概览 Part 0 前言 写作背景 定位 您可以在这里期望获得 您在这里无法期望获得 对读者前置 ...

  6. Meterpreter后渗透阶段关闭防火墙与杀毒软件

  7. 【C#基础概念】程序集Assembliy

    一.      程序集定义 二.      程序集结构 通常,静态程序集可能由以下四个元素组成: 程序集清单(manifest) 类型元数据metadata和程序集元数据. 实现这些类型的 Micro ...

  8. C# CLR简介

     (一)CLR介绍 CLR是一个可以由多编程语言使用的运行时,CLR的核心功能:内存管理,程序集加载,安全性,异常处理,线程同步等等.可以被很多属于微软系列的开发语言使用. 事实上,在运行时,CLR根 ...

  9. 通过修改profile 来修改账号的过期时间

    转至:https://blog.csdn.net/xxzhaobb/article/details/80026028 查看账号的过期时间 SYS@test>select username,exp ...

  10. IntelliJ:下载第三方库

    学习自:(6条消息) IDEA中第三方软件包安装步骤_Li某人_初学者-CSDN博客 1.下载对应的软件包:以commons-logging为例 2.复制这个jar包到项目根目录下的lib目录下(li ...