Go 源碼學習 - http庫:淺析一次 http 請求的處理過程

出發

Go 的 web 編程非常簡潔。python 寫 web app,比如 flask 或者 django,都要使用 uwsgi 或者 gunicorn 這種 web server 來提供生產級服務,web server 可以啓動多進程來利用多核 CPU 實現更高的併發處理能力。Go 天生的 goroutine 實現了用戶級線程,不僅更快而且可以充分利用多核 CPU,併發性能非常好,編譯成二進制文件以後直接啓動就可以提供生產可用的服務了,非常簡潔。

web app 必要構件

最簡單的 web app 只需要如下幾個組成部分:

  • 請求處理函數
func test(w http.ResponseWriter, _ *http.Request){
	fmt.Fprintf(w, "test\n")
}
  • 註冊路由
http.HandleFunc("/test", test)
  • 啓動服務器
log.Fatal(http.ListenAndServe(":5000", nil))

整體流程

在這裏插入圖片描述

閱讀源碼

下面我從服務啓動函數讀起,逐步瞭解一次 http 請求的處理過程。

1 http.ListenAndServe

func ListenAndServe(addr string, handler Handler) error {
	server := &Server{Addr: addr, Handler: handler}
	return server.ListenAndServe()
}

該函數監聽 TCP 地址然後調用處理器(handler)處理到來的請求,典型的 handler 爲 nil,這時使用默認的路由器來路由請求。ListenAndServe 只會返回非 nil 的錯誤。

這個函數的主要作用是初始化一個服務器結構體(Server)的實例,爲它的Handler屬性賦值。

看一下 Server 結構體,它定義了運行一個 http 服務器所需的參數,這個結構體的零值(默認值)就是一個可用的配置。

type Server struct {
	Addr    string  // 監聽的 TCP 地址,爲空的話默認爲 ":http",也就是80端口
	Handler Handler // 處理器,如果爲 nil 的話使用默認的路由器 http.DefaultServeMux

	// TLSConfig 是可選項,配置 TLS
	TLSConfig *tls.Config

	// 讀取完整請求的超時時長,包括請求體
	ReadTimeout time.Duration

	// 允許讀取請求頭的時長。
	ReadHeaderTimeout time.Duration

	// 寫回響應的超時時長
	WriteTimeout time.Duration

	// 啓用 keep-alives 時,一個長連接的兩個請求之間間隔的最大時長
	IdleTimeout time.Duration

	// 允許的請求頭的最大字節數,不限制請求體
	MaxHeaderBytes int

	// TLS 相關的配置
	TLSNextProto map[string]func(*Server, *tls.Conn, Handler)

	// 當客戶端連接狀態改變時調用的回調函數,可選
	ConnState func(net.Conn, ConnState)

	// 可選的自定義 logger,不設就是標準日誌庫的 logger
	ErrorLog *log.Logger

	// 爲來到此服務器的請求指定基 context ,不設就是 context.Background()
	BaseContext func(net.Listener) context.Context

	// 可選,通常指定一個函數,用於修改新連接的 context
	ConnContext func(ctx context.Context, c net.Conn) context.Context

	disableKeepAlives int32     // 關閉 keep-alives
	inShutdown        int32     // 非0表示正在關閉
	nextProtoOnce     sync.Once // http2相關
	nextProtoErr      error     // http2相關

	mu         sync.Mutex
	listeners  map[*net.Listener]struct{}
	activeConn map[*conn]struct{}
	doneChan   chan struct{}
	onShutdown []func()
}

2 Server.ListenAndServe

接下來是 Server 的 ListenAndServe 方法,它監聽 TCP 網絡地址 srv.Addr 然後調用 Serve 函數處理到達連接的請求。支持 TCP keep-alives。該函數返回非空的 error。

func (srv *Server) ListenAndServe() error {
    // 如果服務關閉了,返回的錯誤爲 ErrServerClosed。
	if srv.shuttingDown() {
		return ErrServerClosed
	}
	addr := srv.Addr
	// 如果結構體的 Addr 屬性爲空就設置
	// 默認值 ":http"
	if addr == "" {
		addr = ":http"
	}
	// 調用 net 的 Listen 方法,得到一個 TCPListener
	ln, err := net.Listen("tcp", addr)
	if err != nil {
		return err
	}
	返回 srv 的 Serve 方法,上面得到的 TCPListener作爲參數
	return srv.Serve(ln)
}

net.Listen 方法監聽本地的網絡地址,network參數可以是 tcp、tcp4、tcp6、 unix 或者 unixpacket。address 參數可以用 hostname,但是不建議,因爲這樣創建的 listener(監聽器)最多監聽主機的一個IP地址。如果 address 參數的 port 爲空或者“0”,如“127.0.0.1:” 或者 “[::1]:0”,將自動選擇一個端口號。
這個函數的主要作用就是初始化一個net包的 TCPListener 結構體的實例,然後調用服務器的Serve方法爲這個連接提供服務。

3 Server.Server

Server 結構體的 Serve 方法。它爲 listerner 上每個到達的連接創建一個新的服務協程(goroutine)。服務協程讀取request(請求)然後調用 srv.Handler 方法做出迴應。

Serve 方法返回非空的error並關閉listener。
這個方法運行一個死循環,等待底層tcp或者tls連接到來的請求數據,沒有請求數據就休眠一段時間,數據到來以後把服務器實例(Server)和底層連接(net.Conn)封裝爲一個內部的conn結構體的實例,執行它的serve方法。

func (srv *Server) Serve(l net.Listener) error {
    // 如果有一個包裹了 srv 和 listener 的鉤子函數,就執行它
	if fn := testHookServerServe; fn != nil {
		fn(srv, l) // call hook with unwrapped listener
	}

    // 把 listener 賦值給 origListener 變量,然後將原來的 linstner 
    // 作爲 Listener 屬性初始化一個onceCloseListener結構體的實例,
    // 顧名思義,只關閉一次的監聽器,這個包裹是爲了防止有多個 Close 調用。
	origListener := l
	l = &onceCloseListener{Listener: l}
	defer l.Close()

    // srv.setupHTTP2_Serve 方法有條件的配置 srv 的 HTTP/2。
	if err := srv.setupHTTP2_Serve(); err != nil {
		return err
	}

    // srv.trackListener 向跟蹤的監聽器集(srv.listeners)中添加一個監聽器(l),
    // 並保證它最終會刪除這個監聽器
	if !srv.trackListener(&l, true) {
		return ErrServerClosed
	}
	defer srv.trackListener(&l, false)

	var tempDelay time.Duration // 如果沒有接收到請求的話,休眠多久

    // 定義基礎context
	baseCtx := context.Background()
	if srv.BaseContext != nil {
		baseCtx = srv.BaseContext(origListener)
		if baseCtx == nil {
			panic("BaseContext returned a nil context")
		}
	}

    // 爲ctx的ServerContextKey賦值srv
	ctx := context.WithValue(baseCtx, ServerContextKey, srv)
	// 死循環,即伺服
	for {
	// rw 是net包裏的Conn接口,他是一個通用的面向流的網絡連接。
		rw, e := l.Accept()
		// 如果監聽器接收報錯
		if e != nil {
			select {
			// 如果 srv.getDoneChan 中有內容,返回 ErrServerClosed
			case <-srv.getDoneChan():
				return ErrServerClosed
			default:
			}
			// 如果 e 是 net.Error, 並且錯誤是臨時性的
			if ne, ok := e.(net.Error); ok && ne.Temporary() {
			    // 第一次沒接收到數據,睡眠5毫秒
				if tempDelay == 0 {
					tempDelay = 5 * time.Millisecond
				// 每次隨眠結束,喚醒後還是沒接收到數據,睡眠時間加倍
				} else {
					tempDelay *= 2
				}
				// 單次睡眠時間上限設定爲1秒
				if max := 1 * time.Second; tempDelay > max {
					tempDelay = max
				}
				// 記錄接收數據失敗日誌,休眠 tempDelay
				srv.logf("http: Accept error: %v; retrying in %v", e, tempDelay)
				time.Sleep(tempDelay)
				continue
			}
			// 如果e不是net.Error,或者不是臨時性錯誤,就返回錯誤
			return e
		}
		
		// 如果接收到數據就做如下處理:
		// 如果指定了srv的ConnContext,就用它修改連接的context
		connCtx := ctx
		if cc := srv.ConnContext; cc != nil {
			connCtx = cc(connCtx, rw)
			if connCtx == nil {
				panic("ConnContext returned nil")
			}
		}
		// 休眠定時器歸零
		tempDelay = 0
		// 使用當前的Conn接口構建新的 conn 實例,它包含了srv服務器和rw連接。
		// conn 實例代表一個 HTTP 連接的服務端,rw是底層的網絡連接
		c := srv.newConn(rw)
		// 設置底層連接的狀態
	    // 第一次調用的時候初始化srv.activeConn爲一個map[*conn]struct{}
	    // srv.activeConn是一個集合,保存服務器的活躍連接
	    // setState方法將當前連接加入集合跟蹤狀態
		c.setState(c.rwc, StateNew) // before Serve can return
		// 啓動一個goroutine處理請求
		go c.serve(connCtx)
	}
}

