全部文章 笔记与思考

关于 Go 标准库 net 包和 TChannel 包的深入解析

net 包是 Go 语言标准库中网络编程的核心基石,它提供了跨平台的网络 I/O 抽象,支持 TCP/IP、UDP、域名解析、Unix 域套接字等几乎所有主流网络协议。无论是构建简单的客户端 / 服务端,还是开发复杂的分布式系统,net 包都是最底层的支撑工具。

本文档将从基础到高级,全面解析 net 包的核心功能,针对常用功能进行详细展开,并提供可直接运行的实战代码示例。


1. 包概述

net 包的设计理念是统一接口,分层抽象:

  • 高层接口:Dial、Listen、Accept 等通用函数,屏蔽底层协议差异

  • 中层接口:Conn、Listener、PacketConn 等接口,定义通用的读写、监听行为

  • 底层实现:针对 TCP、UDP、IP、Unix 等不同协议的具体实现,提供协议特有的配置能力

大多数场景下,开发者只需要使用高层接口即可完成开发;而在需要精细控制网络行为的场景下,可以深入到底层实现进行调优。


2. 核心基础类型

网络编程的第一步是处理地址,net 包提供了多种地址类型来适配不同的协议。

2.1 IP 地址(IP 类型)

IP 类型是一个字节切片,用于表示 IPv4 或 IPv6 地址,它提供了一系列方法来判断地址的属性。

1
type IP []byte

核心方法与示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package main

import (
	"fmt"
	"net"
)

func main() {
	// 1. 解析IP地址
	ip := net.ParseIP("192.168.1.100")
	if ip == nil {
		fmt.Println("无效的IP地址")
		return
	}

	// 2. 判断地址类型
	fmt.Println("是否为回环地址:", ip.IsLoopback()) // false
	fmt.Println("是否为私有地址:", ip.IsPrivate())   // true
	fmt.Println("是否为IPv4地址:", ip.To4() != nil)  // true
	// To4方法会返回IPv4地址的4字节表示,如果不是IPv4地址则返回nil;To16方法会返回IPv6地址的16字节表示,如果不是IPv6地址则返回nil。

	// 3. 构造IPv4地址
	ipv4 := net.IPv4(127, 0, 0, 1)
	fmt.Println("回环地址:", ipv4.String()) // 127.0.0.1

	// 4. 掩码计算
	ip2 := net.ParseIP("192.168.1.100")
	mask := net.IPv4Mask(255, 255, 255, 0)
	networkIP := ip2.Mask(mask)
	fmt.Println("网段地址:", networkIP.String()) // 192.168.1.0
}

2.2 IP 网段(IPNet 类型)

IPNet 用于表示一个 IP 网段,常用于判断某个 IP 是否属于某个子网,这在防火墙、路由匹配等场景中非常常用。

核心方法与示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package main

import (
	"fmt"
	"log"
	"net"
)

func main() {
	// 1. 解析CIDR格式的网段
	ip, ipNet, err := net.ParseCIDR("192.168.1.50/24")
	if err != nil {
		log.Fatal(err)      // 解析失败,CIDR格式不正确,或者IP地址无效。
		// log.Fatal会导致程序崩溃,适合在示例代码中使用,但在生产环境中应该使用更优雅的错误处理方式,如log.Printf或返回错误给调用者。
	}

	fmt.Println("IP地址:", ip.String())       // 192.168.1.0
	fmt.Println("网段:", ipNet.String())        // 192.168.1.0/24

	// 2. 判断IP是否属于该网段
	testIP := net.ParseIP("192.168.1.100")
	fmt.Println(testIP.String(), "是否在网段内:", ipNet.Contains(testIP)) // true

	testIP2 := net.ParseIP("8.8.8.8")
	fmt.Println(testIP2.String(), "是否在网段内:", ipNet.Contains(testIP2)) // false
}

2.3 传输层地址(TCPAddr/UDPAddr)

TCP 和 UDP 地址都包含 IP 和端口两部分,用于标识网络中的一个端点。

地址解析示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main

import (
	"fmt"
	"log"
	"net"
)

func main() {
	// 解析TCP地址
	tcpAddr, err := net.ResolveTCPAddr("tcp", "localhost:8080")
	if err != nil {
		log.Println(err.Error())
		return
	}
	fmt.Println("TCP地址:", tcpAddr.String()) // 127.0.0.1:8080

	// 解析UDP地址
	udpAddr, err := net.ResolveUDPAddr("udp", "localhost:8080")
	if err != nil {
		log.Println(err.Error())
		return
	}
	fmt.Println("UDP地址:", udpAddr.String()) // 127.0.0.1:8080
}

2.4 地址处理工具函数

处理 host:port 格式的地址时,JoinHostPort 和 SplitHostPort 是最常用的工具,它们能正确处理 IPv6 地址的括号格式。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main

import (
	"fmt"
	"log"
	"net"
)

func main() {
	// 拼接主机和端口
	host := "127.0.0.1"
	port := "8080"
	addr := net.JoinHostPort(host, port)
	fmt.Println("拼接后地址:", addr) // 127.0.0.1:8080

	// 拆分地址
	host2, port2, err := net.SplitHostPort(addr)
	if err != nil {
		log.Println(err.Error())
		return
	}
	fmt.Println("拆分后主机:", host2, "端口:", port2)

	// 正确处理IPv6地址
	ipv6Addr := "[::1]:8080"
	host3, port3, _ := net.SplitHostPort(ipv6Addr)
	fmt.Println("IPv6拆分后主机:", host3, "端口:", port3) // ::1 8080
}

3. TCP 网络编程(面向连接可靠传输)

TCP 是面向连接的可靠传输协议,是互联网中最常用的传输层协议。net 包通过 TCPListener 和 TCPConn 来实现 TCP 通信。

3.1 基础 TCP 服务端

TCP 服务端的工作流程是:监听端口 -> 接受连接 -> 处理连接。Go 中通过 goroutine 可以轻松实现高并发的服务端。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
package main

import (
	"bufio"     // 提供了缓冲读取和写入功能,适用于网络连接的读写操作,可以按行读取数据。
	"fmt"
	"net"
	"log"
)

func handleConnection(conn *net.TCPConn) {
	// 确保连接关闭
	defer conn.Close()
	log.Printf("客户端 %s 已连接", conn.RemoteAddr())

	// 读取客户端数据
	reader := bufio.NewReader(conn)
	for {
		// 按行读取数据
		data, err := reader.ReadString('\n')
		if err != nil {
			log.Printf("客户端 %s 已断开", conn.RemoteAddr())
			return
		}

		log.Printf("收到来自 %s 的消息: %s", conn.RemoteAddr(), data)
		// 回显消息
		_, err = conn.Write([]byte("Echo: " + data))
		if err != nil {
			log.Printf("发送数据失败: %v", err)
			return
		}
	}
}

func main() {
	// 解析监听地址
	addr, err := net.ResolveTCPAddr("tcp", ":8080") // 监听8080端口
	if err != nil {
		log.Println(err.Error())
		return
	}

	// 监听端口
	listener, err := net.ListenTCP("tcp", addr)
	if err != nil {
		log.Println(err.Error())
		return
	}
	defer listener.Close()
	log.Println("TCP服务端已启动,监听 :8080")

	for {
		// 接受新连接
		conn, err := listener.AcceptTCP()
		if err != nil {
			log.Printf("接受连接失败: %v", err)
			continue
		}

		// 启动goroutine处理连接,实现并发
		go handleConnection(conn)
	}
}

3.2 基础 TCP 客户端

TCP 客户端通过主动连接服务端来建立连接,然后进行数据读写。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package main

import (
	"bufio"
	"fmt"
	"net"
	"os"
	"log"
)

func main() {
	// 连接服务端,中间这个nil表示本地地址,通常设置为nil让系统自动分配一个可用的端口。
	conn, err := net.DialTCP("tcp", nil, &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 8080})
	if err != nil {
		log.Println(err.Error())
		return
	}
	defer conn.Close()

	log.Println("已连接到服务端")

	// 从标准输入读取消息
	scanner := bufio.NewScanner(os.Stdin)   // 从标准输入读取用户输入,os.Stdin表示标准输入设备,通常是键盘。
	writer := bufio.NewWriter(conn)         
	// 创建一个缓冲写入器,用于向TCP连接写数据,它会将数据先写入内存缓冲区,只有在调用Flush方法时才会真正发送到网络上,这样可以提高效率,减少系统调用次数。
	// conn是之前建立的TCP连接,bufio.NewWriter(conn)创建了一个新的缓冲写入器,将TCP连接作为底层的写入目标。

	for scanner.Scan() {
		text := scanner.Text()
		// 发送消息
		_, err = writer.WriteString(text + "\n")
		if err != nil {
			log.Println(err.Error())
		return
		}
		writer.Flush()

		// 读取响应
		resp, err := bufio.NewReader(conn).ReadString('\n') // 以换行符为分隔符读取服务端响应,直到遇到换行符为止。
		if err != nil {
			log.Println(err.Error())
		return
		}
		fmt.Println("服务端响应:", resp)
	}
}

如此即可实现一个简单的 TCP 客户端,能够向服务端发送消息并接收响应。

3.3 TCP 连接高级配置

TCPConn 提供了一系列方法来调整 TCP 协议的参数,以适配不同的业务场景。

3.3.1 禁用 Nagle 算法(SetNoDelay)

Nagle 算法是 TCP 协议中的一个优化机制,它会将小数据包合并成一个大数据包发送,以减少网络开销。但在某些实时性要求高的场景下(如在线游戏、即时通信),这种合并会引入不必要的延迟,因此需要禁用 Nagle 算法。

Nagle 算法默认会合并小数据包以减少网络开销,但会引入延迟。对于实时性要求高的场景(如游戏、即时通信),需要禁用它。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 建立连接后
conn, err := net.DialTCP("tcp", nil, serverAddr)
if err != nil {
	log.Println(err.Error())
		return
}

// 禁用Nagle算法,小数据包立即发送
err = conn.SetNoDelay(true)
if err != nil {
	log.Printf("设置NoDelay失败: %v", err)
}

3.3.2 TCP 保活机制(SetKeepAlive)

TCP 保活机制可以自动检测死连接,避免长时间占用资源。Go 1.13+ 还支持更精细的保活配置。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 启用保活机制
err = conn.SetKeepAlive(true)
if err != nil {
	log.Printf("启用保活失败: %v", err)
}

// 方式1:简单设置保活间隔(旧API)
err = conn.SetKeepAlivePeriod(30 * time.Second)
if err != nil {
	log.Printf("设置保活间隔失败: %v", err)
}

// 方式2:精细配置保活参数(Go 1.13+ 新API)
err = conn.SetKeepAliveConfig(net.KeepAliveConfig{
	Enable:   true,
	Idle:     30 * time.Second,  // 空闲30秒后开始发送探针
	Interval: 10 * time.Second,  // 探针间隔10秒
	Count:    3,                 // 连续3次失败则断开连接
})
if err != nil {
	log.Printf("设置保活配置失败: %v", err)
}

3.3.3 连接半关闭

TCP 支持半关闭,即可以单独关闭读或写通道,这在某些场景下非常有用(比如发送完数据后告诉服务端不再发送,但还能接收响应)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 发送完所有数据后,关闭写通道
err = conn.CloseWrite()
if err != nil {
	log.Printf("关闭写通道失败: %v", err)
}

// 此时还可以继续读取服务端的响应
data, err := conn.Read(buf)

// 读取完后,关闭读通道
err = conn.CloseRead()

4. UDP 网络编程(无连接数据报传输)

UDP 是无连接的传输协议,不需要建立连接,直接发送数据包,适用于对实时性要求高、可容忍少量丢包的场景(如音视频、心跳检测)。

4.1 基础 UDP 服务端

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package main

import (
	"fmt"
	"net"
	"log"
)

func main() {
	// 解析UDP地址
	addr, err := net.ResolveUDPAddr("udp", ":8080")
	if err != nil {
		log.Println(err.Error())
		return
	}

	// 监听UDP端口
	conn, err := net.ListenUDP("udp", addr)
	if err != nil {
		log.Println(err.Error())
		return
	}
	defer conn.Close()
	log.Println("UDP服务端已启动,监听 :8080")

	buf := make([]byte, 1024)
	for {
		// 读取UDP数据包,同时获取客户端地址
		n, clientAddr, err := conn.ReadFromUDP(buf)
		if err != nil {
			log.Printf("读取数据失败: %v", err)
			continue
		}

		data := string(buf[:n])
		log.Printf("收到来自 %s 的消息: %s", clientAddr, data)

		// 向客户端发送响应
		resp := []byte("UDP Ack: " + data)
		_, err = conn.WriteToUDP(resp, clientAddr)
		if err != nil {
			log.Printf("发送响应失败: %v", err)
		}
	}
}

4.2 基础 UDP 客户端

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package main

import (
	"fmt"
	"net"
	"log"
)

func main() {
	// 服务端地址
	serverAddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:8080")
	if err != nil {
		log.Println(err.Error())
		return
	}

	// 建立UDP连接(UDP的连接只是逻辑绑定,不发送网络包),中间这个nil表示本地地址,通常设置为nil让系统自动分配一个可用的端口。
	conn, err := net.DialUDP("udp", nil, serverAddr)
	if err != nil {
		log.Println(err.Error())
		return
	}
	defer conn.Close()

	// 发送数据
	_, err = conn.Write([]byte("Hello UDP Server!"))
	if err != nil {
		log.Println(err.Error())
		return
	}

	// 读取响应
	buf := make([]byte, 1024)
	n, err := conn.Read(buf)
	if err != nil {
		log.Println(err.Error())
		return
	}

	fmt.Println("服务端响应:", string(buf[:n]))
}

如此即可实现一个简单的 UDP 客户端,能够向服务端发送消息并接收响应。

4.3 实战:UDP 心跳检测

UDP 非常适合做心跳检测,因为它轻量、无连接开销。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package main

import (
	"net"
	"time"
	"log"
)

func main() {
	serverAddr, _ := net.ResolveUDPAddr("udp", "127.0.0.1:8080")
	conn, _ := net.DialUDP("udp", nil, serverAddr)
	defer conn.Close()

	// 定时器,每秒发送一次心跳
	ticker := time.NewTicker(1 * time.Second)
	defer ticker.Stop()

	for range ticker.C {
		// 发送心跳包
		_, err := conn.Write([]byte("HEARTBEAT"))
		if err != nil {
			log.Printf("发送心跳失败: %v", err)
			continue
		}

		// 设置读取超时,检测服务端是否存活
		conn.SetReadDeadline(time.Now().Add(2 * time.Second))
		buf := make([]byte, 1024)
		n, err := conn.Read(buf)
		if err != nil {
			log.Printf("心跳检测失败,服务端可能已下线: %v", err)
			continue
		}

		log.Printf("心跳正常,服务端响应: %s", string(buf[:n]))
	}
}

如图所示,UDP 心跳检测可以快速判断服务端的存活状态,适用于分布式系统中的健康检查和服务发现。


5. DNS 域名解析

net 包提供了完整的 DNS 解析能力,从基础的域名转 IP,到高级的 MX、SRV、TXT 记录查询。

5.1 基础域名解析

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package main

import (
	"fmt"
	"net"
	"log"
)

func main() {
	// 解析域名对应的IP列表(同时返回IPv4和IPv6)
	ips, err := net.LookupIP("google.com")
	if err != nil {
		log.Println(err.Error())
		return
	}

	for _, ip := range ips {
		fmt.Println("IP地址:", ip.String())
	}

	// 反向解析:根据IP查找域名
	names, err := net.LookupAddr("8.8.8.8")
	if err != nil {
		log.Println(err.Error())
		return
	}
	for _, name := range names {
		fmt.Println("8.8.8.8对应的域名:", name)
	}
}

解析结果如下图所示:

5.2 高级 DNS 记录查询

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package main

import (
	"fmt"
	"net"
	"log"
)

func main() {
	// 1. 查询MX记录(邮件服务器记录)。MX记录是Mail Exchanger Record的缩写,邮件交换记录,用于指定处理该域邮件的服务器地址和优先级。
	mxs, err := net.LookupMX("gmail.com")
	if err != nil {
		log.Println(err.Error())
		return
	}
	fmt.Println("Gmail的邮件服务器:")
	for _, mx := range mxs {
		fmt.Printf("  优先级: %d, 服务器: %s\n", mx.Pref, mx.Host)
	}

	// 2. 查询SRV记录(服务发现记录,常用于微服务)。SRV记录是Service Record的缩写,用于指定提供特定服务的服务器地址、端口和优先级,常用于微服务架构中的服务发现。
	cname, srvs, err := net.LookupSRV("xmpp-server", "tcp", "gmail.com")
	if err != nil {
		log.Println(err.Error())
		return
	}
	fmt.Println("\nXMPP服务的SRV记录:")
	fmt.Println("CNAME:", cname)
	for _, srv := range srvs {
		fmt.Printf("  目标: %s:%d, 优先级: %d, 权重: %d\n", srv.Target, srv.Port, srv.Priority, srv.Weight)
	}

	// 3. 查询TXT记录(常用于验证域名、SPF、DKIM等)。TXT记录是Text Record的缩写,用于存储任意文本信息,常用于域名验证、SPF(Sender Policy Framework)和DKIM(DomainKeys Identified Mail)等邮件安全机制。
	txts, err := net.LookupTXT("gmail.com")
	if err != nil {
		log.Println(err.Error())
		return
	}
	fmt.Println("\nTXT记录:")
	for _, txt := range txts {
		fmt.Println("  ", txt)
	}
}

查询结果如下图所示:

5.3 自定义 DNS 解析器

默认情况下,net 包使用系统的 DNS 配置,但我们可以自定义 DNS 服务器,比如使用 Google DNS 或 Cloudflare DNS。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package main

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

func main() {
	// 自定义DNS解析器,使用Google的8.8.8.8
	resolver := &net.Resolver{
		// 强制使用Go原生的DNS解析器,不使用系统的cgo解析
		PreferGo: true,
		// 自定义DNS服务器的连接
		Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
			d := net.Dialer{
				Timeout: 3 * time.Second,
			}
			// 连接到自定义的DNS服务器
			return d.DialContext(ctx, "udp", "8.8.8.8:53")
		},
	}

	// 使用自定义解析器解析域名
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	ips, err := resolver.LookupIP(ctx, "ip4", "google.com")
	if err != nil {
		log.Println(err.Error())
		return
	}

	for _, ip := range ips {
		fmt.Println("通过8.8.8.8解析到的IP:", ip.String())
	}
}

解析结果如下图所示:


6. 错误处理与超时控制

网络编程中,错误处理和超时控制是保证服务稳定性的关键。

6.1 网络错误类型(net.Error)

net 包的所有网络错误都实现了 net.Error 接口,它提供了 Timeout() 和 Temporary() 方法来区分错误类型。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import (
	"fmt"
	"net"
	"time"
)

func main() {
    // 这里用google.com:80作为测试地址,设置一个非常短的超时时间来触发错误
	conn, err := net.DialTimeout("tcp", "google.com:80", 1*time.Second)
	if err != nil {
		// 判断错误是否为网络错误
		if ne, ok := err.(net.Error); ok {
			fmt.Println("是否为超时错误:", ne.Timeout())
			fmt.Println("是否为临时错误:", ne.Temporary())
		}
		fmt.Println("错误信息:", err)
		return
	}
	defer conn.Close()
	fmt.Println("连接成功")
}

之所以 Timeout() 和 Temporary() 都返回 true,是因为在这个场景下,连接超时被认为是一个临时错误,可能是网络状况不佳导致的,重试可能会成功。

6.2 超时控制详解

net 包提供了多层级的超时控制:

1. 连接超时

在建立连接时设置超时,避免长时间卡在连接阶段。

1
2
3
4
5
6
7
8
// 方式1:使用DialTimeout
conn, err := net.DialTimeout("tcp", "google.com:80", 5*time.Second)

// 方式2:使用Dialer+Context
d := net.Dialer{Timeout: 5*time.Second}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
conn, err := d.DialContext(ctx, "tcp", "google.com:80")

2. 读写超时

连接建立后,为读写操作设置超时,避免单个读写操作阻塞。

1
2
3
4
5
6
7
8
// 统一设置读写超时
conn.SetDeadline(time.Now().Add(30 * time.Second))

// 单独设置读超时
conn.SetReadDeadline(time.Now().Add(10 * time.Second))

// 单独设置写超时
conn.SetWriteDeadline(time.Now().Add(10 * time.Second))

7. 高级连接配置:Dialer

Dialer 是 net 包中用于配置连接的核心结构体,它可以统一配置连接的超时、保活、DNS 解析器等参数。

7.1 Dialer 核心配置

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package main

import (
	"context"
	"fmt"
	"log"
	"net"
	"time"
)

func main() {
	// 配置Dialer
	dialer := &net.Dialer{
		// 连接超时:如果在指定时间内无法建立连接,则返回错误
		Timeout:   5 * time.Second,
		// 保活间隔:每隔30秒发送一次TCP保活探针
		KeepAlive: 30 * time.Second,
		// 本地地址,用于指定出站网卡
		LocalAddr: nil,
		// 自定义DNS解析器
		Resolver: &net.Resolver{
            // 强制使用Go原生的DNS解析器,不使用系统的cgo解析
			PreferGo: true,
            // 自定义DNS服务器的连接
			Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
				return net.Dial("udp", "8.8.8.8:53")
			},
		},
	}

	// 使用Dialer建立连接
	conn, err := dialer.DialContext(context.Background(), "tcp", "google.com:80")
	if err != nil {
		log.Println(err.Error())
		return
	}
	defer conn.Close()

	fmt.Println("连接成功")
}

如果连不上会得到以下输出:

如果连接成功会得到以下输出:


8. Unix 域套接字(本地进程间通信)

Unix 域套接字是用于同一主机上进程间通信的机制,比 TCP 更快,不需要经过网络协议栈,延迟更低。

8.1 流式 Unix 域套接字(类似 TCP)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 服务端
package main

import (
	"net"
	"os"
	"log"
)

func main() {
	sockPath := "/tmp/test.sock"
	// 清理旧的sock文件
	os.Remove(sockPath)

	// 监听Unix域套接字
	listener, err := net.Listen("unix", sockPath)
	if err != nil {
		log.Println(err.Error())
		return
	}
	defer listener.Close()
	defer os.Remove(sockPath)

	log.Println("Unix服务端已启动")

	for {
		conn, err := listener.Accept()
		if err != nil {
			continue
		}
		go func(c net.Conn) {
			defer c.Close()
			// 处理连接,和TCP完全一样
			buf := make([]byte, 1024)
			n, _ := c.Read(buf)
			c.Write([]byte("Echo: " + string(buf[:n])))
		}(conn)
	}
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 客户端
package main

import (
	"fmt"
	"net"
)

func main() {
	// 连接Unix域套接字
	conn, err := net.Dial("unix", "/tmp/test.sock")
	if err != nil {
		log.Println(err.Error())
		return
	}
	defer conn.Close()

	conn.Write([]byte("Hello IPC!"))
	buf := make([]byte, 1024)
	n, _ := conn.Read(buf)
	fmt.Println(string(buf[:n]))
}

9. 原始 IP 与 ICMP:实现 Ping 工具

通过 net 包的原始 IP 连接,我们可以实现 ICMP 协议,也就是常用的 ping 工具。

注意:ICMP 原始套接字在部分系统上需要 root 权限。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
package main

import (
	"fmt"
	"net"
	"time"
)

// 计算ICMP校验和
func checkSum(msg []byte) uint16 {
	sum := 0
	for i := 0; i < len(msg)-1; i += 2 {
		sum += int(msg[i])*256 + int(msg[i+1])
	}
	sum = (sum >> 16) + (sum & 0xffff)
	sum += sum >> 16
	return uint16(^sum)
}

func ping(host string) error {
	// 建立ICMP连接
	conn, err := net.Dial("ip4:icmp", host)
	if err != nil {
		return err
	}
	defer conn.Close()

	// 构造ICMP Echo请求包
	msg := make([]byte, 64)
	msg[0] = 8 // Type: Echo Request
	msg[1] = 0 // Code: 0
	// Checksum占位
	msg[2] = 0
	msg[3] = 0
	// Identifier
	id := uint16(1234)
	msg[4] = byte(id >> 8)
	msg[5] = byte(id & 0xff)
	// Sequence
	seq := uint16(1)
	msg[6] = byte(seq >> 8)
	msg[7] = byte(seq & 0xff)

	// 计算校验和
	sum := checkSum(msg)
	msg[2] = byte(sum >> 8)
	msg[3] = byte(sum & 0xff)

	start := time.Now()
	// 发送ICMP包
	_, err = conn.Write(msg)
	if err != nil {
		return err
	}

	// 设置超时
	conn.SetReadDeadline(time.Now().Add(2 * time.Second))
	recv := make([]byte, 1024)
	n, err := conn.Read(recv)
	if err != nil {
		return err
	}

	duration := time.Since(start)
	// 解析响应,ICMP响应的Type为0
	if recv[20] == 0 { // 跳过20字节IP头
		fmt.Printf("%s is up, 延迟: %v\n", host, duration)
		return nil
	}
	return fmt.Errorf("收到非预期的响应")
}

func main() {
	err := ping("8.8.8.8")
	if err != nil {
		fmt.Println("Ping失败:", err)
	}
}

10. 网络接口与本机信息获取

net 包还提供了获取本机网卡、IP 信息的能力。

获取本机所有非回环 IP

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main

import (
	"fmt"
	"net"
)

func main() {
	// 获取所有网络接口的地址
	addrs, err := net.InterfaceAddrs()
	if err != nil {
		log.Println(err.Error())
		return
	}

	for _, addr := range addrs {
		// 转换为IPNet
		if ipNet, ok := addr.(*net.IPNet); ok {
			// 排除回环地址
			if !ipNet.IP.IsLoopback() {
				// 只打印IPv4地址
				if ipNet.IP.To4() != nil {
					fmt.Println("本机IP:", ipNet.IP.String())
				}
			}
		}
	}
}

11. 其他常用工具示例

11.1 简单端口扫描

通过 DialTimeout 我们可以快速实现一个简单的端口扫描工具,检测目标主机的端口是否开放。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package main

import (
	"fmt"
	"net"
	"sync"
	"time"
)

func main() {
	target := "localhost"
	var wg sync.WaitGroup

	// 扫描1-1024端口
	for port := 1; port <= 1024; port++ {
		wg.Add(1)
		go func(p int) {
			defer wg.Done()
			address := fmt.Sprintf("%s:%d", target, p)
			// 用500ms超时检测端口
			conn, err := net.DialTimeout("tcp", address, 500*time.Millisecond)
			if err == nil {
				fmt.Printf("端口 %d 开放\n", p)
				conn.Close()
			}
		}(port)
	}
	wg.Wait()
	fmt.Println("扫描完成")
}

11.2 内存管道 Pipe

Pipe 函数可以创建一对内存中的连接,用于进程内的通信,它实现了 net.Conn 接口,所以可以像网络连接一样进行读写。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package main

import (
	"fmt"
	"net"
)

func main() {
	// 创建内存管道
	r, w := net.Pipe()

	// 写入端goroutine
	go func() {
		defer w.Close()
		// 写入数据
		_, err := w.Write([]byte("Hello from Pipe!"))
		if err != nil {
			log.Println(err.Error())
		return
		}
	}()

	// 读取端
	buf := make([]byte, 1024)
	n, err := r.Read(buf)
	if err != nil {
		log.Println(err.Error())
		return
	}

	fmt.Println("读取到的数据:", string(buf[:n]))
}

11.3 Go 1.26 新特性:Dialer 新方法

Go 1.26 为 Dialer 新增了针对特定协议的方法,比如 DialTCP、DialUDP,这些方法支持预解析的地址,兼顾了预解析的效率和 context 的可取消性。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package main

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

func main() {
	// 预解析地址,避免每次连接都解析
	raddr := netip.MustParseAddrPort("127.0.0.1:8080")

	dialer := &net.Dialer{
		Timeout: 5 * time.Second,
	}

	// 使用新的DialTCP方法,支持context
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	conn, err := dialer.DialTCP(ctx, "tcp", netip.AddrPort{}, raddr)
	if err != nil {
		log.Println(err.Error())
		return
	}
	defer conn.Close()

	fmt.Println("连接成功")
}

11.4 自定义 DNS 在 HTTP 客户端中的应用

我们可以将自定义的 DNS 解析器和 HTTP 客户端结合,让 HTTP 请求也使用自定义的 DNS 服务器。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
package main

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

func main() {
	// 自定义DNS解析器,使用Cloudflare的1.1.1.1
	resolver := &net.Resolver{
		PreferGo: true,
		Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
			d := net.Dialer{Timeout: 3 * time.Second}
			return d.DialContext(ctx, "udp", "1.1.1.1:53")
		},
	}

	// 自定义Transport的DialContext
	transport := &http.Transport{
		DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
			// 拆分主机和端口
			host, port, err := net.SplitHostPort(addr)
			if err != nil {
				return nil, err
			}

			// 用自定义解析器解析主机
			ips, err := resolver.LookupHost(ctx, host)
			if err != nil {
				return nil, err
			}

			// 逐个尝试连接解析到的IP
			for _, ip := range ips {
				dialer := &net.Dialer{Timeout: 5 * time.Second}
				conn, err := dialer.DialContext(ctx, network, net.JoinHostPort(ip, port))
				if err == nil {
					return conn, nil
				}
			}
			return nil, fmt.Errorf("所有IP连接失败")
		},
	}

	// 创建使用自定义DNS的HTTP客户端
	client := &http.Client{
		Transport: transport,
		Timeout:   10 * time.Second,
	}

	// 发送请求
	resp, err := client.Get("https://example.com")
	if err != nil {
		log.Println(err.Error())
		return
	}
	defer resp.Body.Close()

	body, _ := ioutil.ReadAll(resp.Body)
	fmt.Println("响应状态:", resp.Status)
	fmt.Println("响应长度:", len(body))
}

12. 避坑指南

  1. 始终处理错误:网络操作的错误必须处理,不能忽略,尤其是 Accept、Read、Write 这些核心操作。

  2. 资源释放:使用 defer conn.Close() 确保连接关闭,避免文件描述符泄漏。

  3. 超时控制:所有网络操作都要设置超时,避免阻塞导致服务不可用。

  4. 并发处理:TCP 服务端必须用 goroutine 处理每个连接,才能实现高并发。

  5. 参数调优:

    • 实时场景:禁用 Nagle 算法(SetNoDelay(true))

    • 长连接场景:启用 TCP 保活,快速检测死连接

    • 批量传输:保持 Nagle 算法,增大读写缓冲区,提升吞吐量

  6. DNS 缓存:默认的 DNS 解析没有缓存,高并发场景下建议自己实现 DNS 缓存,避免频繁解析。

  7. Unix 域套接字:本地进程间通信优先使用 Unix 域套接字,比 TCP 性能更好。


通过以上内容,你已经基本掌握了 Go net 包的核心用法,从基础的 TCP/UDP 通信,到高级的 DNS 解析、ICMP 实现,都可以基于此进行开发。无论是构建简单的网络工具,还是开发复杂的分布式系统,net 包都是你不可或缺的利器。希望这份文档能帮助你更好地理解和使用 Go 的网络编程能力!