参考:https://studygolang.com/pkgdoc

概念解释:

  • request:用户请求的信息,用来解析用户的请求信息,包括post、get、cookie、url等信息
  • response:服务器返回给客户端的信息
  • conn:用户的每次请求链接
  • handler:处理请求和生成返回信息的处理逻辑

go标准库的学习-net/http

该图来自https://www.sohu.com/a/208720509_99960938

 

下面的内容来自http://www.runoob.com/http/http-messages.html

HTTP使用统一资源标识符(Uniform Resource Identifiers, URI)来传输数据和建立连接。

一旦建立连接后,数据消息就通过类似Internet邮件所使用的格式[RFC5322]和多用途Internet邮件扩展(MIME)[RFC2045]来传送。

 

客户端请求消息

客户端发送一个HTTP请求到服务器的请求消息包括以下格式:请求行(request line)、请求头部(header)、空行和请求数据四个部分组成,下图给出了请求报文的一般格式。

go标准库的学习-net/http

举例:

GET /hello.txt HTTP/1.1
User-Agent: curl/7.16.3 libcurl/7.16.3 OpenSSL/0.9.7l zlib/1.2.3
Host: www.example.com
Accept-Language: en, mi

HTTP 协议的 8 种请求方法介绍:

HTTP1.0定义了三种请求方法: GET, POST 和 HEAD方法

HTTP1.1新增了五种请求方法:OPTIONS, PUT, DELETE, TRACE 和 CONNECT 方法。

HTTP 协议中共定义了八种请求方法或者叫“动作”来表明对 Request-URI 指定的资源的不同操作方式,具体介绍如下:

  •  OPTIONS:返回服务器针对特定资源所支持的HTTP请求方法。也可以利用向Web服务器发送'*'的请求来测试服务器的功能性。 
  •  HEAD:向服务器索要与GET请求相一致的响应,只不过响应体将不会被返回。这一方法可以在不必传输整个响应内容的情况下,就可以获取包含在响应消息头中的元信息。 
  •  GET:向特定的资源发出请求。 
  •  POST:向指定资源提交数据进行处理请求(例如提交表单或者上传文件)。数据被包含在请求体中。POST请求可能会导致新的资源的创建和/或已有资源的修改。 
  •  PUT:向指定资源位置上传其最新内容。 
  •  DELETE:请求服务器删除 Request-URI 所标识的资源。 
  •  TRACE:回显服务器收到的请求,主要用于测试或诊断。 
  •  CONNECT:HTTP/1.1 协议中预留给能够将连接改为管道方式的代理服务器。 

虽然 HTTP 的请求方式有 8 种,但是我们在实际应用中常用的也就是 get 和 post,其他请求方式也都可以通过这两种方式间接的来实现。

 

服务器响应消息

HTTP响应也由四个部分组成,分别是:状态行、消息报头、空行和响应正文。

go标准库的学习-net/http

举例

服务端响应:

HTTP/1.1 200 OK
Date: Mon, 27 Jul 2009 12:28:53 GMT
Server: Apache
Last-Modified: Wed, 22 Jul 2009 19:15:56 GMT
ETag: "34aa387-d-1568eb00"
Accept-Ranges: bytes
Content-Length: 51
Vary: Accept-Encoding
Content-Type: text/plain

输出结果:

Hello World! My payload includes a trailing CRLF.

 

1>常量

const (
    StatusContinue           = 100
    StatusSwitchingProtocols = 101
    StatusOK                   = 200
    StatusCreated              = 201
    StatusAccepted             = 202
    StatusNonAuthoritativeInfo = 203
    StatusNoContent            = 204
    StatusResetContent         = 205
    StatusPartialContent       = 206
    StatusMultipleChoices   = 300
    StatusMovedPermanently  = 301
    StatusFound             = 302
    StatusSeeOther          = 303
    StatusNotModified       = 304
    StatusUseProxy          = 305
    StatusTemporaryRedirect = 307
    StatusBadRequest                   = 400
    StatusUnauthorized                 = 401
    StatusPaymentRequired              = 402
    StatusForbidden                    = 403
    StatusNotFound                     = 404
    StatusMethodNotAllowed             = 405
    StatusNotAcceptable                = 406
    StatusProxyAuthRequired            = 407
    StatusRequestTimeout               = 408
    StatusConflict                     = 409
    StatusGone                         = 410
    StatusLengthRequired               = 411
    StatusPreconditionFailed           = 412
    StatusRequestEntityTooLarge        = 413
    StatusRequestURITooLong            = 414
    StatusUnsupportedMediaType         = 415
    StatusRequestedRangeNotSatisfiable = 416
    StatusExpectationFailed            = 417
    StatusTeapot                       = 418
    StatusInternalServerError     = 500
    StatusNotImplemented          = 501
    StatusBadGateway              = 502
    StatusServiceUnavailable      = 503
    StatusGatewayTimeout          = 504
    StatusHTTPVersionNotSupported = 505
)

HTTP状态码

当你得到一个值的时候,如果你想要知道这个值代表的是什么状态,可以使用:

 StatusText

func StatusText(code int) string

StatusText返回HTTP状态码code对应的文本,如220对应"OK"。如果code是未知的状态码,会返回""。

举例:

package main 
import(
    "fmt"
    "net/http"
)

func main() {
    sta1 := http.StatusText(307)
    sta2 := http.StatusText(200)
    fmt.Println(sta1) //Temporary Redirect
    fmt.Println(sta2) //OK
}

 

2>变量

var DefaultClient = &Client{}

DefaultClient是用于包函数Get、Head和Post的默认Client。

var DefaultServeMux = NewServeMux()

DefaultServeMux是用于Serve的默认ServeMux。

 

3>

 CanonicalHeaderKey

func CanonicalHeaderKey(s string) string

CanonicalHeaderKey函数返回头域(表示为Header类型)的键s的规范化格式。规范化过程中让单词首字母和'-'后的第一个字母大写,其余字母小写。例如,"accept-encoding"规范化为"Accept-Encoding"。

举例:

package main 
import(
    "fmt"
    "net/http"
)

func main() {
    hea1 := http.CanonicalHeaderKey("uid-test")
    hea2 := http.CanonicalHeaderKey("accept-encoding")
    fmt.Println(hea1) //Uid-Test
    fmt.Println(hea2) //Accept-Encoding
}

 

 DetectContentType

func DetectContentType(data []byte) string

DetectContentType函数实现了http://mimesniff.spec.whatwg.org/描述的算法,用于确定数据的Content-Type。函数总是返回一个合法的MIME类型;如果它不能确定数据的类型,将返回"application/octet-stream"。它最多检查数据的前512字节。

出处:https://www.cnblogs.com/52fhy/p/5436673.html

Http Header里的Content-Type一般有这三种:

  • application/x-www-form-urlencoded:数据被编码为名称/值对。这是标准的编码格式。
  • multipart/form-data: 数据被编码为一条消息,页上的每个控件对应消息中的一个部分。
  • text/plain: 数据以纯文本形式(text/json/xml/html)进行编码,其中不含任何控件或格式字符。postman软件里标的是RAW。

网页中form的enctype属性为编码方式,常用有两种:

  • application/x-www-form-urlencoded,默认编码方式
  • multipart/form-data

1)当action为get时候,浏览器用x-www-form-urlencoded的编码方式把form数据转换成一个字串(name1=value1&name2=value2...),然后把这个字串追加到url后面,用?分割,加载这个新的url。

2)当action为post时候,浏览器把form数据封装到http body中,然后发送到server。 如果没有type=file的控件,用默认的application/x-www-form-urlencoded就可以了。 但是如果有type=file的话,就要用到multipart/form-data了。

3)当action为post且Content-Type类型是multipart/form-data,浏览器会把整个表单以控件为单位分割,并为每个部分加上Content-Disposition(form-data或者file),Content-Type(默认为text/plain),name(控件name)等信息,并加上分割符(boundary)。

4)当然还有很多其他的类型,可以查看http://www.runoob.com/http/http-content-type.html

因此可以使用DetectContentType来检测传入的[]byte类型的数据是哪种Content-Type,举例说明:\

package main 
import(
    "fmt"
    "net/http"
)

func main() {
    cont1 := http.DetectContentType([]byte{}) //text/plain; charset=utf-8
    cont2 := http.DetectContentType([]byte{1, 2, 3}) //application/octet-stream
    cont3 := http.DetectContentType([]byte(`<HtMl><bOdY>blah blah blah</body></html>`)) //text/html; charset=utf-8
    cont4 := http.DetectContentType([]byte("\n<?xml!")) //text/xml; charset=utf-8
    cont5 := http.DetectContentType([]byte(`GIF87a`)) //image/gif
    cont6 := http.DetectContentType([]byte("MThd\x00\x00\x00\x06\x00\x01")) //audio/midi
    fmt.Println(cont1)
    fmt.Println(cont2)
    fmt.Println(cont3)
    fmt.Println(cont4)
    fmt.Println(cont5)
    fmt.Println(cont6)
}

 

const TimeFormat = "Mon, 02 Jan 2006 15:04:05 GMT"

TimeFormat是当解析或生产HTTP头域中的时间时,用与time.Parse或time.Format函数的时间格式。这种格式类似time.RFC1123但强制采用GMT时区。

 ParseTime

func ParseTime(text string) (t time.Time, err error)

ParseTime用3种格式TimeFormat, time.RFC850和time.ANSIC尝试解析一个时间头的值(如Date: header)。

举例:

package main 
import(
    "fmt"
    "net/http"
    "time"
)


var parseTimeTests = []struct {
    h   http.Header
    err bool
}{
    {http.Header{"Date": {""}}, true},
    {http.Header{"Date": {"invalid"}}, true},
    {http.Header{"Date": {"1994-11-06T08:49:37Z00:00"}}, true},
    {http.Header{"Date": {"Sun, 06 Nov 1994 08:49:37 GMT"}}, false},
    {http.Header{"Date": {"Sunday, 06-Nov-94 08:49:37 GMT"}}, false},
    {http.Header{"Date": {"Sun Nov  6 08:49:37 1994"}}, false},
}

func main() {
    expect := time.Date(1994, 11, 6, 8, 49, 37, 0, time.UTC)
    fmt.Println(expect) //1994-11-06 08:49:37 +0000 UTC
    for i, test := range parseTimeTests {
        d, err := http.ParseTime(test.h.Get("Date"))
        fmt.Println(d)
        if err != nil {
            fmt.Println(i, err)
            if !test.err { //test.err为false才进这里
                fmt.Errorf("#%d:\n got err: %v", i, err)
            }
            continue //有错的进入这后继续下一个循环,不往下执行
        }
        if test.err { //test.err为true,所以该例子中这里不会进入
            fmt.Errorf("#%d:\n  should err", i)
            continue
        }
        if !expect.Equal(d) { //说明后三个例子的结果和expect是相同的,所以没有报错
            fmt.Errorf("#%d:\n got: %v\nwant: %v", i, d, expect)
        }
    }
}

返回:

userdeMBP:go-learning user$ go run test.go
1994-11-06 08:49:37 +0000 UTC
0001-01-01 00:00:00 +0000 UTC //默认返回的空值
0 parsing time "" as "Mon Jan _2 15:04:05 2006": cannot parse "" as "Mon"
0001-01-01 00:00:00 +0000 UTC
1 parsing time "invalid" as "Mon Jan _2 15:04:05 2006": cannot parse "invalid" as "Mon"
0001-01-01 00:00:00 +0000 UTC
2 parsing time "1994-11-06T08:49:37Z00:00" as "Mon Jan _2 15:04:05 2006": cannot parse "1994-11-06T08:49:37Z00:00" as "Mon"
1994-11-06 08:49:37 +0000 UTC
1994-11-06 08:49:37 +0000 GMT
1994-11-06 08:49:37 +0000 UTC

额外补充,time.Date():

 Date

func Date(year int, month Month, day, hour, min, sec, nsec int, loc *Location) Time

