重学Go语言 | Go Web编程详解

公众号:程序员读书,欢迎关注

Web开发是Go语言使用的一个主要方向,使用Go语言做Web开发是一件很简单的事情,你甚至可以只用几行代码就可以启动一个Web Server,这主要还是因为Go标准库net/http包对HTTP协议的完善支持。

标准库net/http包含WebClientServer两个部分,在这里我们只讲与Server相关的部分。

入门

net/http包里处理HTTP请求函数方法都有一个统一的函数签名:

go 复制代码
func (w http.ResponseWriter, r *http.Request)

可以看到,这个函数接收两个参数,分别对应HTTP的请求(request)与响应(response):

  • http.ResponseWriter:响应对象,实现了io.Writer接口,用于将数据发送回客户端(比如浏览器)
  • *http.Request:请求对象,可以通过该对象获得Query参数,表单数据,CookiesHeadersHTTP请求数据。

net/http包的HandleFunc()函数则可以将符合上面签名的函数添加到路由中,用于处理HTTP请求:

go 复制代码
func profile(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w,"this is profile page")
}

//将profile函数注册到路由
http.HandleFunc("/profile",profile)

//添加一个匿名函数用于处理请求
http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "Hello, the url you are requesting is : %s\n", r.URL.Path)
})

路由注册后需要调用http.ListenAndServe()函数启动Web服务监听HTTP请求:

go 复制代码
http.ListenAndServe(":80", nil)

下面是一个启动简单Web服务器的完整代码:

go 复制代码
//main.go
package main

import (
    "fmt"
    "net/http"
)

func profile(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w,"this is profile page")
}

func main() {
    http.HandleFunc("/profile",profile)
  
	http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "Hello, the url you are requesting is : %s\n", r.URL.Path)
	})

    http.ListenAndServe(":80", nil)
}

运行命令启动服务器:

shell 复制代码
$ go run main.go

curl发送如下请求:

shell 复制代码
$ curl http://localhost/hello
Hello, the url you are requesting is : /hello

$ curl http://localhost/profile
this is profile page

探究

Go语言启动一个Web服务器如此简单,原因在于net/http包帮我们做了大量的封装。

下面我们来探究一下net/http到底帮我们做了什么。

当我们把一个函数传给http.HandleFunc()时,http.HandleFunc()实际上是调用

DefaultServeMux变量的HandleFunc()方法:

go 复制代码
//http包的HandleFunc
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
	DefaultServeMux.HandleFunc(pattern, handler)
}

DefaultServeMuxHandleFunc()函数会帮我们把传进去的函数转换为一个实现了http.HandlerFunc函数:

go 复制代码
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
	if handler == nil {
		panic("http: nil handler")
	}
	mux.Handle(pattern, HandlerFunc(handler))
}

http.HandlerFunc函数实现了http.Handler接口:

go 复制代码
type HandlerFunc func(ResponseWriter, *Request)

// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
	f(w, r)
}
type Handler interface {
	ServeHTTP(ResponseWriter, *Request)
}

DefaultServeMuxHandleFunc()方法同时也会调用Handle()方法将路由(pattern)与转换后的函数做绑定后保存到一个map当中:

go 复制代码
func (mux *ServeMux) Handle(pattern string, handler Handler) {
	//省略其他代码...
	e := muxEntry{h: handler, pattern: pattern}
	mux.m[pattern] = e
	//省略其他代码...
}

我们调用http.HandleFunc()函数的目的就是将路由与函数绑定后保存到DefaultServeMux中。

DefaultServeMux实际类型是ServeMux,也叫多路复用器DefaultServeMux也实现了http.Handler接口,ServeHTTP()方法是最终处理HTTP请求的地方。

接下来,当我们调用http.ListenAndServe()函数时,在该函数中会创建一个http.Server结构体类型,并调用该结构体的ListenAndServe()方法监听请求:

go 复制代码
func ListenAndServe(addr string, handler Handler) error {
	server := &Server{Addr: addr, Handler: handler}
	return server.ListenAndServe()
}

http.ServerListenAndServe()方法会一直监听,一旦有请求就会创建连接对象(Connection),并且将http.Server封装到http.serverHandler中,在http.serverHandlerServeHTTP()方法中调用http.ServerHandler处理请求:

go 复制代码
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)
}

http.ListenAndServe()函数的第二个参数是http.Handler,即我们可以自定义一个多路复用器并传递给ListenAndServe()函数,这样就会调用用DefaultServeMux了:

