0.前言

该笔记为笔者第一次学习go的net/http包源码的时候所记,也许写的并不是很精确,希望大家多多包涵,一起讨论学习。

该笔记很大程度的参考了网名为“小徐先生”的前辈所分享的博客,推荐大家可以先看一看它的博客来一起学习,我的只是照葫芦画瓢还有一些代码更新的讲解而已。

当前笔者追溯的是go版本为1.22.2的源码,对于中间看不太懂的片段,大家可以先看在文末我画的总流程图,再跟着流程图一段段来看。

参考博客链接:Golang HTTP 标准库实现原理 (qq.com)

1.核心数据结构:ServerMux

ServeMux 是 Go 标准库中的 HTTP 请求多路复用器(HTTP request multiplexer),负责将传入的 HTTP 请求路由到相应的处理程序。ServeMux 会根据注册的 URL 模式匹配传入的请求,并调用最合适的处理程序。以下是它的详细作用和 Go 1.22 带来的变化:

1. ServeMux 的主要功能

  • 路由匹配ServeMux 根据请求的 URL 和注册的模式(patterns)进行匹配,并根据匹配结果调用对应的处理函数。模式可以包含方法、主机名和路径的组合。例如:

    • "/index.html" 匹配路径为 /index.html 的任何主机和方法的请求。
    • "GET /static/" 匹配方法为 GET,路径以 /static/ 开头的请求。
    • "example.com/" 匹配主机为 example.com 的任何请求。
  • 通配符支持ServeMux 允许使用通配符匹配 URL 路径的一部分,如 "/b/{bucket}/o/{objectname...}",可以匹配路径中特定的部分。
  • 优先级机制:当多个模式匹配同一个请求时,ServeMux 会选择最具体的模式。例如,"/images/thumbnails/""/images/" 更具体,因此会优先匹配。
  • 尾部斜杠重定向ServeMux 会自动处理路径末尾的斜杠,如果注册了某个路径下的子树,ServeMux 会将缺少尾部斜杠的请求重定向到带有尾部斜杠的路径。
  • 请求清理ServeMux 会清理 URL 路径,例如移除 ... 等特殊路径,确保路径的规范化。

2. Go 1.22 的变更

Go 1.22 对 ServeMux 的模式匹配规则做出了重要的改动,与 Go 1.21 版本相比有若干不兼容的变化。以下是主要变化:

  • 通配符:在 Go 1.21 中,通配符被当作普通的文字路径段处理,比如 "/{x}" 只匹配与之完全相同的路径,而在 Go 1.22 中,它可以匹配任意一个路径段。
  • 模式验证:Go 1.22 开始会对模式的语法进行严格检查,任何无效的模式在注册时会导致程序 panic。例如,"/{""/a{x}" 在 1.21 中是合法的模式,但在 1.22 中会被认为是无效的。
  • 转义处理:Go 1.22 开始,每个路径段都会被解码。之前的版本对整个路径进行转义,而不是逐段处理。例如,"/%61" 在 1.22 中会匹配路径 "/a",但在 1.21 中只会匹配 "/%2561"
  • 匹配方式的改变:Go 1.22 会逐段解码路径,并处理 %2F 等字符的转义,这对于处理带有 %2F 的路径来说,与之前的版本有很大的不同。

如果需要保留 Go 1.21 的行为,可以通过设置环境变量 GODEBUG=httpmuxgo121=1 来恢复旧的模式匹配逻辑。

具体例子

以下代码展示了如何使用 ServeMux 并注册不同的路由:

mux := http.NewServeMux()
mux.HandleFunc("/", homeHandler)
mux.HandleFunc("/static/", staticHandler)
mux.HandleFunc("/b/{bucket}/o/{objectname...}", objectHandler) http.ListenAndServe(":8080", mux)

在这个例子中,不同的 URL 模式会被路由到相应的处理函数,Go 1.22 的变化会影响像 "/b/{bucket}/o/{objectname...}" 这种通配符模式的匹配方式。

