mysql在哪里写代码_[译] 如何写好 Go 代码

原文:https://scene-si.org/2018/07/24/writing-great-go-code/

我写了多年的 Go 微服务,并在写完两本关于 (API Foundations in Go 和 12 Factor Applications with Docker and Go) 主题的书之后,有了一些关于如何写好 Go 代码的想法

但首先,我想给阅读这篇文章的读者解释一点。好代码是主观的。你可能对于好代码这一点,有完全不同的想法,而我们可能只对其中一部分意见一致。另一方面,我们可能都没有错,只是我们从两个角度出发,从而选择了不同的方式解决工程问题,并不意味着意见不一致的不是好代码。

包很重要,你可能会反对 - 但是如果你在用 Go 写微服务,你可以将所有代码放在一个包中。当然,下面也有一些反对的观点:

  1. 将定义的类型放入单独的包中

  2. 维护与传输无关的服务层

  3. 在服务层之外,维护一个数据存储(repository)层

我们可以计算一下,一个微服务包的最小数量是 1。如果你有一个大型的微服务,它拥有 websocket 和 http 网关,你最终可能需要 5 个包(类型,数据存储,服务,websocket 和 http 包)。

简单的微服务实际上并不关心从数据存储层(repository),或者从传输层(websocket,http)抽离业务逻辑。你可以写简单的代码,转换数据然后响应,也是可以运行的。但是,添加更多的包可以解决一些问题。例如,如果你熟悉  SOLID 原则,S 代表单一职责。如果我们拆分成包,这些包就可以是单一职责的。

  • types - 声明一些结构,可能还有一些结构的别名等

  • repository - 数据存储层,用来处理存储和读取结构

  • service - 服务层,包装存储层的具体业务逻辑实现

  • http, websocket, … - 传输层,用来调用服务层

当然,根据你使用的情况,还可以进一步细分,例如,可以使用types/requesttypes/response 来更好的分隔一些结构。这样就可以拥有 request.Messageresponse.Message 而不是 MessageRequestMessageResponse。如果一开始就像这样拆分开,可能会更有意义。

但是,为了强调最初的观点 - 如果你只用了这些声明包中的一部分,也没什么影响。像 Docker 这样的大型项目在 server 包下只使用了 types  包,这是它真正需要的。它使用的其他包(像 errors 包),可能是第三方包。

同样需要注意的是,在一个包中,共享正在处理的结构和函数会很容易。如果你有相互依赖的结构,将它们拆分为两个或多个不同的包可能会导致钻石依赖问题。解决方案也很显然 - 将代码放到一块儿,或者将所有代码放在一个包中。

到底选哪一个呢?两种方法都行。如果我非要按规则来的话,将其拆分更多的包可能会使添加新代码变得麻烦。因为你可能要修改这些包才能添加单个 API 调用。如果不是很清楚如何布局,那么在包之间跳转可能会带来一些认知上的开销。在很多情况下,如果项目只有一两个包,阅读代码会更容易。

你肯定也不想要太多的小包。

错误

如果是描述性的 Errors 可能是开发人员检查生产问题的唯一工具。这就是为什么我们要优雅地处理错误,要么将它们一直传递到应有程序的某一层,如果错误无法处理,该层就接收错误并记录下来,这一点非常重要。以下是标准库错误类型缺少的一些特性:

  • 错误信息不含堆栈跟踪

  • 不能堆积错误

  • errors 是预实例化的

但是,通过使用第三方错误包(我最喜欢的是pkg/Errors.))可以帮助解决这些问题。也有其他的第三方错误包,但是这个是 Dave Cheney (Go 语言大神)编写的,它在错误处理的方式在一定程度上是一种标准。他的文章 Don’t just check errors, handle them gracefully 是推荐必读的。

错误的堆栈跟踪

pkg/errors 包在调用 errors.New 时,会将上下文(堆栈跟踪)添加到新建的错误中。

users_test.go:34: testing error Hello world
        github.com/crusttech/crust/rbac_test.TestUsers
                /go/src/github.com/crusttech/crust/rbac/users_test.go:34
        testing.tRunner
                /usr/local/go/src/testing/testing.go:777
        runtime.goexit
                /usr/local/go/src/runtime/asm_amd64.s:2361

考虑到完整的错误信息是 "Hello world",使用 fmt.Printf 带有%+v 的参数或者类似的方式来打印少量的上下文 - 对于查找错误的而言,是一件很棒的事。你可以确切知道是哪里创建了错误(关键字)。当然,当涉及到标准库时,errors 包和本地 error 类型 - 不提供堆栈跟踪。但是,使用 pkg/errors 可以很容易地添加一个。例如:

