通用HTTP处理程序
#编程 #开源 #go #开发人员

任何内容类型... glhf


目录


GO编程语言是Vaunt使用的主要语言。我们的开发人员通常会因为其简单性,性能,欢乐的语法和标准图书馆功能而倾向于Golang。在2022年3月,Go增加了对仿制药的支持,这是一个备受期待且常常有争议的功能,该功能已被社区期待。仿制药使开发人员能够创建可以在任何类型上运行的功能,方法和数据结构,而不是仅限于特定类型。如果您是仿制药的新手,请前往Golang Team发表的introduction博客文章。

在这篇博客文章中,我们将探讨如何利用仿制药为HTTP处理程序创建库。具体来说,我们将研究如何创建一个可以处理任何数据类型请求的框架,从而使我们能够编写更高效,可重复使用和可读的代码。

让我们利用我们的通用,轻巧的处理程序框架(称为GLHF)来深入研究HTTP处理程序中的仿制药。

常见的编组模式

GO的标准库在构建REST API方面有很好的支持。但是,通常有几乎所有HTTP处理程序都必须实施的常见模式,例如编制和解除请求和响应。这创建了代码冗余的区域,这可能导致错误。

让我们看一下一个简单的HTTP服务器,该服务器使用标准HTTP库和Gorilla Mux实现一些路线进行基本URL参数解析。

警告Gorilla Mux已存档。我们仍然利用大猩猩Mux在Vaunt中,但建议您寻找替代方案,例如HTTPRouter

下面是一个简单的示例HTTP API,它实现创建和检索todo。

package main

import (
    "context"
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "os"
    "os/signal"
    "time"

    "github.com/VauntDev/glhf/example/pb"
    "github.com/gorilla/mux"
    "golang.org/x/sync/errgroup"
    "golang.org/x/sys/unix"
)

type TodoService struct {
    todos map[string]*pb.Todo
}

func (ts *TodoService) Add(t *pb.Todo) error {
    ts.todos[t.Id] = t
    return nil
}

func (ts *TodoService) Get(id string) (*pb.Todo, error) {
    t, ok := ts.todos[id]
    if !ok {
        return nil, fmt.Errorf("no todo")
    }
    return t, nil
}

type Handlers struct {
    service *TodoService
}

func (h *Handlers) LookupTodo(w http.ResponseWriter, r *http.Request) {
    p := mux.Vars(r)

    id, ok := p["id"]
    if !ok {
        w.WriteHeader(http.StatusInternalServerError)
        return
    }

    todo, err := h.service.Get(id)
    if err != nil {
        w.WriteHeader(http.StatusNotFound)
        return
    }

    b, err := json.Marshal(todo)
    if err != nil {
        w.WriteHeader(http.StatusInternalServerError)
        return
    }

    w.WriteHeader(http.StatusOK)
    w.Write(b)
}

func (h *Handlers) CreateTodo(w http.ResponseWriter, r *http.Request) {
    todo := &pb.Todo{}

    decode := json.NewDecoder(r.Body)
    if err := decode.Decode(todo); err != nil {
        w.WriteHeader(http.StatusInternalServerError)
        return
    }

    if err := h.service.Add(todo); err != nil {
        w.WriteHeader(http.StatusInternalServerError)
        return
    }

    w.WriteHeader(http.StatusOK)
}

func main() {
    TodoService := &TodoService{
        todos: make(map[string]*pb.Todo),
    }
    h := &Handlers{service: TodoService}

    mux := mux.NewRouter()
    mux.HandleFunc("/todo/{id}", h.LookupTodo)
    mux.HandleFunc("/todo", h.CreateTodo)

    server := http.Server{
        Addr:    ":8080",
        Handler: mux,
    }

    g, ctx := errgroup.WithContext(context.Background())

    g.Go(func() error {
        log.Println("starting server")
        if err := server.ListenAndServe(); err != nil {
            return nil
        }
        return nil
    })

    g.Go(func() error {
        sigs := make(chan os.Signal, 1)
        // We'll accept graceful shutdowns when quit via SIGINT (Ctrl+C)
        // SIGKILL, SIGQUIT or SIGTERM (Ctrl+/) will not be caught.
        signal.Notify(sigs, os.Interrupt, unix.SIGTERM)
        select {
        case <-ctx.Done():
            log.Println("ctx done, shutting down server")
        case <-sigs:
            log.Println("caught sig, shutting down server")
        }
        ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
        defer cancel()

        if err := server.Shutdown(ctx); err != nil {
            return fmt.Errorf("error in server shutdown: %w", err)
        }
        return nil
    })

    if err := g.Wait(); err != nil {
        log.Fatal(err)
    }
}

我们将重点关注的两个主要功能是LookupTodoCreateTodo

乍一看,这些功能足够简单。他们使用请求将我们系统中的待办事项查找或创建待办事项。在这两种情况下,我们都使用JSON作为预期的content-type

