用Go和PostgreSQL建立一个简单的应用程序

150 阅读10分钟

简介

PostgreSQL是当今最流行的SQL数据库之一。根据官方文档,它是 "一个强大的、开源的对象关系型数据库系统,经过30多年的积极开发,在可靠性、功能健壮性和性能方面赢得了良好的声誉"。

在这篇文章中,我们将研究如何在Go应用程序中使用Postgres。

前提条件

在我们开始处理这个应用程序之前,有几件事情我们需要设置好。

  • Go - 因为这是我们选择的编程语言,我们需要在本地环境中安装它。
  • PostgreSQL - 我们将使用PostgreSQL作为我们的数据库。所以,为了开发的目的,你需要在本地环境中安装它。然而,在生产中,你可能会考虑一个更强大和安全的解决方案,如云计算产品。这方面的一个例子是AWS Aurora。你可以在这里从官方网站下载PostgreSQL
  • pgAdmin 4 - 这是一个用户界面,允许我们直观地管理我们的Postgres数据库。你可以在这里下载pgAdmin

我们将建立什么?一个简单的待办事项应用程序

我们将建立一个全栈式的Web应用程序,允许我们对Postgres数据库进行CRUD操作。基本上,我们将建立一个待办事项的应用程序。以下是完成后的应用程序的样子。

Finished To-Do Application

这个应用程序允许我们从数据库中获取、添加、编辑和删除待办事项。不多说了,让我们开始吧。

在你的项目文件夹中创建一个名为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" 来启动你的应用程序。这将启动我们的网络服务器并监视项目目录中的所有文件,使我们能够在文件发生变化时获得热重载。

Import Go Package Air

现在访问http://localhost/来查看这个应用程序。

Visit LocalHost To View The App

让我们创建一个与我们的数据库的连接。在你的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的数据库。

Navigate To Your PgAdmin 4 Console

Create A Database Called Todos

点击 "保存"以创建该数据库。现在,展开todos数据库,在公共模式下,创建一个名为todos的新表,其中有一个名为item的单列。

Expand Todos Databse

Create A Table In Todos

Edit New Table In Todos

Insert A Single Column In The Table Called 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() 要求你传入一个与数据库中存储的数据相对应的数据类型的变量。例如,如果你有多个列,例如姓名和年龄,你将传入一个带有字段nameage 的结构。更多信息请参考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&amp;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)))

再次启动该应用程序,你应该看到以下情况。

The Final Todo List App

对于我们的其他处理程序,请这样修改它们。

 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 函数。

Examine The Index File

这些函数已经被定义在一个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)))
}

如果你正确地遵循了上述教程,这就是你的应用程序的样子。

The Finished App

结语

我们终于来到了本教程的结尾。我们已经了解了如何用Go连接到PostgreSQL数据库,并且我们已经成功地用它建立了一个待办事项应用程序。还有很多方法可以改进,我已经迫不及待地想看到你接下来要做的事情了。谢谢你的阅读。