GoLang 後端入門

📢 本文由 gemini-2.5-flash 翻譯

簡易 (非技術框架) 實作的 Go 後端

眾所周知,後端的開發通常是面向介面的開發,也可以說是 CRUD 工程師。本文將使用 Go 語言描述如何從資料庫讀取資料,並回傳 JSON 格式的資料。

資料庫

本範例是從 MySQL 資料庫的分類表,讀取出分類的名稱與 ID,資料庫結構如下:

1
2
3
4
5
6
DROP TABLE IF EXISTS categories ;
CREATE TABLE categories (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    description VARCHAR(255)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

專案結構

本範例的專案結構如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
project/
├── database/       // 資料庫套件
│   └── database.go // 資料庫連線
├── handler/        // 處理器套件
│   └── category.go // 分類相關介面
├── model/          // 資料模型套件
│   ├── category.go // 分類表模型
│   └── response.go // 回應資料模型
├── router/         // 路由套件
│   └── router.go   // 路由設定
├── utils           // 工具套件
│   └── response.go // 統一回應
├── main.go         // 程式進入點

接下來將分目錄說明。

database

此套件用於管理與資料庫的連線。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// database.go
package database

import (
    "database/sql"
    "fmt"
    "log"

    _ "github.com/go-sql-driver/mysql"
)

var DB *sql.DB

func InitDB() {
    var err error

    dsn := "username:password@tcp(address:3306)/name?charset=utf8"
    DB, err = sql.Open("mysql", dsn)
    if err != nil {
        log.Fatalf("無法開啟資料庫: %v", err)
    }

    // 測試連線
    err = DB.Ping()
    if err != nil {
        log.Fatalf("無法 Ping 資料庫: %v", err)
    }

    fmt.Println("成功連線到資料庫!")
}

handler

此套件負責處理特定的業務邏輯 (類似於三層式架構中的 Service)。其中,資料庫處理部分還可以再分層 (類似於 Mapper),不過這只是一個簡單的範例,就先這樣囉~

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// category.go
package handler

import (
    "net/http"
    "project/database"
    "project/model"
    "project/utils"
)

func GetCategories(w http.ResponseWriter, r *http.Request) {
    // 查詢資料庫
    rows, err := database.DB.Query("select id,name from categories")
    if err != nil {
        http.Error(w, "查詢分類失敗", http.StatusInternalServerError)
        return
    }
    defer rows.Close()

    // 解析資料
    var categories []model.Category
    for rows.Next() {
        var category model.Category
        err = rows.Scan(&category.Id, &category.Name)
        if err != nil {
            utils.JSONResponse(w, http.StatusInternalServerError, "無法解析分類資料", nil)
            //http.Error(w, "Failed to analyze Categories", http.StatusInternalServerError)
            return
        }
        categories = append(categories, category)
    }

    // 回傳 JSON
    //w.Header().Set("Content-Type", "application/json")
    //json.NewEncoder(w).Encode(categories)
    utils.JSONResponse(w, http.StatusOK, "", categories)
}

被註解掉的內容是未採用統一回應介面,直接回傳 JSON 的情況。

model

這裡的資料模型通常可以分為三種:從前端接收到的、資料庫的,以及回傳給前端的。由於這只是一個簡單的範例,所以我並沒有細分。

1
2
3
4
5
6
7
8
// category.go
package model

type Category struct {
    Id          int    `json:"id"`
    Name        string `json:"name"`
    Description string `json:"description"`
}

回應的資料模型為:

1
2
3
4
5
6
7
package model

type Response struct {
    Code int         `json:"code"`
    Msg  string      `json:"msf"`
    Data interface{} `json:"data"`
}

router

此套件管理路由,也就是當存取到什麼路徑時,要指定相對應的處理邏輯 (類似於三層式架構中的 Controller)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// router.go
package router

import (
    "github.com/gorilla/mux"
    "project/handler"
)

func InitRouter() *mux.Router {
    router := mux.NewRouter()
    router.HandleFunc("/categories", handler.GetCategories).Methods("GET")
    return router
}

utils

此套件為工具類別,用於定義可通用的工具。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// response.go
package utils

import (
    "encoding/json"
    "log"
    "net/http"
    "project/model"
)

func JSONResponse(w http.ResponseWriter, code int, message string, data interface{}) {
    w.Header().Set("Content-Type", "application/json; charset=utf-8")
    w.WriteHeader(http.StatusOK)
    err := json.NewEncoder(w).Encode(model.Response{Code: code, Msg: message, Data: data})
    if err != nil {
        log.Fatal(err)
        return
    }
}

main

程式的進入點

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// main.go
package main

import (
    "log"
    "net/http"
    "project/database"
    "project/router"
)

func main() {
    // 初始化資料庫
    database.InitDB()

    // 初始化路由
    r := router.InitRouter()

    // 啟動
    log.Println("在連接埠 8848 啟動伺服器")
    log.Fatal(http.ListenAndServe(":8848", r))
}

執行 go run main.go 後,程式將會監聽連接埠 8848。