go 复制代码
package main

import (
	"fmt"
	"net/http"
)

type MyMux struct {
}

func (m MyMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	fmt.Fprint(w, "this is my mux")
}

func main() {
	http.ListenAndServe(":80", &MyMux{})
}

http.ListenAndServe()函数实际是调用http.Server的,所以我们也可以直接自定义创建一个http.Server结构,这样可以自定义http.Server结构体的字段(该结构体有挺多字段的)。

下面是一个自定义多路复用器以及自定义http.Server来启动Web服务器的小案例:

go 复制代码
package main

import (
	"fmt"
	"net/http"
)

type MyMux struct {
}

func (m MyMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	fmt.Fprint(w, "this is my mux")
}

func main() {
	s := http.Server{
		Addr:    ":80",
		Handler: MyMux{},
		//默认为1M,这里改成2M
		MaxHeaderBytes: 1 << 21,
	}
	s.ListenAndServe()
}

请求参数

HTTP请求中,要发送参数给服务器有两种方式,一种是将参数URL的问号后面并用&分隔的Query,另一种是放在请求体(Body)中的数据。

Query

Query是指发送GET请求,此时参数是跟在URL问号后面的部分,各参数之间使用&分隔:

shell 复制代码
http://localhost?param1=value1&param2=value2

要获得Query参数,要先http.RequestParseForm()方法解析URL之后,解析 后的Query参数被放在http.RequestForm字段中,这个字段的类型为url.Values

go 复制代码
package main
import (
	"fmt"
	"net/http"
)
func main() {

	http.HandleFunc("/user", func(w http.ResponseWriter, r *http.Request) {
    r.ParseForm()
    id := r.Form["id"][0] 
		fmt.Println(id)
	})
	http.ListenAndServe(":80", nil)
}

另一种获取Query参数的方法就是可以访问http.RequestFormValue()方法,这个方法会根据key值自动调用ParseForm()方法并从Form字段中查找数据:

go 复制代码
package main
import (
	"fmt"
	"net/http"
)
func main() {

	http.HandleFunc("/user", func(w http.ResponseWriter, r *http.Request) {
		id := r.FormValue("id") 
		fmt.Println(id)
	})
	http.ListenAndServe(":80", nil)
}

Body

当发送POSTPut或者PATCH请求时,数据主要是通过HTTP协议的Body传递给服务器,而当请求头的Content-Type的值不同时,对应的Body数据格式也不同,这里我们讨论三种最常见的Content-Type取值:

  • application/x-www-form-urlencoded
  • multipart/form-data
  • application/json

application/x-www-form-urlencoded

这种格式也就是一般我们所说的表单数据,有两种获取方式。

第一种方式与前面的获取Query一样,通过http.RequestForm字段或者FormValue()方法获取即可,因为http.RequestParseForm()方法在解析参数时会把BodyQuery的参数合在在Form字段当中。

第二种方式是先调用http.RequestParseForm(),然后从http.RequestPostForm字段取值,这个字段存储着表单数据。

go 复制代码
package main
import (
	"fmt"
	"net/http"
)
func main() {

	http.HandleFunc("/user", func(w http.ResponseWriter, r *http.Request) {
    r.ParseForm()
    name := r.PostForm["name"][0] 
		fmt.Println(name)
	})
	http.ListenAndServe(":80", nil)
}

调用http.RequestPostFormValue()方法同样可以根据key值自动调用ParseForm()方法对应获取Body中的数据:

go 复制代码
package main
import (
	"fmt"
	"net/http"
)
func main() {

	http.HandleFunc("/user", func(w http.ResponseWriter, r *http.Request) {
		name := r.PostFormValue("name") 
		fmt.Println(name)
	})
	http.ListenAndServe(":80", nil)
}

multipart/form-data

Content-Typemultipart/form-data时,更多的时候是用于上传文件的,而关于文件上传的内容,我们在在这篇文章的后续小节会讲到。

当通过这种方式传递非文件数据时,其获取方式则与application/x-www-form-urlencoded是一样的。

application/json

Content-Typeapplication/json时,说明此时Body里的数据格式为JSON格式,这个时候我们就需要自己读取Body的内容并反序列化到自己定义的数据结构中:

go 复制代码
package main

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net/http"
)

type User struct {
	ID   int    `json:"id"`
	Name string `json:"name"`
}

