书接上文,进入代码细节。
socket.go 服务接口
socket.go是程序的入口,从main()函数开始:
......
//Socket侦听参数
const tcp_url = "0.0.0.0:21231"
func main() {
//初始化数据库连接
db := service.InitDB()
//TCP listen
listener, err := net.Listen("tcp", tcp_url)
if err != nil {
log.Fatal("listen error:", err)
}
log.Println("TCP server start..., listen: ", tcp_url)
//tcp accept loop
for {
socket, err := listener.Accept()
if err != nil {
log.Println("tcp accept error:", err)
continue
}
go process(socket, db)
}
}
......
在main函数中首先初始化数据库连接,代码service.InitDB()中service是包名,InitDB()函数在db_mysql.go文件中实现。
然后创建TCP服务,在for循环中通过Accept()侦听每一次客户端的请求,并把请求socket连同db交给process()函数去处理,这里注意是通过go关键字调用process()的,在Go语言中表示这里启用了“协程”。协程也称之为轻量级进程,所谓轻量是因为调度发生在用户空间,不像传统进程或线程是由内核调度。协程的优点是并发数远大于进程和线程,这也是Go语言的靓点之一。
process()函数完成了以下几件事:
- 接收socket传过来的Json数据
- 解析数据结构中的业务方法名称和业务数据
- 反射调用业务方法
- Json封装业务方法返回的数据
- 将数据通过socket返还
func process(socket net.Conn, db service.DB) {
//关闭socket
defer socket.Close()
//捕获goroutine可能抛出的panic
defer func() {
if err := recover(); err != nil {
log.Println("goroutine error: ", err)
}
}()
......
process()函数一开始执行了两个defer,defer是延迟操作的意思,defer socket.Close()表示在process()函数运行结束时自动调用socket关闭,defer func()中recover()相当于Java中的try...catch,用来捕获process()及业务方法可能出现的异常,否则业务方法抛出的错误会导致整个服务进程的崩溃。
defer这种设计,初看要比try...catch方便,但实际使用确达不到try...catch细腻的控制粒度,这也是Go被很多人吐槽的原因之一。
......
//读取req json长度
buf := make([]byte, 12) //长度行总是12字节
readN, readErr := socket.Read(buf)
if readErr != nil {
log.Println("socket read json error", readErr)
}
bufStr := string(buf[:readN])
lenStr := strings.Trim(bufStr, "\r\n") //去除行尾的回车换行
lenStr = strings.Trim(lenStr, " ") //去除行左的空格
jsonLen, _ := strconv.Atoi(lenStr) //获取json的字节长度
//读取req json
jsonBuf := make([]byte, jsonLen)
_, readErr = socket.Read(jsonBuf)
if readErr != nil {
log.Println("socket read json error", readErr)
return
}
......
Module Proxy 中间件将Json数据转发给后端Go服务时,在Json数据前加了12字节的单元,用来表示Json数据的长度。这是因为如果Json数据很大的话TCP会拆包传输,socket接收数据就不容易判断数据是否已完成,这也是socket编程常用的模式:先发一个长度单元,再发送数据。
同样的,socket将数据返回时,也是在数据前加了12字节的长度单元。
......
//解析req json
var jsonMap map[string]interface{}
if err := json.Unmarshal([]byte(jsonBuf), &jsonMap); err != nil {
log.Println("Request json error: ", err)
return
}
methodName := jsonMap["head"].(map[string]interface{})["method"].(string) //方法名称
reqMap := jsonMap["data"].(map[string]interface{}) //方法参数
log.Println("method: ", methodName)
log.Println("reqMap: ", reqMap)
//反射方法名称
method := reflect.ValueOf(db).MethodByName(methodName)
if !method.IsValid() {
log.Println("Method not found!")
return
}
//反射方法参数
args := []reflect.Value{reflect.ValueOf(reqMap)}
//反射方法调用
reflectRsp := method.Call(args)
//反射方法返回值
rsp := reflectRsp[0].Interface()
//返回值转json
rspBytes, err := json.Marshal(rsp)
if err != nil {
log.Println("json.Marshal error: ", err)
return
}
......
客户端请求的Json数据结构分两部分:head和data。head中封装了业务方法名称,data是传递给业务方法的数据参数。上面代码解析出这两部分后,通过反射机制调用对应的业务方法。
因此框架中的所有业务方法需要满足以下规范:
- 方法能够被反射获取(框架中定义为struct DB的实现方法)
- 方法必须要接收唯一的参数,参数符合Json格式
- 方法有唯一的返回值(Go语言允许有多返回值,也不能无返回值),返回值符合Json格式
看到这里,大家应该明白了为什么文章一开始说本框架解决了Go语言Web架构的青涩问题,因为是“绕道”了啊!什么GET、POST、DELETE等都扔到一边去,name=value、?、&这种数据结构怎比得上Json,从此一切和HTTP无关,超简单,Perfect!
db_mysql.go 数据源
package service
import (
"database/sql"
"log"
)
//数据库连接参数
const db_name = "mysql"
const db_url = "video:video@tcp(127.0.0.1:3306)/golang_test?parseTime=true"
//数据库连接struct
type DB struct {
Conn *sql.DB //数据库连接对象
}
//初始化数据库连接
func InitDB() DB {
var db DB
var err error
//初始化数据库连接
db.Conn, err = sql.Open(db_name, db_url)
if err != nil {
log.Fatal(err)
}
log.Println("db open OK")
//测试数据库连接
err = db.Conn.Ping()
if err != nil {
log.Fatal(err)
}
log.Println("db connection OK")
return db
}
db_mysql.go实现mysql数据源,逻辑简单无需多讲,要强调的是:
- type DB struct : 所有业务方法都是这个struct的方法,DB有唯一的属性Conn的类型是*sql.DB,是Go语言的数据库规范。Go语言不能称之为面向对象语言,但也区分函数和方法,区别如下:
//这是函数
func foo() string{
return "hello"
}
//这是方法, DB是truct类型
func (db DB) foo() string{
return "hello"
}
service_dept.go 增删改查
service_dept.go 包含了部门相关的业务方法,我们先看添加部门:
///添加部门
func (db DB) AddDept(reqMap map[string]interface{}) bool {
//从json中提取参数
name := reqMap["name"].(string)
parentId := int(reqMap["parentId"].(float64))
var houseNo interface{}
if reqMap["house_no"] != nil {
houseNo = reqMap["house_no"].(string)
}
var tel interface{}
if reqMap["tel"] != nil {
tel = reqMap["tel"].(string)
}
//SQL预处理
sqlstr := "INSERT INTO dept(name,parent_id,house_no,tel)VALUES(?,?,?,?)"
stmt, err := db.Conn.Prepare(sqlstr)
if err != nil {
log.Println("SQL Prepare error: ", err)
return false
}
//SQL执行
_, err = stmt.Exec(name, parentId, houseNo, tel)
if err != nil {
log.Println("SQL Exec error: ", err)
return false
}
//返回
return true
}
在Go语言中方法名称首字母大写表示方法是public的,在此package外部可以访问,反之首字母小写只能在package内部访问。
参数reqMap的类型map[string]interface{}表示这是一个key-val的结构,相当于其他语言的hashmap类型。方括号中的string表示key的类型,值类型interface{}表示可以是任何类型,相当于Java或C#中的Object。所以 map[string]interface{} 写法在Java中就是Map<String, Object>,写法诡异吧,这也是Go语言不支持泛型被吐槽的地方。
方法开始部分是从参数reqMap中获取部门的各个属性值,值得说的是houseNo和tel,因为这两个数据库对应字段可以为空,客户端发来的Json中可能没有它俩,所以这里获取时先定义类型为interface{},当他们不为空时才获取值,这样在insert时既可以插入string值也可以插入null。
数据连接Conn是通过方法func (db DB) 带进来的,insert、update、delete操作过程是一样的,先Prepare()预处理,再Exec()执行。Exec(name, parentId, houseNo, tel)会根据name、parentId等他们的数据类型自动转换到相应的数据库字段类型,比Java JDBC的stmt.setInt(),stmt.setString()用起来方便一些。
SQL执行出错时方法返回false,成功返回true,true和false本身就是Json类型,process()简单的封装长度单元后就通过socket返给了客户端。
修改部门、删除部门过程相似,无需多讲,下面说说查询操作:
//获得部门信息
func (db DB) GetDept(reqMap map[string]interface{}) map[string]interface{} {
//从json中提取参数
queryId := int(reqMap["id"].(float64))
//查询SQL
sqlstr := "SELECT * FROM dept WHERE id=?"
rows, err := db.Conn.Query(sqlstr, queryId)
if err != nil {
log.Println("Query error: ", err)
rspEmpty := make(map[string]interface{})
return rspEmpty
}
defer rows.Close() //方法返回时rows将关闭
//查询结果集
var (
id int
name string
parentId int
houseNo sql.NullString
tel sql.NullString
)
if !rows.Next() {
log.Println("Can't get the dept, id=", id)
rspEmpty := make(map[string]interface{})
return rspEmpty
}
if err = rows.Scan(&id, &name, &parentId, &houseNo, &tel); err != nil {
log.Println("rows scan err: ", err)
rspEmpty := make(map[string]interface{})
return rspEmpty
}
//返回
rsp := make(map[string]interface{})
rsp["id"] = id
rsp["name"] = name
rsp["parentId"] = parentId
if houseNo.Valid {
rsp["houseNo"] = houseNo.String
} else {
rsp["houseNo"] = nil
}
if tel.Valid {
rsp["tel"] = tel.String
} else {
rsp["tel"] = nil
}
return rsp
}
查询操作函数是Query(还提供了其他的一些查询方法,这里不介绍了),Query()返回的rows表示结果集,和JDBC的ResultSet类似需要next、next的一行行从数据库获取数据。因为是通过ID获取部门,结果只可能有一行或压根没有,所以这里用 !rows.Next()而没有用for。
rows必须关闭否则这次用的连接Conn不能被释放,对应延时语句 defer rows.Close()。回想下前面的添加部门方法好像没有释放Conn的操作啊,因为不需要,stmt在执行Exec()后内部会自动关闭Conn。
数据获取不到或异常时返回空的map,获取成功时将各字段封装进map返回,map在process()中被转换成Json对象,并添加长度单元后返给客户端。
注意houseNo和tel两个允许空的字段的值转换,变量类型必须定义为sql.Nullstring,表示可能为null的字符串,转换时也必须先通过 houseNo.Valid 来判断是否为null,不为空才能取值,否则会发送错误。相对应的还有 sql.Nullint,sql.Nulltime等类型。
获得组织树方法,通过递归调用将部门组装成上下级的树形结构:
//获取组织树
func (db DB) Deptree(_nil interface{}) map[string]interface{} {
//递归获取组织树
list := deptree(db, 0)
//返回包装为map
m := make(map[string]interface{})
m["id"] = 0
m["name"] = "组织树"
m["sub"] = list
return m
}
//递归获取组织树
func deptree(db DB, queryParentId int) []map[string]interface{} {
//返回集合
var list []map[string]interface{}
......
注意 Deptree() 和 deptree() 的区别:Deptree()是公开的方法,deptree()是私有的函数。
Deptree()方法不需要输入参数,但基于当前框架规范必须要有一个参数,因此这里的 _nil interface{} 表示空的参数,客户端也必须发送一个空Json对象 {} 。
deptree()函数的返回值类型 []map[string]interface{},初学Go语言的童鞋可能看着有点懵,无他,就是 List<Map<String, Object>>,带泛型的语言看着就是那么徐福。
service_transaction.go 中的事务操作
service_transaction.go演示了数据库事务的用法,一起来看看添加一个临时部门和其下人员的业务方法(虽然在业务上并无事务的必要性):
//添加临时部门及其人员
func (db DB) AddTempDeptAndPeoples(reqMap map[string]interface{}) bool {
......
//开启事务
tx, err := db.Conn.Begin()
if err != nil {
log.Println("transaction error: ", err)
return false
}
defer tx.Rollback() //回滚
//插入部门
......
res, err := stmt.Exec(deptName, parentDeptId, houseNo, tel)
......
//获得刚插入的部门id
deptId, err := res.LastInsertId()
if err != nil {
log.Println("res.LastInsertId error: ", err)
return false
}
//人员插入 SQL预处理
sql := "INSERT INTO employee(dept_id,name,position,phone,office,gender,birthday,bak)VALUES(?,?,?,?,?,?,?,?)"
stmt, err = tx.Prepare(sql)
if err != nil {
log.Println("SQL Prepare error: ", err)
return false
}
//插入部门中所有人员
for _, item := range peopleList {
m := item.(map[string]interface{})
//人员各字段
......
//SQL执行
_, err = stmt.Exec(deptId, name, position, phone, office, gender, birthday, bak)
if err != nil {
log.Println("SQL Exec error: ", err)
return false
}
}
//事务提交
if err = tx.Commit(); err != nil {
log.Println("tx.Commit() error: ", err)
return false
}
return true
}
通过 Conn.Begin() 获得事务对象 tx,并立即定义 defer tx.Rollback() 在方法非正常结束时回滚,而正常时在return前调用 tx.commit() 进行事务提交。
前面的示例在调用Exec()时我们忽略了方法的第一个返回值,这里我们通过这个值res得到刚插入的部门id。
因为人员有多个,添加人员预处理语句写在for循环之前,这样预处理只需做一次。
最后
以上就是Go语言的Web后端框架的介绍,最后说下我的看法。
先说结论:总的来说可用。
优点:1)通过 Module Proxy 中间件避开了Go语言开发Web框架的“青涩”问题,规范、简单、实用。2)通过一系列的增删改查和事务示例,充分展示了Go在实际项目中业务后端的能力。3)对于从C++、Python转Go的程序员,Go语言相对来说学习门槛低,容易上手;对于从Java转过来的程序员,估计是受不了了Java那一层套一层的封装、模式,想找一个简单明快的语言实现,这点Go也基本满足。4)示例中的Go变量和数据库字段的转换,仍显得啰嗦低能,但这些是可以进行反射封装的,但我个人不希望像Java那样去封装,还是要保留语言本身的活力。
缺点:网上已经被人吐槽的,如异常、defer、泛型等,文章中也吐过了,这里要吐的是Go定义的数据库规范 database/sql。在数据库操作方面,Java JDBC已经耕耘了超过20年时间,Connection、Statement/PreparedStatement、ResultSet三对象深入人心,但Go语言却视而不见,另起炉灶重搞了一套。Go的语言设计者很可能是几个有严重偏执观念的,这点也被网上吐过了,但想想眼高于顶的牛顿牛爵爷当年都说过“我的成就是站在了巨人的肩膀上”,何况一个Go呢。