跳转到主要内容

如果您正在编写任何形式的 Web 应用程序,那么您很可能与 1 个或多个 REST API 交互,以填充应用程序的动态部分并执行更新或删除数据库中的数据等任务。

在本教程中,您将构建一个成熟的 REST API,它公开 GET、POST、DELETE 和 PUT 端点,随后将允许您执行全部范围的 CRUD 操作。

为了保持简单并专注于基本概念,我们不会与任何后端数据库技术交互来存储我们将要玩的文章。但是,我们将编写此 REST API,以便轻松更新我们将定义的函数,以便它们对数据库进行后续调用以执行任何必要的 CRUD 操作。

如果您想了解有关如何使用 Go 与数据库交互的更多信息,可以查看以下文章:


源代码 - 可以在此处找到本文的完整源代码:TutorialEdge/create-rest-api-in-go-tutorial

先决条件


您需要在开发机器上安装 Go 版本 1.11+。


目标


在本教程结束时,您将知道如何在 Go 中创建自己的 REST-ful API 来处理所有方面的问题。您将了解如何在项目中创建可以处理 POST、GET、PUT 和 DELETE HTTP 请求的 REST 端点。

视频教程

https://youtu.be/W5b64DXeP0o

REST 架构


如今,REST 无处不在,从网站到企业应用程序,RESTful 架构风格是提供独立软件组件之间通信的强大方式。构建 REST API 让您可以轻松地将消费者和生产者解耦,并且通常在设计上是无状态的。

注意 - 如果您想了解更多关于 REST API 的基础知识,请查看什么是 RESTful API?

JSON


出于本教程的目的,我将使用 JavaScript Object Notation 作为发送和接收所有信息的一种方式,幸运的是,Go 为使用标准库包 encoding/json 编码和解码这些格式提供了一些出色的支持。

注意 - 有关 encoding/json 包的更多信息,请查看官方文档:encoding/json

编组(Marshalling)


为了方便我们,我们可以使用一种称为编组的方法轻松地将 GO 中的数据结构转换为 JSON,该方法会生成一个字节切片,其中包含一个非常长的字符串,没有多余的空格。

基本 API 入门


首先,我们必须创建一个可以处理 HTTP 请求的非常简单的服务器。为此,我们将创建一个名为 main.go 的新文件。在这个 main.go 文件中,我们要定义 3 个不同的函数。一个 homePage 函数将处理对我们的根 URL 的所有请求,一个 handleRequests 函数将匹配 URL 路径命中定义的函数和一个将启动我们的 API 的 main 函数。

main.go

package main

import (
    "fmt"
    "log"
    "net/http"
)

func homePage(w http.ResponseWriter, r *http.Request){
    fmt.Fprintf(w, "Welcome to the HomePage!")
    fmt.Println("Endpoint Hit: homePage")
}

func handleRequests() {
    http.HandleFunc("/", homePage)
    log.Fatal(http.ListenAndServe(":10000", nil))
}

func main() {
    handleRequests()
}

如果我们现在在我们的机器上运行它,我们应该会看到我们非常简单的 API 在端口 10000 上启动,如果它还没有被另一个进程占用的话。如果我们现在在本地浏览器中导航到 http://localhost:10000/,我们应该会看到 Welcome to the HomePage!在我们的屏幕上打印出来。这意味着我们已经成功创建了构建 REST API 的基础。

注意 - 如果您想要更深入的教程来了解如何创建基于 Go 的 Web 服务器,请在此处查看本教程:使用 Go(Lang) 创建简单的 Web 服务器

我们的文章(Articles )结构


我们将创建一个 REST API,允许我们创建、阅读、更新和删除我们网站上的文章。当我们谈论 CRUD API 时,我们指的是可以处理所有这些任务的 API:创建、读取、更新和删除。

在我们开始之前,我们必须定义我们的文章结构。 Go 有这种结构的概念,非常适合这种情况。让我们创建一个具有标题、描述(desc)和内容的文章结构,如下所示:

type Article struct {
    Title string `json:"Title"`
    Desc string `json:"desc"`
    Content string `json:"content"`
}

// let's declare a global Articles array
// that we can then populate in our main function
// to simulate a database
var Articles []Article

我们的 Struct 包含代表我们网站上所有文章所需的 3 个属性。为了让它工作,我们还必须将“encoding/json”包导入到我们的导入列表中。