看一下這個server.go 內的conn結構體

// conn 代表 HTTP 連接的服務側.
type conn struct {
	// 連接到達的服務器。
	// 不可變,不能爲空。
	server *Server

	// 取消連接層面的context的取消函數
	cancelCtx context.CancelFunc

	// rwc 是底層網絡連接。
	// 它不能被其他類型包裹,通常是 *net.TCPConn 或 *tls.Conn 類型。
	rwc net.Conn

	// remoteAddr 就是 rwc.RemoteAddr().String()。它不在 Listener 接收數據的 goroutine,裏同步填充。
	// 它在 (*conn).serve goroutine裏被立即填充
	// 這是 Handler 的 (*Request).RemoteAddr 的值。
	remoteAddr string

	// tlsState 是使用 TLS 時 TLS 的連接狀態。
	// nil 表示不用 TLS。
	tlsState *tls.ConnectionState

	// werr is set to the first write error to rwc.
	// 通過 checkConnErrorWriter{w} 設置,bufw 寫入。
	werr error

	// r 是 bufr 的讀入源。它是 rwc 的一個包裹器,提供io.LimitedReader
	// 式的限制 (在讀取請求頭的時候)
	// 並支持 CloseNotifier 的功能。詳閱 *connReader 的文檔。
	r *connReader

	// bufr 從 r 讀取。
	bufr *bufio.Reader

	// bufw 寫入 checkConnErrorWriter{c}, 當發生錯誤時填充 werr。
	bufw *bufio.Writer

	// lastMethod 是當前連接的最後一個請求的方法。
	lastMethod string

	curReq atomic.Value // of *response (which has a Request in it)

	curState struct{ atomic uint64 } // packed (unixtime<<8|uint8(ConnState))

	// mu 保護 hijackedv
	mu sync.Mutex

	// hijackedv 表示這個連接是否被一個帶有Hijacker接口的 Handler hijacked了。
	hijackedv bool
}

4 *conn.Serve

下面看看goroutine處理http(s) 請求的過程:

調用上面srv.newConn(rw)方法創建的conn實例的serve方法服務一個新的連接