这些变更旨在提高模式匹配的灵活性和一致性,同时使得 ServeMux 在复杂的路由需求下更具可扩展性。

3.ServeMux结构分析

先来看ServeMux的数据结构

type ServeMux struct {
mu sync.RWMutex //互斥锁
tree routingNode //存储pat对应的handler
index routingIndex //用于进行路由冲突检测
patterns []*pattern // TODO(jba): remove if possible(类似与pat的副本,在未来可能会删除)
mux121 serveMux121 // used only when GODEBUG=httpmuxgo121=1
}

为了进一步了解它内嵌的结构,我们先来观看routingNode的源码实现。

2.routingNode

// A routingNode is a node in the decision tree.
// The same struct is used for leaf and interior nodes.
type routingNode struct {
// 一个页子节点持有一个pat和它注册的handler。
pattern *pattern
handler Handler
// special children keys:
// "/" trailing slash (resulting from {$})
// "" single wildcard
// "*" multi wildcard
children mapping[string, *routingNode] //将路径的一部分映射到子节点中,形成一个路由树。
emptyChild *routingNode // 一个优化字段,用于快速访问键为 "" 的子节点,减少查找开销。
}

对于三个special children keys的解释:

  • 对于“/”,它处理了路径中以斜杠为结尾的情况,例如当一个路由模式以斜杠结尾(如 "/images/"),ServeMux 会自动处理不带斜杠的请求(如 "/images") 并将其重定向到带有斜杠的路径("/images/"
  • “”作为一个匹配单个路径段的通配符
  • “*”作为一个匹配多个路径段的通配符

2.1addPattern

//addPattern方法添加一个pattern和它的handler到树中
func (root *routingNode) addPattern(p *pattern, h Handler) {
// First level of tree is host.
n := root.addChild(p.host) //先获取host对应的routingNode
// Second level of tree is method.
n = n.addChild(p.method) //接着再获取method对应的routingNode
// Remaining levels are path.
n.addSegments(p.segments, p, h)
}

2.2addChild

//向一个 routingNode (路由节点)添加一个子节点,并返回该子节点。如果节点已经存在,则直接返回;如果不存在,则创建一个新的子节点并返回。
func (n *routingNode) addChild(key string) *routingNode {
if key == "" { //路径为空,如果存在emptyChild则直接返回作为一个单项通配符,如果不存在则创建并返回。
if n.emptyChild == nil {
n.emptyChild = &routingNode{}
}
return n.emptyChild
}//查找是否存在对应的pat
if c := n.findChild(key); c != nil {
return c
}
c := &routingNode{}
n.children.add(key, c)
return c
}

我们再来看对应的findChild方法的实现

// findChild returns the child of n with the given key, or nil
// if there is no child with that key.
func (n *routingNode) findChild(key string) *routingNode {
if key == "" {
return n.emptyChild
}
r, _ := n.children.find(key)
return r
}

可以看到,最终是直接调用了map去寻找是否存在对应的value。

2.3addSegements

// addSegments 的作用是将路径中的各个段(segments)逐步添加到路由树中,并最终将模式 (pattern) 和处理器 (handler) 设置在路由树的叶子节点。
func (n *routingNode) addSegments(segs []segment, p *pattern, h Handler) {
if len(segs) == 0 { //如果segs为空,表示当前为一个叶子节点,此时就将p和h绑定在该节点上
n.set(p, h)
return
}
seg := segs[0]
if seg.multi { //如果此时当前的segment是多段通配符,通常为*
if len(segs) != 1 { //检查当前是否为最后一段,因为多段通配符必须是路径的最后一段。
panic("multi wildcard not last")
}
n.addChild("*").set(p, h) //绑定
} else if seg.wild { //若是单段通配符,则进一步递归
n.addChild("").addSegments(segs[1:], p, h)
} else { //同理
n.addChild(seg.s).addSegments(segs[1:], p, h)
}
}

3.routingIndex

//routingIndex是一个用于优化路由冲突检测的数据结构
//它的检测思想是通过排除对于给定的pattern,不可能与之冲突的部分,只检测可能产生冲突的部分来达到快速检测的目的。
type routingIndex struct {
segments map[routingIndexKey][]*pattern
multis []*pattern
}
type routingIndexKey struct {
pos int // 0-based segment position
s string // literal, or empty for wildcard
}
  • sgements:记录了每一个routingIndexKey注册的pattern组,routingIndexKey包含两个字段,pos标识了s出现在pattern中的段的位置。例如,对于一个key{1,"b"},它对应了pattern为"/a/b"、“/a/b/c”,因为b处在第1段。
  • multis:用于存储所有以多段通配符为结尾的patterns

3.1addPattern

func (idx *routingIndex) addPattern(pat *pattern) {
if pat.lastSegment().multi { //若以多段通配符结尾,则直接添加
idx.multis = append(idx.multis, pat)
} else {
if idx.segments == nil { //若第一次注册,先初始化
idx.segments = map[routingIndexKey][]*pattern{}
}
for pos, seg := range pat.segments {
key := routingIndexKey{pos: pos, s: ""}
if !seg.wild {
key.s = seg.s //非通配符就赋值
}
idx.segments[key] = append(idx.segments[key], pat) //添加pat
}
}
}

3.2possiblyConflictingPatterns

//该函数会调用方法f去对所有可能与pat冲突的pattern进行检测,如果f返回一个非空的错误,那么possiblyConflictingPatterns会立即返回这个错误
func (idx *routingIndex) possiblyConflictingPatterns(pat *pattern, f func(*pattern) error) (err error) {
//一个辅助函数,用于对所有的pats进行检测
apply := func(pats []*pattern) error {
if err != nil {
return err
}
for _, p := range pats {
err = f(p)
if err != nil {
return err
}
}
return nil
} //首先对多段通配符进行进测,因为这些pattern可能与任何传入的pat冲突
if err := apply(idx.multis); err != nil {
return err
}
//处理以/为结尾的路径段
if pat.lastSegment().s == "/" {
return apply(idx.segments[routingIndexKey{s: "/", pos: len(pat.segments) - 1}])
}
//如果模式不是以/为结尾,那么函数会检测与它的相同位置有相同的字面值或是单段通配符的模式,
//通过遍历模式的每一个段,函数寻找可能冲突的模式,并在可能冲突最少的段上进行匹配
//函数在查找时会计算每个段位置的匹配模式数量,并选择pattern数量最少的段进行匹配,这样可以优化冲突检测的效率。
var lmin, wmin []*pattern //分别存储literal匹配的pattern集合和通配符wildcard匹配的pattern集合
min := math.MaxInt
hasLit := false //标识是否遇到了literal
for i, seg := range pat.segments {
if seg.multi { //跳过多段匹配符,因为已经在上面检测过
break
}
if !seg.wild { //对于非通配符
hasLit = true
//索引中查找所有与当前字面值 seg.s 和位置 i 匹配的模式集合(lpats)
lpats := idx.segments[routingIndexKey{s: seg.s, pos: i}]
//查找所有与位置 i 匹配的通配符模式集合(wpats)
wpats := idx.segments[routingIndexKey{s: "", pos: i}]
if sum := len(lpats) + len(wpats); sum < min {
lmin = lpats
wmin = wpats
min = sum
}
}
}
if hasLit {
//对可能的冲突进行匹配检测
apply(lmin)
apply(wmin)
return err
} //该pattern是由通配符组成,需要检查和任意的pat会不会冲突
for _, pats := range idx.segments {
apply(pats)
}
return err
}

该方法的主要作用是优化了冲突检测的效率,当我们需要为一个路由注册handler的时候,会先检查是否存在路由模式匹配冲突,这时候使用该方法可以加快检测效率。

4.循序渐进学Serve服务

在main函数中,使用以下几行代码即可启动一个Server

func main() {
http.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("pong"))
}) http.ListenAndServe(":8091", nil)
}