详情看go标准库的学习-time

 

 ParseHTTPVersion

func ParseHTTPVersion(vers string) (major, minor int, ok bool)

ParseHTTPVersion解析HTTP版本字符串。如"HTTP/1.0"返回(1, 0, true)。

举例:
package main 
import(
    "fmt"
    "net/http"
)

func main() {
    m, n, ok := http.ParseHTTPVersion("HTTP/1.0")
    fmt.Println(m, n, ok) //1 0 true

 

4>

1)header-服务端和客户端的数据都有头部

 Header

type Header map[string][]string

Header代表HTTP头域的键值对。

你可以自定义自己的Header,下面的Header中只有Date字段,你还可以加入其他字段:

http.Header{"Date": {"1994-11-06T08:49:37Z00:00"}}

然后就能够调用下面的几种方法来对Header进行修改:

 Get

func (h Header) Get(key string) string

Get返回键对应的第一个值,如果键不存在会返回""。如要获取该键对应的值切片,请直接用规范格式的键访问map。

 Set

func (h Header) Set(key, value string)

Set添加键值对到h,如键已存在则会用只有新值一个元素的切片取代旧值切片。

 Add

func (h Header) Add(key, value string)

Add添加键值对到h,如键已存在则会将新的值附加到旧值切片后面。

 Del

func (h Header) Del(key string)

Del删除键值对。

举例:

package main 
import(
    "fmt"
    "net/http"
)

func main() {
    header := http.Header{"Date": {"1994-11-06T08:49:37Z00:00"}}
    fmt.Println(header.Get("Date")) //1994-11-06T08:49:37Z00:00
    fmt.Println(header.Get("Content-Type")) //因为没有该字段,返回为空

    header.Set("Content-Type", "text/plain; charset=UTF-8") //设置"Content-Type"字段
    fmt.Println(header.Get("Content-Type")) //返回text/plain; charset=UTF-8
    header.Set("Content-Type", "application/x-www-form-urlencoded;") //覆盖原先的值,返回application/x-www-form-urlencoded;
    fmt.Println(header.Get("Content-Type"))

    header.Add("Content-Type", "charset=UTF-8") //在"Content-Type"字段中追加值
    fmt.Println(header) //map[Date:[1994-11-06T08:49:37Z00:00] Content-Type:[application/x-www-form-urlencoded; charset=UTF-8]],可见添加进去
    fmt.Println(header.Get("Content-Type")) //但是这样获取是返回值仍是application/x-www-form-urlencoded;

    header.Del("Content-Type") //删除该字段
    fmt.Println(header.Get("Content-Type")) //然后返回又为空
}

 Write

func (h Header) Write(w io.Writer) error

Write以有线格式将头域写入w。

 WriteSubset

func (h Header) WriteSubset(w io.Writer, exclude map[string]bool) error

WriteSubset以有线格式将头域写入w。当exclude不为nil时,如果h的键值对的键在exclude中存在且其对应值为真,该键值对就不会被写入w。

举例:

package main 
import(
    "fmt"
    "net/http"
    "bytes"
    "os"
)

var headerWriteTests = []struct {
    h        http.Header
    exclude  map[string]bool
    expected string
}{
    {http.Header{}, nil, ""},
    {
        http.Header{
            "Content-Type":   {"text/html; charset=UTF-8"},
            "Content-Length": {"0"},
        },
        nil,
        "Content-Length: 0\r\nContent-Type: text/html; charset=UTF-8\r\n",
    },
    {
        http.Header{
            "Expires":          {"-1"},
            "Content-Length":   {"0"},
            "Content-Encoding": {"gzip"},
        },
        map[string]bool{"Content-Length": true}, //"Content-Length"字段将不会写入io.Writer
        "Content-Encoding: gzip\r\nExpires: -1\r\n",
    },
}

func main() {
    var buf bytes.Buffer //得到io.Writer
    for i, test := range headerWriteTests {
        test.h.WriteSubset(&buf, test.exclude)
        fmt.Println(i)
        buf.WriteTo(os.Stdout)
        fmt.Println()
        if buf.String() != test.expected {
            fmt.Errorf("#%d:\n got: %q\nwant: %q", i, buf.String(), test.expected)
        }
        buf.Reset()
    }
}

返回:

userdeMBP:go-learning user$ go run test.go
0

1
Content-Length: 0
Content-Type: text/html; charset=UTF-8

2
Content-Encoding: gzip
Expires: -1

 

2)Cookie

⚠️session和cookie的区别:

session是存储在服务器的文件,cookie内容保存在客户端,存在被客户篡改的情况,session保存在服务端端防止被用户篡改的情况。

1》

 Cookie

type Cookie struct {
    Name       string
    Value      string
    Path       string
    Domain     string
    Expires    time.Time
    RawExpires string
    // MaxAge=0表示未设置Max-Age属性
    // MaxAge<0表示立刻删除该cookie,等价于"Max-Age: 0"
    // MaxAge>0表示存在Max-Age属性,单位是秒
    MaxAge   int
    Secure   bool
    HttpOnly bool
    Raw      string
    Unparsed []string // 未解析的“属性-值”对的原始文本
}

Cookie代表一个出现在HTTP回复的头域中Set-Cookie头的值里或者HTTP请求的头域中Cookie头的值里的HTTP cookie。

 String

func (c *Cookie) String() string

String返回该cookie的序列化结果。如果只设置了Name和Value字段,序列化结果可用于HTTP请求的Cookie头或者HTTP回复的Set-Cookie头;如果设置了其他字段,序列化结果只能用于HTTP回复的Set-Cookie头。

1)举例:

package main 
import(
    "fmt"
    "net/http"
    "bytes"
    "os"
    "log"
    "time"
)

var writeSetCookiesTests = []struct {
    Cookie *http.Cookie
    Raw    string
}{
    {
        &http.Cookie{Name: "cookie-2", Value: "two", MaxAge: 3600},
        "cookie-2=two; Max-Age=3600",
    },
    {
        &http.Cookie{Name: "cookie-3", Value: "three", Domain: ".example.com"},
        "cookie-3=three; Domain=example.com",
    },
    {
        &http.Cookie{Name: "cookie-4", Value: "four", Path: "/restricted/"},
        "cookie-4=four; Path=/restricted/",
    },
    {
        &http.Cookie{Name: "cookie-9", Value: "expiring", Expires: time.Unix(1257894000, 0)},
        "cookie-9=expiring; Expires=Tue, 10 Nov 2009 23:00:00 GMT",
    },
    // According to IETF 6265 Section 5.1.1.5, the year cannot be less than 1601
    {//故意将这里的cookie-10写成cookie-101,然后下面就会报错
        &http.Cookie{Name: "cookie-10", Value: "expiring-1601", Expires: time.Date(1601, 1, 1, 1, 1, 1, 1, time.UTC)},
        "cookie-101=expiring-1601; Expires=Mon, 01 Jan 1601 01:01:01 GMT",
    },
    { //因此其返回值中没有Expires
        &http.Cookie{Name: "cookie-11", Value: "invalid-expiry", Expires: time.Date(1600, 1, 1, 1, 1, 1, 1, time.UTC)},
        "cookie-11=invalid-expiry",
    },
    // The "special" cookies have values containing commas or spaces which
    // are disallowed by RFC 6265 but are common in the wild.
    {
        &http.Cookie{Name: "special-1", Value: "a z"},
        `special-1="a z"`,
    },
    {
        &http.Cookie{Name: "empty-value", Value: ""},
        `empty-value=`,
    },
    {
        nil,
        ``,
    },
    {
        &http.Cookie{Name: ""},
        ``,
    },
    {
        &http.Cookie{Name: "\t"},
        ``,
    },
}


func main() {
    defer log.SetOutput(os.Stderr)
    var logbuf bytes.Buffer
    log.SetOutput(&logbuf)

    for i, tt := range writeSetCookiesTests {//没有报错则说明得到的Cookie的值与Raw字符串相等
        if g, e := tt.Cookie.String(), tt.Raw; g != e {
            fmt.Printf("Test %d, expecting:\n%s\nGot:\n%s\n", i, e, g)
            continue
        }
    }
}

返回:

userdeMBP:go-learning user$ go run test.go
Test 4, expecting:
cookie-101=expiring-1601; Expires=Mon, 01 Jan 1601 01:01:01 GMT
Got:
cookie-10=expiring-1601; Expires=Mon, 01 Jan 1601 01:01:01 GMT

2)

 SetCookie

func SetCookie(w ResponseWriter, cookie *Cookie)

SetCookie在w的头域中添加Set-Cookie头,该HTTP头的值为cookie。

常用SetCookie来给http的request请求或者http的response响应设置cookie。

然后使用request的Cookies()、Cookie(name string)函数和response的Cookies()函数来获取设置的cookie信息

 ResponseWriter

type ResponseWriter interface {
    // Header返回一个Header类型值,该值会被WriteHeader方法发送。
    // 在调用WriteHeader或Write方法后再改变该对象是没有意义的。
    Header() Header
    // WriteHeader该方法发送HTTP回复的头域和状态码。
    // 如果没有被显式调用,第一次调用Write时会触发隐式调用WriteHeader(http.StatusOK)
    // WriterHeader的显式调用主要用于发送错误码。
    WriteHeader(int)
    // Write向连接中写入作为HTTP的一部分回复的数据。
    // 如果被调用时还未调用WriteHeader,本方法会先调用WriteHeader(http.StatusOK)
    // 如果Header中没有"Content-Type"键,
    // 本方法会使用包函数DetectContentType检查数据的前512字节,将返回值作为该键的值。
    Write([]byte) (int, error)
}

ResponseWriter接口被HTTP处理器用于构造HTTP回复。

举例:

package main 
import(
    "fmt"
    "net/http"
)

type headerOnlyResponseWriter http.Header
//下面定义这些函数是为了使headerOnlyResponseWriter实现ResponseWriter接口,然后可以作为SetCookie的参数传入
func (ho headerOnlyResponseWriter) Header() http.Header {
    return http.Header(ho)
}

func (ho headerOnlyResponseWriter) Write([]byte) (int, error) {
    panic("NOIMPL")
}

func (ho headerOnlyResponseWriter) WriteHeader(int) {
    panic("NOIMPL")
}


func main() {
    m := make(http.Header) //创建一个map[string][]string类型的映射m,headerOnlyResponseWriter(m)即将Header类型的m转成自定义headerOnlyResponseWriter类型
    fmt.Println(m) //运行SetCookie()之前为 map[]
    fmt.Println(headerOnlyResponseWriter(m)) //运行SetCookie()之前为 map[]
    
    //SetCookie在w的头域中添加Set-Cookie头,该HTTP头的值为cookie
    http.SetCookie(headerOnlyResponseWriter(m), &http.Cookie{Name: "cookie-1", Value: "one", Path: "/restricted/"})
    http.SetCookie(headerOnlyResponseWriter(m), &http.Cookie{Name: "cookie-2", Value: "two", MaxAge: 3600})
    fmt.Println(m) //返回:map[Set-Cookie:[cookie-1=one; Path=/restricted/ cookie-2=two; Max-Age=3600]]

    //下面的内容都没有报错,说明得到的值和给出的字符串是相同的
    if l := len(m["Set-Cookie"]); l != 2 {
        fmt.Printf("expected %d cookies, got %d", 2, l)
    }
    if g, e := m["Set-Cookie"][0], "cookie-1=one; Path=/restricted/"; g != e {
        fmt.Printf("cookie #1: want %q, got %q", e, g)
    }
    if g, e := m["Set-Cookie"][1], "cookie-2=two; Max-Age=3600"; g != e {
        fmt.Printf("cookie #2: want %q, got %q", e, g)
    }
}

 

2》

 CookieJar

