Golang API中不同端点返回相同结果的问题如何解决

Golang API中不同端点返回相同结果的问题如何解决 我有一个Go语言编写的API,它会调用其他API来获取结果。当React UI调用其不同的端点来获取并显示内容时,该API会向多个或所有调用发送相同的结果。

这看起来不像是常见的数据库或其他并发问题。

感觉像是它只为一个调用获取了结果,然后将相同的结果写给了来自同一客户端(例如一个用户加载或刷新其网页)的其他调用。

我们使用了net/http和Gorilla mux。

没有找到任何讨论此类问题的文章。非常感谢任何帮助或见解。

谢谢

谢谢

11 回复

你能从你的API中贴出一段相关的代码吗?

更多关于Golang API中不同端点返回相同结果的问题如何解决的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


在你提到的或需要测试的不同情况下,使用 Postman 或其他 API 客户端来尝试你的 API。

检查您的 Go API 代码是否存在意外的数据共享,确保正确使用 goroutine 并审查中间件,以防止共享状态问题。

我不了解Azure环境,但我认为您的本地环境与远程环境不同(也就是所谓的在我电脑上能运行),但即便如此,一个编写良好的程序没有理由不工作。然而,我的第一个猜测是您在重载的路由上遇到了一些问题。

我们在Postman中不会遇到这个问题——可能是因为调用不是快速连续进行的。在React页面中,为了获取不同的结果,它们几乎是同时被调用的。

请查看为George的回复所发布的代码片段。

使用这段代码,我们在本地环境中获得了正确的响应……即使同时进行多次调用。

然而,当我们尝试从 Azure 部署的 Web 应用运行相同的调用时,响应却出现了混乱。

为什么我们只在 Azure 部署的 Web 应用中观察到这种行为,而在本地环境中却没有?

你好 @Dean_Davidson

感谢你检查代码。

我们找到了上述问题的根本原因和解决方案。

router.HandleFunc(/api/{entity}, functions.CallPostmortemApiHandler).Methods(GET, POST, PUT, DELETE)
router.HandleFunc(/api/{entity}/{entityNumber}, functions.CallPostmortemApiHandler).Methods(GET, PUT)
router.HandleFunc(/api/{entity}/{entityId}/{entity}, functions.CallPostmortemApiHandler).Methods(GET)

这段代码完全正常工作… 我们之前得到混乱的响应,是因为使用了全局变量… 有两个或更多变量在包级别被全局声明…这就是我们得到混乱响应的原因…一旦我们将变量的作用域定义在局部…我们就得到了正确的响应。

采用上述代码模式是为了避免重复代码。

谢谢。

正如你在 mux-router 代码中看到的,{entity} 使得重载一个 mux 路由器条目以服务多个端点变得更容易。然而,即使没有这样的 {entity} 参数化,如果我们为每个端点创建单独的路由器条目,并为每个这样的端点创建单独的处理函数,其行为也会是相同的。

router.HandleFunc(/api/{Entity}, functions.GetPostMortemDiag).Methods(GET, POST, PUT, DELETE)|
router.HandleFunc(/api/{entity}/{entityNumber}, functions.GetPostMortemDiag).Methods(GET, PUT)|]
router.HandleFunc(/api/{entity}/{entityId}/{entity}, functions.GetPostMortemDiag).Methods(GET)

乍一看,问题源于对路由器的使用不当。使用同一个处理函数来处理具有相同方法的多条路由并不是一个好的做法,因为结果可能出乎意料。这样做可能会调用与你预期不同的路由。相反,应该为该处理函数使用一条路由,并在其内部解析所有参数。

我已经将路由分开,并为不同的路由编写了不同的函数,但我仍然收到重叠的响应。

