feat: implement WebServer, Reloader interface and atomic hot-reload methods ReplaceProxies, ReplaceRewrites, ReplaceStatics (by AI)

This commit is contained in:
AI Engineer 2026-05-12 23:18:31 +08:00
parent c207bdd400
commit 3925767d2e
8 changed files with 236 additions and 97 deletions

3
.gitignore vendored
View File

@ -1,9 +1,8 @@
.log.meta.json
.ai/
.geminiignore
.gemini
env.json
env.yml
env.yaml
/CODE-FULL.md

3
go.mod
View File

@ -11,6 +11,7 @@ require (
apigo.cc/go/log v1.3.0
apigo.cc/go/redis v1.3.0
apigo.cc/go/safe v1.3.0
apigo.cc/go/starter v1.0.1
apigo.cc/go/timer v1.3.0
github.com/gorilla/websocket v1.5.3
golang.org/x/net v0.54.0
@ -22,7 +23,7 @@ require (
apigo.cc/go/encoding v1.3.0 // indirect
apigo.cc/go/rand v1.3.0 // indirect
apigo.cc/go/shell v1.3.0 // indirect
github.com/gomodule/redigo v1.9.3 // indirect
github.com/gomodule/redigo v2.0.0+incompatible // indirect
golang.org/x/crypto v0.51.0 // indirect
golang.org/x/sys v0.44.0 // indirect
golang.org/x/text v0.37.0 // indirect

68
go.sum
View File

@ -1,47 +1,43 @@
apigo.cc/go/cast v1.2.10 h1:wa9/hz6GW6Z+5co6l7LftMn2Eo06WpVHHDCCQphnmH8=
apigo.cc/go/cast v1.2.10/go.mod h1:lGlwImiOvHxG7buyMWhFzcdvQzmSaoKbmr7bcDfUpHk=
apigo.cc/go/config v1.0.8 h1:ZvontnJngNJrm6EJAPYmPhmBnLC9V7g5kZLiuN1MT60=
apigo.cc/go/config v1.0.8/go.mod h1:FCZj70MCejeWwv81O7sdpg0zmjOzglAMmNEfT3dQYzw=
apigo.cc/go/crypto v1.1.1 h1:AE0jNtKzcq4euz6fL9MAYEHQpbIEfDTHv2mriP/juig=
apigo.cc/go/crypto v1.1.1/go.mod h1:Q26As+TQrNs6olGkiVdD6649DJirxA4CUBT4oukKPuw=
apigo.cc/go/discover v1.0.11 h1:aeAC+xAwGlOeXsRptXJkEn8MvRZ7lom5N5jfBAg9/CE=
apigo.cc/go/discover v1.0.11/go.mod h1:TcIpl1Ocu51koRxugV81Jnz4NH0+Q5f5PF105VczS/0=
apigo.cc/go/encoding v1.1.2 h1:reSrLkyYrtZsf4S91XPdyBY2AQpvA43n9q0Q9wz5uJA=
apigo.cc/go/encoding v1.1.2/go.mod h1:iLuvrYHEK8mLnk8jijx5Sv1tInFreny0yGNBouA1d20=
apigo.cc/go/file v1.0.8 h1:GPkixU080cvrmz7cbdXkC2DqMvsWWyY3UzoyUVQYFvs=
apigo.cc/go/file v1.0.8/go.mod h1:T/wYji/va0S+JM2fAHonhKpnXKIELk/bmgnFEgMMY2s=
apigo.cc/go/http v1.0.11 h1:EOlMXlTGrWY0RI3MynkV7noT49WiUdGVPdOtDJjIkU4=
apigo.cc/go/http v1.0.11/go.mod h1:K2JgyI7DblfbzAnK1OHx4PS/1Pvcoqcp3g2uwsCPe68=
apigo.cc/go/id v1.0.7 h1:vXCK8mUW3s4cJYmli0o2BxgyI9XbJrG8gSGJOP2Fe4g=
apigo.cc/go/id v1.0.7/go.mod h1:wXBrPpcEpyUDM7bp7M5uPM9zFw4VcnvXMQLw4Yd+uZE=
apigo.cc/go/log v1.1.16 h1:uqPqeHvs+FdNupLBzzamJmY4oHAqtPEkGuW/pW5i2nQ=
apigo.cc/go/log v1.1.16/go.mod h1:bOfPXjrX2bY+FNG9eEtBnvaVXoxZDGvz0jQfF3s/mYk=
apigo.cc/go/rand v1.0.6 h1:p51rkaDrYUdZPIRbQAujZmQelWg2ipAMts33A/tG7QE=
apigo.cc/go/rand v1.0.6/go.mod h1:mZ/4Soa3bk+XvDaqPWJuUe1bfEi4eThBj1XmEAuYxsk=
apigo.cc/go/redis v1.0.9 h1:5MnCchcDgoVnGHQn+KJF9PJXXRayK9nlZg+Q0lTnMoU=
apigo.cc/go/redis v1.0.9/go.mod h1:SVOD7iuUL/jxYEa28qObDQf4GY9UVSKjhM9vVj9TXLI=
apigo.cc/go/safe v1.0.7 h1:f0d+v9K2dHPyG5DNqhyddCmAmSiIqIfkPi/AMED/iQI=
apigo.cc/go/safe v1.0.7/go.mod h1:Hu7TVDWPe/I+nBZfYJH4mt+ROzG+rwk2D1zHTXj/2eE=
apigo.cc/go/shell v1.0.6 h1:RngaSMr2AkAFDl545A1Ln+D8ckqV2jknUp4PohDaLIA=
apigo.cc/go/shell v1.0.6/go.mod h1:X7Nozjd7oau4nvAJCI21vxrxfd4ZL5nE4C6eUsmi2Hc=
apigo.cc/go/timer v1.0.7 h1:QUH0t7l9kBiGU/QdDNSthnXLfJXOEp+mpdY2+QPlrEI=
apigo.cc/go/timer v1.0.7/go.mod h1:kOnqTTX+zA4AH7SfC+LpUm4ZvS+DVyWWMqul/V5QWJs=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gomodule/redigo v1.9.3 h1:dNPSXeXv6HCq2jdyWfjgmhBdqnR6PRO3m/G05nvpPC8=
github.com/gomodule/redigo v1.9.3/go.mod h1:KsU3hiK/Ay8U42qpaJk+kuNa3C+spxapWpM+ywhcgtw=
apigo.cc/go/cast v1.3.0 h1:ZTcLYijkqZjSWSCSpJUWMfzJYeJKbwKxquKkPrFsROQ=
apigo.cc/go/cast v1.3.0/go.mod h1:lGlwImiOvHxG7buyMWhFzcdvQzmSaoKbmr7bcDfUpHk=
apigo.cc/go/config v1.3.0 h1:TwI3bv3D+BJrAnFx+o62HQo3FarY2Ge3SCGsKchFYGg=
apigo.cc/go/config v1.3.0/go.mod h1:88lqKEBXlIExFKt1geLONVLYyM+QhRVpBe0ok3OEvjI=
apigo.cc/go/crypto v1.3.0 h1:rGRrrb5O+4M50X5hVUmJQbXx3l87zzlcgzGtUvZrZL8=
apigo.cc/go/crypto v1.3.0/go.mod h1:uSCcmbcFoiltUPMQTSuqmU9nfKEH/lRs7nQ7aa3Z4Mc=
apigo.cc/go/discover v1.3.0 h1:CXuKtAZygU+4TMHtebVkjWyyWmPgoLbsJFdKFGiCOd8=
apigo.cc/go/discover v1.3.0/go.mod h1:VMu1qC6AngVFQMdaCwGoq3/PPX0xDnjkG+1AcSA+Zvs=
apigo.cc/go/encoding v1.3.0 h1:8jqNHoZBR8vOU/BGsLFebfp1Txa1UxDRpd7YwzIFLJs=
apigo.cc/go/encoding v1.3.0/go.mod h1:kT/uUJiuAOkZ4LzUWrUtk/I0iL1D8aatvD+59bDnHBo=
apigo.cc/go/file v1.3.0 h1:xG9FcY3Rv6Br83r9pq9QsIXFrplx4g8ITOkHSzfzXRg=
apigo.cc/go/file v1.3.0/go.mod h1:pYHBlB/XwsrnWpEh7GIFpbiqobrExfiB+rEN8V2d2kY=
apigo.cc/go/http v1.3.0 h1:1ZweotOuAxTI8wfib9knWYXM2t0POOJ3ezgOKObH3sg=
apigo.cc/go/http v1.3.0/go.mod h1:DC3phxBNbt/dOWdhxtffAEYeUs3j6P3BD8e6J8gxU9U=
apigo.cc/go/id v1.3.0 h1:Tr2Yj0Rl19lfwW5wBTJ407o/zgo2oVRLE20WWEgJzdE=
apigo.cc/go/id v1.3.0/go.mod h1:AFH3kMFwENfXNyijnAFWEhSF1o3y++UBPem1IUlrcxA=
apigo.cc/go/log v1.3.0 h1:61Z80WGN6SnhgxgoR8xuVYIieMdjlJKmf8JX1HXzp0Y=
apigo.cc/go/log v1.3.0/go.mod h1:dz4bSz9BnOgutkUJJZfX3uDDwsMpUxt7WF50mLK9hgE=
apigo.cc/go/rand v1.3.0 h1:k+UFAhMySwXf+dq8Om9TniZV6fm6gAE0evbrqMEdwQU=
apigo.cc/go/rand v1.3.0/go.mod h1:mZ/4Soa3bk+XvDaqPWJuUe1bfEi4eThBj1XmEAuYxsk=
apigo.cc/go/redis v1.3.0 h1:3NJE3xPXzhCwL+Mh1iyphFrsKWEuPlY26LHJfMVFSeU=
apigo.cc/go/redis v1.3.0/go.mod h1:KPDPwMOER7WJX3Qev24LTeAOSmCl8OApe8iagPDxOUQ=
apigo.cc/go/safe v1.3.0 h1:uctdAUsphT9p60Tk4oS5xPCe0NoIdOHfsYv4PNS0Rok=
apigo.cc/go/safe v1.3.0/go.mod h1:tC9X14V+qh0BqIrVg4UkXbl+2pEN+lj2ZNI8IjDB6Fs=
apigo.cc/go/shell v1.3.0 h1:hdxuYPN/7T2BuM/Ja8AjVUhbRqU/wpi8OjcJVziJ0nw=
apigo.cc/go/shell v1.3.0/go.mod h1:aNJiRWibxlA485yX3t+07IVAbrALKmxzv4oGEUC+hK4=
apigo.cc/go/starter v1.0.1 h1:7Qv/rRlEVlTX7wjr1LpV1XX1wUD4UAssDi6J+YCh73s=
apigo.cc/go/starter v1.0.1/go.mod h1:xHfo+36hXGdVhhnRqd1l+Vk6Fp1ecN2LDAcsDOVodXk=
apigo.cc/go/timer v1.3.0 h1:dorVGKw0xR6Gj8Pwfl86K46szMBfD31XyO+uUqxU+EI=
apigo.cc/go/timer v1.3.0/go.mod h1:kOnqTTX+zA4AH7SfC+LpUm4ZvS+DVyWWMqul/V5QWJs=
github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0=
github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w=

View File

@ -162,3 +162,31 @@ func copyResponse(res *gohttp.Result, response *Response, logger *log.Logger) {
}
}
}
// ProxyRule 定义了外部传递或 Redis 中获取的代理规则配置
type ProxyRule struct {
Path string // 匹配路径或正则,支持变量捕获如 ^/api/(.*)$
AuthLevel int // 所需鉴权级别
ToApp string // 目标 AppName 或完整 URL (可含 $1 变量替换)
ToPath string // 目标路径 (可含 $1 变量替换)
}
// ReplaceProxies 使用全量指针替换的方式 (Copy-on-Write) 无缝更新指定 host 的所有代理规则。
// 该方法非常轻量,仅在赋值瞬间短暂持有写锁,不会阻塞任何并发请求,并且自动淘汰旧规则。
func ReplaceProxies(host string, rules []ProxyRule) {
newProxies := make([]*proxyType, 0, len(rules))
for _, r := range rules {
p := &proxyType{authLevel: r.AuthLevel, fromPath: r.Path, toApp: r.ToApp, toPath: r.ToPath}
if strings.ContainsRune(r.Path, '(') {
matcher, err := regexp.Compile("^" + r.Path + "$")
if err == nil {
p.matcher = matcher
}
}
newProxies = append(newProxies, p)
}
hostPoliciesLock.Lock()
defer hostPoliciesLock.Unlock()
hostProxies[host] = newProxies
}

33
reload.go Normal file
View File

@ -0,0 +1,33 @@
package service
import (
"apigo.cc/go/log"
"sync"
)
var (
reloadHooks []func() error
reloadLock sync.RWMutex
)
// OnReload 注册一个在接收到 SIGHUP 信号时触发的重新加载钩子
func OnReload(handler func() error) {
reloadLock.Lock()
defer reloadLock.Unlock()
reloadHooks = append(reloadHooks, handler)
}
// triggerReload 触发所有注册的重新加载钩子
func triggerReload() error {
reloadLock.RLock()
hooks := make([]func() error, len(reloadHooks))
copy(hooks, reloadHooks)
reloadLock.RUnlock()
for _, hook := range hooks {
if err := hook(); err != nil {
log.DefaultLogger.Error("reload hook failed", "error", err.Error())
}
}
return nil
}

View File

@ -101,3 +101,28 @@ func processRewrite(request *Request, response *Response, logger *log.Logger) bo
return false
}
// RewriteRule 定义了外部传递的 URL 重写规则
type RewriteRule struct {
Path string // 原始路径或匹配正则,例如 ^/old/(.*)$
ToPath string // 重写后的路径,例如 /new/$1
}
// ReplaceRewrites 使用 Copy-on-Write 机制原子地替换指定 host 下的所有重写规则。
func ReplaceRewrites(host string, rules []RewriteRule) {
newRewrites := make([]*rewriteType, 0, len(rules))
for _, r := range rules {
s := &rewriteType{fromPath: r.Path, toPath: r.ToPath}
if strings.ContainsRune(r.Path, '(') {
matcher, err := regexp.Compile("^" + r.Path + "$")
if err == nil {
s.matcher = matcher
}
}
newRewrites = append(newRewrites, s)
}
hostPoliciesLock.Lock()
defer hostPoliciesLock.Unlock()
hostRewrites[host] = newRewrites
}

