如何使用 Go、Templ 和 HTMX 技术概述构建全栈应用程序 先决条件 入门 在 Xata 上设置数据库 构建应用程序前端 将其组合并构建后端 结论

2025-06-07

如何使用 Go、Templ 和 HTMX 构建全栈应用程序

技术概述

先决条件

入门

在 Xata 上设置数据库

构建应用程序前端

整合并构建后端

结论

Go 是一种静态类型、编译型的高级编程语言,用于构建系统、命令行界面 (CLI) 等。它通常设计用于后端;然而,有时您也希望使用同一种语言来构建一个包含函数式后端和可视化前端的全栈应用程序。

大多数情况下,Go 开发者会选择 React、Vue、Angular 等前端框架/库来构建应用程序的前端部分。这意味着他们必须学习 JavaScript/TypeScript、特定于框架的范式以及其他与前端相关的开销。

在本指南中,您将学习如何使用 Templ、HTMX 和 Xata 通过 Go 构建全栈应用程序。

技术概述

Templ:是一个模板引擎,可让您使用 Go 构建 HTML。它还允许您使用 Go 语法(例如ifswitchfor语句)来构建强大的前端。您将使用 Templ 为前端构建可重用的组件和页面。

HTMX:这是一个前端库,可让您直接使用 HTML 而非 JavaScript 访问现代浏览器功能。您将使用 HTMX 来处理表单提交并执行其他动态操作。

Xata:是一个无服务器数据库,具有分析和自由文本搜索支持,可以轻松构建各种应用程序。

先决条件

要学习本教程,需要满足以下条件:

入门

首先,你需要安装 Templ 二进制文件。该二进制文件会从 Templ 文件生成 Go 代码。



go install github.com/a-h/templ/cmd/templ@latest


Enter fullscreen mode Exit fullscreen mode

创建目录。



mkdir go_fullstack && cd go_fullstack


Enter fullscreen mode Exit fullscreen mode

接下来,初始化一个 Go 模块来管理项目依赖项。



go mod init go-fullstack


Enter fullscreen mode Exit fullscreen mode

最后,我们继续安装所需的依赖项:



go get github.com/gin-gonic/gin github.com/a-h/templ github.com/joho/godotenv


Enter fullscreen mode Exit fullscreen mode

github.com/gin-gonic/gin是一个用于构建 Web 应用程序的框架。

github.com/a-h/templ是项目中使用的Templ库。

github.com/joho/godotenv是一个用于加载环境变量的库。

构建应用程序

为此,在我们的项目目录中创建一个cmd、、internals和文件夹。views

cmd用于构建应用程序入口点。

internals用于构建与 API 相关的文件。

views用于构建与前端相关的文件。

在 Xata 上设置数据库

登录Xata 工作区并创建一个todo数据库。在todo数据库中,创建一个Todo表并添加一个description类型为 的列String

创建项目

获取数据库 URL 并设置 API 密钥

要获取数据库 URL,请点击“获取代码片段”按钮并复制该 URL。然后点击“API 密钥”链接,添加新密钥,保存并复制该 API 密钥。


设置环境变量

.env在根目录中创建一个文件并添加复制的URL和API密钥。



XATA_DATABASE_URL= <REPLACE WITH THE COPIED DATABASE URL>
XATA_API_KEY=<REPLACE WITH THE COPIED API KEY>


Enter fullscreen mode Exit fullscreen mode

构建应用程序前端

为了构建前端,您将使用 Templ 和 HTMX 来构建应用程序并为 Todo 应用程序添加动态。

创建应用程序组件

在文件夹中views,创建一个components/header.templ文件并添加以下代码片段:



package components

templ Header() {
    <head>
        <script
            src="https://unpkg.com/htmx.org@1.9.10"
            integrity="sha384-D1Kt99CQMDuVetoL1lrYwg5t+9QdHe7NLX/SoJYkXDFfX37iInKRy5xLSi8nO7UC"
            crossorigin="anonymous"
        ></script>
        <script src="https://cdn.tailwindcss.com"></script>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>GO Fullstack</title>
    </head>
}


Enter fullscreen mode Exit fullscreen mode

该代码片段创建了一个Header组件,并添加了 HTMX 和 TailwindCSS CDN。TailwindCSS 是一个用于样式设置的低级框架。

接下来,创建一个components/footer.templ文件,使用 TailwindCSS 类创建应用程序页脚和样式。



package components