func main() {

	http.HandleFunc("/testJson", func(w http.ResponseWriter, r *http.Request) {
		params, _ := ioutil.ReadAll(r.Body)
		defer r.Body.Close()
		var u User
		json.Unmarshal(params, &u)
		fmt.Println(u)
	})

	http.ListenAndServe(":80", nil)
}

模板处理

Gohtml/template包提供了HTML模板页面的动态解析,我们可以按照这个包的语法将HTML动态渲染并返回给客户端:

go 复制代码
package main

import (
	"html/template"
	"net/http"
)

func main() {
	http.HandleFunc("/test", func(w http.ResponseWriter, r *http.Request) {
		//模板
        html := `{{.}},{{define "T"}}Hello, {{.}}!{{end}}`
        //创建模板,解析模板
		t, _ := template.New("foo").Parse()

		//_ = t.ExecuteTemplate(w, "T", "ttt")
		//模板输出
		t.Execute(w, "This is a test page")
	})
	http.ListenAndServe(":80", nil)
}

下面是Go的html/template包的一些常用模板语句:

模板语句 说明
{{/* a comment */}} 注释
{{.}} 获取根变量
{{.Title}} 访问名称为Title的变量
{{if .Done}} {{else}} {{end}} IF分支判断
{{range .Todos}} {{.}} {{end}} 遍历Todos变量,使用{{.}}访问Todos的每个子项
{{block "content" .}} {{end}} 设置名称为"content"的模板

由于现在一般都是前后端分离,因此很少会使用这种模板来开发网页,因此在这里只要稍做了解即可。

JSON响应

如果我们开发的是API接口,那么更常见的是返回JSON数据,比如像下面这样的JSON格式:

json 复制代码
{
	"code": 200,
	"message": "success",
	"data": [
		{
			"id": 1,
			"name": "小明",
			"class": "高中二年级二班"
		},
		{
			"id": 2,
			"name": "小花",
			"class": "高中二年级三班"
		},
		{
			"id": 3,
			"name": "小海",
			"class": "高中二年级二班"
		}
	]
}

Go语言的encoding/json包对JSON做了非常好的支持,因为我们完全可以调用这个包来返回数据给客户端:

go 复制代码
package main

import (
	"encoding/json"
	"fmt"
	"net/http"
)

type Student struct {
	ID    int    `json:"id"`
	Name  string `json:"name"`
	Class string `json:"class"`
}

type Result struct {
	Code    int         `json:"code"`
	Message string      `json:"message"`
	Data    interface{} `json:"data"`
}

func main() {

	http.HandleFunc("/students", func(w http.ResponseWriter, r *http.Request) {
		students := []Student{
			{ID: 1, Name: "小明", Class: "高中二年级二班"},
			{ID: 2, Name: "小花", Class: "高中二年级三班"},
			{ID: 3, Name: "小海", Class: "高中二年级二班"},
		}

		rs := Result{Code: 200, Message: "success", Data: students}

		rsJson, _ := json.MarshalIndent(rs, "", "\t")
		fmt.Fprint(w, string(rsJson))
	})
	http.ListenAndServe(":80", nil)
}

静态资源

除了处理动态请求,Web服务器也需要处理静态资源请求,比如JS,HTML,CSS等静态代码,或者是图片。

go 复制代码
package main

import "net/http"

func main() {
	fs := http.FileServer(http.Dir("static/"))
	http.Handle("/assets/", http.StripPrefix("/assets/", fs))

	http.ListenAndServe(":80", nil)
}

上面的示例中,我们将当前目录下的static目录设置为静态资源目录,其访问路由前缀为assets

arduino 复制代码
├── main.go
└── static
    ├── css
    │   └── index.css
    ├── images
    │   └── 1.png
    └── js
        └── index.js

文件上传

http.RequestFormFile()方法可以读取HTTP文件上传请求,该函数的签名如下:

go 复制代码
func (r *Request) FormFile(key string) (multipart.File, *multipart.FileHeader, error)

函数的参数为HTTP请求的文件字段名称,调用后返回multipart.Filemultipart.FileHeader,下面是一个完整的文件上传小案例:

go 复制代码
package main

import (
	"io"
	"net/http"
	"os"
)