resp, err := u.Client.Post(fmt.Sprintf(resourcesCreate, resourceID), body)
if err != nil {
        return errors.Wrap(err, "request failed")
}

在上面这个例子中,pkg/errors包将上下文添加 err 中,加的错误消息("request failed") 和堆栈跟踪都会抛出来。通过调用 errors.Wrap 来添加堆栈跟踪,所以你可以精准追踪到此行的错误。

堆积错误

你的文件系统,数据库,或者其他可能抛出相对不太好描述的错误。例如,Mysql 可能会抛出这种强制错误:

ERROR 1146 (42S02): Table 'test.no_such_table' doesn't exist

这不是很好处理。然而,你可以使用 errors.Wrap(err,"database aseError") 在上面堆积新的错误。这样,就可以更好地处理 "databaseError" 等。pkg/errors 包将在 causer 接口后面保留实际的错误信息。

type causer interface {
       Cause() error
}

这样,错误堆积在一起,不会丢失任何上下文。附带说一下,mysql 错误是一个类型错误,其背后包含的不仅仅是错误字符串的信息。这意味着它有可能被处理的更好:

if driverErr, ok := err.(*mysql.MySQLError); ok {
    if driverErr.Number == mysqlerr.ER_ACCESS_DENIED_ERROR {
        // Handle the permission-denied error
    }
}

此例子来自于 this stackoverflow thread。

错误预实例化

究竟什么是错误(error)呢?非常简单,错误需要实现下面的接口:

type error interface {
    Error() string
}

net/http 的例子中,这个包将几种错误类型暴露为变量,如文档所示。在这里添加堆栈跟踪是不可能的(Go不允许对全局 var 声明可执行代码,只能进行类型声明)。其次,如果标准库将堆栈跟踪添加到错误中 - 它不会指向返回错误的位置,而是指向声明变量(全局变量)的位置。

这意味着,你仍然需要在后面的代码中强制调用类似于  return errors.WithStack(ErrNotSupported) 的代码。这也不是很痛苦,但不幸的是,你不能只导入 pkg/errors ,就让所有现有的错误都带有堆栈跟踪。如果你还没有使用 errors.New 来实例化你的错误,那么它需要一些手动调用。

日志

接下来是日志,或者更恰当的说,结构化日志。这里提供了许多软件包,类似于 sirupsen/logrus 或我最喜欢的APEX/LOG。这些包也支持将日志发送到远程的机器或者服务,我们可以用工具来监控这些日志。

当谈到标准日志包时,我不常看到的一个选项是创建一个自定义 logger,并将 log.LShorfilelog.LUTC 等标志传递给它,以再次获得一点上下文,这能让你的工作变轻松 - 尤其在处理不同时区的服务器时。

const (
        Ldate         = 1 <iota     // the date in the local time zone: 2009/01/23
        Ltime                         // the time in the local time zone: 01:23:23
        Lmicroseconds                 // microsecond resolution: 01:23:23.123123.  assumes Ltime.
        Llongfile                     // full file name and line number: /a/b/c/d.go:23
        Lshortfile                    // final file name element and line number: d.go:23. overrides Llongfile
        LUTC                          // if Ldate or Ltime is set, use UTC rather than the local time zone
        LstdFlags     = Ldate | Ltime // initial values for the standard logger
)

即使你没有创建自定义 logger,你也可以使用 SetFlags 来修改默认 logger。(playground link):

package main

import (
    "log"
)

func main() {
    log.SetFlags(log.LstdFlags | log.Lshortfile)
    log.Println("Hello, playground")
}

结果如下:

2009/11/10 23:00:00 main.go:9: Hello, playground

你不想知道你在哪里打印了日志吗?这会让跟踪代码变得更容易。

接口

如果你正在写接口并命名接口中的参数,请考虑以下的代码片段:

type Mover interface {
    Move(context.Context, string, string) error
}

你知道这里的参数代表什么吗?只需要在接口中使用命名参数就可以让它很清晰。

type Mover interface {
    Move(context.Context, source string, destination string)
}

我还经常看到一些使用一个具体类型作为返回值的接口。一种未得到充分利用的做法是,根据一些已知的结构体或接口参数,以某种方式声明接口,然后在接收器中填充结果。这可能是 Go 中最强大的接口之一。

type Filler interface {
    Fill(r *http.Request) error
}

func (s *YourStruct) Fill(r *http.Request) error {
    // here you write your code...
}