templ Footer() {
    <footer class="fixed p-1 bottom-0 bg-gray-100 w-full border-t">
    <div class="rounded-lg p-4 text-xs italic text-gray-700 text-center">
        &copy; Go Fullstack
    </div>
    </footer>
}


Enter fullscreen mode Exit fullscreen mode

最后,index.templ在同一个views文件夹中创建一个文件并添加以下代码片段:



package views

import (
    "fmt"
    "go_fullstack/views/components"
)

type Todo struct {
    Id          string
    Description string
}

templ Index(todos []*Todo) {
    <!DOCTYPE html>
    <html lang="en">
        @components.Header()
        <body>
            <main class="min-h-screen w-full">
                <nav class="flex w-full border border-b-zinc-200 px-4 py-4">
                    <h3 class="text-base lg:text-lg font-medium text-center">
                        GO Fullstack app
                    </h3>
                </nav>
                <div class="mt-6 w-full flex justify-center items-center flex-col">
                    // FORM PROCESSING
                    <form
                        hx-post="/"
                        hx-trigger="submit"
                        hx-swap="none"
                        onsubmit="reloadPage()"
                        class="w-96"
                    >
                        <textarea
                            name="description"
                            cols="30"
                            rows="2"
                            class="w-full border rounded-lg mb-2 p-4"
                            placeholder="Input todo details"
                            required
                        ></textarea>
                        <button
                            class="py-1 px-4 w-full h-10 rounded-lg text-white bg-zinc-800"
                        >
                            Create
                        </button>
                    </form>
                    <section class="border-t border-t-zinc-200 mt-6 px-2 py-4 w-96">
                        // LOOP THROUGH THE TODOS
                        <ul id="todo-list">
                            for _, todo := range todos {
                                <li class="ml-4 ml-4 border p-2 rounded-lg mb-2" id={ fmt.Sprintf("%s", todo.Id) }>
                                    <p class="font-medium text-sm">Todo item { todo.Id }</p>
                                    <p class="text-sm text-zinc-500 mb-2">
                                        { todo.Description }
                                    </p>
                                    <div class="flex gap-4 items-center mt-2">
                                        <a
                                            href="#"
                                            class="flex items-center border py-1 px-2 rounded-lg"
                                        >
                                            <p class="text-sm">Edit</p>
                                        </a>
                                        <button
                                            hx-delete={ fmt.Sprintf("/%s", todo.Id) }
                                            hx-swap="delete"
                                            hx-target={ fmt.Sprintf("#%s", todo.Id) }
                                            class="flex items-center border py-1 px-2 rounded-lg hover:bg-red-300"
                                        >
                                            <p class="text-sm">Delete</p>
                                        </button>
                                    </div>
                                </li>
                            }
                        </ul>
                    </section>
                </div>
            </main>
        </body>
        @components.Footer()
    </html>
    <script>
        function reloadPage() {
                setTimeout(function() {
            window.location.reload();
        }, 2000);
        }
    </script>
}


Enter fullscreen mode Exit fullscreen mode

上面的代码片段执行以下操作:

  • 导入所需的依赖项
  • 创建一个Todo结构体来表示来自后端的响应数据
  • 创建一个Index使用HeaderFooter组件构建页面的组件。然后,它使用 HTMX 属性通过调用相应的端点/和来处理表单提交和待办事项删除。/{todo.Id}

从 Templ 文件生成 Go 文件

接下来,使用您之前安装的 Templ 二进制文件,通过在终端中运行以下命令从上面创建的视图中生成 Go 代码:



templ generate


Enter fullscreen mode Exit fullscreen mode

运行此命令后,您将看到为每个视图生成新的 Go 文件。您将在下一节中使用生成的文件来渲染前端。

生成的文件

生成的文件不可编辑。

整合并构建后端

完成后,您可以使用它来构建后端并呈现所需的页面。

创建 API 模型和辅助函数

为了表示应用程序数据,请在文件夹model.go内创建一个文件internals并添加以下代码片段:



package internals

type Todo struct {
    Id          string `json:"id,omitempty"`
    Description string `json:"description,omitempty"`
}

type TodoRequest struct {
    Description string `json:"description,omitempty"`
}

type TodoResponse struct {
    Id string `json:"id,omitempty"`
}


Enter fullscreen mode Exit fullscreen mode

上面的代码片段创建了一个TodoTodoRequestTodoResponse结构,具有描述请求和响应类型所需的属性。

最后,创建一个helpers.go具有可重用函数的文件来加载环境变量。



