使用 Go 构建基本 HTTP 服务器:分步教程

2025-06-04

使用 Go 构建基本 HTTP 服务器:分步教程

介绍

Go(Golang)是一种编译型语言,拥有丰富的标准库,因其易读性和并发模型而广受 Web 服务器欢迎。Go 语言采用静态类型并支持垃圾回收,这使得它在 Web 服务器和微服务中备受欢迎。

最近我一直在阅读大量有关 Go 的资料,并决定是时候深入研究一下了。本教程一部分是学习,一部分是教学,所以请和我一起通过一个相关且有用的应用程序来探索这门美妙的语言!

您可以在 GitHub 上找到完整的代码,因此如果您在途中迷失了方向,您将获得完整的工作参考。

我们将一起构建一个 Web 服务器,只使用 Go 的标准库(不使用任何框架),以便我们真正理解其底层工作原理。它将是一个 JSON 服务器,用于管理帖子,我们可以创建、删除和获取帖子。为了简洁起见,我省略了更新帖子的步骤。

它还不能用于生产,但会向你和我介绍 Go 的一些核心概念。

我知道你已经坐立不安了,所以事不宜迟,让我们开始吧!

设置您的环境

如果您尚未安装 Go,请按照此处的说明将其下载到您的机器上。

确保将 go 添加到您的路径中:

export GOPATH=$HOME/go
export PATH=$PATH:$GOPATH/bin
Enter fullscreen mode Exit fullscreen mode

然后重新启动您的终端,或者启动您的.bashrc.

source ~/.bashrc  # Or ~/.zshrc or equivalent, depending on your shell
Enter fullscreen mode Exit fullscreen mode

让我们开始设置工作区。我们将创建一个包含所有文件的文件夹,并请求它go为我们初始化一个模块。

mkdir go-server
cd go-server
go mod init go-server
Enter fullscreen mode Exit fullscreen mode

这将在我们的文件夹中创建一个go.mod包含以下内容的文件。

module go-server

go 1.22.0
Enter fullscreen mode Exit fullscreen mode

我想我们已经准备好了;是时候编码了……

