可扩展系统设计的黄金法则与Go语言实践|得物技术

一、 引言:为什么需要可扩展的系统?

在软件开发领域,需求变更如同家常便饭。一个缺乏扩展性的系统,往往在面对新功能需求或业务调整时,陷入"改一行代码,崩整个系统"的困境。可扩展性设计的核心目标是:让系统能够以最小的修改成本,适应未来的变化。对于Go语言开发者而言,利用其接口、并发、组合等特性,可以高效构建出适应业务演进的系统。

本文将从架构设计原则、编码实践、架构实现模式、验证指标到演进路线,系统讲解如何设计一个"生长型"系统。

二、可扩展系统的核心设计原则

2.1 开闭原则: 对扩展开放,对修改关闭

理论补充:

开闭原则是面向对象设计的基石之一。它要求系统中的模块、类或函数,应该对扩展新功能保持开放,而对修改现有代码保持关闭。这意味着,当需求变更时,我们应通过添加新代码(如新增实现类)来满足需求,而不是修改已有的代码逻辑。

Go语言的实现方式:

Go语言通过接口(Interface)和组合(Composition)特性,天然支持开闭原则。接口定义了稳定的契约,具体实现可以独立变化;组合则允许通过"搭积木"的方式扩展功能,而无需修改原有结构。

示例:数据源扩展

假设我们需要支持从不同数据源(如MySQL、S3)读取数据,核心逻辑是"读取数据",而具体数据源的实现可能频繁变化。此时,我们可以通过接口定义稳定的读取契约:

go 复制代码
// DataSource 定义数据读取的稳定接口(契约)
type DataSource interface {
    Read(p []byte) (n int, err error)  // 读取数据到缓冲区
    Close() error                      // 关闭数据源
}


// MySQLDataSource 具体实现:MySQL数据源
type MySQLDataSource struct {
    db *sql.DB  // 依赖MySQL连接
}


func (m *MySQLDataSource) Read(p []byte) (int, error) {
    // 实现MySQL数据读取逻辑(如执行查询、填充缓冲区)
    return m.db.QueryRow("SELECT data FROM table").Scan(&p)
}


func (m *MySQLDataSource) Close() error {
    return m.db.Close()  // 关闭数据库连接
}


// S3DataSource 新增实现:S3数据源(无需修改原有代码)
type S3DataSource struct {
    client *s3.Client  // 依赖AWS S3客户端
    bucket string      // S3存储桶名
}


func (s *S3DataSource) Read(p []byte) (int, error) {
    // 实现S3数据读取逻辑(如下载对象到缓冲区)
    obj, err := s.client.GetObject(context.Background(), &s3.GetObjectInput{
        Bucket: aws.String(s.bucket),
        Key:    aws.String("data.txt"),
    })
    if err != nil {
        return 0, err
    }
    defer obj.Body.Close()
    return obj.Body.Read(p)  // 读取数据到缓冲区
}


func (s *S3DataSource) Close() error {
    // S3客户端通常无需显式关闭,可根据需要实现
    return nil
}

设计说明:

  • DataSource接口定义了所有数据源必须实现的方法(Read和 Close),这是系统的"稳定契约"。
  • 当需要新增数据源(如S3)时,只需实现该接口,无需修改现有的MySQL数据源或其他依赖DataSource的代码。
  • 这一设计符合开闭原则:系统对扩展(新增S3数据源)开放,对修改(无需改动现有代码)关闭。

2.2 模块化设计:低耦合、高内聚

理论补充:

模块化设计的核心是将系统拆分为独立的功能模块,模块之间通过明确的接口交互。衡量模块化质量的关键指标是:

  • 耦合度:模块之间的依赖程度(越低越好)。
  • 内聚度:模块内部功能的相关性(越高越好)。

理想情况下,模块应满足"高内聚、低耦合":模块内部功能高度相关(如订单处理模块仅处理订单相关逻辑),模块之间通过接口通信(如订单模块通过接口调用支付模块,而非直接依赖支付模块的实现)。

Go语言的实现方式:

Go语言通过包(Package)管理模块边界,通过接口隔离依赖。开发者可以通过以下方式提升模块化质量:

  • 单一职责原则:每个模块/包仅负责单一功能(如order包处理订单逻辑,payment包处理支付逻辑)。
  • 接口隔离:模块间通过小而精的接口交互,避免暴露内部实现细节。

示例:订单模块的模块化设计

go 复制代码
// order/order.go:订单核心逻辑(高内聚)
package order


// Order 表示一个订单(核心数据结构)
type Order struct {
    ID     string
    Items  []Item
    Status OrderStatus
}


// Item 表示订单中的商品项
type Item struct {
    ProductID string
    Quantity  int
    Price     float64
}


// OrderStatus 订单状态枚举
type OrderStatus string


const (
    OrderStatusCreated  OrderStatus = "created"
    OrderStatusPaid     OrderStatus = "paid"
    OrderStatusShipped  OrderStatus = "shipped"
)


// CalculateTotal 计算订单总金额(核心业务逻辑,无外部依赖)
func (o *Order) CalculateTotal() float64 {
    total := 0.0
    for _, item := range o.Items {
        total += item.Price * float64(item.Quantity)
    }
    return total
}


// payment/payment.go:支付模块(独立模块)
package payment


// PaymentService 定义支付接口(与订单模块解耦)
type PaymentService interface {
    Charge(orderID string, amount float64) error  // 支付操作
}


// AlipayService 支付宝支付实现
type AlipayService struct {
    client *alipay.Client  // 支付宝SDK客户端
}


func (a *AlipayService) Charge(orderID string, amount float64) error {
    // 调用支付宝API完成支付
    return a.client.TradeAppPay(orderID, amount)
}

设计说明:

  • order包专注于订单的核心逻辑(如计算总金额),不依赖任何外部支付实现。
  • payment包定义支付接口,具体实现(如支付宝、微信支付)独立存在。
  • 订单模块通过PaymentService接口调用支付功能,与具体支付实现解耦。当需要更换支付方式时,只需新增支付实现(如WechatPayService),无需修改订单模块。

三、Go语言的扩展性编码实践

3.1 策略模式:动态切换算法

理论补充:

策略模式(Strategy Pattern)属于行为型设计模式,用于定义一系列算法(策略),并将每个算法封装起来,使它们可以相互替换。策略模式让算法的变化独立于使用它的客户端。

Go语言的实现方式:

Go语言通过接口实现策略的抽象,通过上下文(Context)管理策略的切换。这种模式适用于需要动态选择不同算法的场景(如缓存策略、路由策略)。

示例:缓存策略的动态切换

假设系统需要支持多种缓存(Redis、Memcached),且可以根据业务场景动态切换。通过策略模式,可以将缓存的Get和Set操作抽象为接口,具体实现由不同缓存提供。

go 复制代码
// cache/cache.go:缓存策略接口
package cache


// CacheStrategy 定义缓存操作的接口
type CacheStrategy interface {
    Get(key string) (interface{}, error)       // 从缓存获取数据
    Set(key string, value interface{}, ttl time.Duration) error  // 向缓存写入数据
}
// redis_cache.go:Redis缓存实现


type RedisCache struct {
    client *redis.Client  // Redis客户端
    ttl    time.Duration  // 默认过期时间
}


func NewRedisCache(client *redis.Client, ttl time.Duration) *RedisCache {
    return &RedisCache{client: client, ttl: ttl}
}


func (r *RedisCache) Get(key string) (interface{}, error) {
    return r.client.Get(context.Background(), key).Result()
}


func (r *RedisCache) Set(key string, value interface{}, ttl time.Duration) error {
    return r.client.Set(context.Background(), key, value, ttl).Err()
}


// memcached_cache.go:Memcached缓存实现
type MemcachedCache struct {
    client *memcache.Client  // Memcached客户端
}


func NewMemcachedCache(client *memcache.Client) *MemcachedCache {
    return &MemcachedCache{client: client}
}


func (m *MemcachedCache) Get(key string) (interface{}, error) {
    item, err := m.client.Get(key)
    if err != nil {
        return nil, err
    }
    var value interface{}
    if err := json.Unmarshal(item.Value, &value); err != nil {
        return nil, err
    }
    return value, nil
}


func (m *MemcachedCache) Set(key string, value interface{}, ttl time.Duration) error {
    data, err := json.Marshal(value)
    if err != nil {
        return err
    }
    return m.client.Set(&memcache.Item{
        Key:        key,
        Value:      data,
        Expiration: int32(ttl.Seconds()),
    }).Err()
}


// cache_context.go:缓存上下文(管理策略切换)
type CacheContext struct {
    strategy CacheStrategy  // 当前使用的缓存策略
}


func NewCacheContext(strategy CacheStrategy) *CacheContext {
    return &CacheContext{strategy: strategy}
}