package internals

import (
    "log"
    "os"
    "github.com/joho/godotenv"
)

func GetEnvVariable(key string) string {
    err := godotenv.Load()
    if err != nil {
        log.Fatal("Error loading .env file")
    }
    return os.Getenv(key)
}


Enter fullscreen mode Exit fullscreen mode

创建应用程序和 API 路由

创建一个route.go用于配置 API 路由的文件并添加以下代码片段:



package api

import "github.com/gin-gonic/gin"

type Config struct {
    Router *gin.Engine
}

func (app *Config) Routes() {
    //routes will come here
}


Enter fullscreen mode Exit fullscreen mode

上面的代码片段执行以下操作:

  • 导入所需的依赖项
  • 创建一个Config具有Router属性的结构来配置应用程序方法
  • 创建一个以结构体为指针的Routes函数Config

创建 API 服务

完成后,xata_service.go为应用程序创建一个文件并通过执行以下操作来更新它:

首先,导入所需的依赖项并创建一个辅助函数:



package internals

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

var xataAPIKey = GetEnvVariable("XATA_API_KEY")
var baseURL = GetEnvVariable("XATA_DATABASE_URL")

func createRequest(method, url string, bodyData *bytes.Buffer) (*http.Request, error) {
    var req *http.Request
    var err error'

    if method == "GET" || method == "DELETE" || bodyData == nil {
        req, err = http.NewRequest(method, url, nil)
    } else {
        req, err = http.NewRequest(method, url, bodyData)
    }

    if err != nil {
        return nil, err
    }

    req.Header.Add("Content-Type", "application/json")
    req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", xataAPIKey))

    return req, nil
}


Enter fullscreen mode Exit fullscreen mode

上面的代码片段执行以下操作:

  • 导入所需的依赖项
  • 创建所需的环境变量
  • 创建一个createRequest函数,该函数创建具有所需标头的 HTTP 请求

最后,添加createTodoServicedeleteTodoServicegetAllTodosService方法来创建、删除和获取待办事项列表。



//imports goes here

func createRequest(method, url string, bodyData *bytes.Buffer) (*http.Request, error) {
    //createRequest code goes here
}

func (app *Config) createTodoService(newTodo *TodoRequest) (*TodoResponse, error) {
    createTodo := TodoResponse{}
    jsonData := Todo{
        Description: newTodo.Description,
    }

    bodyData := new(bytes.Buffer)
    json.NewEncoder(bodyData).Encode(jsonData)

    fullURL := fmt.Sprintf("%s:main/tables/Todo/data", baseURL)
    req, err := createRequest("POST", fullURL, bodyData)
    if err != nil {
        return nil, err
    }

    client := &http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        return nil, err
    }

    defer resp.Body.Close()
    if err := json.NewDecoder(resp.Body).Decode(&createTodo); err != nil {
        return nil, err
    }

    return &createTodo, nil
}
func (app *Config) deleteTodoService(id string) (string, error) {
    fullURL := fmt.Sprintf("%s:main/tables/Todo/data/%s", baseURL, id)
    client := &http.Client{}
    req, err := createRequest("DELETE", fullURL, nil)
    if err != nil {
        return "", err
    }

    resp, err := client.Do(req)
    if err != nil {
        return "", err
    }
    defer resp.Body.Close()

    return id, nil
}

func (app *Config) getAllTodosService() ([]*Todo, error) {
    var todos []*Todo

    fullURL := fmt.Sprintf("%s:main/tables/Todo/query", baseURL)
    client := &http.Client{}
    req, err := createRequest("POST", fullURL, nil)
    if err != nil {
        return nil, err
    }

    resp, err := client.Do(req)
    if err != nil {
        return nil, err
    }

    defer resp.Body.Close()
    var response struct {
        Records []*Todo `json:"records"`
    }

    decoder := json.NewDecoder(resp.Body)
    if err := decoder.Decode(&response); err != nil {
        return nil, err
    }

    todos = response.Records
    return todos, nil
}


Enter fullscreen mode Exit fullscreen mode

创建 API 处理程序

完成后,您可以使用这些服务来创建 API 处理程序。在文件夹handler.go中创建一个文件internals并添加以下代码片段:



package internals

import (
    "context"
    "fmt"
    "go_fullstack/views"
    "net/http"
    "time"
    "github.com/a-h/templ"
    "github.com/gin-gonic/gin"
)

const appTimeout = time.Second * 10

