第 2 章
[Day 2] Application
App struct
We wrote all codes in main.go, and it is not a good idea: when project scales, developers puts lots of code, logic, logging, comments in single file, that will make everyone crazy.
I always create a App{} struct in cmd/olivine/app.go, it controls application lifecycle (bootstrap, run, close, …).
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
}
}
The Server implementation
I create App{} for application lifecycle handling, but I still write request handler in this sturct, it’s bad smell in code.
The http.Server in stdard libary provides ListenAndServe() function, there’s an example: Go by Exaple: HTTP Servers. However, net.Listener didn’t provide this function, it’s time to make it by self!
Create an “internal package” in internal/server, it is a private package can be used by olivine package only. For more info: 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
}
Why Server interface?
I built type Server interface with ListenAndServe() error function, and implement type simpleSrv struct{} for this interface.
But, why?
It’s undoubt over-design for this project at this moment, but I plan to implement other server implementations to compare performance or something interesting.
Where’s listener.Close()?
Unlike http.Server, there’s no Close() or Shutdown() in simpleSrv{}, not yet.
I’ll add Shutdown() for graceful shutdown in the future, maybe.