百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术资源 > 正文

Go语言图书管理RESTful API开发实战

lipiwang 2025-06-13 14:45 6 浏览 0 评论

Go(Golang)是最近流行起来,且相对较新的编程语言。

它小而稳定,使用和学习简单,速度快,经过编译(原生代码),并大量用于云工具和服务(Docker、Kubernetes...)。

考虑到它所带来的所有好处,没有理由不去尝试一下。

在本教程中,我们将建立一个简单的图书商店REST API。

程序员宝藏库
https://github.com/Jackpopc/CS-Books-Store

1. 准备工作

在我们开始之前,我们需要事先做一些准备工作:

  • 浏览器
  • gorilla/handlers
  • gorilla/mux

做好这些准备,我们就可以开始Go之旅了!

2. 应用结构

你现在应该已经安装好Go,并且做好了事先准备工作。

打开你最喜欢的IDE(Visual Studio Code, GoLand, ...),创建一个新的项目。

正如我前面提到的,我们的想法是通过使用Mux建立一个简单的REST API,用于图书商店管理。

一旦你创建了你的空白项目,在其中创建以下结构:

├── main.go
└── src
    ├── app.go
    ├── data.go
    ├── handlers.go
    ├── helpers.go
    └── middlewares.go

Go工具包和模块

我们先来了解一下Go模块和包,如果你熟悉Python,你可能会对这些东西有一个概念,因为它们的操作很相似。

描述 Go 包的最好方法是,它是同一目录下的源文件的集合,被编译成一个可重复使用的单元。

这意味着所有有类似用途的文件都应该放在一个包里。

按照我们上面的结构,src是我们的包之一。

Go模块是Go包及其依赖关系的集合,这意味着一个模块可以由多个包组成。

为了便于理解,你可以把我们的整个应用程序看成是一个Go模块。

让我们在项目目录下执行这个命令来创建我们的模块。

go mod init bookstore

你应该在你的根目录中看到一个新文件,名为go.mod

3. 构建API

现在是时候开始构建我们的应用程序了。

打开你的main.go文件,在其中插入以下代码。

package main

import"bookstore/src"

func main() {
    src.Start()
}

我们声明了我们的主Go包(package main),并将我们的src包与模块bookstore的前缀一起导入。

在函数main()中,我们将运行包srcStart()函数。

这是我们的入口文件(main.go)的唯一职责--启动API。

路线和处理程序

现在我们需要创建我们的API路由器(Mux),并通过创建一些端点和它们的处理程序来配置它。

在你的src包中打开app.go并在其中插入以下代码。

package src

import (
    "github.com/gorilla/handlers"
    "github.com/gorilla/mux"
    "log"
    "net/http"
    "os"
)

func Start() {
    router := mux.NewRouter()
    router.Use(commonMiddleware)
    router.HandleFunc("/book", getAllBooks).Methods(http.MethodGet)
    router.HandleFunc("/book", addBook).Methods(http.MethodPost)
    router.HandleFunc("/book/{book_id:[0-9]+}", getBook).Methods(http.MethodGet)
    router.HandleFunc("/book/{book_id:[0-9]+}", updateBook).Methods(http.MethodPut)
    router.HandleFunc("/book/{book_id:[0-9]+}", deleteBook).Methods(http.MethodDelete)
    log.Fatal(http.ListenAndServe("localhost:5000", handlers.LoggingHandler(os.Stdout, router)))
}

如你所见,我们声明app.go是src包的一部分,它包含了我们在main.go文件中使用的Start()函数。

我们还导入两个外部模块,我们需要程序中需要依赖到的muxhandlers

在你的终端中执行以下命令:

go get github.com/gorilla/handlers
go get github.com/gorilla/mux

你的go.mod文件应该也同步了,现在它应该是这样的:

module bookstore

go1.17

require (
    github.com/gorilla/handlers v1.5.1
    github.com/gorilla/mux v1.8.0
)

require github.com/felixge/httpsnoop v1.0.1// indirect

让我们深入了解一下我们的Start()函数。

首先,我们声明了一个新的Mux路由器变量,它将负责路由和处理整个API的请求。

然后,我们告诉Mux,我们要用到一个中间件,它将在每个来到我们API的请求中执行下面这一行:

router.Use(commonMiddleware)

稍后会有更多关于中间件的内容。

继续分析我们的代码,我们可以看到我们在哪里创建端点以及处理程序(回调函数)和一些原始的验证,例如:

router.HandleFunc("/book/{book_id:[0-9]+}", updateBook).Methods(http.MethodPut)

一旦用户在/book/123(或任何其他数字)路径上用PUT方法点击我们的服务器,这个端点就会启动。

然后,它将把请求传递给updateBook处理函数进行进一步处理。

book_id变量必须是一个数字,因为我们在变量名声明后指定了一个简单的验证。

最后,我们将在特定的主机和端口组合上运行我们的服务器,并让它把所有的东西都记录到我们的终端。

中间件

我们都知道,REST APIs在接受请求和返回响应时大多使用JSON。

这是通过使用Content-Type头信息传达给我们的浏览器/HTTP客户端的。

由于我们的API将只使用JSON表示的数据,我们可以使用一个中间件,以确保我们的内容类型始终被设置为JSON。

如前所述,app.goStart()方法包含这一行:

router.Use(commonMiddleware)

让我们打开我们的middlewares.go文件并创建所需的函数:

package src

import"net/http"

func commonMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("content-type", "application/json; charset=utf-8")
        w.Header().Set("x-content-type-options", "nosniff")
        next.ServeHTTP(w, r)
    })
}

一旦用户点击我们在Start()函数中向Mux路由器注册的任何一个端点,中间件将拦截该请求并添加我们在commonMiddleware函数中指定的两个头信息。

然后,它将把修改后的请求进一步传递给被请求端点的处理函数或另一个中间件。

静态数据

由于我们不会使用任何数据存储服务(数据库、缓存......),我们需要有某种静态数据。

另外,我们将为自定义响应创建一个数据类型,我将在后面解释。

打开src包内的data.go,在其中放入以下内容。

package src

type Book struct {
    Id   int`json:"id"`
    Title string`json:"title"`
    Author string`json:"author"`
    Genre  string`json:"genre"`
}

var booksDB = []Book{
    {Id: 123, Title: "The Hobbit", Author: "J. R. R. Tolkien", Genre: "Fantasy"},
    {Id: 456, Title: "Harry Potter and the Philosopher's Stone", Author: "J. K. Rowling", Genre: "Fantasy"},
    {Id: 789, Title: "The Little Prince", Author: "Antoine de Saint-Exupéry", Genre: "Novella"},
}

我们刚刚创建了一个数据结构,它将在我们的API中保存一本书所需的信息。

我还创建了json标签,如果数据类型将以JSON形式传递,它将把字段名翻译成JSON表示。此外,还创建了一个原始的图书存储系统(在内存中)和一些初始图书数据(booksDB)。

把这段代码添加到上面的表格下面:

type CustomResponse struct {
    Code        int    `json:"code"`
    Message     string `json:"message"`
    Description string `json:"description,omitempty"`
}

var responseCodes = map[int]string {
    400: "Bad Request",
    401: "Unauthorized",
    403: "Forbidden",
    404: "Not Found",
    409: "Conflict",
    422: "Validation Error",
    429: "Too Many Requests",
    500: "Internal Server Error",
}

我们刚刚做了一个新的数据结构,将统一我们的API将返回的错误/响应,稍后会有更多这方面的内容。

辅助工具

我们将需要一些辅助工具来充分利用我们的API。例如,我们将需要检查具有给定ID的书是否存在(添加新书,修改现有的书)、需要删除一个具有给定ID的书(删除书)、需要为给定的HTTP状态代码返回一个自定义的JSON响应。

打开src包中的helpers.go,在里面插入以下内容:

package src

import (
    "encoding/json"
    "net/http"
)

func removeBook(s []Book, i int) []Book {
    if i != len(s)-1 {
        s[i] = s[len(s)-1]
    }
    return s[:len(s)-1]
}

func checkDuplicateBookId(s []Book, id int) bool {
    for _, book := range s {
        if book.Id == id {
            return true
        }
    }
    return false
}

func JSONResponse(w http.ResponseWriter, code int, desc string) {
    w.WriteHeader(code)
    message, ok := responseCodes[code]
    if !ok {
        message = "Undefined"
    }
    r := CustomResponse{
        Code:        code,
        Message:     message,
        Description: desc,
    }
    _ = json.NewEncoder(w).Encode(r)
}