router.HandleFunc(/api/retrieve-images, functions.GetPostMortemDiagImg).Methods(GET)
router.HandleFunc(/api/getplatformportfoliodata, functions.GetPostMortemDiagPortfolioData).Methods(GET)
router.HandleFunc(/api/fetch-postmortem-data-by-number/{number}, functions.GetPostMortemDiagFetchPostMortemDataByNumber).Methods(GET)
router.HandleFunc(/api/fetch-all-post-mortem-data, functions.GetPostMortemDiagFetchPostMortemData).Methods(GET)
router.HandleFunc(/api/fetch-paginated-post-mortem-data, functions.GetPostMortemDiagFetchPaginatedPostMortemData).Methods(GET)
router.HandleFunc(/api/teams/email/{email}, functions.GetTeamsDataByEmail).Methods(GET)
router.HandleFunc(/api/teams/{pmtID}, functions.GetTeamsDataByPostMortemId).Methods(GET)

乍一看,问题源于路由器的使用不当。使用同一个处理器处理多个具有相同方法的路由并不是一个好的做法,因为这可能导致意想不到的结果。这样做可能会调用到与你预期不同的路由。相反,应该为该处理器使用一个路由,并在其内部解析所有参数。

很好的建议。补充一点:用户 @Free_Bird 发布的代码不是有效的代码。并且在这个路由中,有多个同名的命名参数:

// "entity" 被声明了两次。
// Route 不是一个字符串(这无法编译)。
// Methods(GET) 中的 GET 也不是一个字符串,同样无法编译。
// 他们有一个叫 "functions" 的包吗?可能需要重命名。
router.HandleFunc(/api/{entity}/{entityId}/{entity}, functions.GetPostMortemDiag).Methods(GET)

我怀疑另一个问题是:由于模糊的路由声明,多个路由都匹配,而第一个被选中。根据文档

路由按照它们被添加到路由器中的顺序进行测试。如果两个路由都匹配,则第一个胜出:

r := mux.NewRouter()
r.HandleFunc("/specific", specificHandler)
r.PathPrefix("/").Handler(catchAllHandler)

因此,你可以尝试将更具体的路由放在顺序的前面,而最不具体的路由放在最后。

我也将路由分开了,并为不同的路由编写了不同的函数,但我仍然得到重叠的响应。

既然你用 Postman 测试一切正常,那么问题很可能出在你的 React 应用中。

这是一个典型的缓存或数据共享问题,很可能是由于在包级别或全局作用域中共享了可变数据导致的。以下是最常见的几种情况和解决方案:

1. 检查全局变量或包级变量

最常见的问题是使用了全局变量来存储请求结果:

// ❌ 错误示例:全局变量导致数据共享
var cachedResponse []byte
var responseMutex sync.Mutex

func GetDataHandler(w http.ResponseWriter, r *http.Request) {
    responseMutex.Lock()
    defer responseMutex.Unlock()
    
    if cachedResponse == nil {
        // 调用外部API
        resp, _ := http.Get("https://external-api.com/data")
        body, _ := io.ReadAll(resp.Body)
        cachedResponse = body
    }
    
    w.Header().Set("Content-Type", "application/json")
    w.Write(cachedResponse) // 所有请求都返回相同数据
}

解决方案:使用局部变量或请求上下文:

// ✅ 正确示例:每个请求独立处理
func GetDataHandler(w http.ResponseWriter, r *http.Request) {
    // 每个请求都重新获取数据
    resp, err := http.Get("https://external-api.com/data")
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    defer resp.Body.Close()
    
    body, err := io.ReadAll(resp.Body)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    
    w.Header().Set("Content-Type", "application/json")
    w.Write(body)
}

2. 检查HTTP客户端复用

如果复用了HTTP客户端且未正确重置请求,可能导致相同响应:

// ❌ 错误示例:复用请求对象
var client = &http.Client{}
var req *http.Request

func CallExternalAPI(endpoint string) ([]byte, error) {
    if req == nil {
        req, _ = http.NewRequest("GET", "https://external-api.com/"+endpoint, nil)
    } else {
        req.URL, _ = url.Parse("https://external-api.com/" + endpoint)
    }
    
    resp, err := client.Do(req)
    // ... 处理响应
}