现在让我们更新我们的 main 函数,以便我们的 Articles 变量填充一些我们可以稍后检索和修改的虚拟数据。

func main() {
    Articles = []Article{
        Article{Title: "Hello", Desc: "Article Description", Content: "Article Content"},
        Article{Title: "Hello 2", Desc: "Article Description", Content: "Article Content"},
    }
    handleRequests()
}


完美,现在让我们继续创建我们的 /articles 端点,它将返回我们刚刚在此处定义的所有文章。

检索所有文章


在本教程的这一部分中,我们将创建一个新的 REST 端点,当遇到 HTTP GET 请求时,它将返回我们站点的所有文章。

我们将首先创建一个名为 returnAllArticles 的新函数,该函数将执行简单的任务,即返回我们新填充的 Articles 变量,以 JSON 格式编码:

main.go

func returnAllArticles(w http.ResponseWriter, r *http.Request){
    fmt.Println("Endpoint Hit: returnAllArticles")
    json.NewEncoder(w).Encode(Articles)
}


对 json.NewEncoder(w).Encode(article) 的调用将我们的文章数组编码为 JSON 字符串,然后作为响应的一部分写入。

在此之前,我们还需要向我们的 handleRequests 函数添加一个新路由,它将对 http://localhost:10000/articles 的任何调用映射到我们新定义的函数。

func handleRequests() {
    http.HandleFunc("/", homePage)
    // add our articles route and map it to our 
    // returnAllArticles function like so
    http.HandleFunc("/articles", returnAllArticles)
    log.Fatal(http.ListenAndServe(":10000", nil))
}


现在我们已经完成了,通过键入 go run main.go 运行代码,然后在浏览器中打开 http://localhost:10000/articles,您应该会看到文章列表的 JSON 表示,如下所示:

http://localhost:10000/articles response

[
  {
    Title: "Hello",
    desc: "Article Description",
    content: "Article Content"
  },
  {
    Title: "Hello 2",
    desc: "Article Description",
    content: "Article Content"
  }
];


我们已经成功定义了我们的第一个 API 端点。

在本系列的下一部分中,您将更新 REST API 以使用 gorilla/mux 路由器而不是传统的 net/http 路由器。

交换路由器将使您能够更轻松地执行任务,例如解析可能驻留在传入 HTTP 请求中的任何路径或查询参数,我们稍后将需要这些请求。

开始使用路由器


现在标准库足以提供您启动和运行自己的简单 REST API 所需的一切,但现在我们已经掌握了基本概念,我觉得是时候引入第三方路由器包了。最著名和使用最频繁的是 gorilla/mux 路由器,目前在 Github 上有 2,281 颗星。

构建我们的路由器
我们可以更新我们现有的 main.go 文件,并用一个基于 gorilla/mux 的 HTTP 路由器来代替之前存在的标准库。

修改您的 handleRequests 函数,以便它创建一个新路由器。

main.go

package main

import (
    "fmt"
    "log"
    "net/http"
    "encoding/json"
    "github.com/gorilla/mux"
)

… // Existing code from above
func handleRequests() {
    // creates a new instance of a mux router
    myRouter := mux.NewRouter().StrictSlash(true)
    // replace http.HandleFunc with myRouter.HandleFunc
    myRouter.HandleFunc("/", homePage)
    myRouter.HandleFunc("/all", returnAllArticles)
    // finally, instead of passing in nil, we want
    // to pass in our newly created router as the second
    // argument
    log.Fatal(http.ListenAndServe(":10000", myRouter))
}

func main() {
    fmt.Println("Rest API v2.0 - Mux Routers")
    Articles = []Article{
        Article{Title: "Hello", Desc: "Article Description", Content: "Article Content"},
        Article{Title: "Hello 2", Desc: "Article Description", Content: "Article Content"},
    }
    handleRequests()
}

当你现在运行它时,你不会看到我们系统的工作方式发生了真正的变化。它仍然会在同一个端口上启动并根据您点击的端点返回相同的结果。

唯一真正的区别是我们现在有一个 gorilla/mux 路由器,它可以让我们在本教程后面轻松地完成诸如检索路径和查询参数之类的事情。

$ go run main.go

Rest API v2.0 - Mux Routers


Path Variables


到目前为止一切顺利,我们创建了一个非常简单的 REST API,它返回一个主页和我们所有的文章。但是如果我们只想查看一篇文章会发生什么?

好吧,多亏了 gorilla mux 路由器,我们可以将变量添加到我们的路径中,然后根据这些变量选择我们想要返回的文章。在我们的 /articles 路由下方的 handleRequests() 函数中创建一个新路由:

myRouter.HandleFunc("/article/{id}", returnSingleArticle)


请注意,我们已将 {id} 添加到我们的路径中。这将代表我们的 id 变量,当我们希望仅返回具有该确切键的文章时,我们将能够使用该变量。目前,我们的 Article 结构没有 Id 属性。现在让我们添加:

type Article struct {
    Id      string `json:"Id"`
    Title   string `json:"Title"`
    Desc    string `json:"desc"`
    Content string `json:"content"`
}


然后我们可以更新我们的 main 函数以在我们的 Articles 数组中填充我们的 Id 值:

func main() {
    Articles = []Article{
        Article{Id: "1", Title: "Hello", Desc: "Article Description", Content: "Article Content"},
        Article{Id: "2", Title: "Hello 2", Desc: "Article Description", Content: "Article Content"},
    }
    handleRequests()
}


现在我们已经完成了,在我们的 returnSingleArticle 函数中,我们可以从我们的 URL 获取这个 {id} 值,我们可以返回符合这个条件的文章。由于我们没有将数据存储在任何地方,我们只会返回传递给浏览器的 ID。

func returnSingleArticle(w http.ResponseWriter, r *http.Request){
    vars := mux.Vars(r)
    key := vars["id"]

    fmt.Fprintf(w, "Key: " + key)
}

如果我们现在运行它之后导航到 http://localhost:1000/article/1,您应该会看到 Key: 1 在浏览器中打印出来。

让我们使用这个键值来返回与该键匹配的特定文章。

func returnSingleArticle(w http.ResponseWriter, r *http.Request) {
    vars := mux.Vars(r)
    key := vars["id"]

    // Loop over all of our Articles
    // if the article.Id equals the key we pass in
    // return the article encoded as JSON
    for _, article := range Articles {
        if article.Id == key {
            json.NewEncoder(w).Encode(article)
        }
    }
}

通过调用 go run main.go 运行它,然后在浏览器中打开 http://localhost:10000/article/1:

http://localhost:10000/article/1 response

{
Id: "1",
Title: "Hello",
desc: "Article Description",
content: "Article Content"
}


您现在将看到匹配键 1 的文章以 JSON 形式返回。

创建和更新文章


在本教程的这一部分中,您将构建 CRUD REST API 的创建、更新和删除部分。我们已经介绍了 R 具有阅读单篇文章和所有文章的能力。

创建新文章


再一次,您需要创建一个新函数来完成创建这篇新文章的工作。

让我们首先在 main.go 文件中创建一个 createNewArticle() 函数。

func createNewArticle(w http.ResponseWriter, r *http.Request) {
    // get the body of our POST request
    // return the string response containing the request body    
    reqBody, _ := ioutil.ReadAll(r.Body)
    fmt.Fprintf(w, "%+v", string(reqBody))
}


定义此函数后,您现在可以将路由添加到在 handleRequests 函数中定义的路由列表中。然而,这一次,我们将在路由末尾添加 .Methods("POST") 以指定我们只想在传入请求是 HTTP POST 请求时调用此函数:

func handleRequests() {
    myRouter := mux.NewRouter().StrictSlash(true)
    myRouter.HandleFunc("/", homePage)
    myRouter.HandleFunc("/articles", returnAllArticles)
    // NOTE: Ordering is important here! This has to be defined before
    // the other `/article` endpoint. 
    myRouter.HandleFunc("/article", createNewArticle).Methods("POST")
    myRouter.HandleFunc("/article/{id}", returnSingleArticle)
    log.Fatal(http.ListenAndServe(":10000", myRouter))
}


尝试再次运行,然后尝试提交包含以下 POST 正文的 HTTP POST 请求:

{
    "Id": "3", 
    "Title": "Newly Created Post", 
    "desc": "The description for my new post", 
    "content": "my articles content" 
}


我们的端点将触发并随后回显请求正文中的任何值。

现在您已经验证了您的新端点正常工作,让我们更新我们的 createNewArticle 函数,以便它将请求正文中的 JSON 解组为一个新的 Article 结构,该结构随后可以附加到我们的 Articles 数组中:

func createNewArticle(w http.ResponseWriter, r *http.Request) {
    // get the body of our POST request
    // unmarshal this into a new Article struct
    // append this to our Articles array.    
    reqBody, _ := ioutil.ReadAll(r.Body)
    var article Article 
    json.Unmarshal(reqBody, &article)
    // update our global Articles array to include
    // our new Article
    Articles = append(Articles, article)

    json.NewEncoder(w).Encode(article)
}

惊人的!如果您现在运行它并向您的应用程序发送相同的 POST 请求,您将看到它回显与以前相同的 JSON 格式,但它还将新文章附加到您的 Articles 数组中。

现在通过点击 http://localhost:10000/articles 来验证这一点:

http://localhost:10000/articles response

[
    {
        "Id": "1",
        "Title": "Hello",
        "desc": "Article Description",
        "content": "Article Content"
    },
    {
        "Id": "2",
        "Title": "Hello 2",
        "desc": "Article Description",
        "content": "Article Content"
    },
    {
        "Id": "3",
        "Title": "Newly Created Post",
        "desc": "The description for my new post",
        "content": "my articles content"
    }
]


您现在已经成功地将 Create 函数添加到新的 REST API 中!

在本教程的下一部分中,您将了解如何添加一个允许您删除文章的新 API 端点。

删除文章


有时您可能需要删除 REST API 公开的数据。为此,您需要在 API 中公开一个 DELETE 端点,该端点将接收一个标识符并删除与该标识符关联的任何内容。

在本教程的这一部分中,您将创建另一个端点,该端点接收 HTTP DELETE 请求并在它们与给定的 Id 路径参数匹配时删除文章。

在 main.go 文件中添加一个新函数,我们将其称为 deleteArticle:

func deleteArticle(w http.ResponseWriter, r *http.Request) {
    // once again, we will need to parse the path parameters
    vars := mux.Vars(r)
    // we will need to extract the `id` of the article we
    // wish to delete
    id := vars["id"]

    // we then need to loop through all our articles
    for index, article := range Articles {
        // if our id path parameter matches one of our
        // articles
        if article.Id == id {
            // updates our Articles array to remove the 
            // article
            Articles = append(Articles[:index], Articles[index+1:]...)
        }
    }

}

再一次,您需要向 handleRequests 函数添加一个路由,该路由映射到这个新的 deleteArticle 函数:

func handleRequests() {
    myRouter := mux.NewRouter().StrictSlash(true)
    myRouter.HandleFunc("/", homePage)
    myRouter.HandleFunc("/articles", returnAllArticles)
    myRouter.HandleFunc("/article", createNewArticle).Methods("POST")
    // add our new DELETE endpoint here
    myRouter.HandleFunc("/article/{id}", deleteArticle).Methods("DELETE")
    myRouter.HandleFunc("/article/{id}", returnSingleArticle)
    log.Fatal(http.ListenAndServe(":10000", myRouter))
}


尝试向 http://localhost:10000/article/2 发送一个新的 HTTP DELETE 请求。这将删除您的 Articles 数组中的第二篇文章,当您随后使用 HTTP GET 请求访问 http://localhost:10000/articles 时,您应该会看到它现在只包含一个文章。

注意 - 为了简单起见,我们正在更新一个全局变量。然而,我们没有做任何检查来确保我们的代码没有竞争条件。为了使这段代码线程安全,我建议查看我关于 Go Mutexes 的其他教程

更新文章端点


您需要实现的最后一个端点是更新端点。这个端点将是一个基于 HTTP PUT 的端点,并且需要接受一个 Id 路径参数,就像我们为 HTTP DELETE 端点所做的那样,以及一个 JSON 请求正文。

传入 HTTP PUT 请求正文中的这个 JSON 将包含我们要更新的文章的较新版本。

挑战


尝试在 handleRequests 函数中创建一个 updateArticle 函数和对应的路由。这将匹配 PUT 请求。完成此操作后,使用您在 createNewArticle 函数中使用的相同代码实现 updateArticle 函数,以便它解析 HTTP 请求正文。

最后,您将不得不遍历您的 Articles 数组中的文章并匹配并随后更新文章。

结论


这个例子代表了一个使用 Go 编写的非常简单的 RESTful API。在实际项目中,我们通常会将其与数据库联系起来,以便我们返回真实值。

源代码 - 本教程的完整源代码可以在这里找到:TutorialEdge/create-rest-api-in-go

文章链接