
DevOps 學習筆記

從零開始建立一個符合北美業界標準的最小 Go HTTP server, 包含專案結構說明與 chi router 使用

我的背景是 .NET Engineer, 因此筆記中會用 .NET 的對應概念來輔助釐清 Go 的設計

> 如果你也有 .NET 背景, 這些對比應該會有幫助

## 專案結構

### 整體配置

```
go-Api/
├── cmd/
│   └── server/
│       └── main.go
├── internal/
│   └── handler/
│       └── health.go
├── go.mod
└── go.sum
```

### cmd/ 目錄

Go 規定每個可執行程式需要一個 `package main` 和 `main()` 函數。`cmd/` 是 Go 社群慣例, 用來放這個 repo 會產生幾個 binary 的入口

```
cmd/
├── server/    → HTTP server binary
├── worker/    → background job binary (future expansion)
└── migrate/   → DB migration binary (future expansion)
```

**Binary 是什麼?** Go 編譯後產生的單一可執行檔, 不需要任何 runtime 就能直接執行。對比 .NET 的 `MyApp.dll` (需要 runtime), Go binary 是完全獨立的。只有 `package main` 的檔案才產生 binary, `internal/` 裡的程式碼是被編譯進去的, 不是獨立存在的

`main.go` 只負責讀設定、組裝元件、啟動 server, 不寫業務邏輯

對比 .NET:

| Go | .NET |
|----|------|
| `cmd/server/main.go` | `Program.cs` |

### internal/ 目錄

Go 語言層級的封裝機制。`internal/` 裡面的 package, 只有同一個 module 內的程式碼可以 import, 外部 module 完全無法使用

```
internal/
├── handler/      → equivalent to .NET Controller
├── service/      → equivalent to .NET Service (to be added)
└── repository/   → equivalent to .NET Repository (to be added)
```

`internal/` 本身不是 Service 也不是 Repository, 它只是**封裝邊界**, 裡面自己決定怎麼分層

.NET 也有類似概念:

```csharp
internal class UserService { }  // only accessible within the same assembly
```

Go 用目錄名稱來強制執行同樣的事, 編譯器層面直接阻擋外部 import

## go.mod 與 go.sum

### 是什麼?

| 檔案 | 對比 .NET | 用途 |
|------|-----------|------|
| `go.mod` | `*.csproj` | 定義 module 名稱、Go 版本、依賴套件 |
| `go.sum` | `packages.lock.json` | 每個依賴的 hash, 確保套件沒被竄改 |

`go.sum` 是自動產生的, 不需要手寫。**兩個都要 commit 進 git**, 這樣團隊每個人拿到的套件版本和內容完全一致

### 建立流程

```bash
mkdir my-project && cd my-project

go mod init github.com/yourname/my-project   # generates go.mod

# create directory structure, write code

go get github.com/go-chi/chi/v5              # add dependency, updates go.mod, generates go.sum

go mod tidy                                  # remove unused dependencies, add missing ones
```

## Router 選擇

業界 Go backend 幾乎都會加一個輕量 router:

| 選項 | 特性 |
|------|------|
| `chi` | 輕量、完全相容 `net/http`、業界常用 |
| `gin` | 功能完整, 也很主流 |
| 標準庫 | 純練習可以, 業界幾乎都加 router |

`chi` 是 `net/http` 的薄包裝, 最接近 Go idiom, 又符合業界實際做法

## 實作

### internal/handler/health.go

```go
package handler

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

func Health(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusOK)
    json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
}
```

### cmd/server/main.go

```go
package main

import (
    "log"
    "net/http"

    "github.com/go-chi/chi/v5"
    "github.com/go-chi/chi/v5/middleware"
    "github.com/lou/go-api/internal/handler"
)

func main() {
    r := chi.NewRouter()

    r.Use(middleware.Logger)
    r.Use(middleware.Recoverer)

    r.Get("/healthz", handler.Health)

    log.Println("server starting on :8080")
    if err := http.ListenAndServe(":8080", r); err != nil {
        log.Fatalf("server failed: %v", err)
    }
}
```