func (c *conn) serve(ctx context.Context) {
    // 設置遠端地址和本端地址
	c.remoteAddr = c.rwc.RemoteAddr().String()
	ctx = context.WithValue(ctx, LocalAddrContextKey, c.rwc.LocalAddr())
    // 錯誤處理
	defer func() {
		if err := recover(); err != nil && err != ErrAbortHandler {
			const size = 64 << 10
			buf := make([]byte, size)
			buf = buf[:runtime.Stack(buf, false)]
			c.server.logf("http: panic serving %v: %v\n%s", c.remoteAddr, err, buf)
		}
		if !c.hijacked() {
			c.close()
			c.setState(c.rwc, StateClosed)
		}
	}()

    // https相關
	if tlsConn, ok := c.rwc.(*tls.Conn); ok {
		if d := c.server.ReadTimeout; d != 0 {
			c.rwc.SetReadDeadline(time.Now().Add(d))
		}
		if d := c.server.WriteTimeout; d != 0 {
			c.rwc.SetWriteDeadline(time.Now().Add(d))
		}
		if err := tlsConn.Handshake(); err != nil {
			// If the handshake failed due to the client not speaking
			// TLS, assume they're speaking plaintext HTTP and write a
			// 400 response on the TLS conn's underlying net.Conn.
			if re, ok := err.(tls.RecordHeaderError); ok && re.Conn != nil && tlsRecordHeaderLooksLikeHTTP(re.RecordHeader) {
				io.WriteString(re.Conn, "HTTP/1.0 400 Bad Request\r\n\r\nClient sent an HTTP request to an HTTPS server.\n")
				re.Conn.Close()
				return
			}
			c.server.logf("http: TLS handshake error from %s: %v", c.rwc.RemoteAddr(), err)
			return
		}
		c.tlsState = new(tls.ConnectionState)
		*c.tlsState = tlsConn.ConnectionState()
		if proto := c.tlsState.NegotiatedProtocol; validNPN(proto) {
			if fn := c.server.TLSNextProto[proto]; fn != nil {
				h := initNPNRequest{ctx, tlsConn, serverHandler{c.server}}
				fn(c.server, tlsConn, h)
			}
			return
		}
	}

	// HTTP/1.x 相關從這裏開始
    // 確保執行cancelCtx
	ctx, cancelCtx := context.WithCancel(ctx)
	c.cancelCtx = cancelCtx
	defer cancelCtx()

    // connReader 是一個包裹了 *conn的 io.Reader。
	c.r = &connReader{conn: c}
	c.bufr = newBufioReader(c.r)
	c.bufw = newBufioWriterSize(checkConnErrorWriter{c}, 4<<10)

	for {
	    // 從連接中讀取請求,在讀取超時時間內解析驗證請求頭中的內容。
	    // w 是返回的response實例
		w, err := c.readRequest(ctx)
		if c.r.remain != c.server.initialReadLimitSize() {
			// If we read any bytes off the wire, we're active.
			c.setState(c.rwc, StateActive)
		}
		// 根據錯誤碼,向底層的net.Conn(實現了讀寫關閉接口)實例寫入錯誤信息
		if err != nil {
			const errorHeaders = "\r\nContent-Type: text/plain; charset=utf-8\r\nConnection: close\r\n\r\n"

			switch {
			case err == errTooLarge:
			    // 請求頭字段過大。
				// Their HTTP client may or may not be
				// able to read this if we're
				// responding to them and hanging up
				// while they're still writing their
				// request. Undefined behavior.
				const publicErr = "431 Request Header Fields Too Large"
				fmt.Fprintf(c.rwc, "HTTP/1.1 "+publicErr+errorHeaders+publicErr)
				// 寫回響應並關閉連接。
				c.closeWriteAndWait()
				return

			case isUnsupportedTEError(err):
			    // 無法識別請求編碼。
				// Respond as per RFC 7230 Section 3.3.1 which says,
				//      A server that receives a request message with a
				//      transfer coding it does not understand SHOULD
				//      respond with 501 (Unimplemented).
				code := StatusNotImplemented

				// We purposefully aren't echoing back the transfer-encoding's value,
				// so as to mitigate the risk of cross side scripting by an attacker.
				fmt.Fprintf(c.rwc, "HTTP/1.1 %d %s%sUnsupported transfer encoding", code, StatusText(code), errorHeaders)
				return

			case isCommonNetReadError(err):
				return // don't reply

			default:
			    // err 是 badRequestError 類型
				publicErr := "400 Bad Request"
				if v, ok := err.(badRequestError); ok {
					publicErr = publicErr + ": " + string(v)
				}

				fmt.Fprintf(c.rwc, "HTTP/1.1 "+publicErr+errorHeaders+publicErr)
				return
			}
		}

		// Expect 100 Continue support
		req := w.req
		if req.expectsContinue() {
			if req.ProtoAtLeast(1, 1) && req.ContentLength != 0 {
				// Wrap the Body reader with one that replies on the connection
				req.Body = &expectContinueReader{readCloser: req.Body, resp: w}
			}
		} else if req.Header.get("Expect") != "" {
			w.sendExpectationFailed()
			return
		}

		c.curReq.Store(w)

		if requestBodyRemains(req.Body) {
			registerOnHitEOF(req.Body, w.conn.r.startBackgroundRead)
		} else {
			w.conn.r.startBackgroundRead()
		}

		// HTTP 不能同時有多個活躍的請求。[*]
		// 服務器回覆了一個請求後才能讀取下一個請求,
		// 所以我們可以在這個 goroutine 裏執行 handler 。
		// [*] 並非嚴格限定: HTTP pipelining. 即便它們需要串行地響應,
		// 我們也可以讓它們全部並行地處理。
		// 但我們不打算實現 HTTP pipelining because it
		// was never deployed in the wild and the answer is HTTP/2.
		
		// 將 server 包裹爲 serverHandler 的實例,執行它的
		// ServeHTTP 方法,處理請求,返回響應。
		// serverHandler 委託給 server 的 Handler 或者 DefaultServeMux(默認路由器)
		// 來處理 "OPTIONS *" 請求。
		serverHandler{c.server}.ServeHTTP(w, w.req)
		w.cancelCtx()
		if c.hijacked() {
			return
		}
		w.finishRequest()
		if !w.shouldReuseConnection() {
			if w.requestBodyLimitHit || w.closedRequestBodyEarly() {
				c.closeWriteAndWait()
			}
			return
		}
		c.setState(c.rwc, StateIdle)
		c.curReq.Store((*response)(nil))

		if !w.conn.server.doKeepAlives() {
			// We're in shutdown mode. We might've replied
			// to the user without "Connection: close" and
			// they might think they can send another
			// request, but such is life with HTTP/1.1.
			return
		}

		if d := c.server.idleTimeout(); d != 0 {
			c.rwc.SetReadDeadline(time.Now().Add(d))
			if _, err := c.bufr.Peek(4); err != nil {
				return
			}
		}
		c.rwc.SetReadDeadline(time.Time{})
	}
}