我们调用了HandleFunc函数,为"/ping"这一个路由注册了一个处理函数,然后通过ListenAndServe启动监听,通过这个实例,我们跟进HandleFunc查看具体的源码过程。

4.1HandlerFunc函数实现

// HandleFunc registers the handler function for the given pattern in [DefaultServeMux].
// The documentation for [ServeMux] explains how patterns are matched.
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
if use121 {
DefaultServeMux.mux121.handleFunc(pattern, handler)
} else {
DefaultServeMux.register(pattern, HandlerFunc(handler))
}
}

当调用公开的函数HandleFunc时,会默认将路由注册到context包下的DefaultServeMux中,它是一个ServeMux的具体实现。

本笔记探讨的是1.22版本开始的Server模式,于是跟进register函数查看。

func (mux *ServeMux) register(pattern string, handler Handler) {
if err := mux.registerErr(pattern, handler); err != nil {
panic(err)
}
} func (mux *ServeMux) registerErr(patstr string, handler Handler) error {
if patstr == "" {
return errors.New("http: invalid pattern")
}
if handler == nil {
return errors.New("http: nil handler")
}
if f, ok := handler.(HandlerFunc); ok && f == nil {
return errors.New("http: nil handler")
} pat, err := parsePattern(patstr) //将patstr转换为标准的pattern结构
if err != nil {
return fmt.Errorf("parsing %q: %w", patstr, err)
} // Get the caller's location, for better conflict error messages.
// Skip register and whatever calls it. //这里使用了runtime.Caller函数来获取调用这个函数的代码位置(即调用者的信息),主要是为了在发生错误时,能在错误信息中提供更准确的源代码位置。这可以帮助开发者更方便地定位问题。
//runtime.Caller()中的参数3表示获取调用栈中第三层的调用信息,在这里层级的顺序大概是
//1.runtimer.Caller本身
//2.当前方法registerErr的调用
//3.调用registerErr的函数
_, file, line, ok := runtime.Caller(3)
if !ok {
pat.loc = "unknown location"
} else {
pat.loc = fmt.Sprintf("%s:%d", file, line)
} //以下方法用于检测新注册的路由pat是否会和已有的路由pat2存在冲突的逻辑
mux.mu.Lock()
defer mux.mu.Unlock()
// Check for conflict.
if err := mux.index.possiblyConflictingPatterns(pat, func(pat2 *pattern) error {
if pat.conflictsWith(pat2) {
d := describeConflict(pat, pat2)
return fmt.Errorf("pattern %q (registered at %s) conflicts with pattern %q (registered at %s):\n%s",
pat, pat.loc, pat2, pat2.loc, d)
}
return nil
}); err != nil {
return err
}
//若不存在冲突,则为对应的pat注册handler方法
mux.tree.addPattern(pat, handler)
mux.index.addPattern(pat)
mux.patterns = append(mux.patterns, pat)
return nil
}

4.2启动Serve服务的ListenAndServe

调用net/http包下的公开的ListenAndServe函数,就可以实现对服务端的一键启动功能,跟进该函数看看它都做了什么

1.ListenAndServe

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

该方法创建了一个Server实例,并且调用该server的ListenAndServe方法,Server实例中若handler为nil,则自动调用DefaultServeMux。

2.server.ListenAndServe

func (srv *Server) ListenAndServe() error {
if srv.shuttingDown() {
return ErrServerClosed
}
addr := srv.Addr
if addr == "" {
addr = ":http" //若srv.Addre为“”,则默认为此
}
ln, err := net.Listen("tcp", addr)//启用TCP监听
if err != nil {
return err
}
return srv.Serve(ln)
}

该方法使用TCP连接Server中的地址,并且使用该实例中注册的Handler去处理业务

3.server.Serve

var ServerContextKey = &contextKey{"http-server"}

type contextKey struct {
name string
}
func (srv *Server) Serve(l net.Listener) error {
// ...
ctx := context.WithValue(baseCtx, ServerContextKey, srv)
for {
rw, err := l.Accept()
// ...
connCtx := ctx
// ...
c := srv.newConn(rw)
// ...
go c.serve(connCtx)
}
}

Serve方法是go进行服务端处理连接的核心方法,在这段核心片段中,首先先创建了一个带有键值对的上下文ctx,其中键是ServerContextKey,对应着一个服务器实例srv。

接着使用for循环的方式,负责监听并接受来自Listener的连接(l.Accept()),每一次的循环都会尝试从监听器中获取一个新的连接rw。

每一个连接都会关联一个connCtx,在默认的情况它就是之前创建的ctx

使用srv.newConn创建了一个新的连接对象c,封装了rw作为http连接,最后启动了一个新的协程异步处理该连接的请求,这个c.serve方法会处理来自客户端HTTP的请求。

可以看到,整体方法的思路是采用for循环监听连接,并且为之分配一个协程进行处理。

4.conn.Serve

接着我们来看conn.Serve,它作为实际处理请求的核心,非常重要,它的整个方法代码比较长,我们分成几部分,着重看请求处理的部分。

第一个部分,进行设置上下文ctx和异常处理

ctx = context.WithValue(ctx, LocalAddrContextKey, c.rwc.LocalAddr())
var inFlightResponse *response
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 inFlightResponse != nil {
inFlightResponse.cancelCtx()
}
if !c.hijacked() {
if inFlightResponse != nil {
inFlightResponse.conn.r.abortPendingRead()
inFlightResponse.reqBody.Close()
}
c.close()
c.setState(c.rwc, StateClosed, runHooks)
}
}()

使用了defer设置了一个错误处理机制,如果在请求期间发生了panic,会捕获错误并且记录日志。

接着第二个部分为处理TLS(HTTPS)连接

if tlsConn, ok := c.rwc.(*tls.Conn); ok {
tlsTO := c.server.tlsHandshakeTimeout()
if tlsTO > 0 {
dl := time.Now().Add(tlsTO)
c.rwc.SetReadDeadline(dl)
c.rwc.SetWriteDeadline(dl)
}
if err := tlsConn.HandshakeContext(ctx); err != nil {
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
}
...
}

该代码段进行TLS握手,若握手成功便会记录TLS的状态。

接着来到了读取并且处理HTTP请求的阶段

ctx, cancelCtx := context.WithCancel(ctx)
c.cancelCtx = cancelCtx
defer cancelCtx() c.r = &connReader{conn: c}
c.bufr = newBufioReader(c.r)
c.bufw = newBufioWriterSize(checkConnErrorWriter{c}, 4<<10)

对于每一个连接,都会分配一个新的读取器bufr和写入器bufw,提高读取和写入的效率

请求处理循环

for {
w, err := c.readRequest(ctx)
if err != nil {
// 错误处理,比如请求过大或不支持的传输编码
} // 如果 Expect: 100 continue,支持继续
if req.expectsContinue() { ... } // 处理请求,调用服务器的处理函数
serverHandler{c.server}.ServeHTTP(w, w.req) // 完成请求并判断是否需要复用连接
w.finishRequest()
if !w.shouldReuseConnection() {
return
} // 设置连接为空闲状态
c.setState(c.rwc, StateIdle, runHooks)
c.curReq.Store(nil)
}
  • 这个循环会反复读取请求、处理请求、并且发送响应
  • 使用serverHandler.ServeHTTP来调用对应的处理程序,比如用户设置的Server或者DefaultServerMux

最终处理完请求会检查是否可以复用连接,如果不能复用(例如客户端请求 Connection: close),则关闭连接。

5.serverHandler.ServeHTTP

在ServeHTTP方法中,会对Handler做判断,倘若没有声明,则取全局单例DefaultServeMux进行路由匹配

func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
handler := sh.srv.Handler
if handler == nil {
handler = DefaultServeMux
}
if !sh.srv.DisableGeneralOptionsHandler && req.RequestURI == "*" && req.Method == "OPTIONS" {
handler = globalOptionsHandler{}
} ​ handler.ServeHTTP(rw, req)
}

6.ServeMux.ServeHTTP

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
}
var h Handler
if use121 {
h, _ = mux.mux121.findHandler(r)
} else {
h, _, r.pat, r.matches = mux.findHandler(r)
}
h.ServeHTTP(w, r)
}
  • 首先处理了特殊的RequestURI为“*”的请求
  • 接着对于1.21版本开始的go,它会去查找request对应的Handler,返回对应的pattern和匹配结果

一旦找到了与请求路径匹配的处理器 h,调用该处理器的 ServeHTTP 方法来处理当前请求。

7.ServeMux.findHandler

// ...
host = stripHostPort(r.Host)
path = cleanPath(path)
n, matches, u = mux.matchOrRedirect(host, r.Method, path, r.URL)
// ...
return n.handler, n.pattern.String(), n.pattern, matches
  • 通过 stripHostPort 去掉主机名中的端口信息,然后使用 cleanPath 净化请求路径,去除多余的 /.
  • 调用 mux.matchOrRedirect 进行匹配,如果匹配成功,则返回相应的路由节点 (n) 和匹配的参数 (matches)
  • 最终会返回对应的handler

8.ServeMux.matchOrRedirect

func (mux *ServeMux) matchOrRedirect(host, method, path string, u *url.URL) (_ *routingNode, matches []string, redirectTo *url.URL) {
mux.mu.RLock()
defer mux.mu.RUnlock()
n, matches := mux.tree.match(host, method, path)
//...
return n, matches, nil
}

核心是调用了routingNode的match方法去找到对应的node节点。

5.回顾总结(可视化总流程)

我们以下面的main函数为例子,再重新回顾一下启动服务器的具体流程

func main() {
http.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("pong"))
}) http.ListenAndServe(":8091", nil)
}

首先我们为"/ping"路径注册了一个handler方法,go会自动将我们写的func转换为实现了Handler接口的HandlerFunc类型,并且将其添加到默认分路器的节点中。

DefaultServeMux.register(pattern, HandlerFunc(handler)) //将我们写的func转换为HandlerFunc

HandlerFunc和ServeMux都实现了Handler接口,所以都可以被看作为handler,handler用于处理一个request。

接着,我们启用监听,假设在监听中,我们访问了localhost:8091/ping,最终流程如下。

最终我们启动main服务,在网页打开localhost:8091/ping,就能看到网页输出pong啦

Go语言net/http包源码学习的更多相关文章

  1. Go语言 context包源码学习

    你必须非常努力,才能看起来毫不费力! 微信搜索公众号[ 漫漫Coding路 ],一起From Zero To Hero ! 前言 日常 Go 开发中,Context 包是用的最多的一个了,几乎所有函数 ...

  2. go标准库-log包源码学习

    log包是go语言提供的一个简单的日志记录功能,其中定义了一个结构体类型 Logger,是整个包的基础部分,包中的其他方法都是围绕这整个结构体创建的. Logger结构 Logger结构的定义如下: ...

  3. git 源码学习(init-db) 提交版本号 083c516331

    写在前面的废话: 学完git之后,还是感觉云里雾里的,于是乎,就想到了通过学习git源码,来加深git的熟练度,同时学习一下c语言编程. git源码学习,逐步分析 这篇帖子是逐步分析git源码的,将g ...

  4. Hadoop源码学习笔记(2) ——进入main函数打印包信息

    Hadoop源码学习笔记(2) ——进入main函数打印包信息 找到了main函数,也建立了快速启动的方法,然后我们就进去看一看. 进入NameNode和DataNode的主函数后,发现形式差不多: ...

  5. spring源码学习——spring整体架构和设计理念

    Spring是在Rod Johnson的<Expert One-On-One J2EE Development and Design >的基础上衍生而来的.主要目的是通过使用基本的java ...

  6. SpringBoot源码学习系列之SpringMVC自动配置

    目录 1.ContentNegotiatingViewResolver 2.静态资源 3.自动注册 Converter, GenericConverter, and Formatter beans. ...

  7. Spring5.0源码学习系列之Spring AOP简述

    前言介绍 附录:Spring源码学习专栏 在前面章节的学习中,我们对Spring框架的IOC实现源码有了一定的了解,接着本文继续学习Springframework一个核心的技术点AOP技术. 在学习S ...

  8. 06.ElementUI 2.X 源码学习:源码剖析之工程化(一)

    0x.00 前言 在用了5章篇幅 ElementUI源码学习:从零开始搭建Vue组件库汇总 讲解了如何编写一个组件.发布npm以及生成展示文档之后.接下来将分析Element项目的代码结构,学习其工程 ...

  9. Java并发包源码学习之AQS框架(一)概述

    AQS其实就是java.util.concurrent.locks.AbstractQueuedSynchronizer这个类. 阅读Java的并发包源码你会发现这个类是整个java.util.con ...

  10. Android 如何在Eclipse中查看Android API源码 及 support包源码

    当我们阅读android API开发文档时候,上面的每个类,以及类的各个方法都是已经写好的方法和控件,可是我们只是在搬来使用,不知道它的原理,它是如何被实现的.android系统是开源的,所以谷歌官方 ...

随机推荐

  1. TFC-Pretraining: 基于时间频率一致性对时间序列进行自监督对比预训练《Self-Supervised Contrastive Pre-Training for Time Series via Time-Frequency Consistency》(时间序列、时序表征、时频一致性、对比学习、自监督学习)

    2023年11月10日,今天看一篇论文,现在17:34,说实话,想摆烂休息,不想看,可还是要看,拴Q. 论文:Self-Supervised Contrastive Pre-Training for ...

  2. CSS – border-radius (Rounded Corners)

    前言 之前的文章 CSS – W3Schools 学习笔记 (3), 这篇独立出来写, 作为整理. 参考: Youtube – Advanced CSS Border-Radius Tutorial ...

  3. C++ 数据输入cin (解决CLoin输入中文程序出错)

    数据输入cin 语法:cin >> 变量 解决 CLoin 使用cin输入中文程序无法正常运行 按住Ctrl+alt+shift+/键 弹出对话框选择注册表 取消勾选run.process ...

  4. DQL—查询操作

    一.查询语法 select 字段列表 from 表名列表 where 条件列表 group by 分组列表 having 分组后条件 order by 排序字段 limit 分页限定 (提供一个表来操 ...

  5. CatGPT Puzzle

    规则简述 一个 Nonogram 谜题包含一个 \(m*n\) 大小的空白方格矩阵,以及在表格每一行右侧.每一列下方的一组线索数. 每组都有一个或多个数字,这些数字就是解题的线索. 要想解开 Nono ...

  6. 关于 xfg 的班会

  7. 【赵渝强老师】在MongoDB中使用游标

    一.什么是游标? 游标(Cursor)是处理数据的一种方法,为了查看或者处理结果集中的数据,游标提供了在结果集中一次一行或者多行前进或向后浏览数据的能力. 游标实际上是一种能从包括多条数据记录的结果集 ...

  8. 当git仓库里面已经有上传好的框架时,二次上传到仓库需要的指令

    初始化仓库 1 git init git add . "提交信息"里面换成自己的需要 如"first commit" git commit -m "提 ...

  9. C#/.NET/.NET Core技术前沿周刊 | 第 8 期(2024年10.01-10.06)

    前言 C#/.NET/.NET Core技术前沿周刊,你的每周技术指南针!记录.追踪C#/.NET/.NET Core领域.生态的每周最新.最实用.最有价值的技术文章.社区动态.优质项目和学习资源等. ...

  10. 2022年10月中国数据库排行榜:达梦冲刺IPO热度不减,PolarDB立足创新夺锦才

    秋风萧瑟,洪波涌起. 2022年10月的 墨天轮中国数据库流行度排行榜 火热出炉,本月共有245个数据库参与排名,相比上月新增七个数据库,本月排行榜前十名变动较大:达梦反超openGauss重摘探花: ...