更可能的是,一个或多个结构体可以实现该接口。如下:

type RequestParser interface {
    Parse(r *http.Request) (*types.ServiceRequest, error)
}

此接口返回具体类型(而不是接口)。通常,这样的代码会使你代码库中的接口变得杂乱无章,因为每个接口只有一个实现,并且在你的应用包结构之外会变得不可用。

小帖士

如果你希望在编译时确保你的结构体符合并完全实现一个接口(或多个接口),你可以这么做:

var _ io.Reader = &YourStruct{}
var _ fmt.Stringer = &YourStruct{}

如果你缺少这些接口所需的某些函数,编译器就会报错。字符 _ 表示丢弃变量,所以没有副作用,编译器完全优化了这些代码,会忽视这些被丢弃的行。

空接口

与上面的观点相比,这可能是更有争议的观点 - 但是我觉得使用 interface{} 有时非常有效。在 HTTP API 响应的例子中,最后一步通常是 json 编码,它接收一个接口参数:

func (enc *Encoder) Encode(v interface{}) error

因此,完全可以避免将 API 响应设置成具体类型。我并不建议对所有情况都这么处理,但是在某些情况下,可以在 API 中完全忽略响应的具体类型,或者至少说明具体类型声明的意义。脑海中浮现的一个例子是使用匿名结构体。

body := struct {
    Username string   `json:"username"`
    Roles    []string `json:"roles,omitempty"`
}{username, roles}

首先,不使用 interface{} 的话,无法从函数里返回这种结构体。显然,json 编码器可以接受任何类型的内容,因此,按传递空接口(对我来说)是完全有意义的。虽然趋势是声明具体类型,但有时候你可能不需要一层中间层。对于包含某些逻辑并可能返回各种形式的匿名结构体的函数,空接口也很合适。

更正:匿名结构体不是不可能返回,只是做起来很麻烦:playground

  • 感谢 @Ikearens at Discord Gophers #golang channel

第二个用例是数据库驱动的 API 设计,我之前写过一些有关内容,我想指出的是,实现一个完全由数据库驱动的 API 是非常可能的。这也意味着添加和修改字段是仅仅在数据库中完成的,而不会以 ORM 的形式添加额外的间接层。显然,你仍然需要声明类型才能在数据库中插入数据,但是从数据库中读取数据可以省略声明。

// getThread fetches comments by data, order by ID
func (api *API) getThread(params *CommentListThread) (comments []interface{}, err error) {
    // calculate pagination parameters
    start := params.PageNumber * params.PageSize
    length := params.PageSize
    query := fmt.Sprintf("select * from comments where news_id=? and self_id=? and visible=1 and deleted=0 order by id %s limit %d, %d", params.Order, start, length)
    err = api.db.Select(&comments, query, params.NewsID, params.SelfID)
    return
}

同样,你的应用程序可能充当反向代理,或者只使用无模式(schema-less)的数据库存储。在这些情况下,目的只是传递数据。

一个大警告(这是你需要输入结构体的地方)是,修改 Go 中的接口值并不是一件容易的事。你必须将它们强制转换为各种内容,如 map、slice 或结构体,以便可以在访问这些返回的数据。如果你不能保持结构体一成不变,而只是将它从 DB(或其他后端服务)传递到 JSON 编码器(会涉及到断言成具体类型),那么显然这个模式不适合你。这种情况下不应该存在这样的空接口代码。也就是说,当你不想了解任何关于载荷的信息时,空接口就是你需要的。

代码生成

尽可能使用代码生成。如果你想生成用于测试的 mock,如果你想生成 proc/GRPC 代码,或者你可能拥有的任何类型的代码生成,可以直接生成代码并提交。在发生冲突的情况下,可以随时将其丢弃,然后重新生成。

唯一可能的例外是提交类似于 public_html 文件夹的内容,其中包含你将使用 rakyll/statik 打包的内容。如果有人想告诉我,由 gomock 生成的代码在每次提交时都会以兆字节的数据污染 GIT 历史记录?不会的。

结束语

关于 Go 的最佳实践和最差实践的另一本值得注意的好书应该是Idiomatic Go。
如果你不熟悉的话,可以阅读一下 - 它是与本文很好的搭配。

我想在这里引用Jeff Atwood post - The Best Code is No Code At All文章的一句话,这是一句令人难忘的结束语:

如果你真的喜欢写代码,你会非常喜欢尽可能少地写代码。

但是,一定要编写那些单元测试。完结

6b15f9dfea4864312f05d2a30bb9ba20.png

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/532138.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

