mirror of
https://github.com/infinite-iroha/touka.git
synced 2026-02-03 08:51:11 +08:00
feat: Implement data binding, enhance HTML rendering, add context tests
This commit introduces several significant enhancements and completes key features:
1. **Enhanced HTML Rendering:**
- Defined a new `touka.HTMLRender` interface in `engine.go` for pluggable HTML rendering.
- `Engine.HTMLRender` now uses this interface type.
- `Context.HTML` is updated to use the configured `HTMLRender` instance, with improved error handling if no renderer is set or if rendering fails. The previous `fmt.Sprintf` fallback has been removed.
- Added `DefaultHTMLRenderer` (using `html/template`) and helper methods `Engine.LoadHTMLGlob()` and `Engine.SetHTMLTemplate()` for easy setup.
2. **Comprehensive Data Binding:**
- **`Context.ShouldBindForm`**: Implemented using `gorilla/schema` to bind `x-www-form-urlencoded` and `multipart/form-data` (fields) from the request body.
- **`Context.ShouldBindQuery`**: Implemented using `gorilla/schema` to bind URL query parameters.
- **`Context.ShouldBindXML`**: Implemented using `encoding/xml` to bind XML data from the request body. This method also respects the `Engine.MaxRequestBodySize` limit.
- **`Context.ShouldBind`**: Implemented as a generic binder that inspects the `Content-Type` header and dispatches to the appropriate specific binder (JSON, XML, Form). It handles missing or unsupported content types.
3. **Comprehensive Unit Tests for `context.go`:**
- Massively expanded `context_test.go` to provide extensive test coverage for nearly all methods in `context.go`.
- This includes detailed tests for all new data binding methods, the updated HTML rendering logic, state management (`Keys`), request/response utilities, error handling, header and cookie manipulation, streaming, Go context integration, and logging.
- Mocks for `HTMLRender`, `ErrorHandler`, and `reco.Logger` were used to facilitate thorough testing.
These changes significantly improve the framework's feature set, robustness, and maintainability due to increased test coverage.
This commit is contained in:
parent
82099e26ee
commit
6339532d78
5 changed files with 1547 additions and 95 deletions
218
context.go
218
context.go
|
|
@ -4,11 +4,13 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/gob"
|
"encoding/gob"
|
||||||
|
"encoding/xml" // Added for XML binding
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"io"
|
"io"
|
||||||
"math"
|
"math"
|
||||||
|
"mime" // Added for Content-Type parsing in ShouldBind
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
|
@ -19,6 +21,7 @@ import (
|
||||||
|
|
||||||
"github.com/fenthope/reco"
|
"github.com/fenthope/reco"
|
||||||
"github.com/go-json-experiment/json"
|
"github.com/go-json-experiment/json"
|
||||||
|
"github.com/gorilla/schema" // Added for form binding
|
||||||
|
|
||||||
"github.com/WJQSERVER-STUDIO/go-utils/copyb"
|
"github.com/WJQSERVER-STUDIO/go-utils/copyb"
|
||||||
"github.com/WJQSERVER-STUDIO/httpc"
|
"github.com/WJQSERVER-STUDIO/httpc"
|
||||||
|
|
@ -298,21 +301,24 @@ func (c *Context) HTML(code int, name string, obj interface{}) {
|
||||||
c.Writer.Header().Set("Content-Type", "text/html; charset=utf-8")
|
c.Writer.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
c.Writer.WriteHeader(code)
|
c.Writer.WriteHeader(code)
|
||||||
|
|
||||||
if c.engine != nil && c.engine.HTMLRender != nil {
|
if c.engine == nil || c.engine.HTMLRender == nil {
|
||||||
// 假设 HTMLRender 是一个 *template.Template 实例
|
errMsg := "HTML renderer not configured"
|
||||||
if tpl, ok := c.engine.HTMLRender.(*template.Template); ok {
|
if c.engine != nil && c.engine.LogReco != nil {
|
||||||
err := tpl.ExecuteTemplate(c.Writer, name, obj)
|
c.engine.LogReco.Error("[Context.HTML] HTMLRender not configured on engine")
|
||||||
if err != nil {
|
} else {
|
||||||
c.AddError(fmt.Errorf("failed to render HTML template '%s': %w", name, err))
|
// Fallback logging if LogReco is also nil, though unlikely if engine is not nil
|
||||||
//c.String(http.StatusInternalServerError, "Internal Server Error: Failed to render HTML template")
|
// log.Println("[Context.HTML] HTMLRender not configured on engine")
|
||||||
c.ErrorUseHandle(http.StatusInternalServerError, fmt.Errorf("failed to render HTML template '%s': %w", name, err))
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
// 可以扩展支持其他渲染器接口
|
c.ErrorUseHandle(http.StatusInternalServerError, errors.New(errMsg))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := c.engine.HTMLRender.Render(c.Writer, name, obj, c)
|
||||||
|
if err != nil {
|
||||||
|
renderErr := fmt.Errorf("failed to render HTML template '%s': %w", name, err)
|
||||||
|
c.AddError(renderErr)
|
||||||
|
c.ErrorUseHandle(http.StatusInternalServerError, renderErr)
|
||||||
}
|
}
|
||||||
// 默认简单输出,用于未配置 HTMLRender 的情况
|
|
||||||
c.Writer.Write([]byte(fmt.Sprintf("<!-- HTML rendered for %s -->\n<pre>%v</pre>", name, obj)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Redirect 执行 HTTP 重定向
|
// Redirect 执行 HTTP 重定向
|
||||||
|
|
@ -356,21 +362,181 @@ func (c *Context) ShouldBindJSON(obj interface{}) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ShouldBindXML 尝试将请求体中的 XML 数据绑定到 obj。
|
||||||
|
func (c *Context) ShouldBindXML(obj interface{}) error {
|
||||||
|
if c.Request == nil || c.Request.Body == nil {
|
||||||
|
return errors.New("request body is empty for XML binding")
|
||||||
|
}
|
||||||
|
// defer c.Request.Body.Close() // Caller is responsible for closing the body
|
||||||
|
|
||||||
|
var reader io.Reader = c.Request.Body
|
||||||
|
if c.engine != nil && c.engine.MaxRequestBodySize > 0 {
|
||||||
|
if c.Request.ContentLength != -1 && c.Request.ContentLength > c.engine.MaxRequestBodySize {
|
||||||
|
return fmt.Errorf("request body size (%d bytes) exceeds XML binding limit (%d bytes)", c.Request.ContentLength, c.engine.MaxRequestBodySize)
|
||||||
|
}
|
||||||
|
reader = http.MaxBytesReader(nil, c.Request.Body, c.engine.MaxRequestBodySize)
|
||||||
|
}
|
||||||
|
|
||||||
|
decoder := xml.NewDecoder(reader)
|
||||||
|
if err := decoder.Decode(obj); err != nil {
|
||||||
|
// Check for MaxBytesError specifically
|
||||||
|
var maxBytesErr *http.MaxBytesError
|
||||||
|
if errors.As(err, &maxBytesErr) {
|
||||||
|
return fmt.Errorf("request body size exceeds XML binding limit (%d bytes): %w", c.engine.MaxRequestBodySize, err)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("xml binding error: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShouldBindQuery 尝试将 URL 查询参数绑定到 obj。
|
||||||
|
// 它使用 gorilla/schema 来执行绑定。
|
||||||
|
func (c *Context) ShouldBindQuery(obj interface{}) error {
|
||||||
|
if c.Request == nil {
|
||||||
|
return errors.New("request is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.queryCache == nil {
|
||||||
|
c.queryCache = c.Request.URL.Query()
|
||||||
|
}
|
||||||
|
values := c.queryCache
|
||||||
|
|
||||||
|
if len(values) == 0 {
|
||||||
|
// No query parameters to bind
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
decoder := schema.NewDecoder()
|
||||||
|
// decoder.IgnoreUnknownKeys(true) // Optional
|
||||||
|
|
||||||
|
if err := decoder.Decode(obj, values); err != nil {
|
||||||
|
return fmt.Errorf("query parameter binding error using schema: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// ShouldBind 尝试将请求体绑定到各种类型(JSON, Form, XML 等)
|
// ShouldBind 尝试将请求体绑定到各种类型(JSON, Form, XML 等)
|
||||||
// 这是一个复杂的通用绑定接口,通常根据 Content-Type 或其他头部来判断绑定方式
|
// 根据请求的 Content-Type 自动选择合适的绑定器。
|
||||||
// 预留接口,可根据项目需求进行扩展
|
|
||||||
func (c *Context) ShouldBind(obj interface{}) error {
|
func (c *Context) ShouldBind(obj interface{}) error {
|
||||||
// TODO: 完整的通用绑定逻辑
|
if c.Request == nil {
|
||||||
// 可以根据 c.Request.Header.Get("Content-Type") 来判断是 JSON, Form, XML 等
|
return errors.New("request is nil for binding")
|
||||||
// 例如:
|
}
|
||||||
// contentType := c.Request.Header.Get("Content-Type")
|
|
||||||
// if strings.HasPrefix(contentType, "application/json") {
|
// If there's no body, no binding from body can occur.
|
||||||
// return c.ShouldBindJSON(obj)
|
if c.Request.Body == nil || c.Request.Body == http.NoBody {
|
||||||
// }
|
// Consider if query binding should be attempted for GET requests by default.
|
||||||
// if strings.HasPrefix(contentType, "application/x-www-form-urlencoded") || strings.HasPrefix(contentType, "multipart/form-data") {
|
// For now, if no body, assume successful (empty) binding from body perspective.
|
||||||
// return c.ShouldBindForm(obj) // 需要实现 ShouldBindForm
|
return nil
|
||||||
// }
|
}
|
||||||
return errors.New("generic binding not fully implemented yet, implement based on Content-Type")
|
|
||||||
|
contentType := c.ContentType() // This uses c.GetReqHeader("Content-Type")
|
||||||
|
if contentType == "" {
|
||||||
|
// If there is a body (ContentLength > 0 or chunked) but no Content-Type, this is an issue.
|
||||||
|
if c.Request.ContentLength > 0 || len(c.Request.TransferEncoding) > 0 {
|
||||||
|
return errors.New("missing Content-Type header for request body binding")
|
||||||
|
}
|
||||||
|
// If no Content-Type and no actual body content indicated, effectively no body to bind.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
mimeType, _, err := mime.ParseMediaType(contentType)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error parsing Content-Type header '%s': %w", contentType, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch mimeType {
|
||||||
|
case "application/json":
|
||||||
|
return c.ShouldBindJSON(obj)
|
||||||
|
case "application/xml", "text/xml":
|
||||||
|
return c.ShouldBindXML(obj)
|
||||||
|
case "application/x-www-form-urlencoded":
|
||||||
|
return c.ShouldBindForm(obj)
|
||||||
|
case "multipart/form-data":
|
||||||
|
return c.ShouldBindForm(obj) // ShouldBindForm handles multipart fields
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported Content-Type for binding: %s", mimeType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShouldBindForm 尝试将请求体中的表单数据绑定到 obj。
|
||||||
|
// 它使用 gorilla/schema 来执行绑定。
|
||||||
|
// 注意:此方法期望请求体是 application/x-www-form-urlencoded 或 multipart/form-data 类型。
|
||||||
|
// 调用此方法前,应确保请求的 Content-Type 是合适的。
|
||||||
|
func (c *Context) ShouldBindForm(obj interface{}) error {
|
||||||
|
if c.Request == nil {
|
||||||
|
return errors.New("request is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseMultipartForm populates c.Request.PostForm and c.Request.MultipartForm.
|
||||||
|
// defaultMemory is used to limit the size of memory used for storing file parts.
|
||||||
|
// If the form data exceeds this, it will be stored in temporary files.
|
||||||
|
// MaxBytesReader applied earlier (if any) would have limited the total body size.
|
||||||
|
if err := c.Request.ParseMultipartForm(defaultMemory); err != nil {
|
||||||
|
// Ignore "http: multipart handled by ParseMultipartForm" error, which means it was already parsed.
|
||||||
|
// This can happen if a middleware or previous ShouldBind call already parsed the form.
|
||||||
|
// Other errors (e.g., I/O errors during parsing) should be returned.
|
||||||
|
// Note: Gorilla schema might not need this if it directly uses r.Form or r.PostForm
|
||||||
|
// which are populated by ParseForm/ParseMultipartForm.
|
||||||
|
// For now, we ensure it's parsed. A more specific check might be needed if this causes issues.
|
||||||
|
// A common error to ignore here is `http.ErrNotMultipart` if the content type isn't multipart,
|
||||||
|
// as ParseMultipartForm expects that. ParseForm might be more general if we only expect
|
||||||
|
// x-www-form-urlencoded, but ParseMultipartForm handles both.
|
||||||
|
// Let's proceed and let schema decoder handle empty PostForm if parsing wasn't applicable.
|
||||||
|
// However, a direct "request body too large" from MaxBytesReader should have priority if it happened before.
|
||||||
|
// This specific error from ParseMultipartForm might relate to parts of a valid multipart form being too large for memory,
|
||||||
|
// not the overall request size.
|
||||||
|
// For simplicity in this step, we'll return the error unless it's a known "already parsed" scenario (which is not standard).
|
||||||
|
// A better approach for "already parsed" would be to check if c.Request.PostForm is already populated.
|
||||||
|
if c.formCache == nil && c.Request.PostForm == nil { // Attempt to parse only if not already cached or populated
|
||||||
|
if perr := c.Request.ParseMultipartForm(defaultMemory); perr != nil {
|
||||||
|
// http.ErrNotMultipart is returned if Content-Type is not multipart/form-data
|
||||||
|
// For x-www-form-urlencoded, ParseForm() is implicitly called by accessing PostForm
|
||||||
|
// Let's try to populate PostForm if it's not already
|
||||||
|
if c.Request.PostForm == nil {
|
||||||
|
if perr2 := c.Request.ParseForm(); perr2 != nil {
|
||||||
|
return fmt.Errorf("form parse error (ParseForm): %w", perr2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If it was not multipart and ParseForm also failed or PostForm is still nil,
|
||||||
|
// then we might have an issue. However, gorilla/schema works on `url.Values`
|
||||||
|
// which `c.Request.PostForm` provides.
|
||||||
|
// If `Content-Type` was not form-like, `PostForm` would be empty.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If `c.formCache` is not nil, `PostForm()` would have already tried parsing.
|
||||||
|
// We will use `c.Request.PostForm` which gets populated by `ParseMultipartForm` or `ParseForm`.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize schema decoder
|
||||||
|
decoder := schema.NewDecoder()
|
||||||
|
// decoder.IgnoreUnknownKeys(true) // Optional: if you want to ignore fields in form not in struct
|
||||||
|
|
||||||
|
// Get form values. c.Request.PostForm includes values from both
|
||||||
|
// application/x-www-form-urlencoded and multipart/form-data bodies.
|
||||||
|
// It needs to be called after ParseMultipartForm or ParseForm.
|
||||||
|
// Accessing c.Request.PostForm itself can trigger ParseForm if not already parsed and content type is x-www-form-urlencoded.
|
||||||
|
if err := c.Request.ParseForm(); err != nil && c.Request.PostForm == nil {
|
||||||
|
// If ParseForm itself errors and PostForm is still nil, then return error.
|
||||||
|
// This ensures that for x-www-form-urlencoded, parsing is attempted.
|
||||||
|
// ParseMultipartForm handles multipart, and its error is handled above.
|
||||||
|
return fmt.Errorf("form parse error (PostForm init): %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
values := c.Request.PostForm
|
||||||
|
if len(values) == 0 {
|
||||||
|
// If PostForm is empty, there's nothing to bind from the POST body.
|
||||||
|
// This is not necessarily an error for schema.Decode, it will just bind zero values.
|
||||||
|
// Depending on requirements, one might want to return an error here if binding is mandatory.
|
||||||
|
// For now, we let schema.Decode handle it (it will likely do nothing or bind zero values).
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode the form values into the object
|
||||||
|
if err := decoder.Decode(obj, values); err != nil {
|
||||||
|
return fmt.Errorf("form binding error using schema: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddError 添加一个错误到 Context
|
// AddError 添加一个错误到 Context
|
||||||
|
|
|
||||||
1385
context_test.go
1385
context_test.go
File diff suppressed because it is too large
Load diff
32
engine.go
32
engine.go
|
|
@ -9,15 +9,31 @@ import (
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"html/template"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path"
|
"path"
|
||||||
|
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/WJQSERVER-STUDIO/httpc"
|
"github.com/WJQSERVER-STUDIO/httpc"
|
||||||
"github.com/fenthope/reco"
|
"github.com/fenthope/reco"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// HTMLRender defines the interface for HTML rendering.
|
||||||
|
type HTMLRender interface {
|
||||||
|
Render(writer io.Writer, name string, data interface{}, c *Context) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultHTMLRenderer is a basic implementation of HTMLRender using html/template.
|
||||||
|
type DefaultHTMLRenderer struct {
|
||||||
|
Templates *template.Template
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render executes the template and writes to the writer.
|
||||||
|
func (r *DefaultHTMLRenderer) Render(writer io.Writer, name string, data interface{}, c *Context) error {
|
||||||
|
return r.Templates.ExecuteTemplate(writer, name, data)
|
||||||
|
}
|
||||||
|
|
||||||
// Last 返回链中的最后一个处理函数
|
// Last 返回链中的最后一个处理函数
|
||||||
// 如果链为空,则返回 nil
|
// 如果链为空,则返回 nil
|
||||||
func (c HandlersChain) Last() HandlerFunc {
|
func (c HandlersChain) Last() HandlerFunc {
|
||||||
|
|
@ -50,7 +66,7 @@ type Engine struct {
|
||||||
|
|
||||||
LogReco *reco.Logger
|
LogReco *reco.Logger
|
||||||
|
|
||||||
HTMLRender interface{} // 用于 HTML 模板渲染,可以设置为 *template.Template 或自定义渲染器接口
|
HTMLRender HTMLRender // 用于 HTML 模板渲染
|
||||||
|
|
||||||
routesInfo []RouteInfo // 存储所有注册的路由信息
|
routesInfo []RouteInfo // 存储所有注册的路由信息
|
||||||
|
|
||||||
|
|
@ -219,6 +235,18 @@ func Default() *Engine {
|
||||||
|
|
||||||
// === 外部操作方法 ===
|
// === 外部操作方法 ===
|
||||||
|
|
||||||
|
// LoadHTMLGlob loads HTML templates from a glob pattern and sets them as the HTML renderer.
|
||||||
|
func (engine *Engine) LoadHTMLGlob(pattern string) {
|
||||||
|
tpl := template.Must(template.ParseGlob(pattern))
|
||||||
|
engine.HTMLRender = &DefaultHTMLRenderer{Templates: tpl}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetHTMLTemplate sets a custom *template.Template as the HTML renderer.
|
||||||
|
// This will wrap the *template.Template with the DefaultHTMLRenderer.
|
||||||
|
func (engine *Engine) SetHTMLTemplate(tpl *template.Template) {
|
||||||
|
engine.HTMLRender = &DefaultHTMLRenderer{Templates: tpl}
|
||||||
|
}
|
||||||
|
|
||||||
// SetMaxRequestBodySize 设置读取Body的最大字节数
|
// SetMaxRequestBodySize 设置读取Body的最大字节数
|
||||||
func (engine *Engine) SetMaxRequestBodySize(size int64) {
|
func (engine *Engine) SetMaxRequestBodySize(size int64) {
|
||||||
engine.MaxRequestBodySize = size
|
engine.MaxRequestBodySize = size
|
||||||
|
|
|
||||||
5
go.mod
5
go.mod
|
|
@ -9,4 +9,7 @@ require (
|
||||||
github.com/go-json-experiment/json v0.0.0-20250517221953-25912455fbc8
|
github.com/go-json-experiment/json v0.0.0-20250517221953-25912455fbc8
|
||||||
)
|
)
|
||||||
|
|
||||||
require github.com/valyala/bytebufferpool v1.0.0 // indirect
|
require (
|
||||||
|
github.com/gorilla/schema v1.4.1 // indirect
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
|
)
|
||||||
|
|
|
||||||
2
go.sum
2
go.sum
|
|
@ -6,5 +6,7 @@ github.com/fenthope/reco v0.0.3 h1:RmnQ0D9a8PWtwOODawitTe4BztTnS9wYwrDbipISNq4=
|
||||||
github.com/fenthope/reco v0.0.3/go.mod h1:mDkGLHte5udWTIcjQTxrABRcf56SSdxBOCLgrRDwI/Y=
|
github.com/fenthope/reco v0.0.3/go.mod h1:mDkGLHte5udWTIcjQTxrABRcf56SSdxBOCLgrRDwI/Y=
|
||||||
github.com/go-json-experiment/json v0.0.0-20250517221953-25912455fbc8 h1:o8UqXPI6SVwQt04RGsqKp3qqmbOfTNMqDrWsc4O47kk=
|
github.com/go-json-experiment/json v0.0.0-20250517221953-25912455fbc8 h1:o8UqXPI6SVwQt04RGsqKp3qqmbOfTNMqDrWsc4O47kk=
|
||||||
github.com/go-json-experiment/json v0.0.0-20250517221953-25912455fbc8/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M=
|
github.com/go-json-experiment/json v0.0.0-20250517221953-25912455fbc8/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M=
|
||||||
|
github.com/gorilla/schema v1.4.1 h1:jUg5hUjCSDZpNGLuXQOgIWGdlgrIdYvgQ0wZtdK1M3E=
|
||||||
|
github.com/gorilla/schema v1.4.1/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM=
|
||||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue