Go数据库基本教程

发布于:2023-06-30 14:26:36

1 介绍

本文是对官网的翻译,但是尽可能符合我们的阅读习惯,同时为了使用上的方便将MySQL数据库替换成了SQlite数据库。

在Go中通常使用database/sql包去操作SQL或SQL-Like数据库。它为关系型数据库提供了一套轻量级的接口。对于如果使用该包,本文给出了较为全面的说明。

database/sql的文档提供了所有的信息,但是它却没有告诉我们如何使用这个包。而我们大多数人希望有一个快速参考和入门教程来了解具体用法而不仅仅知识罗列功能。

2 概述

为了在Go中访问数据库,需要使用sql.DB。使用这个类型可以创建语句(statements)和事务,执行查询以及获取结果。

*先需要知道sql.DB不是一个数据库连接。它也不是database或schema的映射。它是某种数据库的抽象接口,数据库有多种形式,可以是本地文件,通过网络访问或者在内存中通过进程直接访问。

sql.DB在幕后执行着一些重要任务:

通过驱动打开和关闭和实际数据库的连接。管理着一个连接池,连接可以是上面提到的多种形式。

sql.DB的这种抽象设计使得我们无需关心如何管理对数据库的并发访问。当使用一个连接去执行任务时会被标记成使用状态,当它不再被使用时则会放回到可用的连接池中。这种方式可能会造成一种后果,如果我们没有释放连接,将会导致sql.DB打开许多连接,从而导致资源耗尽(太多的连接,太多打开的文件句柄,缺少可用的网络端口等等)。我们将在后面进一步讨论这个话题。

创建sql.DB后,就可使用它来查询数据库、创建语句和事务。

3 导入数据库驱动

为了使用database/sql,不仅需要这个引入这个包本身,同时还需要引入需要访问的数据库的驱动(Driver)。

通常情况下,不应该直接使用驱动,我们应该尽可能只引用database/sql中定义的类型,避免依赖特定的驱动,从而只需要修改少量代码就可以适配其它的驱动。这样将强迫我们使用Go的习惯用法而不是某个特定驱动的作者提供的特定方法。

在这个文档中, 我们将使用http://github.com/mattn/go-sqlite3,来进行演示,该驱动是一个Sqlite3驱动

将下面的代码添加到你的源码文件的顶部:

import ( "database/sql" _ "github.com/mattn/go-sqlite3" )

在上面的代码中我们以匿名方式导入驱动,使用”_”命名引入的驱动从而使得驱动中的所有名字都不可见。在背地里,驱动将自己注册到database/sql中,所有的这些实际上是通过init函数实现的。

4 访问数据库

截止到现在,我们已经导入了驱动包;然后就可以创建一个数据库对象了,即sql.DB。 为了创建sql.DB,需要调用sql.Open(),它返回一个*sql.DB,代码如下:

func main() { db, err := sql.Open("sqlite3", "foo.db") if err != nil { log.Fatal(err) } defer db.Close() }

在上面的示例中,说明了一些事情:

sql.Open的第一个参数是驱动的名称,这是驱动注册到database/sql时所使用的字符串。通常情况下为了避免混淆,通常是与驱动的包名相同。例如,mysql用于github.com/go-sql-driver/mysql。但也有一些驱动名称不遵循约定,比如我们正在用的这个包,postgres用于github.com/lib/pq。第二个参数是驱动的特定语法,用来告诉驱动如何去访问数据库。在这个例子中我们连接到了一个本地的Sqlite3数据库“foo.db"。通常情况下,应该检查和处理database/sql所有操作返回的错误信息,我们将在以后讨论在特定条件下也可不处理的情况。当sql.DB的生命周期不会超出function的范围时应该添加defer db.Close(),这是一个常用惯例。

与直觉不同,sql.Open()并没有建立与数据库的连接,也不验证驱动的连接参数。它只是简单地准备好以备后用。第一个实际的连接将在第一次使用时被延迟建立。如果你想立刻检查数据库是否可用(比如,检查是否可以建立网络连接并登入),可使用db.Ping()并记得检查error.

