简介
PostgreSQL是当今最流行的SQL数据库之一。根据官方文档,它是 "一个强大的、开源的对象关系型数据库系统,经过30多年的积极开发,在可靠性、功能健壮性和性能方面赢得了良好的声誉"。
在这篇文章中,我们将研究如何在Go应用程序中使用Postgres。
前提条件
在我们开始处理这个应用程序之前,有几件事情我们需要设置好。
- Go - 因为这是我们选择的编程语言,我们需要在本地环境中安装它。
- PostgreSQL - 我们将使用PostgreSQL作为我们的数据库。所以,为了开发的目的,你需要在本地环境中安装它。然而,在生产中,你可能会考虑一个更强大和安全的解决方案,如云计算产品。这方面的一个例子是AWS Aurora。你可以在这里从官方网站下载PostgreSQL。
- pgAdmin 4 - 这是一个用户界面,允许我们直观地管理我们的Postgres数据库。你可以在这里下载pgAdmin
我们将建立什么?一个简单的待办事项应用程序
我们将建立一个全栈式的Web应用程序,允许我们对Postgres数据库进行CRUD操作。基本上,我们将建立一个待办事项的应用程序。以下是完成后的应用程序的样子。
这个应用程序允许我们从数据库中获取、添加、编辑和删除待办事项。不多说了,让我们开始吧。
在你的项目文件夹中创建一个名为server.go 的文件,并添加以下代码。
package main
import (
"fmt"
"log"
"os"
"github.com/gofiber/fiber/v2"
)
func main() {
app := fiber.New()
port := os.Getenv("PORT")
if port == "" {
port = "3000"
}
log.Fatalln(app.Listen(fmt.Sprintf(":%v", port)))
}
我们首先导入os 模块,log 模块,当然还有我们选择的网络框架,在这个例子中是Go Fiber。如果你对Go Fiber没有什么经验,这里有一个Go Fiber文档的链接供你查看。
我们在这里做的是用fiber.New ,创建一个新的fiber 对象,并将其分配给app变量。接下来,我们检查我们的环境变量是否有一个名为PORT 的变量,如果不存在,我们将端口分配给3000 。
然后,我们调用app.Listen ,启动一个HTTP服务器,该服务器正在监听我们的端口。接下来,我们调用log.Fatalln() ,以便在出现任何错误时将输出记录到控制台。在我们运行这段代码之前,让我们添加一些路由。
func main() {
app := fiber.New()
app.Get("/", indexHandler) // Add this
app.Post("/", postHandler) // Add this
app.Put("/update", putHandler) // Add this
app.Delete("/delete", deleteHandler) // Add this
port := os.Getenv("PORT")
if port == "" {
port = "3000"
}
log.Fatalln(app.Listen(fmt.Sprintf(":%v", port)))
}
正如你所看到的,我已经添加了四个方法来处理我们应用程序的GET、POST、PUT和DELETE操作,以及四个处理方法,每当有人访问这些路由时就会调用。现在,让我们来定义这些方法,以便Go不再抛出错误。
func indexHandler(c *fiber.Ctx) error {
return c.SendString("Hello")
}
func postHandler(c *fiber.Ctx) error {
return c.SendString("Hello")
}
func putHandler(c *fiber.Ctx) error {
return c.SendString("Hello")
}
func deleteHandler(c *fiber.Ctx) error {
return c.SendString("Hello")
}
现在,我们只是在所有的路由上返回 "Hello"。让我们运行我们的应用程序。在命令行中,运行命令"go mod init" ,然后是"go mod tidy" 。这将创建一个go.mod 文件,并获得应用程序需要的所有依赖项。
为了让我们在开发时有热重载,我们将需要一个叫Air的Go包。
用"go get github.com/cosmtrek/air" 来导入它。现在通过运行"go run github.com/cosmtrek/air" 来启动你的应用程序。这将启动我们的网络服务器并监视项目目录中的所有文件,使我们能够在文件发生变化时获得热重载。
现在访问http://localhost/来查看这个应用程序。
让我们创建一个与我们的数据库的连接。在你的main 方法中,在创建Fiber应用程序的实例之前,添加以下代码。
connStr := "postgresql://<username>:<password>@<database_ip>/todos?sslmode=disable
"
// Connect to database
db, err := sql.Open("postgres", connStr)
if err != nil {
log.Fatal(err)
}
确保用你的数据库的用户名、密码和IP地址替换username,password**,**和database_ip 。
首先,我们需要导入我们将用于连接数据库的SQL驱动。CockroachDB是一个SQL数据库,所以我们可以使用任何Go Postgres/SQL数据库驱动来连接它。在我们的例子中,我们将使用pq驱动。将你的导入文件更新为这个。
import (
"database/sql" // add this
"fmt"
"log"
"os"
_ "github.com/lib/pq" // add this
"github.com/gofiber/fiber/v2"
)
pq驱动依赖于数据库/sql包,所以我们也要导入它。我们不会直接使用pq驱动,所以我们在其导入前加一个下划线。
我们将使用数据库/sql包来执行所有的数据库操作,比如连接和执行查询。现在停止应用程序,运行"go get github.com/lib/pq" ,安装pq驱动。
接下来,我们将添加代码来创建数据库连接,同时更新我们的路由,将数据库连接传递给我们的处理程序,这样我们就可以用它来执行数据库查询。
connStr := "postgresql://<username>:<password>@<database_ip>/todos?sslmode=disable"
// Connect to database
db, err := sql.Open("postgres", connStr)
if err != nil {
log.Fatal(err)
}
app := fiber.New()
app.Get("/", func(c *fiber.Ctx) error {
return indexHandler(c, db)
})
app.Post("/", func(c *fiber.Ctx) error {
return postHandler(c, db)
})
app.Put("/update", func(c *fiber.Ctx) error {
return putHandler(c, db)
})
app.Delete("/delete", func(c *fiber.Ctx) error {
return deleteHandler(c, db)
})
正如你所看到的,我们现在传递一个函数来代替我们的处理程序,该函数接受fiber 上下文对象,并将其与数据库连接一起传递给我们的处理程序。fiber 上下文对象包含关于传入请求的所有内容,如头信息、查询字符串参数、帖子正文等。更多细节请参考Fiber文档。
现在让我们更新我们的处理程序,接受一个指向数据库连接的指针。
func indexHandler(c *fiber.Ctx, db *sql.DB) error {
return c.SendString("Hello")
}
func postHandler(c *fiber.Ctx, db *sql.DB) error {
return c.SendString("Hello")
}
func putHandler(c *fiber.Ctx, db *sql.DB) error {
return c.SendString("Hello")
}
func deleteHandler(c *fiber.Ctx, db *sql.DB) error {
return c.SendString("Hello")
}
Now start the app again and you see it runs without errors. Here’s the full code up to here for reference.
package main
import (
"database/sql" // add this
"fmt"
"log"
"os"
_ "github.com/lib/pq" // add this
"github.com/gofiber/fiber/v2"
)
func indexHandler(c *fiber.Ctx, db *sql.DB) error {
return c.SendString("Hello")
}
func postHandler(c *fiber.Ctx, db *sql.DB) error {
return c.SendString("Hello")
}
func putHandler(c *fiber.Ctx, db *sql.DB) error {
return c.SendString("Hello")
}
func deleteHandler(c *fiber.Ctx, db *sql.DB) error {
return c.SendString("Hello")
}
func main() {
connStr := "postgresql://<username>:<password>@<database_ip>/todos?sslmode=disable"
// Connect to database
db, err := sql.Open("postgres", connStr)
if err != nil {
log.Fatal(err)
}
app := fiber.New()
app.Get("/", func(c *fiber.Ctx) error {
return indexHandler(c, db)
})
app.Post("/", func(c *fiber.Ctx) error {
return postHandler(c, db)
})
app.Put("/update", func(c *fiber.Ctx) error {
return putHandler(c, db)
})
app.Delete("/delete", func(c *fiber.Ctx) error {
return deleteHandler(c, db)
})
port := os.Getenv("PORT")
if port == "" {
port = "3000"
}
log.Fatalln(app.Listen(fmt.Sprintf(":%v", port)))
}
充实我们的路由处理程序
在我们开始充实我们的处理程序之前,让我们设置我们的数据库。导航到pgAdmin 4控制台,创建一个名为todos的数据库。
点击 "保存"以创建该数据库。现在,展开todos数据库,在公共模式下,创建一个名为todos的新表,其中有一个名为item的单列。
你已经成功创建了我们将要连接的数据库。关闭pgAdmin应用程序,让我们开始充实我们的处理方法。
将索引处理程序修改成这样。
func indexHandler(c *fiber.Ctx, db *sql.DB) error {
var res string
var todos []string
rows, err := db.Query("SELECT * FROM todos")
defer rows.Close()
if err != nil {
log.Fatalln(err)
c.JSON("An error occured")
}
for rows.Next() {
rows.Scan(&res)
todos = append(todos, res)
}
return c.Render("index", fiber.Map{
"Todos": todos,
})
}
好的,这有很多东西需要接受!首先,我们使用。首先,我们使用db 对象,用db.Query() 函数在数据库上执行一个SQL查询。这将向我们返回所有符合我们查询的行以及可能发生的错误。我们调用defer rows.Close() 来关闭这些行,并在函数完成后防止进一步的枚举。我们检查是否有任何错误,然后我们循环浏览所有的行,每次迭代都调用rows.Next() ,并使用rows.Scan() 方法将行的当前值分配给res 变量,我们将其定义为一个字符串。然后我们将res 的值追加到todos 数组中。
注意rows.Scan() 要求你传入一个与数据库中存储的数据相对应的数据类型的变量。例如,如果你有多个列,例如姓名和年龄,你将传入一个带有字段name 和age 的结构。更多信息请参考SQL文档。
然后我们返回到index 视图,将todos 数组传入其中。谈到视图,让我们配置我们的Fiber应用程序以提供我们的HTML视图。这样修改你的main 方法。
engine := html.New("./views", ".html")
app := fiber.New(fiber.Config{
Views: engine,
})
我们将Fiber应用程序配置为使用HTML模板引擎,并传入./views ,作为我们视图所在的路径。停止应用程序,用go get github.com/gofiber/template/html 来安装HTML引擎,并确保将它也导入。
然后,在你的项目根目录下创建一个名为views 的文件夹。在views ,创建一个名为index .html 的文件并添加以下代码。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/style.css"/>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css"/>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300;0,400;0,600;0,700;0,800;1,300;1,400;1,600;1,700;1,800&display=swap"/>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css"/>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-datepicker/1.9.0/css/bootstrap-datepicker.standalone.min.css"/>
<title>Document</title>
</head>
<body>
<div class="container m-5 p-2 rounded mx-auto bg-light shadow">
<!-- App title section -->
<div class="row m-1 p-4">
<div class="col">
<div class="p-1 h1 text-primary text-center mx-auto display-inline-block">
<i class="fa fa-check bg-primary text-white rounded p-2"></i>
<u>Todo List</u>
</div>
</div>
</div>
<!-- Create todo section -->
<div class="row m-1 p-3">
<div class="col col-11 mx-auto">
<form action="/" method="POST" class="row bg-white rounded shadow-sm p-2 add-todo-wrapper align-items-center justify-content-center">
<div class="col">
<input name="Item" class="form-control form-control-lg border-0 add-todo-input bg-transparent rounded" type="text" placeholder="Add new ..">
</div>
<div class="col-auto px-0 mx-0 mr-2">
<button type="submit" class="btn btn-primary">Add</button>
</div>
</form>
</div>
</div>
<div class="p-2 m-2 mx-4 border-black-25 border-bottom"></div>
<!-- Todo list section -->
<div class="row mx-1 px-5 pb-3 w-80">
<div class="col mx-auto">
<!-- Todo Item-->
{{range .Todos}}
<div class="row px-3 align-items-center todo-item editing rounded">
<div class="col px-1 m-1 d-flex align-items-center">
<input type="text" class="form-control form-control-lg border-0 edit-todo-input bg-transparent rounded px-3 d-none" readonly value="{{.}}" title="{{.}}" />
<input id="{{.}}" type="text" class="form-control form-control-lg border-0 edit-todo-input rounded px-3" value="{{.}}" />
</div>
<div class="col-auto m-1 p-0 px-3 d-none">
</div>
<div class="col-auto m-1 p-0 todo-actions">
<div class="row d-flex align-items-center justify-content-end">
<h5 class="m-0 p-0 px-2">
<i onclick="updateDb('{{.}}')" class="fa fa-pencil text-warning btn m-0 p-0" data-toggle="tooltip" data-placement="bottom" title="Edit todo"></i>
</h5>
<h5 class="m-0 p-0 px-2">
<i onclick="removeFromDb('{{.}}')" class="fa fa-trash-o text-danger btn m-0 p-0" data-toggle="tooltip" data-placement="bottom" title="Delete todo"></i>
</h5>
</div>
</div>
</div>
{{end}}
</div>
</div>
</div>
</form>
<script src="index.js"></script>
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootlint/1.1.0/bootlint.min.js"></script>
</body>
</html>
这将循环浏览我们传入的todos 数组,并显示每个项目。如果你检查这个文件,你会看到我们也在链接一个样式表。创建一个名为public 的文件夹,并在其中创建一个名为style.css 的文件,添加以下代码。
body {
font-family: "Open Sans", sans-serif;
line-height: 1.6;
}
.add-todo-input,
.edit-todo-input {
outline: none;
}
.add-todo-input:focus,
.edit-todo-input:focus {
border: none !important;
box-shadow: none !important;
}
.view-opt-label,
.date-label {
font-size: 0.8rem;
}
.edit-todo-input {
font-size: 1.7rem !important;
}
.todo-actions {
visibility: hidden !important;
}
.todo-item:hover .todo-actions {
visibility: visible !important;
}
.todo-item.editing .todo-actions .edit-icon {
display: none !important;
}
现在,让我们配置Go来服务这个文件。在启动Web服务器之前,将此添加到你的main 方法中。
app.Static("/", "./public") // add this before starting the app
log.Fatalln(app.Listen(fmt.Sprintf(":%v", port)))
再次启动该应用程序,你应该看到以下情况。
对于我们的其他处理程序,请这样修改它们。
type todo struct {
Item string
}
func postHandler(c *fiber.Ctx, db *sql.DB) error {
newTodo := todo{}
if err := c.BodyParser(&newTodo); err != nil {
log.Printf("An error occured: %v", err)
return c.SendString(err.Error())
}
fmt.Printf("%v", newTodo)
if newTodo.Item != "" {
_, err := db.Exec("INSERT into todos VALUES ($1)", newTodo.Item)
if err != nil {
log.Fatalf("An error occured while executing query: %v", err)
}
}
return c.Redirect("/")
}
func putHandler(c *fiber.Ctx, db *sql.DB) error {
olditem := c.Query("olditem")
newitem := c.Query("newitem")
db.Exec("UPDATE todos SET item=$1 WHERE item=$2", newitem, olditem)
return c.Redirect("/")
}
func deleteHandler(c *fiber.Ctx, db *sql.DB) error {
todoToDelete := c.Query("item")
db.Exec("DELETE from todos WHERE item=$1", todoToDelete)
return c.SendString("deleted")
}
首先,我们创建一个结构来保存一个待办事项。然后,在我们的postHandler ,我们从请求正文中获得我们想要插入数据库的待办事项的名称。接下来,我们使用db.Exec() 方法来执行一个SQL查询,将新的待办事项添加到数据库中。然后我们重定向到主页。
注意, 每当我们期待数据库查询的结果时,我们就使用 db.Query() 方法, 当我们不期待时,就使用db.Exec() 。同样,请参考SQL文档获取更多信息。
对于我们的put处理程序,我们从请求的查询字符串参数中获得新旧项目的名称。然后我们执行一个查询,在数据库中用新的名字替换旧的名字。最后,我们重定向到主页。
对于我们的删除处理程序,我们从请求的查询字符串参数中得到要删除的名字,并执行一个查询,从数据库中删除这个名字,然后我们送回一个字符串:"deleted" 。我们返回这个字符串,这样我们就知道这个函数已经成功完成。
如果你检查index.html 文件,你会注意到,每当你点击编辑按钮和删除按钮时,我们都会调用一个updateDb 和一个deleteFromDb 函数。
这些函数已经被定义在一个index.js 文件中,我们在下面的HTML文件中进行了链接。下面是这个index.js 文件的样子。
function removeFromDb(item){
fetch(`/delete?item=${item}`, {method: "Delete"}).then(res =>{
if (res.status == 200){
window.location.pathname = "/"
}
})
}
function updateDb(item) {
let input = document.getElementById(item)
let newitem = input.value
fetch(`/update?olditem=${item}&newitem=${newitem}`, {method: "PUT"}).then(res =>{
if (res.status == 200){
alert("Database updated")
window.location.pathname = "/"
}
})
}
Now add the above code in a file called index.js in the public folder.
Ok here’s the full server.go file code for a reference
package main
import (
"database/sql" // add this
"fmt"
"log"
"os"
_ "github.com/lib/pq" // add this
"github.com/gofiber/fiber/v2"
"github.com/gofiber/template/html"
)
func indexHandler(c *fiber.Ctx, db *sql.DB) error {
var res string
var todos []string
rows, err := db.Query("SELECT * FROM todos")
defer rows.Close()
if err != nil {
log.Fatalln(err)
c.JSON("An error occured")
}
for rows.Next() {
rows.Scan(&res)
todos = append(todos, res)
}
return c.Render("index", fiber.Map{
"Todos": todos,
})
}
type todo struct {
Item string
}
func postHandler(c *fiber.Ctx, db *sql.DB) error {
newTodo := todo{}
if err := c.BodyParser(&newTodo); err != nil {
log.Printf("An error occured: %v", err)
return c.SendString(err.Error())
}
fmt.Printf("%v", newTodo)
if newTodo.Item != "" {
_, err := db.Exec("INSERT into todos VALUES ($1)", newTodo.Item)
if err != nil {
log.Fatalf("An error occured while executing query: %v", err)
}
}
return c.Redirect("/")
}
func putHandler(c *fiber.Ctx, db *sql.DB) error {
olditem := c.Query("olditem")
newitem := c.Query("newitem")
db.Exec("UPDATE todos SET item=$1 WHERE item=$2", newitem, olditem)
return c.Redirect("/")
}
func deleteHandler(c *fiber.Ctx, db *sql.DB) error {
todoToDelete := c.Query("item")
db.Exec("DELETE from todos WHERE item=$1", todoToDelete)
return c.SendString("deleted")
}
func main() {
connStr := "postgresql://postgres:gopher@localhost/todos?sslmode=disable"
// Connect to database
db, err := sql.Open("postgres", connStr)
if err != nil {
log.Fatal(err)
}
engine := html.New("./views", ".html")
app := fiber.New(fiber.Config{
Views: engine,
})
app.Get("/", func(c *fiber.Ctx) error {
return indexHandler(c, db)
})
app.Post("/", func(c *fiber.Ctx) error {
return postHandler(c, db)
})
app.Put("/update", func(c *fiber.Ctx) error {
return putHandler(c, db)
})
app.Delete("/delete", func(c *fiber.Ctx) error {
return deleteHandler(c, db)
})
port := os.Getenv("PORT")
if port == "" {
port = "3000"
}
app.Static("/", "./public")
log.Fatalln(app.Listen(fmt.Sprintf(":%v", port)))
}
如果你正确地遵循了上述教程,这就是你的应用程序的样子。
结语
我们终于来到了本教程的结尾。我们已经了解了如何用Go连接到PostgreSQL数据库,并且我们已经成功地用它建立了一个待办事项应用程序。还有很多方法可以改进,我已经迫不及待地想看到你接下来要做的事情了。谢谢你的阅读。