// SwitchStrategy 动态切换缓存策略
func (c *CacheContext) SwitchStrategy(strategy CacheStrategy) {
    c.strategy = strategy
}


// Get 使用当前策略获取缓存
func (c *CacheContext) Get(key string) (interface{}, error) {
    return c.strategy.Get(key)
}


// Set 使用当前策略写入缓存
func (c *CacheContext) Set(key string, value interface{}, ttl time.Duration) error {
    return c.strategy.Set(key, value, ttl)
}

设计说明:

  • CacheStrategy接口定义了缓存的核心操作(Get和Set),所有具体缓存实现必须实现该接口。
  • RedisCache和MemcachedCache是具体的策略实现,分别封装了Redis和Memcached的底层逻辑。
  • CacheContext作为上下文,持有当前使用的缓存策略,并提供SwitchStrategy方法动态切换策略。客户端只需与CacheContext交互,无需关心具体使用的是哪种缓存。

优势: 当需要新增缓存类型(如本地内存缓存)时,只需实现CacheStrategy接口,无需修改现有代码;切换缓存策略时,只需调用SwitchStrategy方法,客户端无感知。

3.2 中间件链:可插拔的请求处理流程

理论补充:

中间件(Middleware)是位于请求处理链中的组件,用于实现横切关注点(如日志记录、限流、鉴权)。中间件链模式允许将多个中间件按顺序组合,形成处理流水线,每个中间件可以处理请求、传递请求或终止请求。

Go语言的实现方式:

Go语言通过函数类型(func(http.HandlerFunc) http.HandlerFunc)定义中间件,通过组合多个中间件形成处理链。这种模式灵活且易于扩展,适用于HTTP服务的请求处理。

示例:HTTP中间件链的实现

假设需要为Web服务添加日志记录、限流和鉴权功能,通过中间件链可以将这些功能解耦,按需组合。

go 复制代码
// middleware/middleware.go:中间件定义
package middleware


import (
    "net/http"
    "time"
    "golang.org/x/time/rate"
)


// Middleware 定义中间件类型:接收http.HandlerFunc,返回新的http.HandlerFunc
type Middleware func(http.HandlerFunc) http.HandlerFunc


// LoggingMiddleware 日志中间件:记录请求信息
func LoggingMiddleware(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        // 记录请求方法和路径
        println("Request received:", r.Method, r.URL.Path)
        // 调用下一个中间件或处理函数
        next(w, r)
        // 记录请求耗时
        println("Request completed in:", time.Since(start))
    }
}


// RateLimitMiddleware 限流中间件:限制请求频率
func RateLimitMiddleware(next http.HandlerFunc, limiter *rate.Limiter) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        if !limiter.Allow() {
            http.Error(w, "Too Many Requests", http.StatusTooManyRequests)
            return
        }
        next(w, r)
    }
}


// AuthMiddleware 鉴权中间件:验证请求令牌
func AuthMiddleware(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        token := r.Header.Get("Authorization")
        if token != "valid-token" {
            http.Error(w, "Unauthorized", http.StatusUnauthorized)
            return
        }
        next(w, r)
    }
}


// chain.go:中间件链组合
func Chain(middlewares ...Middleware) Middleware {
    return func(final http.HandlerFunc) http.HandlerFunc {
        // 反向组合中间件(确保执行顺序正确)
        for i := len(middlewares) - 1; i >= 0; i-- {
            final = middlewares[i](final)
        }
        return final
    }
}

使用示例:

go 复制代码
// main.go:Web服务入口
package main


import (
    "net/http"
    "middleware"
    "golang.org/x/time/rate"
)


func main() {
    // 创建限流器:每秒允许100个请求,突发10个
    limiter := rate.NewLimiter(100, 10)
    
    // 定义业务处理函数
    handleRequest := func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("Hello, World"))
    }
    
    // 组合中间件链:日志 → 限流 → 鉴权
    middlewareChain := middleware.Chain(
        middleware.LoggingMiddleware,
        middleware.RateLimitMiddlewareWithLimiter(limiter),
        middleware.AuthMiddleware,
    )
    
    // 应用中间件链到处理函数
    http.HandleFunc("/", middlewareChain(handleRequest))
    
    // 启动服务
    http.ListenAndServe(":8080", nil)
}