解决方案:为每个请求创建新的Request对象:

// ✅ 正确示例:每次创建新请求
var client = &http.Client{
    Timeout: 30 * time.Second,
}

func CallExternalAPI(endpoint string) ([]byte, error) {
    req, err := http.NewRequest("GET", "https://external-api.com/"+endpoint, nil)
    if err != nil {
        return nil, err
    }
    
    // 添加请求头
    req.Header.Set("Content-Type", "application/json")
    
    resp, err := client.Do(req)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()
    
    return io.ReadAll(resp.Body)
}

3. 检查中间件或包装函数

中间件可能缓存了响应:

// ❌ 错误示例:中间件缓存了响应
func CachingMiddleware(next http.HandlerFunc) http.HandlerFunc {
    var cache []byte
    var mu sync.Mutex
    
    return func(w http.ResponseWriter, r *http.Request) {
        mu.Lock()
        defer mu.Unlock()
        
        if cache != nil {
            w.Write(cache)
            return
        }
        
        // 捕获响应
        rw := &responseWriter{ResponseWriter: w}
        next(rw, r)
        
        cache = rw.body
    }
}

type responseWriter struct {
    http.ResponseWriter
    body []byte
}

func (rw *responseWriter) Write(b []byte) (int, error) {
    rw.body = b
    return rw.ResponseWriter.Write(b)
}

4. 完整示例:正确的端点实现

package main

import (
    "encoding/json"
    "io"
    "net/http"
    "time"
    
    "github.com/gorilla/mux"
)

type APIHandler struct {
    client *http.Client
}

func NewAPIHandler() *APIHandler {
    return &APIHandler{
        client: &http.Client{
            Timeout: 30 * time.Second,
        },
    }
}

func (h *APIHandler) GetUserData(w http.ResponseWriter, r *http.Request) {
    vars := mux.Vars(r)
    userID := vars["id"]
    
    // 每个请求独立调用外部API
    data, err := h.fetchFromExternalAPI("/users/" + userID)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(data)
}

func (h *APIHandler) GetProductData(w http.ResponseWriter, r *http.Request) {
    vars := mux.Vars(r)
    productID := vars["id"]
    
    // 独立的调用,不会共享结果
    data, err := h.fetchFromExternalAPI("/products/" + productID)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(data)
}

func (h *APIHandler) fetchFromExternalAPI(path string) (map[string]interface{}, error) {
    req, err := http.NewRequest("GET", "https://external-api.com"+path, nil)
    if err != nil {
        return nil, err
    }
    
    // 为每个请求设置唯一标识
    req.Header.Set("X-Request-ID", time.Now().String())
    
    resp, err := h.client.Do(req)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()
    
    body, err := io.ReadAll(resp.Body)
    if err != nil {
        return nil, err
    }
    
    var result map[string]interface{}
    if err := json.Unmarshal(body, &result); err != nil {
        return nil, err
    }
    
    return result, nil
}

func main() {
    r := mux.NewRouter()
    handler := NewAPIHandler()
    
    r.HandleFunc("/users/{id}", handler.GetUserData)
    r.HandleFunc("/products/{id}", handler.GetProductData)
    
    http.ListenAndServe(":8080", r)
}

5. 调试建议

添加日志来跟踪每个请求:

func (h *APIHandler) GetUserData(w http.ResponseWriter, r *http.Request) {
    requestID := time.Now().UnixNano()
    log.Printf("[%d] Starting GetUserData", requestID)
    
    // ... 处理逻辑
    
    log.Printf("[%d] Completed GetUserData", requestID)
}

检查是否在以下位置共享了数据:

  1. 包级别变量
  2. 单例模式的服务对象
  3. HTTP客户端或请求对象的复用
  4. 响应写入器的复用
  5. 数据库连接或连接池的配置

问题通常出现在数据被意外共享,而不是并发问题。确保每个请求的处理流程都是独立的,不依赖前一个请求的状态。

回到顶部