Golang中这种API调用方式是否违反了任何规则?

Golang中这种API调用方式是否违反了任何规则? 我的目标是创建一个未来能够服务于数百个端点的API。在我的尝试中,我可能会打破几条“规则”,以使代码更通用、避免输入错误并使其更易于维护。

我打破的第一条规则是在处理Postgresql时使用sqlx,这显著减少了代码量。

我打破的第二条规则是将查询移至查找表。这减少了端点的数量。因此,每个API调用都会两次调用数据库:首先获取查询,然后执行查询。这样做的一个好处是,我可以动态维护查询而无需重启Go程序。

我可能打破的第三条规则是使用http.Get,因为它更易于理解。

这是我作为新手首次尝试创建API。到目前为止,它只实现了CRUD中的GET部分。我可能过度简化了。以下是我的代码:

Web客户端

func main() {
    fmt.Println("hello world")
}

API

func main() {
    fmt.Println("hello world")
}

实时演示 http://94.237.92.101:6060

我的问题:

  1. API是否应始终交付JSON(而不是map[])?
  2. 您认为这种方法存在任何潜在危险吗?
  3. 我应该重新考虑哪些部分?

更多关于Golang中这种API调用方式是否违反了任何规则?的实战教程也可以访问 https://www.itying.com/category-94-b0.html

21 回复

为什么不使用 gRPC?

更多关于Golang中这种API调用方式是否违反了任何规则?的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


func main() {
    fmt.Println("hello world")
}

geosoft1:

应用程序可能会崩溃

谢谢!我会进一步调查这个问题。

amnon:

json.NewEncoder(w).Encode(data)

谢谢!这个方法非常有效!http://94.237.92.101:6060/posts

amnon:

类似这样吗?

那么其他99张表呢?我该如何处理?不仅仅是“books”,还有“pens”、“glasses”、“flowers”等等。处理一张表的端点和处理器对我来说非常清楚。我需要理解的是如何处理100张SQL表的情况。

Sibert:

最坏的情况会是什么?只是好奇……

应用程序可能会崩溃(参见这里的示例)。但只有在计划在并发环境中通过映射交换数据时,才需要担心这个问题。

请查看 https://github.com/gorilla/mux,它完全符合你的需求… (查看 mux.Vars) 你只需要定义 4 个端点处理器, 并将 {title} 指定为一个变量。

一个建议是将

fmt.Fprintf(w, "%s", json)

替换为

w.Write(json)

关于映射(maps),我看到你将其用作局部变量,但一般来说,在并发环境中,写入时必须小心地对其进行锁定。

关于API,一个好的做法是返回JSON结构体。

或者更好的做法是替换

		json, _ := json.Marshal(data)
		// present result for the web, partner or client
		w.Header().Set("Content-Type", "application/json")
		fmt.Fprintf(w, "%s", json)

    json.NewEncoder(w).Encode(data)

替换为 w.Write(json)

完成了。但这有什么好处呢?除了代码更简洁之外?

关于映射(maps),我看到你将其用作局部变量,但一般来说,在并发环境中,写入时必须小心地加锁。

能否请你详细说明一下?我在哪里使用了局部变量,以及我应该如何以更好的方式处理?

先谢谢了。

这当然只是将选择访问哪个表的问题转移到了 getObj() 函数中。 但这能让你的 HTTP 处理程序保持简单和符合惯用法…

谢谢!现在看起来更清晰一些了。我必须亲手实践才能完全理解。

但我认为客户端和 API 之间可能存在差异。在我看来,API 对此的需求更大。我错了吗?

我没有找到关于如何实现这一点的示例。最接近的是这个,但我没有理解其要点。 https://opensource.com/article/18/8/http-request-routing-validation-gorillamux

我可能只有4个端点,但有1000个处理器?有没有比我找到的更好的例子?

我想学习gRPC,但一直无法理解它。网上有很多关于如何使用传统REST API的“操作指南”,但几乎没有涉及使用gRPC进行常见CRUD操作的资料。gRPC的复杂性确实更高。

这段代码是REST API的示例:http://94.237.92.101:6060/api

我该如何将这段代码“翻译”成gRPC版本?这样做会不会更复杂?

如果我理解正确的话,REST API 和 HTML 客户端基本上做的是同一件事。 但前者将数据序列化为 JSON,而后者则将其渲染到 HTML 模板中。

因此,如果扩展我的简单示例,我们可以直接将 JSON 编码行替换为类似这样的代码:

tpl.ExecuteTemplate(w, objType, obj)

我个人的做法是只做 API,然后不编写 HTML 客户端,而是用 JS 编写一个前端,该前端与 API 通信以在浏览器中渲染页面。这让你可以很好地将表示层与后端分离——代价是必须处理 JavaScript。

Sibert:

但这样做有什么好处呢?除了看起来更简洁之外?

它比 fmt 库更快。

Sibert:

我在哪里使用局部变量,以及如何以更好的方式使用它?

只是说在并发环境中最好避免使用 map,或者谨慎使用它们。

Sibert:

你认为这种方法有什么潜在的危险吗?

嗯,我认为将查询语句保存在数据库中可能会导致安全漏洞,或者如果有人在数据库中更改了查询语句,或者该记录以某种方式被修改,可能会导致应用程序故障。我猜将查询语句硬编码在应用程序中会更安全。

在并发环境中最好避免使用映射或谨慎使用它们。

最坏的情况会发生什么?只是好奇……

嗯,我认为将查询语句保存在数据库中可能会导致安全漏洞或应用程序故障,如果有人更改了数据库中的查询语句或以某种方式更改了该记录。我想将查询语句硬编码在应用程序中会更安全。

API 应该在角色级别设置为只读。管理和维护可能多达 1000 个端点/查询,对于类型错误等风险可能更大。

我看不到更改查找数据库有任何风险。但可能还有其他我现在不知道的风险。

类似这样吗?

import (
	"net/http"

	"github.com/gorilla/mux"
)

func readBook(w http.ResponseWriter, r *http.Request) {
	vars := mux.Vars(r)
	title  := vars["title"]
	book, err := getBook(title)
	if err != nil {
		http.NotFound(w, r)
		return
	}
    json.NewEncoder(w).Encode(book)
}

func main() {
	r := mux.NewRouter()
	r.HandleFunc("/books/{title}", createBook).Methods("POST")
	r.HandleFunc("/books/{title}", readBook).Methods("GET")
	r.HandleFunc("/books/{title}", updateBook).Methods("PUT")
	r.HandleFunc("/books/{title}", deleteBook).Methods("DELETE")
	http.ListenAndServe(":80", r)
}

我也注意到你写了很多这样的行:

  page := (path + ".html")

括号是不必要的,所以:

 page := path + ".html"

效果完全一样……

endpoint() 函数基本上是一个手写的 Mux。 这没问题。但可以看看其他一些 Mux(包括标准库中的 http.ServeMux),了解一下它们通常的行为方式。

有些地方的代码行数比实际需要的多。

所以:

// get data and fill template
var data interface{}
data = json2map(url)

if data == nil {
  tpl.ExecuteTemplate(w, page, nil)
} else {
  tpl.ExecuteTemplate(w, page, data)
}

可以替换为:

// get data and fill template
data := json2map(url)
tpl.ExecuteTemplate(w, page, data)

更少的代码能让其他人更容易阅读和理解发生了什么。

享受 Go 语言,祝你好运……

哦,我明白了……

那么像下面这样如何:

package main

import (
	"encoding/json"
	"net/http"

	"github.com/gorilla/mux"
)

func readObj(w http.ResponseWriter, r *http.Request) {
	vars := mux.Vars(r)
	objType := vars["objtype"]
	title := vars["title"]
	obj, err := getObj(objType, title)
	if err != nil {
		http.NotFound(w, r)
		return
	}
	json.NewEncoder(w).Encode(obj)
}