144
server.go
View File

@ -5,6 +5,7 @@ import (
"apigo.cc/go/log"
"apigo.cc/go/redis"
"apigo.cc/go/safe"
"apigo.cc/go/starter"
"context"
"fmt"
"golang.org/x/net/http2"
@ -12,46 +13,35 @@ import (
"net"
"net/http"
"os"
"os/signal"
"strings"
"syscall"
"time"
)
// GlobalDiscoverer 供服务框架内部使用的发现实例
var GlobalDiscoverer *discover.Discoverer
// AsyncServer 异步服务实例
type AsyncServer struct {
// WebServer 实现了 starter.Service 和 starter.Reloader 接口
type WebServer struct {
server *http.Server
listener net.Listener
Addr string
stopChan chan os.Signal
startChan chan bool
useDiscover bool
discoverer *discover.Discoverer
}
// AsyncStart 异步启动服务
func AsyncStart() *AsyncServer {
as := &AsyncServer{
startChan: make(chan bool, 1),
stopChan: make(chan os.Signal, 1),
// NewWebServer 创建并返回一个新的 WebServer 实例
func NewWebServer() *WebServer {
return &WebServer{}
}
go as.start()
<-as.startChan
return as
}
func (as *AsyncServer) start() {
// Start 启动服务,实现 starter.Service 接口
func (ws *WebServer) Start(ctx context.Context, logger *log.Logger) error {
listenStr := Config.Listen
as.useDiscover = false
ws.useDiscover = false
if listenStr == "" {
listenStr = ":0,h2c"
as.useDiscover = true
ws.useDiscover = true
}
// 解析第一个监听配置
@ -76,7 +66,7 @@ func (as *AsyncServer) start() {
}
appName := Config.App
if appName != "" || Config.Register != "" {
as.useDiscover = true
ws.useDiscover = true
}
// 初始化服务器唯一标识 (8位物理上限 3,844/s)
@ -92,18 +82,16 @@ func (as *AsyncServer) start() {
listener, err := net.Listen("tcp", addr)
if err != nil {
log.DefaultLogger.Error("failed to listen", "addr", addr, "error", err.Error())
as.startChan <- false
return
return fmt.Errorf("failed to listen on %s: %w", addr, err)
}
as.listener = listener
as.Addr = listener.Addr().String()
serverAddr = as.Addr
ws.listener = listener
ws.Addr = listener.Addr().String()
serverAddr = ws.Addr
// 如果使用了随机端口且没有明确指定不需要服务发现,则开启
if addr == ":0" || strings.HasSuffix(addr, ":0") {
as.useDiscover = true
ws.useDiscover = true
}
h2s := &http2.Server{}
@ -112,7 +100,7 @@ func (as *AsyncServer) start() {
handler = h2c.NewHandler(handler, h2s)
}
as.server = &http.Server{
ws.server = &http.Server{
Handler: handler,
ReadTimeout: time.Duration(Config.ReadTimeout) * time.Millisecond,
ReadHeaderTimeout: time.Duration(Config.ReadHeaderTimeout) * time.Millisecond,
@ -122,8 +110,8 @@ func (as *AsyncServer) start() {
}
// 启动服务发现
if as.useDiscover {
_, port, _ := net.SplitHostPort(as.Addr)
if ws.useDiscover {
_, port, _ := net.SplitHostPort(ws.Addr)
ip := GetServerIp()
discoverAddr := fmt.Sprintf("%s:%s", ip, port)
@ -163,52 +151,98 @@ func (as *AsyncServer) start() {
registry = "127.0.0.1:6379::15" // Default fallback
}
as.discoverer = discover.Start(registry, appName, discoverAddr, log.DefaultLogger, discConf)
GlobalDiscoverer = as.discoverer
if as.discoverer != nil {
log.DefaultLogger.Info("discover registered", "app", appName, "addr", discoverAddr)
ws.discoverer = discover.Start(registry, appName, discoverAddr, logger, discConf)
GlobalDiscoverer = ws.discoverer
if ws.discoverer != nil {
logger.Info("discover registered", "app", appName, "addr", discoverAddr)
}
}
signal.Notify(as.stopChan, os.Interrupt, syscall.SIGTERM)
errChan := make(chan error, 1)
go func() {
log.DefaultLogger.Info("service starting", "addr", as.Addr, "proto", protocol)
as.startChan <- true
if err := as.server.Serve(listener); err != nil && err != http.ErrServerClosed {
log.DefaultLogger.Error("server error", "error", err.Error())
logger.Info("service starting", "addr", ws.Addr, "proto", protocol)
if err := ws.server.Serve(listener); err != nil && err != http.ErrServerClosed {
errChan <- err
}
close(errChan)
}()
// 短暂等待验证是否闪退
select {
case err := <-errChan:
if err != nil {
return err
}
case <-time.After(100 * time.Millisecond):
}
// Stop 停止服务
func (as *AsyncServer) Stop() {
return nil
}
// Stop 停止服务,实现 starter.Service 接口
func (ws *WebServer) Stop(ctx context.Context) error {
log.DefaultLogger.Info("service stopping")
if as.discoverer != nil {
as.discoverer.Stop()
if ws.discoverer != nil {
ws.discoverer.Stop()
}
if ws.server != nil {
if err := ws.server.Shutdown(ctx); err != nil {
return err
}
}
log.DefaultLogger.Info("service stopped")
return nil
}
// Health 检查服务健康状态,实现 starter.Service 接口
func (ws *WebServer) Health() error {
if ws.server == nil {
return fmt.Errorf("server is not running")
}
return nil
}
// Reload 实现配置重新加载,实现 starter.Reloader 接口
func (ws *WebServer) Reload() error {
log.DefaultLogger.Info("reloading configurations...")
return triggerReload()
}
// AsyncServer 兼容旧版异步服务实例
type AsyncServer struct {
*WebServer
}
// Stop 兼容旧版的无参数停止方法
func (as *AsyncServer) Stop() {
stopTimeout := time.Duration(Config.StopTimeout) * time.Millisecond
if stopTimeout <= 0 {
stopTimeout = 5 * time.Second
}
ctx, cancel := context.WithTimeout(context.Background(), stopTimeout)
defer cancel()
if err := as.server.Shutdown(ctx); err != nil {
log.DefaultLogger.Error("server shutdown error", "error", err.Error())
}
log.DefaultLogger.Info("service stopped")
_ = as.WebServer.Stop(ctx)
}
// Wait 等待服务结束 (信号监听)
// AsyncStart 兼容旧版的异步启动方法
func AsyncStart() *AsyncServer {
ws := NewWebServer()
_ = ws.Start(context.Background(), log.DefaultLogger)
return &AsyncServer{WebServer: ws}
}
// Wait 等待服务结束 (兼容旧版,直接阻塞)
func (as *AsyncServer) Wait() {
<-as.stopChan
as.Stop()
select {}
}
// Start 同步启动服务
// Start 兼容旧版的同步启动方法 (通过内部注册 starter 实现)
func Start() {
AsyncStart().Wait()
stopTimeout := time.Duration(Config.StopTimeout) * time.Millisecond
if stopTimeout <= 0 {
stopTimeout = 5 * time.Second
}
starter.Register("web-server", NewWebServer(), 100, 5*time.Second, stopTimeout)
starter.Run()
}

View File

@ -43,6 +43,29 @@ func StaticByHost(path, rootPath, host string) {
}
}
// ReplaceStatics 使用 Copy-on-Write 机制原子地替换指定 host 下的所有静态目录规则
func ReplaceStatics(host string, config map[string]string) {
newStatics := make(map[string]*string, len(config))
for path, rootPath := range config {
rp := rootPath
if !filepath.IsAbs(rp) {
if absPath, err := filepath.Abs(rp); err == nil {
rp = absPath
}
}
newStatics[path] = &rp
}
staticsByHostLock.Lock()
defer staticsByHostLock.Unlock()
if host == "" {
statics = newStatics
} else {
staticsByHost[host] = newStatics
}
}
func getStaticFilePath(requestPath, host string) string {
staticsByHostLock.RLock()
defer staticsByHostLock.RUnlock()