1. Introduction
Network programming in go language is mainly implemented through the net package , the net package provides network I/O interfaces, including HTTP, TCP/IP, UDP, domain name resolution, and Unix domain sockets. Like most languages, go can start a server with just a few lines of code, but thanks to goroutine, the server implemented with go has powerful concurrent processing capabilities.
2. Socket Programming
Socket is also called "socket". Applications usually make requests to the network through "socket" Or respond to network requests.
Socket essentially sets up a channel between two computers that are connected to the network. The two computers use this channel to transfer data to each other. We know that network communication is based on IP port to locate the specific service on the specific machine of the target. The operating system has 0-65535 ports. Each port can independently provide services to the outside world. If a company is compared to a computer , then the company's switchboard number is equivalent to the IP address, and each employee's extension number is equivalent to the port. If you want to find someone in the company, you must first call the switchboard, and then transfer to the extension.
Socket programming in go is very convenient to implement. The following is the processing flow
Server side:
1. Listening port
2. Accept client connections
3. Create goroutine to process the connection
Client:
1.Establish connection
2.Send and receive data
3.Close Connection
Server example:
package main import ( "fmt" "net" ) func handle(conn net.Conn) { //处理连接方法 defer conn.Close() //关闭连接 for{ buf := make([]byte,100) n,err := conn.Read(buf) //读取客户端数据 if err!=nil { fmt.Println(err) return } fmt.Printf("read data size %d msg:%s", n, string(buf[0:n])) msg := []byte("hello,world\n") conn.Write(msg) //发送数据 } } func main() { fmt.Println("start server....") listen,err := net.Listen("tcp","0.0.0.0:3000") //创建监听 if err != nil{ fmt.Println("listen failed! msg :" ,err) return } for{ conn,errs := listen.Accept() //接受客户端连接 if errs != nil{ fmt.Println("accept failed") continue } go handle(conn) //处理连接 } }
Client example:
package main import ( "bufio" "fmt" "net" "os" "strings" ) func main() { conn, err := net.Dial("tcp", "127.0.0.1:3000") if err != nil { fmt.Println("err dialing:", err.Error()) return } defer conn.Close() inputReader := bufio.NewReader(os.Stdin) for { str, _ := inputReader.ReadString('\n') data := strings.Trim(str, "\n") if data == "quit" { //输入quit退出 return } _, err := conn.Write([]byte(data)) //发送数据 if err != nil { fmt.Println("send data error:", err) return } buf := make([]byte,512) n,err := conn.Read(buf) //读取服务端端数据 fmt.Println("from server:", string(buf[:n])) } }
conn example also provides other methods:
type Conn interface { // Read reads data from the connection. // Read can be made to time out and return an Error with Timeout() == true // after a fixed time limit; see SetDeadline and SetReadDeadline. Read(b []byte) (n int, err error) //读取连接中数据 // Write writes data to the connection. // Write can be made to time out and return an Error with Timeout() == true // after a fixed time limit; see SetDeadline and SetWriteDeadline. Write(b []byte) (n int, err error) //发送数据 // Close closes the connection. // Any blocked Read or Write operations will be unblocked and return errors. Close() error //关闭链接 // LocalAddr returns the local network address. LocalAddr() Addr //返回本地连接地址 // RemoteAddr returns the remote network address. RemoteAddr() Addr //返回远程连接的地址 // SetDeadline sets the read and write deadlines associated // with the connection. It is equivalent to calling both // SetReadDeadline and SetWriteDeadline. // // A deadline is an absolute time after which I/O operations // fail with a timeout (see type Error) instead of // blocking. The deadline applies to all future and pending // I/O, not just the immediately following call to Read or // Write. After a deadline has been exceeded, the connection // can be refreshed by setting a deadline in the future. // // An idle timeout can be implemented by repeatedly extending // the deadline after successful Read or Write calls. // // A zero value for t means I/O operations will not time out. SetDeadline(t time.Time) error //设置链接读取或者写超时时间 // SetReadDeadline sets the deadline for future Read calls // and any currently-blocked Read call. // A zero value for t means Read will not time out. SetReadDeadline(t time.Time) error //单独设置读取超时时间 // SetWriteDeadline sets the deadline for future Write calls // and any currently-blocked Write call. // Even if write times out, it may return n > 0, indicating that // some of the data was successfully written. // A zero value for t means Write will not time out. SetWriteDeadline(t time.Time) error//单独设置写超时时间 }
三, HTTP service processing process in go
Introduction
With the development of the network, many network applications are built on the basis of HTTP services. From its inception to the present, the HTTP protocol has developed continuously from 1.0, 1.1 to 2.0. Apart from the details, to understand the network application built by HTTP, you only need to pay attention to the two ends---the client (clinet) and the server (server). The interaction between the two ends comes from the request of the clinet and the response of the server. The so-called http server mainly depends on how to accept clinet's request and return response to the client. In the process of receiving requests, the most important thing is the router, that is, the implementation of a Multiplexer. In Go, you can use the built-in mutilplexer --- DefautServeMux, or you can customize it. The purpose of Multiplexer routing is to find the handler function (handler), which will process the request and build the response at the same time.
The final simplified request processing process is:
Clinet -> Requests -> [Multiplexer(router) -> handler -> Response -> Clinet
Therefore, the most important thing to understand the http service in go is to understand Multiplexer and handler. Multiplexer in Golang is based on the ServeMux structure and also implements Handler interface.
Object description:
1. Handler function: A function with the signature of func(w http.ResponseWriter, r *http.Requests)
2. Handler function: After HandlerFunc The handler function wrapped by the structure, which implements the functions of the ServeHTTP interface method. When the ServeHTTP method of the handler is called, the handler function itself is called.
3. Handler object: implements the structure of the Handler interface ServeHTTP method.
The difference between the handler processor and the handler object is that one is a function and the other is a structure. They both implement the ServeHTTP method. In many cases, their functions are similar, and they are collectively referred to as handlers below.
Handler
Golang does not have inheritance, and class polymorphism can be achieved through interfaces. The so-called interface is a definition that declares a function signature. As long as any structure implements the same method as the interface function signature, it is equivalent to realizing the interface. Go's http services are all processed based on handlers.
type Handler interface { ServeHTTP(ResponseWriter, *Request) }
Any structure, as long as it implements the ServeHTTP method, this structure can be called a handler object. ServeMux will use the handler and call its ServeHTTP method to process the request and return the response.
ServeMux
Source code part:
type ServeMux struct { mu sync.RWMutex m map[string]muxEntry hosts bool } type muxEntry struct { explicit bool h Handler pattern string }
The most important field in the ServeMux structure is m, which is a map, and the key is some url pattern. value is a muxEntry structure, which defines and stores specific url patterns and handlers.
Of course, the so-called ServeMux also implements the ServeHTTP interface, which is also a handler. However, the ServeHTTP method of ServeMux is not used to process requests and responses, but to find the handler for route registration, which will be explained later.
Server
In addition to ServeMux and Handler, there is another structure Server that needs to be understood. It can be seen from the source code of http.ListenAndServe that it creates a server object and calls the ListenAndServe method of the server object:
func ListenAndServe(addr string, handler Handler) error { server := &Server{Addr: addr, Handler: handler} return server.ListenAndServe() }
View the structure of the server as follows:
type Server struct { Addr string Handler Handler ReadTimeout time.Duration WriteTimeout time.Duration TLSConfig *tls.Config MaxHeaderBytes int TLSNextProto map[string]func(*Server, *tls.Conn, Handler) ConnState func(net.Conn, ConnState) ErrorLog *log.Logger disableKeepAlives int32 nextProtoOnce sync.Once nextProtoErr error }
server结构存储了服务器处理请求常见的字段。其中Handler字段也保留Handler接口。如果Server接口没有提供Handler结构对象,那么会使用DefautServeMux做multiplexer,后面再做分析。
创建HTTP服务
创建一个http服务,大致需要经历两个过程,首先需要注册路由,即提供url模式和handler函数的映射,其次就是实例化一个server对象,并开启对客户端的监听。
http.HandleFunc("/", indexHandler) http.ListenAndServe("127.0.0.1:8000", nil) 或 server := &Server{Addr: addr, Handler: handler} server.ListenAndServe()
示例:
package main import ( "fmt" "net/http" ) func Hello(w http.ResponseWriter, r *http.Request) { fmt.Println("Hello World.") fmt.Fprintf(w, "Hello World.\n") } func main() { http.HandleFunc("/", Hello) err := http.ListenAndServe("0.0.0.0:6000", nil) if err != nil { fmt.Println("http listen failed.") } } //curl http://127.0.0.1:6000 // 结果:Hello World
路由注册
net/http包暴露的注册路由的api很简单,http.HandleFunc选取了DefaultServeMux作为multiplexer:
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) { DefaultServeMux.HandleFunc(pattern, handler) }
DefaultServeMux是ServeMux的一个实例。当然http包也提供了NewServeMux方法创建一个ServeMux实例,默认则创建一个DefaultServeMux:
// NewServeMux allocates and returns a new ServeMux. func NewServeMux() *ServeMux { return new(ServeMux) } // DefaultServeMux is the default ServeMux used by Serve. var DefaultServeMux = &defaultServeMux var defaultServeMux ServeMux
DefaultServeMux的HandleFunc(pattern, handler)方法实际是定义在ServeMux下的:
// HandleFunc registers the handler function for the given pattern.func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) { mux.Handle(pattern, HandlerFunc(handler)) }
HandlerFunc是一个函数类型。同时实现了Handler接口的ServeHTTP方法。使用HandlerFunc类型包装一下路由定义的indexHandler函数,其目的就是为了让这个函数也实现ServeHTTP方法,即转变成一个handler处理器(函数)。
type HandlerFunc func(ResponseWriter, *Request) // ServeHTTP calls f(w, r). func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) { f(w, r) }
我们最开始写的例子中
http.HandleFunc("/",Indexhandler)
这样 IndexHandler 函数也有了ServeHTTP方法。ServeMux的Handle方法,将会对pattern和handler函数做一个map映射:
func ListenAndServe(addr string, handler Handler) error { server := &Server{Addr: addr, Handler: handler} return server.ListenAndServe() } // ListenAndServe listens on the TCP network address srv.Addr and then // calls Serve to handle requests on incoming connections. // Accepted connections are configured to enable TCP keep-alives. // If srv.Addr is blank, ":http" is used. // ListenAndServe always returns a non-nil error. func (srv *Server) ListenAndServe() error { addr := srv.Addr if addr == "" { addr = ":http" } ln, err := net.Listen("tcp", addr) if err != nil { return err } return srv.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)}) }
Server的ListenAndServe方法中,会初始化监听地址Addr,同时调用Listen方法设置监听。最后将监听的TCP对象传入Serve方法:
// Serve accepts incoming connections on the Listener l, creating a // new service goroutine for each. The service goroutines read requests and // then call srv.Handler to reply to them. // // For HTTP/2 support, srv.TLSConfig should be initialized to the // provided listener's TLS Config before calling Serve. If // srv.TLSConfig is non-nil and doesn't include the string "h2" in // Config.NextProtos, HTTP/2 support is not enabled. // // Serve always returns a non-nil error. After Shutdown or Close, the // returned error is ErrServerClosed. func (srv *Server) Serve(l net.Listener) error { defer l.Close() if fn := testHookServerServe; fn != nil { fn(srv, l) } var tempDelay time.Duration // how long to sleep on accept failure if err := srv.setupHTTP2_Serve(); err != nil { return err } srv.trackListener(l, true) defer srv.trackListener(l, false) baseCtx := context.Background() // base is always background, per Issue 16220 ctx := context.WithValue(baseCtx, ServerContextKey, srv) for { rw, e := l.Accept() if e != nil { select { case <-srv.getDoneChan(): return ErrServerClosed default: } if ne, ok := e.(net.Error); ok && ne.Temporary() { if tempDelay == 0 { tempDelay = 5 * time.Millisecond } else { tempDelay *= 2 } if max := 1 * time.Second; tempDelay > max { tempDelay = max } srv.logf("http: Accept error: %v; retrying in %v", e, tempDelay) time.Sleep(tempDelay) continue } return e } tempDelay = 0 c := srv.newConn(rw) c.setState(c.rwc, StateNew) // before Serve can return go c.serve(ctx) } }
监听开启之后,一旦客户端请求到底,go就开启一个协程处理请求,主要逻辑都在serve方法之中。
serve方法比较长,其主要职能就是,创建一个上下文对象,然后调用Listener的Accept方法用来 获取连接数据并使用newConn方法创建连接对象。最后使用goroutine协程的方式处理连接请求。因为每一个连接都开起了一个协程,请求的上下文都不同,同时又保证了go的高并发。serve也是一个长长的方法:
// Serve a new connection. func (c *conn) serve(ctx context.Context) { c.remoteAddr = c.rwc.RemoteAddr().String() ctx = context.WithValue(ctx, LocalAddrContextKey, c.rwc.LocalAddr()) defer func() { if err := recover(); err != nil && err != ErrAbortHandler { const size = 64 << 10 buf := make([]byte, size) buf = buf[:runtime.Stack(buf, false)] c.server.logf("http: panic serving %v: %v\n%s", c.remoteAddr, err, buf) } if !c.hijacked() { c.close() c.setState(c.rwc, StateClosed) } }() if tlsConn, ok := c.rwc.(*tls.Conn); ok { if d := c.server.ReadTimeout; d != 0 { c.rwc.SetReadDeadline(time.Now().Add(d)) } if d := c.server.WriteTimeout; d != 0 { c.rwc.SetWriteDeadline(time.Now().Add(d)) } if err := tlsConn.Handshake(); err != nil { c.server.logf("http: TLS handshake error from %s: %v", c.rwc.RemoteAddr(), err) return } c.tlsState = new(tls.ConnectionState) *c.tlsState = tlsConn.ConnectionState() if proto := c.tlsState.NegotiatedProtocol; validNPN(proto) { if fn := c.server.TLSNextProto[proto]; fn != nil { h := initNPNRequest{tlsConn, serverHandler{c.server}} fn(c.server, tlsConn, h) } return } } // HTTP/1.x from here on. ctx, cancelCtx := context.WithCancel(ctx) c.cancelCtx = cancelCtx defer cancelCtx() c.r = &connReader{conn: c} c.bufr = newBufioReader(c.r) c.bufw = newBufioWriterSize(checkConnErrorWriter{c}, 4<<10) for { w, err := c.readRequest(ctx) if c.r.remain != c.server.initialReadLimitSize() { // If we read any bytes off the wire, we're active. c.setState(c.rwc, StateActive) } if err != nil { const errorHeaders = "\r\nContent-Type: text/plain; charset=utf-8\r\nConnection: close\r\n\r\n" if err == errTooLarge { // Their HTTP client may or may not be // able to read this if we're // responding to them and hanging up // while they're still writing their // request. Undefined behavior. const publicErr = "431 Request Header Fields Too Large" fmt.Fprintf(c.rwc, "HTTP/1.1 "+publicErr+errorHeaders+publicErr) c.closeWriteAndWait() return } if isCommonNetReadError(err) { return // don't reply } publicErr := "400 Bad Request" if v, ok := err.(badRequestError); ok { publicErr = publicErr + ": " + string(v) } fmt.Fprintf(c.rwc, "HTTP/1.1 "+publicErr+errorHeaders+publicErr) return } // Expect 100 Continue support req := w.req if req.expectsContinue() { if req.ProtoAtLeast(1, 1) && req.ContentLength != 0 { // Wrap the Body reader with one that replies on the connection req.Body = &expectContinueReader{readCloser: req.Body, resp: w} } } else if req.Header.get("Expect") != "" { w.sendExpectationFailed() return } c.curReq.Store(w) if requestBodyRemains(req.Body) { registerOnHitEOF(req.Body, w.conn.r.startBackgroundRead) } else { if w.conn.bufr.Buffered() > 0 { w.conn.r.closeNotifyFromPipelinedRequest() } w.conn.r.startBackgroundRead() } // HTTP cannot have multiple simultaneous active requests.[*] // Until the server replies to this request, it can't read another, // so we might as well run the handler in this goroutine. // [*] Not strictly true: HTTP pipelining. We could let them all process // in parallel even if their responses need to be serialized. // But we're not going to implement HTTP pipelining because it // was never deployed in the wild and the answer is HTTP/2. serverHandler{c.server}.ServeHTTP(w, w.req) w.cancelCtx() if c.hijacked() { return } w.finishRequest() if !w.shouldReuseConnection() { if w.requestBodyLimitHit || w.closedRequestBodyEarly() { c.closeWriteAndWait() } return } c.setState(c.rwc, StateIdle) c.curReq.Store((*response)(nil)) if !w.conn.server.doKeepAlives() { // We're in shutdown mode. We might've replied // to the user without "Connection: close" and // they might think they can send another // request, but such is life with HTTP/1.1. return } if d := c.server.idleTimeout(); d != 0 { c.rwc.SetReadDeadline(time.Now().Add(d)) if _, err := c.bufr.Peek(4); err != nil { return } } c.rwc.SetReadDeadline(time.Time{}) } } serve方法
使用defer定义了函数退出时,连接关闭相关的处理。然后就是读取连接的网络数据,并处理读取完毕时候的状态。接下来就是调用serverHandler{c.server}.ServeHTTP(w, w.req)方法处理请求了。最后就是请求处理完毕的逻辑。
serverHandler是一个重要的结构,它近有一个字段,即Server结构,同时它也实现了Handler接口方法ServeHTTP,并在该接口方法中做了一个重要的事情,初始化multiplexer路由多路复用器。
如果server对象没有指定Handler,则使用默认的DefaultServeMux作为路由Multiplexer。并调用初始化Handler的ServeHTTP方法。
// serverHandler delegates to either the server's Handler or // DefaultServeMux and also handles "OPTIONS *" requests. type serverHandler struct { srv *Server } func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) { handler := sh.srv.Handler if handler == nil { handler = DefaultServeMux } if req.RequestURI == "*" && req.Method == "OPTIONS" { handler = globalOptionsHandler{} } handler.ServeHTTP(rw, req) }
这里DefaultServeMux的ServeHTTP方法其实也是定义在ServeMux结构中的,相关代码如下:
// Find a handler on a handler map given a path string. // Most-specific (longest) pattern wins. func (mux *ServeMux) match(path string) (h Handler, pattern string) { // Check for exact match first. v, ok := mux.m[path] if ok { return v.h, v.pattern } // Check for longest valid match. var n = 0 for k, v := range mux.m { if !pathMatch(k, path) { continue } if h == nil || len(k) > n { n = len(k) h = v.h pattern = v.pattern } } return } func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) { // CONNECT requests are not canonicalized. if r.Method == "CONNECT" { return mux.handler(r.Host, r.URL.Path) } // All other requests have any port stripped and path cleaned // before passing to mux.handler. host := stripHostPort(r.Host) path := cleanPath(r.URL.Path) if path != r.URL.Path { _, pattern = mux.handler(host, path) url := *r.URL url.Path = path return RedirectHandler(url.String(), StatusMovedPermanently), pattern } return mux.handler(host, r.URL.Path) } // handler is the main implementation of Handler. // The path is known to be in canonical form, except for CONNECT methods. func (mux *ServeMux) handler(host, path string) (h Handler, pattern string) { mux.mu.RLock() defer mux.mu.RUnlock() // Host-specific pattern takes precedence over generic ones if mux.hosts { h, pattern = mux.match(host + path) } if h == nil { h, pattern = mux.match(path) } if h == nil { h, pattern = NotFoundHandler(), "" } return } // ServeHTTP dispatches the request to the handler whose // pattern most closely matches the request URL. func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) { if r.RequestURI == "*" { if r.ProtoAtLeast(1, 1) { w.Header().Set("Connection", "close") } w.WriteHeader(StatusBadRequest) return } h, _ := mux.Handler(r) h.ServeHTTP(w, r) }
mux的ServeHTTP方法通过调用其Handler方法寻找注册到路由上的handler函数,并调用该函数的ServeHTTP方法,本例则是IndexHandler函数。
mux的Handler方法对URL简单的处理,然后调用handler方法,后者会创建一个锁,同时调用match方法返回一个handler和pattern。 在match方法中,mux的m字段是map[string]muxEntry图,后者存储了pattern和handler处理器函数,因此通过迭代m寻找出注册路由的patten模式与实际url匹配的handler函数并返回。
返回的结构一直传递到mux的ServeHTTP方法,接下来调用handler函数的ServeHTTP方法,即IndexHandler函数,然后把response写到http.RequestWirter对象返回给客户端。
上述函数运行结束即`serverHandler{c.server}.ServeHTTP(w, w.req)`运行结束。接下来就是对请求处理完毕之后上希望和连接断开的相关逻辑。 至此,Golang中一个完整的http服务介绍完毕,包括注册路由,开启监听,处理连接,路由处理函数。
总结
多数的web应用基于HTTP协议,客户端和服务器通过request-response的方式交互。一个server并不可少的两部分莫过于路由注册和连接处理。Golang通过一个ServeMux实现了的multiplexer路由多路复用器来管理路由。同时提供一个Handler接口提供ServeHTTP用来实现handler处理其函数,后者可以处理实际request并构造response。
ServeMux和handler处理器函数的连接桥梁就是Handler接口。ServeMux的ServeHTTP方法实现了寻找注册路由的handler的函数,并调用该handler的ServeHTTP方法。
ServeHTTP方法就是真正处理请求和构造响应的地方。 回顾go的http包实现http服务的流程,可见大师们的编码设计之功力。学习有利提高自身的代码逻辑组织能力。更好的学习方式除了阅读,就是实践,接下来,我们将着重讨论来构建http服务。尤其是构建http中间件函数。
四、HTTP客户端工具
net/http不仅提供了服务端处理,还提供了客户端处理功能。
http包中提供了Get、Post、Head、PostForm方法实现HTTP请求:
//GET func Get(url string) (resp *Response, err error) { return DefaultClient.Get(url) } //POST func Post(url string, contentType string, body io.Reader) (resp *Response, err error) { return DefaultClient.Post(url, contentType, body) } //HEAD func Head(url string) (resp *Response, err error) { return DefaultClient.Head(url) } //POSTFORM func PostForm(url string, data url.Values) (resp *Response, err error) { return DefaultClient.PostForm(url, data) }
GET请求示例
package main import ( "fmt" "net/http" "log" "reflect" "bytes" ) func main() { resp, err := http.Get("http://www.baidu.com") if err != nil { // 错误处理 log.Println(err) return } defer resp.Body.Close() //关闭链接 headers := resp.Header for k, v := range headers { fmt.Printf("k=%v, v=%v\n", k, v) //所有头信息 } fmt.Printf("resp status %s,statusCode %d\n", resp.Status, resp.StatusCode) fmt.Printf("resp Proto %s\n", resp.Proto) fmt.Printf("resp content length %d\n", resp.ContentLength) fmt.Printf("resp transfer encoding %v\n", resp.TransferEncoding) fmt.Printf("resp Uncompressed %t\n", resp.Uncompressed) fmt.Println(reflect.TypeOf(resp.Body)) buf := bytes.NewBuffer(make([]byte, 0, 512)) length, _ := buf.ReadFrom(resp.Body) fmt.Println(len(buf.Bytes())) fmt.Println(length) fmt.Println(string(buf.Bytes())) }
使用http.Do设置请求头、cookie等
package main import ( "net/http" "strings" "io/ioutil" "log" "fmt" ) func main() { client := &http.Client{} req, err := http.NewRequest("POST", "http://www.baidu.com", strings.NewReader("name=xxxx&passwd=xxxx")) if err != nil { fmt.Println(err) return } req.Header.Set("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8") //设置请求头信息 resp, err := client.Do(req) defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { log.Println(err) return } var res string res = string(body[:]) fmt.Println(res) }
POST请求示例
package main import ( "net/http" "strings" "fmt" "io/ioutil" ) func main() { resp, err := http.Post("http://www.baidu.com", "application/x-www-form-urlencoded", strings.NewReader("username=xxx&password=xxxx")) if err != nil { fmt.Println(err) return } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { fmt.Println(err) return } fmt.Println(string(body)) }
PostForm请求示例
package main import ( "net/http" "fmt" "io/ioutil" "net/url" ) func main() { postParam := url.Values{ "name": {"wd"}, "password": {"1234"}, } resp, err := http.PostForm("https://cn.bing.com/", postParam) if err != nil { fmt.Println(err) return } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { fmt.Println(err) return } fmt.Println(string(body)) }
The above is the detailed content of go language network programming, http processing process details. For more information, please follow other related articles on the PHP Chinese website!