### /healthz 是什麼?

業界標配 endpoint, 用途:

- **Docker**: `HEALTHCHECK` 指令定期打這個 endpoint, 確認 container 是否存活
- **Kubernetes**: liveness probe / readiness probe
- **Load balancer**: 確認 instance 可以接流量

Response 就是 `200 OK` + `{"status": "ok"}`

## 幾個 Go 特有的問題

### 為什麼 `r` 是 pointer (`*http.Request`)?

Go 傳參數預設是複製整份資料 (pass by value)。`http.Request` 是大 struct, 裡面含 headers、body、URL、context 等, 傳 pointer 只傳 8 bytes 的記憶體位址, 不複製資料

另外, middleware 可能會修改 request (例如加 context), 用 pointer 才能讓修改對所有後續 handler 可見

`http.ResponseWriter` 沒有 `*` 是因為它本身已經是 interface, interface 在 Go 內部就是 pointer, 不需要再加

### 為什麼 `r` 沒用到但不報錯?

Go 不允許宣告變數卻不用, 但**函數參數例外**。`r *http.Request` 必須寫, 因為 chi 要求 handler 的函數簽名必須符合這個格式:

```go
func(http.ResponseWriter, *http.Request)
```

這是一個契約, 類似 .NET 的 interface。chi 不管你用不用 `r`, 它只看函數簽名是否符合

### `map[string]string` 是什麼?

Go 的 map 語法: `map[KeyType]ValueType`

```go
// Go
map[string]string{"status": "ok"}

// C# equivalent
new Dictionary<string, string> { { "status", "ok" } }
```

轉成 JSON 就是 `{"status": "ok"}`

### 每個檔案都要 import? 不能集中嗎?

Go 沒有全域 import, 每個檔案必須自己宣告用到的東西。這是刻意的設計, 讓每個檔案的依賴一目瞭然

- 標準庫寫短路徑: `"net/http"`、`"encoding/json"`
- 第三方套件寫完整路徑: `"github.com/go-chi/chi/v5"`
- **使用時只用最後一段的 package name**: `chi.NewRouter()`, 不是 `github.com/go-chi/chi/v5.NewRouter()`

名稱衝突時可加別名:

```go
import (
    chiMW "github.com/go-chi/chi/v5/middleware"
    myMW  "github.com/lou/go-api/internal/middleware"
)
```

## Handler 對比 .NET Controller

```go
// Go: handler is just a function
func Health(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusOK)
    json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
}
```

```csharp
// .NET: requires class + attribute
[ApiController]
[Route("api/[controller]")]
public class HealthController {
    [HttpGet]
    public IActionResult Get() {
        return Ok(new { status = "ok" });
    }
}
```

Go 沒有 class, handler 就是符合特定簽名的函數。Route 的設定在 `main.go` 集中管理, 不是用 attribute 分散在各個 controller 上

## middleware 的作用

```go
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
```

- `Logger`: 每個 request 進來都自動 log, 對比 .NET 的 `app.UseHttpLogging()`
- `Recoverer`: handler 如果 panic, 不讓整個 server 掛掉, 自動回 500, 對比 .NET 的 `app.UseExceptionHandler()`

## 本機測試

```bash
# start server
go run ./cmd/server/

# test in another terminal
curl http://localhost:8080/healthz

# expected response
{"status": "ok"}
```

## References

- [Go Documentation](https://go.dev/doc/) — 官方文件, 涵蓋語言規範與標準庫
- [Go Modules Reference](https://go.dev/ref/mod) — go.mod、go.sum、依賴管理的完整說明
- [Standard Go Project Layout](https://github.com/golang-standards/project-layout) — 社群整理的 Go 專案結構慣例, `cmd/`、`internal/` 等目錄的由來
- [chi router](https://github.com/go-chi/chi) — 輕量 HTTP router, 完全相容 `net/http`
