加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
文件
克隆/下载
framework.go 21.55 KB
一键复制 编辑 原始数据 按行查看 历史
henrylee2cn 提交于 2018-07-21 13:05 . improve code
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715
// Copyright 2016 HenryLee. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package faygo
import (
"bytes"
"context"
"fmt"
"net/http"
"runtime"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/henrylee2cn/faygo/logging"
"github.com/henrylee2cn/faygo/logging/color"
"github.com/henrylee2cn/faygo/session"
"github.com/henrylee2cn/faygo/swagger"
)
// Framework is the faygo web framework.
type Framework struct {
// name of the application
name string
// version of the application
version string
config Config
// root muxAPI node
*MuxAPI
muxesForRouter MuxAPIs
// called before the route is matched
filter HandlerChain
servers []*Server
running bool
buildOnce sync.Once
lock sync.RWMutex
sessionManager *session.Manager
// for framework
syslog *logging.Logger
// for user bissness
bizlog *logging.Logger
apidoc *swagger.Swagger
dynamicSrcTree map[string]*node // dynamic resource router tree
staticSrcTree map[string]*node // dynamic resource router tree
// Redirect from 'http://hostname:port1' to 'https://hostname:port2'
httpRedirectHttps bool
// One of the https ports to be listened
httpsPort string
// Enables automatic redirection if the current route can't be matched but a
// handler for the path with (without) the trailing slash exists.
// For example if /foo/ is requested but a route only exists for /foo, the
// client is redirected to /foo with http status code 301 for GET requests
// and 307 for all other request methods.
redirectTrailingSlash bool
// If enabled, the router tries to fix the current request path, if no
// handle is registered for it.
// First superfluous path elements like ../ or // are removed.
// Afterwards the router does a case-insensitive lookup of the cleaned path.
// If a handle can be found for this route, the router makes a redirection
// to the corrected path with status code 301 for GET requests and 307 for
// all other request methods.
// For example /FOO and /..//Foo could be redirected to /foo.
// redirectTrailingSlash is independent of this option.
redirectFixedPath bool
// If enabled, the router checks if another method is allowed for the
// current route, if the current request can not be routed.
// If this is the case, the request is answered with 'Method Not Allowed'
// and HTTP status code 405.
// If no other Method is allowed, the request is delegated to the NotFound
// handler.
handleMethodNotAllowed bool
// If enabled, the router automatically replies to OPTIONS requests.
// Custom OPTIONS handlers take priority over automatic replies.
handleOPTIONS bool
contextPool sync.Pool
}
// Make sure the Framework conforms with the http.Handler interface
var _ http.Handler = new(Framework)
// newFramework uses the faygo web framework to create a new application.
func newFramework(config *Config, name string, version []string) *Framework {
mutexNewApp.Lock()
defer mutexNewApp.Unlock()
var frame = new(Framework)
frame.name = strings.TrimSpace(name)
if len(version) > 0 && len(version[0]) > 0 {
frame.version = strings.TrimSpace(version[0])
}
id := frame.NameWithVersion()
if _, ok := GetFrame(id); ok {
Fatalf("There are two applications with exactly the same name and version: %s", id)
}
if config == nil {
config = newConfigFromFileAndCheck(frame.ConfigFilename())
} else {
config.check()
}
frame.setConfig(config)
frame.redirectTrailingSlash = frame.config.Router.RedirectTrailingSlash
frame.redirectFixedPath = frame.config.Router.RedirectFixedPath
frame.handleMethodNotAllowed = frame.config.Router.HandleMethodNotAllowed
frame.handleOPTIONS = frame.config.Router.HandleOPTIONS
frame.contextPool = sync.Pool{
New: func() interface{} {
ctx := &Context{
frame: frame,
enableGzip: global.config.Gzip.Enable,
enableSession: frame.config.Session.Enable,
enableXSRF: frame.config.XSRF.Enable,
}
ctx.W = &Response{context: ctx}
return ctx
},
}
frame.initSysLogger()
frame.initBizLogger()
frame.MuxAPI = newMuxAPI(frame, "root", "", "/")
addFrame(frame)
return frame
}
var (
mutexNewApp sync.Mutex
mutexForBuild sync.Mutex
)
func (frame *Framework) setConfig(config *Config) {
frame.config = *config
}
// Name returns the name of the application
func (frame *Framework) Name() string {
return frame.name
}
// Version returns the version of the application
func (frame *Framework) Version() string {
return frame.version
}
// NameWithVersion returns the name with version
func (frame *Framework) NameWithVersion() string {
if len(frame.version) == 0 {
return frame.name
}
return frame.name + "_" + frame.version
}
// Config returns the framework's config copy.
func (frame *Framework) Config() Config {
return frame.config
}
// ConfigFilename returns the framework's config file name.
func (frame *Framework) ConfigFilename() string {
return configDir + "/" + frame.NameWithVersion() + ".ini"
}
// Run starts the web service.
func (frame *Framework) Run() {
if frame.Running() {
return
}
go frame.run()
global.graceOnce.Do(func() {
graceSignal()
})
select {}
}
// Running returns whether the frame service is running.
func (frame *Framework) Running() bool {
frame.lock.RLock()
defer frame.lock.RUnlock()
return frame.running
}
func (frame *Framework) run() {
frame.lock.Lock()
frame.build()
frame.running = true
count := len(frame.servers)
for i := 0; i < count; i++ {
go frame.servers[i].run()
}
frame.lock.Unlock()
}
func (frame *Framework) build() {
frame.buildOnce.Do(func() {
// Make sure that the initialization logs for multiple applications are printed in sequence
mutexForBuild.Lock()
defer mutexForBuild.Unlock()
// register the default MuxAPIs
{
// apidoc
if frame.config.APIdoc.Enable {
frame.regAPIdoc()
}
// static
frame.presetSystemMuxes()
}
// register router
if frame.dynamicSrcTree == nil {
frame.dynamicSrcTree = make(map[string]*node)
}
if frame.staticSrcTree == nil {
frame.staticSrcTree = make(map[string]*node)
}
for _, api := range frame.MuxAPIsForRouter() {
handle := frame.makeHandle(api.handlers)
for _, method := range api.methods {
if api.path[0] != '/' {
Panic("path must begin with '/' in path '" + api.path + "'")
}
var root *node
if strings.HasSuffix(api.path, "/*"+FilepathKey) &&
api.path != "/apidoc/*"+FilepathKey &&
api.path != "/upload/*"+FilepathKey &&
api.path != "/static/*"+FilepathKey {
// custom static
root = frame.staticSrcTree[method]
if root == nil {
root = new(node)
frame.staticSrcTree[method] = root
}
} else {
// dynamic or default static
root = frame.dynamicSrcTree[method]
if root == nil {
root = new(node)
frame.dynamicSrcTree[method] = root
}
}
root.addRoute(api.path, handle)
frame.syslog.Criticalf("\x1b[46m[SYS]\x1b[0m %7s | %-30s", method, api.path)
}
}
// new server
nameWithVersion := frame.NameWithVersion()
for i, netType := range frame.config.NetTypes {
srv := &Server{
nameWithVersion: nameWithVersion,
netType: netType,
tlsCertFile: frame.config.TLSCertFile,
tlsKeyFile: frame.config.TLSKeyFile,
letsencryptDir: frame.config.LetsencryptDir,
unixFileMode: frame.config.unixFileMode,
Server: &http.Server{
Addr: frame.config.Addrs[i],
Handler: frame,
ReadTimeout: frame.config.ReadTimeout,
WriteTimeout: frame.config.WriteTimeout,
},
log: frame.syslog,
}
if frame.config.HttpRedirectHttps && srv.isHttps() {
frame.httpRedirectHttps = true
frame.httpsPort = srv.port()
}
frame.servers = append(frame.servers, srv)
}
// register session
frame.registerSession()
})
}
// shutdown closes the frame service gracefully.
func (frame *Framework) shutdown(ctxTimeout context.Context) (graceful bool) {
frame.lock.Lock()
defer frame.lock.Unlock()
if !frame.running {
return true
}
var flag int32 = 1
count := new(sync.WaitGroup)
for _, server := range frame.servers {
count.Add(1)
go func(srv *Server) {
if err := srv.Shutdown(ctxTimeout); err != nil {
atomic.StoreInt32(&flag, 0)
frame.Log().Errorf("[shutdown-%s] %s", frame.NameWithVersion(), err.Error())
}
count.Done()
}(server)
}
count.Wait()
frame.running = false
frame.CloseLog()
return flag == 1
}
// Log returns the logger used by the user bissness.
func (frame *Framework) Log() *logging.Logger {
return frame.bizlog
}
// CloseLog closes loggers.
func (frame *Framework) CloseLog() {
frame.bizlog.Close()
frame.syslog.Close()
}
// MuxAPIsForRouter get an ordered list of nodes used to register router.
func (frame *Framework) MuxAPIsForRouter() []*MuxAPI {
if frame.muxesForRouter == nil {
// comb mux.handlers, mux.paramInfos, mux.returns and mux.path,.
frame.MuxAPI.comb()
frame.muxesForRouter = frame.MuxAPI.HandlerProgeny()
}
return frame.muxesForRouter
}
// Filter operations that are called before the route is matched.
func (frame *Framework) Filter(fn ...HandlerFunc) *Framework {
handlers := make([]Handler, len(fn))
for i, h := range fn {
handlers[i] = h
}
frame.filter = append(handlers, frame.filter...)
return frame
}
// Route append middlewares of function type to root muxAPI.
// Used to register router in tree style.
func (frame *Framework) Route(children ...*MuxAPI) *MuxAPI {
frame.MuxAPI.children = append(frame.MuxAPI.children, children...)
for _, child := range children {
child.parent = frame.MuxAPI
}
return frame.MuxAPI
}
// NewGroup create an isolated grouping muxAPI node.
func (frame *Framework) NewGroup(pattern string, children ...*MuxAPI) *MuxAPI {
return frame.NewNamedGroup("", pattern, children...)
}
// NewAPI creates an isolated muxAPI node.
func (frame *Framework) NewAPI(methodset Methodset, pattern string, handlers ...Handler) *MuxAPI {
return frame.NewNamedAPI("", methodset, pattern, handlers...)
}
// NewNamedGroup creates an isolated grouping muxAPI node with the name.
func (frame *Framework) NewNamedGroup(name string, pattern string, children ...*MuxAPI) *MuxAPI {
group := frame.NewNamedAPI(name, "", pattern)
group.children = append(group.children, children...)
for _, child := range children {
child.parent = group
}
return group
}
// NewNamedAPI creates an isolated muxAPI node with the name.
func (frame *Framework) NewNamedAPI(name string, methodset Methodset, pattern string, handlers ...Handler) *MuxAPI {
return newMuxAPI(frame, name, methodset, pattern, handlers...)
}
// NewGET is a shortcut for frame.NewAPI("GET", pattern, handlers...)
func (frame *Framework) NewGET(pattern string, handlers ...Handler) *MuxAPI {
return frame.NewAPI("GET", pattern, handlers...)
}
// NewHEAD is a shortcut for frame.NewAPI("HEAD", pattern, handlers...)
func (frame *Framework) NewHEAD(pattern string, handlers ...Handler) *MuxAPI {
return frame.NewAPI("HEAD", pattern, handlers...)
}
// NewOPTIONS is a shortcut for frame.NewAPI("OPTIONS", pattern, handlers...)
func (frame *Framework) NewOPTIONS(pattern string, handlers ...Handler) *MuxAPI {
return frame.NewAPI("OPTIONS", pattern, handlers...)
}
// NewPOST is a shortcut for frame.NewAPI("POST", pattern, handlers...)
func (frame *Framework) NewPOST(pattern string, handlers ...Handler) *MuxAPI {
return frame.NewAPI("POST", pattern, handlers...)
}
// NewPUT is a shortcut for frame.NewAPI("PUT", pattern, handlers...)
func (frame *Framework) NewPUT(pattern string, handlers ...Handler) *MuxAPI {
return frame.NewAPI("PUT", pattern, handlers...)
}
// NewPATCH is a shortcut for frame.NewAPI("PATCH", pattern, handlers...)
func (frame *Framework) NewPATCH(pattern string, handlers ...Handler) *MuxAPI {
return frame.NewAPI("PATCH", pattern, handlers...)
}
// NewDELETE is a shortcut for frame.NewAPI("DELETE", pattern, handlers...)
func (frame *Framework) NewDELETE(pattern string, handlers ...Handler) *MuxAPI {
return frame.NewAPI("DELETE", pattern, handlers...)
}
// NewNamedGET is a shortcut for frame.NewNamedAPI(name, "GET", pattern, handlers...)
func (frame *Framework) NewNamedGET(name string, pattern string, handlers ...Handler) *MuxAPI {
return frame.NewNamedAPI(name, "GET", pattern, handlers...)
}
// NewNamedHEAD is a shortcut for frame.NewNamedAPI(name, "HEAD", pattern, handlers...)
func (frame *Framework) NewNamedHEAD(name string, pattern string, handlers ...Handler) *MuxAPI {
return frame.NewNamedAPI(name, "HEAD", pattern, handlers...)
}
// NewNamedOPTIONS is a shortcut for frame.NewNamedAPI(name, "OPTIONS", pattern, handlers...)
func (frame *Framework) NewNamedOPTIONS(name string, pattern string, handlers ...Handler) *MuxAPI {
return frame.NewNamedAPI(name, "OPTIONS", pattern, handlers...)
}
// NewNamedPOST is a shortcut for frame.NewNamedAPI(name, "POST", pattern, handlers...)
func (frame *Framework) NewNamedPOST(name string, pattern string, handlers ...Handler) *MuxAPI {
return frame.NewNamedAPI(name, "POST", pattern, handlers...)
}
// NewNamedPUT is a shortcut for frame.NewNamedAPI(name, "PUT", pattern, handlers...)
func (frame *Framework) NewNamedPUT(name string, pattern string, handlers ...Handler) *MuxAPI {
return frame.NewNamedAPI(name, "PUT", pattern, handlers...)
}
// NewNamedPATCH is a shortcut for frame.NewNamedAPI(name, "PATCH", pattern, handlers...)
func (frame *Framework) NewNamedPATCH(name string, pattern string, handlers ...Handler) *MuxAPI {
return frame.NewNamedAPI(name, "PATCH", pattern, handlers...)
}
// NewNamedDELETE is a shortcut for frame.NewNamedAPI(name, "DELETE", pattern, handlers...)
func (frame *Framework) NewNamedDELETE(name string, pattern string, handlers ...Handler) *MuxAPI {
return frame.NewNamedAPI(name, "DELETE", pattern, handlers...)
}
// NewStatic creates an isolated static muxAPI node.
func (frame *Framework) NewStatic(pattern string, root string, nocompressAndNocache ...bool) *MuxAPI {
return frame.NewNamedStatic("", pattern, root, nocompressAndNocache...)
}
// NewNamedStatic creates an isolated static muxAPI node with the name.
func (frame *Framework) NewNamedStatic(name, pattern string, root string, nocompressAndNocache ...bool) *MuxAPI {
return (&MuxAPI{frame: frame}).NamedStatic(name, pattern, root, nocompressAndNocache...)
}
// NewStaticFS creates an isolated static muxAPI node.
func (frame *Framework) NewStaticFS(pattern string, fs FileSystem) *MuxAPI {
return frame.NewNamedStaticFS("", pattern, fs)
}
// NewNamedStaticFS creates an isolated static muxAPI node with the name.
func (frame *Framework) NewNamedStaticFS(name, pattern string, fs FileSystem) *MuxAPI {
return (&MuxAPI{frame: frame}).NamedStaticFS(name, pattern, fs)
}
func (frame *Framework) presetSystemMuxes() {
var hadUpload, hadStatic bool
for _, child := range frame.MuxAPI.children {
if strings.Contains(child.pattern, "/upload/") {
hadUpload = true
}
if strings.Contains(child.pattern, "/static/") {
hadStatic = true
}
}
// When does not have a custom route, the route is automatically created.
if !hadUpload && frame.config.Router.DefaultUpload {
frame.MuxAPI.NamedStatic(
"Directory for uploading files",
"/upload/",
global.upload.root,
global.upload.nocompress,
global.upload.nocache,
).Use(global.upload.handlers...)
}
if !hadStatic && frame.config.Router.DefaultStatic {
frame.MuxAPI.NamedStatic(
"Directory for public static files",
"/static/",
global.static.root,
global.static.nocompress,
global.static.nocache,
).Use(global.static.handlers...)
}
}
func (frame *Framework) registerSession() {
if !frame.config.Session.Enable {
return
}
conf := &session.ManagerConfig{
CookieName: frame.config.Session.Name,
EnableSetCookie: frame.config.Session.AutoSetCookie,
CookieLifeTime: frame.config.Session.CookieLifeSecond,
Gclifetime: frame.config.Session.GcLifeSecond,
Maxlifetime: frame.config.Session.MaxLifeSecond,
Secure: true,
ProviderConfig: frame.config.Session.ProviderConfig,
Domain: frame.config.Session.Domain,
EnableSidInHttpHeader: frame.config.Session.EnableSidInHttpHeader,
SessionNameInHttpHeader: frame.config.Session.NameInHttpHeader,
EnableSidInUrlQuery: frame.config.Session.EnableSidInUrlQuery,
}
var err error
frame.sessionManager, err = session.NewManager(frame.config.Session.Provider, conf)
if err != nil {
panic(err)
}
go frame.sessionManager.GC()
}
// ServeHTTP makes the router implement the http.Handler interface.
func (frame *Framework) ServeHTTP(w http.ResponseWriter, req *http.Request) {
var start = time.Now()
var ctx = frame.getContext(w, req)
defer func() {
if rcv := recover(); rcv != nil {
panicHandler(ctx, rcv)
}
frame.putContext(ctx)
}()
var method = ctx.Method()
var u = ctx.URI()
if u == "" {
u = "/"
}
frame.serveHTTP(ctx)
var n = ctx.Status()
var code string
switch {
case n >= 500:
code = color.Red(n)
case n >= 400:
code = color.Magenta(n)
case n >= 300:
code = color.Grey(n)
default:
code = color.Green(n)
}
cost := time.Since(start)
if cost < frame.config.slowResponseThreshold {
frame.syslog.Infof("[I] %15s %7s %3s %10d %12s %-30s | %s", ctx.RealIP(), method, code, ctx.Size(), cost, u, ctx.recordBody())
} else {
frame.syslog.Warningf(color.Yellow("[W]")+" %15s %7s %3s %10d %12s(slow) %-30s | %s", ctx.RealIP(), method, code, ctx.Size(), cost, u, ctx.recordBody())
}
}
func (frame *Framework) serveHTTP(ctx *Context) {
if frame.httpRedirectHttps && !ctx.IsSecure() {
u := ctx.URL()
u.Scheme = "https"
u.Host = ctx.Domain() + ":" + frame.httpsPort
http.Redirect(ctx.W, ctx.R, u.String(), 307)
return
}
if !ctx.doFilter() {
return
}
var path = ctx.Path()
var method = ctx.Method()
// find dynamic resource or default static resource
if frame.tryHandle(ctx, path, method, frame.dynamicSrcTree) {
return
}
// find custom static resource
if frame.tryHandle(ctx, path, method, frame.staticSrcTree) {
return
}
// Handle 404
global.errorFunc(ctx, "Not Found", 404)
}
func (frame *Framework) tryHandle(ctx *Context, path, method string, tree map[string]*node) bool {
if root := tree[method]; root != nil {
if handle, ps, tsr := root.getValue(path); handle != nil {
handle(ctx, ps)
return true
} else if method != "CONNECT" && path != "/" {
code := 301 // Permanent redirect, request with GET method
if method != "GET" {
// Temporary redirect, request with same method
// As of Go 1.3, Go does not support status code 308.
code = 307
}
if tsr && frame.redirectTrailingSlash {
if len(path) > 1 && path[len(path)-1] == '/' {
ctx.ModifyPath(path[:len(path)-1])
} else {
ctx.ModifyPath(path + "/")
}
http.Redirect(ctx.W, ctx.R, ctx.URL().String(), code)
return true
}
// Try to fix the request path
if frame.redirectFixedPath {
fixedPath, found := root.findCaseInsensitivePath(
CleanToURL(path),
frame.redirectTrailingSlash,
)
if found {
ctx.ModifyPath(BytesToString(fixedPath))
http.Redirect(ctx.W, ctx.R, ctx.URL().String(), code)
return true
}
}
}
}
if method == "OPTIONS" {
// Handle OPTIONS requests
if frame.handleOPTIONS {
if allow := frame.allowed(path, method); len(allow) > 0 {
ctx.SetHeader("Allow", allow)
ctx.W.WriteHeader(204)
return true
}
}
} else {
// Handle 405
if frame.handleMethodNotAllowed {
if allow := frame.allowed(path, method); len(allow) > 0 {
ctx.SetHeader("Allow", allow)
global.errorFunc(ctx, "Method Not Allowed", 405)
return true
}
}
}
return false
}
func (frame *Framework) allowed(path, reqMethod string) (allow string) {
if path == "*" { // server-wide
for method := range frame.dynamicSrcTree {
if method == "OPTIONS" {
continue
}
// add request method to list of allowed methods
if len(allow) == 0 {
allow = method
} else {
allow += ", " + method
}
}
} else { // specific path
for method := range frame.dynamicSrcTree {
// Skip the requested method - we already tried this one
if method == reqMethod || method == "OPTIONS" {
continue
}
handle, _, _ := frame.dynamicSrcTree[method].getValue(path)
if handle != nil {
// add request method to list of allowed methods
if len(allow) == 0 {
allow = method
} else {
allow += ", " + method
}
}
}
}
if len(allow) > 0 {
allow += ", OPTIONS"
}
return
}
// makeHandle makes an *apiware.ParamsAPI implements the Handle interface.
func (frame *Framework) makeHandle(handlerChain HandlerChain) Handle {
return func(ctx *Context, pathParams PathParams) {
ctx.doHandler(handlerChain, pathParams)
}
}
func panicHandler(ctx *Context, rcv interface{}) {
s := []byte("/src/runtime/panic.go")
e := []byte("\ngoroutine ")
line := []byte("\n")
stack := make([]byte, 4<<10) //4KB
length := runtime.Stack(stack, true)
start := bytes.Index(stack, s)
stack = stack[start:length]
start = bytes.Index(stack, line) + 1
stack = stack[start:]
end := bytes.LastIndex(stack, line)
if end != -1 {
stack = stack[:end]
}
end = bytes.Index(stack, e)
if end != -1 {
stack = stack[:end]
}
stack = bytes.TrimRight(stack, "\n")
global.errorFunc(ctx, fmt.Sprintf("%v\n[TRACE]\n%s\n", rcv, stack), http.StatusInternalServerError)
}
Loading...
马建仓 AI 助手
尝试更多
代码解读
代码找茬
代码优化