100行代码,带你编写一个go语言版的数据库客户端

柏链项目学院P叔 · · 1148 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

GoLang,习惯被人成为go语言,是目前后端程序员比较喜欢的一款语言,在区块链行业内,go语言更被称为是第一编程语言。go语言的优势有很多,比如内存回收、高并发、语法简洁、开发效率和运行效率都高等。本文通过一个例子,给大家介绍如何用go语言来实现一个访问数据库(mysql)的客户端,也就是我们经常用到的那种命令行窗口! 在go语言中为我们提供了sql操作的api,需要引用包:database/sql,不过如果要使用mysql,还需要引用包(驱动包):github.com/go-sql-driver/mysql ,而且这个包在引用的时候要求是匿名引用,也就是说database/sql在实现时需要借助驱动包的内容。在实现我们的小目标之前,我们先要分析一下都需要做哪些事情。

首先我们要做数据库的开发,肯定要知道如何调用curd(增删改查)接口,当然在调用这些接口前,你需要先知道如何连接或者说登陆到数据库,毕竟针对数据库的操作,都是在登陆之后才可以操作的!

连接数据库,我们使用sql包内的Open函数,顾名思义是打开一个数据库,它的函数原型如下:

func Open(driverName, dataSourceName string) (*DB, error) 

driverName显然就是驱动的名字,在这里我们填写“mysql”就可以,dataSourceName是数据源,需要填写mysql的连接串。

username:password@protocol(address)/dbname?param=value

protocol是代表连接mysql的方式,可以用tcp,也可以用本地unix,dbname就是要连接数据库的名字,param=value则是在登陆时的设置,比如登陆时限定字符集为utf8。参考例子如下:

root:abc123@tcp(10.211.55.3:3306)/yekai?charset=utf8

在搞清楚数据源如何填写后,我们就可以使用Open函数了,它的返回值是一个数据库连接句柄,此外还有一个错误信息提示,当没有错误时,此值为nil。

    db, err := sql.Open("mysql", "root:abc123@tcp(10.211.55.3:3306)/yekai?charset=utf8")
    if err != nil {
        log.Panic("failed to open mysql ", err)
    }

这样我们就获得了一个数据库的连接句柄,但是小编惊奇的发现,当yekai这个数据库不存在的时候,该函数并不会报错,但是如果ip地址填错了,则访问超时报错。因此为了确保连接确实没问题,确实可用,我们可以在用Sql.DB结构体内部的Ping函数来测试一下,如果Ping没有报错,则代表真的连接成功,可以将此部分代码放在init函数中,这样代码在初始化时会自动运行。

var dbconn *sql.DB

func init() {
    db, err := sql.Open("mysql", "root:abc123@tcp(10.211.55.3:3306)/yekai?charset=utf8")
    if err != nil {
        log.Panic("failed to open mysql ", err)
    }
    if err = db.Ping(); err != nil {
        log.Panic("failed to ping mysql ", err)
    }
    dbconn = db
}

连接到mysql数据库后,接下来就可以做增删改查操作了。其实说是增删改查操作,那是指sql层面的,对我们开发客户端来说,可以认为主要是两类数据库操作,一类是有结果集返回的,一类是没有结果集返回的。比如create、drop、insert、update等这些都是无结果集返回的语句,而show、desc、select则是有结果集返回的sql,其中当然用的最多的也就是select这样的sql。

在go语言开发中,我们实际用的也可以认为多是两类接口,一个是Exec,一个是Query,这两个接口都是Sql.DB结构内的函数,我们在Open之后得到的结果刚好就是调用的入口。

func exec_sql(xsql string) {
    result, err := db.Exec(xsql)
    if err != nil {
        fmt.Println("failed to exec sql:", xsql, err)
        return
    }
    rowsaff, _ := result.RowsAffected()
    fmt.Println("RowsAffected:", rowsaff)
}

从返回的result中,可以查询到影响的记录数,以上就是没有结果集的函数操作。对于有结果集的函数,操作起来就要麻烦一些,主要就是针对结果集的处理。

    rows, err := db.Query(xsql)
    if err != nil {
        fmt.Println("failed for query sql:", err)
        return
    }
    cols, err := rows.Columns()
    if err != nil {
        fmt.Println("failed for get columns:", err)
        return
    }
    colCount := len(cols)

    //fmt.Println(colCount, "\n", cols)
    for _, v := range cols {
        fmt.Printf("%s\t", v)
    }

使用Query函数,可以进行查询操作,返回一个结果集的rows,我们可以把他理解为结果集的多行记录。首先在rows这个结构集结构体中,我们可以用Columns()获得全部的列名,如果要打印结果集,可以先把列名打印出来,当然我们也可以根据Columns()获得对应的字段个数。

如果要获得每一行结果集以及具体字段的value值,那么就需要遍历结果集以及扫描结果记录了。主要使用rows.Next()以及rows.Scan()相配合,当Next返回为真时,可以调用Scan去获得该条记录的结果集,对于明确结果集的查询,比较好办,我们可以直接定义好要接收的变量,将它传入到Scan中去获得相应的值,因为Scan接口是这样的:

Scan(dest ...interface{}) 

我们可以把要接收的多个目标传进去,这样就可以获得对应的该条记录的不同字段的值,比如代码可以写成这样:

rows, err := db.Query("SELECT name FROM users WHERE age = ?", age)
if err != nil {
    log.Fatal(err)
}
for rows.Next() {
    var name string
    if err := rows.Scan(&name); err != nil {
        log.Fatal(err)
    }
    fmt.Printf("%s is %d\n", name, age)
}
if err := rows.Err(); err != nil {
    log.Fatal(err)
}

对于我们要写一个sql是用人随便输入的客户端来说,显然不能用这样的方式,在这里我们可以利用绑定接口值的方式,提前定义好两个map进行接口变量的绑定。

    values := make([]String, colCount)
    oneRows := make([]interface{}, colCount)
    for k, _ := range values {
        oneRows[k] = &values[k] //将查询结果的返回地址绑定,这样才能变参获取数据
    }

然后扫描的代码就可以直接使用oneRows了,当oneRows被扫描后,values的结果也填充完成了。

for rows.Next() {

        //扫描结果集,一定要在Next调用后,方可使用
        err = rows.Scan(oneRows...)
        if err != nil {
            fmt.Println("failed to Scan result set", err)
            break
        }
        //fmt.Println(values)
        for _, v := range values {
            if v.Valid {
                fmt.Printf("%s\t", v.String)
            } else {
                fmt.Printf("%s\t", "NULL")
            }
        }
        fmt.Println()
    }

分析到此,我们可以具备编写客户端的能力了,只需要编写一个循环接收输入的命令终端,将命令分类,如果是查询类,也就是有结果集这一类的操作时我们调用Query相关的处理,当调用无结果集的操作时,我们直接调用Exec即可。

整理下来的步骤应该是这样:

  • 1.连接到数据库
  • 2.循环等待命令输入
  • 3.判断是查询还是非查询
    • 3.1 如果是查询,调用Query,打印结果集
    • 3.2 如果非查询,调用Exec,打印影响记录数

参考代码如下:

/*
   file    : client.go
   author  : yekai
   company : pdj(pdjedu.com)
*/

package main

import (
    "bufio"
    "database/sql"
    "fmt"
    "log"
    "os"
    "strings"

    _ "github.com/go-sql-driver/mysql"
)

type Client struct {
    connstr string
    driver  string
    dbconn  *sql.DB
}

func NewClient(user, pass, dbname, protocol string) *Client {
    connstr := fmt.Sprintf("%s:%s@%s/%s?charset=utf8", user, pass, protocol, dbname)
    return &Client{connstr, "mysql", nil}
}

func (cli *Client) Conn() error {
    db, err := sql.Open(cli.driver, cli.connstr)
    if err != nil {
        fmt.Println("failed to open database ", err)
        return err
    }
    cli.dbconn = db
    return cli.dbconn.Ping()
}

