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
|
# 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)
|
## v1.0.2 (2026-05-09)
|
||||||
### Changed
|
### Changed
|
||||||
- **Infrastructure Alignment**: `go.mod` 升级 `go/config` 至 `v1.0.7`,`go/http` 至 `v1.0.10`。
|
- **Infrastructure Alignment**: `go.mod` 升级 `go/config` 至 `v1.0.7`,`go/http` 至 `v1.0.10`。
|
||||||
|
|||||||
@ -4,6 +4,8 @@
|
|||||||
|
|
||||||
## 核心特性
|
## 核心特性
|
||||||
- **极致精简**: 剥离非核心组件(如 Starter, Task, 业务 Result 定义),保持底座纯净。
|
- **极致精简**: 剥离非核心组件(如 Starter, Task, 业务 Result 定义),保持底座纯净。
|
||||||
|
- **零配置启动**: 默认监听随机端口并启用 `h2c` 协议,自动完成 `discover` 注册,实现真正的 "Zero-Config" 微服务。
|
||||||
|
- **智能探测**: 自动识别应用名称 (`ReadBuildInfo`) 与局域网 IP,消除环境摩擦。
|
||||||
- **路由反射**: 自动解析函数参数,支持 `*Request`, `*Response`, `*log.Logger` 及自定义结构体自动注入。
|
- **路由反射**: 自动解析函数参数,支持 `*Request`, `*Response`, `*log.Logger` 及自定义结构体自动注入。
|
||||||
- **自动校验**: 集成 `verify` 引擎,通过 Struct Tag 实现入参合法性自动检查。
|
- **自动校验**: 集成 `verify` 引擎,通过 Struct Tag 实现入参合法性自动检查。
|
||||||
- **功能闭环**: 内置静态文件服务、基础 WebSocket 注册、URL 重写、反向代理(对接 Discover)。
|
- **功能闭环**: 内置静态文件服务、基础 WebSocket 注册、URL 重写、反向代理(对接 Discover)。
|
||||||
|
|||||||
4
TEST.md
4
TEST.md
@ -20,6 +20,9 @@
|
|||||||
- [x] `TestNestedVerify`: 嵌套结构校验
|
- [x] `TestNestedVerify`: 嵌套结构校验
|
||||||
- [x] `TestCustomVerify`: 自定义校验函数
|
- [x] `TestCustomVerify`: 自定义校验函数
|
||||||
- [x] `TestWebSocketService`: WebSocket 注册
|
- [x] `TestWebSocketService`: WebSocket 注册
|
||||||
|
- [x] `TestGetDefaultName`: 自动应用名识别
|
||||||
|
- [x] `TestGetServerIp`: 自动 IP 探测
|
||||||
|
- [x] `TestSmartStartup`: 零配置智能启动与 Discover 注册
|
||||||
|
|
||||||
## 基础设施对齐验证
|
## 基础设施对齐验证
|
||||||
- [x] 成功集成 `apigo.cc/go/cast` 用于参数解析与类型强转。
|
- [x] 成功集成 `apigo.cc/go/cast` 用于参数解析与类型强转。
|
||||||
@ -27,3 +30,4 @@
|
|||||||
- [x] 成功集成 `apigo.cc/go/log` 并实现完整的 Request 日志记录。
|
- [x] 成功集成 `apigo.cc/go/log` 并实现完整的 Request 日志记录。
|
||||||
- [x] 强制集成 `apigo.cc/go/file` 替代原生 `os`,全面支持内存虚拟文件系统。
|
- [x] 强制集成 `apigo.cc/go/file` 替代原生 `os`,全面支持内存虚拟文件系统。
|
||||||
- [x] 成功集成 `apigo.cc/go/id` 与 `go/redis` 实现分布式有序 ID。
|
- [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
|
SessionWithoutCookie bool // Session 禁用 Cookie
|
||||||
DeviceWithoutCookie bool // 设备ID禁用 Cookie
|
DeviceWithoutCookie bool // 设备ID禁用 Cookie
|
||||||
IdServer string // Redis 服务器连接 (用于全局唯一 ID 生成)
|
IdServer string // Redis 服务器连接 (用于全局唯一 ID 生成)
|
||||||
|
DiscoverApp string // 强制指定 Discover 应用名称,如果不指定且 Listen 为空则自动获取并注册
|
||||||
KeepKeyCase bool // 是否保持 Key 的首字母大小写
|
KeepKeyCase bool // 是否保持 Key 的首字母大小写
|
||||||
IndexFiles []string // 静态文件索引文件
|
IndexFiles []string // 静态文件索引文件
|
||||||
IndexDir bool // 访问目录时显示文件列表
|
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/redis v1.0.5
|
||||||
apigo.cc/go/timer v1.0.6
|
apigo.cc/go/timer v1.0.6
|
||||||
github.com/gorilla/websocket v1.5.3
|
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
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"apigo.cc/go/discover"
|
||||||
"apigo.cc/go/log"
|
"apigo.cc/go/log"
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"golang.org/x/net/http2"
|
||||||
|
"golang.org/x/net/http2/h2c"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@ -18,6 +23,7 @@ type AsyncServer struct {
|
|||||||
Addr string
|
Addr string
|
||||||
stopChan chan os.Signal
|
stopChan chan os.Signal
|
||||||
startChan chan bool
|
startChan chan bool
|
||||||
|
useDiscover bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// AsyncStart 异步启动服务
|
// AsyncStart 异步启动服务
|
||||||
@ -34,13 +40,42 @@ func AsyncStart() *AsyncServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (as *AsyncServer) start() {
|
func (as *AsyncServer) start() {
|
||||||
if Config.Listen == "" {
|
listenStr := Config.Listen
|
||||||
Config.Listen = ":8080" // 默认端口
|
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 {
|
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
|
as.startChan <- false
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -49,14 +84,39 @@ func (as *AsyncServer) start() {
|
|||||||
as.Addr = listener.Addr().String()
|
as.Addr = listener.Addr().String()
|
||||||
serverAddr = as.Addr
|
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{
|
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)
|
signal.Notify(as.stopChan, os.Interrupt, syscall.SIGTERM)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
log.DefaultLogger.Info("service starting", "addr", as.Addr)
|
log.DefaultLogger.Info("service starting", "addr", as.Addr, "proto", protocol)
|
||||||
as.startChan <- true
|
as.startChan <- true
|
||||||
if err := as.server.Serve(listener); err != nil && err != http.ErrServerClosed {
|
if err := as.server.Serve(listener); err != nil && err != http.ErrServerClosed {
|
||||||
log.DefaultLogger.Error("server error", "error", err.Error())
|
log.DefaultLogger.Error("server error", "error", err.Error())
|
||||||
@ -67,6 +127,10 @@ func (as *AsyncServer) start() {
|
|||||||
// Stop 停止服务
|
// Stop 停止服务
|
||||||
func (as *AsyncServer) Stop() {
|
func (as *AsyncServer) Stop() {
|
||||||
log.DefaultLogger.Info("service stopping")
|
log.DefaultLogger.Info("service stopping")
|
||||||
|
if as.useDiscover {
|
||||||
|
discover.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
|
|||||||
43
utility.go
43
utility.go
@ -4,6 +4,10 @@ import (
|
|||||||
"apigo.cc/go/id"
|
"apigo.cc/go/id"
|
||||||
"apigo.cc/go/log"
|
"apigo.cc/go/log"
|
||||||
"apigo.cc/go/redis"
|
"apigo.cc/go/redis"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"runtime/debug"
|
||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -12,6 +16,45 @@ var (
|
|||||||
idMakerLock sync.Mutex
|
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 生成器接口
|
// IDMakerInterface ID 生成器接口
|
||||||
type IDMakerInterface interface {
|
type IDMakerInterface interface {
|
||||||
Get(size int) string
|
Get(size int) string
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user