学习go语言编程之网络编程

Socket编程

Golang语言标准库对Socket编程进行了抽象,无论使用什么协议建立什么形式的连接,都只需要调用net.Dial()即可。

Dial()函数

Dial()函数的原型如下:

代码语言:javascript
复制
func Dial(network, address string) (Conn, error)

参数含义如下:

  • network:网络协议名字,如:tcp,udp等 Dial()函数支持的网络协议有:tcp,tcp4(仅限TPv4),tcp6(仅限IPv6),udp,udp4(仅限IPv4),udp6(仅限IPv6),ip,ip4(仅限IPv4),ip6(仅限IPv6)
  • address:IP地址或域名,端口号以":"的形式跟随在地址或域名的后面,端口号可选,如:localhost:8080,127.0.0.1:8080等

如下是几种常见协议的调用方式: TCP连接:

代码语言:javascript
复制
conn, err := net.Dial("tcp", "127.0.0.1:8080")

UDP连接:

代码语言:javascript
复制
conn, err := net.Dial("udp", "127.0.0.1:9000")

ICMP连接:

代码语言:javascript
复制
// 使用协议名称
conn, err := net.Dial("ip4:icmp", "www.baidu.com")
// 使用协议编号
// 查看协议编号:https://www.iana.org/assignments/protocol-numbers/protocol-numbers.xml
conn, err := net.Dial("ip4:1", "10.0.0.3")

在成功建立连接后,就可以进行数据的发送和接收。接收数据时使用Read()方法,发送数据时使用Write()方法。

ICMP示例程序

使用ICMP协议向在线主机发送一个问候,并等待主机返回。

代码语言:javascript
复制
func checkErr(err error) {
	if err != nil {
		fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
		os.Exit(1)
	}
}

func checkSum(msg []byte) uint16 {
sum := 0
for n := 0; n < len(msg)-1; n += 2 {
sum += int(msg[n])*256 + int(msg[n+1])
}
sum = (sum >> 16) + (sum & 0xffff)
sum += sum >> 16
var answer = uint16(^sum)
return answer
}

func main() {
if len(os.Args) != 2 {
fmt.Println("Usage: ", os.Args[0], "host")
os.Exit(1)
}

service := os.Args[1]
conn, err := net.Dial(&#34;ip:icmp&#34;, service)
defer conn.Close()
checkErr(err)

var msg [512]byte
msg[0] = 8  // echo
msg[1] = 0  // code 0
msg[2] = 0  // checksum
msg[3] = 0  // checksum
msg[4] = 0  // identifier[0]
msg[5] = 13 // identifier[13]
msg[6] = 0  // sequence[0]
msg[7] = 37 // sequence[1]

len := 8
check := checkSum(msg[:len])
msg[2] = byte(check &gt;&gt; 8)
msg[3] = byte(check &amp; 255)

_, err = conn.Write(msg[0:len])
checkErr(err)

_, err = conn.Read(msg[0:])
checkErr(err)

fmt.Println(&#34;Got response&#34;)
if msg[5] == 13 {
	fmt.Println(&#34;Identifier matches&#34;)
}
if msg[7] == 37 {
	fmt.Println(&#34;Sequence matches&#34;)
}
os.Exit(0)

}

127.0.0.1作为目标,输出:Got response

TCP示例程序

建立TCP连接来实现初步的HTTP协议,通过向网络主机发送HTTP Head请求,读取网络主机返回信息。

代码语言:javascript
复制
func checkErr(err error) {
if err != nil {
fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
os.Exit(1)
}
}

func readFully(conn net.Conn) ([]byte, error) {
defer conn.Close()
result := bytes.NewBuffer(nil)
var buf [512]byte
for {
n, err := conn.Read(buf[0:])
result.Write(buf[0:n])
if err != nil {
if err == io.EOF {
break
}
return nil, err
}
}
return result.Bytes(), nil
}

func main() {
if len(os.Args) != 2 {
fmt.Fprintf(os.Stderr, "Usage: %s host:port", os.Args[0])
os.Exit(1)
}

service := os.Args[1]

conn, err := net.Dial(&#34;tcp&#34;, service)
checkErr(err)

_, err = conn.Write([]byte(&#34;HEAD / HTTP/1.0\r\n\r\n&#34;))
checkErr(err)

result, err := readFully(conn)
checkErr(err)

fmt.Println(string(result))
os.Exit(0)

}

baidu.com:80作为参数,输出:

代码语言:javascript
复制
HTTP/1.1 200 OK
Date: Mon, 14 Aug 2023 08:40:35 GMT
Server: Apache
Last-Modified: Tue, 12 Jan 2010 13:48:00 GMT
ETag: "51-47cf7e6ee8400"
Accept-Ranges: bytes
Content-Length: 81
Cache-Control: max-age=86400
Expires: Tue, 15 Aug 2023 08:40:35 GMT
Connection: Close
Content-Type: text/html

更丰富的网络通信

Dial()函数是对DialTCP()DialUDP()DialIP()DialUnix()的封装,可以直接调用这些函数,它们的功能是一致的。
这些函数的原型如下:

代码语言:javascript
复制
func DialTCP(network string, laddr, raddr *TCPAddr) (*TCPConn, error)
func DialUDP(network string, laddr, raddr *UDPAddr) (*UDPConn, error)
func DialIP(network string, laddr, raddr *IPAddr) (*IPConn, error)
func DialUnix(network string, laddr, raddr *UnixAddr) (*UnixConn, error)

直接使用DialTCP()函数实现一个初步的HTTP协议访问。

代码语言:javascript
复制
func checkErr(err error) {
if err != nil {
fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
os.Exit(1)
}
}

func main() {
if len(os.Args) != 2 {
fmt.Fprintf(os.Stderr, "Usage: %s host:port", os.Args[0])
os.Exit(1)
}

service := os.Args[1]
tcpAddr, err := net.ResolveTCPAddr(&#34;tcp4&#34;, service)
checkErr(err)

conn, err := net.DialTCP(&#34;tcp&#34;, nil, tcpAddr)
checkErr(err)

_, err = conn.Write([]byte(&#34;HEAD / HTTP/1.0\r\n\r\n&#34;))
checkErr(err)

result, err := io.ReadAll(conn)
checkErr(err)

fmt.Println(string(result))
os.Exit(1)

}

baidu.com:80作为参数,输出:

代码语言:javascript
复制
HTTP/1.1 200 OK
Date: Mon, 14 Aug 2023 09:02:10 GMT
Server: Apache
Last-Modified: Tue, 12 Jan 2010 13:48:00 GMT
ETag: "51-47cf7e6ee8400"
Accept-Ranges: bytes
Content-Length: 81
Cache-Control: max-age=86400
Expires: Tue, 15 Aug 2023 09:02:10 GMT
Connection: Close
Content-Type: text/html

与之前的Dial()例子相比,这里有2个不同:

  • 使用net.ResolveTCPAddr解析地址和端口
  • 使用DialTCP()函数建立连接

此外,net包中还包含了一系列工具函数。

  • func ParseIP(s string) IP:验证IP的有效性
  • func IPv4Mask(a, b, c, d byte) IPMask:创建子网掩码
  • func (ip IP) DefaultMask() IPMask :获取默认子网掩码
  • func ResolveIPAddr(network, address string) (*IPAddr, error)func LookupHost(host string) (addrs []string, err error):根据域名查找IP

HTTP编程

Golang语言标准库内建了net/http包,涵盖了HTTP客户端和服务端的具体实现。

HTTP客户端

基本方法

net/http包的Client类型提供了如下几个方法:

代码语言:javascript
复制
func (c *Client) Get(url string) (resp *Response, err error)
func (c *Client) Post(url, contentType string, body io.Reader) (resp *Response, err error)
func PostForm(url string, data url.Values) (resp *Response, err error)
func (c *Client) Head(url string) (resp *Response, err error)
func (c *Client) Do(req *Request) (*Response, error)
  • http.Get():请求资源
代码语言:javascript
复制
resp, err := http.Get("https://www.baidu.com")
if err != nil {
// 处理错误
return
}
defer resp.Body.Close()
io.Copy(os.Stdout, resp.Body)
  • http.Post():上传数据
代码语言:javascript
复制
resp, err := http.Post("https://example.com/upload", "image/jpeg", &net.Buffers{})
if err != nil {
// 处理错误
}
if resp.StatusCode != http.StatusOK {
// 处理错误
}

-http.PostForm():提交表单

代码语言:javascript
复制
resp, err := http.PostForm("https://example.com/posts", url.Values{"title": {"Article title"}, "content": {"article body"}})
if err != nil {
// 处理错误
}
if resp.StatusCode != http.StatusOK {
// 处理错误
}

-http.Head():请求头部信息

代码语言:javascript
复制
resp, err := http.Head("https://example.com")
if err != nil {
// 处理错误
}
if resp.StatusCode != http.StatusOK {
// 处理错误
}

-client.Do():定制HTTP请求,比如设置请求头参数,传递Cookie等

代码语言:javascript
复制
req, _ := http.NewRequest("GET", "https://example.com", nil)
req.Header.Add("User-Agent", "Custom User-Agent")
client := http.Client{}
resp, _ := client.Do(req)
if resp.StatusCode != http.StatusOK {
// 处理错误
}
高级封装

Golang语言标准库暴露了比较底层的HTTP相关库,让开发者可以基于这些库灵活定制HTTP服务器和使用HTTP服务。

  • 自定义http.Client

在Golang标准库中,http.Client类型包含了3个公开数据成员:

代码语言:javascript
复制
type Client struct {
Transport RoundTripper
CheckRedirect func(req *Request, via []*Request) error
Jar CookieJar
}

其中:
1.Transport类型必须实现http.RoundTripper接口,Transport指定了执行一个HTTP请求的运行机制,倘若不指定具体的Transport,默认会使用http.DefaultTransport,这意味着http.Transport也是可以自定义的。
2.CheckRedirect函数指定处理重定向的策略,若响应返回的状态码为30x,HTTP Client会在遵循跳转规则之前先调用这个CheckRedirect函数。
3.Jar可用于在HTTP Client中设定Cookie,Jar的类型必须实现了http.CookieJar接口,该接口预定义了SetCookies()Cookies()两个方法。如果HTTP Client中没有设定Jar,Cookie将被忽略而不会发送到客户端。实际上,我们一般都用http.SetCookie()方法来设定Cookie。

使用自定义的http.Client及其Do()方法,可以非常灵活地控制HTTP请求,比如发送自定义HTTP Header或是改写重定向策略等。

代码语言:javascript
复制
// 自定义重定向策略函数
redirectPolicyFunc := func(req *http.Request, via []*http.Request) error {
// 执行操作
return nil
}
client := &http.Client{
CheckRedirect: redirectPolicyFunc,
}

req, _ := http.NewRequest("GET", "https://example.com", nil)
req.Header.Add("User-Agent", "Custom User Agent")
req.Header.Add("If-None-Match", "W/TheFileEtag")
resp, _ := client.Do(req)
if resp.StatusCode != http.StatusOK {
// 处理错误
}

  • 自定义http.Transport

http.Client类型的第一个成员是一个http.Transport对象,该对象指定执行一个HTTP请求时的运行规则。
如下是http.Transport类型的结构:

代码语言:javascript
复制
type Transport struct {
Proxy func(*Request) (*url.URL, error)
Dial func(network, addr string) (net.Conn, error)
TLSClientConfig *tls.Config
DisableKeepAlives bool
DisableCompression bool
MaxIdleConnsPerHost int
}

其中:
1.Proxy func(*Request) (*url.URL, error):Proxy指定了一个代理方法,该方法接受一个*Request类型的请求实例作为参数并返回一个最终的HTTP代理。如果Proxy未指定或者返回的*URL为零值,将不会有代理被启用。
2.Dial func(network, addr string) (net.Conn, error):Dial指定具体的dial()方法来创建TCP连接,如果不指定,默认将使用net.Dial()方法。
3.TLSClientConfig *tls.Config:SSL连接专用,TLSClientConfig指定tls.Client所用的TLS配置信息,如果不指定,也会使用默认的配置。
4.DisableKeepAlives bool:是否取消长连接,默认值为false,即启用长连接。
5.DisableCompression bool:是否取消压缩(GZip),默认值为false,即启用压缩。
6.MaxIdleConnsPerHost int:指定与每个请求的目标主机之间的最大非活跃连接(keep-alive)数量。如果不指定,默认使用DefaultMaxIdleConnsPerHost的常量值。

除了http.Transport类型中定义的公开数据成员以外,它同时还提供了几个公开的成员方法。
1.func (t *Transport) CloseIdleConnections():该方法用于关闭所有非活跃的连接。
2.func (t *Transport) RegisterProtocol(scheme string, rt RoundTripper):该方法可用于注册并启用一个新的传输协议,比如WebSocket的传输协议标准(ws),或者FTP、File协议等。
3.func (t *Transport) RoundTrip(req *Request) (*Response, error):用于实现http.RoundTripper接口。

如下代码为自定义http.Transport

代码语言:javascript
复制
tr := &http.Transport{
TLSClientConfig: &tls.Config{RootCAs: &x509.CertPool{}},
DisableCompression: true,
}
client := &http.Client{Transport: tr}
resp, err := client.Get("https://example.com")
if err != nil {
// 处理错误
}
if resp.StatusCode != http.StatusOK {
// 处理错误
}

ClientTransport在执行多个goroutine的并发过程中都是安全的,但出于性能考虑,应当创建一次后反复使用。

HTTP服务端

处理HTTP请求

使用net/http包提供的http.ListenAndServe()方法,可以在指定的地址进行监听并开启一个HTTP服务端,该方法的原型如下:

代码语言:javascript
复制
func ListenAndServe(addr string, handler Handler) error

该方法用于在指定的TCP网络地址addr进行监听,然后调用服务端处理程序来处理传入的连接请求。
该方法有两个参数:第一个参数addr即监听地址;第二个参数表示服务端处理程序,通常为空,这意味着服务端调用http.DefaultServeMux进行处理,而服务端编写的业务逻辑处理程序http.Handle()http.HandleFunc()默认注入http.DefaultServeMux中。
示例代码如下:

代码语言:javascript
复制
type IndexHandler struct {
content string
}

func (handler *IndexHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, handler.content)
}

http.Handle("/foo", &IndexHandler{"Hello, World!"})
http.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
})
log.Fatal(http.ListenAndServe(":8080", nil))

如果希望更多地控制服务器行为,可以自定义http.Server

代码语言:javascript
复制
myHandler := &IndexHandler{"Hello, World!"}
s := &http.Server{
Addr: ":8080",
Handler: myHandler,
ReadTimeout: 10 * time.Second,
MaxHeaderBytes: 1 << 20,
}
log.Fatal(s.ListenAndServe())
处理HTTPS请求

net/http包还提供`http.ListenAndServeTLS() 方法,用于处理HTTPS连接请求:

代码语言:javascript
复制
func ListenAndServeTLS(addr, certFile, keyFile string, handler Handler) error

ListenAndServeTLS()ListenAndServe()的行为一致,区别在于只处理HTTPS请求。
此外,服务器上必须存在包含证书和与之匹配的私钥的相关文件,比如certFile对应SSL证书文件存放路径,keyFile对应证书私钥文件路径。
如果证书是由证书颁发机构签署的,certFile参数指定的路径必须是存放在服务器上的经由CA认证过的SSL证书。

代码语言:javascript
复制
myHandler := &IndexHandler{"Hello, World!"}
http.Handle("/foo", myHandler)
http.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
})
log.Fatal(http.ListenAndServeTLS(":443", "cert.pem", "key.pem", nil))

或者:

代码语言:javascript
复制
myHandler := &IndexHandler{"Hello, World!"}
http.Handle("/foo", myHandler)
http.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
})
// 对HTTP服务器进行定制化
s := &http.Server{
Addr: ":443",
Handler: myHandler,
ReadTimeout: 10 * time.Second,
MaxHeaderBytes: 1 << 20,
}
log.Fatal(s.ListenAndServeTLS("cert.pem", "key.pem"))

RPC编程

RPC(Remote Procedure Call,远程过程调用)是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络细节的应用程序通信协议。
RPC协议构建于TCP或UDP,或者是HTTP之上,允许开发者直接调用另一台计算机上的程序,而开发者无需额外地为这个调用过程编写网络通信相关代码,使得开发包括网络分布式程序在内的应用程序更加容易。RPC采用客户端—服务器(Client/Server)的工作模式。请求程序就是一个客户端(Client),而服务提供程序就是一个服务器(Server)。

Go语言中的RPC支持与处理

Golang标准库提供的net/rpc包实现了RPC协议需要的相关细节,开发者可以很方便地使用该包编写RPC的服务端和客户端程序,这使得用Go语言开发的多个进程之间的通信变得非常简单。
net/rpc包允许RPC客户端程序通过网络或是其他I/O连接调用一个远端对象的公开方法(必须是大写字母开头、可外部调用的)。在RPC服务端,可将一个对象注册为可访问的服务,之后该对象的公开方法就能够以远程的方式提供访问。
一个RPC服务端可以注册多个不同类型的对象,但不允许注册同一类型的多个对象。

一个对象中只有满足如下这些条件的方法,才能被RPC服务端设置为可供远程访问:
1.必须是在对象外部可公开调用的方法(首字母大写);
2.必须有两个参数,且参数的类型都必须是包外部可以访问的类型或者是Go内建支持的类型;
3.第二个参数必须是一个指针;
4.方法必须返回一个error类型的值。

以上4个条件,可以简单地用如下一行代码表示:

代码语言:javascript
复制
// 类型T、T1和T2默认会使用Go内置的encoding/gob包进行编码解码
func (t *T) MethodName(argType T1, replyType *T2) error

第一个参数表示由RPC客户端传入的参数,第二个参数表示要返回给RPC客户端的结果,该方法最后返回一个error类型的值。

RPC服务端可以通过调用rpc.ServeConn处理单个连接请求。多数情况下,通过TCP或是HTTP在某个网络地址上进行监听来创建该服务是个不错的选择。

在RPC客户端,Go的net/rpc包提供了便利的rpc.Dial()rpc.DialHTTP()方法来与指定的RPC服务端建立连接,在建立连接之后,Go的net/rpc包允许使用同步或者异步的方式接收RPC服务端的处理结果。调用RPC客户端的Call()方法则进行同步处理,这时候客户端程序按顺序执行,只有接收完RPC服务端的处理结果之后才可以继续执行后面的程序。当调用RPC客户端的Go()方法时,则可以进行异步处理,RPC客户端程序无需等待服务端的结果即可执行后面的程序,而当接收到RPC服务端的处理结果时,再对其进行相应的处理。

无论是调用RPC客户端的Call()或者是Go()方法,都必须指定要调用的服务及其方法名称,以及一个客户端传入参数的引用,还有一个用于接收处理结果参数的指针。

如果没有明确指定RPC传输过程中使用何种编码解码器,默认将使用Go标准库提供的encoding/gob包进行数据传输。
如下示例展示RPC服务端和客户端的交互:

  • RPC服务端
代码语言:javascript
复制
type Args struct {
A, B int
}

// 定义一个新的int类型,并为它添加1个成员方法
type Arith int

func (a *Arith) Multiply(args *Args, reply *int) error {
*reply = args.A * args.B
return nil
}

// 注册服务对象并开启RPC服务端
a := new(server.Arith)
rpc.Register(a)
rpc.HandleHTTP()
l, e := net.Listen("tcp", ":4321")
if e != nil {
log.Fatal("Listen error:", e)
return
}
go http.Serve(l, nil)

  • RPC客户端
代码语言:javascript
复制
// 建立连接
client, err := rpc.DialHTTP("tcp", "localhost:4321")
if err != nil {
log.Fatal("dialing err:", err)
return
}

// 请求参数
args := &Args{A: 15, B: 3}
// 返回值
var reply int

// 同步请求RPC服务端
err = client.Call("Arith.Multiply", args, &reply)
if err != nil {
log.Fatal("client.Call() failed:", err)
return
}
fmt.Printf("%d*%d=%d", args.A, args.B, reply) // 输出:15*3=45

// 异步请求RPC服务端
asyncCall := client.Go("Arith.Multiply", args, &reply, nil)
replyCall := <-asyncCall.Done
if replyCall != nil {
fmt.Printf("%d*%d=%d", args.A, args.B, reply) // 输出:15*3=45
}

Gob简介

Gob是Golang的一个序列化数据结构的编码解码工具,在Golang标准库中内置encoding/gob包以供使用。
一个数据结构使用Gob进行序列化之后,能够用于网络传输。
Gob是二进制编码的数据流,并且Gob流是可以自解释的,它在保证高效率的同时,也具备完整的表达能力。

作为针对Go的数据结构进行编码和解码的专用序列化方法,这意味着Gob无法跨语言使用。在Go的net/rpc包中,传输数据所需要用到的编码解码器,默认就是Gob。
由于Gob仅局限于使用Go语言开发的程序,这意味着我们只能用Go的RPC实现进程间通信。

设计优雅的RPC接口

Go的net/rpc很灵活,它在数据传输前后实现了编码解码器的接口定义,开发者可以自定义数据的传输方式以及RPC服务端和客户端之间的交互行为。
RPC提供的编码解码器接口如下:

代码语言:javascript
复制
type ClientCodec interface {
WriteRequest(*Request, any) error
ReadResponseHeader(*Response) error
ReadResponseBody(any) error
Close() error
}

type ServerCodec interface {
ReadRequestHeader(*Request) error
ReadRequestBody(any) error
WriteResponse(*Response, any) error
Close() error
}

接口rpc.ClientCodec定义了RPC客户端如何在一个RPC会话中发送请求和读取响应:通过WriteRequest()方法将一个请求写入到RPC连接中,并通过ReadResponseHeader()ReadResponseBody()读取服务端的响应信息。当整个过程执行完毕后,再通过Close()方法来关闭该连接。
接口rpc.ServerCodec定义了RPC服务端如何在一个RPC会话中接收请求并发送响应:通过ReadRequestHeader()ReadRequestBody()方法从一个RPC连接中读取请求信息,然后再通过WriteResponse()方法向该连接中的RPC客户端发送响应。当完成该过程后,通过Close()方法来关闭连接。

通过实现上述接口,可以自定义数据传输前后的编码解码方式,而不仅仅局限于Gob。同样,可以自定义RPC服务端和客户端的交互行为。Go标准库提供的net/rpc/json包,就是一套实现了rpc.ClientCodecrpc.ServerCodec接口的JSON-RPC模块。

JSON处理

编码为JSON格式

使用json.Marshal()函数可以对一组数据进行JSON格式的编码,该函数声明如下:

代码语言:javascript
复制
func Marshal(v any) ([]byte, error)

假设有一个如下类型的结构体:

代码语言:javascript
复制
type Book struct {
Title string
Authors []string
Publisher string
IsPublished bool
Price float32
}

并且存在对象:

代码语言:javascript
复制
book := &Book{"Go语言编程", []string{"XuShiwei", "HughLv", "Pandaman", "GuaguaSong", "HanTuo", "BertYuan", "XuDaoli"}, "ituring.com.cn", true, 9.9}

可以使用json.Marshal()函数将book实例生成一段JSON格式的文本:

代码语言:javascript
复制
b, err := json.Marshal(book)

如果编码成功,err将赋于零值nil,变量b将会是一个进行JSON格式化之后的[]byte类型。

Golang的大多数数据类型都可以转化为有效的JSON文本,但channelcomplex函数这几种类型除外。

如果转化前的数据结构中出现指针,那么将会转化指针所指向的值,如果指针指向的是零值,那么null将作为转化后的结果输出。

在Golang中,JSON转化前后的数据类型映射如下:

  • 布尔值转化为JSON后还是布尔类型
  • 浮点数和整型会被转化为JSON里边的常规数字
  • 字符串将以UTF-8编码转化输出为Unicode字符集的字符串,特殊字符比如<将会被转义为\u003c
  • 数组和切片会转化为JSON里边的数组,但[]byte类型的值将会被转化为Base64编码后的字符串,切片类型的零值会被转化为null
  • 结构体会转化为JSON对象,并且只有结构体里边以大写字母开头的可被导出的字段才会被转化输出,而这些可导出的字段会作为JSON对象的字符串索引
  • 转化一个map类型的数据结构时,该数据的类型必须是map[string]T(T可以是encoding/json包支持的任意数据类型)

解码JSON数据

可以使用json.Unmarshal()函数将JSON格式的文本解码为Go里边预期的数据结构,该函数的原型如下:

代码语言:javascript
复制
func Unmarshal(data []byte, v any) error

该函数的第一个参数是输入,即JSON格式的文本(比特序列[]byte),第二个参数表示目标输出容器,用于存放解码后的值。

要解码一段JSON数据,首先需要在Go中创建一个目标类型的实例对象,用于存放解码后的值。然后调用json.Unmarshal()函数,将[]byte类型的JSON数据作为第一个参数传入,将实例变量的指针作为第二个参数传入。
如下示例:

代码语言:javascript
复制
// 先编码,编码后变量是一个[]byte数组切片
book := &Book{"Go语言编程", []string{"XuShiwei", "HughLv", "Pandaman", "GuaguaSong", "HanTuo", "BertYuan", "XuDaoli"}, "ituring.com.cn", true, 9.9}
b, err := json.Marshal(book)
if err != nil {
return
}

// 再解码,解码函数的第二个参数以类型实例指针传递
// 解码后保存到book2变量中
var book2 Book
json.Unmarshal(b, &book2)
fmt.Println(book2)

解码未知结构的JSON数据

在Golang中,接口是一组预定义方法的组合,任何一个类型均可通过实现接口预定义的方法来实现,且无需显示声明,所以没有任何方法的空接口可以代表任何类型。换句话说,每一个类型其实都至少实现了一个空接口。
Golang内建这样灵活的类型系统,向我们传达了一个很有价值的信息:空接口是通用类型。

如果要解码一段未知结构的JSON,只需将这段JSON数据解码输出到一个空接口即可。
在解码JSON数据的过程中,JSON数据里边的元素类型将做如下转换:

  • JSON中的布尔值将会转换为Go中的bool类型
  • 数值会被转换为Go中的float64类型
  • 字符串转换后还是string类型
  • JSON数组会转换为[]interface{}类型
  • JSON对象会转换为map[string]interface{}类型
  • null值会转换为nil

在Golang的标准库encoding/json包中,允许使用map[string]interface{}[]interface{}类型的值来分别存放未知结构的JSON对象或数组。

代码语言:javascript
复制
// 先编码
book := &Book{"Go语言编程", []string{"XuShiwei", "HughLv", "Pandaman", "GuaguaSong", "HanTuo", "BertYuan", "XuDaoli"}, "ituring.com.cn", true, 9.9}
b, err := json.Marshal(book)
if err != nil {
return
}

// 再解码
// 使用interface{}来存放未知结构的JSON对象
var r interface{}
err = json.Unmarshal(b, &r)
fmt.Println(book)
fmt.Println(r)

输出:

代码语言:javascript
复制
&{Go语言编程 [XuShiwei HughLv Pandaman GuaguaSong HanTuo BertYuan XuDaoli] ituring.com.cn true 9.9}
map[Authors:[XuShiwei HughLv Pandaman GuaguaSong HanTuo BertYuan XuDaoli] IsPublished:true Price:9.9 Publisher:ituring.com.cn Title:Go语言编程]

如上述代码,r被定义为一个空接口,json.Unmarshal()函数将一个JSON对象解码到空接口r中,最终r将会是一个键值对的map[string]interface{}结构。

代码语言:javascript
复制
map[string]interface{}{
"Authors": ["XuShiwei", "HughLv", "Pandaman", "GuaguaSong", "HanTuo", "BertYuan", "XuDaoli"],
"IsPublished": true,
"Price": 9.99,
"Publisher": "ituring.com.cn",
"Title": "Go语言编程"
}

JSON的流式读写

Golang内建的encoding/json包还提供DecoderEncoder两个类型,用于支持JSON数据的流式读写,并提供NewDecoder()NewEncoder()两个函数来便于具体实现。

代码语言:javascript
复制
func NewEncoder(w io.Writer) *Encoder
func NewDecoder(r io.Reader) *Decoder

示例如下:

代码语言:javascript
复制
dec := json.NewDecoder(os.Stdin)  // 从标准输入中获取JSON数据
enc := json.NewEncoder(os.Stdout) // 将JSON数据重新输出到标准输出中

var v map[string]interface{}
if err := dec.Decode(&v); err != nil {
log.Println(err)
return
}
for k := range v {
if k != "Title" {
v[k] = nil
}
}
if err := enc.Encode(&v); err != nil {
log.Println(err)
}

如上代码从标准输入流中读取JSON数据,然后将其解码,但只保留Title字段,再写入到标准输出流中,具体的输入输出:

代码语言:javascript
复制
// 从标准输入获取JSON数据
{"Authors":["XuShiwei","HughLv","Pandaman","GuaguaSong","HanTuo","BertYuan","XuDaoli"],"IsPublished":true,"Price":9.9,"Publisher":"ituring.com.cn","Title":"Go语言编程"}
// 输出到标准输出的JSON数据
{"Authors":null,"IsPublished":null,"Price":null,"Publisher":null,"Title":"Go语言编程"}