学画画软件app推荐_路由器管理软件哪个好?6款路由器管理APP推荐_软件评测

无限网络应用越来越广泛&#xff0c;由此应运而生了许多可以蹭网的软件&#xff0c;家里的网速突然变慢了&#xff0c;也许就是隔壁的小哥哥小姐姐在蹭网络&#xff0c;那么如何避免被蹭网&#xff1f;今天小编给各位小伙伴推荐几款路由器管理软件&#xff0c;发现网络变慢了&a…

各个图标的意思_冬奥体育图标设计团队负责人林存真:每一个图标要画100稿以上...

林存真中央美术学院设计学院副院长、北京冬奥组委文化活动部形象景观艺术总监、北京冬奥会体育图标设计团队负责人。12月31日晚&#xff0c;北京2022年冬奥会和冬残奥会体育图标正式发布。在体育图标发布前夕&#xff0c;新京报记者采访了北京冬奥会体育图标设计团队的负责人&a…

delphi制作上下开幕效果_2019中超联赛揭幕战在深圳举行 现场开幕式亮点多多

3月2日晚&#xff0c;2019中超联赛开幕式在深圳大运中心体育场举行。随着中国足协党委书记杜兆才正式宣布2019年中超联赛开幕&#xff0c;以“超精彩”为口号的2019新赛季中超联赛正式打响。本赛季中超联赛是出台“注资帽”、“薪酬帽”、“奖金帽”、“转会帽”之后的第一个赛…

ae正在发生崩溃_AE错误:正在发生崩溃的解决方法,原创问题解决方案

AE在使用过程中,可能会出现一些问题。有的问题是莫名其妙出现。例如刚刚还在正常使用AE软件,下一次再打开的时候就会出现问题。今天给大家说的是如何去解决after effects错误:正在发生崩溃这个问题 。该问题所提供的解决方法为实际操作过,并成功解决。所以才写出来给大家提…

计算机二级公共,计算机二级公共基础知识

计算机二级公共基础知识计算机二级考试包括计算机基础知识。虽然分值不高但是我们还是要把握好每一分。下面百分网小编整理了相关计算机二级公共基础知识&#xff0c;希望大家喜欢。计算机二级公共基础知识1.1栈和队列1、栈及其基本运算栈是限定在一端进行插入与删除运算的线性…

echarts map 点击地图区域变色_绘制炫酷的地图,不只是pyecharts.map!

导读&#xff1a;地图可视化是一种非常直观的数据分析结果展现形式&#xff0c;python有很多可视化库可以实现&#xff0c;pyecharts就是很多python爱好者喜爱的实现地图可视化方法之一。不可否认&#xff0c;pyecharts绘制的地图实现方便、图形美观而且支持交互&#xff0c;但…

金蝶kis专业版公网访问_金蝶KIS云专业版—【账务处理】进阶操作101问

对KIS专业版【账务处理】模块日常操作之外的各种问题进行回答&#xff0c;帮助老师们快速进阶此模块的操作&#xff0c;提高软件的便利性。1.专业版资产负债表如何移动表页位置&#xff1f;【操作步骤】 1、单击【报表与分析】-【资产负债表】&#xff1b;2、单击左上角菜单栏【…

es集群搭建_滴滴Elasticsearch 集群跨版本升级与平台重构之路

前不久&#xff0c;滴滴ES团队将维护的30多个ES集群&#xff0c;3500多个ES节点&#xff0c;8PB的数据&#xff0c;从2.3.3跨大版本无缝升级到6.6.1。在对用户查询写入基本零影响和改动的前提下&#xff0c;解决了ES跨大版本协议不兼容、文件格式不兼容、mapping不兼容等难题&a…

电子工程可以报考二建_非工程类专业也能报考二建吗?

非工程类专业也能报考二建吗&#xff1f;2020年非工程类专业能考二级建造师的省份汇总整理&#xff01;2020年二级建造师考试报名公告陆续公布中&#xff0c;目前江西、陕西、江苏三省公布了报名时间&#xff0c;其他省份报名时间暂时未确定。四川省已经受疫情影响推迟五月的考…

计算机考试中英文打字题,计算机信息技术(五笔及中英文打字测试试题)

计算机信息技术(五笔及中英文打字测试试题) (14页)本资源提供全文预览&#xff0c;点击全文预览即可全文预览,如果喜欢文档就下载吧&#xff0c;查找使用更方便哦&#xff01;14.9 积分第一章基本知识习题答案一、填空题1. 计算机信息高新技术考试划分为五、四、三、二、_ 5个等…