设计说明:

  • 每个中间件(如LoggingMiddleware、RateLimitMiddleware)专注于单一功能,通过Middleware类型定义,确保接口统一。
  • Chain函数将多个中间件按顺序组合,形成一个处理链。请求会依次经过日志记录、限流、鉴权,最后到达业务处理函数。
  • 新增中间件(如CORS跨域中间件)时,只需实现Middleware类型,即可通过Chain函数轻松加入处理链,无需修改现有中间件或业务逻辑。

四、可扩展架构的实现模式

4.1 插件化架构:热插拔的功能扩展

理论补充:

插件化架构允许系统在运行时动态加载、卸载插件,从而实现功能的灵活扩展。这种架构适用于需要支持第三方扩展或多租户定制的场景(如IDE插件、电商平台应用市场)。

Go语言的实现方式:

Go语言通过plugin包支持动态库加载,结合接口定义插件契约,可以实现安全的插件化架构。插件需实现统一的接口,主程序通过接口调用插件功能。

示例:插件化系统的实现

假设需要开发一个支持插件的数据处理系统,主程序可以动态加载处理数据的插件(如csv_parser、json_parser)。

go 复制代码
// plugin/interface.go:插件接口定义(主程序与插件共享)
package plugin


// DataProcessor 定义数据处理插件的接口
type DataProcessor interface {
    Name() string                      // 插件名称(如"csv_parser")
    Process(input []byte) (output []byte, err error)  // 处理数据
}


// plugin/csv_parser/csv_processor.go:CSV处理插件(动态库)
package main


import (
    "encoding/csv"
    "io"
    "os"
    "plugin"
)


// CSVProcessor 实现DataProcessor接口
type CSVProcessor struct{}


func (c *CSVProcessor) Name() string {
    return "csv_parser"
}


func (c *CSVProcessor) Process(input []byte) ([]byte, error) {
    // 解析CSV数据
    r := csv.NewReader(bytes.NewReader(input))
    records, err := r.ReadAll()
    if err != nil {
        return nil, err
    }
    // 转换为JSON格式输出
    var result []map[string]string
    for _, record := range records {
        row := make(map[string]string)
        for i, field := range record {
            row[fmt.Sprintf("col_%d", i)] = field
        }
        result = append(result, row)
    }
    jsonData, err := json.Marshal(result)
    if err != nil {
        return nil, err
    }
    return jsonData, nil
}


// 插件的入口函数(必须命名为"Plugin",主程序通过此函数获取插件实例)
var Plugin plugin.DataProcessor = &CSVProcessor{}
go 复制代码
// main.go:主程序(加载插件并调用)
package main


import (
    "fmt"
    "plugin"
    "path/filepath"
)


func main() {
    // 插件路径(假设编译为so文件)
    pluginPath := filepath.Join("plugins", "csv_parser.so")
    
    // 加载插件
    p, err := plugin.Open(pluginPath)
    if err != nil {
        panic(err)
    }


        // 获取插件实例(通过接口类型断言)
    sym, err := p.Lookup("Plugin")
    if err != nil {
        panic(err)
    }
    processor, ok := sym.(plugin.DataProcessor)
    if !ok {
        panic("插件未实现DataProcessor接口")
    }


        // 使用插件处理数据
    inputData := []byte("name,age
张三,20
李四,25")
    output, err := processor.Process(inputData)
    if err != nil {
        panic(err)
    }
    fmt.Println(string(output))  // 输出JSON格式数据
}

设计说明:

  • 接口定义:主程序定义DataProcessor接口,规定插件必须实现的方法(Name和Process)。
  • 插件实现:插件(如csv_parser)实现DataProcessor接口,并导出名为Plugin的全局变量(主程序通过此变量获取插件实例)。
  • 动态加载:主程序通过plugin.Open加载插件,通过Lookup获取插件实例,并转换为DataProcessor接口调用。

优势:

  • 主程序与插件解耦,插件的添加、删除或升级不影响主程序运行。
  • 支持热插拔:插件可以在运行时动态加载(需注意Go插件的局限性,如版本兼容性)。

4.2 配置驱动架构:外部化的灵活配置

理论补充:

配置驱动架构(Configuration-Driven Architecture)通过将系统行为参数化,使系统可以通过修改配置(而非代码)来适应不同的运行环境或业务需求。这种架构适用于需要支持多环境(开发、测试、生产)、多租户定制或多场景适配的系统。

Go语言的实现方式:

Go语言通过encoding/json、encoding/yaml等包支持配置文件的解析,结合viper等第三方库可以实现更复杂的配置管理(如环境变量覆盖、热更新)。

示例:配置驱动的数据库连接

假设系统需要支持不同环境(开发、生产)的数据库配置,通过配置文件动态加载数据库连接参数。

go 复制代码
// config/config.go:配置结构体定义
package config


// DBConfig 数据库配置
type DBConfig struct {
    DSN         string `json:"dsn"`          // 数据库连接字符串
    MaxOpenConn int    `json:"max_open_conn"` // 最大打开连接数
    MaxIdleConn int    `json:"max_idle_conn"` // 最大空闲连接数
    ConnTimeout int    `json:"conn_timeout"`  // 连接超时时间(秒)
}


// AppConfig 应用全局配置
type AppConfig struct {
    Env  string   `json:"env"`   // 环境(dev/test/prod)
    DB   DBConfig `json:"db"`    // 数据库配置
    Log  LogConfig `json:"log"`   // 日志配置
}


// LogConfig 日志配置
type LogConfig struct {
    Level string `json:"level"` // 日志级别(debug/info/warn/error)
    Path  string `json:"path"`  // 日志文件路径
}
go 复制代码
// config/loader.go:配置加载器(支持热更新)
package config


import (
    "encoding/json"
    "os"
    "path/filepath"
    "time"


        "github.com/fsnotify/fsnotify"
)


// LoadConfig 加载配置文件
func LoadConfig(path string) (*AppConfig, error) {
    file, err := os.Open(path)
    if err != nil {
        return nil, err
    }
    defer file.Close()
    
    var cfg AppConfig
    decoder := json.NewDecoder(file)
    if err := decoder.Decode(&cfg); err != nil {
        return nil, err
    }
    return &cfg, nil
}


// WatchConfig 监听配置文件变化(热更新)
func WatchConfig(path string, callback func(*AppConfig)) error {
    watcher, err := fsnotify.NewWatcher()
    if err != nil {
        return err
    }
    defer watcher.Close()
    
    // 监听配置文件所在目录
    dir := filepath.Dir(path)
    if err := watcher.Add(dir); err != nil {
        return err
    }
    
    go func() {
        for {
            select {
            case event, ok := <-watcher.Events:
                if !ok {
                    return
                }
                // 仅处理写事件
                if event.Op&fsnotify.Write == fsnotify.Write {
                    // 重新加载配置
                    newCfg, err := LoadConfig(path)
                    if err != nil {
                        println("加载配置失败:", err.Error())
                        continue
                    }
                    // 触发回调(通知其他模块配置已更新)
                    callback(newCfg)
                }
            case err, ok := <-watcher.Errors:
                if !ok {
                    return
                }
                println("配置监听错误:", err.Error())
            }
        }
    }()
    
    // 保持程序运行
    select {}
}
go 复制代码
// main.go:使用配置驱动的数据库连接
package main


import (
    "database/sql"
    "fmt"
    "config"
    _ "github.com/go-sql-driver/mysql"
)


func main() {
    // 加载初始配置
    cfg, err := config.LoadConfig("config.json")
    if err != nil {
        panic(err)
    }
    
    // 初始化数据库连接
    db, err := sql.Open("mysql", cfg.DB.DSN)
    if err != nil {
        panic(err)
    }
    defer db.Close()
    
    // 设置连接池参数(从配置中读取)
    db.SetMaxOpenConns(cfg.DB.MaxOpenConn)
    db.SetMaxIdleConns(cfg.DB.MaxIdleConn)
    db.SetConnMaxLifetime(time.Duration(cfg.DB.ConnTimeout) * time.Second)
    
    // 启动配置监听(热更新)
    go func() {
        err := config.WatchConfig("config.json", func(newCfg *config.AppConfig) {
            // 配置更新时,重新设置数据库连接池参数
            db.SetMaxOpenConns(newCfg.DB.MaxOpenConn)
            db.SetMaxIdleConns(newCfg.DB.MaxIdleConn)
            db.SetConnMaxLifetime(time.Duration(newCfg.DB.ConnTimeout) * time.Second)
            fmt.Println("配置已更新,数据库连接池参数调整")
        })
        if err != nil {
            panic(err)
        }
    }()
    
    // 业务逻辑...
}

设计说明:

  • 配置结构化:通过AppConfig、DBConfig等结构体定义配置的层次结构,确保配置的清晰性和可维护性。
  • 热更新支持:通过fsnotify监听配置文件变化,触发回调函数重新加载配置,并更新系统状态(如数据库连接池参数)。
  • 多环境适配:通过不同的配置文件(如config-dev.json、config-prod.json)或环境变量覆盖,实现不同环境的配置隔离。

优势:

  • 系统行为的调整无需修改代码,只需修改配置文件,降低了维护成本。
  • 支持动态调整关键参数(如数据库连接池大小、日志级别),提升了系统的灵活性和可观测性。

五、可扩展性的验证与演进

5.1 扩展性验证指标

为了确保系统具备良好的扩展性,需要从多个维度进行验证。以下是关键指标及测量方法:

指标 测量方法 目标值
新功能开发周期 统计新增一个中等复杂度功能所需的时间(包括设计、编码、测试) < 2人日
修改影响范围 统计修改一个功能时,需要修改的模块数量和代码行数 < 5个模块,< 500行代码
配置生效延迟 测量配置变更到系统完全应用新配置的时间 < 100ms
并发扩展能力 测量系统在增加CPU核数时,吞吐量的增长比例(理想为线性增长) 吞吐量增长 ≥ 核数增长 × 80%
插件加载时间 测量动态加载一个插件的时间 < 1秒

5.2 扩展性演进路线

系统的扩展性不是一蹴而就的,需要随着业务的发展逐步演进。以下是一个典型的演进路线:

css 复制代码
graph TD
    A[单体架构] -->|垂直拆分| B[核心服务+支撑服务]
    B -->|接口抽象| C[模块化架构]
    C -->|策略模式/中间件| D[可扩展的分布式架构]
    D -->|插件化/配置驱动| E[云原生可扩展架构]
  • 阶段1单体架构:初期业务简单,系统以单体形式存在。此时应注重代码的可读性和可维护性,为后续扩展打下基础。
  • 阶段2核心服务+支撑服务:随着业务增长,将核心功能(如订单、用户)与非核心功能(如日志、监控)拆分,降低耦合。
  • 阶段3模块化架构:通过接口抽象和依赖倒置,将系统拆分为高内聚、低耦合的模块,支持独立开发和部署。
  • 阶段4可扩展的分布式架构:引入策略模式、中间件链等模式,支持动态切换算法和处理流程,适应多样化的业务需求。
  • 阶段5云原生可扩展架构:结合容器化(Docker)、编排(Kubernetes)和Serverless技术,实现资源的弹性扩展和自动伸缩。

六、结 语

可扩展性设计是软件系统的"生命力"所在。通过遵循开闭原则、模块化设计等核心原则,结合策略模式、中间件链、插件化架构等Go语言友好的编码模式,开发者可以构建出适应业务变化的"生长型"系统。

需要注意的是,扩展性设计并非追求"过度设计",而是在当前需求和未来变化之间找到平衡。建议定期进行架构评审,通过压力测试和代码分析(如go mod graph查看模块依赖)评估系统的扩展性健康度,及时调整设计策略。

最后,记住:优秀的系统不是完美的,而是能够持续进化的。保持开放的心态,拥抱变化,才能在快速发展的技术领域中立于不败之地。

往期回顾

  1. 得物新商品审核链路建设分享

  2. 营销会场预览直通车实践|得物技术

  3. 基于TinyMce富文本编辑器的客服自研知识库的技术探索和实践|得物技术

  4. AI质量专项报告自动分析生成|得物技术

  5. Rust 性能提升"最后一公里":详解 Profiling 瓶颈定位与优化|得物技术

文 / 悟

关注得物技术,每周更新技术干货

要是觉得文章对你有帮助的话,欢迎评论转发点赞~

未经得物技术许可严禁转载,否则依法追究法律责任。

相关推荐
用户298698530143 分钟前
如何使用 Spire.PDF 在 C# 中创建和绘制 PDF 表单?
后端
用户903700167156 分钟前
生产环境的线程池参数问题思考分享
后端
superlls11 分钟前
(Redis)缓存三大问题及布隆过滤器详解
java·后端·spring
Java水解24 分钟前
MySQL 中的 UPDATE 语句
后端·mysql
大象席地抽烟25 分钟前
Spring Websocket Chatroom
后端
磊磊落落32 分钟前
Java 中如何使用 MapStruct 进行对象映射?
后端
lovebugs1 小时前
🚀 Kubernetes核心命令详解:Java开发者必备指南
java·后端·kubernetes
灰色头像1 小时前
Nginx 内置变量详解:从原理到实战案例
后端
程序员小假1 小时前
我们来说一说解决线程安全的方案
后端
李九四1 小时前
章节16:实现注释功能
后端·架构