初学go_web记录1
问题1:r.ParseForm() 不是有这个才能读取到请求头里面的东西吗,怎么login里面开始就读取了r.method
你提出了一个非常好的问题,这能帮助我们更精确地区分 http.Request 对象中不同部分的访问方式。
答案是:r.Method 和 r.ParseForm() 处理的是请求中完全不同的两个部分,它们互不依赖。
让我们把一个 HTTP 请求想象成一个快递包裹:
r.Method,r.URL,r.Header等: 这些信息就像写在快递包裹外面的“运单”。运单上有发件人、收件人地址、包裹类型(普通/加急)等信息。你不需要打开包裹就能直接读取这些信息。r.Form,r.PostForm,r.MultipartForm: 这些信息是包裹内部装的东西,特别是当包裹里是一个填好的“订单”(HTML 表单数据)时。你需要一个专门的动作——“拆开包裹并整理订单内容”——才能读取这些信息。这个动作就是r.ParseForm()。
r.Method 是什么?
r.Method存储的是 HTTP 请求的方法 (Method),例如GET,POST,PUT,DELETE等。- 这个信息位于 HTTP 请求报文的起始行 (Request Line),是整个请求最开始、最基本的部分。
- 当 Go 的
net/http服务器接收到一个请求时,它会最先解析这部分内容,因为它需要根据请求方法和 URL 来决定如何处理这个请求。 - 因此,
r.Method的值在http.Handler函数被调用时就已经被解析好并填充到r对象里了。你随时都可以直接访问它,完全不需要调用r.ParseForm()。
一个简化的 HTTP GET 请求报文示例:
1 | GET /login HTTP/1.1 <-- r.Method 就在这里 (起始行) |
r.ParseForm() 是做什么的?
r.ParseForm() 的作用是解析请求中和表单数据相关的部分。具体来说,它会做两件事:
对于
GET请求: 它会解析 URL 中的查询字符串 (Query String)。- 例如,对于 URL
http://.../login?username=alice&lang=en,r.ParseForm()会把username和lang这两个键值对解析出来,放到r.Form这个 map 里。
- 例如,对于 URL
对于
POST或PUT请求: 它会解析请求体 (Request Body)。- 前提是请求的
Content-Type是application/x-www-form-urlencoded或multipart/form-data。 - 它会读取请求体的内容,解析出里面的键值对(例如
username=bob&password=123),然后把它们也放到r.Form里。
- 前提是请求的
关键点:
- 这是一个有成本的操作,因为它可能需要读取和解析整个请求体,这会消耗时间和内存。
- 因此,Go 的设计者决定让这个操作**“按需执行”**。只有当你真正需要访问表单数据(
r.Form,r.PostForm)时,才需要手动调用一次r.ParseForm()。 - 如果你不调用
r.ParseForm()就尝试访问r.Form,r.Form会是空的 (nil)。
总结与代码中的体现
在你的 login 函数中:
1 | func login(w http.ResponseWriter, r *http.Request) { |
结论:
- 读取请求的元数据(如
Method,URL,Header,Host),不需要r.ParseForm()。 - 读取请求的表单数据(URL 查询参数或
POST请求体),必须先调用r.ParseForm()。
问题2:请用具体真实的例子解释一下
好的,没问题!我们用一个非常具体、真实的例子来彻底拆解一个 HTTP 请求,看看它的各个部分都对应 r 对象的哪些字段,以及哪些需要 ParseForm。
场景:用户在浏览器中提交登录表单
假设我们的登录页面 (login.gtpl) 是这样的:
1 | <form action="/login?source=web" method="post"> |
action="/login?source=web": 表单将提交到/login路径,并且 URL 里还带了一个查询参数source=web。method="post": 使用 POST 方法提交。
用户在表单里输入了:
- Username:
cofeesy - Password:
123456
然后点击了 “Login” 按钮。
第一步:浏览器生成 HTTP 请求
浏览器会根据用户的操作,生成一段类似下面这样的原始文本数据,然后通过网络发送给你的 Go 服务器。这就是一个真实的 HTTP 请求报文 (HTTP Request Message):
1 | POST /login?source=web |
第二步:Go 服务器解析请求
当你的 Go 服务器收到这段文本后,net/http 包会像一个“解码器”一样,自动地、在调用你的 login 函数之前,解析其中的一部分,并填充到 r *http.Request 对象里。
我们来逐行看看这段报文被解析成了什么,以及哪些不需要 ParseForm() 就能直接读取。
1. 起始行 (Start Line)
1 | POST /login?source=web |
这是请求的最核心部分,包含了三块信息。Go 服务器会立刻解析它:
POST: 这是请求方法。- 对应代码:
r.Method - 值:
"POST" - 是否需要 ParseForm(): 否。这是最先被解析的,可以直接读。
- 对应代码:
/login?source=web: 这是请求的 URI。Go 会把它进一步拆分:- 对应代码:
r.URL.Path->/loginr.URL.RawQuery->source=web
- 是否需要 ParseForm(): 否。URL 是路由的基础,必须先解析,可以直接读。
- 对应代码:
HTTP/1.1: 这是协议版本。- 对应代码:
r.Proto - 值:
"HTTP/1.1" - 是否需要 ParseForm(): 否。
- 对应代码:
2. 请求头 (Headers)
1 | Host: 127.0.0.1:9090 |
这些是键值对,提供了关于请求的元数据。
- 对应代码:
r.Header(这是一个map[string][]string)r.Header.Get("Content-Type")->"application/x-www-form-urlencoded"r.Host是一个方便访问Host头的快捷字段,值为"127.0.0.1:9090"
- 是否需要 ParseForm(): 否。请求头是紧跟在起始行后面的,也会被立即解析。
第三步:login 函数被调用,ParseForm 登场
到现在为止,你的 login 函数已经被调用,并且 r 对象里已经填满了上述所有信息。但还有一部分数据,Go 服务器没有碰,因为它不知道你是否需要它,也不确定它是什么格式。
3. 请求体 (Request Body)
1 | (一个空行) |
这部分数据就是我们说的“包裹里的东西”。
对应代码:
- 原始数据流:
r.Body(这是一个io.ReadCloser) - 解析后的数据:
r.Form和r.PostForm
- 原始数据流:
为什么需要
ParseForm():r.Body可能非常大(比如上传一个几GB的文件),服务器不会自动读取并解析它,因为这可能耗尽内存。- 只有当你调用
r.ParseForm()时,Go 才会根据请求头里的Content-Type(application/x-www-form-urlencoded),去读取r.Body的数据流,并按照指定的格式(key=value&key2=value2)进行解析。 - 解析完成后,它会把结果
username=cofeesy和password=123456填充到r.PostForm这个 map 里。
关于
r.Form的一个细节:r.ParseForm()不仅解析请求体,它还会顺便把 URL 里的查询参数(source=web)也解析了。- 最终,
r.Form会是URL 查询参数和POST 表单数据的合集。 r.PostForm只包含 POST 表单数据。
所以,在你的 login 函数里:
1 | func login(w http.ResponseWriter, r *http.Request) { |
总结
| 请求的部分 | 对应 r 中的字段/方法 |
何时被解析 | 是否需要 r.ParseForm() |
|---|---|---|---|
| 请求方法 (POST) | r.Method |
自动,在处理函数调用前 | 否 |
| URL路径 (/login) | r.URL.Path |
自动,在处理函数调用前 | 否 |
| 请求头 (Host, …) | r.Header, r.Host |
自动,在处理函数调用前 | 否 |
| URL查询参数 (?source=web) | r.URL.RawQuery (原始), r.Form (解析后) |
按需,在调用 ParseForm 时 |
是 |
| 请求体 (username=…) | r.Body (原始), r.PostForm, r.Form (解析后) |
按需,在调用 ParseForm 时 |
是 |

