第 2 章
Day 2:應用程式架構
App struct
把所有程式碼都放在 main.go 並不是好主意。隨著專案成長,邏輯、紀錄與註解不斷擠進同一個檔案,會變得難以維護。
我通常會在 cmd/olivine/app.go 建立 App{} struct,管理應用程式生命週期,包含初始化、執行與關閉。
package main
import (
"bufio"
"fmt"
"log/slog"
"net"
"strings"
)
type App struct{}
func (app *App) Run() error {
slog.Info("starting olivine server...")
listener, err := net.Listen("tcp", ":16879")
if err != nil {
return fmt.Errorf("failed to listen tcp: %w", err)
}
defer listener.Close()
for {
connection, err := listener.Accept()
if err != nil {
slog.Error("failed to accept connection:", slog.Any("error", err))
continue
}
go handleConnection(connection)
}
}
func handleConnection(conn net.Conn) {
reader := bufio.NewReader(conn)
message, err := reader.ReadString('\n')
if err != nil {
slog.Error("failed to read from conn:", slog.Any("error", err))
return
}
ackmsg := strings.ToUpper(strings.ToUpper(message))
response := "ACK: " + ackmsg
if _, err := conn.Write([]byte(response)); err != nil {
slog.Error("failed to write to conn:", slog.Any("error", err))
return
}
}
Server 實作
雖然建立了 App{} 管理生命週期,但 request handler 仍寫在這個 struct 中,這是一個需要改善的設計。
標準函式庫的 http.Server 提供 ListenAndServe(),可以參考 Go by Example: HTTP Servers。net.Listener 沒有提供這個函式,因此要自己實作。
在 internal/server 建立 internal package,作為 olivine 專案內部使用的 package。相關說明見 Organize a Go module: Package or command with supporting packages。
package server
import (
"bufio"
"log/slog"
"net"
"strings"
)
type Server interface {
ListenAndServe() error
}
func NewServer() Server {
return &simpleSrv{}
}
type simpleSrv struct {
listener net.Listener
}
func (s *simpleSrv) ListenAndServe() (err error) {
if s.listener, err = net.Listen("tcp", ":16879"); err != nil {
return
}
for {
conn, err := s.listener.Accept()
if err != nil {
slog.Error("failed to accept connection:", slog.Any("error", err))
return err
}
go s.serve(conn)
}
}
func (s *simpleSrv) serve(conn net.Conn) {
defer conn.Close()
rd := bufio.NewReader(conn)
msg, err := rd.ReadString('\n')
if err != nil {
slog.Error("failed to read from conn:", slog.Any("error", err))
return
}
if _, err := conn.Write([]byte("ACK: " + strings.ToUpper(msg))); err != nil {
slog.Error("failed to write to conn:", slog.Any("error", err))
return
}
}
package main
import (
"log/slog"
"olivine/internal/server"
)
type App struct {
srv server.Server
}
func NewApp() *App {
return &App{
srv: server.NewServer(),
}
}
func (app *App) Run() error {
slog.Info("starting olivine server...")
if err := app.srv.ListenAndServe(); err != nil {
return err
}
return nil
}
為什麼使用 Server interface?
我建立了含有 ListenAndServe() error 的 type Server interface,再以 type simpleSrv struct{} 實作它。
目前這對專案來說確實有些過度設計,但我計畫加入其他 server 實作,用來比較效能或進行其他有趣的實驗。
listener.Close() 呢?
與 http.Server 不同,simpleSrv{} 目前還沒有 Close() 或 Shutdown()。
未來或許會加入 Shutdown(),實作 graceful shutdown。