现在我们有了基本的流程,让我们添加处理人员使用JSON或Protobuf接收/响应的能力。

func (h *Handlers) LookupTodo(w http.ResponseWriter, r *http.Request) {
    p := mux.Vars(r)

    id, ok := p["id"]
    if !ok {
        w.WriteHeader(http.StatusInternalServerError)
        return
    }

    todo, err := h.service.Get(id)
    if err != nil {
        w.WriteHeader(http.StatusNotFound)
        return
    }

    switch r.Header.Get("accept") {
    case "application/json":
        b, err := json.Marshal(todo)
        if err != nil {
            w.WriteHeader(http.StatusInternalServerError)
            return
        }
        w.WriteHeader(http.StatusOK)
        w.Write(b)
    case "application/proto":
        b, err := proto.Marshal(todo)
        if err != nil {
            w.WriteHeader(http.StatusInternalServerError)
            return
        }
        w.WriteHeader(http.StatusOK)
        w.Write(b)
    }
}

func (h *Handlers) CreateTodo(w http.ResponseWriter, r *http.Request) {
    todo := &pb.Todo{}

    switch r.Header.Get("content-type") {
    case "application/json":
        decode := json.NewDecoder(r.Body)
        if err := decode.Decode(todo); err != nil {
            w.WriteHeader(http.StatusInternalServerError)
            return
        }
    case "application/proto":
        b, err := ioutil.ReadAll(r.Body)
        if err != nil {
            w.WriteHeader(http.StatusInternalServerError)
            return
        }
        if err := proto.Unmarshal(b, todo); err != nil {
            w.WriteHeader(http.StatusInternalServerError)
            return
        }
    default:
        w.WriteHeader(http.StatusBadRequest)
        return
    }

    if err := h.service.Add(todo); err != nil {
        w.WriteHeader(http.StatusInternalServerError)
        return
    }

    w.WriteHeader(http.StatusOK)
}

上面的示例开始突出显示常见的套图模式。在这种情况下,我们实施了一条可以返回两种不同格式的路线,即JSON和ProtoBuf。

您可能会问,为什么我们要这样做?

,如果您想使用非人类可读格式(例如Protobuf)在应用程序之间进行通信,这是一个相当普遍的用途。与JSON。

虽然这只是一个小例子,但它开始布置冗余的逻辑。现在想象一下所有有条件地支持独特格式的数十个或数百种API路由。

那是很多安排...

Image description

可能的解决方案

有很多方法可以解决上述冗余。但是,我们的方法使用GO GENICS解决上述问题。

让我们看一下如何利用GLHF来减少处理程序中的编组逻辑。

func (h *Handlers) LookupTodo(r *glhf.Request[glhf.EmptyBody], w *glhf.Response[pb.Todo]) {
    p := mux.Vars(r.HTTPRequest())

    id, ok := p["id"]
    if !ok {
        w.SetStatus(http.StatusInternalServerError)
        return
    }

    todo, err := h.service.Get(id)
    if err != nil {
        w.SetStatus(http.StatusNotFound)
        return
    }

    w.SetBody(todo)
    w.SetStatus(http.StatusOK)
}

func (h *Handlers) CreateTodo(r *glhf.Request[pb.Todo], w *glhf.Response[glhf.EmptyBody]) {
    if r.Body() == nil {
        w.SetStatus(http.StatusBadRequest)
        return
    }

    if err := h.service.Add(r.Body()); err != nil {
        w.SetStatus(http.StatusInternalServerError)
        return
    }
    w.SetStatus(http.StatusOK)
}

就是这样!以上是编写相同的HTTP处理程序,该处理程序现在根据请求中的标题支持JSON和Protobuff同时支持JSON和Protobuff。

让我们浏览GLHF并解开更多细节。

通用处理程序

GLHF通过包装GO的标准库HTTP处理程序功能来起作用。这样可以确保我们可以使用标准的HTTP软件包。为此,我们需要创建一些基本类型。

首先,我们首先定义一个新的请求和响应一个特定于GLHF的对象。

// Body is the request's body.
type Body any

// A Request represents an HTTP request received by a server
type Request[T Body] struct {
    r    *http.Request
    body *T
}
// Response represents the response from an HTTP request.
type Response[T any] struct {
    w          http.ResponseWriter
    statusCode int
    body       *T
    marshal    MarshalFunc[T]
}

这两个结构的关键是使用我们的通用体型。我们目前没有在身体上使用其他类型约束,而是将其定义为可读性和可能添加的未来类型约束的类型。

RequestResponse类型定义了初始化期间的身体类型。

GLHF的第二个组件是负责我们的通用RequestResponse structs的句柄函数定义。

type HandleFunc[I Body, O Body] func(*Request[I], *Response[O])

这使我们能够编写类似标准库处理程序功能的功能。

