简介
万维网是一个庞大的,拥有数以百万计用户的分布式系统。网站就是一个运行着 HTTP 服务器的 Web 主机。而 Web 客户端通常是浏览器用户,当然也还有许多其他的“用户”,如网络蜘蛛,Web 应用程序客户端等。
Web 使用的 HTTP(超文本传输协议)
是基于 TCP 协议的。HTTP 有三个公开可用的版本,目前最常用的是最新的版本 1.1。
HTTP 概述
URL 和资源
URL
指定资源的位置。资源通常是 HTML 文档、图片、声音文件这样的静态文件,但越来越多的资源是动态生成的对象,比如根据数据库信息生成。
“用户”请求资源时,返回的并不是资源本身,而是资源的代表。如果资源是静态文件,那么返回给用户的就是文件的一个副本。
不同的 URL 可以指向相同的资源,HTTP 服务器会给每个 URL 返回适当的代表。例如,针对同一个产品,某公司可以使用不同的 URL 给本地和外地的用户查看其产品信息,本地用户可以看到本地产品联系人这类内容,而外地用户看到的内容则包括产品销售门店的地址等等。
这其实就意味着,HTTP 协议本身
非常简单直接,但 HTTP 服务器
却可能非常复杂。HTTP将用户请求发送到服务器,并返回字节流,而服务器针对该请求可能需要做很多很多处理。
HTTP 的特点
HTTP 协议是无状态
,面向连接
和可靠的
。最简单的形式是,每个从用户发起的请求被可靠地处理,然后断开连接。每次请求都包括一个独立的 TCP 连接,所以如果要请求很多资源(如在 HTML 页面中嵌入的图像),则必须在很短的时间内建立并断开许多 TCP 连接。
为构建更高效更可靠的协议,有许多在这种简单结构基础上添加复杂性的优化技术。
版本
HTTP 有三个版本
- Version 0.9 - 完全废弃
- Version 1.0 - 基本废弃
- Version 1.1 -
当前版本
每个版本必须兼容早期的版本。
HTTP 0.9
请求格式
Request = Simple- Request
Simple-Request = "GET" SP Request-URI CRLF
响应格式
Response = Simple- Response
Simple-Response = [Entity-Body]
HTTP1.0
该版本在请求和响应中增加了很多信息。与其说是 0.9 的升级版,还不如说它是一个全新的版本。
请求格式
从客户端到服务器端的请求格式:
Request = Simple-Request | Full Request
Simple-Request = "GET" SP Request-URI CRLF
Full-Request = Request-Line
*(General-Header
| Request-Header
| Entity-Header)
CRLF
[Entity-Body]
简单请求(Simple-Request)表明是一个 HTTP/0.9 请求,必须回复简单响应(Simple-Response)。
请求行(Request-Line)
的格式如下:
Request-Line = Method SP Request-URI SP HTTP-Version CRLF
其中
Method = "GET" | "HEAD" | POST |
extension-method
如:GET http://jan.newmarch.name/index.html HTTP/1.0
响应格式
响应的形式如下:
Response = Simple-Response | Full-Response
Simple-Response = [Entity-Body]
Full-Response = Status-Line
*(General-Header
| Response-Header
| Entity-Header)
CRLF
[Entity-Body]
状态行(Status-Line)
会给出请求的最后的状态信息:
Status-Line = HTTP-Version SP Status-Code SP Reason-Phrase CRLF
如HTTP/1.0 200 OK
状态码:
Status-Code = "200" ; OK
| "201" ; Created
| "202" ; Accepted
| "204" ; No Content
| "301" ; Moved permanently
| "302" ; Moved temporarily
| "304" ; Not modified
| "400" ; Bad request
| "401" ; Unauthorised
| "403" ; Forbidden
| "404" ; Not found
| "500" ; Internal server error
| "501" ; Not implemented
| "502" ; Bad gateway
| "503" | Service unavailable
| extension-code
实体头(Entity-Header)包含了有关实体(Entity-Body)的有用信息
Entity-Header = Allow
| Content-Encoding
| Content-Length
| Content-Type
| Expires
| Last-Modified
| extension-header
例如:
HTTP/1.1 200 OK
Date: Fri, 29 Aug 2003 00:59:56 GMT
Server: Apache/2.0.40 (Unix)
Accept-Ranges: bytes
Content-Length: 1595
Connection: close
Content-Type: text/html; charset=ISO-8859-1
HTTP 1.1
HTTP 1.1 修复了 HTTP 1.0 中的很多问题,因此更加复杂。例如此版本中扩展和完善了HTTP 1.0 中的可选项。
- 增加了命令,如
TRACE
和CONNECT
- 注意在通过代理服务器进行连接时,应当使用绝对路径。如:
GET http://www.w3.org/index.html HTTP/1.1
- 增加了更多属性,例如针对代理服务器的
If-Modified-Since
这些变动包括:
- 主机名识别(支持虚拟主机)
- 内容协商(多语言)
- 持久连接(降低 TCP 开销)
- 分块传送
- 字节范围(请求文件部分内容)
- 代理支持
0.9 版本的协议只有一页,1.0 版本用了大约 20 页来说明,而 1.1 则用了 120 页
简单用户代理(Simple user-agents)
用户代理(User agent)
(例如浏览器)用来发起请求和接收响应。代码中的 response type 如下:
type Response struct {
Status string // e.g. "200 OK"
StatusCode int // e.g. 200
Proto string // e.g. "HTTP/1.0"
ProtoMajor int // e.g. 1
ProtoMinor int // e.g. 0
RequestMethod string // e.g. "HEAD", "CONNECT", "GET", etc.
Header map[string]string
Body io.ReadCloser
ContentLength int64
TransferEncoding []string
Close bool
Trailer map[string]string
}
通过实例可以了解其数据结构。最简单的请求是由用户代理发起"HEAD"
命令,其中包括请求的资源和 HTTP 服务器。函数
func Head(url string) (r *Response, err os.Error)
可用来发起此请求。
响应状态
对应 response
中的 Status
属性,而 Header
属性对应 HTTP 响应的 header 域
。下面的程序用来发起请求和显示结果:
/* Head
*/
package main
import (
"fmt"
"net/http"
"os"
)
func main() {
if len(os.Args) != 2 {
fmt.Println("Usage: ", os.Args[0], "host:port")
os.Exit(1)
}
url := os.Args[1]
response, err := http.Head(url)
if err != nil {
fmt.Println(err.Error())
os.Exit(2)
}
fmt.Println(response.Status)
for k, v := range response.Header{
fmt.Println(k+":", v)
}
os.Exit(0)
}
程序运行请求资源,Head http://www.baidu.com/
,输出结果类似:
[root@localhost home]# ./test http://www.baidu.com
200 OK
Connection: [Keep-Alive]
Content-Type: [text/html]
Etag: ["575e1f8a-115"]
Accept-Ranges: [bytes]
Cache-Control: [private, no-cache, no-store, proxy-revalidate, no-transform]
Last-Modified: [Mon, 13 Jun 2016 02:50:50 GMT]
Pragma: [no-cache]
Server: [bfe/1.0.8.18]
Content-Length: [277]
Date: [Fri, 14 Jun 2019 07:26:28 GMT]
通常我们希望接收到一个资源内容而不是其有关信息。"GET"
请求就是做来做这个的,使用如下函数即可:
func Get(url string) (r *Response, err os.Error)
响应内容
为 response 的 Body 属性。它是一个 io.ReadCloser
类型。我们可以用以下程序在屏幕上打印相应内容
/* Get*/
package main
import (
"fmt"
"net/http"
"net/http/httputil"
"os"
"strings"
)
func main() {
if len(os.Args) != 2 {
fmt.Println("Usage: ", os.Args[0], "host:port")
os.Exit(1)
}
url := os.Args[1]
response, err := http.Get(url)
if err != nil {
fmt.Println(err.Error())
os.Exit(2)
}
if response.Status!= "200 OK"{
fmt.Println(response.Status)
os.Exit(2)
}
b, _:= httputil.DumpResponse(response, false)
fmt.Print(string(b))
contentTypes := response.Header["Content-Type"]
if !acceptableCharset(contentTypes) {
fmt.Println("Cannot handle", contentTypes)
os.Exit(4)
}
var buf [512]byte
reader := response.Body
for{
n, err := reader.Read(buf[0:])
if err != nil {
os.Exit(0)
}
fmt.Print(string(buf[0:n]))
}
os.Exit(0)
}
func acceptableCharset(contentTypes []string) bool {
// each type is like [text/html; charset=UTF-8]
// we want the UTF-8 only
for _, cType := range contentTypes {
//这里存在一个问题,经过测试,大多数页面采用默认编码,未指定charset,则会产生异常
if strings.Index(cType, "UTF-8") != -1 {
return true
}
}
return false
}
注意这里有一个重要的字符集类型问题
,在前面章节也讨论过。服务器提供内容时使用的字符集编码,甚至传输编码,通常是用户代理和服务器之间协商的结果,但我们使用的 Get的命令很简单,它不包括用户代理的内容协商组件。因此,服务器可以自行决定使用什么字符编码。
我第一次写的时候是在中国。当我用这个程序访问 www.google.com 时,谷歌的服务器尝试猜测我的地理位置,然后很厉害地使用了 Big5 码给我发送文本!后面会讨论如何告知服务器给我什么字符编码最好。
设置 HTTP 请求
Go 还提供一个较低级别的用户代理接口用来与 HTTP 服务器进行通信。你可能已经想到,这样可以更灵活地控制客户端请求,当然创建请求也会更费力气。不过这只需要多费一点点力气。
用来创建请求的数据类型是Request
。这是个复杂的类型,Go 语言文档中给出的定义如下:
type Request struct {
Method string // GET, POST, PUT, etc.
RawURL string // The raw URL given in the request.
URL *URL // Parsed URL.
Proto string // "HTTP/1.0"
ProtoMajor int // 1 ProtoMajor int
ProtoMinor int // 0 ProtoMinor int
// A header maps request lines to their values.
// If the header says
//
// accept-encoding: gzip, deflate
// Accept-Language: en-us
// Connection: keep-alive
//
// then
//
// Header = map[string]string{
// "Accept-Encoding": "gzip, deflate",
// "Accept-Language": "en-us",
// "Connection": "keep-alive",
// }
//
// HTTP defines that header names are case-insensitive
// The request parser implements this by canonicalizing
// name, making the first character and any characters
// following a hyphen uppercase and the rest lowercase.
Header map[string]string
// The message body.
Body io.ReadCloser
// ContentLength records the length of the associated content.
// The value -1 indicates that the length is unknown.
// Values >= 0 indicate that the given number of bytes may be read from Body.
ContentLength int64
// TransferEncoding lists the transfer encodings from outermost to innermost.
// An empty list denotes the "identity" encoding.
TransferEncoding []string
// Whether to close the connection after replying to this request.
Close bool
// The host on which the URL is sought.
// Per RFC 2616, this is either the value of the Host: header
// or the host name given in the URL itself.
Host string
// The referring URL, if sent in the request.
//
// Referer is misspelled as in the request itself,
// a mistake from the earliest days of HTTP.
// This value can also be fetched from the Header map
// as Header["Referer"]; the benefit of making it
// available as a structure field is that the compiler
// can diagnose programs that use the alternate
// (correct English) spelling req.Referrer but cannot
// diagnose programs that use Header["Referrer"].
Referer string
// The User-Agent: header string, if sent in the request.
UserAgent string
// The parsed form. Only available after ParseForm is called.
Form map[string][]string
// Trailer maps trailer keys to values. Like for Header, if the
// response has multiple trailer lines with the same key, they will be
// concatenated, delimited by commas.
Trailer map[string]string
}
请求中可以存放大量的信息,但你不需要填写所有的内容,只填必要的即可。最简单的使用默认值创建请求的方法如下:
request, err := http.NewRequest("GET", url.String(), nil)
请求创建后,可以修改其内容字段(field)。比如,需指定只接受 UTF-8,可添加一个"Accept-Charset"字段:
request.Header.Add("Accept-Charset", "UTF-8;q=1, ISO-8859-1;q=0")
(注意,若没有在列表中提及,则默认设置 ISO-8859-1 总是返回值 1).
如上所述,客户端设置字符集请求很简单。但对于服务器返回的字符集,发生的事情就比较复杂。返回的资源理应包含 Content-Type
,用来指明内容的媒介类型,如:text/html。有些媒介类型应当声明字符集,如 text/html; charset=UTF-8。如果没有指明字符集,按照 HTTP规范就应当作为默认的 ISO8859-1 字符集处理。但是很多服务器并不符合此约定,因此 HTML4 规定此时不能做任何假设。
如果服务器的 Content-Type 指定了字符集,那么就认为它是正确的。如果未指定字符集,由于 50%的页面是 UTF-8 的,20%的页面是 ASCII 的,因此假设字符集是 UTF-8 的会比较安全,但仍然有 30%的页面可能会出错:-(。
客户端对象
向服务器发送一个请求并取得回复,最简单的方法是使用方便对象 Client
。此对象可以管理多个请求,并处理一些问题,如与服务器间的 TCP 连接是否保持活动状态等。
下面的程序给出了示例:
/* ClientGet */
package main
import(
"fmt"
"net/http"
"net/url"
"os"
"strings"
)
func main() {
if len(os.Args) != 2 {
fmt.Println("Usage: ", os.Args[0], "http://host:port/page")
os.Exit(1)
}
url, err := url.Parse(os.Args[1])
checkError(err)
client := &http.Client{}
request, err := http.NewRequest("GET", url.String(), nil)
// only accept UTF-8
request.Header.Add("Accept-Charset", "UTF-8;q=1, ISO-8859-1;q=0")
checkError(err)
response, err := client.Do(request)
if response.Status!= "200 OK"{
fmt.Println(response.Status)
os.Exit(2)
}
chSet := getCharset(response)
fmt.Printf("got charset %s\n", chSet)
if chSet != "UTF-8"{
fmt.Println("Cannot handle", chSet)
os.Exit(4)
}
var buf [512]byte
reader := response.Body
fmt.Println("got body")
for{
n, err := reader.Read(buf[0:])
if err != nil {
os.Exit(0)
}
fmt.Print(string(buf[0:n]))
}
os.Exit(0)
}
func getCharset(response *http.Response) string {
contentType := response.Header.Get("Content-Type")
if contentType == ""{
// guess
return "UTF-8"
}
idx := strings.Index(contentType, "charset:")
if idx == -1 {
// guess
return "UTF-8"
}
return strings.Trim(contentType[idx:], " ")
}
func checkError(err error) {
if err != nil {
fmt.Println("Fatal error ", err.Error())
os.Exit(1)
}
}
代理处理
简单代理
HTTP 1.1 规定了 HTTP 应当如何通过代理工作。向代理服务器发送一个"GET"请求。但是请求 URL 必须是完整的目标地址。此外,设置代理的 HTTP 头应当包括"Host"字段。只要代理服务器设置为允许这样的请求通过,那么做这些就够了。
Go 把这看成 HTTP 传输层的一部分。可使用 Transport 类进行管理。可以使用函数将代理服务器的 URL 返回到它的一个字段。假设有一个代理服务器地址字符串 URL,相应的创建Transport 对象并交给 Client 对象的代码就是:
proxyURL, err := url.Parse(proxyString)
transport := &http.Transport{Proxy: http.ProxyURL(proxyURL)}
client := &http.Client{Transport: transport}
客户端可以像之前一样继续使用
下面是程序范例:
/* ProxyGet */
package main
import (
"fmt"
"io"
"net/http"
"net/http/httputil"
"net/url"
"os"
)
func main() {
if len(os.Args) != 3 {
fmt.Println("Usage: ", os.Args[0], "http://proxy-host:port http://host:port/page")
os.Exit(1)
}
proxyString := os.Args[1]
proxyURL, err := url.Parse(proxyString)
checkError(err)
rawURL := os.Args[2]
url, err := url.Parse(rawURL)
checkError(err)
transport := &http.Transport{Proxy: http.ProxyURL(proxyURL)}
client := &http.Client{Transport: transport}
request, err := http.NewRequest("GET", url.String(), nil)
dump, _:= httputil.DumpRequest(request, false)
fmt.Println(string(dump))
response, err := client.Do(request)
checkError(err)
fmt.Println("Read ok")
if response.Status!= "200 OK"{
fmt.Println(response.Status)
os.Exit(2)
}
fmt.Println("Reponse ok")
var buf [512]byte
reader := response.Body
for{
n, err := reader.Read(buf[0:])
if err != nil {
os.Exit(0)
}
fmt.Print(string(buf[0:n]))
}
os.Exit(0)
}
func checkError(err error) {
if err != nil {
if err == io.EOF{
return
}
fmt.Println("Fatal error ", err.Error())
os.Exit(1)
}
}
假设有一个代理服务器 XYZ.com,端口 8080,测试命令就是
go run ProxyGet.go http://XYZ.com:8080/ http://www.google.com
如果没有合适的代理服务器可供测试,也可以在自己的计算机上下载安装 Squid proxy。
上面的程序是将已知的代理服务器地址作为参数传入的。有很多办法可以将代理服务器的地址通知到应用程序。大多数浏览器可以通过配置菜单输入代理服务器的信息:但这些信息对Go 应用没有用。有些应用程序可以从网络中某处找到 autoproxy.pac 文件取得其中的代理服务器信息,但 Go(目前还)不能解析 JavaScript 文件,因此也不能使用。Gnome Linux 系统使用的配置系统 gconf 里可以存储代理服务器信息,但 Go 也访问不了。但是,如果在操作系统环境变量中设置代理服务器信息(如 HTTP_PROXY
或 http_proxy
),Go 可以通过以下函数访问到:
func ProxyFromEnvironment(req *Request) (*url.URL, error)
假如你的程序运行在这样的环境中,就可以使用此功能,而不用明确指定代理服务器参数。
身份验证代理
有些代理服务器要求通过用户名和密码进行身份验证才能传递请求。一般的方法是“基本身份验证”:将用户名和密码串联成一个字符串“user:password”,然后进行 Base64 编码,然后添加到 HTTP 请求头的“Proxy-Authorization”中,再发送到代理服务器
在前一个程序的基础上增加 Proxy-Authorization 头,示例如下:
/* ProxyAuthGet */
package main
import (
"encoding/base64"
"fmt"
"io"
"net/http"
"net/http/httputil"
"net/url"
"os"
)
const auth = "jannewmarch:mypassword"
func main() {
if len(os.Args) != 3 {
fmt.Println("Usage: ", os.Args[0], "http://proxy-host:port http://host:port/page")
os.Exit(1)
}
proxy := os.Args[1]
proxyURL, err := url.Parse(proxy)
checkError(err)
rawURL := os.Args[2]
url, err := url.Parse(rawURL)
checkError(err)
// encode the auth
basic := "Basic "+ base64.StdEncoding.EncodeToString([]byte(auth))
transport := &http.Transport{Proxy: http.ProxyURL(proxyURL)}
client := &http.Client{Transport: transport}
request, err := http.NewRequest("GET", url.String(), nil)
request.Header.Add("Proxy-Authorization", basic)
dump, _:= httputil.DumpRequest(request, false)
fmt.Println(string(dump))
// send the request
response, err := client.Do(request)
checkError(err)
fmt.Println("Read ok")
if response.Status!= "200 OK"{
fmt.Println(response.Status)
os.Exit(2)
}
fmt.Println("Reponse ok")
var buf [512]byte
reader := response.Body
for{
n, err := reader.Read(buf[0:])
if err != nil {
os.Exit(0)
}
fmt.Print(string(buf[0:n]))
}
os.Exit(0)
}
func checkError(err error) {
if err != nil {
if err == io.EOF{
return
}
fmt.Println("Fatal error ", err.Error())
os.Exit(1)
}
}
客户端发起 HTTPS 连接
为保证连接的安全和加密,HTTP 使用其在安全性章节中说明的 TLS 技术。HTTP+TLS 的协议
被称为 HTTPS
,它使用 https://地址,而不是 http://地址。
服务器必须在客户端接受从其数据前返回有效的 X.509 证书。如果证书有效,Go 会在内部处理好所有的事情,而客户端会在使用 HTTPS 地址时和以前工作得一样出色。
许多网站都使用无效的证书。这些证书可能已经过期,或者是自行签名的,而没有让认可的证书颁发机构签名;又或者他们可能只是用错了(比如服务器名称不对)。浏览器(如 Firefox),会显示一个很大的警告通知,通知上放着“立即离开!”按钮,但你也可以仍然继续此风险- 很多人会这么做。
Go 目前在遇到证书错误时,会 bails out。对继续工作的支持非常谨慎,我还没有找到正确的方法。因此,目前也没有“继续此风险”任何示例 :-)。以后再说吧。
服务器
这边创建客户端,另一边 Web 服务器则需要处理 HTTP 请求。最早最简单的服务器只是返回文件的副本。然而,目前的服务器上,随便一个 URL 都可能触发任何计算。
文件服务器
我们从一个基本的文件服务器开始。Go 提供了一个 multi-plexer
,即一个读取和解释请求的对象。它把请求交给运行在自己线程中的 handlers。这样,许多读取 HTTP 请求,解码并转移到合适功能上的工作都可以在各自的线程中进行。
对于文件服务器,Go 也提供了一个FileServer
对象,它知道如何发布本地文件系统中的文件。它需要一个“root”目录,该目录是在本地系统中文件树的顶端;还有一个针对 URL 的匹配模式。最简单的模式是“/”,这是所有 URL 的顶部,可以匹配所有的 URL。
HTTP 服务器从本地文件系统中发布文件太简单了,让人都有点不好意思举例。如下:
/* File Server */
package main
import (
"fmt"
"net/http"
"os"
)
func main() {
// deliver files from the directory /var/www
// fileServer := http.FileServer(http.Dir("/var/www"))
fileServer := http.FileServer(http.Dir("/home/httpd/html/"))
// register the handler and deliver requests to it
err := http.ListenAndServe(":8000", fileServer)
checkError(err)
// That's it!
}
func checkError(err error) {
if err != nil {
fmt.Println("Fatal error ", err.Error())
os.Exit(1)
}
}
甚至当请求到一个不存在的文件资源时,这个服务器还提供了“404 未找到”的信息!
处理函数(Handler function)
上一个程序中,handler
被作为第二个参数传给 ListenAndServe
。可以先注册任意多个 handler
供 Handle
或 handleFunc
使用。调用方式:
func Handle(pattern string, handler Handler)
func HandleFunc(pattern string, handler func(*Conn, *Request))
ListenAndServe
的第二个参数是可以是 nil,调用会被分派到所有已注册的 handler。每个对立对象都有不同的 URL 匹配模式。例如,可能文件 handler 的 URL 匹配模式是"/",而一个函数 handler 的 URL 匹配模式是"/cgi-bin"。这里具体的模式优先级高于一般的模式。
常见的 CGI 程序有 test-cgi(shell 程序)或 printenv(Perl 程序)用来打印环境变量的值。可以让handler 用类似的方式工作。
/* Print Env */
package main
import (
"fmt"
"net/http"
"os"
)
func main() {
// file handler for most files
fileServer := http.FileServer(http.Dir("/var/www"))
http.Handle("/", fileServer)
//function handler for /cgi-bin/printenv
http.HandleFunc("/cgi-bin/printenv", printEnv)
//deliver requests to the handlers
err := http.ListenAndServe(":8000", nil)
checkError(err)
// That's it!
}
func printEnv(writer http.ResponseWriter, req *http.Request) {
env := os.Environ()
writer.Write([]byte("<h1>Environment</h1>\n<pre>"))
for _,v := range env {
writer.Write([]byte(v + "\n"))
}
writer.Write([]byte("</pre>"))
}
func checkError(err error) {
if err != nil {
fmt.Println("Fatal error ", err.Error())
os.Exit(1)
}
}
注:为简单起见,本程序不提供完整的的 HTML。这里缺少 html、head 和 body 标签。
这个程序在使用 cgi-bin 目录时有点耍赖。其实它并没有调用外部的 CGI 脚本程序,而只是使用了一个 Go 的内部函数。Go 确实可以通过 os.ForkExec 调用外部的程序,但还不能支持像 Apache 的 mod_perl 这样的动态连接库
绕过默认的 multiplexer
Go 服务器接收到的 HTTP 请求通常是由一个 multiplexer 进行处理,检查 HTTP 请求的路径,然后调用合适的文件 handler 等等。你也可以定义自己的 handler。将一个匹配模式参数和一个函数作为参数,调用http.HandleFunc
,可以将其注册为默认的 multiplexer。像** ListenAndServe这样的函数就可以使用 nil 作为 handler function。上一个例子就是这样做的。
如果你想扮演 multiplexer 的角色,那么你就可以给一个非零函数作为 handler function**。这个函数将会全权负责管理请求和响应。
下面的例子非常简单,但它说明了如何使 multiplexer 对所有请求都只返回一个“204 No content”:
/* ServerHandler */
package main
import (
"net/http"
)
func main() {
myHandler := http.HandlerFunc(func(rw http.ResponseWriter, request *http.Request) {
// Just return no content -arbitrary headers can be set, arbitrary body
rw.WriteHeader(http.StatusNoContent)
})
http.ListenAndServe(":8080", myHandler)
}
当然,也可以把它做成无所不能的。