removeBook函数会遍历Book,如果它不是片段的最后一个元素,它将把它移到片段的末尾并返回一个没有它的新片段(避免最后一个元素)。

checkDuplicateBookId函数将返回一个bool值(真或假),这取决于给定的id是否存在于Book中。

JSONResponse函数负责使用我们先前创建的CustomResponseresponseCodes。它将返回一个CustomResponse的JSON表示,其中包含了responseCodes将提供的状态代码和消息。

这样,我们将避免在我们的API中对相同的HTTP状态代码有不同的消息。

处理程序

现在来到了最后一步,把端点处理程序放在一起。

打开你的handlers.go,让我们在其中输入一些代码:

package src

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

获取单本图书

func getBook(w http.ResponseWriter, r *http.Request) {
    vars := mux.Vars(r)
    bookId, _ := strconv.Atoi(vars["book_id"])
    for _, book := range booksDB {
        if book.Id == bookId {
            _ = json.NewEncoder(w).Encode(book)
            return
        }
    }
    JSONResponse(w, http.StatusNotFound, "")
}

我们从Mux路由器获得传递的变量,把它从字符串转换为int值。然后,遍历我们的booksDB,寻找匹配的书籍ID。如果它存在,返回它 - 如果不存在,我们返回404: Not Found错误。

获取所有图书

func getAllBooks(w http.ResponseWriter, r *http.Request) {
    _ = json.NewEncoder(w).Encode(booksDB)
}

是不是很简单?将booksDB转换为JSON,并将其返回给用户。

添加一本新图书

func addBook(w http.ResponseWriter, r *http.Request) {
    decoder := json.NewDecoder(r.Body)
    var b Book
    err := decoder.Decode(&b)
    if err != nil {
        JSONResponse(w, http.StatusBadRequest, "")
        return
    }
    if checkDuplicateBookId(booksDB, b.Id) {
        JSONResponse(w, http.StatusConflict, "")
        return
    }
    booksDB = append(booksDB, b)
    w.WriteHeader(201)
    _ = json.NewEncoder(w).Encode(b)
}

由于这是在POST方法上触发的,用户必须在请求体中提供符合Book结构的JSON数据。

{
    "id": 999,
    "title": "SomeTitle",
    "author": "SomeAuthor",
    "genre": "SomeGenre"
}

一旦我们根据我们的图书结构解码并验证JSON主体(如果失败,我们将返回400: Bad Request error),我们需要检查具有相同ID的图书是否已经存在。如果是这样,我们将返回409: Conflict error back。反之,将用用户提供的书追加我们的booksDB,并将其JSON表示返回给用户。

更新现有书籍

func updateBook(w http.ResponseWriter, r *http.Request) {
    vars := mux.Vars(r)
    bookId, _ := strconv.Atoi(vars["book_id"])
    decoder := json.NewDecoder(r.Body)
    var b Book
    err := decoder.Decode(&b)
    if err != nil {
        JSONResponse(w, http.StatusBadRequest, "")
        return
    }
    for i, book := range booksDB {
        if book.Id == bookId {
            booksDB[i] = b
            _ = json.NewEncoder(w).Encode(b)
            return
        }
    }
    JSONResponse(w, http.StatusNotFound, "")
}

与addBook函数处理程序几乎相同,但有一个主要区别。

要更新这本书,它必须已经存在(ID必须在booksDB中)。

如果它存在,我们将更新现有书籍的值,否则,我们将返回404: Not Found错误。

删除现有书籍

func deleteBook(w http.ResponseWriter, r *http.Request) {
    vars := mux.Vars(r)
    bookId, _ := strconv.Atoi(vars["book_id"])
    for i, book := range booksDB {
        if book.Id == bookId {
            booksDB = removeBook(booksDB, i)
            _ = json.NewEncoder(w).Encode(book)
            return
        }
    }
    JSONResponse(w, http.StatusNotFound, "")
}

在我们得到book_id变量的整数值后,我们遍历booksDB,找到用户想删除的书。

如果它存在,我们利用我们的辅助函数removeBook,从Book结构片中删除该书。如果它不存在,我们将返回404: Not Found错误。

