背景
整理一下上个月学习用go语言搭建服务器的笔记。
本文基于go1.14,文本编辑器采用vscode1.43.2,如果对go语言不甚了解,可以参见我的博客go语言基础学习笔记完整版
监听端口号,响应客户端请求
package main
import (
"fmt"
"net/http"
)
func handler(w http.ResponseWriter, r *http.Request) {
// 处理请求函数,函数名任意,参数固定
fmt.Fprintln(w, "Hi everyone", r.URL.Path)
// 向w输入后面的参数
}
func main() {
http.HandleFunc("/", handler)
// 注册处理函数,传入路径(根目录)和对应处理器
http.ListenAndServe(":8080", nil)
// 创建路由,传入端口号和多路复用器(nil表示默认)
}
ListenAndServe()函数的第二个参数为传入的多路复用器,多路复用器接收用户请求根据url,来判断用哪个处理器来处理请求。
编译运行
PS D:\develop\Microsoft VS Code\workspace> go build -o web.exe go_code/go_web/main
PS D:\develop\Microsoft VS Code\workspace> .\web.exe
然后打开localhost:8080,进行验证
访问根目录
访问/next
也可以通过实现ServeHttp方法的方式,来处理请求,方法如下
package main
import (
"fmt"
"net/http"
)
type myHandler struct{}
func (my_handler *myHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hi from struct server", r.URL.Path)
}
func main() {
http.Handle("/", &myHandler{})
// 注册处理函数,传入路径(根目录)和对应处理器
http.ListenAndServe(":8080", nil)
// 创建路由,传入端口号和多路复用器(nil表示默认)
}
还可以通过实例化http.Server的方式进行初始化服务器
func main() {
server := http.Server{
Addr: ":8080",
Handler: &myHandler{},
ReadHeaderTimeout: 2 * time.Second,
}
server.ListenAndServe()
}
导入time包即可
import (
"fmt"
"net/http"
"time"
)
使用自己的多路复用器,用的很少
mux := http.NewServeMux()
mux.Handle("/", &myHandler{})
http.ListenAndServe(":8080", mux)
获取客户端请求相关信息
func (my_handler *myHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hi from struct server", r.URL.Path)
fmt.Fprintln(w, "header", r.Header) //头部信息
fmt.Fprintln(w, "method:", r.Method) // 请求方法
fmt.Fprintln(w, "RawQuery:", r.URL.RawQuery) // GET方法的请求参数
}
浏览器输入 http://localhost:8080/index?user=szc&pwd=123后,浏览器输出如下
可见header是一个map[string][]string类型,可以根据映射取值方式来获取想要的内容
fmt.Println("header中的accept-encoding:", r.Header["Accept-Encoding"])
fmt.Println("header中的浏览器:", r.Header["User-Agent"])
输出如下
header中的accept-encoding: [gzip, deflate, br]
header中的浏览器: [Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36]
获取POST方法的请求体时,要先构造byte切片,再通过r.Body.Read()方法进行获取
body := make([]byte, r.ContentLength)
r.Body.Read(body)
fmt.Fprintln(w, "body:", string(body))
构造以下html文件,向localhost:8080/info路径发出请求
<html>
<head>
<meta charset="utf-8"/>
</head>
<body>
<form action="http://localhost:8080/info" method="POST">
用户名:<input type="text" name="username"/><br/>
密码:<input type="password" name="password"/><br/>
<input type="submit"/>
</form>
</body>
</html>
用浏览器打开,输入以下内容
提交后的输出如下所示
获取表单信息
err = r.ParseForm() // 必须先调用ParseForm()方法
if err != nil {
fmt.Println("Parse form error:", err)
return
}
fmt.Println(r.Form) // 表单 + url中参数,如果有同名,表单中的值靠前
fmt.Println(r.PostForm) // 仅表单
对应html如下
<html>
<head>
<meta charset="utf-8"/>
</head>
<body>
<form action="http://localhost:8080/info?k1=v1&k2=v2" method="POST">
用户名:<input type="text" name="username"/><br/>
密码:<input type="password" name="password"/><br/>
<input type="submit"/>
</form>
</body>
</html>
用浏览器打开,输入以下内容
提交后控制台输出如下
map[k1:[v1] k2:[v2] password:[pass] username:[user]]
map[password:[pass] username:[user]]
注意,如果之前调用了r.Body().Read(),那么这里只能获取空映射
如果form的enctype是multipart/form-data的话,就要调用MultipartForm()来获取映射了
也可以调用FormValue()和PostFormValue()直接获取表单中的值,这两个方法必要时会调用ParseForm()方法
fmt.Println(r.FormValue("k1"))
fmt.Println(r.PostFormValue("username"))
输出如下
v1
user
响应客户端请求
直接响应请求
主要是向w里写数据,之前的fmt.FPrintln()就可以。如果要向客户端写入json数据,就要先导入encoding/json包,然后给w设置header,最后写入json字节切片
import (
"encoding/json"
...
)
type User struct {
Id int `json:"id"`
Username string `json:"username"`
Password string `json:"password"`
}
func (my_handler *myHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
...
w.Header().Set("Content-Type", "application/json") // 设置header
user := User{
Id: 1,
Username: "szc",
Password: "pwd",
}
json, _ := json.Marshal(&user) // 解析json
w.Write(json)
}
输出结果如下
如果要在响应中重定向到其他网址,可以使用以下代码,注意不要再重定向前进行其他响应
func (my_handler *myHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Location", "https://www.baidu.com")
w.WriteHeader(302)
}
在浏览器输入localhost:8080后,就会跳转到百度首页,并且在浏览器控制台,会找到我们的响应报文
利用模板响应请求
从模板中响应请求
import (
...
"html/template"
...
)
func (my_handler *myHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
t, _ := template.ParseFiles("output.html") // 解析模板文件
t.Execute(w, "Response from template") // 执行模板引擎,传入数据
}
模板文件output.html内容如下,{{}}是模板动作,来接收响应数据,.表示所有数据
<html>
<head>
<meta charset="utf-8"/>
</head>
<body>
后台传来的数据:{{.}}
</body>
</html>
浏览器输出如下
如果要传入多个模板文件,就要调用ExecuteTemplate()方法,传入指定输出的模板文件名以及要输出的数据
t, _ := template.ParseFiles("output.html", "output_1.html") // 解析多个模板文件
t.ExecuteTemplate(w, "output_1.html", "Response from template to html2") // 参数列表:writer、输出模板文件、输出数据
模板文件output_1.html内容如下
<html>
<head>
<meta charset="utf-8"/>
</head>
<body>
后台传来的数据:{{.}} (html1)
</body>
</html>
浏览器输出如下
模板动作
以下是一些模板引擎中的动作语句
分支选择
<html>
<head>
<meta charset="utf-8"/>
</head>
<body>
后台传来的数据:
{{if .}}
select1
{{else}}
select 2
{{end}} (html)
</body>
</html>
后台输出测试如下
func (my_handler *myHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
t, _ := template.ParseFiles("output.html")
t.Execute(w, 2 > 1)
}
浏览器输出如下
遍历动作
遍历并输出某个元素的指定字段
<html>
<head>
<meta charset="utf-8"/>
</head>
<body>
后台传来的数据:
{{range .}}
{{if .}} <!-- 如果当前元素不为空 -->
<br/>当前元素:{{.Username}}
{{end}}
{{else}}
nothing
{{end}}
<br/>(html)
</body>
</html>
后台输出测试如下
func (my_handler *myHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
t, _ := template.ParseFiles("output.html")
slice_ := make([]*User, 5)
slice_ = append(slice_, &User{Id: 0, Username: "user0", Password: "pwd0"})
slice_ = append(slice_, &User{Id: 1, Username: "user1", Password: "pwd1"})
slice_ = append(slice_, &User{Id: 2, Username: "user2", Password: "pwd2"})
slice_ = append(slice_, &User{Id: 3, Username: "user3", Password: "pwd3"})
slice_ = append(slice_, &User{Id: 4, Username: "user4", Password: "pwd4"})
t.Execute(w, slice_)
}
浏览器输出如下
遍历元素时,也可以输出整个元素,如下
<html>
<head>
<meta charset="utf-8"/>
</head>
<body>
后台传来的数据:
{{range .}}
{{if .}} <!-- 如果当前元素不为空 -->
<br/>当前元素:{{.}}
{{end}}
{{else}} <!-- 没有遍历到 -->
nothing
{{end}}
<br/>(html)
</body>
</html>
浏览器输出如下
设置动作
设置动作用with就可以,但是如果with传入的字符串为空,就不会进行设置
<html>
<head>
<meta charset="UTF-8"/>
</head>
<body>
后台传来的数据:{{.}}
<div>尝试改变数据
{{with "newData"}}
新数据:{{.}}
{{end}}
</div><br/>
<div>
{{with ""}}
传入""后的结果:{{.}}
{{else}}
空字符串没有修改: {{.}}
{{end}}
</div>
<br/>(html)
</body>
</html>
后台代码如下
func handler(w http.ResponseWriter, r *http.Request) {
t, _ := template.ParseFiles("output.html")
t.Execute(w, "后台数据")
}
浏览器的输出如下
包含动作
使用template动作把另一个模板文件里的内容拷贝进来。
output.html模板内容如下
<html>
<head>
<meta charset="UTF-8"/>
</head>
<body>
后台传来的数据:{{.}}
<div>output1.html里的数据:<br/>{{template "output_1.html"}}</div><bt/> <!-- 仅输出模板 -->
<div>{{template "output_1.html" .}}</div> <!-- 输出模板和数据 -->
<br/>(html)
</body>
</html>
output_1.html模板内容如下
<html>
<head>
<meta charset="utf-8"/>
</head>
<body>
后台传来的数据:{{.}} (html1)
</body>
</html>
后台处理逻辑如下
func handler(w http.ResponseWriter, r *http.Request) {
t, _ := template.ParseFiles("output.html", "output_1.html")
t.Execute(w, "后台数据")
}
浏览器输出如下
可见output.html中的两个模板动作里,上面的仅仅拷贝了模板,下面连同数据也引入了
定义动作
可以使用define去定义一个新动作
{{define "model"}}
<html>
<head>
<meta charset="utf-8"/>
</head>
<body>
后台传来的数据:{{.}} (html1)
<br/>
{{template "content"}} <!-- 引用自定义动作 -->
</body>
</html>
{{end}}
{{define "content"}}
<a href="https://www.baidu.com">百度</a>
{{end}}
后台使用ExecuteTemplate()方法指定加载的动作和参数
func handler(w http.ResponseWriter, r *http.Request) {
t, _ := template.ParseFiles("output_1.html")
t.ExecuteTemplate(w, "model", "")
}
浏览器输出如下
我们还可以根据不同的情况加载不同的模板文件,从而加载不同的动作,哪怕重名。
现有output.html和output_1.html,唯一不同的是前者把百度改成了新浪,故而只贴出后台逻辑
func handler(w http.ResponseWriter, r *http.Request) {
var t *template.Template
if strings.Contains(r.URL.Path, "_1") {
t, _ = template.ParseFiles("output_1.html")
} else {
t, _ = template.ParseFiles("output.html")
}
t.ExecuteTemplate(w, "model", "")
}
浏览器输入 http://localhost:9999/_1,浏览器输出如下
而http://localhost:9999/的输出如下
块动作
当后台执行模板时,模板没有对应的动作,就可以使用block定义默认的动作
output.html内容如下,当没有给"content"指定动作时,使用block进行默认的输出
{{define "model"}}
<html>
<head>
<meta charset="utf-8"/>
</head>
<body>
后台传来的数据:{{.}} (html1)
<br/>
{{block "content" .}}
block
{{end}}
</body>
</html>
{{end}}
而"content"动作定义在output_1.html中
{{define "model"}}
<html>
<head>
<meta charset="utf-8"/>
</head>
<body>
后台传来的数据:{{.}} (html1)
<br/>
{{template "content"}}
</body>
</html>
{{end}}
{{define "content"}}
<a href="https://www.baidu.com">百度</a>
{{end}}
后台处理逻辑如下
func handler(w http.ResponseWriter, r *http.Request) {
var t *template.Template
if strings.Contains(r.URL.Path, "_1") {
t, _ = template.ParseFiles("output.html", "output_1.html")
} else {
t, _ = template.ParseFiles("output.html")
}
t.ExecuteTemplate(w, "model", "")
}
当输入url包含_1时,加载两个模板文件,这时"content"的定义在output_1.html中被找到,所以会加载出相应内容
当输入url不含_1是,只加载一个模板文件,没有"content"的定义,所以加载block中默认的内容
处理静态资源
go语言不能直接访问css、js等静态资源,需要我们为这些资源的请求注册处理函数
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir(utils.Static_res))))
// 处理静态资源,入参:请求路径,处理器。
http.Handle("/pages/", http.StripPrefix("/pages/", http.FileServer(http.Dir(utils.Pages))))
Handle()方法里的/static/表示要处理的请求相对路径,也就是静态css和js所在的目录,后面的http.FileServer()返回一个处理器,这里用来访问静态资源所在目录。
http.FileServer的入参就是目录的路径,这里使用自定义常量来表示
var (
Project_src = "D:/develop/Go/workspace/src/go_code/book_store/src/"
Index_html = Project_src + "views/index.html"
Pages = Project_src + "views/pages/"
Static_res = Project_src + "views/static/"
)
cookie
设置cookie时,可以使用Set或Add方法
func setCookie(w http.ResponseWriter, r *http.Request) {
cookie := http.Cookie{
Name: "user2",
Value: "admin2",
}
http.SetCookie(w, &cookie)
}
或者
func setCookie(w http.ResponseWriter, r *http.Request) {
cookie := http.Cookie{
Name: "user2",
Value: "admin2",
}
w.Header().Set("Set-Cookie", cookie.String())
w.Header().Add("Set-Cookie", cookie.String())
}
所有Set方法都会覆盖老的,Add方法则不会
除了键值对,也可以为Cookie指定有效期限,单位是秒
cookie2 := http.Cookie{
Name: "user",
Value: "admin2",
MaxAge: 60,
}
获取cookie时,可以从头部信息获取全部的cookie,也可以获取指定的cookie
获取header中的所有cookie,返回的是一个string切片
cookies := r.Header["Cookie"]
获取指定cookie,返回的是一个Cookie指针
cookie_, _ := r.Cookie("user")
cookie_.String()
结语
go语言搭建服务器还是很简单的,不用安装任何其他包,就可以进行端口的监听、请求的响应等操作。