這一步的主要工作是解析驗證請求頭,然後把server 包裹爲 serverHandler 的實例,執行它的ServeHTTP 方法,處理請求,返回響應。

5. serverHandler.ServeHTTP

func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
	handler := sh.srv.Handler
	如果沒有定義 server.Handler,就是用默認路由器
	if handler == nil {
		handler = DefaultServeMux
	}
	if req.RequestURI == "*" && req.Method == "OPTIONS" {
		handler = globalOptionsHandler{}
	}
	handler.ServeHTTP(rw, req)
}

Handler 是一個接口,嵌入在Server裏。默認的DefaultServeMux是一個ServeMux類型的實例。

type Handler interface {
	ServeHTTP(ResponseWriter, *Request)
}

Handler 響應 HTTP 請求。

ServeHTTP 方法將響應頭賀響應體寫入 ResponseWriter 並返回。返回標誌着請求的結束;與 ServeHTTP 調用同時或者在ServeHTTP後使用 ResponseWriter 或者 讀取 Request.Body 都是無效的。

由於 HTTP 客戶端、HTTP 協議版本和任何客戶端與Go服務器之間的中間人的不同,有可能在寫入 ResponseWriter 後就不能讀取Request.Body了。注意handlers應該先讀取Request.Body然後再響應。

除了讀取請求體之外,handlers不應該修改請求。

ServeHTTP panic了,(調用ServeHTTP方法的)服務器假設 panic 的影響僅限於當前活躍的請求。它恢復 panic、將堆棧追蹤記入錯誤日誌,然後根據不同的HTTP協議執行關閉網絡連接或者發送一個HTTP/2 RST_STREAM,使用 ErrAbortHandler panic可以中斷處理請求,客戶端可以看到中斷的響應而服務器不會記錄錯誤日誌。

6. ServeMux.ServeHTTP

ServeMux是標準庫提供的HTTP請求路由器,它將到來的請求的url與一套註冊的模板進行匹配,調用與請求的URL匹配最接近的handler處理請求。

type ServeMux struct {
	mu    sync.RWMutex
	m     map[string]muxEntry
	es    []muxEntry // 路由項切片,從長到短排好序的
	hosts bool       // 匹配的模板是否包含主機名
}

ServeHTTP 方法將請求分發給與請求URL匹配最接近的handler

func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
	if r.RequestURI == "*" {
		if r.ProtoAtLeast(1, 1) {
			w.Header().Set("Connection", "close")
		}
		w.WriteHeader(StatusBadRequest)
		return
	}
	h, _ := mux.Handler(r)
	h.ServeHTTP(w, r)
}

*ServeMux的Handler方法根據請求的方法、主機名和URL路徑返回給定請求的處理器(handler)。它總是會返回非空的handler。如果路徑不符合格式規範,handler將是一個跳轉到規範路徑的內部生成的handler。如果host有端口,在匹配handler時會忽略端口。

Handler也返回請求匹配的模板,如果是內部生成的跳轉,模板就在跳轉後匹配。

如果沒有爲請求註冊handler,Handler就返回 ‘page not found’ 和空模板

h.ServeHTTP(w, r) 是具體的處理函數,最後說明。

func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) {

	// CONNECT requests are not canonicalized.
	if r.Method == "CONNECT" {
		// If r.URL.Path is /tree and its handler is not registered,
		// the /tree -> /tree/ redirect applies to CONNECT requests
		// but the path canonicalization does not.
		if u, ok := mux.redirectToPathSlash(r.URL.Host, r.URL.Path, r.URL); ok {
			return RedirectHandler(u.String(), StatusMovedPermanently), u.Path
		}

		return mux.handler(r.Host, r.URL.Path)
	}

	// 刪除port,整理path
	host := stripHostPort(r.Host)
	path := cleanPath(r.URL.Path)

	// 如果給的路徑是 /tree 但沒註冊這個handler,就跳轉到 /tree/.
	if u, ok := mux.redirectToPathSlash(host, path, r.URL); ok {
		return RedirectHandler(u.String(), StatusMovedPermanently), u.Path
	}

	if path != r.URL.Path {
		_, pattern = mux.handler(host, path)
		url := *r.URL
		url.Path = path
		return RedirectHandler(url.String(), StatusMovedPermanently), pattern
	}

	return mux.handler(host, r.URL.Path)
}

7 *ServeMux.handler

這是Handler的主要實現。除了 CONNECT 方法以外,路徑格式必須符合規範。

func (mux *ServeMux) handler(host, path string) (h Handler, pattern string) {
	mux.mu.RLock()
	defer mux.mu.RUnlock()

	// 指定host的模板優先處理
	if mux.hosts {
		h, pattern = mux.match(host + path)
	}
	if h == nil {
		h, pattern = mux.match(path)
	}
	if h == nil {
		h, pattern = NotFoundHandler(), ""
	}
	return
}

這裏的match函數返回HandlerFunc(f).

之前函數func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request)的最後一句h.ServeHTTP(w, r)找了半天沒找到是什麼,其實上面這個func (mux *ServeMux) handler(host, path string) (h Handler, pattern string)方法返回的就是通過路由匹配到的註冊到Server的HandleFunc,它的ServeHTTP方法即是註冊的HandleFunc函數,也就是最上面的那個test==函數。至此,一個http 請求的完整處理過程基本捋出來了。==下面是HandlerFunc類型和它的ServeHTTP方法:

// HandlerFunc 類型是一個允許一個普通函數用於HTTP處理函數的適配器。
// 如果 f 是一個簽名適配的函數,HandlerFunc(f) 是一個調用 f 的 Handler。
type HandlerFunc func(ResponseWriter, *Request)

// ServeHTTP 調用 f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
	f(w, r)
}

總結

因爲很喜歡 Go 的 Http Server 所以很想了解源碼。第一次啃這麼大量的源碼,用了幾天的時間,只是捋出一個梗概的脈絡,有一個總體的認知,並沒有深入很多細枝末節,但是收穫還是很大,後面有時間再細緻研究。

代碼方面的收穫主要有以下幾點:

  • 使用接口,實現了 ServeHTTP(ResponseWriter, *Request) 方法就是Handler接口,http包的server.go裏面有大量這個方法調用。有很多類型都實現了這個方法,比如*ServeMuxHandlerFunc,而且HandlerFunc 是一個函數類型,也可以實現方法。有點麻煩的地方在於不一定能在IDE裏直接跳轉到方法定義,可能需要先找到正確的結構體,再找它的方法定義。
  • 結構體嵌套,代碼用了很多次結構體包裹結構體的方式,實現類似繼承但更加清晰簡潔的複用方式。
  • 集合類型用值是空結構體的map,最節省空間。
  • 伺服函數用死循環,每次循環的等待時間成倍增加,設置一個最大等待時間的上限,這種逐漸增大等待時間配合等待時間上限即節約了cpu又保證了響應的性能。
  • type badRequestError string類型有個返回string的Error()方法,對於自定義類型,fmt.Print 會打印Error方法返回的字符串,如果沒有Error方法會打印String方法返回的字符串
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章