pil python 安装_20行Python代码给微信头像戴帽子

作者 | Leauky&#xff0c;北理工硕士在读&#xff0c;非CS专业的Python爱好者。朋友圈里微信官方要求戴圣诞帽的活动曾经火爆一时&#xff0c;有些会玩的小伙伴都悄咪咪地用美图秀秀一类的 app 给自己头像 p 一顶&#xff0c;然后可高兴地表示“哎呀好神奇hhhh”&#xff0c;呆…

arcgis 属性表 汇总_Arcgis中遥感影像地理配准、矢量化与地图制作

目的&#xff1a;将遥感图像进行地理配准、矢量化&#xff0c;并且制作地图。要求&#xff1a;对的遥感图像进行地理配准&#xff1b;矢量化建筑物、绿地、道路、水体等主要地物要素&#xff1b;对各类地物要素进行符号化设置并对其名称进行标注&#xff1b;添加指北针、比例尺…

怎么查看计算机的系统内存大小,Windows10系统怎么查看电脑内存大小

很多用户在升级到windows10系统之后&#xff0c;因为很多界面和操作都跟之前的Windows系统不一样&#xff0c;所以很多操作都不知道要如何下手&#xff0c;比如想要查看电脑内存大小的时候却不知道要怎么操作&#xff0c;其实方法很简单&#xff0c;下面给大家介绍一下Windows1…

java类初始化顺序_《To Be a Better Javaer》-- Java 基础篇 vol.2:面向对象

Java是面向对象的高级编程语言&#xff0c;面向对象的特征如下&#xff1a;面向对象具有抽象、封装、继承、多态等特性&#xff1b;面向对象可以将复杂的业务逻辑简单化&#xff0c;增强代码复用性&#xff1b;面向对象是一种常见的思想&#xff0c;比较符合人们的思考习惯。面…

计算机开机时间停在上次关机,怎么在电脑开机的时候查看上次关机前的操作

满意答案wencai242013.09.05采纳率&#xff1a;51% 等级&#xff1a;12已帮助&#xff1a;12606人1.看计算机在哪天运行过~运行了多久!(系统安装在c盘)找到c:\windows\SchedLgU.txt文件 里面有你自这个系统产生以来曾经工作过的时间&#xff0c;包括哪天开了机 开机时间 关机…

php 递归实现无限极分类和排序_PHP实现选择排序

这次说说选择排序。 选择排序&#xff08;Selection sort&#xff09;是一种简单直观的排序算法。它的工作原理如下。首先在未排序序列中找到最小&#xff08;大&#xff09;元素&#xff0c;存放到排序序列的起始位置&#xff0c;然后&#xff0c;再从剩余未排序元素中继续寻找…

idea for循环快捷键_IDEA骚技巧,编码速度至少快一倍

IDEA是目前市场上最好用的IDE&#xff0c;公认的&#xff01;前几年eclipse在市场上非常流行&#xff0c;因此大多数人都习惯了eclipse的一些快捷键。近年来&#xff0c;随着IDEA的兴起&#xff0c;很多人都放弃了exlipse&#xff0c;进而选择了IDEA&#xff0c;但是有些人习惯…

从物联网到 3D 打印:硬件相关的开源项目概览 | 开源专题 No.52

arendst/Tasmota Stars: 20.4k License: GPL-3.0 Tasmota 是一款为 ESP8266 和 ESP32 设备提供的替代固件&#xff0c;具有易于配置的 webUI、OTA 更新、定时器或规则驱动的自动化功能以及通过 MQTT、HTTP、串口或 KNX 进行完全本地控制。该项目主要特点包括&#xff1a; 支持…

helm安装postgres_Helm 入门介绍 Kubernetes 上的包管理软件

这篇文章介绍一下云原生应用在 Kubernetes 上安装时&#xff0c;经常会用到的一个重要工具&#xff0c;Helm。Helm 是 Kubernetes 的包管理软件。提到包管理软件&#xff0c;很多人都不陌生。Maven、Gradle、pip、RubyGems 和 npm 都是包管理软件。作为一个包管理软件&#xff…

flutter 分割线_Flutter 底部弹框 showModalBottomSheet 使用Demo

题记—— 执剑天涯&#xff0c;从你的点滴积累开始&#xff0c;所及之处&#xff0c;必精益求精。Flutter是谷歌推出的最新的移动开发框架。【x1】微信公众号的每日提醒 随时随记 每日积累 随心而过 文章底部扫码关注【x2】各种系列的视频教程 免费开源 关注 你不会迷路【x3】系…