type CookieJar interface {
    // SetCookies管理从u的回复中收到的cookie
    // 根据其策略和实现,它可以选择是否存储cookie
    SetCookies(u *url.URL, cookies []*Cookie)
    // Cookies返回发送请求到u时应使用的cookie
    // 本方法有责任遵守RFC 6265规定的标准cookie限制
    Cookies(u *url.URL) []*Cookie
}

CookieJar管理cookie的存储和在HTTP请求中的使用。CookieJar的实现必须能安全的被多个go程同时使用。

net/http/cookiejar包提供了一个CookieJar的实现。

 
3》

 Request

type Request struct {
    // Method指定HTTP方法(GET、POST、PUT等)。对客户端,""代表GET。
    Method string
    // URL在服务端表示被请求的URI,在客户端表示要访问的URL。
    //
    // 在服务端,URL字段是解析请求行的URI(保存在RequestURI字段)得到的,
    // 对大多数请求来说,除了Path和RawQuery之外的字段都是空字符串。
    // (参见RFC 2616, Section 5.1.2)
    //
    // 在客户端,URL的Host字段指定了要连接的服务器,
    // 而Request的Host字段(可选地)指定要发送的HTTP请求的Host头的值。
    URL *url.URL
    // 接收到的请求的协议版本。本包生产的Request总是使用HTTP/1.1
    Proto      string // "HTTP/1.0"
    ProtoMajor int    // 1
    ProtoMinor int    // 0
    // Header字段用来表示HTTP请求的头域。如果头域(多行键值对格式)为:
    //    accept-encoding: gzip, deflate
    //    Accept-Language: en-us
    //    Connection: keep-alive
    // 则:
    //    Header = map[string][]string{
    //        "Accept-Encoding": {"gzip, deflate"},
    //        "Accept-Language": {"en-us"},
    //        "Connection": {"keep-alive"},
    //    }
    // HTTP规定头域的键名(头名)是大小写敏感的,请求的解析器通过规范化头域的键名来实现这点。
    // 在客户端的请求,可能会被自动添加或重写Header中的特定的头,参见Request.Write方法。
    Header Header
    // Body是请求的主体。
    //
    // 在客户端,如果Body是nil表示该请求没有主体买入GET请求。
    // Client的Transport字段会负责调用Body的Close方法。
    //
    // 在服务端,Body字段总是非nil的;但在没有主体时,读取Body会立刻返回EOF。
    // Server会关闭请求的主体,ServeHTTP处理器不需要关闭Body字段。
    Body io.ReadCloser
    // ContentLength记录相关内容的长度。
    // 如果为-1,表示长度未知,如果>=0,表示可以从Body字段读取ContentLength字节数据。
    // 在客户端,如果Body非nil而该字段为0,表示不知道Body的长度。
    ContentLength int64
    // TransferEncoding按从最外到最里的顺序列出传输编码,空切片表示"identity"编码。
    // 本字段一般会被忽略。当发送或接受请求时,会自动添加或移除"chunked"传输编码。
    TransferEncoding []string
    // Close在服务端指定是否在回复请求后关闭连接,在客户端指定是否在发送请求后关闭连接。
    Close bool
    // 在服务端,Host指定URL会在其上寻找资源的主机。
    // 根据RFC 2616,该值可以是Host头的值,或者URL自身提供的主机名。
    // Host的格式可以是"host:port"。
    //
    // 在客户端,请求的Host字段(可选地)用来重写请求的Host头。
    // 如过该字段为"",Request.Write方法会使用URL字段的Host。
    Host string
    // Form是解析好的表单数据,包括URL字段的query参数和POST或PUT的表单数据。
    // 本字段只有在调用ParseForm后才有效。在客户端,会忽略请求中的本字段而使用Body替代。
    Form url.Values
    // PostForm是解析好的POST或PUT的表单数据。
    // 本字段只有在调用ParseForm后才有效。在客户端,会忽略请求中的本字段而使用Body替代。
    PostForm url.Values
    // MultipartForm是解析好的多部件表单,包括上传的文件。
    // 本字段只有在调用ParseMultipartForm后才有效。
    // 在客户端,会忽略请求中的本字段而使用Body替代。
    MultipartForm *multipart.Form
    // Trailer指定了会在请求主体之后发送的额外的头域。
    //
    // 在服务端,Trailer字段必须初始化为只有trailer键,所有键都对应nil值。
    // (客户端会声明哪些trailer会发送)
    // 在处理器从Body读取时,不能使用本字段。
    // 在从Body的读取返回EOF后,Trailer字段会被更新完毕并包含非nil的值。
    // (如果客户端发送了这些键值对),此时才可以访问本字段。
    //
    // 在客户端,Trail必须初始化为一个包含将要发送的键值对的映射。(值可以是nil或其终值)
    // ContentLength字段必须是0或-1,以启用"chunked"传输编码发送请求。
    // 在开始发送请求后,Trailer可以在读取请求主体期间被修改,
    // 一旦请求主体返回EOF,调用者就不可再修改Trailer。
    //
    // 很少有HTTP客户端、服务端或代理支持HTTP trailer。
    Trailer Header
    // RemoteAddr允许HTTP服务器和其他软件记录该请求的来源地址,一般用于日志。
    // 本字段不是ReadRequest函数填写的,也没有定义格式。
    // 本包的HTTP服务器会在调用处理器之前设置RemoteAddr为"IP:port"格式的地址。
    // 客户端会忽略请求中的RemoteAddr字段。
    RemoteAddr string
    // RequestURI是被客户端发送到服务端的请求的请求行中未修改的请求URI
    // (参见RFC 2616, Section 5.1)
    // 一般应使用URI字段,在客户端设置请求的本字段会导致错误。
    RequestURI string
    // TLS字段允许HTTP服务器和其他软件记录接收到该请求的TLS连接的信息
    // 本字段不是ReadRequest函数填写的。
    // 对启用了TLS的连接,本包的HTTP服务器会在调用处理器之前设置TLS字段,否则将设TLS为nil。
    // 客户端会忽略请求中的TLS字段。
    TLS *tls.ConnectionState
}

Request类型代表一个服务端接受到的或者客户端发送出去的HTTP请求

Request各字段的意义和用途在服务端和客户端是不同的。除了字段本身上方文档,还可参见Request.Write方法和RoundTripper接口的文档。

 NewRequest

func NewRequest(method, urlStr string, body io.Reader) (*Request, error)

NewRequest使用指定的方法、网址和可选的主题创建并返回一个新的*Request。

如果body参数实现了io.Closer接口,Request返回值的Body 字段会被设置为body,并会被Client类型的Do、Post和PostFOrm方法以及Transport.RoundTrip方法关闭。

对于该NewRequest方法的三个参数的不同输入对返回request中相应数据的影响:

1)NewRequest中urlStr参数对req.Host值的影响,举例说明:

package main 
import(
    "fmt"
    "net/http"
)

var newRequestHostTests = []struct {
    in, out string
}{
    {"http://www.example.com/", "www.example.com"},
    {"http://www.example.com:8080/", "www.example.com:8080"},

    {"http://192.168.0.1/", "192.168.0.1"},
    {"http://192.168.0.1:8080/", "192.168.0.1:8080"},
    {"http://192.168.0.1:/", "192.168.0.1"},

    {"http://[fe80::1]/", "[fe80::1]"},
    {"http://[fe80::1]:8080/", "[fe80::1]:8080"},
    {"http://[fe80::1%25en0]/", "[fe80::1%en0]"},
    {"http://[fe80::1%25en0]:8080/", "[fe80::1%en0]:8080"},
    {"http://[fe80::1%25en0]:/", "[fe80::1%en0]"},
}


func main() {
    for i, tt := range newRequestHostTests {
        req, err := http.NewRequest("GET", tt.in, nil)
        if err != nil {
            fmt.Printf("#%v: %v", i, err)
            continue
        }
        if req.Host != tt.out { //返回结果中没有报错,则说明req.Host == tt.out
            fmt.Printf("got %q; want %q", req.Host, tt.out)
        }
    }
}

2)NewRequest中method参数对req.Method值的影响,举例:

package main 
import(
    "fmt"
    "net/http"
    "strings"
)

func main() {
    _, err := http.NewRequest("bad method", "http://foo.com/", nil)
    if err == nil { //返回没有输出则说明"bad method"是错误的请求方法,err != nil
        fmt.Println("expected error from NewRequest with invalid method")
    }
    fmt.Println(err) //net/http: invalid method "bad method"

    req, err := http.NewRequest("GET", "http://foo.example/", nil)
    if err != nil { //当你使用的是正确的请求方法时,就不会出现错误
        fmt.Println(err)
    }
    req.Method = "bad method" //将请求方法改成错误的"bad method"

    _, err = http.DefaultClient.Do(req) //然后发送该请求,然后会返回HTTP response和error
    if err == nil || !strings.Contains(err.Error(), "invalid method") { //没有返回,则说明返回的err !=  nil或err中包含字符串"invalid method"
        fmt.Printf("Transport error = %v; want invalid method\n", err)
    }
    fmt.Println(err) //bad method http://foo.example/: net/http: invalid method "bad method"

    req, err = http.NewRequest("", "http://foo.com/", nil)
    fmt.Println(req) //&{GET http://foo.com/ HTTP/1.1 1 1 map[] <nil> <nil> 0 [] false foo.com map[] map[] <nil> map[]   <nil> <nil> <nil> <nil>}
    if err != nil {//没返回说明err == nil,说明请求方法可以为空
        fmt.Printf("NewRequest(empty method) = %v; want nil\n", err)
    } else if req.Method != "GET" { //当请求方法为空时,会默认使用的是"GET方法"
        fmt.Printf("NewRequest(empty method) has method %q; want GET\n", req.Method)
    }
}

3)NewRequest中body参数对req.Body、req.ContentLength值的影响:

package main 
import(
    "fmt"
    "net/http"
    "strings"
    "bytes"
    "io"
)

func main() {
    readByte := func(r io.Reader) io.Reader {
        var b [1]byte
        r.Read(b[:])
        return r
    }
    tests := []struct {
        r    io.Reader
        want int64
    }{
        {bytes.NewReader([]byte("123")), 3},
        {bytes.NewBuffer([]byte("1234")), 4},
        {strings.NewReader("12345"), 5},
        {strings.NewReader(""), 0},

        // Not detected. During Go 1.8 we tried to make these set to -1, but
        // due to Issue 18117, we keep these returning 0, even though they're
        // unknown.
        {struct{ io.Reader }{strings.NewReader("xyz")}, 0},
        {io.NewSectionReader(strings.NewReader("x"), 0, 6), 0},
        {readByte(io.NewSectionReader(strings.NewReader("xy"), 0, 6)), 0},
    }
    for i, tt := range tests {
        req, err := http.NewRequest("POST", "http://localhost/", tt.r)
        fmt.Println(req.Body)
        if err != nil {
            fmt.Println(err)
        }
        if req.ContentLength != tt.want {//没有返回,说明req.ContentLength == tt.want
            fmt.Printf("test[%d]: ContentLength(%T) = %d; want %d", i, tt.r, req.ContentLength, tt.want)
        }
    }
}

req.Body返回:

userdeMBP:go-learning user$ go run test.go
{0xc000094cc0}
{1234}
{0xc0000ac360}
{}
{{0xc0000ac3a0}}
{0xc000094cf0}
{0xc000094d20}

 

request请求中与Cookie相关的方法:

 AddCookie

func (r *Request) AddCookie(c *Cookie)

AddCookie向请求中添加一个cookie。按照RFC 6265 section 5.4的跪地,AddCookie不会添加超过一个Cookie头字段。这表示所有的cookie都写在同一行,用分号分隔(cookie内部用逗号分隔属性)。

 Cookies

func (r *Request) Cookies() []*Cookie

Cookies解析并返回该请求的Cookie头设置的cookie。

 Cookie

func (r *Request) Cookie(name string) (*Cookie, error)

Cookie返回请求中名为name的cookie,如果未找到该cookie会返回nil, ErrNoCookie。

举例:

package main 
import(
    "fmt"
    "net/http"
)