func render(ctx *gin.Context, status int, template templ.Component) error {
    ctx.Status(status)
    return template.Render(ctx.Request.Context(), ctx.Writer)
}

func (app *Config) indexPageHandler() gin.HandlerFunc {
    return func(ctx *gin.Context) {
        _, cancel := context.WithTimeout(context.Background(), appTimeout)
        defer cancel()

        todos, err := app.getAllTodosService()
        if err != nil {
            ctx.JSON(http.StatusBadRequest, err.Error())
            return
        }

        var viewsTodos []*views.Todo
        for _, todo := range todos {
            viewsTodo := &views.Todo{
                Id:          todo.Id,
                Description: todo.Description,
            }
            viewsTodos = append(viewsTodos, viewsTodo)
        }

        render(ctx, http.StatusOK, views.Index(viewsTodos))
    }
}

func (app *Config) createTodoHandler() gin.HandlerFunc {
    return func(ctx *gin.Context) {
        _, cancel := context.WithTimeout(context.Background(), appTimeout)
        description := ctx.PostForm("description")
        defer cancel()

        newTodo := TodoRequest{
            Description: description,
        }

        data, err := app.createTodoService(&newTodo)
        if err != nil {
            ctx.JSON(http.StatusBadRequest, err.Error())
            return
        }

        ctx.JSON(http.StatusCreated, data)
    }
}

func (app *Config) deleteTodoHandler() gin.HandlerFunc {
    return func(ctx *gin.Context) {
        _, cancel := context.WithTimeout(context.Background(), appTimeout)
        id := ctx.Param("id")
        defer cancel()

        data, err := app.deleteTodoService(id)
        if err != nil {
            ctx.JSON(http.StatusBadRequest, err.Error())
            return
        }

        ctx.JSON(http.StatusAccepted, fmt.Sprintf("Todo with ID: %s deleted successfully!!", data))
    }
}


Enter fullscreen mode Exit fullscreen mode

上面的代码片段执行以下操作

  • 导入所需的依赖项
  • 创建一个render使用该Templ包来渲染匹配模板的函数
  • 创建一个indexPageHandler函数,返回一个 Gin-gonic 处理程序,并接受Config结构体作为指针。在返回的处理程序中,使用该getAllTodosService服务获取待办事项列表,然后使用 views 包(前端)生成的代码渲染相应的页面。
  • 创建一个createdTodoHandlerdeleteProjectHandler函数,返回一个 Gin-gonic 处理程序,并接收Config结构体作为指针。使用之前创建的服务在返回的处理程序中执行相应的操作。

更新 API 路由以使用处理程序

使用处理程序更新routes.go文件,如下所示:



package internals

import (
    "github.com/gin-gonic/gin"
)

type Config struct {
    Router *gin.Engine
}

func (app *Config) Routes() {
    //views
    app.Router.GET("/", app.indexPageHandler())

    //apis
    app.Router.POST("/", app.createTodoHandler())
    app.Router.DELETE("/:id", app.deleteTodoHandler())
}


Enter fullscreen mode Exit fullscreen mode

整合起来

创建用于提供路由的应用程序入口点。为此,请在文件夹main.go中创建一个文件cmd并添加以下代码片段:



package main

import (
    "go_fullstack/internals"
    "github.com/gin-gonic/gin"
)

func main() {
    router := gin.Default()

    //initialize config
    app := internals.Config{Router: router}

    //routes
    app.Routes()

    router.Run(":8080")
}


Enter fullscreen mode Exit fullscreen mode

上面的代码片段执行以下操作:

  • 导入所需的依赖项
  • Default使用配置创建 Gin 路由器
  • Config通过传入Router
  • 添加路由并在端口上运行应用程序:8080

完成后,您可以使用以下命令启动开发服务器:

go run cmd/main.go
Enter fullscreen mode Exit fullscreen mode

完整的源代码可以在GitHub上找到

结论

本文讨论如何使用 Go、Templ、HTMX 和 Xata 构建一个全栈应用程序。您可以进一步扩展该应用程序,以支持查看和编辑待办事项。

以下资源也可能有帮助:

文章来源:https://dev.to/hackmamba/how-to-build-a-fullstack-application-with-go-templ-and-htmx-4444
PREV
如何使用 Auth0 和 Cloudinary 通过 React 构建音乐流媒体应用 music-app-with-auth0-and-cloudinary
NEXT
使用 Apache Airflow 管理数据管道