err = db.Ping() if err != nil { // do something here }

根据习惯当使用完数据库后应该调用Close()关闭数据库,但是sql.DB对象是被设计成支持长连接的。不需要频繁地Open()和Close()数据库。应该为每一个需要访问的数据库创建一个独有的sql.DB对象,在程序访问完数据库前一直持有它。在需要时传递它,或让它全局可用,确保是打开的。不要在一个短暂的function中Open()和Close()一个数据库,而是通过将sql.DB作为参数传入到函数中。

如果不这样使用sql.DB,将会碰到很多问题,比如无法复用和共享连接,网络资源耗尽,缘于许多TCP连接停留在TIME_WAIT状态而导致。此类问题意味着没用按照database/sql的设计意图来使用它。

5 获取结果集

有几种常用的方法来从数据库中获取结果集:

执行一个查询,返回多行数据;预编译一个可重复使用的statement,多次执行它,然后销毁它。执行一个一次性的statement,因为只执行一次因而无需预编译。执行一个查询,返回一行数据。这种特殊情况有一个快捷方法。

Go的database/sql的函数名称是有实际意义的。如果函数名称包含Query,则这个函数的目的是向数据库发送一个请求,然后返回一个数据集,即使它是空集合。如果不返回数据集则使用Exec()。

5.1 从数据库获取数据

让我们看一个如何查询数据库并处理的例子。我们查询users表中id为1的用户,并打印出该用户的id和name。针对每一行数据使用rows.Scan()将数据赋值给变量。

var ( id int name string ) rows, err := db.Query("select id, name from users where id = ?", 1) if err != nil { log.Fatal(err) } defer rows.Close() for rows.Next() { err := rows.Scan(&id, &name) if err != nil { log.Fatal(err) } log.Println(id, name) } err = rows.Err() if err != nil { log.Fatal(err) }

上面代码的执行过程如下:

使用db.Query()向数据库发送查询,并检查错误。使用defer rows.Close()。这一步很重要。使用rows.Next()遍历结果集。使用rows.Scan()将每一行的每一列保存到变量中。在遍历完结果集后进行错误检查。

在Go中这几乎是获取数据的唯一方式。例如,我们不能将一行数据保存成一个map类型。这是因为Go中所有的类型都是强类型。如代码所示,我们需要创建正确类型的变量再传入其指针。

这里一些容易出错的地方,会导致错误的结果。

在for rows.Next()循环结束后应该检查错误。如果在循环出现了错误,不应被忽略。不要假设循环完成,直到处理完所有行。.其次,只要有任意一个结果集(rows)被打开,那么这个连接就是被占用的,不能再被其他查询使用。这意味着在连接池中这个连接不可用。如果你用rows.Next()遍历所有行,当读取最后一行后,rows.Next()将会返回一个EOF,然后调用rows.Close()。但是如果由于某种原因退出了循环,比如程序过早return等等,那么rows就不会被关闭,从而导致连接仍处于打开状态。(尽管rows.Next()碰到错误会返回false并且会自动关闭rows)。这很容易引发资源耗尽。如果rows已关闭,再次调用Close()是一个无害的空操作,所以可以重复调用。为了避免runtime panic,我们应该先检查错误,无错时再调用rows.Close()。 应该总是使用defer rows.Close(),即使在循环结束后显示的调用rows.Close(),这是一个好的习惯。不要在循环中使用defer,defer语句会延迟到函数结束时才执行,所以不要在耗时的函数中使用它。否则将会逐渐消耗内存。如果要在循环中重复查询并使用结果集,那么应该使用rows.Close()而不是defer rows.Close() 。

5.2 Scan工作原理

当遍历结果集,并将每一个行的数据存储到指定变量是,在幕后,Scan根据目标变量的类型进行了相应的转换操作。了解到这一点可以理清代码避免重复性的工作。

例如,我们从一个表中的获得了一个结果集,这个表的列类型是string,比如VARCHAR(45)或其他类似的定义。假设表中包含了数字。

如果我们传入一个string类型的指针,Scan将拷贝数据到string中,然后调用strconv.ParseInt()或类似的方法将string转成数值。在此过程中必须检查SQL操作中的错误以及类型转化的错误,这是复杂、单调乏味的工作。

但是如果我们直接传入一个整数类型的指针,Scan会自动调用strconv.ParseInt()来处理。如果转换时碰到了一个错误,Scan()将返回错误信息。我们的代码可以更加短小精悍,这也是database/sql推荐使用方法。

5.3 预编译查询

如果一个查询需要重复使用,那么应该采用预编译查询。预编译查询的结果就是一个预编译的“statement”,它包括了一些占位符,在执行statement的时候,将给每个占位符提供相应的参数。这种方式比字符串拼接SQL语句的方式好,其可避免一些常见的问题(比如SQL注入)。

在MySQL中,占位符是?,在PostgreSQL中是$N,其中N是一个数值。SQLite两种都支持。在Oracle中占位符是以冒号开头的名称,比如:param1。这里我们使用?。

stmt, err := db.Prepare("select id, name from users where id = ?") if err != nil { log.Fatal(err) } defer stmt.Close() rows, err := stmt.Query(1) if err != nil { log.Fatal(err) } defer rows.Close() for rows.Next() { // ... } if err = rows.Err(); err != nil { log.Fatal(err) }

在底层,db.Query()其实依次完成了预编译,执行和关闭的操作,即与数据库交互了三次。一不小心, 与数据库的交互将增至3倍。有些驱动可避免这种情况,但不是所有的驱动都会这样做。更多内容请看预编译的语句

5.4 单行查询

如果一个查询至多只返回一行数据,可以使用一个快捷方式来避免冗长的样板代码。

var name string err = db.QueryRow("select name from users where id = ?", 1).Scan(&name) if err != nil { log.Fatal(err) } fmt.Println(name)

Errors from the query are deferred until Scan() is called, and then are returned from that. You can also call QueryRow() on a prepared statement:

查询发生的错误被延迟到调用Scan()后才返回,也可以使用QueryRow()调用一个预编译的statement。

stmt, err := db.Prepare("select name from users where id = ?") if err != nil { log.Fatal(err) } defer stmt.Close() var name string err = stmt.QueryRow(1).Scan(&name) if err != nil { log.Fatal(err) } fmt.Println(name)


免责声明:本站所有内容及图片均采集来源于网络,并无商业使用,如若侵权请联系删除。

上一篇:免费!好玩!收好这份小长假出游指南,玩转泰安有它就GO了!

下一篇:Go快速入门

资讯 观察行业视觉,用专业的角度,讲出你们的心声。
MORE

I NEED TO BUILD WEBSITE

我需要建站

*请认真填写需求信息,我们会在24小时内与您取得联系。