func main() {
	r := mux.NewRouter()
	r.HandleFunc("/{objtype}/{title}", createObj).Methods("POST")
	r.HandleFunc("/{objtype}/{title}", readObj).Methods("GET")
	r.HandleFunc("/{objtype}/{title}", updateObj).Methods("PUT")
	r.HandleFunc("/{objtype}/{title}", deleteObj).Methods("DELETE")
	http.ListenAndServe(":80", r)
}

当然,这只是将选择访问哪个表的问题转移到了 getObj() 函数中。 但这能让你的 HTTP 处理程序保持简洁和符合惯例……

感谢您的建议。已实施。

amnon: endpoint() 函数基本上是一个手写的 Mux。 这没问题。但可以看看其他一些现成的 mux(包括标准库中的 http.ServeMux),了解一下它们通常是如何工作的。

我曾试图理解在“hello world”示例之外,mux 是如何工作的,但从未找到一个可能使用 1000 个端点的例子。下面我模拟了一个例子,说明我是如何理解它的。从长远来看,这将很难维护。如果我理解错了,请纠正我。

我对端点的计算是基于 100 个表 * CRUD 操作 = 400 个端点 + 一些额外的端点。

r.HandleFunc("/books/{title}", CreateBook).Methods("POST")
r.HandleFunc("/books/{title}", ReadBook).Methods("GET")
r.HandleFunc("/books/{title}", UpdateBook).Methods("PUT")
r.HandleFunc("/books/{title}", DeleteBook).Methods("DELETE")
r.HandleFunc("/books/{title}", CreateBook).Methods("POST")
r.HandleFunc("/books/{title}", ReadBook).Methods("GET")
r.HandleFunc("/books/{title}", UpdateBook).Methods("PUT")
r.HandleFunc("/books/{title}", DeleteBook).Methods("DELETE")
r.HandleFunc("/books/{title}", CreateBook).Methods("POST")
r.HandleFunc("/books/{title}", ReadBook).Methods("GET")
r.HandleFunc("/books/{title}", UpdateBook).Methods("PUT")
r.HandleFunc("/books/{title}", DeleteBook).Methods("DELETE")
r.HandleFunc("/books/{title}", CreateBook).Methods("POST")
r.HandleFunc("/books/{title}", ReadBook).Methods("GET")
r.HandleFunc("/books/{title}", UpdateBook).Methods("PUT")
r.HandleFunc("/books/{title}", DeleteBook).Methods("DELETE")
r.HandleFunc("/books/{title}", CreateBook).Methods("POST")
r.HandleFunc("/books/{title}", ReadBook).Methods("GET")
r.HandleFunc("/books/{title}", UpdateBook).Methods("PUT")
r.HandleFunc("/books/{title}", DeleteBook).Methods("DELETE")
r.HandleFunc("/books/{title}", CreateBook).Methods("POST")
r.HandleFunc("/books/{title}", ReadBook).Methods("GET")
r.HandleFunc("/books/{title}", UpdateBook).Methods("PUT")
r.HandleFunc("/books/{title}", DeleteBook).Methods("DELETE")
r.HandleFunc("/books/{title}", CreateBook).Methods("POST")
r.HandleFunc("/books/{title}", ReadBook).Methods("GET")
r.HandleFunc("/books/{title}", UpdateBook).Methods("PUT")
r.HandleFunc("/books/{title}", DeleteBook).Methods("DELETE")
+ 另外 1000 个端点...

因此,我正在尝试将 mux 创建为动态端点。还有其他可维护的方法来实现这一点吗?

关于你的API设计问题的专业分析

1. JSON响应格式问题

是的,API应该始终返回标准化的JSON响应,而不是原始的map[]。这确保了API的一致性和可预测性。

// 正确的JSON响应示例
type APIResponse struct {
    Success bool        `json:"success"`
    Data    interface{} `json:"data,omitempty"`
    Error   string      `json:"error,omitempty"`
    Message string      `json:"message,omitempty"`
}

func respondWithJSON(w http.ResponseWriter, status int, data interface{}) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    
    response := APIResponse{
        Success: status >= 200 && status < 300,
        Data:    data,
    }
    
    if err := json.NewEncoder(w).Encode(response); err != nil {
        http.Error(w, "Failed to encode response", http.StatusInternalServerError)
    }
}

