跳转到主要内容

介绍

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

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

先决条件

在我们开始使用这个应用程序之前,我们需要设置以下几件事:

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

我们将构建的内容:一个简单的待办事项应用程序

我们将构建一个全栈web应用程序,允许我们在Postgres数据库上执行CRUD操作。基本上,我们将构建一个待办应用程序。以下是完成的应用程序的外观:

golang

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

创建名为server的文件。进入项目文件夹并添加以下代码:

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)))
}

我们首先导入操作系统模块、日志模块,当然还有我们选择的web框架,在本例中是Go Fiber。如果您对Go Fiber没有太多经验,这里有一个Go Fibre文档的链接供您查看。

我们正在做的是用纤维制造一个新的纤维物体。新建并将其分配给app变量。接下来,我们检查环境变量中名为PORT的变量,如果不存在,我们将端口指定为3000。

然后我们调用app。侦听以启动正在侦听端口的HTTP服务器。接下来,我们调用日志。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")
}

目前,我们只是在所有路线上返回“你好”。让我们运行我们的应用程序。在命令行上,运行命令"go mod init",然后运行"go mod tidy"。这将创造一个机会。mod文件并获取应用程序所需的所有依赖项。

为了在开发时进行热重新加载,我们需要一个名为Air的Go包。

使用“go-get-github.com/cosmtrek/air”导入。现在运行“go run github.com/cosmtrek/air”启动应用程序。这将启动我们的web服务器并监视项目目录中的所有文件,从而允许我们在文件更改时进行热重新加载。

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

Visit LocalHost To View The App

让我们创建一个到数据库的连接。在主方法中,在创建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)
   }

确保将用户名、密码和database_ip替换为数据库的用户名、密码以及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驱动程序,因此我们在其导入前加上下划线。

我们将使用database/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

Edit New Table In Todos

您已经成功创建了我们要连接的数据库。关闭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的数据库执行SQL查询。Query()函数。这将向我们返回与查询匹配的所有行以及可能发生的任何错误。我们称之为延迟行。Close()关闭行,并在函数完成时防止进一步枚举。我们检查是否有任何错误,然后遍历所有行,调用行。Next(),并使用行。Scan()方法将行的当前值分配给res变量,我们将其定义为字符串。然后,我们将res的值附加到todos数组。

注释行。Scan()要求您传入与数据库中存储的数据相对应的数据类型变量。例如,如果您有多个列,比如名称和年龄,您将传入一个包含字段名称和年龄的结构。有关详细信息,请参阅SQL文档。

然后,我们返回到索引视图,并将todos数组传入其中。关于视图,让我们配置我们的Fiber应用程序以提供HTML视图。因此,修改您的主要方法:

engine := html.New("./views", ".html")
   app := fiber.New(fiber.Config{
       Views: engine,
   })

我们将Fiber应用程序配置为使用HTML模板引擎,并传入./views作为视图所在的路径。停止应用程序并使用go-get-github安装HTML引擎。com/gofiber/template/html,并确保也导入它。

然后,在项目根目录中创建一个名为视图的文件夹。在视图中,创建一个名为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服务器之前,将其添加到主方法中:

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查询,我们将新的待办事项添加到数据库中。然后我们重定向回主页。

N、 我们使用db.Query()方法。不执行时执行db.Exec()再次,请参阅SQL文档以了解更多信息。

对于我们的put处理程序,我们从请求查询字符串参数中获取新旧项名称。然后我们执行一个查询,用数据库中的新名称替换旧名称。最后,我们重定向回主页。

对于我们的delete处理程序,我们从请求查询字符串参数中获取要删除的名称,并执行一个查询从数据库中删除该名称,然后我们返回一个表示“deleted”的字符串。我们正在返回此字符串,以便知道函数已成功完成。

如果你检查索引。html文件,您会注意到,每当您分别单击编辑按钮和删除按钮时,我们都在调用updateDb和deleteFromDb函数。

Examine The Index File

这些函数已在索引中定义。js文件。这是索引。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数据库,并成功地用它构建了一个待办应用程序。有很多方法可以改进这一点,我迫不及待地想看看你接下来要做什么。谢谢你的阅读。

文章链接