iT邦幫忙

2026 iThome 鐵人賽

DAY 6
0

雖然我們進一步將原本擠在 ./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/netpollpanjf2000/gnet 來獲得可能的性能提升。

註:Go 標準庫的設計不完全是以性能為優先,他們更加考慮程式庫的易讀性、可維護性與可移植性,所以在部份極端場景下有些公司會自己跳出來做高性能的解決方案。

例如字節跳動的開源 JSON 函式庫 bytedance/sonic,但這些函式庫可能會存在某些限制(例如在某些平台或指令集下沒有作用)


上一篇
[Day 05] App:應用程式基礎建設
系列文
從零開始打造 Redis:以 Go 建立 Production Ready 應用程式6
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言