Slim down service module: remove redundant features and optimize ID generation (by AI)
This commit is contained in:
parent
bdb104aa2f
commit
5b63fd83a9
54
README.md
54
README.md
@ -3,10 +3,11 @@
|
||||
极简、自动化的 Web 与 WebSocket 服务框架,实现极致的依赖注入与路由映射。
|
||||
|
||||
## 核心特性
|
||||
- **极致精简**: 剥离非核心组件(如 Starter, Task, 业务 Result 定义),保持底座纯净。
|
||||
- **路由反射**: 自动解析函数参数,支持 `*Request`, `*Response`, `*log.Logger` 及自定义结构体自动注入。
|
||||
- **自动校验**: 集成 `verify` 引擎,通过 Struct Tag 实现入参合法性自动检查。
|
||||
- **功能闭环**: 内置静态文件服务、WebSocket (带 Action 路由)、URL 重写、反向代理(对接 Discover)。
|
||||
- **零摩擦启动**: 支持命令行指令管理 (start/stop/help) 及异步平滑启停。
|
||||
- **功能闭环**: 内置静态文件服务、基础 WebSocket 注册、URL 重写、反向代理(对接 Discover)。
|
||||
- **统一 ID 体系**: 整合 Redis 集群版 ID 生成器,全局统一生成 `RequestId` 与 `LogTraceID`。
|
||||
|
||||
## API 指南
|
||||
|
||||
@ -14,39 +15,42 @@
|
||||
```go
|
||||
import "apigo.cc/go/service"
|
||||
|
||||
// 注册标准 Web 服务
|
||||
service.Register(0, "/hello", func(in struct{ Name string }) string {
|
||||
// 注册标准 Web 服务,自动注入 Struct 参数并执行校验
|
||||
service.Register(0, "/hello", func(in struct{ Name string `verify:"length:2+"` }) string {
|
||||
return "Hello " + in.Name
|
||||
}, "打招呼接口")
|
||||
|
||||
// 注册 Restful 服务
|
||||
service.Restful(0, "POST", "/user/{id}", func(args map[string]any) service.Result {
|
||||
res := service.Result{}
|
||||
res.OK()
|
||||
return res
|
||||
}, "更新用户")
|
||||
```
|
||||
|
||||
### 2. WebSocket 支持
|
||||
### 2. WebSocket 支持 (极简模式)
|
||||
```go
|
||||
ar := service.RegisterWebsocket(0, "/ws", onOpen, onClose, "聊天室")
|
||||
ar.RegisterAction(0, "chat", func(in ChatMessage, sess *MySession) {
|
||||
// 处理消息
|
||||
}, "发送消息")
|
||||
// 业务自行处理消息循环与逻辑
|
||||
service.RegisterWebsocket(0, "/ws", func(conn *websocket.Conn, logger *log.Logger) {
|
||||
defer conn.Close()
|
||||
for {
|
||||
_, msg, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
logger.Info("received", "msg", string(msg))
|
||||
}
|
||||
}, "聊天室")
|
||||
```
|
||||
|
||||
### 3. 增强插件
|
||||
### 3. 生命周期管理
|
||||
```go
|
||||
func main() {
|
||||
// 异步启动
|
||||
as := service.AsyncStart()
|
||||
// 执行其他初始化...
|
||||
as.Wait() // 阻塞并监听信号优雅退出
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 增强插件
|
||||
- **静态文件**: `service.Static("/ui", "./static_dir")`
|
||||
- **URL 重写**: `service.Rewrite("/old", "/new")`
|
||||
- **反向代理**: `service.Proxy(0, "/api", "other_app", "/api")`
|
||||
|
||||
### 4. 生命周期管理
|
||||
```go
|
||||
func main() {
|
||||
service.CheckCmd() // 处理 start/stop/help 指令
|
||||
service.Start() // 阻塞启动
|
||||
}
|
||||
```
|
||||
- **文档生成**: `service.MakeDocument()` 返回全量接口描述
|
||||
|
||||
## 基础设施对齐
|
||||
- **类型转换**: `apigo.cc/go/cast`
|
||||
|
||||
29
document.go
29
document.go
@ -77,10 +77,10 @@ func MakeDocument() []Api {
|
||||
AuthLevel: a.authLevel,
|
||||
Memo: a.memo,
|
||||
}
|
||||
if a.openFuncType != nil && a.openFuncType.NumIn() > 0 {
|
||||
if a.handlerType != nil && a.handlerType.NumIn() > 0 {
|
||||
// Find struct in
|
||||
for i := 0; i < a.openFuncType.NumIn(); i++ {
|
||||
t := a.openFuncType.In(i)
|
||||
for i := 0; i < a.handlerType.NumIn(); i++ {
|
||||
t := a.handlerType.In(i)
|
||||
if t.Kind() == reflect.Struct {
|
||||
api.In = getType(t)
|
||||
break
|
||||
@ -88,22 +88,6 @@ func MakeDocument() []Api {
|
||||
}
|
||||
}
|
||||
out = append(out, api)
|
||||
|
||||
for name, act := range a.actions {
|
||||
actionApi := Api{
|
||||
Type: "Action",
|
||||
Path: name,
|
||||
AuthLevel: act.authLevel,
|
||||
Memo: act.memo,
|
||||
}
|
||||
if act.inType != nil {
|
||||
actionApi.In = getType(act.inType)
|
||||
}
|
||||
if act.funcType.NumOut() > 0 {
|
||||
actionApi.Out = getType(act.funcType.Out(0))
|
||||
}
|
||||
out = append(out, actionApi)
|
||||
}
|
||||
}
|
||||
websocketServicesLock.RUnlock()
|
||||
|
||||
@ -153,10 +137,3 @@ func getType(t reflect.Type) any {
|
||||
return t.String()
|
||||
}
|
||||
}
|
||||
|
||||
// 自动注册文档服务
|
||||
func init() {
|
||||
Register(0, "/__DOC__", func() string {
|
||||
return MakeJsonDocument()
|
||||
}, "API Document")
|
||||
}
|
||||
|
||||
@ -2,7 +2,6 @@ package service
|
||||
|
||||
import (
|
||||
"apigo.cc/go/cast"
|
||||
"apigo.cc/go/id"
|
||||
"apigo.cc/go/log"
|
||||
"apigo.cc/go/standard"
|
||||
"encoding/json"
|
||||
@ -25,7 +24,7 @@ func (rh *routeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
startTime := time.Now()
|
||||
requestId := r.Header.Get(standard.DiscoverHeaderRequestId)
|
||||
if requestId == "" {
|
||||
requestId = id.MakeID(12)
|
||||
requestId = MakeId(12)
|
||||
r.Header.Set(standard.DiscoverHeaderRequestId, requestId)
|
||||
}
|
||||
|
||||
@ -278,7 +277,7 @@ func handleClientKeys(request *Request, response *Response) {
|
||||
if sessionIdMaker != nil {
|
||||
sessionId = sessionIdMaker()
|
||||
} else {
|
||||
sessionId = id.MakeID(14)
|
||||
sessionId = MakeId(14)
|
||||
}
|
||||
if !Config.SessionWithoutCookie {
|
||||
http.SetCookie(response.Writer, &http.Cookie{
|
||||
@ -302,7 +301,7 @@ func handleClientKeys(request *Request, response *Response) {
|
||||
}
|
||||
}
|
||||
if deviceId == "" {
|
||||
deviceId = id.MakeID(14)
|
||||
deviceId = MakeId(14)
|
||||
if !Config.DeviceWithoutCookie {
|
||||
http.SetCookie(response.Writer, &http.Cookie{
|
||||
Name: usedDeviceIdKey,
|
||||
|
||||
@ -9,6 +9,12 @@ import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Map 通用 Map 类型
|
||||
type Map = map[string]any
|
||||
|
||||
// Arr 通用切片类型
|
||||
type Arr = []any
|
||||
|
||||
// WebServiceOptions 服务注册选项
|
||||
type WebServiceOptions struct {
|
||||
Priority int
|
||||
@ -58,9 +64,8 @@ var (
|
||||
webServicesList = make([]*webServiceType, 0)
|
||||
|
||||
websocketServices = make(map[string]*websocketServiceType)
|
||||
regexWebsocketServices = make([]*websocketServiceType, 0)
|
||||
websocketServicesLock = sync.RWMutex{}
|
||||
websocketServicesList = make([]*websocketServiceType, 0)
|
||||
websocketServicesList = make([]*webServiceType, 0)
|
||||
|
||||
// 过滤器与拦截器
|
||||
inFilters = make([]func(*map[string]any, *Request, *Response, *log.Logger) any, 0)
|
||||
|
||||
49
starter.go
49
starter.go
@ -1,49 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// StartCmd 命令行命令定义
|
||||
type StartCmd struct {
|
||||
Name string
|
||||
Comment string
|
||||
Func func()
|
||||
}
|
||||
|
||||
var startCmds = []StartCmd{
|
||||
{"start", "Start server", Start},
|
||||
}
|
||||
|
||||
// AddCmd 添加自定义命令行命令
|
||||
func AddCmd(name, comment string, function func()) {
|
||||
startCmds = append(startCmds, StartCmd{name, comment, function})
|
||||
}
|
||||
|
||||
// CheckCmd 检查并执行命令行命令
|
||||
func CheckCmd() {
|
||||
if len(os.Args) > 1 {
|
||||
cmd := os.Args[1]
|
||||
if cmd == "help" || cmd == "--help" {
|
||||
showHelp()
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
for _, cmdInfo := range startCmds {
|
||||
if cmd == cmdInfo.Name {
|
||||
cmdInfo.Func()
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func showHelp() {
|
||||
fmt.Printf("Usage: %s [command]\n\n", filepath.Base(os.Args[0]))
|
||||
fmt.Println("Available commands:")
|
||||
for _, cmdInfo := range startCmds {
|
||||
fmt.Printf(" %-10s %s\n", cmdInfo.Name, cmdInfo.Comment)
|
||||
}
|
||||
}
|
||||
68
types.go
68
types.go
@ -1,68 +0,0 @@
|
||||
package service
|
||||
|
||||
// Map 通用 Map 类型
|
||||
type Map = map[string]any
|
||||
|
||||
// Arr 通用切片类型
|
||||
type Arr = []any
|
||||
|
||||
// Argot 错误码/标识符类型
|
||||
type Argot string
|
||||
|
||||
// Result 通用返回结构
|
||||
type Result struct {
|
||||
Ok bool `json:"ok"`
|
||||
Argot Argot `json:"argot,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
// CodeResult 带状态码的返回结构
|
||||
type CodeResult struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
// ArgotInfo 标识符信息(用于文档生成)
|
||||
type ArgotInfo struct {
|
||||
Name Argot
|
||||
Memo string
|
||||
}
|
||||
|
||||
// OK 设置成功状态
|
||||
func (r *Result) OK(argots ...Argot) {
|
||||
r.Ok = true
|
||||
if len(argots) > 0 {
|
||||
r.Argot = argots[0]
|
||||
}
|
||||
}
|
||||
|
||||
// Failed 设置失败状态
|
||||
func (r *Result) Failed(message string, argots ...Argot) {
|
||||
r.Ok = false
|
||||
r.Message = message
|
||||
if len(argots) > 0 {
|
||||
r.Argot = argots[0]
|
||||
}
|
||||
}
|
||||
|
||||
// Done 根据布尔值设置状态
|
||||
func (r *Result) Done(ok bool, failedMessage string, argots ...Argot) {
|
||||
r.Ok = ok
|
||||
if !ok {
|
||||
r.Message = failedMessage
|
||||
if len(argots) > 0 {
|
||||
r.Argot = argots[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// OK 设置成功状态 (Code=1)
|
||||
func (r *CodeResult) OK() {
|
||||
r.Code = 1
|
||||
}
|
||||
|
||||
// Failed 设置失败状态与错误码
|
||||
func (r *CodeResult) Failed(code int, message string) {
|
||||
r.Code = code
|
||||
r.Message = message
|
||||
}
|
||||
@ -1,41 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestResult(t *testing.T) {
|
||||
r := &Result{}
|
||||
r.OK()
|
||||
if !r.Ok {
|
||||
t.Error("Result.OK() failed")
|
||||
}
|
||||
|
||||
r.Failed("error", Argot("ERR_CODE"))
|
||||
if r.Ok || r.Message != "error" || r.Argot != "ERR_CODE" {
|
||||
t.Error("Result.Failed() failed")
|
||||
}
|
||||
|
||||
r.Done(true, "never")
|
||||
if !r.Ok {
|
||||
t.Error("Result.Done(true) failed")
|
||||
}
|
||||
|
||||
r.Done(false, "failed", Argot("FAIL"))
|
||||
if r.Ok || r.Message != "failed" || r.Argot != "FAIL" {
|
||||
t.Error("Result.Done(false) failed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCodeResult(t *testing.T) {
|
||||
cr := &CodeResult{}
|
||||
cr.OK()
|
||||
if cr.Code != 1 {
|
||||
t.Error("CodeResult.OK() failed")
|
||||
}
|
||||
|
||||
cr.Failed(500, "internal error")
|
||||
if cr.Code != 500 || cr.Message != "internal error" {
|
||||
t.Error("CodeResult.Failed() failed")
|
||||
}
|
||||
}
|
||||
47
utility.go
47
utility.go
@ -2,19 +2,60 @@ package service
|
||||
|
||||
import (
|
||||
"apigo.cc/go/id"
|
||||
"apigo.cc/go/log"
|
||||
"apigo.cc/go/redis"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var (
|
||||
idMaker IDMakerInterface
|
||||
idMakerLock sync.Mutex
|
||||
)
|
||||
|
||||
// IDMakerInterface ID 生成器接口
|
||||
type IDMakerInterface interface {
|
||||
Get(size int) string
|
||||
GetForMysql(size int) string
|
||||
GetForPostgreSQL(size int) string
|
||||
}
|
||||
|
||||
func getIDMaker() IDMakerInterface {
|
||||
if idMaker != nil {
|
||||
return idMaker
|
||||
}
|
||||
|
||||
idMakerLock.Lock()
|
||||
defer idMakerLock.Unlock()
|
||||
|
||||
if idMaker != nil {
|
||||
return idMaker
|
||||
}
|
||||
|
||||
if Config.IdServer != "" {
|
||||
rd := redis.GetRedis(Config.IdServer, log.DefaultLogger)
|
||||
if rd.Error == nil {
|
||||
idMaker = redis.NewIDMaker(rd)
|
||||
}
|
||||
}
|
||||
|
||||
if idMaker == nil {
|
||||
idMaker = id.DefaultIDMaker
|
||||
}
|
||||
|
||||
return idMaker
|
||||
}
|
||||
|
||||
// MakeId 生成指定长度的 ID
|
||||
func MakeId(size int) string {
|
||||
return id.MakeID(size)
|
||||
return getIDMaker().Get(size)
|
||||
}
|
||||
|
||||
// MakeIdForMysql 生成适用于 MySQL 的有序 ID
|
||||
func MakeIdForMysql(size int) string {
|
||||
return id.DefaultIDMaker.GetForMysql(size)
|
||||
return getIDMaker().GetForMysql(size)
|
||||
}
|
||||
|
||||
// MakeIdForPostgreSQL 生成适用于 PostgreSQL 的有序 ID
|
||||
func MakeIdForPostgreSQL(size int) string {
|
||||
return id.DefaultIDMaker.GetForPostgreSQL(size)
|
||||
return getIDMaker().GetForPostgreSQL(size)
|
||||
}
|
||||
|
||||
142
websocket.go
142
websocket.go
@ -1,97 +1,42 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"apigo.cc/go/cast"
|
||||
"apigo.cc/go/log"
|
||||
"github.com/gorilla/websocket"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
// websocketServiceType WebSocket 服务元数据
|
||||
type websocketServiceType struct {
|
||||
authLevel int
|
||||
path string
|
||||
pathMatcher *regexp.Regexp
|
||||
pathArgs []string
|
||||
updater *websocket.Upgrader
|
||||
openFuncValue reflect.Value
|
||||
openFuncType reflect.Type
|
||||
closeFuncValue reflect.Value
|
||||
closeFuncType reflect.Type
|
||||
sessionType reflect.Type
|
||||
actions map[string]*websocketActionType
|
||||
isSimple bool
|
||||
options WebServiceOptions
|
||||
handlerValue reflect.Value
|
||||
handlerType reflect.Type
|
||||
memo string
|
||||
}
|
||||
|
||||
// websocketActionType WebSocket Action 元数据
|
||||
type websocketActionType struct {
|
||||
authLevel int
|
||||
funcValue reflect.Value
|
||||
funcType reflect.Type
|
||||
inType reflect.Type
|
||||
memo string
|
||||
}
|
||||
|
||||
// ActionRegister WebSocket Action 注册器
|
||||
type ActionRegister struct {
|
||||
ws *websocketServiceType
|
||||
}
|
||||
|
||||
// RegisterWebsocket 注册 WebSocket 服务
|
||||
func RegisterWebsocket(authLevel int, path string, onOpen, onClose any, memo string) *ActionRegister {
|
||||
func RegisterWebsocket(authLevel int, path string, handler any, memo string) {
|
||||
v := reflect.ValueOf(handler)
|
||||
t := v.Type()
|
||||
if t.Kind() != reflect.Func {
|
||||
return
|
||||
}
|
||||
|
||||
s := &websocketServiceType{
|
||||
authLevel: authLevel,
|
||||
path: path,
|
||||
memo: memo,
|
||||
updater: &websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { return true }},
|
||||
actions: make(map[string]*websocketActionType),
|
||||
}
|
||||
|
||||
if onOpen != nil {
|
||||
s.openFuncValue = reflect.ValueOf(onOpen)
|
||||
s.openFuncType = s.openFuncValue.Type()
|
||||
if s.openFuncType.NumOut() > 0 {
|
||||
s.sessionType = s.openFuncType.Out(0)
|
||||
}
|
||||
}
|
||||
|
||||
if onClose != nil {
|
||||
s.closeFuncValue = reflect.ValueOf(onClose)
|
||||
s.closeFuncType = s.closeFuncValue.Type()
|
||||
handlerValue: v,
|
||||
handlerType: t,
|
||||
}
|
||||
|
||||
websocketServicesLock.Lock()
|
||||
websocketServices[path] = s
|
||||
websocketServicesLock.Unlock()
|
||||
|
||||
return &ActionRegister{ws: s}
|
||||
}
|
||||
|
||||
// RegisterAction 注册 WebSocket Action
|
||||
func (ar *ActionRegister) RegisterAction(authLevel int, name string, action any, memo string) {
|
||||
v := reflect.ValueOf(action)
|
||||
t := v.Type()
|
||||
a := &websocketActionType{
|
||||
authLevel: authLevel,
|
||||
funcValue: v,
|
||||
funcType: t,
|
||||
memo: memo,
|
||||
}
|
||||
|
||||
// 查找输入参数类型
|
||||
for i := 0; i < t.NumIn(); i++ {
|
||||
inT := t.In(i)
|
||||
if inT.Kind() == reflect.Struct {
|
||||
a.inType = inT
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
ar.ws.actions[name] = a
|
||||
}
|
||||
|
||||
func doWebsocketService(ws *websocketServiceType, request *Request, response *Response, logger *log.Logger) {
|
||||
@ -102,74 +47,21 @@ func doWebsocketService(ws *websocketServiceType, request *Request, response *Re
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
var session any
|
||||
if ws.openFuncValue.IsValid() {
|
||||
// 简化版:仅支持基础参数注入
|
||||
params := make([]reflect.Value, ws.openFuncType.NumIn())
|
||||
// 调用业务处理函数,注入依赖
|
||||
params := make([]reflect.Value, ws.handlerType.NumIn())
|
||||
for i := 0; i < len(params); i++ {
|
||||
t := ws.openFuncType.In(i)
|
||||
t := ws.handlerType.In(i)
|
||||
if t == reflect.TypeOf(request) {
|
||||
params[i] = reflect.ValueOf(request)
|
||||
} else if t == reflect.TypeOf(logger) {
|
||||
params[i] = reflect.ValueOf(logger)
|
||||
} else {
|
||||
params[i] = reflect.New(t).Elem()
|
||||
}
|
||||
}
|
||||
outs := ws.openFuncValue.Call(params)
|
||||
if len(outs) > 0 {
|
||||
session = outs[0].Interface()
|
||||
}
|
||||
}
|
||||
|
||||
for {
|
||||
var msg Map
|
||||
if err := conn.ReadJSON(&msg); err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
actionName := cast.String(msg["action"])
|
||||
action := ws.actions[actionName]
|
||||
if action == nil {
|
||||
action = ws.actions[""] // 默认 action
|
||||
}
|
||||
|
||||
if action != nil {
|
||||
params := make([]reflect.Value, action.funcType.NumIn())
|
||||
for i := 0; i < len(params); i++ {
|
||||
t := action.funcType.In(i)
|
||||
if t == ws.sessionType {
|
||||
params[i] = reflect.ValueOf(session)
|
||||
} else if t == reflect.TypeOf(conn) {
|
||||
params[i] = reflect.ValueOf(conn)
|
||||
} else if t.Kind() == reflect.Struct {
|
||||
in := reflect.New(t).Interface()
|
||||
cast.Convert(in, msg)
|
||||
params[i] = reflect.ValueOf(in).Elem()
|
||||
} else if obj := GetInject(t); obj != nil {
|
||||
params[i] = reflect.ValueOf(obj)
|
||||
} else {
|
||||
params[i] = reflect.New(t).Elem()
|
||||
}
|
||||
}
|
||||
outs := action.funcValue.Call(params)
|
||||
if len(outs) > 0 {
|
||||
result := outs[0].Interface()
|
||||
if result != nil {
|
||||
_ = conn.WriteJSON(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ws.closeFuncValue.IsValid() {
|
||||
params := make([]reflect.Value, ws.closeFuncType.NumIn())
|
||||
for i := 0; i < len(params); i++ {
|
||||
t := ws.closeFuncType.In(i)
|
||||
if t == ws.sessionType {
|
||||
params[i] = reflect.ValueOf(session)
|
||||
} else {
|
||||
params[i] = reflect.New(t).Elem()
|
||||
}
|
||||
}
|
||||
ws.closeFuncValue.Call(params)
|
||||
}
|
||||
ws.handlerValue.Call(params)
|
||||
}
|
||||
|
||||
@ -9,10 +9,15 @@ import (
|
||||
|
||||
func TestWebSocketService(t *testing.T) {
|
||||
// 注册 WebSocket 服务
|
||||
ar := RegisterWebsocket(0, "/ws", nil, nil, "test websocket")
|
||||
ar.RegisterAction(0, "echo", func(in struct{ Msg string }) Map {
|
||||
return Map{"action": "echo", "reply": in.Msg}
|
||||
}, "echo action")
|
||||
RegisterWebsocket(0, "/ws", func(conn *websocket.Conn) {
|
||||
for {
|
||||
var msg Map
|
||||
if err := conn.ReadJSON(&msg); err != nil {
|
||||
break
|
||||
}
|
||||
_ = conn.WriteJSON(Map{"reply": msg["msg"]})
|
||||
}
|
||||
}, "test websocket")
|
||||
|
||||
// 启动测试服务器
|
||||
server := httptest.NewServer(&routeHandler{})
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user