var addCookieTests = []struct {
    Cookies []*http.Cookie
    Raw     string
}{
    {
        []*http.Cookie{},
        "",
    },
    {
        []*http.Cookie{{Name: "cookie-1", Value: "v$11"}},
        "cookie-1=v$11",
    },
    {
        []*http.Cookie{
            {Name: "cookie-1", Value: "v$21"},
            {Name: "cookie-2", Value: "v$2"},
            {Name: "cookie-3", Value: "v$3"},
        },
        "cookie-1=v$21; cookie-2=v$2; cookie-3=v$3",
    },
}


func main() {
    for i, tt := range addCookieTests {
        req, _ := http.NewRequest("GET", "http://example.com/", nil)
        for _, c := range tt.Cookies {
            req.AddCookie(c)
        }
        //没有报错,则说明添加进的Cookie的值与给的Raw的字符串的值相同
     //得到Cookie的值可以使用req.Header.Get("Cookie"),也可以使用下面的req.Cookies() if g := req.Header.Get("Cookie"); g != tt.Raw { fmt.Printf("Test %d:\nwant: %s\n got: %s\n", i, tt.Raw, g) continue } fmt.Println(req.Cookies()) value, _ := req.Cookie("cookie-1") fmt.Println(value) } }

返回:

userdeMBP:go-learning user$ go run test.go
[]

[cookie-1=v$11]
cookie-1=v$11
[cookie-1=v$21 cookie-2=v$2 cookie-3=v$3]
cookie-1=v$21

 

其他方法:

 ReadRequest

func ReadRequest(b *bufio.Reader) (req *Request, err error)

ReadRequest从b读取并解析出一个HTTP请求。(本函数主要用在服务端从下层获取请求)

举例:

package main 
import(
    "fmt"
    "net/http"
    "strings"
    "io"
    "reflect"
    "bufio"
)

var readRequestErrorTests = []struct {
    in  string
    err string

    header http.Header
}{
    0: {"GET / HTTP/1.1\r\nheader:foo\r\n\r\n", "", http.Header{"Header": {"foo"}}},
    1: {"GET / HTTP/1.1\r\nheader:foo\r\n", io.ErrUnexpectedEOF.Error(), nil},
    2: {"", io.EOF.Error(), nil},
    3: {
        in:  "HEAD / HTTP/1.1\r\nContent-Length:4\r\n\r\n",
        err: "http: method cannot contain a Content-Length",
    },
    4: {
        in:     "HEAD / HTTP/1.1\r\n\r\n",
        header: http.Header{},
    },

    // Multiple Content-Length values should either be
    // deduplicated if same or reject otherwise
    // See Issue 16490.
    5: {
        in:  "POST / HTTP/1.1\r\nContent-Length: 10\r\nContent-Length: 0\r\n\r\nGopher hey\r\n",
        err: "cannot contain multiple Content-Length headers",
    },
    6: {
        in:  "POST / HTTP/1.1\r\nContent-Length: 10\r\nContent-Length: 6\r\n\r\nGopher\r\n",
        err: "cannot contain multiple Content-Length headers",
    },
    7: {
        in:     "PUT / HTTP/1.1\r\nContent-Length: 6 \r\nContent-Length: 6\r\nContent-Length:6\r\n\r\nGopher\r\n",
        err:    "",
        header: http.Header{"Content-Length": {"6"}},
    },
    8: {
        in:  "PUT / HTTP/1.1\r\nContent-Length: 1\r\nContent-Length: 6 \r\n\r\n",
        err: "cannot contain multiple Content-Length headers",
    },
    9: {
        in:  "POST / HTTP/1.1\r\nContent-Length:\r\nContent-Length: 3\r\n\r\n",
        err: "cannot contain multiple Content-Length headers",
    },
    10: {
        in:     "HEAD / HTTP/1.1\r\nContent-Length:0\r\nContent-Length: 0\r\n\r\n",
        header: http.Header{"Content-Length": {"0"}},
    },
}


func main() {
    for i, tt := range readRequestErrorTests {
        req, err := http.ReadRequest(bufio.NewReader(strings.NewReader(tt.in)))
        if err == nil { //从返回可以看出,只有0,4,7,10返回的err是nil,即能够成功解析出一个HTTP请求
            fmt.Println(i, " : ", req)
            if tt.err != "" {
                fmt.Printf("#%d: got nil err; want %q\n", i, tt.err)
            }

            if !reflect.DeepEqual(tt.header, req.Header) {//如果发现两者不同
                fmt.Printf("#%d: gotHeader: %q wantHeader: %q\n", i, req.Header, tt.header)
            }
            continue
        }

        if tt.err == "" || !strings.Contains(err.Error(), tt.err) { //如果tt.err != "" 或者 返回的err中包含tt.err的内容,则不会输出下面的字符串
            fmt.Printf("%d: got error = %v; want %v\n", i, err, tt.err)
        }
        fmt.Println(i, "when err is not nil : ", err)
    }
}

返回:

userdeMBP:go-learning user$ go run test.go
0  :  &{GET / HTTP/1.1 1 1 map[Header:[foo]] {} <nil> 0 [] false  map[] map[] <nil> map[]  / <nil> <nil> <nil> <nil>}
1 when err is not nil :  unexpected EOF
2 when err is not nil :  EOF
3 when err is not nil :  http: method cannot contain a Content-Length; got ["4"]
4  :  &{HEAD / HTTP/1.1 1 1 map[] {} <nil> 0 [] false  map[] map[] <nil> map[]  / <nil> <nil> <nil> <nil>}
5 when err is not nil :  http: message cannot contain multiple Content-Length headers; got ["10" "0"]
6 when err is not nil :  http: message cannot contain multiple Content-Length headers; got ["10" "6"]
7  :  &{PUT / HTTP/1.1 1 1 map[Content-Length:[6]] 0xc000096200 <nil> 6 [] false  map[] map[] <nil> map[]  / <nil> <nil> <nil> <nil>}
8 when err is not nil :  http: message cannot contain multiple Content-Length headers; got ["1" "6"]
9 when err is not nil :  http: message cannot contain multiple Content-Length headers; got ["" "3"]
10  :  &{HEAD / HTTP/1.1 1 1 map[Content-Length:[0]] {} <nil> 0 [] false  map[] map[] <nil> map[]  / <nil> <nil> <nil> <nil>}

 

 ProtoAtLeast

func (r *Request) ProtoAtLeast(major, minor int) bool

ProtoAtLeast报告该请求使用的HTTP协议版本至少是major.minor。

 UserAgent

func (r *Request) UserAgent() string

UserAgent返回请求中的客户端用户代理信息(请求的User-Agent头)。

 Referer

func (r *Request) Referer() string

Referer返回请求中的访问来路信息。(请求的Referer头)即得到访问的信息的来源,比如某个链接的来源地址

Referer在请求中就是拼错了的,这是HTTP早期就有的错误。该值也可以从用Header["Referer"]获取; 让获取Referer字段变成方法的好处是,编译器可以诊断使用正确单词拼法的req.Referrer()的程序,但却不能诊断使用Header["Referrer"]的程序。

举例:

package main 
import(
    "fmt"
    "net/http"
)

func main() {
    req, _ := http.NewRequest("GET", "http://www.baidu.com/", nil)
    req.Header.Set("User-Agent", "Mozilla/5.0")

    //没有解析前req.Form和req.PostForm中的值为空
    fmt.Println(req.ProtoAtLeast(1,0)) //true
    fmt.Println(req.ProtoAtLeast(1,1)) //true
    fmt.Println(req.UserAgent()) //Mozilla/5.0
    fmt.Println(req.Referer())//因为没有来源,为空
}

 

 SetBasicAuth

func (r *Request) SetBasicAuth(username, password string)

SetBasicAuth使用提供的用户名和密码,采用HTTP基本认证,设置请求的Authorization头。HTTP基本认证会明码传送用户名和密码,即用户名和密码是不加密的

func (r *Request) BasicAuth

func (r *Request) BasicAuth() (username, password string, ok bool)

如果请求使用http基本认证,返回request header中的用户名和密码。

举例:

package main 
import(
    "fmt"
    "net/http"
)

type getBasicAuthTest struct {
    username, password string
    ok                 bool
}

type basicAuthCredentialsTest struct {
    username, password string
}

var getBasicAuthTests = []struct {
    username, password string
    ok                 bool
}{
    {"Aladdin", "open sesame", true},
    {"Aladdin", "open:sesame", true},
    {"", "", true},
}


func main() {
    for _, tt := range getBasicAuthTests {
        r, _ := http.NewRequest("GET", "http://example.com/", nil)
        r.SetBasicAuth(tt.username, tt.password)
        fmt.Println(r.Header.Get("Authorization"))//在Header中授权信息是加密过的,返回:
        // Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
        // Basic QWxhZGRpbjpvcGVuOnNlc2FtZQ==
        // Basic Og==

        username, password, ok := r.BasicAuth()
        if ok != tt.ok || username != tt.username || password != tt.password { //满足其中的任意一种情况都说明有错
            fmt.Printf("BasicAuth() = %#v, want %#v", getBasicAuthTest{username, password, ok},
                getBasicAuthTest{tt.username, tt.password, tt.ok})
        }
    }

    //没有授权的request
    r, _ := http.NewRequest("GET", "http://example.com/", nil)
    username, password, ok := r.BasicAuth()
    fmt.Println(username, password, ok) //因为没有授权,返回 "" "" false
    if ok {
        fmt.Printf("expected false from BasicAuth when the request is unauthenticated")
    }
    want := basicAuthCredentialsTest{"", ""} //没有授权返回的username和password都应该为""
    if username != want.username || password != want.password {
        fmt.Printf("expected credentials: %#v when the request is unauthenticated, got %#v",
            want, basicAuthCredentialsTest{username, password})
    }
}

对授权信息进行手动加密后再添加到Header中:

package main 
import(
    "fmt"
    "net/http"
    "encoding/base64"
)


type getBasicAuthTest struct {
    username, password string
    ok                 bool
}

var parseBasicAuthTests = []struct {
    header, username, password string
    ok                         bool
}{
    {"Basic " + base64.StdEncoding.EncodeToString([]byte("Aladdin:open sesame")), "Aladdin", "open sesame", true},

    // 大小写不影响
    {"BASIC " + base64.StdEncoding.EncodeToString([]byte("Aladdin:open sesame")), "Aladdin", "open sesame", true},
    {"basic " + base64.StdEncoding.EncodeToString([]byte("Aladdin:open sesame")), "Aladdin", "open sesame", true},

    {"Basic " + base64.StdEncoding.EncodeToString([]byte("Aladdin:open:sesame")), "Aladdin", "open:sesame", true},
    {"Basic " + base64.StdEncoding.EncodeToString([]byte(":")), "", "", true},
    {"Basic" + base64.StdEncoding.EncodeToString([]byte("Aladdin:open sesame")), "", "", false},
    {base64.StdEncoding.EncodeToString([]byte("Aladdin:open sesame")), "", "", false},
    {"Basic ", "", "", false},
    {"Basic Aladdin:open sesame", "", "", false},
    {`Digest username="Aladdin"`, "", "", false},
}


func main() {
    for _, tt := range parseBasicAuthTests {
        r, _ := http.NewRequest("GET", "http://example.com/", nil)
        r.Header.Set("Authorization", tt.header)
        fmt.Println(r.Header.Get("Authorization")) //得到的是加密后的结果
        
        username, password, ok := r.BasicAuth()
        if ok != tt.ok || username != tt.username || password != tt.password {
            fmt.Printf("BasicAuth() = %#v, want %#v", getBasicAuthTest{username, password, ok},
                getBasicAuthTest{tt.username, tt.password, tt.ok})
        }
    }
}

 

 Write

func (r *Request) Write(w io.Writer) error

Write方法以有线格式将HTTP/1.1请求写入w(用于将请求写入下层TCPConn等)。本方法会考虑请求的如下字段:

Host
URL
Method (defaults to "GET")
Header
ContentLength
TransferEncoding
Body

如果存在Body,ContentLength字段<= 0且TransferEncoding字段未显式设置为["identity"],Write方法会显式添加"Transfer-Encoding: chunked"到请求的头域。Body字段会在发送完请求后关闭。

 WriteProxy

func (r *Request) WriteProxy(w io.Writer) error

WriteProxy类似Write但会将请求以HTTP代理期望的格式发送。

尤其是,按照RFC 2616 Section 5.1.2,WriteProxy会使用绝对URI(包括协议和主机名)来初始化请求的第1行(Request-URI行)。无论何种情况,WriteProxy都会使用r.Host或r.URL.Host设置Host头。

举例:

package main 
import(
    "fmt"
    "net/http"
)

type logWrites struct {
    dst *[]string
}

//实现Write函数说明logWrites实现了io.Writer接口
func (l logWrites) Write(p []byte) (n int, err error) {
    *l.dst = append(*l.dst, string(p))
    return len(p), nil
}


func main() {
    got1 := []string{}
    got2 := []string{}
    req, _ := http.NewRequest("GET", "http://foo.com/", nil)
    fmt.Println(req)
    req.Write(logWrites{&got1}) //logWrites{&got}得到的是一个io.Writer对象作为req.Write的参数,这样就会自动调用func (l logWrites) Write(p []byte),将req写入got中
    req.WriteProxy(logWrites{&got2}) //logWrites{&got}得到的是一个io.Writer对象作为req.Write的参数,这样就会自动调用func (l logWrites) Write(p []byte),将req写入got中
    fmt.Println(got1)
    fmt.Println(got2)
}

 

 ParseForm

func (r *Request) ParseForm() error

ParseForm解析URL中的查询字符串,并将解析结果更新到r.Form字段。

对于POST或PUT请求,ParseForm还会将body当作表单解析,并将结果既更新到r.PostForm也更新到r.Form。解析结果中,POST或PUT请求主体要优先于URL查询字符串(同名变量,主体的值在查询字符串的值前面)。

如果请求的主体的大小没有被MaxBytesReader函数设定限制,其大小默认限制为开头10MB。

ParseMultipartForm会自动调用ParseForm。重复调用本方法是无意义的。

⚠️ParseForm方法用来解析表单提供的数据,即content-type 为 x-www-form-urlencode的数据。

对于form-data的格式的数据,ParseForm的方法只会解析url中的参数,并不会解析body中的参数。因此当请求的content-type为form-data的时候,ParseFrom则需要改成 MutilpartFrom,否则r.From是读取不到body的内容,只能读取到query string中的内容。

举例:

package main 
import(
    "fmt"
    "net/http"
    "strings"
)

func main() {
    req, _ := http.NewRequest("POST", "http://www.google.com/search?q=foo&q=bar&both=x&prio=1&orphan=nope&empty=not",
        strings.NewReader("z=post&both=y&prio=2&=nokey&orphan;empty=&"))
    req.Header.Set("Content-Type", "application/x-www-form-urlencoded; param=value")
    //没有解析前req.Form和req.PostForm中的值为空
    fmt.Println(req)
    fmt.Println(req.Form)
    fmt.Println(req.PostForm)
    fmt.Println()
    
    //解析后对应的值才写入req.Form和req.PostForm
    req.ParseForm()
    fmt.Println(req)
    fmt.Println(req.Form)
    fmt.Println(req.PostForm)
}

返回:

userdeMBP:go-learning user$ go run test.go
&{POST http://www.google.com/search?q=foo&q=bar&both=x&prio=1&orphan=nope&empty=not HTTP/1.1 1 1 map[Content-Type:[application/x-www-form-urlencoded; param=value]] {0xc00000c360} 0x11e9430 42 [] false www.google.com map[] map[] <nil> map[]   <nil> <nil> <nil> <nil>}
map[]
map[]

&{POST http://www.google.com/search?q=foo&q=bar&both=x&prio=1&orphan=nope&empty=not HTTP/1.1 1 1 map[Content-Type:[application/x-www-form-urlencoded; param=value]] {0xc00000c360} 0x11e9430 42 [] false www.google.com map[prio:[2 1] :[nokey] q:[foo bar] orphan:[ nope] empty:[ not] z:[post] both:[y x]] map[z:[post] both:[y] prio:[2] :[nokey] orphan:[] empty:[]] <nil> map[]   <nil> <nil> <nil> <nil>}
map[orphan:[ nope] empty:[ not] z:[post] both:[y x] prio:[2 1] :[nokey] q:[foo bar]]
map[orphan:[] empty:[] z:[post] both:[y] prio:[2] :[nokey]]

 

 ParseMultipartForm

func (r *Request) ParseMultipartForm(maxMemory int64) error

ParseMultipartForm将请求的主体作为multipart/form-data解析。请求的整个主体都会被解析,得到的文件记录最多maxMemery字节保存在内存,其余部分保存在硬盘的temp文件里。如果必要,ParseMultipartForm会自行调用ParseForm。重复调用本方法是无意义的。

form-data格式用得最多方式就是在图片上传的时候。MultipartForm.Value是post的body字段数据,MultipartForm.File则包含了图片数据

举例:

package main 
import(
    "fmt"
    "net/http"
    "strings"
    "io/ioutil"
    "net/url"
    "reflect"
)

func main() {
    //说明写入PostForm中的字段name-value,field2有两个value,一个作为Form,一个作为PostForm
    //注意value1、value2和binary data上面的空行不能够删除,否则会报错:malformed MIME header initial line
    postData :=
        `--xxx
Content-Disposition: form-data; name="field1" 

value1
--xxx
Content-Disposition: form-data; name="field2"

value2
--xxx
Content-Disposition: form-data; name="file"; filename="file"
Content-Type: application/octet-stream
Content-Transfer-Encoding: binary

binary data
--xxx--
`
    req := &http.Request{
        Method: "POST",
        Header: http.Header{"Content-Type": {`multipart/form-data; boundary=xxx`}},
        Body:   ioutil.NopCloser(strings.NewReader(postData)), //NopCloser用一个无操作的Close方法包装输入参数然后返回一个ReadCloser接口
    }
    // req.ParseForm()//在POST表单中,这种解析是没有用的,要使用下面的,当然,这个只是为了查看目前的表单值,其实不应该在这里解析
    err := req.ParseMultipartForm(10000)
    if err != nil {
        fmt.Printf("unexpected multipart error %v\n", err)
    }
    fmt.Println(req)
    fmt.Println(req.Body)
    fmt.Println(req.Form) //map[field1:[value1] field2:[value2]]
    fmt.Println(req.PostForm) //map[field1:[value1] field2:[value2]],现在两者值是相同的,但是下面req.Form.Add后就变了
    fmt.Println()

    initialFormItems := map[string]string{
        "language": "Go",
        "name":     "gopher",
        "skill":    "go-ing",
        "field2":   "initial-value2",
    }

    req.Form = make(url.Values) //url.Values即map[string][]string
    for k, v := range initialFormItems {
        req.Form.Add(k, v)
    }

    //应该解析的位置是这里,否则会导致最终的结果与构想的结果不同
    // err := req.ParseMultipartForm(10000)
    // if err != nil {
    //     fmt.Printf("unexpected multipart error %v\n", err)
    // }
    fmt.Println(req)
    fmt.Println(req.Body)
    fmt.Println(req.Form) //map[language:[Go] name:[gopher] skill:[go-ing] field2:[initial-value2]]
    fmt.Println(req.PostForm) //map[field1:[value1] field2:[value2]]

    wantForm := url.Values{
        "language": []string{"Go"},
        "name":     []string{"gopher"},
        "skill":    []string{"go-ing"},
        "field1":   []string{"value1"},
        "field2":   []string{"initial-value2", "value2"},
    }
    if !reflect.DeepEqual(req.Form, wantForm) { //这里会报出不相等的结果
        fmt.Printf("req.Form = %v, want %v\n", req.Form, wantForm)
    }

    wantPostForm := url.Values{
        "field1": []string{"value1"},
        "field2": []string{"value2"},
    }
    if !reflect.DeepEqual(req.PostForm, wantPostForm) {
        fmt.Printf("req.PostForm = %v, want %v\n", req.PostForm, wantPostForm)
    }
}

返回:

userdeMBP:go-learning user$ go run test.go
&{POST <nil>  0 0 map[Content-Type:[multipart/form-data; boundary=xxx]] {0xc00000c3a0} <nil> 0 [] false  map[field2:[value2] field1:[value1]] map[field1:[value1] field2:[value2]] 0xc000010ce0 map[]   <nil> <nil> <nil> <nil>}
{0xc00000c3a0}
map[field1:[value1] field2:[value2]]
map[field1:[value1] field2:[value2]]

&{POST <nil>  0 0 map[Content-Type:[multipart/form-data; boundary=xxx]] {0xc00000c3a0} <nil> 0 [] false  map[language:[Go] name:[gopher] skill:[go-ing] field2:[initial-value2]] map[field1:[value1] field2:[value2]] 0xc000010ce0 map[]   <nil> <nil> <nil> <nil>}
{0xc00000c3a0}
map[language:[Go] name:[gopher] skill:[go-ing] field2:[initial-value2]]
map[field1:[value1] field2:[value2]]
req.Form = map[language:[Go] name:[gopher] skill:[go-ing] field2:[initial-value2]], want map[language:[Go] name:[gopher] skill:[go-ing] field1:[value1] field2:[initial-value2 value2]]

正确为:

package main 
import(
    "fmt"
    "net/http"
    "strings"
    "io/ioutil"
    "net/url"
    "reflect"
)

func main() {
    //说明写入PostForm中的字段name-value,field2有两个value,一个作为Form,一个作为PostForm
    //注意value1、value2和binary data上面的空行不能够删除,否则会报错:malformed MIME header initial line
    postData :=
        `--xxx
Content-Disposition: form-data; name="field1" 

value1
--xxx
Content-Disposition: form-data; name="field2"

value2
--xxx
Content-Disposition: form-data; name="file"; filename="file"
Content-Type: application/octet-stream
Content-Transfer-Encoding: binary

binary data
--xxx--
`
    req := &http.Request{
        Method: "POST",
        Header: http.Header{"Content-Type": {`multipart/form-data; boundary=xxx`}},
        Body:   ioutil.NopCloser(strings.NewReader(postData)), //NopCloser用一个无操作的Close方法包装输入参数然后返回一个ReadCloser接口
    }

    initialFormItems := map[string]string{
        "language": "Go",
        "name":     "gopher",
        "skill":    "go-ing",
        "field2":   "initial-value2",
    }

    req.Form = make(url.Values) //url.Values即map[string][]string
    for k, v := range initialFormItems {
        req.Form.Add(k, v)
    }

    //应该解析的位置是这里,否则会导致最终的结果与构想的结果不同
    err := req.ParseMultipartForm(10000)
    if err != nil {
        fmt.Printf("unexpected multipart error %v\n", err)
    }
    fmt.Println(req)
    fmt.Println(req.Body)
    fmt.Println(req.Form) //map[language:[Go] name:[gopher] skill:[go-ing] field2:[initial-value2 value2] field1:[value1]]
    fmt.Println(req.PostForm) //map[field1:[value1] field2:[value2]]

    wantForm := url.Values{
        "language": []string{"Go"},
        "name":     []string{"gopher"},
        "skill":    []string{"go-ing"},
        "field1":   []string{"value1"},
        "field2":   []string{"initial-value2", "value2"},
    }
    if !reflect.DeepEqual(req.Form, wantForm) {
        fmt.Printf("req.Form = %v, want %v\n", req.Form, wantForm)
    }

    wantPostForm := url.Values{
        "field1": []string{"value1"},
        "field2": []string{"value2"},
    }
    if !reflect.DeepEqual(req.PostForm, wantPostForm) {
        fmt.Printf("req.PostForm = %v, want %v\n", req.PostForm, wantPostForm)
    }
}

返回:

userdeMBP:go-learning user$ go run test.go
&{POST <nil>  0 0 map[Content-Type:[multipart/form-data; boundary=xxx]] {0xc0000ac340} <nil> 0 [] false  map[name:[gopher] skill:[go-ing] field2:[initial-value2 value2] field1:[value1] language:[Go]] map[field1:[value1] field2:[value2]] 0xc000090d00 map[]   <nil> <nil> <nil> <nil>}
{0xc0000ac340}
map[language:[Go] name:[gopher] skill:[go-ing] field2:[initial-value2 value2] field1:[value1]]
map[field1:[value1] field2:[value2]]

 

 FormValue

func (r *Request) FormValue(key string) string

FormValue返回key为键查询r.Form字段得到结果[]string切片的第一个值。POST和PUT主体中的同名参数优先于URL查询字符串。如果必要,本函数会隐式调用ParseMultipartForm和ParseForm

 PostFormValue

func (r *Request) PostFormValue(key string) string

PostFormValue返回key为键查询r.PostForm字段得到结果[]string切片的第一个值。如果必要,本函数会隐式调用ParseMultipartForm和ParseForm

举例:

package main 
import(
    "fmt"
    "net/http"
    "strings"
    "reflect"
)

func main() {
    req, _ := http.NewRequest("POST", "http://www.google.com/search?q=foo&q=bar&both=x&prio=1&orphan=nope&empty=not",
        strings.NewReader("z=post&both=y&prio=2&=nokey&orphan;empty=&"))
    req.Header.Set("Content-Type", "application/x-www-form-urlencoded; param=value")

    if q := req.FormValue("q"); q != "foo" {
        fmt.Printf(`req.FormValue("q") = %q, want "foo"`, q)
    }
   //因为上面的req.FormValue方法会隐式解析,所以下面能够得到值 fmt.Println(req) fmt.Println(req.Form) fmt.Println(req.PostForm)
if z := req.FormValue("z"); z != "post" { fmt.Printf(`req.FormValue("z") = %q, want "post"`, z) } if bq, found := req.PostForm["q"]; found {//PostForm 中没有"q" fmt.Printf(`req.PostForm["q"] = %q, want no entry in map`, bq) } if bz := req.PostFormValue("z"); bz != "post" { fmt.Printf(`req.PostFormValue("z") = %q, want "post"`, bz) } if qs := req.Form["q"]; !reflect.DeepEqual(qs, []string{"foo", "bar"}) { fmt.Printf(`req.Form["q"] = %q, want ["foo", "bar"]`, qs) } if both := req.Form["both"]; !reflect.DeepEqual(both, []string{"y", "x"}) { fmt.Printf(`req.Form["both"] = %q, want ["y", "x"]`, both) } if prio := req.FormValue("prio"); prio != "2" { fmt.Printf(`req.FormValue("prio") = %q, want "2" (from body)`, prio) } if orphan := req.Form["orphan"]; !reflect.DeepEqual(orphan, []string{"", "nope"}) { fmt.Printf(`req.FormValue("orphan") = %q, want "" (from body)`, orphan) } if empty := req.Form["empty"]; !reflect.DeepEqual(empty, []string{"", "not"}) { fmt.Printf(`req.FormValue("empty") = %q, want "" (from body)`, empty) } if nokey := req.Form[""]; !reflect.DeepEqual(nokey, []string{"nokey"}) { fmt.Printf(`req.FormValue("nokey") = %q, want "nokey" (from body)`, nokey) } }

返回:

userdeMBP:go-learning user$ go run test.go
&{POST http://www.google.com/search?q=foo&q=bar&both=x&prio=1&orphan=nope&empty=not HTTP/1.1 1 1 map[Content-Type:[application/x-www-form-urlencoded; param=value]] {0xc00000c380} 0x11ef3e0 42 [] false www.google.com map[:[nokey] orphan:[ nope] empty:[ not] z:[post] q:[foo bar] both:[y x] prio:[2 1]] map[empty:[] z:[post] both:[y] prio:[2] :[nokey] orphan:[]] <nil> map[]   <nil> <nil> <nil> <nil>}
map[z:[post] q:[foo bar] both:[y x] prio:[2 1] :[nokey] orphan:[ nope] empty:[ not]]
map[z:[post] both:[y] prio:[2] :[nokey] orphan:[] empty:[]]

 

 FormFile

func (r *Request) FormFile(key string) (multipart.File, *multipart.FileHeader, error)

FormFile返回以key为键查询r.MultipartForm字段得到结果中的第一个文件和它的信息。如果必要,本函数会隐式调用ParseMultipartForm和ParseForm。查询失败会返回ErrMissingFile错误。

举例:

package main 
import(
    "fmt"
    "net/http"
    "strings"
    "io/ioutil"
    "net/url"
    "log"
)

func main() {
    //说明写入PostForm中的字段name-value,field2有两个value,一个作为Form,一个作为PostForm
    //filename指明写入的文件
    //注意value1、value2和binary data上面的空行不能够删除,否则会报错:malformed MIME header initial line
    postData :=
        `--xxx
Content-Disposition: form-data; name="field1" 

value1
--xxx
Content-Disposition: form-data; name="field2"

value2
--xxx
Content-Disposition: form-data; name="file"; filename="file"
Content-Type: application/octet-stream
Content-Transfer-Encoding: binary

binary data
--xxx--
`
    req := &http.Request{
        Method: "POST",
        Header: http.Header{"Content-Type": {`multipart/form-data; boundary=xxx`}},
        Body:   ioutil.NopCloser(strings.NewReader(postData)), //NopCloser用一个无操作的Close方法包装输入参数然后返回一个ReadCloser接口
    }

    initialFormItems := map[string]string{
        "language": "Go",
        "name":     "gopher",
        "skill":    "go-ing",
        "field2":   "initial-value2",
    }

    req.Form = make(url.Values) //url.Values即map[string][]string
    for k, v := range initialFormItems {
        req.Form.Add(k, v)
    }

    //应该解析的位置是这里,否则会导致最终的结果与构想的结果不同
    err := req.ParseMultipartForm(10000)
    if err != nil {
        fmt.Printf("unexpected multipart error %v\n", err)
    }

    fmt.Println(req.Form) //map[language:[Go] name:[gopher] skill:[go-ing] field2:[initial-value2 value2] field1:[value1]]
    fmt.Println(req.PostForm) //map[field1:[value1] field2:[value2]]
    //本字段只有在调用ParseMultipartForm后才有效
    fmt.Println(req.MultipartForm) //&{map[field1:[value1] field2:[value2]] map[file:[0xc00009e140]]}

    // file, fileHeader, err := req.FormFile("field1") // 出错,返回:
    // 2019/02/13 18:40:02 http: no such file
    // exit status 1
    file, fileHeader, err := req.FormFile("file") // 成功
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(file) //{0xc0000a3080}
    fmt.Println(fileHeader) //&{file map[Content-Transfer-Encoding:[binary] Content-Disposition:[form-data; name="file"; filename="file"] Content-Type:[application/octet-stream]] 11 [98 105 110 97 114 121 32 100 97 116 97] }
}

 

 MultipartReader

func (r *Request) MultipartReader() (*multipart.Reader, error)

如果请求是multipart/form-data POST请求,MultipartReader返回一个multipart.Reader接口,否则返回nil和一个错误。使用本函数代替ParseMultipartForm,可以将r.Body作为流处理

举例:

package main 
import(
    "fmt"
    "net/http"
    "io/ioutil"
    "bytes"
)

func main() {
    req := &http.Request{
        Method: "POST",
        Header: http.Header{"Content-Type": {`multipart/form-data; boundary="foo123"`}},
        Body:   ioutil.NopCloser(new(bytes.Buffer)),
    }
    multipart, err := req.MultipartReader()//r.Body将作为流处理
    if multipart == nil {
        fmt.Printf("expected multipart; error: %v", err)
    }
    fmt.Println(multipart)//&{0xc00007e240 <nil> 0 [13 10] [13 10 45 45 102 111 111 49 50 51] [45 45 102 111 111 49 50 51 45 45] [45 45 102 111 111 49 50 51]}

    req = &http.Request{
        Method: "POST",
        Header: http.Header{"Content-Type": {`multipart/mixed; boundary="foo123"`}},
        Body:   ioutil.NopCloser(new(bytes.Buffer)),
    }
    multipart, err = req.MultipartReader()
    if multipart == nil {
        fmt.Printf("expected multipart; error: %v", err)
    }
    fmt.Println(multipart)//&{0xc00007e2a0 <nil> 0 [13 10] [13 10 45 45 102 111 111 49 50 51] [45 45 102 111 111 49 50 51 45 45] [45 45 102 111 111 49 50 51]}

    req.Header = http.Header{"Content-Type": {"text/plain"}}
    multipart, err = req.MultipartReader()
    if multipart != nil {
        fmt.Printf("unexpected multipart for text/plain")
    }
    fmt.Println(multipart)//<nil>
}

 总结:

  • FormValue和Form可以读取到body和url的数据
  • PostFormValue和PostForm只读取body的数据
  • MultipartForm只会读取body的数据,不会读取url的query数据

 

5>客户端

 1>Response

 Response

type Response struct {
    Status     string // 例如"200 OK"
    StatusCode int    // 例如200
    Proto      string // 例如"HTTP/1.0"
    ProtoMajor int    // 例如1
    ProtoMinor int    // 例如0
    // Header保管头域的键值对。
    // 如果回复中有多个头的键相同,Header中保存为该键对应用逗号分隔串联起来的这些头的值
    // (参见RFC 2616 Section 4.2)
    // 被本结构体中的其他字段复制保管的头(如ContentLength)会从Header中删掉。
    //
    // Header中的键都是规范化的,参见CanonicalHeaderKey函数
    Header Header
    // Body代表回复的主体。
    // Client类型和Transport类型会保证Body字段总是非nil的,即使回复没有主体或主体长度为0。
    // 关闭主体是调用者的责任。
    // 如果服务端采用"chunked"传输编码发送的回复,Body字段会自动进行解码。
    Body io.ReadCloser
    // ContentLength记录相关内容的长度。
    // 其值为-1表示长度未知(采用chunked传输编码)
    // 除非对应的Request.Method是"HEAD",其值>=0表示可以从Body读取的字节数
    ContentLength int64
    // TransferEncoding按从最外到最里的顺序列出传输编码,空切片表示"identity"编码。
    TransferEncoding []string
    // Close记录头域是否指定应在读取完主体后关闭连接。(即Connection头)
    // 该值是给客户端的建议,Response.Write方法的ReadResponse函数都不会关闭连接。
    Close bool
    // Trailer字段保存和头域相同格式的trailer键值对,和Header字段相同类型
    Trailer Header
    // Request是用来获取此回复的请求
    // Request的Body字段是nil(因为已经被用掉了)
    // 这个字段是被Client类型发出请求并获得回复后填充的
    Request *Request
    // TLS包含接收到该回复的TLS连接的信息。 对未加密的回复,本字段为nil。
    // 返回的指针是被(同一TLS连接接收到的)回复共享的,不应被修改。
    TLS *tls.ConnectionState
}

Response代表一个HTTP请求的回复。

 ReadResponse

func ReadResponse(r *bufio.Reader, req *Request) (*Response, error)

ReadResponse从r读取并返回一个HTTP 回复。req参数是可选的,指定该回复对应的请求(即是对该请求的回复)。如果是nil,将假设请求是GET请求。客户端必须在结束resp.Body的读取后关闭它。读取完毕并关闭后,客户端可以检查resp.Trailer字段获取回复的trailer的键值对。(本函数主要用在客户端从下层获取回复)

 ProtoAtLeast

func (r *Response) ProtoAtLeast(major, minor int) bool

ProtoAtLeast报告该回复使用的HTTP协议版本至少是major.minor。

 Write

func (r *Response) Write(w io.Writer) error

Write以有线格式将回复写入w(用于将回复写入下层TCPConn等)。本方法会考虑如下字段:

StatusCode
ProtoMajor
ProtoMinor
Request.Method
TransferEncoding
Trailer
Body
ContentLength
Header(不规范的键名和它对应的值会导致不可预知的行为)

Body字段在发送完回复后会被关闭。

 举例1:

package main 
import(
    "fmt"
    "net/http"
    "bufio"
    "strings"
    "os"

)

type respTest struct {
    Raw  string
    Resp http.Response
    Body string
}

func dummyReq(method string) *http.Request {
    return &http.Request{Method: method}
}

var respTests = []respTest{
    // Unchunked response without Content-Length.
    {
        "HTTP/1.0 200 OK\r\n" +
            "Connection: close\r\n" +
            "\r\n" +
            "Body here\n",

        http.Response{
            Status:     "200 OK",
            StatusCode: 200,
            Proto:      "HTTP/1.0",
            ProtoMajor: 1,
            ProtoMinor: 0,
            Request:    dummyReq("GET"),
            Header: http.Header{
                "Connection": {"close"}, // TODO(rsc): Delete?
            },
            Close:         true,
            ContentLength: -1,
        },

        "Body here\n",
    },

    // Unchunked HTTP/1.1 response without Content-Length or
    // Connection headers.
    {
        "HTTP/1.1 200 OK\r\n" +
            "\r\n" +
            "Body here\n",

        http.Response{
            Status:        "200 OK",
            StatusCode:    200,
            Proto:         "HTTP/1.1",
            ProtoMajor:    1,
            ProtoMinor:    1,
            Header:        http.Header{},
            Request:       dummyReq("GET"),
            Close:         true,
            ContentLength: -1,
        },

        "Body here\n",
    },

    // Unchunked HTTP/1.1 204 response without Content-Length.
    {
        "HTTP/1.1 204 No Content\r\n" +
            "\r\n" +
            "Body should not be read!\n",

        http.Response{
            Status:        "204 No Content",
            StatusCode:    204,
            Proto:         "HTTP/1.1",
            ProtoMajor:    1,
            ProtoMinor:    1,
            Header:        http.Header{},
            Request:       dummyReq("GET"),
            Close:         false,
            ContentLength: 0,
        },

        "",
    },

    // Unchunked response with Content-Length.
    {
        "HTTP/1.0 200 OK\r\n" +
            "Content-Length: 10\r\n" +
            "Connection: close\r\n" +
            "\r\n" +
            "Body here\n",

        http.Response{
            Status:     "200 OK",
            StatusCode: 200,
            Proto:      "HTTP/1.0",
            ProtoMajor: 1,
            ProtoMinor: 0,
            Request:    dummyReq("GET"),
            Header: http.Header{
                "Connection":     {"close"},
                "Content-Length": {"10"},
            },
            Close:         true,
            ContentLength: 10,
        },

        "Body here\n",
    },

    // Chunked response without Content-Length.
    {
        "HTTP/1.1 200 OK\r\n" +
            "Transfer-Encoding: chunked\r\n" +
            "\r\n" +
            "0a\r\n" +
            "Body here\n\r\n" +
            "09\r\n" +
            "continued\r\n" +
            "0\r\n" +
            "\r\n",

        http.Response{
            Status:           "200 OK",
            StatusCode:       200,
            Proto:            "HTTP/1.1",
            ProtoMajor:       1,
            ProtoMinor:       1,
            Request:          dummyReq("GET"),
            Header:           http.Header{},
            Close:            false,
            ContentLength:    -1,
            TransferEncoding: []string{"chunked"},
        },

        "Body here\ncontinued",
    },

    // Chunked response with Content-Length.
    {
        "HTTP/1.1 200 OK\r\n" +
            "Transfer-Encoding: chunked\r\n" +
            "Content-Length: 10\r\n" +
            "\r\n" +
            "0a\r\n" +
            "Body here\n\r\n" +
            "0\r\n" +
            "\r\n",

        http.Response{
            Status:           "200 OK",
            StatusCode:       200,
            Proto:            "HTTP/1.1",
            ProtoMajor:       1,
            ProtoMinor:       1,
            Request:          dummyReq("GET"),
            Header:           http.Header{},
            Close:            false,
            ContentLength:    -1,
            TransferEncoding: []string{"chunked"},
        },

        "Body here\n",
    },

    // Chunked response in response to a HEAD request
    {
        "HTTP/1.1 200 OK\r\n" +
            "Transfer-Encoding: chunked\r\n" +
            "\r\n",

        http.Response{
            Status:           "200 OK",
            StatusCode:       200,
            Proto:            "HTTP/1.1",
            ProtoMajor:       1,
            ProtoMinor:       1,
            Request:          dummyReq("HEAD"),
            Header:           http.Header{},
            TransferEncoding: []string{"chunked"},
            Close:            false,
            ContentLength:    -1,
        },

        "",
    },

    // Content-Length in response to a HEAD request
    {
        "HTTP/1.0 200 OK\r\n" +
            "Content-Length: 256\r\n" +
            "\r\n",

        http.Response{
            Status:           "200 OK",
            StatusCode:       200,
            Proto:            "HTTP/1.0",
            ProtoMajor:       1,
            ProtoMinor:       0,
            Request:          dummyReq("HEAD"),
            Header:           http.Header{"Content-Length": {"256"}},
            TransferEncoding: nil,
            Close:            true,
            ContentLength:    256,
        },

        "",
    },

    // Content-Length in response to a HEAD request with HTTP/1.1
    {
        "HTTP/1.1 200 OK\r\n" +
            "Content-Length: 256\r\n" +
            "\r\n",

        http.Response{
            Status:           "200 OK",
            StatusCode:       200,
            Proto:            "HTTP/1.1",
            ProtoMajor:       1,
            ProtoMinor:       1,
            Request:          dummyReq("HEAD"),
            Header:           http.Header{"Content-Length": {"256"}},
            TransferEncoding: nil,
            Close:            false,
            ContentLength:    256,
        },

        "",
    },

    // No Content-Length or Chunked in response to a HEAD request
    {
        "HTTP/1.0 200 OK\r\n" +
            "\r\n",

        http.Response{
            Status:           "200 OK",
            StatusCode:       200,
            Proto:            "HTTP/1.0",
            ProtoMajor:       1,
            ProtoMinor:       0,
            Request:          dummyReq("HEAD"),
            Header:           http.Header{},
            TransferEncoding: nil,
            Close:            true,
            ContentLength:    -1,
        },

        "",
    },

    // explicit Content-Length of 0.
    {
        "HTTP/1.1 200 OK\r\n" +
            "Content-Length: 0\r\n" +
            "\r\n",

        http.Response{
            Status:     "200 OK",
            StatusCode: 200,
            Proto:      "HTTP/1.1",
            ProtoMajor: 1,
            ProtoMinor: 1,
            Request:    dummyReq("GET"),
            Header: http.Header{
                "Content-Length": {"0"},
            },
            Close:         false,
            ContentLength: 0,
        },

        "",
    },

    // Status line without a Reason-Phrase, but trailing space.
    // (permitted by RFC 7230, section 3.1.2)
    {
        "HTTP/1.0 303 \r\n\r\n",
        http.Response{
            Status:        "303 ",
            StatusCode:    303,
            Proto:         "HTTP/1.0",
            ProtoMajor:    1,
            ProtoMinor:    0,
            Request:       dummyReq("GET"),
            Header:        http.Header{},
            Close:         true,
            ContentLength: -1,
        },

        "",
    },

    // Status line without a Reason-Phrase, and no trailing space.
    // (not permitted by RFC 7230, but we'll accept it anyway)
    {
        "HTTP/1.0 303\r\n\r\n",
        http.Response{
            Status:        "303",
            StatusCode:    303,
            Proto:         "HTTP/1.0",
            ProtoMajor:    1,
            ProtoMinor:    0,
            Request:       dummyReq("GET"),
            Header:        http.Header{},
            Close:         true,
            ContentLength: -1,
        },

        "",
    },

    // golang.org/issue/4767: don't special-case multipart/byteranges responses
    {
        `HTTP/1.1 206 Partial Content
Connection: close
Content-Type: multipart/byteranges; boundary=18a75608c8f47cef

some body`,
        http.Response{
            Status:     "206 Partial Content",
            StatusCode: 206,
            Proto:      "HTTP/1.1",
            ProtoMajor: 1,
            ProtoMinor: 1,
            Request:    dummyReq("GET"),
            Header: http.Header{
                "Content-Type": []string{"multipart/byteranges; boundary=18a75608c8f47cef"},
            },
            Close:         true,
            ContentLength: -1,
        },

        "some body",
    },

    // Unchunked response without Content-Length, Request is nil
    {
        "HTTP/1.0 200 OK\r\n" +
            "Connection: close\r\n" +
            "\r\n" +
            "Body here\n",

        http.Response{
            Status:     "200 OK",
            StatusCode: 200,
            Proto:      "HTTP/1.0",
            ProtoMajor: 1,
            ProtoMinor: 0,
            Header: http.Header{
                "Connection": {"close"}, // TODO(rsc): Delete?
            },
            Close:         true,
            ContentLength: -1,
        },

        "Body here\n",
    },

    // 206 Partial Content. golang.org/issue/8923
    {
        "HTTP/1.1 206 Partial Content\r\n" +
            "Content-Type: text/plain; charset=utf-8\r\n" +
            "Accept-Ranges: bytes\r\n" +
            "Content-Range: bytes 0-5/1862\r\n" +
            "Content-Length: 6\r\n\r\n" +
            "foobar",

        http.Response{
            Status:     "206 Partial Content",
            StatusCode: 206,
            Proto:      "HTTP/1.1",
            ProtoMajor: 1,
            ProtoMinor: 1,
            Request:    dummyReq("GET"),
            Header: http.Header{
                "Accept-Ranges":  []string{"bytes"},
                "Content-Length": []string{"6"},
                "Content-Type":   []string{"text/plain; charset=utf-8"},
                "Content-Range":  []string{"bytes 0-5/1862"},
            },
            ContentLength: 6,
        },

        "foobar",
    },

    // Both keep-alive and close, on the same Connection line. (Issue 8840)
    {
        "HTTP/1.1 200 OK\r\n" +
            "Content-Length: 256\r\n" +
            "Connection: keep-alive, close\r\n" +
            "\r\n",

        http.Response{
            Status:     "200 OK",
            StatusCode: 200,
            Proto:      "HTTP/1.1",
            ProtoMajor: 1,
            ProtoMinor: 1,
            Request:    dummyReq("HEAD"),
            Header: http.Header{
                "Content-Length": {"256"},
            },
            TransferEncoding: nil,
            Close:            true,
            ContentLength:    256,
        },

        "",
    },

    // Both keep-alive and close, on different Connection lines. (Issue 8840)
    {
        "HTTP/1.1 200 OK\r\n" +
            "Content-Length: 256\r\n" +
            "Connection: keep-alive\r\n" +
            "Connection: close\r\n" +
            "\r\n",

        http.Response{
            Status:     "200 OK",
            StatusCode: 200,
            Proto:      "HTTP/1.1",
            ProtoMajor: 1,
            ProtoMinor: 1,
            Request:    dummyReq("HEAD"),
            Header: http.Header{
                "Content-Length": {"256"},
            },
            TransferEncoding: nil,
            Close:            true,
            ContentLength:    256,
        },

        "",
    },

    // Issue 12785: HTTP/1.0 response with bogus (to be ignored) Transfer-Encoding.
    // Without a Content-Length.
    {
        "HTTP/1.0 200 OK\r\n" +
            "Transfer-Encoding: bogus\r\n" +
            "\r\n" +
            "Body here\n",

        http.Response{
            Status:        "200 OK",
            StatusCode:    200,
            Proto:         "HTTP/1.0",
            ProtoMajor:    1,
            ProtoMinor:    0,
            Request:       dummyReq("GET"),
            Header:        http.Header{},
            Close:         true,
            ContentLength: -1,
        },

        "Body here\n",
    },

    // Issue 12785: HTTP/1.0 response with bogus (to be ignored) Transfer-Encoding.
    // With a Content-Length.
    {
        "HTTP/1.0 200 OK\r\n" +
            "Transfer-Encoding: bogus\r\n" +
            "Content-Length: 10\r\n" +
            "\r\n" +
            "Body here\n",

        http.Response{
            Status:     "200 OK",
            StatusCode: 200,
            Proto:      "HTTP/1.0",
            ProtoMajor: 1,
            ProtoMinor: 0,
            Request:    dummyReq("GET"),
            Header: http.Header{
                "Content-Length": {"10"},
            },
            Close:         true,
            ContentLength: 10,
        },

        "Body here\n",
    },

    {
        "HTTP/1.1 200 OK\r\n" +
            "Content-Encoding: gzip\r\n" +
            "Content-Length: 23\r\n" +
            "Connection: keep-alive\r\n" +
            "Keep-Alive: timeout=7200\r\n\r\n" +
            "\x1f\x8b\b\x00\x00\x00\x00\x00\x00\x00s\xf3\xf7\a\x00\xab'\xd4\x1a\x03\x00\x00\x00",
        http.Response{
            Status:     "200 OK",
            StatusCode: 200,
            Proto:      "HTTP/1.1",
            ProtoMajor: 1,
            ProtoMinor: 1,
            Request:    dummyReq("GET"),
            Header: http.Header{
                "Content-Length":   {"23"},
                "Content-Encoding": {"gzip"},
                "Connection":       {"keep-alive"},
                "Keep-Alive":       {"timeout=7200"},
            },
            Close:         false,
            ContentLength: 23,
        },
        "\x1f\x8b\b\x00\x00\x00\x00\x00\x00\x00s\xf3\xf7\a\x00\xab'\xd4\x1a\x03\x00\x00\x00",
    },

    // Issue 19989: two spaces between HTTP version and status.
    {
        "HTTP/1.0  401 Unauthorized\r\n" +
            "Content-type: text/html\r\n" +
            "WWW-Authenticate: Basic realm=\"\"\r\n\r\n" +
            "Your Authentication failed.\r\n",
        http.Response{
            Status:     "401 Unauthorized",
            StatusCode: 401,
            Proto:      "HTTP/1.0",
            ProtoMajor: 1,
            ProtoMinor: 0,
            Request:    dummyReq("GET"),
            Header: http.Header{
                "Content-Type":     {"text/html"},
                "Www-Authenticate": {`Basic realm=""`},
            },
            Close:         true,
            ContentLength: -1,
        },
        "Your Authentication failed.\r\n",
    },
}



func main() {
    for i, tt := range respTests {
        resp, err := http.ReadResponse(bufio.NewReader(strings.NewReader(tt.Raw)), tt.Resp.Request)
        if err != nil {
            fmt.Printf("#%d: %v", i, err)
            continue
        }

        fmt.Println(i, resp) //返回得到的response
        fmt.Println("ProtoAtLeast 1.0 : ", resp.ProtoAtLeast(1,0))
        fmt.Println()

        fmt.Println("write : ")
        err = resp.Write(os.Stdout) //将得到的response写到终端上
        fmt.Println()
        if err != nil {
            fmt.Printf("#%d: %v", i, err)
            continue
        }
    }
}
View Code

返回:

wanghuideMBP:go-learning wanghui$ go run test.go
0 &{200 OK 200 HTTP/1.0 1 0 map[Connection:[close]] 0xc00001e200 -1 [] true false map[] 0xc0000f2000 <nil>}
ProtoAtLeast 1.0 :  true

write : 
HTTP/1.0 200 OK
Connection: close

Body here

1 &{200 OK 200 HTTP/1.1 1 1 map[] 0xc00001e240 -1 [] true false map[] 0xc0000f2100 <nil>}
ProtoAtLeast 1.0 :  true

write : 
HTTP/1.1 200 OK
Connection: close

Body here

2 &{204 No Content 204 HTTP/1.1 1 1 map[] {} 0 [] false false map[] 0xc0000f2200 <nil>}
ProtoAtLeast 1.0 :  true

write : 
HTTP/1.1 204 No Content


3 &{200 OK 200 HTTP/1.0 1 0 map[Content-Length:[10] Connection:[close]] 0xc00001e280 10 [] true false map[] 0xc0000f2300 <nil>}
ProtoAtLeast 1.0 :  true

write : 
HTTP/1.0 200 OK
Content-Length: 10
Connection: close

Body here

4 &{200 OK 200 HTTP/1.1 1 1 map[] 0xc00001e2c0 -1 [chunked] false false map[] 0xc0000f2400 <nil>}
ProtoAtLeast 1.0 :  true

write : 
HTTP/1.1 200 OK
Transfer-Encoding: chunked

13
Body here
continued
0


5 &{200 OK 200 HTTP/1.1 1 1 map[] 0xc00001e300 -1 [chunked] false false map[] 0xc0000f2500 <nil>}
ProtoAtLeast 1.0 :  true

write : 
HTTP/1.1 200 OK
Transfer-Encoding: chunked

a
Body here

0


6 &{200 OK 200 HTTP/1.1 1 1 map[] {} -1 [chunked] false false map[] 0xc0000f2600 <nil>}
ProtoAtLeast 1.0 :  true

write : 
HTTP/1.1 200 OK
Transfer-Encoding: chunked



7 &{200 OK 200 HTTP/1.0 1 0 map[Content-Length:[256]] {} 256 [] true false map[] 0xc0000f2700 <nil>}
ProtoAtLeast 1.0 :  true

write : 
HTTP/1.0 200 OK
Connection: close
Content-Length: 256


8 &{200 OK 200 HTTP/1.1 1 1 map[Content-Length:[256]] {} 256 [] false false map[] 0xc0000f2800 <nil>}
ProtoAtLeast 1.0 :  true

write : 
HTTP/1.1 200 OK
Content-Length: 256


9 &{200 OK 200 HTTP/1.0 1 0 map[] {} -1 [] true false map[] 0xc0000f2900 <nil>}
ProtoAtLeast 1.0 :  true

write : 
HTTP/1.0 200 OK
Connection: close


10 &{200 OK 200 HTTP/1.1 1 1 map[Content-Length:[0]] {} 0 [] false false map[] 0xc0000f2a00 <nil>}
ProtoAtLeast 1.0 :  true

write : 
HTTP/1.1 200 OK
Content-Length: 0


11 &{303  303 HTTP/1.0 1 0 map[] 0xc00001e340 -1 [] true false map[] 0xc0000f2b00 <nil>}
ProtoAtLeast 1.0 :  true

write : 
HTTP/1.0 303 
Connection: close


12 &{303 303 HTTP/1.0 1 0 map[] 0xc00001e380 -1 [] true false map[] 0xc0000f2c00 <nil>}
ProtoAtLeast 1.0 :  true

write : 
HTTP/1.0 303 303
Connection: close


13 &{206 Partial Content 206 HTTP/1.1 1 1 map[Content-Type:[multipart/byteranges; boundary=18a75608c8f47cef]] 0xc00001e3c0 -1 [] true false map[] 0xc0000f2d00 <nil>}
ProtoAtLeast 1.0 :  true

write : 
HTTP/1.1 206 Partial Content
Connection: close
Content-Type: multipart/byteranges; boundary=18a75608c8f47cef

some body
14 &{200 OK 200 HTTP/1.0 1 0 map[Connection:[close]] 0xc00001e400 -1 [] true false map[] <nil> <nil>}
ProtoAtLeast 1.0 :  true

write : 
HTTP/1.0 200 OK
Connection: close

Body here

15 &{206 Partial Content 206 HTTP/1.1 1 1 map[Content-Type:[text/plain; charset=utf-8] Accept-Ranges:[bytes] Content-Range:[bytes 0-5/1862] Content-Length:[6]] 0xc00001e480 6 [] false false map[] 0xc0000f2e00 <nil>}
ProtoAtLeast 1.0 :  true

write : 
HTTP/1.1 206 Partial Content
Content-Length: 6
Accept-Ranges: bytes
Content-Range: bytes 0-5/1862
Content-Type: text/plain; charset=utf-8

foobar
16 &{200 OK 200 HTTP/1.1 1 1 map[Content-Length:[256]] {} 256 [] true false map[] 0xc0000f2f00 <nil>}
ProtoAtLeast 1.0 :  true

write : 
HTTP/1.1 200 OK
Connection: close
Content-Length: 256


17 &{200 OK 200 HTTP/1.1 1 1 map[Content-Length:[256]] {} 256 [] true false map[] 0xc0000f3000 <nil>}
ProtoAtLeast 1.0 :  true

write : 
HTTP/1.1 200 OK
Connection: close
Content-Length: 256


18 &{200 OK 200 HTTP/1.0 1 0 map[] 0xc00001e4c0 -1 [] true false map[] 0xc0000f3100 <nil>}
ProtoAtLeast 1.0 :  true

write : 
HTTP/1.0 200 OK
Connection: close

Body here

19 &{200 OK 200 HTTP/1.0 1 0 map[Content-Length:[10]] 0xc00001e500 10 [] true false map[] 0xc0000f3200 <nil>}
ProtoAtLeast 1.0 :  true

write : 
HTTP/1.0 200 OK
Connection: close
Content-Length: 10

Body here

20 &{200 OK 200 HTTP/1.1 1 1 map[Content-Encoding:[gzip] Content-Length:[23] Connection:[keep-alive] Keep-Alive:[timeout=7200]] 0xc00001e580 23 [] false false map[] 0xc0000f3300 <nil>}
ProtoAtLeast 1.0 :  true

write : 
HTTP/1.1 200 OK
Content-Length: 23
Connection: keep-alive
Content-Encoding: gzip
Keep-Alive: timeout=7200

s???'?
21 &{401 Unauthorized 401 HTTP/1.0 1 0 map[Content-Type:[text/html] Www-Authenticate:[Basic realm=""]] 0xc00001e5c0 -1 [] true false map[] 0xc0000f3400 <nil>}
ProtoAtLeast 1.0 :  true

write : 
HTTP/1.0 401 Unauthorized
Connection: close
Content-Type: text/html
Www-Authenticate: Basic realm=""

Your Authentication failed.

wanghuideMBP:go-learning wanghui$ 
View Code

举例2:

package main 
import(
    "fmt"
    "net/http"
    "bytes"
    "strings"
)

func main() {
    r := &http.Response{
        Status:     "123 some status",
        StatusCode: 123,
        ProtoMajor: 1,
        ProtoMinor: 3,
    }
    var buf bytes.Buffer
    r.Write(&buf)
    fmt.Println(buf.String())
    if strings.Contains(buf.String(), "123 123") {
        fmt.Printf("stutter in status: %s", buf.String())
    }
}

返回:

userdeMBP:go-learning user$ go run test.go
HTTP/1.3 123 some status


userdeMBP:go-learning user$ 

其他方法:

 Cookies

func (r *Response) Cookies() []*Cookie

Cookies解析并返回该回复中的Set-Cookie头设置的cookie。

 Location

func (r *Response) Location() (*url.URL, error)

Location返回该回复的Location头设置的URL。相对地址的重定向会相对于该回复对应的请求request.url来确定绝对地址。如果回复中没有Location头,会返回nil, ErrNoLocation。

相关文章:

  • 2021-08-14
  • 2021-08-14
  • 2022-12-23
  • 2022-12-23
  • 2022-12-23
  • 2022-12-23
  • 2022-12-23
  • 2022-12-23
猜你喜欢
  • 2022-01-07
  • 2021-11-20
  • 2022-12-23
  • 2022-12-23
  • 2022-12-23
  • 2021-12-17
  • 2021-12-18
相关资源
相似解决方案