func main() {
	http.HandleFunc("/upload", func(w http.ResponseWriter, r *http.Request) {
		file, header, err := r.FormFile("file")
		if err != nil {
			w.WriteHeader(http.StatusBadRequest)
			w.Write([]byte("无效请求"))
			return
		}
		defer file.Close()
		//最大为2M
		max := 1 << 21
		//大小限制
		if header.Size > int64(max) {
			w.WriteHeader(http.StatusBadRequest)
			w.Write([]byte("文件大小不能超过2M"))
			return
		}

		//读取ContentType
		buffer := make([]byte, 512)
		ff, _ := header.Open()
		ff.Read(buffer)
		contentType := http.DetectContentType(buffer)

		//只允许上传png图片
		if contentType != "image/png" {
			w.WriteHeader(http.StatusBadRequest)
			w.Write([]byte("文件格式错误"))
			return
		}
		//把文件放在/upload目录下
		f, err := os.Create("./upload/" + header.Filename)
		if err != nil {
			w.WriteHeader(http.StatusInternalServerError)
			w.Write([]byte("服务器内部错误"))
			return
		}
		defer f.Close()
		io.Copy(f, file)
		w.Write([]byte("文件上传成功"))
	})
	http.ListenAndServe(":80", nil)
}

Cookie与Session

HTTP协议是一种无状态的应用层协议,也就说HTTP服务器无法判断两次HTTP请求是否为同一个用户发送的,所以需要一种可以追踪用户的机制。

最常用的机制就是CookieSession

Cookie是一种把追踪用户数据存放在客户端(一般是指浏览器)的机制,其原理是Web服务器通过Set-Cookie响应头 将数据发送给客户端,客户端保存起来,并且在之后的请求中通过Cookie请求头 将该数据发送给Web服务器。

go 复制代码
package main

import (
	"fmt"
	"net/http"
)

func main() {

	http.HandleFunc("/testCookie", func(w http.ResponseWriter, r *http.Request) {

		cookieOne := http.Cookie{
			Name:  "c1",
			Value: "this is c1",
		}

		cookieTwo := http.Cookie{
			Name:  "c2",
			Value: "this is c2",
		}
		w.Header().Set("Set-Cookie", cookieOne.String())
		w.Header().Add("Set-Cookie", cookieTwo.String())

		fmt.Fprintln(w, "test")
	})

	http.ListenAndServe(":8080", nil)
}

Session

Cookie相比,Session则是一种把用户数据存储在服务端的会话机制,当然使用Session的前提是要能跟踪客户端,而Cookie可以跟踪客户端,所以Session一般跟Cookie配合使用。

Go标准库并没有提供操作Session的实现,而Session也仅仅是把数据存储在服务端而已,所以自己实现一个就可以了。

不过为了避免重复造轮子,还是推荐使用第三方库,比如github.com/gorilla/sessions

go 复制代码
package main

import (
	"net/http"
	"os"

	"github.com/gorilla/sessions"
)

//SESSION_KEY不要直接保存在源码里
//可以放在命令行或者环境变量中
var store = sessions.NewCookieStore([]byte(os.Getenv("SESSION_KEY")))

func main() {

	http.HandleFunc("/session", func(w http.ResponseWriter, r *http.Request) {
		session, _ := store.Get(r, "session-name")
		session.Values["foo"] = "bar"
		session.Values[42] = 43
		err := session.Save(r, w)
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
	})

	http.ListenAndServe(":80", nil)
}

小结

使用Go语言进行Web开发是一种非常简单的事情,使用标准库net/http包就可以开发一个完整的Web应用程序,再引出其他第三方库,比如路由,session等,则使我们的应用程序更加完善。

相关推荐
求知若饥7 分钟前
NestJS 项目实战-权限管理系统开发(六)
后端·node.js·nestjs
gb42152871 小时前
springboot中Jackson库和jsonpath库的区别和联系。
java·spring boot·后端
程序猿进阶1 小时前
深入解析 Spring WebFlux:原理与应用
java·开发语言·后端·spring·面试·架构·springboot
颜淡慕潇1 小时前
【K8S问题系列 |19 】如何解决 Pod 无法挂载 PVC问题
后端·云原生·容器·kubernetes
向前看-9 小时前
验证码机制
前端·后端
黄油饼卷咖喱鸡就味增汤拌孜然羊肉炒饭10 小时前
SpringBoot如何实现缓存预热?
java·spring boot·spring·缓存·程序员
超爱吃士力架10 小时前
邀请逻辑
java·linux·后端
AskHarries12 小时前
Spring Cloud OpenFeign快速入门demo
spring boot·后端
isolusion13 小时前
Springboot的创建方式
java·spring boot·后端