// 使用示例
func getUserHandler(w http.ResponseWriter, r *http.Request) {
    user := map[string]interface{}{
        "id":    1,
        "name": "John Doe",
        "email": "john@example.com",
    }
    respondWithJSON(w, http.StatusOK, user)
}

2. 潜在危险分析

查询查找表的安全风险:

// 危险:SQL注入风险
func getQueryFromLookup(queryName string) (string, error) {
    var query string
    err := db.Get(&query, "SELECT query_sql FROM query_lookup WHERE name = ?", queryName)
    return query, err
}

// 执行动态查询时的风险
func executeDynamicQuery(db *sqlx.DB, query string, params ...interface{}) error {
    // 如果查询来自不可信源,可能包含恶意SQL
    rows, err := db.Queryx(query, params...)
    // ...
}

http.Get的问题:

// 问题:缺乏超时控制
func fetchData(url string) ([]byte, error) {
    resp, err := http.Get(url) // 没有超时设置
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()
    return io.ReadAll(resp.Body)
}

// 改进版本
func fetchDataWithTimeout(url string) ([]byte, error) {
    client := &http.Client{
        Timeout: 10 * time.Second, // 设置超时
    }
    resp, err := client.Get(url)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()
    return io.ReadAll(resp.Body)
}

3. 需要重新考虑的部分

查询缓存机制:

type QueryCache struct {
    queries map[string]string
    mu      sync.RWMutex
    db      *sqlx.DB
}

func (qc *QueryCache) GetQuery(name string) (string, error) {
    qc.mu.RLock()
    query, exists := qc.queries[name]
    qc.mu.RUnlock()
    
    if exists {
        return query, nil
    }
    
    // 缓存未命中,从数据库加载
    qc.mu.Lock()
    defer qc.mu.Unlock()
    
    err := qc.db.Get(&query, 
        "SELECT query_sql FROM query_lookup WHERE name = $1", name)
    if err != nil {
        return "", err
    }
    
    qc.queries[name] = query
    return query, nil
}

API端点结构优化:

type EndpointHandler struct {
    db         *sqlx.DB
    queryCache *QueryCache
}

func (h *EndpointHandler) HandleGenericGet(w http.ResponseWriter, r *http.Request) {
    queryName := r.URL.Query().Get("query")
    if queryName == "" {
        respondWithJSON(w, http.StatusBadRequest, 
            map[string]string{"error": "query parameter required"})
        return
    }
    
    // 获取缓存的查询
    query, err := h.queryCache.GetQuery(queryName)
    if err != nil {
        respondWithJSON(w, http.StatusNotFound, 
            map[string]string{"error": "query not found"})
        return
    }
    
    // 执行查询(使用参数化查询防止SQL注入)
    var results []map[string]interface{}
    err = h.db.Select(&results, query) // sqlx自动处理参数化查询
    if err != nil {
        respondWithJSON(w, http.StatusInternalServerError, 
            map[string]string{"error": err.Error()})
        return
    }
    
    respondWithJSON(w, http.StatusOK, results)
}

数据库连接池配置:

func initDB() (*sqlx.DB, error) {
    db, err := sqlx.Connect("postgres", "user=postgres dbname=api sslmode=disable")
    if err != nil {
        return nil, err
    }
    
    // 优化连接池设置
    db.SetMaxOpenConns(25)
    db.SetMaxIdleConns(25)
    db.SetConnMaxLifetime(5 * time.Minute)
    
    return db, nil
}

中间件添加:

func loggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        next.ServeHTTP(w, r)
        log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start))
    })
}

func panicRecoveryMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if err := recover(); err != nil {
                log.Printf("panic recovered: %v", err)
                respondWithJSON(w, http.StatusInternalServerError, 
                    map[string]string{"error": "internal server error"})
            }
        }()
        next.ServeHTTP(w, r)
    })
}

你的方法在概念上是可行的,但需要加强安全性和性能优化。查询查找表的设计可以继续使用,但必须添加缓存层和安全验证。

回到顶部