Go 1.7 相比 Go 1.6 有哪些值得注意的改动?

本系列旨在梳理 Go 的 release notes 与发展史,来更加深入地理解 Go 语言设计的思路。

go.dev/doc/go1.7

Go 1.7 值得关注的改动:

  1. 语言规范微调: 明确了语句列表的终止语句是以"最后一个非空语句"为准,这与编译器 gcgccgo 的现有行为一致,对现有代码没有影响。之前的定义仅指"最终语句",导致尾随空语句的效果不明确。
  2. 平台支持: 新增了对 macOS 10.12 Sierra 的支持(注意:低于 Go 1.7 构建的二进制文件在 Sierra 上可能无法正常工作)。增加了对 Linux on z Systems (linux/s390x) 的实验性移植。同时更新了对 MIPS64、PowerPC 和 OpenBSD 的支持。
  3. Cgo 改进: 使用 Cgo 的包现在可以包含 Fortran 源文件。新增了 C.CBytes 辅助函数用于 []byte 到 C 的 void* 转换。同时,在配合较新版本的 GCC 或 Clang 时,Cgo 构建的确定性得到了提升。
  4. Context 包:golang.org/x/net/context 包引入标准库,成为 context 包,用于在 API 边界之间传递请求范围的值、取消信号和超时。此变更使得包括 net, net/http, 和 os/exec 在内的标准库包也能利用 context
  5. HTTP 追踪: 新增 net/http/httptrace 包,提供了在 HTTP 请求内部追踪事件的机制,方便开发者诊断和分析 HTTP 请求的生命周期细节。

下面是一些值得展开的讨论:

Cgo 改进:支持 Fortran、新增 C.CBytes 及构建确定性

Go 1.7 对 Cgo (Cgo) 进行了几项改进:

  1. Fortran 支持 :现在,使用 Cgo 的 Go 包可以直接包含 Fortran 语言编写的源文件(.f, .F, .f90, .F90, .f95, .F95)。不过,Go 代码与 Fortran 代码交互时,仍然需要通过 C 语言的 API 作为桥梁。
  2. 新增 C.CBytes 辅助函数
  • 之前,如果想把 Go 的 string 传递给 C 函数(通常是 char* 类型),可以使用 C.CString。这个函数会在 C 的内存堆上分配空间,并将 Go 字符串的内容(包括结尾的 \0)复制过去,返回一个 *C.char。开发者需要记得在使用完毕后调用 C.free 来释放这块内存。
  • Go 1.7 新增了 C.CBytes 函数。它接受一个 Go 的 []byte 切片,返回一个 unsafe.Pointer(对应 C 的 void*)。与 C.CString 不同,C.CBytes 不会 复制数据,而是直接返回指向 Go 切片底层数组的指针。关键在于 :这个指针指向的是 Go 的内存,其生命周期由 Go 的垃圾回收器管理。这意味着这个 unsafe.Pointer 通常只在 C 函数调用的短暂期间内有效。C 代码不应该持有这个指针长期使用,因为它指向的内存可能随时被 Go GC 回收或移动。C.CBytes 的主要优势在于避免了内存分配和数据复制,提高了性能,特别适用于 C 函数只需要临时读取 Go 字节数据的场景。

下面是一个使用 C.CBytes 的例子:

假设我们有一个 C 函数,它接收一个字节缓冲区和长度:

c 复制代码
// #include <stdio.h>
// #include <string.h>
//
// void process_data(void* data, size_t len) {
//     char buf[100];
//     // 注意:这里只是读取数据,并且假设 len 不会超长
//     memcpy(buf, data, len < 99 ? len : 99);
//     buf[len < 99 ? len : 99] = '\0';
//     printf("C received: %s (length: %zu)\n", buf, len);
// }
import "C"
import (
    "fmt"
    "unsafe"
)

func main() {
    goBytes := []byte("Hello from Go Slice!")

    // 将 Go []byte 传递给 C 函数
    // C.CBytes 返回 unsafe.Pointer,对应 C 的 void*
    // C 函数接收数据指针和长度
    C.process_data(C.CBytes(goBytes), C.size_t(len(goBytes)))

    fmt.Println("Go function finished.")

    // 注意:goBytes 的内存在 Go 中管理,不需要手动 free
    // C.CBytes 返回的指针仅在 C.process_data 调用期间保证有效
}

运行上述 Go 程序(需要 C 编译器环境),C 函数 process_data 将能正确接收并打印 Go 传递过来的字节数据。

  1. 构建确定性提升
  • 在 Go 1.7 之前,使用 Cgo 构建包或二进制文件时,每次构建的结果(二进制内容)可能都不同。这主要是因为构建过程中会涉及到一些临时目录,而这些临时目录的路径会被嵌入到最终的调试信息中。
  • Go 1.7 利用了较新版本 C 编译器(如 GCC 或 Clang)提供的一个特性:-fdebug-prefix-map 选项。这个选项允许将源码或构建时的路径映射到一个固定的、与环境无关的前缀。当 Go 的构建工具链检测到可用的 C 编译器支持此选项时,就会使用它来处理 Cgo 生成的 C 代码编译过程中的路径信息。
  • 其结果是,只要输入的 Go 源码、依赖库和构建工具链版本相同,并且使用了支持该选项的 C 编译器,那么重复构建产生的二进制文件内容将是完全一致的。这种 确定性构建 (deterministic builds) 对于依赖二进制文件哈希进行验证、缓存或分发的场景非常重要。

Context 包:标准化请求范围管理与取消机制

Go 1.7 最重要的变化之一是将原先位于扩展库 golang.org/x/net/contextcontext 包正式引入标准库。这标志着 Go 语言在处理并发、超时和请求数据传递方面有了统一的、官方推荐的模式。

为什么需要 context

在典型的 Go 服务器应用中,每个请求通常在一个单独的 协程 (goroutine) 中处理。处理请求的过程中,可能需要启动更多的 goroutine 来访问数据库、调用其他 RPC 服务等。这些为同一个请求工作的 goroutine 集合通常需要共享一些信息,例如:

  • 用户的身份标识或授权令牌。
  • 请求的截止时间 (deadline)。
  • 一个取消信号,当原始请求被取消(如用户关闭连接)或超时时,所有相关的 goroutine 都应该尽快停止工作,释放资源。

context 包就是为了解决这些问题而设计的。它提供了一种在 API 调用链中传递 请求范围的值 (request-scoped values)取消信号 (cancellation signals)截止时间 (deadlines) 的标准方法。

核心接口 context.Context

go 复制代码
package context

import "time"

type Context interface {
    // Deadline 返回此 Context 被取消的时间,如果没有设置 Deadline,ok 返回 false。
    Deadline() (deadline time.Time, ok bool)

    // Done 返回一个 channel,当 Context 被取消或超时时,该 channel 会被关闭。
    // 多次调用 Done 会返回同一个 channel。
    // 如果 Context 永不取消,Done 可能返回 nil。
    Done() <-chan struct{}

    // Err 在 Done channel 关闭后,返回 Context 被取消的原因。
    // 如果 Context 未被取消,返回 nil。
    Err() error

    // Value 返回与此 Context 关联的键 key 对应的值,如果没有则返回 nil。
    // key 必须是可比较的类型,通常不应是内置的 string 类型或任何其他内置类型,
    // 以避免不同包之间定义的键发生冲突。
    Value(key interface{}) interface{}
}
  • Done(): 这是实现取消信号的核心。下游的 goroutine 可以 select 这个 Done() channel,一旦它被关闭,就意味着上游发出了取消指令,goroutine 应该停止当前工作并返回。
  • Err(): 当 Done() 关闭后,可以通过 Err() 获取取消的原因。如果是超时取消,通常返回 context.DeadlineExceeded;如果是手动调用 cancel 函数取消,通常返回 context.Canceled
  • Deadline(): 允许 goroutine 检查是否还有足够的时间来完成任务。
  • Value(): 用于传递请求范围的数据,如用户 ID、追踪 ID 等。注意 :官方建议谨慎使用 Value,它主要用于传递贯穿整个请求调用链的元数据,而不是用来传递可选参数。滥用 Value 会使代码的依赖关系变得不明确。

创建和派生 Context

通常我们不直接实现 Context 接口,而是使用 context 包提供的函数来创建和派生 Context

  • context.Background(): 返回一个非 nil 的空 Context。它通常用在 main 函数、初始化以及测试代码中,作为所有 Context 树的根节点。它永远不会被取消,没有值,也没有截止时间。
  • context.TODO(): 与 Background() 类似,也是一个空的 Context。它的用途是指示当前代码还不清楚应该使用哪个 Context,或者函数签名后续可能会更新以接收 Context。它是一个临时的占位符。
  • context.WithCancel(parent Context) (ctx Context, cancel CancelFunc): 创建一个新的 Context,它是 parent 的子节点。同时返回一个 cancel 函数。调用这个 cancel 函数会取消新的 ctx 及其所有子 Context。如果 parent 被取消,ctx 也会被取消。
  • context.WithDeadline(parent Context, d time.Time) (Context, CancelFunc): 创建一个带有截止时间的 Context。当到达时间 dparent 被取消,或者调用返回的 cancel 函数时,ctx 会被取消。
  • context.WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc): 是 WithDeadline 的便利写法,等价于 WithDeadline(parent, time.Now().Add(timeout))
  • context.WithValue(parent Context, key, val interface{}) Context: 创建一个携带键值对的 Context。获取值时,会先在当前 Context 查找,如果找不到,会递归地在父 Context 中查找。

这些派生函数创建了一个 Context 树。取消操作会向下传播,但值传递是向上查找的。

实际应用场景示例

  1. 优雅地取消长时间运行的任务

假设有一个函数需要执行一项可能耗时较长的操作,我们希望能在外部取消它。

go 复制代码
package main

import (
    "context"
    "fmt"
    "time"
)

// worker 模拟一个耗时任务,它会监听 Context 的取消信号
func worker(ctx context.Context, id int) {
    fmt.Printf("Worker %d started\n", id)
    select {
    case <-time.After(5 * time.Second): // 模拟工作耗时
        fmt.Printf("Worker %d finished normally\n", id)
    case <-ctx.Done(): // 监听取消信号
        // Context 被取消,清理并退出
        fmt.Printf("Worker %d canceled: %v\n", id, ctx.Err())
    }
}

func main() {
    // 创建一个可以被取消的 Context
    ctx, cancel := context.WithCancel(context.Background())

    // 启动一个 worker goroutine
    go worker(ctx, 1)

    // 等待一段时间
    time.Sleep(2 * time.Second)

    // 发出取消信号
    fmt.Println("Main: Sending cancellation signal...")
    cancel() // 调用 cancel 函数

    // 等待一小段时间,确保 worker 有时间响应取消并打印信息
    time.Sleep(1 * time.Second)
    fmt.Println("Main: Finished")
}
bash 复制代码
$ go run main.go 
Worker 1 started
Main: Sending cancellation signal...
Worker 1 canceled: context canceled
Main: Finished

在这个例子中,main 函数创建了一个可取消的 Context 并传递给 workerworker 使用 select 同时等待任务完成和 ctx.Done()。当 main 调用 cancel() 后,ctx.Done() 的 channel 会被关闭,worker 能够捕获到这个信号并提前退出。

  1. 设置 API 调用超时

在调用外部服务或数据库时,设置超时是非常常见的需求。

go 复制代码
package main

import (
    "context"
    "fmt"
    "net/http"
    "time"
)

func fetchURL(ctx context.Context, url string) (string, error) {
    // 使用 http.NewRequestWithContext 将 Context 与请求关联
    // 这个例子实际上有些超前, NewRequestWithContext 在 go 1.13 中才被添加
    req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
    if err != nil {
        return "", fmt.Errorf("failed to create request: %w", err)
    }

    // 发送请求
    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        // 如果是因为 Context 超时或取消导致的错误,err 会是 context.DeadlineExceeded 或 context.Canceled
        return "", fmt.Errorf("failed to fetch URL: %w", err)
    }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusOK {
        return "", fmt.Errorf("unexpected status code: %d", resp.StatusCode)
    }

    // 这里简化处理,实际应用中会读取 Body 内容
    return fmt.Sprintf("Success: Status %d", resp.StatusCode), nil
}

func main() {
    // 创建一个带有 1 秒超时的 Context
    ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
    defer cancel() // 良好的实践:即使超时,也调用 cancel 释放资源

    // 尝试访问一个响应较慢的 URL (httpbin.org/delay/3 会延迟 3 秒响应)
    result, err := fetchURL(ctx, "https://httpbin.org/delay/3")

    if err != nil {
        fmt.Printf("Error fetching URL: %v\n", err)
        // 检查错误是否由 Context 引起
        if ctx.Err() == context.DeadlineExceeded {
            fmt.Println("Reason: Context deadline exceeded")
        } else if ctx.Err() == context.Canceled {
            fmt.Println("Reason: Context canceled")
        }
    } else {
        fmt.Printf("Result: %s\n", result)
    }
}
bash 复制代码
$ go run main.go
# 实际上在 go 1.13 以上才能运行
Error fetching URL: failed to fetch URL: Get "https://httpbin.org/delay/3": context deadline exceeded
Reason: Context deadline exceeded

这里,我们使用 context.WithTimeout 创建了一个 1 秒后自动取消的 Contexthttp.NewRequestWithContext (Go 1.7 及以后版本提供) 将这个 Context 附加到 HTTP 请求上。http.DefaultClient.Do 会监控这个 Context。如果请求在 1 秒内没有完成(包括连接、发送、接收响应头等阶段),Do 方法会返回一个错误,并且这个错误可以通过 errors.Is(err, context.DeadlineExceeded) 来判断是否是超时引起的。

  1. 传递请求范围的数据

如官方博客文章示例,传递用户 IP 地址。

go 复制代码
package main

import (
    "context"
    "fmt"
    "net"
    "net/http"
    "time"
)

// 使用未导出的类型作为 key,防止命名冲突
type contextKey string

const userIPKey contextKey = "userIP"

// 将 IP 存入 Context
func NewContextWithUserIP(ctx context.Context, userIP net.IP) context.Context {
    return context.WithValue(ctx, userIPKey, userIP)
}

// 从 Context 取出 IP
func UserIPFromContext(ctx context.Context) (net.IP, bool) {
    ip, ok := ctx.Value(userIPKey).(net.IP)
    return ip, ok
}

// 模拟一个需要用户 IP 的下游处理函数
func processRequest(ctx context.Context) {
    fmt.Println("Processing request...")
    if ip, ok := UserIPFromContext(ctx); ok {
        fmt.Printf("  User IP found in context: %s\n", ip.String())
    } else {
        fmt.Println("  User IP not found in context.")
    }
    // 模拟工作
    time.Sleep(50 * time.Millisecond)
    fmt.Println("Processing finished.")
}

// HTTP handler
func handleRequest(w http.ResponseWriter, r *http.Request) {
    // 尝试从请求中解析 IP (简化处理)
    ipStr, _, err := net.SplitHostPort(r.RemoteAddr)
    var userIP net.IP
    if err == nil {
        userIP = net.ParseIP(ipStr)
    }

    // 获取请求的 Context (http.Request 自带 Context)
    ctx := r.Context() // 通常这个 Context 已经与请求的生命周期绑定

    // 如果获取到 IP,将其添加到 Context 中
    if userIP != nil {
        ctx = NewContextWithUserIP(ctx, userIP)
    }

    // 调用下游处理函数,传递带有用户 IP 的 Context
    processRequest(ctx)

    fmt.Fprintln(w, "Request processed.")
}

func main() {
    http.HandleFunc("/", handleRequest)
    fmt.Println("Starting server on :8080")
    // 注意:在实际生产中,需要配置 http.Server 并优雅地关闭
    if err := http.ListenAndServe(":8080", nil); err != nil {
        fmt.Printf("Server failed: %v\n", err)
    }
}
bash 复制代码
$ go run main.go
Starting server on :8080
# 另一个终端 curl 127.0.0.1:8080
Processing request...
  User IP found in context: 127.0.0.1
Processing finished.

在这个例子中,HTTP handler 从请求中提取了客户端 IP,并使用 context.WithValue 将其放入 Context 中。然后,它调用下游的 processRequest 函数,并将这个增强后的 Context 传递下去。processRequest 可以通过 UserIPFromContext 函数安全地取出这个 IP 地址,而无需知道它是如何被添加到 Context 中的。这实现了跨函数边界传递请求元数据的目的。

context 包的引入极大地提升了 Go 在构建健壮、可维护的并发程序,特别是网络服务器方面的能力。它成为了 Go 并发编程事实上的标准模式之一。

HTTP 追踪:深入了解 HTTP 请求的生命周期

Go 1.7 引入了 net/http/httptrace 包,为开发者提供了一种细粒度观察和测量 net/http 客户端请求生命周期中各个阶段耗时的方法。这对于性能分析、问题诊断(例如,是 DNS 查询慢,还是建立连接慢,或是服务器响应慢?)非常有帮助。

核心机制:httptrace.ClientTrace

httptrace 包的核心是 ClientTrace 结构体。这个结构体包含了一系列函数类型的字段,每个字段对应 HTTP 请求过程中的一个特定事件点(hook)。你可以为感兴趣的事件点提供回调函数。

go 复制代码
package httptrace

import (
	"context"
	"crypto/tls"
	"net"
	"time"
)

// ClientTrace 是一组可以注册的回调函数,用于追踪 HTTP 客户端请求期间发生的事件。
type ClientTrace struct {
	// GetConn 在获取连接之前被调用。hostPort 是目标地址。
	GetConn func(hostPort string)
	// GotConn 在成功获取连接后被调用。
	GotConn func(GotConnInfo)
	// PutIdleConn 在连接返回到空闲池时被调用。
	PutIdleConn func(err error)
	// GotFirstResponseByte 在收到响应的第一个字节时被调用。
	GotFirstResponseByte func()
	// Got100Continue 在收到 "HTTP/1.1 100 Continue" 响应时被调用。
	Got100Continue func()
	// Got1xxResponse 在收到以 1 开头的非 100 状态码的响应时被调用。
	Got1xxResponse func(code int, header string) error
	// DNSStart 在开始 DNS 查询时被调用。
	DNSStart func(DNSStartInfo)
	// DNSDone 在 DNS 查询结束后被调用。
	DNSDone func(DNSDoneInfo)
	// ConnectStart 在开始新的 TCP 连接时被调用。
	ConnectStart func(network, addr string)
	// ConnectDone 在新的 TCP 连接成功建立或失败后被调用。
	ConnectDone func(network, addr string, err error)
	// WroteHeaderField 在 Transport 写入 HTTP 请求头中的每个键值对后被调用。
	WroteHeaderField func(key string, value []string)
	// WroteHeaders 在 Transport 成功写入所有请求头字段后被调用。
	WroteHeaders func()
	// Wait100Continue 在发送完请求头后,如果请求包含 "Expect: 100-continue",
	// 在等待服务器的 "100 Continue" 响应之前被调用。
	Wait100Continue func()
	// WroteRequest 在 Transport 成功写入整个请求(包括主体)后被调用。
	WroteRequest func(WroteRequestInfo)
}

// GotConnInfo 包含关于已获取连接的信息。
type GotConnInfo struct {
	Conn net.Conn // 获取到的连接
	Reused bool    // 连接是否是从空闲池中复用的
	WasIdle bool   // 如果是复用连接,它在空闲池中时是否是空闲状态
	IdleTime time.Duration // 如果是复用连接且是空闲状态,它空闲了多久
}

// ... 其他 Info 结构体定义 ...

开发者可以创建一个 ClientTrace 实例,并为需要追踪的事件(如 DNS 查询、TCP 连接、TLS 握手、收到首字节等)设置回调函数。在这些回调函数中,通常会记录事件发生的时间戳,以便后续计算各阶段的耗时。

如何使用 httptrace

  1. 创建 ClientTrace 实例 :定义你关心的回调函数。
  2. 创建带有 Trace 的 Context :使用 httptrace.WithClientTrace(parentCtx, trace) 将你的 ClientTrace 实例与一个 Context 关联起来。
  3. 创建带有该 ContextRequest :使用 http.NewRequestWithContext(ctx, ...)req = req.WithContext(ctx) 将上一步得到的 Context 附加到你的 http.Request 上。
  4. 执行请求 :使用 http.Client(如 http.DefaultClient)的 Do 方法执行这个请求。

在请求执行过程中,net/http 包内部会在相应的事件点检查 Request 关联的 Context 中是否包含 ClientTrace,如果包含,则调用其中设置的回调函数。

代码示例:测量 DNS 和 TCP 连接耗时

go 复制代码
package main

import (
	"context"
	"fmt"
	"log"
	"net/http"
	"net/http/httptrace"
	"time"
)

func main() {
	url := "https://go.dev"
	req, _ := http.NewRequest("GET", url, nil)

	var start, connect, dns time.Time

	trace := &httptrace.ClientTrace{
		// DNS 查询开始
		DNSStart: func(info httptrace.DNSStartInfo) {
			dns = time.Now()
			fmt.Println("DNS Start:", info.Host)
		},
		// DNS 查询结束
		DNSDone: func(info httptrace.DNSDoneInfo) {
			fmt.Printf("DNS Done: %v, Err: %v, Duration: %v\n", info.Addrs, info.Err, time.Since(dns))
		},
		// TCP 连接开始 (包括 DNS 解析后的地址)
		ConnectStart: func(network, addr string) {
			connect = time.Now()
			fmt.Printf("Connect Start: Network=%s, Addr=%s\n", network, addr)
		},
		// TCP 连接结束
		ConnectDone: func(network, addr string, err error) {
			fmt.Printf("Connect Done: Network=%s, Addr=%s, Err: %v, Duration: %v\n", network, addr, err, time.Since(connect))
		},
		// 获取到连接 (可能是新建的或复用的)
		GotConn: func(info httptrace.GotConnInfo) {
			start = time.Now() // 将获取连接作为请求开始计时点
			fmt.Printf("Got Conn: Reused: %t, WasIdle: %t, IdleTime: %v\n", info.Reused, info.WasIdle, info.IdleTime)
		},
		// 收到响应的第一个字节
		GotFirstResponseByte: func() {
			fmt.Printf("Time to First Byte: %v\n", time.Since(start))
		},
	}

	// 将 trace 关联到 Context
	ctx := httptrace.WithClientTrace(context.Background(), trace)
	// 将带有 trace 的 Context 附加到 Request
	req = req.WithContext(ctx)

	fmt.Println("Starting request to", url)
	// 执行请求
	client := &http.Client{
		// 禁用 KeepAlives 可以确保每次都建立新连接,方便观察 ConnectStart/Done
		// Transport: &http.Transport{DisableKeepAlives: true},
	}
	resp, err := client.Do(req)
	if err != nil {
		log.Fatalf("Request failed: %v", err)
	}
	defer resp.Body.Close()

	fmt.Printf("Request finished. Status: %s\n", resp.Status)
	// 注意:这里无法直接得到总耗时,总耗时需要自己记录请求前后的时间戳来计算。
	// trace 主要用于分解内部各阶段的耗时。
}
bash 复制代码
$ go run main.go
Starting request to https://go.dev
DNS Start: go.dev
DNS Done: [{216.239.36.21 } {216.239.34.21 } {216.239.32.21 } {216.239.38.21 } {2001:4860:4802:36::15 } {2001:4860:4802:34::15 } {2001:4860:4802:32::15 } {2001:4860:4802:38::15 }], Err: <nil>, Duration: 311.409ms
Connect Start: Network=tcp, Addr=216.239.36.21:443
Connect Done: Network=tcp, Addr=216.239.36.21:443, Err: <nil>, Duration: 5.076ms
Got Conn: Reused: false, WasIdle: false, IdleTime: 0s
Time to First Byte: 373.383ms
Request finished. Status: 200 OK

运行这段代码,你将看到控制台输出 DNS 查询、TCP 连接、TLS 握手等阶段的开始和结束信息,以及它们的耗时。这对于定位 HTTP 请求中的性能瓶颈非常有价值。

httptrace 包的引入,为 Go 开发者提供了一个强大的内省工具,使得理解和优化 HTTP 客户端性能变得更加容易。

相关推荐
hrrrrb3 小时前
【Spring Boot 快速入门】八、登录认证(一)基础登录与认证校验
spring boot·后端
王大锤·4 小时前
基于spring boot的个人博客系统
java·spring boot·后端
bobz9654 小时前
QT designer 常用技巧
后端
shi57834 小时前
C# 常用的线程同步方式
开发语言·后端·c#
没逻辑5 小时前
抗量子密码技术(PQC)演变
后端·量子计算
day>day>up5 小时前
django uwsgi启动报错failed to get the Python codec of the filesystem encoding
后端·python·django
Livingbody5 小时前
FastMCP In Action跑通第一个MCP之跟学python版
后端
zhuyasen6 小时前
Sponge:一个重构Go开发体验的框架,让你在开发项目开"外挂"
go·gin·grpc
bobz9656 小时前
QT 中的三种基本UI类型:Main Window | Widget | Dialog
后端
zhoupenghui1686 小时前
golang实现支持100万个并发连接(例如,HTTP长连接或WebSocket连接)系统架构设计详解
开发语言·后端·websocket·golang·系统架构·echo·100万并发