4. 运行和测试API

现在我们的API已经完成,让我们来运行一下,在你的终端执行这个程序:

go run main.go

启动你最喜欢的HTTP客户端(Insomnia, Postman, ...),试试我们创建的一些接口吧

相关推荐

Go语言图书管理RESTful API开发实战

Go(Golang)是最近流行起来,且相对较新的编程语言。它小而稳定,使用和学习简单,速度快,经过编译(原生代码),并大量用于云工具和服务(Docker、Kubernetes...)。考虑到它所带来的...

轻松搞定Golang 中的内存管理(golang设置内存大小)

除非您正在对服务进行原型设计,否则您可能会关心应用程序的内存使用情况。内存占用更小,基础设施成本降低,扩展变得更容易/延迟。尽管Go以不消耗大量内存而闻名,但仍有一些方法可以进一步减少消耗。其中一...

golang实现deepseek 聊天功能(golang deepcopy)

在搭建完deepseek环境后在docker内部署deepseekrag环境,我们可以用golang实现聊天功能。在实现这个功能之前,我们先了解下提示词工程(prompt)。大模型虽然知道的东西多...

golang slice的扩容机制(golang设置内存大小)

在Go语言中,切片(slice)是一种动态数组,其长度可以在运行时改变。当向切片中添加元素时,如果切片的容量不足以容纳新元素,就会触发扩容机制。下面详细介绍Go语言切片的扩容机制。扩容触发条件...

Etcd服务注册与发现封装实现--golang

服务注册register.gopackageregisterimport("fmt""time"etcd3"github.com/cor...

嘿,轻松获取区间内所有日期的Golang小技巧!

在Go语言中,获取两个日期之间的所有日期可以手动实现一个函数来完成。以下是一个示例函数,它会返回一个日期切片,包含从开始日期到结束日期(包括这两个日期)的所有日期:packagemainimpo...

仓颉、Java、Golang性能测试——数组扩容

版本信息仓颉版本0.53.18Golang版本1.22.8Java版本corretto-1.8.0_452源码仓颉packagecangjie_testimportstd.collect...

Golang 58个坑 – 中级篇:36-51(golang cef)

36.关闭HTTP的响应体37.关闭HTTP连接38.将JSON中的数字解码为interface类型39.struct、array、slice和map的值比较40.从panic...

一篇文章学会golang语法,golang简明教程快速入门

Go(又称Golang)是Google开发的一种静态强类型、编译型、并发型,并具有垃圾回收功能的编程语言。——Go-wikipedia.org1Go安装最新版本下载地址官方下载https...

运维大神如何使用 Golang 日志监控应用程序

你是如何使用Golang日志监控你的应用程序的呢?Golang没有异常,只有错误。因此你的第一印象可能就是开发Golang日志策略并不是一件简单的事情。不支持异常事实上并不是什么问题,异常在...

Golang操作elasticsearch(golang操作word)

简介开源的Elasticsearch是目前全文搜索引擎的首选,很多日志都是放到elasticsearch里面,然后再根据具体的需求进行分析。目前我们的运维系统是使用golang开发的,需要定时到e...

一文带你看懂Golang最新特性(golang x)

作者:腾讯PCG代码委员会经过十余年的迭代,Go语言逐渐成为云计算时代主流的编程语言。下到云计算基础设施,上到微服务,越来越多的流行产品使用Go语言编写。可见其影响力已经非常强大。一、Go语言发展历史...

Golang 最常用函数(备用查询)(golang函数和方法)

hello.gopackagemainimport"fmt"funcmain(){fmt.Println("Hello,world!")}直...

Golang:将日志以Json格式输出到Kafka

在上一篇文章中我实现了一个支持Debug、Info、Error等多个级别的日志库,并将日志写到了磁盘文件中,代码比较简单,适合练手。有兴趣的可以通过这个链接前往:https://github.com/...

如何从 PHP 过渡到 Golang?(php转go需要多久)

我是PHP开发者,转Go两个月了吧,记录一下使用Golang怎么一步步开发新项目。本着有坑填坑,有错改错的宗旨,从零开始,开始学习。因为我司没有专门的Golang大牛,所以我也只能一步步自己去...

取消回复欢迎 发表评论: