雖然我們進一步將原本擠在 ./cmd/olivine/main.go 中的實作原封不動塞進 ./cmd/olivine/app.go 中,但這其實並沒有實際解決問題。
客戶端的請求仍會直接在 App{} 中被處理,程式碼並不夠優雅,而且未來 App{} 可能會承載更多職責。比較好的方法應該是建立 Server{} 來接收並處理這些請求。
Go 的標準庫其實提供了很好的典範,它提供了 http.(*Server).ListenAndServe(),將整個 HTTP Server 的行為抽象出來;從 Go by Example: HTTP Server 可以看到它是怎麼使用。
Server{} 與 ListenAndServe()不幸的是,標準庫並沒有提供 net.(Listener).ListenAndServe() 函式,我們需要自行實作。
在 ./internal/server 中建立 Internal Package(內部套件),藉由放在 ./internal 底下,定義一個僅能在這個專案中被使用的私有套件做法,可以參考 Organize a Go module: Package or command with supporting packages。
// file: ./internal/server/server.go
package server
type Server interface interface {
ListenAndServe() error
}
func NewServer() Server { panic("not implemented") } // 尚未完成,這邊先拋出 panic
藉由定義一個 server.(Server) 的 Interface(介面) 提供 ListenAndServe(),並且在 main.(*App) 中套用:
// file: ./cmd/olivine/app.go
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
}
}
simpleSrv{}:對 Server{} 介面的實作建立一個 server.(*simpleSrv) 作為對 server.(Server) 介面的 Implementation(實作)。
// file: ./internal/server/server.go
// ...
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
}
}
Server{} 介面與 simpleSrv{} 實作?在上面的範例中,我先定義了一個 server.(Server) 介面,然後用 server.(*simpleSrv) 來實作。
對於現階段的專案而言,這無疑是一種過度設計:但這種設計的優勢在於未來設計者可以隨時抽換不同的實作,例如使用 cloudwego/netpoll 或 panjf2000/gnet 來獲得可能的性能提升。
註:Go 標準庫的設計不完全是以性能為優先,他們更加考慮程式庫的易讀性、可維護性與可移植性,所以在部份極端場景下有些公司會自己跳出來做高性能的解決方案。
例如字節跳動的開源 JSON 函式庫 bytedance/sonic,但這些函式庫可能會存在某些限制(例如在某些平台或指令集下沒有作用)