编写 HTTP 服务器代码(main.go

初始设置

现在在文件夹根目录创建一个文件main.go——它将包含我们服务器的所有代码。让我们添加一些样板代码和我们需要的导入:

package main

import (
    "encoding/json"
    "fmt"
    "io/ioutil"
    "log"
    "net/http"
    "strconv"
    "sync"
)

type Post struct {
    ID   int    `json:"id"`
    Body string `json:"body"`
}

var (
    posts   = make(map[int]Post)
    nextID  = 1
    postsMu sync.Mutex
)
Enter fullscreen mode Exit fullscreen mode

导入后,我们添加了一个Post结构体来定义我们的帖子数据,并定义了一些全局变量:

  • posts-map将我们的帖子保存在内存中(本教程中没有 DB)
  • nextID- 一个变量,可以帮助我们在创建新帖子时创建唯一的帖子 ID
  • postsMu- 一个互斥锁,允许我们在修改地图时锁定程序posts。如果没有这个互斥锁,理论上并发请求可能会导致竞争条件。

实现服务器

在这些变量的声明下方,添加main函数,它是我们模块的入口点。

func main() {
    http.HandleFunc("/posts", postsHandler)
    http.HandleFunc("/posts/", postHandler)

    fmt.Println("Server is running at http://localhost:8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}
Enter fullscreen mode Exit fullscreen mode

让我们看看这里发生了什么:

http.HandleFunc("/posts", postsHandler)
http.HandleFunc("/posts/", postHandler)
Enter fullscreen mode Exit fullscreen mode

这里我们为/posts/posts/路由设置处理程序,由一些尚不存在的函数处理。

log.Fatal(http.ListenAndServe(":8080", nil))
Enter fullscreen mode Exit fullscreen mode

相当简单,这是启动服务器并监听端口:8080,因此当我们点击时,:8080/posts我们将收到一系列帖子。

现在我们将添加这些处理程序。

处理请求

因此,我们设置的方式是,每个路由都有一个处理程序,无论是GET/ POST/ DELETE/ WHATEVER,我们都会在函数中处理所有内容。处理程序函数会检查方法并决定如何处理请求。

为了做到这一点,请在main函数下方添加以下代码。

func postsHandler(w http.ResponseWriter, r *http.Request) {
    switch r.Method {
    case "GET":
        handleGetPosts(w, r)
    case "POST":
        handlePostPosts(w, r)
    default:
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
    }
}

func postHandler(w http.ResponseWriter, r *http.Request) {
    id, err := strconv.Atoi(r.URL.Path[len("/posts/"):])
    if err != nil {
        http.Error(w, "Invalid post ID", http.StatusBadRequest)
        return
    }

    switch r.Method {
    case "GET":
        handleGetPost(w, r, id)
    case "DELETE":
        handleDeletePost(w, r, id)
    default:
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
    }
}
Enter fullscreen mode Exit fullscreen mode

这里首先要注意的是,处理函数都接收w http.ResponseWriter, r *http.Request参数。这两个参数使我们能够读取请求,并用 JSON 进行响应。

注意:我使用了单字符变量,因为这似乎是 Go 的惯例。由于我有 JavaScript 背景,我更倾向于将它们定义为完整的单词,但为了适应,所以就这样了。

剩下的就很明显了;我们将处理请求的任务委托给了更具体的函数,这些函数针对的是各自的方法。这里唯一有趣的地方可能就在 中postHandler,我们提取了id帖子的 来进行处理。

id, err := strconv.Atoi(r.URL.Path[len("/posts/"):])
if err != nil {
    http.Error(w, "Invalid post ID", http.StatusBadRequest)
    return
}
Enter fullscreen mode Exit fullscreen mode

这是将路径作为字符串 - r.URL.Path- 并使用语法创建子字符串,str[x:y]其中str是原始字符串x,是子字符串的起始索引,y是子字符串的结尾。

在我们的例子中,y省略了,所以它抓取了从 末尾/posts/到整个路径末尾的路径子字符串。因此,如果我们有/posts/123,它将123以字符串形式返回。

注意:请注意,我们也可以这样做[7:],这将产生相同的结果,但 7 是一个没有明显理由的神奇数字。

然后我们用将其转换为整数strconv.Atoi,并正确处理错误。

CRUD 操作

我们已经取得了很大进展!我们创建了一个监听 port 的服务器8080,并处理我们感兴趣的路由。然后,我们实现了这些处理程序,将每个单独的方法路由到其各自的处理程序。现在,让我们来实现这些处理程序。

为了方便理解,我会贴出完整的代码,并对感兴趣的部分进行注释。所以请务必仔细阅读,以便理解这里发生了什么。

func handleGetPosts(w http.ResponseWriter, r *http.Request) {
    // This is the first time we're using the mutex.
    // It essentially locks the server so that we can
    // manipulate the posts map without worrying about
    // another request trying to do the same thing at
    // the same time.
    postsMu.Lock()

    // I love this feature of go - we can defer the
    // unlocking until the function has finished executing,
    // but define it up the top with our lock. Nice and neat.
    // Caution: deferred statements are first-in-last-out,
    // which is not all that intuitive to begin with.
    defer postsMu.Unlock()

    // Copying the posts to a new slice of type []Post
    ps := make([]Post, 0, len(posts))
    for _, p := range posts {
        ps = append(ps, p)
    }

    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(ps)
}

func handlePostPosts(w http.ResponseWriter, r *http.Request) {
    var p Post

    // This will read the entire body into a byte slice 
    // i.e. ([]byte)
    body, err := ioutil.ReadAll(r.Body)
    if err != nil {
        http.Error(w, "Error reading request body", http.StatusInternalServerError)
        return
    }

    // Now we'll try to parse the body. This is similar
    // to JSON.parse in JavaScript.
    if err := json.Unmarshal(body, &p); err != nil {
        http.Error(w, "Error parsing request body", http.StatusBadRequest)
        return
    }

    // As we're going to mutate the posts map, we need to
    // lock the server again
    postsMu.Lock()
    defer postsMu.Unlock()

    p.ID = nextID
    nextID++
    posts[p.ID] = p

    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(p)
}

func handleGetPost(w http.ResponseWriter, r *http.Request, id int) {
    postsMu.Lock()
    defer postsMu.Unlock()

    p, ok := posts[id]
    if !ok {
        http.Error(w, "Post not found", http.StatusNotFound)
        return
    }

    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(p)
}

func handleDeletePost(w http.ResponseWriter, r *http.Request, id int) {
    postsMu.Lock()
    defer postsMu.Unlock()

    // If you use a two-value assignment for accessing a
    // value on a map, you get the value first then an
    // "exists" variable.
    _, ok := posts[id]
    if !ok {
        http.Error(w, "Post not found", http.StatusNotFound)
        return
    }

    delete(posts, id)
    w.WriteHeader(http.StatusOK)
}
Enter fullscreen mode Exit fullscreen mode

运行服务器

好了——我们准备好测试服务器,看看它是否正常工作了!鼓声响起?

go run main.go
Enter fullscreen mode Exit fullscreen mode

此命令编译并运行服务器。您应该看到一条消息,表明服务器正在运行并监听 8080 端口。然后,您可以通过在浏览器中访问http://localhost:8080/posts或使用 Postman 等工具curl发送请求来测试服务器。

结论

恭喜!你和我成功地用 Go 构建了一个基本的 HTTP 服务器。该服务器无需依赖外部框架即可创建、获取和删除帖子。我认为我们已经展示了 Go 作为一种相当低级的语言是多么的直观和强大,但它的垃圾收集器减轻了内存管理的负担。

虽然我们的服务器已经可以正常运行,但仍有许多地方可以扩展或改进。如果您喜欢进一步学习,请考虑以下几点:

  • 添加更新功能以允许编辑现有帖子。
  • 通过集成数据库而不是将帖子存储在内存中来实现数据持久化。
  • 添加用户身份验证以控制对某些端点的访问。
  • 实施输入验证以提高安全性。
  • 增强错误处理以获得更强大、更具信息量的响应。

本教程只是对 Go 的皮毛进行了介绍。我学习 Go 的过程非常有趣,而且我不认为这会是我学习 Go 语言之旅的终点。

文章来源:https://dev.to/andyjessop/building-a-basic-http-server-in-go-a-step-by-step-tutorial-ma4
PREV
为什么我选择 NextJS 而不是 CRA 来进行新项目
NEXT
您最喜欢的无用 repo/package/website/etc 是什么?