53

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

 4 years ago
source link: https://studygolang.com/articles/25411
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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$

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK