Go语言学习笔记【17】 Go语言常见库:net/http、log

本文详细介绍了Go语言中net/http库的使用,包括HTTP协议、客户端和服务器的实现,并给出了GET、POST请求的示例。同时,探讨了log库的基本操作,包括标准日志记录和自定义配置。适合Go后端开发者学习参考。

【声明】

非完全原创,部分内容来自于学习其他人的理论。如果有侵权,请联系我,可以立即删除掉。

一、net/http

该基础库提供了HTTP客户端和服务端的实现。使用此包,可以很方便地编写HTTP客户端或服务端的程序

1、http协议

超文本传输协议(Hyper Text Transfer Protocol,HTTP)是一个简单的请求-响应协议,它通常运行在TCP之上。它指定了客户端可能发送给服务器什么样的消息以及得到什么样的响应。请求和响应消息的头以ASCII形式给出;

HTTP协议通常承载于TCP协议之上,有时也承载于TLS或SSL协议层之上,这个时候,就成了我们常说的HTTPS。
https

1.1、http请求报文

HTTP 请求报文由请求行、请求头部、空行、请求包体4个部分组成
在这里插入图片描述

1.1.1、请求行

请求行由方法字段、URL 字段 和HTTP 协议版本字段 3 个部分组成,他们之间使用空格隔开。常用的 HTTP 请求方法有 GET、POST

  • GET
    (1)当客户端要从服务器中读取某个资源时,使用GET 方法。GET 方法要求服务器将URL 定位的资源放在响应报文的数据部分,回送给客户端,即向服务器请求某个资源。
    (2)使用GET方法时,请求参数和对应的值附加在 URL 后面,利用一个问号(“?”)代表URL 的结尾与请求参数的开始,传递参数长度受限制,因此GET方法不适合用于上传数据
    (3)通过GET方法来获取网页时,参数会显示在浏览器地址栏上,因此保密性很差
    (4)GET类似于读取资源,反复读取不应该对访问的数据有副作用,没有副作用被称为“幂等”。对GET请求数据的缓存可以做到浏览器本身上(彻底避免浏览器发请求),也可以做到代理上(如nginx),或者做到server端(用Etag,至少可以减少带宽消耗)

  • POST
    (1)当客户端给服务器提供信息较多时可以使用POST 方法,POST 方法向服务器提交数据,比如完成表单数据的提交,将数据提交给服务器处理
    (2)GET 一般用于获取/查询资源信息,POST 会附带用户数据,一般用于更新资源信息。POST 方法将请求参数封装在HTTP 请求数据中,而且长度没有限制,因为POST携带的数据,在HTTP的请求正文中,以名称/值的形式出现,可以传输大量数据
    (3)POST请求的事往往有副作用、不幂等,即不能随意多次执行,因此也就不能缓存。如,POST下一个单,服务器创建了新的订单,然后返回订单成功的界面。这个页面不能被缓存

1.1.2、请求头部

请求头部为请求报文添加了一些附加信息,由“名/值”对组成,每行一对,名和值之间使用冒号分隔。
请求头部通知服务器有关于客户端请求的信息,典型的请求头有:

请求头含义
User-Agent请求的浏览器类型
Accept客户端可识别的响应内容类型列表,星号“ * ”用于按范围将类型分组,用“ / ”指示可接受全部类型,用“ type/* ”指示可接受 type 类型的所有子类型
Accept-Language客户端可接受的自然语言
Accept-Encoding客户端可接受的编码压缩格式
Accept-Charset可接受的应答的字符集
Host请求的主机名,允许多个域名同处一个IP 地址,即虚拟主机
connection连接方式(close或keepalive)
Cookie存储于客户端扩展字段,向同一域名的服务端发送属于该域的cookie
1.1.3、空行

最后一个请求头之后是一个空行,发送回车符和换行符,通知服务器以下不再有请求头。

1.1.4、请求包体

请求包体不在GET方法中使用,而是POST方法中使用。
POST方法适用于需要客户填写表单的场合。与请求包体相关的最常使用的是包体类型Content-Type和包体长度Content-Length

1.2、http响应报文

HTTP 响应报文由状态行、响应头部、空行、响应包体4个部分组成
http响应报文

1.2.1、状态行

状态行由 HTTP 协议版本字段、状态码和状态码的描述文本3个部分组成,他们之间使用空格隔开。

状态码:
状态码由三位数字组成,第一位数字表示响应的类型,常用的状态码有五大类

状态码含义
1xx表示服务器已接收了客户端请求,客户端可继续发送请求
2xx表示服务器已成功接收到请求并进行处理
3xx表示服务器要求客户端重定向
4xx表示客户端的请求有非法内容
5xx表示服务器未能正常处理客户端的请求而出现意外错误

常见的状态码举例:

状态码含义
200 OK客户端请求成功
400 Bad Request请求报文有语法错误
401 Unauthorized未授权
403 Forbidden服务器拒绝服务
404 Not Found请求的资源不存在
500 Internal Server Error服务器内部错误
503 Server Unavailable服务器临时不能处理客户端请求(稍后可能可以)
1.2.2、响应头部
响应头含义
LocationLocation响应报头域用于重定向接受者到一个新的位置
ServerServer 响应报头域包含了服务器用来处理请求的软件信息及其版本
Vary指示不可缓存的请求头列表
Connection连接方式
1.2.3、空行

最后一个响应头部之后是一个空行,发送回车符和换行符,通知服务器以下不再有响应头部。

1.2.4、响应包体

服务器返回给客户端的文本信息

2、http客户端

2.1、客户端的代码格式

2.1.1、使用http默认的参数

Get、Head、Post和PostForm函数发出HTTP/ HTTPS请求。程序在使用完回复后必须关闭回复的主体。

resp, err := http.Get("http://example.com/")
if err != nil {
	// handle error
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
// ...

...
resp, err := http.Post("http://example.com/upload", "image/jpeg", &buf)

...
resp, err := http.PostForm("http://example.com/form",
	url.Values{"key": {"Value"}, "id": {"123"}})
2.1.2、自定义参数

要管理HTTP客户端的头域、重定向策略和其他设置,创建一个Client:

client := &http.Client{
	CheckRedirect: redirectPolicyFunc,
}
resp, err := client.Get("http://example.com")
// ...
req, err := http.NewRequest("GET", "http://example.com", nil)
// ...
req.Header.Add("If-None-Match", `W/"wyzzy"`)
resp, err := client.Do(req)
// ...

要管理代理、TLS配置、keep-alive、压缩和其他设置,创建一个Transport:

tr := &http.Transport{
	TLSClientConfig:    &tls.Config{RootCAs: pool},
	DisableCompression: true,
}
client := &http.Client{Transport: tr}
resp, err := client.Get("https://example.com")

Client和Transport类型都可以安全的被多个go程同时使用。出于效率考虑,应该一次建立、尽量重用。

2.2、简单的GET请求示例

package main

import (
	"fmt"
	"io"
	"log"
	"net/http"
)

func main() {
	resp, err := http.Get("http://www.baidu.com")
	if err != nil {
		log.Print(err)
		return
	}

	defer resp.Body.Close() //关闭

	fmt.Println("header = ", resp.Header)
	fmt.Printf("resp status %s\nstatusCode %d\n", resp.Status, resp.StatusCode)
	fmt.Printf("body type = %T\n", resp.Body)

	buf := make([]byte, 2048) //切片缓冲区
	var tmp string

	for {
		n, err := resp.Body.Read(buf) //读取body包内容
		if err != nil && err != io.EOF {
			fmt.Println(err)
			return
		}

		if n == 0 {
			fmt.Println("读取内容结束")
			break
		}
		tmp += string(buf[:n]) //累加读取的内容
	}

	fmt.Println("buf = ", string(tmp))

}

上述代码从网址www.baidu.com首页获取返回的信息,运行结果:

header =  map[Bdpagetype:[1] Bdqid:[0xdc3ba8700006ad37] Connection:[keep-alive] Content-Type:[text/html; charset=utf-8] Date:[Sat, 20 Aug 2022 10:20:31 GMT] P3p:[CP=" OTI DSP COR IVA OUR IND COM " CP=" OTI DSP COR IVA OUR IND COM "] Server:[BWS/1.1] Set-Cookie:[BAIDUID=0ACAB441A0B77069AC4CD4036247012B:FG=1; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.com BIDUPSID=0ACAB441A0B77069AC4CD4036247012B; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.com PSTM=1660990831; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.com BAIDUID=0ACAB441A0B770693AB9965F05A15EB0:FG=1; max-age=31536000; expires=Sun, 20-Aug-23 10:20:31 GMT; domain=.baidu.com; path=/; version=1; comment=bd BDSVRTM=0; path=/ BD_HOME=1; path=/ H_PS_PSSID=36544_36465_36884_36917_37003_36569_37134_37055_26350_36864; path=/; domain=.baidu.com] Traceid:[1660990831048475700215869462910960315703] X-Frame-Options:[sameorigin] X-Ua-Compatible:[IE=Edge,chrome=1]]

resp status 200 OK

statusCode 200

body type = *http.gzipReader

读取内容结束
buf =  <!DOCTYPE html><!--STATUS OK--><html><head><meta http-equiv="Content-Type" content="text/html;charset=utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"><meta content="always" name="referrer"><meta name="theme-color" content="#ffffff">...

从浏览器访问,也可以看到:
百度首页的返回信息

2.3、带参数的GET请求示例

2.3.1、服务端代码
import (
	"fmt"
	"net/http"
)

func main() {
	http.HandleFunc("/get", func(w http.ResponseWriter, r *http.Request) {
		defer r.Body.Close()
		fmt.Printf("r.URL = %v\n", r.URL)
		w.Write([]byte(`{"statusCode":200}`))
	})
	http.ListenAndServe(":8088", nil)
}
2.3.2、客户端代码
import (
	"fmt"
	"io/ioutil"
	"net/http"
	"net/url"
)

func main() {
	//1. 构建URL参数
	//设置url参数,map[string][]string
	data := url.Values{}
	data.Add("name", "Hello")
	data.Set("language", "golang")
	/*ParseRequestURI parses a raw url into a URL structure. It assumes that
	url was received in an HTTP request, so the url is interpreted
	only as an absolute URI or an absolute path.*/
	u, err := url.ParseRequestURI("http://127.0.0.1:8088/get")
	if err != nil {
		fmt.Printf("parse url requestUrl failed, err:%v\n", err)
		return
	}
	//URL encode,如果参数中有中文参数,这个方法会进行URLEncode
	u.RawQuery = data.Encode()
	fmt.Printf("http.Get string = %s\n", u.String())

	//2. 发送带参数的GET请求
	resp, err := http.Get(u.String())
	if err != nil {
		fmt.Println("http get failed, err = ", err)
		return
	}
	defer resp.Body.Close()

	//3. 读取服务端返回的数据
	ret, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		fmt.Println("get server's resp failed, err = ", err)
		return
	}
	fmt.Printf("server's resp_body = %v\n", string(ret))
}
2.3.4、运行结果
//server
r.URL = /get?language=golang&name=Hello

//client
http.Get string = http://127.0.0.1:8088/get?language=golang&name=Hello
server's resp_body = {"statusCode":200}

2.4、简单的POST请求示例

2.4.1、服务端代码
package main
import (
	"fmt"
	"io/ioutil"
	"net/http"
)

func main() {
	http.HandleFunc("/post", func(w http.ResponseWriter, r *http.Request) {
		defer r.Body.Close() //记住需要close

		//解析的步骤只针对表单数据的类型的post,json数据的POST可以不需要这一步
		//ParseForm解析URL中的查询字符串,并将解析结果更新到r.Form字段。
		//对于POST或PUT请求,ParseForm还会将body当作表单解析,并将结果既更新到r.PostForm也更新到r.Form
		r.ParseForm()
		fmt.Printf("r.PostForm = %v\n", r.PostForm) //打印客户端提交的表单数据

		prasebyte, err := ioutil.ReadAll(r.Body)
		if err != nil {
			fmt.Println("server read r.body from client failed, err = ", err)
			return
		}
		fmt.Printf("r.Body = %v\n", string(prasebyte))
		w.Write([]byte(`{"status": "ok"}`))
	})
	if err := http.ListenAndServe(":8088", nil); err != nil {
		fmt.Println("server listen :8088 failed, err = ", err)
		return
	}
}

2.4.2、客户端代码
package main
import (
	"fmt"
	"io/ioutil"
	"net/http"
	"strings"
)

func main() {
	url := "http://127.0.0.1:8088/post"
	//1. json的格式,数据在body中,表单无数据
	contentType := "application/json"
	data := `{"name":"Hello", "language":"golang"}`
	//2. 表单数据的格式,数据在表单中,body无数据
	//contentType = "application/x-www-form-urlencoded"
	//data = "name=Hello&language=golang"

	resp, err := http.Post(url, contentType, strings.NewReader(data))
	if err != nil {
		fmt.Println("client post failed, err = ", err)
		return
	}
	defer resp.Body.Close()

	retbyte, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		fmt.Println("client get server's resp failed, err = ", err)
		return
	}
	fmt.Println(string(retbyte))
}
2.4.3、运行结果

先运行服务端代码,再运行客户端(设置json格式的POST)代码,结果如下:

//server: json数据的POST,表单中无数据,数据在body中
r.PostForm = map[]
r.Body = {"name":"Hello", "language":"golang"}

//client
PS post\client> go run .\main.go
{"status": "ok"}

如果客户端设置表单数据格式的POST,则服务端的结果:

//server: 表单数据的POST,数据在表单中,body中无数据
r.PostForm = map[language:[golang] name:[Hello]]
r.Body = 

3、http服务端

3.1、简单的Server示例

示例的代码
package main

import (
	"fmt"
	"net/http"
)

func sHandleFunc(w http.ResponseWriter, r *http.Request) {
	fmt.Println("method = ", r.Method)
	fmt.Println("url path = ", r.URL.Path)
	fmt.Println("header = ", r.Header)
	fmt.Println("body = ", r.Body)
	w.Write([]byte("<h1>Hello, golang</h1>"))
}

func main() {
	http.HandleFunc("/index", sHandleFunc)
	http.ListenAndServe(":8088", nil)
}

在浏览器中输入对应的网址,可以获取返回的信息
在这里插入图片描述
同时,上面的go代码会打印浏览器发送的请求信息:

method =  GET
url path =  /index
header =  map[Accept:[text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9] Accept-Encoding:[gzip, deflate, br] Accept-Language:[zh-CN,zh;q=0.9] Cache-Control:[max-age=0] Connection:[keep-alive] Sec-Ch-Ua:["Chromium";v="104", " Not A;Brand";v="99", "Google Chrome";v="104"] Sec-Ch-Ua-Mobile:[?0] Sec-Ch-Ua-Platform:["Windows"] Sec-Fetch-Dest:[document] Sec-Fetch-Mode:[navigate] Sec-Fetch-Site:[none] Sec-Fetch-User:[?1] Upgrade-Insecure-Requests:[1] User-Agent:[Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36]]
body =  {}
分析

net/http包创建http服务端主要执行的步骤:
(1)http.HandleFunc 绑定处理函数,将处理器函数handler和对应的模式pattern传给默认的结构体ServeMux对象DefaultServeMuxDefaultServeMux的将传入的pattern和handler作为一个muxEntry整体添加到映射map中

// HandleFunc registers the handler function for the given pattern
// in the DefaultServeMux.
// The documentation for ServeMux explains how patterns are matched.
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
	DefaultServeMux.HandleFunc(pattern, handler)
}

(2)http.ListenAndServe实例化一个server结构体,其ListenAndServe()方法中调用net.Listen("tcp", addr)对传入的地址进行tcp服务监听。接着在Serve方法中启动l.Accept()处理用户连接,go c.serve(connCtx) 处理业务段(如判断信息,拼接http、找到对应处理函数)

// ListenAndServe listens on the TCP network address addr and then calls
// Serve with handler to handle requests on incoming connections.
// Accepted connections are configured to enable TCP keep-alives.
//
// The handler is typically nil, in which case the DefaultServeMux is used.
//
// ListenAndServe always returns a non-nil error.
func ListenAndServe(addr string, handler Handler) error {
	server := &Server{Addr: addr, Handler: handler}
	return server.ListenAndServe()
}

二、log

log包实现了简单的日志服务。该包中定义了Logger类型,该类型提供了一些格式化输出的方法。
提供了一个预定义的“标准”Logger,可以通过辅助函数Print[f|ln]、Fatal[f|ln]和Panic[f|ln]访问,比手工创建一个Logger对象更容易使用。
Logger会打印每条日志信息的日期、时间,默认输出到标准错误。
Fatal系列函数会在写入日志信息后调用os.Exit(1)。Panic系列函数会在写入日志信息后panic。

1、直接使用标准的Logger

import (
	"log"
	"time"
)

func main() {
	log.Printf("Logger使用示例, time = %v\n", time.Now().Local())
	//运行下面语句后,会直接调用os.Exit(1)退出程序
	log.Fatalln("Logger使用示例: 发生了fatal错误")
	//运行下面语句后,会跑出一个panic
	log.Panicln("Logger使用示例: 抛出了panic")
}

output:
2022/08/21 02:13:31 Logger使用示例, time = 2022-08-21 02:13:31.5121939 +0800 CST
2022/08/21 02:13:31 Logger使用示例: 发生了fatal错误
Process 3024 has exited with status 1

2、配置标准的Logger

2.1、配置flag

func Flags() int
//Flags返回标准logger的输出选项。

func SetFlags(flag int)
//SetFlags设置标准logger的输出选项。

//flag可配置的项
const (
    // 字位共同控制输出日志信息的细节。不能控制输出的顺序和格式。
    // 在所有项目后会有一个冒号:2009/01/23 01:23:23.123123 /a/b/c/d.go:23: message
    Ldate         = 1 << iota     // 日期:2009/01/23
    Ltime                         // 时间:01:23:23
    Lmicroseconds                 // 微秒分辨率:01:23:23.123123(用于增强Ltime位)
    Llongfile                     // 文件全路径名+行号: /a/b/c/d.go:23
    Lshortfile                    // 文件无路径名+行号:d.go:23(会覆盖掉Llongfile)
    LstdFlags     = Ldate | Ltime // 标准logger的初始值
)
func main() {
	log.Println("Logger使用示例, flag = ", log.Flags())
	log.SetFlags(log.LstdFlags | log.Llongfile)
	log.Println("Logger使用示例, flag = ", log.Flags())
}

output:
2022/08/21 02:27:01 Logger使用示例, flag =  3
2022/08/21 02:27:01 d:/WorkSpace/Golang/src/Test01/log/flag/flag.go:8: Logger使用示例, flag =  11

2.2、配置Prefix

func Prefix() string
//Prefix返回标准logger的输出前缀。

func SetPrefix(prefix string)
//SetPrefix设置标准logger的输出前缀
func main() {
	log.Println("Logger使用示例, prefix = ", log.Prefix())
	log.SetFlags(log.LstdFlags | log.Llongfile)
	log.SetPrefix("[golang]")
	log.Println("Logger使用示例, prefix = ", log.Prefix())
}

output:
2022/08/21 02:30:31 Logger使用示例, prefix =  
[golang]2022/08/21 02:30:31 d:/WorkSpace/Golang/src/Test01/log/flag/flag.go:9: Logger使用示例, prefix =  [golang]

2.3、输出到文件

func SetOutput(w io.Writer)
//SetOutput设置标准logger的输出目的地,默认是标准错误输出
var file *os.File

func init() {
	file, err := os.OpenFile("./log.log", os.O_CREATE|os.O_APPEND, 0666)
	if err != nil {
		fmt.Println("open log file failed, err:", err)
		return
	}
	log.SetOutput(file)
	log.SetFlags(log.Lmicroseconds | log.Ldate | log.Llongfile)
	log.SetPrefix("[GoLang]")
}

func main() {
	defer file.Close()
	log.Println("Logger使用示例")
}

output:
//./log.log
[GoLang]2022/08/21 02:39:56.739174 d:/WorkSpace/Golang/src/Test01/log/flag/flag.go:24: Logger使用示例

3、自定义Logger

func New(out io.Writer, prefix string, flag int) *Logger
//New创建一个Logger。参数out设置日志信息写入的目的地。参数prefix会添加到生成的每一条日志前面。参数flag定义日志的属性(时间、文件等等)
//自定义logger并输出到屏幕
func main() {
	logger := log.New(os.Stdout, "[Go]", log.Ldate|log.Ltime|log.Lshortfile)
	logger.Println("自定义logger")
}

output:
[Go]2022/08/21 02:43:29 demo.go:10: 自定义logger

4、总结

Go内置的log库功能有限,例如无法满足记录不同级别日志的情况,实际项目中根据需求选择第三方的日志库,如logrus、zap

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值