用 Go 开发终端接口服务--保证高性能项目的法宝
我们在《准备项目所需的 Go 类包》章节里,选择所需的类包,其实是为保证项目的高性能做好了准备。整个项目自上而下,我们都尽量避免产生性能损耗发生的情况。
最顶层路由器部分,是终端每次请求服务必经的模块,我们直接放弃了第三方的路由器,采用 Go 原生内置的 ServeMux 路由器, 并全面禁止在 URL 上携带任何参数,每个 URL 在部署之前都是已固定了。采用此方案后,我们的 chapter01 项目和现在比较流行的 RESTfull 架构,有了较大的区别。RESTfull 在路由器上使用了一定的约定,把 URL 动态参数和请求方式结合起来使用,产生灵活的路由用法。我们的方案却类似相反的,我们禁止了 URL 任何形式的参数,避免路由器复杂的字符串匹配和 Go 的反射功能,请求方式也采用了单一的 POST 方式,忽略了 PUT、DELETE、OPTIONS 一系列的请求方式,保持路由的简单性。而参数都是通过 JSON 直接传递给控制器。 正是因为我们的路由器足够简单单一,所以我们使用 ServeMux 已足够,如果采用 RESTfull 架构,恐怕需要依赖一些第三方类包,比如 :
- gorilla/mux:github.com/gorilla/mux
- httprouter:github.com/julienschmidt/httprouter
有人说 Go 语言的反射是一把双刃剑,它既可以保证灵活性,也能保证一定的性能。但 Go 还算是一门比较年轻的语言,她的反射机制还是有很大的提升空间的,她的反射尚未能做到性能上的极致,所以我们暂时尽量避免使用它,类包的选择上,我们的原则也是一样的,比如 http 中间件 martini 和 negroni,我们选了 negroni。虽然两者都出自同一个作者,但 martini 使用了大量的反射机制,整个底层架构都是基于反射构造出来的,以至于 martini 在实际项目中,很多人反馈它存在致命的性能缺陷,这点也得到了作者的共识和反思,所以才会有后来的negroni 诞生。为了吸取教训,我们项目选择了 negroni,开发过程中很明显得到了很好的体验,第一是它非常简单实用,第二是它非常接近原生的 net/http 的使用方式,可相互切换,第三是它的性能和损耗,跟 net/http 是几乎相同的,negroni 只是简要封装了 net/http,并提供更多便捷性的东西,比如中间件的引用等。
通常一个项目,访问压力都集中在数据的存取上,这和数据库软硬件配置、数据层编码直接有关,除了数据库外部设施需提供足够的抗压能力外,我们开发人员也需要重视 dao 数据层的性能,尽可能避免性能的损耗,所以我们选择了 sqlx 作为数据的存取方案。sqlx 与原生 database/sql 高度兼容,另外还做了许多扩展,相对于 database/sql 性能损耗 2%-3% 左右,性能非常优秀。
*代码清单 - sqlx 和原生 database/sql 保持兼容的部分函数*
最顶层路由器部分,是终端每次请求服务必经的模块,我们直接放弃了第三方的路由器,采用 Go 原生内置的 ServeMux 路由器, 并全面禁止在 URL 上携带任何参数,每个 URL 在部署之前都是已固定了。采用此方案后,我们的 chapter01 项目和现在比较流行的 RESTfull 架构,有了较大的区别。RESTfull 在路由器上使用了一定的约定,把 URL 动态参数和请求方式结合起来使用,产生灵活的路由用法。我们的方案却类似相反的,我们禁止了 URL 任何形式的参数,避免路由器复杂的字符串匹配和 Go 的反射功能,请求方式也采用了单一的 POST 方式,忽略了 PUT、DELETE、OPTIONS 一系列的请求方式,保持路由的简单性。而参数都是通过 JSON 直接传递给控制器。 正是因为我们的路由器足够简单单一,所以我们使用 ServeMux 已足够,如果采用 RESTfull 架构,恐怕需要依赖一些第三方类包,比如 :
- gorilla/mux:github.com/gorilla/mux
- httprouter:github.com/julienschmidt/httprouter
gorilla/mux 追求可扩展性,用法倾向于 Go 理念;httprouter 追求更高性能,独创性的嵌入式使用方式。这里我们展示这三者的简单使用方式:
import ( "net/http" "github.com/gorilla/mux" "github.com/julienschmidt/httprouter" ) // Go 原生内置 ServeMux 的使用 router := http.NewServeMux() router.HandleFunc("/api/v1/products", controller.ProductList) router.HandleFunc("/api/v1/products/detail", controller.ProductDetail) // gorilla/mux 的使用 router := mux.NewRouter() router.HandleFunc("/api/v1/products", controller.ProductList) router.HandleFunc("/api/v1/products/{id:[0-9]+}", controller.ProductDetail) // httprouter 的使用 router := httprouter.New() router.HandleFunc("/api/v1/products", controller.ProductList) router.HandleFunc("/api/v1/products/:id", controller.ProductDetail)路由器采用 Go 原生内置的 ServeMux,并禁止 URL 携带参数,直接带来的效应是:超高性能,极低内存损耗,更加符合 Go 的使用习惯。截止目前为止还没有哪个路由器的高性能和低消耗可以超过 Go 原生的 ServeMux。
有人说 Go 语言的反射是一把双刃剑,它既可以保证灵活性,也能保证一定的性能。但 Go 还算是一门比较年轻的语言,她的反射机制还是有很大的提升空间的,她的反射尚未能做到性能上的极致,所以我们暂时尽量避免使用它,类包的选择上,我们的原则也是一样的,比如 http 中间件 martini 和 negroni,我们选了 negroni。虽然两者都出自同一个作者,但 martini 使用了大量的反射机制,整个底层架构都是基于反射构造出来的,以至于 martini 在实际项目中,很多人反馈它存在致命的性能缺陷,这点也得到了作者的共识和反思,所以才会有后来的negroni 诞生。为了吸取教训,我们项目选择了 negroni,开发过程中很明显得到了很好的体验,第一是它非常简单实用,第二是它非常接近原生的 net/http 的使用方式,可相互切换,第三是它的性能和损耗,跟 net/http 是几乎相同的,negroni 只是简要封装了 net/http,并提供更多便捷性的东西,比如中间件的引用等。
通常一个项目,访问压力都集中在数据的存取上,这和数据库软硬件配置、数据层编码直接有关,除了数据库外部设施需提供足够的抗压能力外,我们开发人员也需要重视 dao 数据层的性能,尽可能避免性能的损耗,所以我们选择了 sqlx 作为数据的存取方案。sqlx 与原生 database/sql 高度兼容,另外还做了许多扩展,相对于 database/sql 性能损耗 2%-3% 左右,性能非常优秀。
*代码清单 - sqlx 和原生 database/sql 保持兼容的部分函数*
Exec(...) (sql.Result, error) // 和 database/sql 的一样 Query(...) (*sql.Rows, error) // 和 database/sql 的一样 QueryRow(...) *sql.Row // 和 database/sql 的一样
*代码清单 - sqlx 扩展的 Get、Select函数*
p := Place{} // this will pull the first place directly into p err = db.Get(&p, "SELECT * FROM place LIMIT 1") pp := []Place{} // this will pull places with telcode > 50 into the slice pp err = db.Select(&pp, "SELECT * FROM place WHERE telcode > ?", 50)
sqlx 保证高性能的同时仍然保持原生 database/sql 的使用方式,是非常好的选择,在此方案基础上再增加它灵活性,我们可以使用 sqrl 来做辅助生成 sql 语句和参数,使用起来和 orm 一样灵活和便利,并可以做到非嵌入的结构体,又不损耗性能。
*代码清单 - sqrl 使用的部分例子*
import sq "github.com/elgris/sqrl" users := sq.Select("*").From("users").Where(sq.Eq{"status": 1}) sql, args, err := users.ToSql() // 生成的 sql 语句 sql == "SELECT * FROM users WHERE status=?" // 生成的参数 args == [1] sql, args, err := sq. Insert("users").Columns("name", "age"). Values("moe", 13).Values("larry", sq.Expr("? + 5", 12)). ToSql() // 生成的 sql 语句 sql == "INSERT INTO users (name,age) VALUES (?,?),(?,? + 5)"
数据层采用的方案,是目前我觉得最满意的,它既保证了高性能的底层数据存取,又不缺乏操作上的灵活性,实体结构体又能做的很漂亮。
顶层、中间层和底层,我们都做了严格的技术考量和实施,减少项目性能损耗,保证稳定的高性能。实际上我们项目部署完成后,在服务器极端的环境下,一直长期保持服务高性能、低损耗的运行着,而且在业务交易不断增长的过程中没有出现过任何中断的问题。
《用 Go 开发终端接口服务》 目录
- 小册介绍
- 前言
- 环境搭建与开发工具选择
- Go 语言基本语法
- Go 语言编码规范
- 快速编写一个 Web 服务器
- 项目整体结构介绍
- 准备项目所需的 Go 类包
- 公共类关键函数
- 定义 model 实体层结构体
- 灵活写 dao 数据层函数
- 按需写 service 服务层逻辑
- 暴露 controller 控制层接口
- 测试已写好的接口
- 把项目部署到服务器
- 保证高性能项目的法宝
- 写在后面
1 楼: 勒克斯 发表于 2020-09-18 11:56:44 回复 TA