chore(service): release v1.0.3 with smart startup and discover integration (by AI)
This commit is contained in:
parent
864dadda64
commit
571662116c
11
CHANGELOG.md
11
CHANGELOG.md
@ -1,5 +1,16 @@
|
||||
# CHANGELOG - go/service
|
||||
|
||||
## v1.0.3 (2026-05-09)
|
||||
### Added
|
||||
- **Zero-Config Microservices**: 实现智能启动逻辑。当 `Listen` 为空时,自动开启随机端口并使用 `h2c` 协议。
|
||||
- **Auto-Discover**: 自动集成 `go/discover`。当使用随机端口或配置了应用名时,自动完成服务注册。
|
||||
- **Smart Detection**: 内置应用名自动识别 (`debug.ReadBuildInfo`) 与服务 IP 自动探测 (UDP 伪拨号法)。
|
||||
- **H2C Server**: 原生支持 HTTP/2 Cleartext (h2c) 服务端协议,提升微服务间通信性能。
|
||||
|
||||
### Changed
|
||||
- **Infrastructure Alignment**: `go.mod` 引入 `golang.org/x/net` 以支持 H2C。
|
||||
- **IO Security**: 持续优化 `go/file` 在静态文件服务中的应用。
|
||||
|
||||
## v1.0.2 (2026-05-09)
|
||||
### Changed
|
||||
- **Infrastructure Alignment**: `go.mod` 升级 `go/config` 至 `v1.0.7`,`go/http` 至 `v1.0.10`。
|
||||
|
||||
@ -4,6 +4,8 @@
|
||||
|
||||
## 核心特性
|
||||
- **极致精简**: 剥离非核心组件(如 Starter, Task, 业务 Result 定义),保持底座纯净。
|
||||
- **零配置启动**: 默认监听随机端口并启用 `h2c` 协议,自动完成 `discover` 注册,实现真正的 "Zero-Config" 微服务。
|
||||
- **智能探测**: 自动识别应用名称 (`ReadBuildInfo`) 与局域网 IP,消除环境摩擦。
|
||||
- **路由反射**: 自动解析函数参数,支持 `*Request`, `*Response`, `*log.Logger` 及自定义结构体自动注入。
|
||||
- **自动校验**: 集成 `verify` 引擎,通过 Struct Tag 实现入参合法性自动检查。
|
||||
- **功能闭环**: 内置静态文件服务、基础 WebSocket 注册、URL 重写、反向代理(对接 Discover)。
|
||||
|
||||
4
TEST.md
4
TEST.md
@ -20,6 +20,9 @@
|
||||
- [x] `TestNestedVerify`: 嵌套结构校验
|
||||
- [x] `TestCustomVerify`: 自定义校验函数
|
||||
- [x] `TestWebSocketService`: WebSocket 注册
|
||||
- [x] `TestGetDefaultName`: 自动应用名识别
|
||||
- [x] `TestGetServerIp`: 自动 IP 探测
|
||||
- [x] `TestSmartStartup`: 零配置智能启动与 Discover 注册
|
||||
|
||||
## 基础设施对齐验证
|
||||
- [x] 成功集成 `apigo.cc/go/cast` 用于参数解析与类型强转。
|
||||
@ -27,3 +30,4 @@
|
||||
- [x] 成功集成 `apigo.cc/go/log` 并实现完整的 Request 日志记录。
|
||||
- [x] 强制集成 `apigo.cc/go/file` 替代原生 `os`,全面支持内存虚拟文件系统。
|
||||
- [x] 成功集成 `apigo.cc/go/id` 与 `go/redis` 实现分布式有序 ID。
|
||||
- [x] 成功集成 `apigo.cc/go/discover` 并支持 H2C 协议的零配置自动注册。
|
||||
|
||||
@ -42,6 +42,7 @@ type ServiceConfig struct {
|
||||
SessionWithoutCookie bool // Session 禁用 Cookie
|
||||
DeviceWithoutCookie bool // 设备ID禁用 Cookie
|
||||
IdServer string // Redis 服务器连接 (用于全局唯一 ID 生成)
|
||||
DiscoverApp string // 强制指定 Discover 应用名称,如果不指定且 Listen 为空则自动获取并注册
|
||||
KeepKeyCase bool // 是否保持 Key 的首字母大小写
|
||||
IndexFiles []string // 静态文件索引文件
|
||||
IndexDir bool // 访问目录时显示文件列表
|
||||
|
||||
49
discover_test.go
Normal file
49
discover_test.go
Normal file
@ -0,0 +1,49 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetDefaultName(t *testing.T) {
|
||||
// Test from env
|
||||
os.Setenv("DISCOVER_APP", "test-app")
|
||||
if name := GetDefaultName(); name != "test-app" {
|
||||
t.Errorf("Expected test-app, got %s", name)
|
||||
}
|
||||
os.Unsetenv("DISCOVER_APP")
|
||||
|
||||
// Test from build info or args
|
||||
name := GetDefaultName()
|
||||
if name == "" {
|
||||
t.Error("Expected non-empty name")
|
||||
}
|
||||
t.Logf("Detected name: %s", name)
|
||||
}
|
||||
|
||||
func TestGetServerIp(t *testing.T) {
|
||||
ip := GetServerIp()
|
||||
if ip == "" || ip == "0.0.0.0" {
|
||||
t.Errorf("Invalid IP detected: %s", ip)
|
||||
}
|
||||
t.Logf("Detected IP: %s", ip)
|
||||
}
|
||||
|
||||
func TestSmartStartup(t *testing.T) {
|
||||
// Reset config
|
||||
Config.Listen = ""
|
||||
Config.DiscoverApp = "smart-test"
|
||||
|
||||
as := AsyncStart()
|
||||
if as.Addr == "" {
|
||||
t.Fatal("Server address should not be empty")
|
||||
}
|
||||
|
||||
t.Logf("Server started on %s", as.Addr)
|
||||
|
||||
if !as.useDiscover {
|
||||
t.Error("Should have enabled discover")
|
||||
}
|
||||
|
||||
as.Stop()
|
||||
}
|
||||
1
go.mod
1
go.mod
@ -13,4 +13,5 @@ require (
|
||||
apigo.cc/go/redis v1.0.5
|
||||
apigo.cc/go/timer v1.0.6
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
golang.org/x/net v0.53.0
|
||||
)
|
||||
|
||||
76
server.go
76
server.go
@ -1,12 +1,17 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"apigo.cc/go/discover"
|
||||
"apigo.cc/go/log"
|
||||
"context"
|
||||
"fmt"
|
||||
"golang.org/x/net/http2"
|
||||
"golang.org/x/net/http2/h2c"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
@ -18,6 +23,7 @@ type AsyncServer struct {
|
||||
Addr string
|
||||
stopChan chan os.Signal
|
||||
startChan chan bool
|
||||
useDiscover bool
|
||||
}
|
||||
|
||||
// AsyncStart 异步启动服务
|
||||
@ -34,13 +40,42 @@ func AsyncStart() *AsyncServer {
|
||||
}
|
||||
|
||||
func (as *AsyncServer) start() {
|
||||
if Config.Listen == "" {
|
||||
Config.Listen = ":8080" // 默认端口
|
||||
listenStr := Config.Listen
|
||||
as.useDiscover = false
|
||||
|
||||
if listenStr == "" {
|
||||
listenStr = ":0,h2c"
|
||||
as.useDiscover = true
|
||||
}
|
||||
|
||||
listener, err := net.Listen("tcp", Config.Listen)
|
||||
// 解析第一个监听配置
|
||||
part := strings.Split(listenStr, "|")[0]
|
||||
addr, opts, _ := strings.Cut(part, ",")
|
||||
|
||||
protocol := "http"
|
||||
for _, opt := range strings.Split(opts, ",") {
|
||||
opt = strings.ToLower(strings.TrimSpace(opt))
|
||||
if opt == "h2c" || opt == "h2" {
|
||||
protocol = opt
|
||||
}
|
||||
}
|
||||
|
||||
if !strings.Contains(addr, ":") {
|
||||
addr = ":" + addr
|
||||
}
|
||||
|
||||
// 检查是否需要启动服务发现
|
||||
appName := Config.DiscoverApp
|
||||
if appName == "" {
|
||||
appName = GetDefaultName()
|
||||
}
|
||||
if appName != "" {
|
||||
as.useDiscover = true
|
||||
}
|
||||
|
||||
listener, err := net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
log.DefaultLogger.Error("failed to listen", "addr", Config.Listen, "error", err.Error())
|
||||
log.DefaultLogger.Error("failed to listen", "addr", addr, "error", err.Error())
|
||||
as.startChan <- false
|
||||
return
|
||||
}
|
||||
@ -49,14 +84,39 @@ func (as *AsyncServer) start() {
|
||||
as.Addr = listener.Addr().String()
|
||||
serverAddr = as.Addr
|
||||
|
||||
// 如果使用了随机端口且没有明确指定不需要服务发现,则开启
|
||||
if addr == ":0" || strings.HasSuffix(addr, ":0") {
|
||||
as.useDiscover = true
|
||||
}
|
||||
|
||||
h2s := &http2.Server{}
|
||||
var handler http.Handler = &RouteHandler{}
|
||||
if protocol == "h2c" {
|
||||
handler = h2c.NewHandler(handler, h2s)
|
||||
}
|
||||
|
||||
as.server = &http.Server{
|
||||
Handler: &RouteHandler{},
|
||||
Handler: handler,
|
||||
}
|
||||
|
||||
// 启动服务发现
|
||||
if as.useDiscover && appName != "" {
|
||||
_, port, _ := net.SplitHostPort(as.Addr)
|
||||
ip := GetServerIp()
|
||||
discoverAddr := fmt.Sprintf("%s:%s", ip, port)
|
||||
|
||||
conf := discover.GetConfig()
|
||||
conf.App = appName
|
||||
discover.SetConfig(conf)
|
||||
if discover.Start(discoverAddr) {
|
||||
log.DefaultLogger.Info("discover registered", "app", appName, "addr", discoverAddr)
|
||||
}
|
||||
}
|
||||
|
||||
signal.Notify(as.stopChan, os.Interrupt, syscall.SIGTERM)
|
||||
|
||||
go func() {
|
||||
log.DefaultLogger.Info("service starting", "addr", as.Addr)
|
||||
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())
|
||||
@ -67,6 +127,10 @@ func (as *AsyncServer) start() {
|
||||
// Stop 停止服务
|
||||
func (as *AsyncServer) Stop() {
|
||||
log.DefaultLogger.Info("service stopping")
|
||||
if as.useDiscover {
|
||||
discover.Stop()
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
|
||||
43
utility.go
43
utility.go
@ -4,6 +4,10 @@ import (
|
||||
"apigo.cc/go/id"
|
||||
"apigo.cc/go/log"
|
||||
"apigo.cc/go/redis"
|
||||
"net"
|
||||
"os"
|
||||
"path"
|
||||
"runtime/debug"
|
||||
"sync"
|
||||
)
|
||||
|
||||
@ -12,6 +16,45 @@ var (
|
||||
idMakerLock sync.Mutex
|
||||
)
|
||||
|
||||
// GetDefaultName 获取默认应用名称
|
||||
func GetDefaultName() string {
|
||||
name := os.Getenv("DISCOVER_APP")
|
||||
if name == "" {
|
||||
name = os.Getenv("discover_app")
|
||||
}
|
||||
if name == "" {
|
||||
if info, ok := debug.ReadBuildInfo(); ok && info.Path != "" && info.Path != "command-line-arguments" {
|
||||
name = path.Base(info.Path)
|
||||
}
|
||||
}
|
||||
if name == "" {
|
||||
name = path.Base(os.Args[0])
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
// GetServerIp 获取真实局域网 IP (UDP 8.8.8.8 伪拨号法)
|
||||
func GetServerIp() string {
|
||||
conn, err := net.Dial("udp", "8.8.8.8:80")
|
||||
if err == nil {
|
||||
localAddr := conn.LocalAddr().(*net.UDPAddr)
|
||||
_ = conn.Close()
|
||||
return localAddr.IP.String()
|
||||
}
|
||||
|
||||
addrs, err := net.InterfaceAddrs()
|
||||
if err == nil {
|
||||
for _, a := range addrs {
|
||||
if an, ok := a.(*net.IPNet); ok {
|
||||
if an.IP.IsGlobalUnicast() {
|
||||
return an.IP.To4().String()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return "127.0.0.1"
|
||||
}
|
||||
|
||||
// IDMakerInterface ID 生成器接口
|
||||
type IDMakerInterface interface {
|
||||
Get(size int) string
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user