func (cli *Client) Run() {
    reader := bufio.NewReader(os.Stdin)
    fmt.Println("Welcome to mysql client ")
    for {
        fmt.Printf("yekai-mysql>")
        sqlstr, err := reader.ReadString('\n')
        if err != nil {
            log.Panic("failed to ReadString ", err)
        }
        sqlstr = strings.Trim(sqlstr, "\r\n")
        sqls := []byte(sqlstr)
        if len(sqls) > 6 {
            if string(sqls[:6]) == "select" || string(sqls[:4]) == "show" || string(sqls[:4]) == "desc" {
                //result set sql
                cli.query_sql(sqlstr)
            } else {
                //no result set sql
                cli.exec_sql(sqlstr)
            }
        }
        if sqlstr == "quit" {
            fmt.Println("bye bye ")
            break
        }
    }
}

func (cli *Client) exec_sql(xsql string) {
    result, err := cli.dbconn.Exec(xsql)
    if err != nil {
        fmt.Println("failed to exec sql:", xsql, err)
        return
    }
    rowsaff, _ := result.RowsAffected()
    fmt.Println("RowsAffected:", rowsaff)
}

func (cli *Client) query_sql(xsql string) {
    rows, err := cli.dbconn.Query(xsql)
    if err != nil {
        fmt.Println("failed for query sql:", err)
        return
    }
    cols, err := rows.Columns()
    if err != nil {
        fmt.Println("failed for get columns:", err)
        return
    }
    colCount := len(cols)

    //fmt.Println(colCount, "\n", cols)
    for _, v := range cols {
        fmt.Printf("%s\t", v)
    }
    fmt.Println("\n----------------------------------------")

    values := make([]String, colCount)
    oneRows := make([]interface{}, colCount)
    for k, _ := range values {
        oneRows[k] = &values[k] //将查询结果的返回地址绑定,这样才能变参获取数据
    }

    for rows.Next() {

        //扫描结果集,一定要在Next调用后,方可使用
        err = rows.Scan(oneRows...)
        if err != nil {
            fmt.Println("failed to Scan result set", err)
            break
        }
        //fmt.Println(values)
        for _, v := range values {
            if v.Valid {
                fmt.Printf("%s\t", v.String)
            } else {
                fmt.Printf("%s\t", "NULL")
            }
        }
        fmt.Println()
    }
}

/*
   file    : main.go
   author  : yekai
   company : pdj(pdjedu.com)
*/
package main

import (
    "log"
)

func main() {
    cli := NewClient("root", "abc123", "yekai", "tcp(10.211.55.3:3306)")
    if cli.Conn() != nil {
        log.Panic("failed to conn to mysql ")
    }
    cli.Run()
}

上述代码还有点小问题,因为数据库里还有一个非常坑人的小玩意儿--NULL,在查询到空值的时候,我们的普通String类型处理不了,这时需要携带指示器类型的结构体,在sql包内给我们提供了sql.NullString,我们直接使用即可,也就是将上述代码稍稍替换一下。

    //使用NullString可以很好的支持空值
    values := make([]sql.NullString, colCount)
    oneRows := make([]interface{}, colCount)

好,终于写完了,我们不妨来测试一下效果!

localhost:sql yekai$ go run main.go client.go 
Welcome to mysql client 
yekai-mysql>show databses
failed for query sql: Error 1064: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'databses' at line 1
yekai-mysql>show databases
Database    
----------------------------------------
information_schema  
mysql   
performance_schema  
sys 
yekai   
yekai-mysql>show tables
Tables_in_yekai 
----------------------------------------
person  
person2 
yekai-mysql>desc person
Field   Type    Null    Key Default Extra   
----------------------------------------
name    varchar(30) YES     NULL        
age int(11) YES     NULL        
yekai-mysql>select * from person
name    age 
----------------------------------------
luffy   18  
zero    30  
nami    20  
sanji   25  
robin   35  
usopp   20  
yekai-mysql>quit
bye bye 
localhost:sql yekai$ 

有疑问加站长微信联系(非本文作者)

本文来自:简书

感谢作者:柏链项目学院P叔

查看原文:100行代码,带你编写一个go语言版的数据库客户端

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

1148 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传