最后,我们定义了一组功能,这些功能占用GLHF HandleFunc并返回标准库http.handleFunc

这是我们的Get实现的示例:

func Get[I EmptyBody, O any](fn HandleFunc[I, O], options ...Options) http.HandlerFunc {
    opts := defaultOptions()
    for _, opt := range options {
        opt.Apply(opts)
    }

    return func(w http.ResponseWriter, r *http.Request) {
        var errResp *errorResponse
        if r.Method != http.MethodGet {
            errResp = &errorResponse{
                Code:    http.StatusMethodNotAllowed,
                Message: "invalid method used, expected GET found " + r.Method,
            }
        }

        req := &Request[I]{r: r}
        response := &Response[O]{w: w}

        // call the handler
        fn(req, response)

        if response.body != nil {
            var bodyBytes []byte
            // if there is a custom marshaler, prioritize it
            if response.marshal != nil {
                b, err := response.marshal(*response.body)
                if err != nil {
                    errResp = &errorResponse{
                        Code:    http.StatusInternalServerError,
                        Message: "failed to marshal response with custom marhsaler",
                    }
                }
                bodyBytes = b
            } else {
                // client preferred content-type
                b, err := marshalResponse(r.Header.Get(Accept), response.body)
                if err != nil {
                    // server preferred content-type
                    contentType := response.w.Header().Get(ContentType)
                    if len(contentType) == 0 {
                        contentType = opts.defaultContentType
                    }
                    b, err = marshalResponse(contentType, response.body)
                    if err != nil {
                        errResp = &errorResponse{
                            Code:    http.StatusInternalServerError,
                            Message: "failed to marshal response with content-type: " + contentType,
                        }
                    }
                }
                bodyBytes = b
            }
            // Response failed to marshal
            if errResp != nil {
                w.WriteHeader(errResp.Code)
                if opts.verbose {
                    b, _ := json.Marshal(errResp)
                    w.Write(b)
                }
                return
            }

            // ensure user supplied status code is valid
            if validStatusCode(response.statusCode) {
                w.WriteHeader(response.statusCode)
            }
            if len(bodyBytes) > 0 {
                w.Write(bodyBytes)
            }
            return
        }
    }
}

此功能有很多东西,但是从核心上讲,它的作用很像简单的中间件。 GLHF试图根据Content-typeAccept的HTTP标头值来调整HTTP请求并响应正确的格式,以照顾常见的整理流。

最终,我们可以在各种HTTP路由器中利用这种模式来简单地简单地使用HTTP处理程序逻辑。

这是与大猩猩Mux一起使用的GLHF的示例。

mux := mux.NewRouter()
    mux.HandleFunc("/todo", glhf.Post(h.CreateTodo))
    mux.HandleFunc("/todo/{id}", glhf.Get(h.LookupTodo))

结束思想

希望,这使您对如何在GO中使用仿制药以及它们可以帮助解决的问题有一种感觉。简而言之,在Golang的HTTP处理程序中使用仿制药可以提供多种好处,包括:

  1. 可重复使用性:使用Generics允许您编写可以与各种类型一起使用的代码,而无需为每种类型编写重复的代码。这使重复使用代码和减少您需要编写的样板代码的数量变得更加容易。
  2. 类型安全:仿制药允许您对代码可以使用的类型指定约束。这可以有助于在编译时间而不是在运行时捕获错误,从而提高代码的整体安全性。
  3. 灵活性:使用仿制药允许您编写可以与不同类型的数据(包括不同的数据结构和数据格式)一起使用的代码。当使用来自外部来源的数据时,这可能特别有用。
  4. 性能:仿制药可以通过减少使用的内存数量并减少执行的不必要类型转换的数量来提高代码的性能。

虽然Go中的仿制药提供了许多好处,但也有一些潜在的缺点:

  1. 增加的复杂性:仿制药可以使代码更加复杂,更难理解,尤其是对于不熟悉类型参数概念的开发人员。
  2. 增加维护:仿制药可以使代码更加抽象,这可能需要更多的维护和测试,以确保代码与所有可能的类型都正确地工作。
  3. 学习曲线:引入仿制药可能需要一些不熟悉该概念的开发人员的其他学习。
  4. 兼容性:添加仿制药可能会与引入仿制药之前编写的现有代码兼容问题。

尽管存在这些潜在的缺点,但仿制药在GO中的好处通常超过缺点,许多开发人员对仿制药提供的新可能性感到兴奋。

我们发现GLHF很有价值,并希望其他人也这样做!如果您有兴趣了解有关GLHF的更多信息,请前往我们的GitHub。该博客的源代码可以在GLHF的example目录中找到。

我们有许多针对GLHF计划的令人兴奋的功能和更新,因此请继续关注即将发布的版本!

要留在未来发展的循环中,请在Twitter上关注我们或加入我们的Discord!不要犹豫提出功能请求。

祝你好运,玩得开心!