chore(service): release v1.0.3 with smart startup and discover integration (by AI)

This commit is contained in:
AI Engineer 2026-05-09 17:20:32 +08:00
parent 864dadda64
commit 571662116c
8 changed files with 186 additions and 11 deletions

View File

@ -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`

View File

@ -4,6 +4,8 @@
## 核心特性
- **极致精简**: 剥离非核心组件(如 Starter, Task, 业务 Result 定义),保持底座纯净。
- **零配置启动**: 默认监听随机端口并启用 `h2c` 协议,自动完成 `discover` 注册,实现真正的 "Zero-Config" 微服务。
- **智能探测**: 自动识别应用名称 (`ReadBuildInfo`) 与局域网 IP消除环境摩擦。
- **路由反射**: 自动解析函数参数,支持 `*Request`, `*Response`, `*log.Logger` 及自定义结构体自动注入。
- **自动校验**: 集成 `verify` 引擎,通过 Struct Tag 实现入参合法性自动检查。
- **功能闭环**: 内置静态文件服务、基础 WebSocket 注册、URL 重写、反向代理(对接 Discover

View File

@ -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 协议的零配置自动注册。

View File

@ -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
View 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
View File

@ -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
)

View File

@ -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()

View File

@ -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