使用Go语言编写一个web应用 初始知识 go语言的学习 其实很简单,就不详细写了。
可以去类似于去菜鸟教程这个网站,去初步的了解编程语言的写法。
或者是类似于哔哔哔哩这样的视频网站,直接照这视频一步一步的去学习它的写法,不过这种方法比较消耗时间。但是我觉得如果你有一定的编程基础,学这个go真的很简单,没有什么复杂地方。你仅需要注意的是,语法有些不同,你可能要花一段时间去适应它的写法。
工具 虽然网上很多教程都是使用VScode去写的,但是我觉得不是很好,可能自己对于这种轻量级的工具不是很适应。
我使用的是IntelliJ IDEA ,然后安装了go的插件之后去写的,我觉得这样写起来比较轻松,这个要看个人了。
第一个demo 这个编程语言去做一个web服务的话,有点像cpp一样复古的写法,但是相比于cpp这样语言来肯定要简单得多。不过,如果你有一定的基础的话,会比较轻松吧。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package mainimport "net/http" func main () { http.HandleFunc("/" , func (w http.ResponseWriter, r *http.Request) { w.Write([]byte ("hello world" )) }) http.ListenAndServe("localhost:8888" , nil ) }
启动后就可以看到结果了。
正式编写 处理请求 DefaultServeMux 首先要编写一个go程序,就如上面的例子一样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 package mainimport "net/http" type myHandler struct {} func (m *myHandler) ServeHTTP (w http.ResponseWriter, r *http.Request) { w.Write([]byte ("hello, my lover" )) } func main () { mh := myHandler{} server := http.Server{ Addr: "localhost:8888" , Handler: &mh, } server.ListenAndServe() }
这种情况下,所有的网址都会输出同样的信息,因为每个请求都是使用同一个Handler。而我们应该使用:DefaultServeMux 去进行处理不同的Handler,而我们就需要对每个Handler进行注册,这样才能进行出结果。
所以我们应该这么改:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 package mainimport "net/http" type helloHandler struct {} func (m *helloHandler) ServeHTTP (w http.ResponseWriter, r *http.Request) { w.Write([]byte ("hello, my lover" )) } type aboutHandler struct {} func (m *aboutHandler) ServeHTTP (w http.ResponseWriter, r *http.Request) { w.Write([]byte ("About, my message" )) } func main () { mh := helloHandler{} a := aboutHandler{} server := http.Server{ Addr: "localhost:8888" , Handler: nil , } http.Handle("/hello" , &mh) http.Handle("/about" , &a) server.ListenAndServe() }
就相当于我们把我们需要自定义的Handler,根据自己的需要去更改,最后注册到DefaultServeMux当中,完成对不同请求产生不同的响应。
HandleFunc 但前面的处理完全可以简写为一个方式,增加可读性,于是乎,我们就要用到这个函数HandleFunc:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 package mainimport "net/http" type helloHandler struct {} func (m *helloHandler) ServeHTTP (w http.ResponseWriter, r *http.Request) { w.Write([]byte ("hello, my lover" )) } type aboutHandler struct {} func (m *aboutHandler) ServeHTTP (w http.ResponseWriter, r *http.Request) { w.Write([]byte ("About, my message" )) } func welcome (writer http.ResponseWriter, request *http.Request) { writer.Write([]byte ("welcome,my friends" )) } func main () { mh := helloHandler{} a := aboutHandler{} server := http.Server{ Addr: "localhost:8888" , Handler: nil , } http.Handle("/hello" , &mh) http.Handle("/about" , &a) http.HandleFunc("/home" , func (writer http.ResponseWriter, request *http.Request) { writer.Write([]byte ("back home,my son" )) }) http.HandleFunc("/welcome" , welcome) server.ListenAndServe() }
而这个HandleFunc本质上还是调用Handler
1 2 3 func HandleFunc (pattern string , handler func (ResponseWriter, *Request) ) { DefaultServeMux.HandleFunc(pattern, handler) }
当我们把上述代码中的:
1 2 3 http.HandleFunc("/welcome" , welcome) http.HandleFunc("/welcome" , http.HandlerFunc(welcome))
其结果也是一样的。因为这其实是接口型函数,表示函数的类型:
1 2 3 4 5 6 type HandlerFunc func (ResponseWriter, *Request) func (f HandlerFunc) ServeHTTP (w ResponseWriter, r *Request) { f(w, r) }
这就是go语言精妙的地方了。你可以面对多种不一样的处理方式,但是只用放在同一个接口当中,这就是go语言使用组成而不是继承的原因。
总结,如何注册DefaultServeHTTP
方法一、使用http.Handle(第二个参数是Handler)
方法二、使用http.HandleFunc(第二个参数是Handler函数)
方法三、http.HandlerFunc可以进行类型转换,将Handler函数转化成Handler(结构体)来使用
内置Handler
代码演示:FileServer 1 http.ListenAndServe(":8888" , http.FileServer(http.Dir("root" )))
可以通过这个方法指定root这个根路径,这样打开文件的时候直接去root,直接找到需要的url路径,加载文件
请求类型 最基本的肯定是:
而 net/http 包提供了用于表示 HTTP 消息的结构,其中Reqeust(是个 struct),代表了客户端发送的 HTTP 请求消息
URL类型 URL Query RawQuery 会提供实际查询的字符串。
例如: http://www.example.com/post?id=123&thread_id=456
它的 RawQuery 的值就是 id=123&thread_id=456
还有一个简便方法可以得到 Key-Value 对:通过 Request 的 Form 字段(以后再说)
URL Fragment 如果从浏览器发出的请求,那么你无法提取出 Fragment 字段的值
浏览器在发送请求时会把 fragment 部分去掉
但不是所有的请求都是从浏览器发出的(例如从 HTTP 客户端包)。
请求和响应(Request、Response)的 headers 是通过 Header 类型来描述的,它是一个 map,用来表述 HTTP Header 里的 Key-Value 对。
Header map 的 key 是 string 类型,value 是 []string
设置 key 的时候会创建一个空的 []string 作为 value,value 里面第一个元素就是新 header 的值;
为指定的 key 添加一个新的 header 值,执行 append 操作即可
Request Body 请求和响应的 bodies 都是使用 Body 字段来表示的
Body 是一个 io.ReadCloser 接口
一个 Reader 接口
一个 Closer 接口
Reader 接口定义了一个 Open 方法:
参数:[]byte
返回:byte 的数量、可选的错误
Closer 接口定义了一个 Close 方法:
没有参数,返回可选的错误
代码演示:Fragment 1 2 3 4 5 6 7 8 9 func main () { server := http.Server{ Addr: "localhost:8888" , } http.HandleFunc("/url" , func (writer http.ResponseWriter, request *http.Request) { fmt.Fprintln(writer, request.URL.Fragment) }) server.ListenAndServe() }
可以通过这种方式,看看自己请求有没有Fragment
1 2 3 4 5 6 7 8 9 10 11 func main () { server := http.Server{ Addr: "localhost:8888" , } http.HandleFunc("/header" , func (writer http.ResponseWriter, request *http.Request) { fmt.Fprintln(writer, request.Header) fmt.Fprintln(writer, request.Header["Accept-Encoding" ]) fmt.Fprintln(writer, request.Header.Get("Accept-Encoding" )) }) server.ListenAndServe() }
然后使用Postman测试:
1 http://localhost:8888/header
输出结果为:
1 2 3 map[Accept:[*/*] Accept-Encoding:[gzip, deflate, br] Connection:[keep-alive] Postman-Token:[9ddda678-d6f8-42d2-aea5-67816a86c3d7] User-Agent:[PostmanRuntime/7.28.3]] [gzip, deflate, br] gzip, deflate, br
1 2 3 4 5 6 7 8 9 10 11 12 func main () { server := http.Server{ Addr: "localhost:8888" , } http.HandleFunc("/post" , func (writer http.ResponseWriter, request *http.Request) { length := request.ContentLength body := make ([]byte , length) request.Body.Read(body) fmt.Fprintln(writer, string (body)) }) server.ListenAndServe() }
用这个方式去展现出body的内容
URL Query 例如:http://www.example.com/post?id=123&thread_id=456
r.URL.RawQuery 会提供实际查询的原始字符串,因为 RawQuery 的值就是 id=123&thread_id=456
r.URL.Query(),会提供查询字符串对应的 map string string
例子 1 2 3 4 5 6 7 8 9 10 11 12 13 14 func main () { http.HandleFunc("/home" , func (writer http.ResponseWriter, request *http.Request) { url := request.URL query := url.Query() id := query["id" ] log.Println(id) name := query.Get("name" ) log.Println(name) }) http.ListenAndServe("localhost:8888" , nil ) }
然后测试:
1 http://localhost:8888/home?id=123&name=panda&id=456&name=fox
控制台显示结果为:
这表明 query[“id”] 是获取所有的值,而query.Get(“name”) 只会获取第一个值
enctype 接下来看看,如何处理表单类的数据。
HTML 表单里面的数据会以 name-value 对的形式,通过 POST 请求发送出去,它的数据内容会放在 POST 请求的 Body 里面。
通过 POST 发送的 name-value 数据对的格式可以通过表单的 Content Type 来指定,也就是 enctype 属性。
默认值是:application/x-www-form-urlencoded 浏览器被要求至少要支持: application/x-www-form-urlencoded 、multipart/form-data。HTML 5 的话,还需要支持 text/plain
如果 enctype 是 application/x-www-form-urlencoded,那么浏览器会将表单数据编码到查询字符串里面。例如: first_name=sau%20sheong&last_name=chang
如果 enctype 是 multipart/form-data,那么:每一个 name-value 对都会被转换为一个MIME消息部分,每一个部分都有自己的 Content Type 和 Content Disposition
那么,改如何选择enctype呢?
简单文本:表单 URL 编码
大量数据,例如上传文件:multipart-MIME,甚至可以把二进制数据通过选择 Base64 编码,来当作文本进行发送
Request 上的函数允许我们从 URL 或/和 Body 中提取数据,通过这些字段:
Form
PostForm
MultipartForm
Form 里面的数据是 key-value 对。通常的做法是:先调用 ParseForm 或 ParseMultipartForm 来解析 Request,然后相应的访问 Form、PostForm 或 MultipartForm 字段。
1 2 3 4 5 6 7 8 func main () { http.HandleFunc("/process" , func (writer http.ResponseWriter, request *http.Request) { request.ParseForm() fmt.Fprintln(writer, request.Form) }) http.ListenAndServe("localhost:8888" , nil ) }
这样会打印出一个map,包含传入的所有的值。包括url的值和表单的值。
如果只想要表单的 key-value 对,不要 URL 的,可以使用 PostForm 字段。 PostForm 只支持 application/x-www-form-urlencoded
1 2 3 4 5 6 7 8 9 10 11 12 func main () { server := http.Server{ Addr: "localhost:8888" , Handler: nil , } http.HandleFunc("/process" , func (writer http.ResponseWriter, request *http.Request) { request.ParseForm() fmt.Fprintln(writer, request.PostForm) }) server.ListenAndServe() }
结果就只有一个字段
而想要得到 multipart key-value 对,必须使用 MultipartForm 字段。
想要使用 MultipartForm 这个字段的话,首先需要调用ParseMultipartForm 这个方法,该方法会在必要时调用 ParseForm 方法 ,参数是需要读取数据的长度 MultipartForm 只包含表单的 key-value 对,返回类型是一个 struct 而不是 map。这个 struct 里有两个 map:1、key 是 string,value 是 []string 。2、空的(key 是 string,value 是文件)
1 2 3 4 5 6 7 8 9 10 11 12 func main () { server := http.Server{ Addr: "localhost:8888" , Handler: nil , } http.HandleFunc("/process" , func (writer http.ResponseWriter, request *http.Request) { request.ParseMultipartForm(1024 ) fmt.Fprintln(writer, request.PostForm) }) server.ListenAndServe() }
FormValue 和 PostFormValue 方法
FormValue 方法会返回 Form 字段中指定 key 对应的第一个 value,无需调用 ParseForm 或 ParseMultipartForm
PostFormValue 方法也一样,但只能读取 PostForm
FormValue 和 PostFormValue 都会调用 ParseMultipartForm 方法
但如果表单的 enctype 设为 multipart/form-data,那么即使你调用ParseMultipartForm 方法,也无法通过 FormValue 获得想要的值。
1 2 3 4 5 6 7 8 9 10 11 12 func main () { server := http.Server{ Addr: "localhost:8888" , Handler: nil , } http.HandleFunc("/process" , func (writer http.ResponseWriter, request *http.Request) { request.ParseMultipartForm(1024 ) fmt.Fprintln(writer, request.FormValue("name" )) fmt.Fprintln(writer, request.PostFormValue("name" )) }) server.ListenAndServe() }
上传文件 multipart/form-data 最常见的应用场景就是上传文件(例子):
首先调用 ParseMultipartForm 方法
从 File 字段获得 FileHeader,调用其 Open 方法来获得文件
可以使用 ioutil.ReadAll 函数把文件内容读取到 byte 切片里
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 func main () { server := http.Server{ Addr: "localhost:8888" , Handler: nil , } http.HandleFunc("/process" , process) server.ListenAndServe() } func process (writer http.ResponseWriter, request *http.Request) { request.ParseMultipartForm(1024 ) fileHeader := request.MultipartForm.File["upload" ][0 ] file, err := fileHeader.Open() if err == nil { data, err := ioutil.ReadAll(file) if err == nil { fmt.Fprintln(writer, string (data)) } } }
字如其意,非常简单。但是这部分代码仍然有改进的空间,比如使用FormFile
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func process (writer http.ResponseWriter, request *http.Request) { file, _, err := request.FormFile("uploader" ) if err == nil { data, err := ioutil.ReadAll(file) if err == nil { fmt.Fprintln(writer, string (data)) } } }
MultipartReader() 方法签名:func (r Request) MultipartReader() ( multipart.Reader, error) 如果是 multipart/form-data 或 multipart 混合的 POST 请求:1、MultipartReader 返回一个 MIME multipart reader 2、否则返回 nil 和一个错误 可以使用该函数代替 ParseMultipartForm 来把请求的 body 作为 stream 进行处理。1、不是把表单作为一个对象来处理的,不是一次性获得整个 map。2、逐个检查来自表单的值,然后每次处理一个
响应 ResponseWriter 从服务器向客户端返回响应需要使用 ResponseWriter。ResponseWriter 是一个接口,handler 用它来返回响应。而真正支撑 ResponseWriter 的幕后 struct 是非导出的 http.response。
但是,为什么request是指针,而writer不用是呢?
1 (writer http.ResponseWriter, request *http.Request)
其实,这两个都是按引用进行传递的,ResponseWriter是具有Header、Write、WriteHeader三种方法的接口,response指针实现了以上三种方法,故response是一种特殊的ResponseWriter。
并且,ResponseWriter还能使用writer.Write([]byte(str))方法,把字符串写入到body里面。
WriteHeader 方法接收一个整数类型(HTTP 状态码)作为参数,并把它作为 HTTP 响应的状态码返回 如果该方法没有显式调用,那么在第一次调用 Write 方法前,会隐式的调用 WriteHeader(http.StatusOK),所以 WriteHeader 主要用来发送错误类的 HTTP 状态码 调用完 WriteHeader 方法之后,仍然可以写入到 ResponseWriter,但无法再修改 header 了
Header 方法返回 headers 的 map,可以进行修改 修改后的 headers 将会体现在返回给客户端的 HTTP 响应里
内置的 Response NotFound 函数,包装一个 404 状态码和一个额外的信息 ServeFile 函数,从文件系统提供文件,返回给请求者 ServeContent 函数,它可以把实现了 io.ReadSeeker 接口的任何东西里面的内容返回给请求者,并且,还可以处理 Range 请求(范围请求),如果只请求了资源的一部分内容,那么 ServeContent 就可以如此响应。而 ServeFile 或 io.Copy 则不行。 Redirect 函数,告诉客户端重定向到另一个 URL
模板 Web 模板就是预先设计好的 HTML 页面,它可以被模板引擎反复的使用,来产生 HTML 页面 Go 的标准库提供了 text/template,html/template 两个模板库,大多数 Go 的 Web 框架都使用这些库作为 默认的模板引擎
Go 的模板和模板引擎 go主要使用的是 text/template,HTML 相关的部分使用了 html/template,是个混合体。go模板可以完全无逻辑,但又具有足够的嵌入特性。和大多数模板引擎一样,Go Web 的模板位于无逻辑和嵌入逻辑之间的某个地方
工作原理 在 Web 应用中,通产是由 handler 来触发模板引擎。handler 调用模板引擎,并将使用的模板传递给引擎,通常是一组模板文件和动态数据。 模板引擎生成 HTML,并将其写入到 ResponseWriter,ResponseWriter 再将它加入到 HTTP 响应中,返回给客户端。
模板的例子 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <!DOCTYPE html > <html > <head > <meta http-equiv ="Content-Type" content ="text/html; charset=utf-8" > <title > Go Web Programming</title > </head > <body > {{ . }} </body > </html > 模板必须是可读的文本格式,扩展名任意。对于 Web 应用通常就是 HTML,里面会内嵌一些命令(叫做 action) text/template 是通用模板引擎,html/template 是 HTML 模板引擎 action 位于双层花括号之间:{{ . }}。这里的 . 就是一个 action。它可以命令模板引擎将其替换成一个值。
是不是有点像jsp
使用模板引擎
解析模板源(可以是字符串或模板文件),从而创建一个解析好的 模板的 struct
执行解析好的模板,并传入 ResponseWriter 和 数据。这会触发模板引擎组合解析好的模板和数据,来产生最终的 HTML,并将它传递给 ResponseWriter
例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 func main () { server := http.Server{ Addr: "localhost:8888" , Handler: nil , } http.HandleFunc("/process" , process) server.ListenAndServe() } func process (writer http.ResponseWriter, request *http.Request) { t, _ := template.ParseFiles("mytest.html" ) t.Execute(writer, "hello,my friends" ) }
但是鉴于现今几乎都是前后端分离的项目了,模板引擎已经几乎要绝迹了,随便学学吧。
解析模板 ParseFiles 解析模板文件,并创建一个解析好的模板 struct,后续可以被执行 ParseFiles 函数是 Template struct 上 ParseFiles 方法的简便调用 调用 ParseFiles 后,会创建一个新的模板,模板的名字是文件名 ParseFiles 的参数数量可变,但只返回一个模板,当解析多个文件时,第一个文件作为返回的模板(名、内容),其余的作为 map,供后续执行使用
ParseGlob 使用模式匹配来解析特定的文件
Parse 可以解析字符串模板,其它方式最终都会调用 Parse
Lookup 方法 通过模板名来寻找模板,如果没找到就返回 nil
Must 函数 可以包裹一个函数,返回到一个模板的指针 和 一个错误。如果错误不为 nil,那么就 panic
模板的Action 1 Action 就是 Go 模板中嵌入的命令,位于两组花括号之间 {{ xxx }},就是一个 Action,而且是最重要的一个。它代表了传入模板的数据
Action 主要可以分为五类:条件类,迭代/遍历类,设置类,包含类,定义类
条件Action 1 2 3 4 5 6 7 8 9 {{ if arg }} some content {{ end }} {{ if arg }} some content {{ else }} other content {{ end }}
迭代/遍历 Action 1 2 3 {{ range array }} Dot is set to the element {{ . }} {{ end }}
这类 Action 用来遍历数组、slice、map 或 channel 等数据结构,“.”用来表示每次迭代循环中的元素
设置Action 1 2 3 {{ with arg }} Dot is set to arg {{ end }}
它允许在指定范围内,让“.”来表示其它指定的值(arg)
包含 Action 1 2 3 4 {{ template "name" }} 它允许你在模板中包含其它的模板 {{ template "name" arg }} 给被包含模板传递参数
函数与管道 参数(argument) 参数就是模板里面用到的值。可以是 bool、整数、string … ,也可以是 struct、struct 的字段、数组的 key 等等 参数可以是变量、方法(返回单个值或返回一个值和一个错误)或函数 参数可以是一个点“.”,也就是传入模板引擎的那个值。
1 2 3 4 {{ if arg }} some content {{ end }} 这里的 arg 就是参数
在 Action 中设置变量 可以在 action 中设置变量,变量以 $ 开头:$variable := value 一个迭代 action 的例子:
1 2 3 {{ range $key, $value := . }} The key is {{ $key }} and the value is {{ $value }} {{ end }}
管道(pipeline) 管道是按顺序连接到一起的参数、函数和方法。和 Unix 的管道类似:
1 2 例如:{{ p1 | p2 | p3 }} ,p1、p2、p3 要么是参数,要么是函数 管道允许我们把参数的输出发给下一个参数,下一个参数由管道(|)分隔开。
函数 参数可以是一个函数,Go 模板引擎提供了一些基本的内置函数,功能比较有限。例如 fmt.Sprint 的各类变体等 开发者可以自定义函数,可以接收任意数量的输入参数 返回:一个值 或 一个值+一个错误
内置函数 1 2 3 4 5 6 7 8 define、template、block html、js、urlquery。对字符串进行转义,防止安全问题 如果是 Web 模板,那么不会需要经常使用这些函数。 index print/printf/println len with 这些都是内置的函数
自定义函数 1 2 3 4 5 6 7 8 9 10 11 12 template.Funcs(funcMap FuncMap) *Template type FuncMap map [string ]interface {}
常见用法:template.New(“”).Funcs(funcMap).Parse(…),调用顺序非常重要。可以在管道中使用,也可以作为正常函数使用。
模板组合 Layout 模板 Layout 模板就是网页中固定的部分,它可以被多个网页重复使用:
1 Include(包含)action 的形式:{{ template "name" . },
以这种方式做 layout 模板是不可行的。而正确的做法是在模板文件里面使用 define action 再定义一个模板。这种形式特别像thymeleaf,就是单纯的将HTML页面强行模块化,使其具有公共部分的属性,总体而言还是比较简单的。而也可以在多个模板文件里,定义同名的模板。
使用 block action 定义默认模板 1 2 3 4 5 6 7 {{ block arg }} Dot is set to arg {{ end }} block action 可以定义模板,并同时就使用它 template:模板必须可用 block:模板可以不存在
逻辑运算符 1 2 3 4 5 6 eq/ne lt/gt le/ge and or not
字如其意。
数据库 接下来看看如何使用golang去连接数据库,并进行基本的CRUD。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 package mainimport ( "database/sql" _ "github.com/go-sql-driver/mysql" "fmt" ) func main () { db,_:=sql.Open("mysql" ,"root:root@(127.0.0.1:3306)/testdatabases" ) defer db.Close() err:=db.Ping() if err!=nil { fmt.Println("数据库连接失败" ) return } rows,_:=db.Query("select * from stu" ) var id,name string for rows.Next(){ rows.Scan(&id,&name) fmt.Println(id,"--" ,name) } }
路由 前面很多时候,都是使用:
1 2 3 4 http.HandleFunc("/home" , func (writer http.ResponseWriter, request *http.Request) { writer.Write([]byte ("back home,my son" )) }) http.HandleFunc("/welcome" , http.HandlerFunc(welcome))
HandleFunc这种方式去进行路由控制,但是其实还有更为方便的写法。
Controller main():设置类工作 controller:1、静态资源 。2、把不同的请求送到不同的 controller 进行处理
我们实际上应该这么设置才是最为正确的写法。
例子,先制作出两个页面:
1 2 3 4 5 6 7 8 9 package controllerimport "net/http" func registerHomeRoutes () { http.HandleFunc("/home" , func (writer http.ResponseWriter, request *http.Request) { writer.Write([]byte ("back home,my son" )) }) }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package controllerimport "net/http" func registerWelcomeRoutes () { http.HandleFunc("/welcome" , http.HandlerFunc(welcome)) } func welcome (writer http.ResponseWriter, request *http.Request) { _, err := writer.Write([]byte ("welcome,my friends" )) if err != nil { return } }
再将两个函数进行注册:
1 2 3 4 5 6 7 8 package controllerfunc RegisterRoutes () { registerHomeRoutes() registerWelcomeRoutes() }
最后在主函数中调用:
1 2 3 4 5 6 7 8 9 10 11 func main() { server := http.Server{ Addr: "localhost:8888", Handler: nil, } controller.RegisterRoutes() err := server.ListenAndServe() if err != nil { return } }
这实际上是一种代码层面的解耦合,将我们的函数进一步模块化。
路由的参数 静态路由:一个路径对应一个页面:
/home 或 /about
带参数的路由:根据路由参数,创建出一族不同的页面:
/companies/123 或 /companies/Google
这里使用HandlerFunc就可以了,将字符串传入URL,效果是相同的,与Spring+thymeleaf可以说是一模一样。
JSON 现在讲究前后端分离,大多数数据都是使用JSON去传递数据,接下来看看如何在go中使用JSON对象。
类型映射 Go bool:JSON boolean Go float64:JSON 数值 Go string:JSON strings Go nil:JSON null.
1 2 3 4 5 type Company struct { ID int `json:"id"` Name string `json:"name"` Country string `json:"country"` }
对于未知结构的 JSON map[string]interface{} 可以存储任意 JSON 对象 []interface{} 可以存储任意的 JSON 数组
读取 JSON 需要一个解码器:dec := json.NewDecoder(r.Body) ,参数需实现 Reader 接口 解码器上进行解码:dec.Decode(&query)
写入 JSON 需要一个编码器:enc := json.NewEncoder(w),参数需实现 Writer 接口 编码是:enc.Encode(results)
例子 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 type Company struct { ID int `json:"id"` Name string `json:"name"` Country string `json:"country"` } func main () { http.HandleFunc("/companies" , func (writer http.ResponseWriter, request *http.Request) { switch request.Method { case http.MethodPost: dec := json.NewDecoder(request.Body) company := Company{} err := dec.Decode(&company) if err != nil { log.Println(err.Error()) writer.WriteHeader(http.StatusInternalServerError) return } enc := json.NewEncoder(writer) err = enc.Encode(company) if err != nil { log.Println(err.Error()) writer.WriteHeader(http.StatusInternalServerError) return } default : writer.WriteHeader(http.StatusMethodNotAllowed) } }) server := http.Server{ Addr: "localhost:8888" , Handler: nil , } err := server.ListenAndServe() if err != nil { return } }
然后使用Postman进行测试:
1 2 3 4 5 6 7 http://localhost:8888/companies { "id": 123, "name": "google", "country": "USA" }
编码的类型:Marshal 和 Unmarshal Marshal(编码): 把 go struct 转化为 json 格式。MarshalIndent,带缩进 Unmarshal(解码): 把 json 转化为 go struct
测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 type Company struct { ID int `json:"id"` Name string `json:"name"` Country string `json:"country"` } func main () { jsonStr := `{ "id": 123, "name": "google", "country": "USA" }` c := Company{} _ = json.Unmarshal([]byte (jsonStr), &c) fmt.Println(c) bytes, _ := json.Marshal(c) fmt.Println(string (bytes)) bytes2, _ := json.MarshalIndent(c, "," , " " ) fmt.Println(string (bytes2)) } {123 google USA} {"id" :123 ,"name" :"google" ,"country" :"USA" } { , "id" : 123 , , "name" : "google" , , "country" : "USA" ,}
区别:
针对 string 或 bytes:
Marshal => String
Unmarshal <= String
针对 stream:
Encode => Stream,把数据写入到 io.Writer
Decode <= Stream,从 io.Reader 读取数据
中间件 中间件大家都懂的,像消息队列,缓存,也是中间件。当然也可以自己捏一个中间件的处理逻辑。
创建中间件 1 2 3 4 5 6 func ListenAndServe (addr string , handler Handler) error type Handler interface { ServeHTTP(ResponseWriter, *Request) }
像这个Handler接口,并可以支持中间件的处理。
首先创建一下:
1 2 3 4 5 6 7 8 type MyMiddleware struct { Next http.Handler } func (m MyMiddleware) ServeHTTP (w http.ResponseWriter, r *http.Request) {m.Next.ServeHTTP(w, r) }
途中可以定义中间件需要做的事情。
中间件的用途
Logging,日志
安全,身份认证
请求超时,减少资源消耗
响应压缩,提升效率
例子 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package middlewareimport "net/http" type AuthMiddleware struct { Next http.Handler } func (am *AuthMiddleware) ServeHTTP (writer http.ResponseWriter, request *http.Request) { if am.Next == nil { am.Next = http.DefaultServeMux } auth := request.Header.Get("Authorization" ) if auth != "" { am.Next.ServeHTTP(writer, request) } else { writer.WriteHeader(http.StatusUnauthorized) } }
这个是自定义的中间件类型,接着是主函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 type Company struct { ID int `json:"id"` Name string `json:"name"` Country string `json:"country"` } func main () { http.HandleFunc("/companies" , func (writer http.ResponseWriter, request *http.Request) { c := Company{ ID: 111 , Name: "Microsoft" , Country: "USA" , } enc := json.NewEncoder(writer) enc.Encode(c) }) http.ListenAndServe("localhost:8888" , new (middleware.AuthMiddleware)) }
将该中间件注册过后,便可以执行逻辑。
使用Postman进行测试,一个带Authorization,而另一个不带,测试略。
请求上下文 从请求的上下文中获取信息,以用于处理
Request Context 1 2 3 4 func (*Request) Context () context .Context func (*Request) WithContext (ctx context.Context) context .Context
有这么两种方式去处理上下文
看看里面实际上是什么:
context.Context 1 2 3 4 5 6 7 type Context interface { Deadline() (deadline time.Time, ok bool ) Done() <-chan struct {} Err() error Value(key interface {}) interface {} }
这就是上下文接口里面的方法
context这个包,也有一些方法,可以返回新的context
Context API
WithCancel(),它有一个 CancelFunc
WithDeadline(),带有一个时间戳(time.Time)
WithTimeout(),带有一个具体的时间段(time.Duration)
WithValue(),在里面可以添加一些值
一个超时的例子 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 package middlewareimport ( "context" "net/http" "time" ) type TimeoutMiddleware struct { Next http.Handler } func (tm TimeoutMiddleware) ServeHTTP (writer http.ResponseWriter, request *http.Request) { if tm.Next == nil { tm.Next = http.DefaultServeMux } ctx := request.Context() ctx, _ = context.WithTimeout(ctx, 3 *time.Second) request.WithContext(ctx) ch := make (chan struct {}) go func () { tm.Next.ServeHTTP(writer, request) ch <- struct {}{} }() select { case <-ch: return case <-ctx.Done(): writer.WriteHeader(http.StatusRequestTimeout) } ctx.Done() }
先新造好这个中间件,然后注册到main函数里面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 package mainimport ( "encoding/json" "go_web/middleware" "net/http" ) type Company struct { ID int `json:"id"` Name string `json:"name"` Country string `json:"country"` } func main () { http.HandleFunc("/companies" , func (writer http.ResponseWriter, request *http.Request) { c := Company{ ID: 111 , Name: "Microsoft" , Country: "USA" , } enc := json.NewEncoder(writer) enc.Encode(c) }) http.ListenAndServe("localhost:8888" , &middleware.TimeoutMiddleware{Next: new (middleware.AuthMiddleware)}) }
然后像之前一样去测试,即可。
HTTPS 这是HTTP的流程
1 2 3 POST /login HTTP/1.1… username=admin&password=123456
都是明文传输的,所以我们有时候就需要HTTPS
HTTP Listener
http.ListenAndServe 函数
http.ListenAndServeTLS 函数
可以使用ListenAndServeTLS去使其页面变为HTTPS类型,看看接口:
1 2 3 4 func ListenAndServeTLS (addr, certFile, keyFile string , handler Handler) error { server := &Server{Addr: addr, Handler: handler} return server.ListenAndServeTLS(certFile, keyFile) }
发现需要安全证书,而我们的go就可以自己生成安全证书:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 go run D:\Golang\sdk\go1.15 .1 \src\crypto\tls\generate_cert.go -h //看看帮助 -ca whether this cert should be its own Certificate Authority -duration duration Duration that certificate is valid for (default 8760 h0m0s) -ecdsa-curve string ECDSA curve to use to generate a key. Valid values are P224, P256 (recommended), P384, P521 -ed25519 Generate an Ed25519 key -host string Comma-separated hostnames and IPs to generate a certificate for -rsa-bits int Size of RSA key to generate. Ignored if --ecdsa-curve is set (default 2048 ) -start -date string Creation date formatted as Jan 1 15 :04 :05 2011
于是乎,便生成证书:
1 2 3 go run D:\Golang\sdk\go1.15 .1 \src\crypto\tls\generate_cert.go -host localhost //wrote cert.pem //wrote key.pem
之后再测试,发现只有使用HTTPS的前缀,才能打开网页。
1 2 3 4 5 6 7 8 9 10 11 12 package mainimport ( "go_web/controller" "net/http" ) func main () { controller.RegisterRoutes() http.ListenAndServeTLS("localhost:8888" , "cert.pem" , "key.pem" , nil ) }
然后,我们使用的HTTP1.1协议就会自动升级到HTTP2.0协议
HTTP的协议 HTTP/1.1 在HTTP/1.1的情况下:
请求 header+body
响应 header+body
这样请求和响应,他们的信息都无法被压缩,会导致传输效率低,但是HTTP2.0使得他们能够压缩加密
HTTP/2.0 在这个协议下,请求和响应都是使用Stream来进行的,把消息拆成多个Frame进行发送,每个Frame都可以单独的进行优化。
Frame类型:Headers、Continuation、Data等等,把请求和响应分成多个Frame,每个数据类型都可以单独优化。
特点
请求多路复用
Header 压缩
默认安全
HTTP ,但很多决定不支持 HTTP
HTTPS
Server Push
Server Push 在没有Server Push的情况下,每个页面的素材例如:css、html等都是必须发送一个单独的请求来进行的。
在有了Server Push的情况下,但我们的html页面包含css文件的时候,Server Push会自动传输css文件,即使这个html文件还没有进行对css的引用。这样一来,当html文件需要使用的时候,就不用再次发送请求了。节省了一些加载时间。
例子 1 2 3 4 5 6 7 8 9 10 11 12 13 14 package controllerimport "net/http" func registerHomeRoutes () { http.HandleFunc("/home" , func (writer http.ResponseWriter, request *http.Request) { if pusher, ok := writer.(http.Pusher); ok { pusher.Push("/css/app.css" , &http.PushOptions{ Header: http.Header{"Content-Type" : []string {"text/css" }}, }) } writer.Write([]byte ("back home,my son" )) }) }
之后启动服务,进入页面,打开F12看看,结果就清晰明了了。
测试 接下来学习,如何对go web应用进行测试:
测试 Model 层 可以编写一个单独的go程序进行测试,然后编写测试函数,并且应该注重命名:
user_test.go
测试代码所在文件的名称以 _test 结尾
对于生产编译,不会包含以 _test 结尾的文件
对于测试编译,会包含以 _test 结尾的文件
func TestUpdatesModifiedTime(t *testing.T) { … }
测试函数名应以 Test 开头(需要导出)
函数名需要表达出被验证的特性
测试函数的参数类型是 *testing.T,它会提供测试相关的一些工具
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package modelimport "strings" type Company struct { ID int `json:"id"` Name string `json:"name"` Country string `json:"country"` } func (c *Company) GetCompanyType () (result string ) { if strings.HasSuffix(c.Name, ".LTD" ) { result = "Limited Liability Company" } else { result = "Others" } return }
现在使用一个极为简易的例子去测试,判断公司的名称。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 package modelimport "testing" func TestCompany_GetCompanyType (t *testing.T) { c := Company{ ID: 123 , Name: "Google.LTD" , Country: "USA" , } companyType := c.GetCompanyType() if companyType != "Limited Liability Company" { t.Error("this is others!" ) } }
这便是测试的结果。
测试 Controller 层
为了尽量保证单元测试的隔离性,测试不要使用例如数据库、外部API、文件系统等外部资源。
模拟请求和响应
需要使用 net/http/httptest 提供的功能
这里有几个函数值得关注:
NewRequest 函数 1 2 3 4 5 6 func NewRequest (method, url string , body io.Reader) (*Request, error)
ResponseRecorder 1 2 3 4 5 6 7 8 type ResponseRecorder { Code int HeaderMap http.Header Body *bytes.Buffer Flushed bool }
例子 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package controllerimport ( "encoding/json" "go_web/model" "net/http" ) func RegisterRoutesController () { http.HandleFunc("/companies" , handlerCompany) } func handlerCompany (writer http.ResponseWriter, request *http.Request) { c := model.Company{ ID: 123 , Name: "Google" , Country: "USA" , } enc := json.NewEncoder(writer) enc.Encode(c) }
接着写一个测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 package controllerimport ( "encoding/json" "go_web/model" "io/ioutil" "net/http" "net/http/httptest" "testing" ) func TestHandleCompanyCorrect (t *testing.T) { r := httptest.NewRequest(http.MethodGet, "/companies" , nil ) w := httptest.NewRecorder() handlerCompany(w, r) result, _ := ioutil.ReadAll(w.Result().Body) c := model.Company{} json.Unmarshal(result, &c) if c.ID != 123 { t.Error("this is a failed" ) } }
这还是比较方便的
Profiling 性能分析 分析的对象
内存消耗
CPU 使用
阻塞的 goroutine
执行追踪
还有一个 Web 界面:应用的实时数据
如何分析 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import _ “net /http/pprof” 引入一个包,设置一些监听的 URL,它们会提供各类诊断信息 go tool pprof http://localhost:8000 /debug/pprof/heap // 内存 从应用获取内存 dump:应用在使用哪些内存,它们会去哪 go tool pprof http://localhost:8000 /debug/pprof/profile // CPU CPU 的快照,可以看到谁在用 CPU go tool pprof http://localhost:8000 /debug/pprof/block // goroutine 看到阻塞的 goroutine go tool pprof http://localhost:8000 /debug/pprof/trace?seconds=5 // trace 监控这段时间内,什么在执行,什么在调用什么… http:// localhost :8000/debug /pprof // 网页
未完待续…….
使用Go语言编写一个简易的分布式系统 想法 在一开始我要去学习用go语言编写一个分布式系统的时候。我会在想什么是分布式系统,分布式系统又跟以往的系统有什么很大的差异,或者说区别嘛。带着这个好奇,我去搜索一下什么才是真正的分布式系统,我以为是非常高深,又难以明白的一门学科,但是我仔细了解分布式系统的原理后。我发现我好像学过?!
以下资料是来源于我在网上搜索得出的信息:
一.概念 集 群: 同一个业务,部署在多个服务器上
分布式: 同一个业务,拆分成多个子业务,部署在不同的服务器上
微服务: 同一个业务,按照功能模块拆分,每一个服务只对应一个功能模块
二.区别 集群 是多台服务器一起处理同一个业务,可以使用负载均衡使得每一个服务器的负载相对平衡,集群中的一台服务器出现问题,该服务器所负责的业务可以由其他的服务器代为处理.集群是一种物理形态.
分布式 是把一个业务拆分成多个子业务,给不同的服务器去处理,这里的服务器可以是单个的服务器,也可以是多个服务器集群,一旦处理该业务的服务器出现问题,那么该业务就无法实现了.分布式是一种工作方式.
微服务 是把一个业务中的各种功能模块进行拆分,给不同的服务去处理,每个服务只处理一个功能模块,该服务可以是单个服务器也可以是多个服务器集群,每个服务之间都是低耦合的.微服务是一种架构风格.
为什么说分布式不一定是微服务:
假设有一个很大应用,拆分成几个小应用,但还是很庞大,即便使用了分布式,但其依旧不算是微服务,因为微服务的核心要素是微小,简单来说就是这个应用还不够小(嗯..没错就是这样!)
所以我们可以理解为:微服务是分布式的一个子集
三.应用场景 假设有一个业务,该业务有5个功能,每个功能单独处理需要1个小时.
此时,如果只部署一台服务器,则需要5个小时才能处理完该业务,若采用集群或者分布式来处理,结果如下:
1.采用集群处理:提供5台服务器一起处理该业务,则处理每个功能只需12分钟,即处理整个业务只需1个小时
2.采用分布式处理:提供5台服务器,每个服务器处理不同的功能,则一共也只需要一个小时.
该情况下,微服务和分布式的工作原理和最终结果是一样的.
四.总结 分布式中的每一个节点,都可以做集群.而集群并不一定就是分布式的.
微服务肯定是分布式的,但分布式不一定是微服务的.
作者:晔歌歌 链接:https://www.jianshu.com/p/5f157ac8efcf 来源:简书 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
所以说分布式系统其实是一个非常广泛的概念,很多的应用都可以是一个分布式系统,所以我想以我曾经学过的知识微服务,这一方面去了解,或者说是使用:如何用go编写一个微服务 ,也就是分布式系统。
注:我觉得写go还用前后端耦合,并且还用模板,是非常愚蠢的行为。而微服务是天生前后端分离的(战术后仰)。
总体分为三个部分:服务注册,服务发现,状态监控。
服务注册 创建自定义的日志服务 实现基本逻辑 目的在于接受请求,并把请求写入到log里面,是很多应用必备的功能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 package logimport ( "io/ioutil" stlog "log" "net/http" "os" ) var log *stlog.Loggertype fileLog string func (fl fileLog) Write (data []byte ) (int , error) { f, err := os.OpenFile(string (fl), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600 ) if err != nil { return 0 , err } defer f.Close() return f.Write(data) } func Run (destination string ) { log = stlog.New(fileLog(destination), "go" , stlog.LstdFlags) } func RegisterHandler () { http.HandleFunc("/log" , func (writer http.ResponseWriter, request *http.Request) { switch request.Method { case http.MethodPost: msg, err := ioutil.ReadAll(request.Body) if err != nil || len (msg) == 0 { writer.WriteHeader(http.StatusBadRequest) } write(string (msg)) default : writer.WriteHeader(http.StatusMethodNotAllowed) return } }) } func write (message string ) { log.Printf("%v\n" , message) }
先写好一个基本的日志服务的逻辑,逻辑较为简单。但还需要完善,接下来就要实现能够运行的日志服务。也就是说,还需要把web服务集中化管理,使其能够正常的运行。
接着创立一个service,去完善服务:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 package serviceimport ( "context" "fmt" "log" "net/http" ) func Start (ctx context.Context, serviceName, host, port string , registerHandlersFunc func () ) (context.Context, error) { registerHandlersFunc() ctx = startService(ctx, serviceName, host, port) return ctx, nil } func startService (ctx context.Context, name string , host string , port string ) context .Context { ctx, cancel := context.WithCancel(ctx) var server http.Server server.Addr = ":" + port go func () { log.Println(server.ListenAndServe()) cancel() }() go func () { fmt.Printf("%v 服务开始。按任意键停止. \n" , name) var s string fmt.Scanln(&s) server.Shutdown(ctx) cancel() }() return ctx }
这里完善了服务启动的逻辑,接着还需要去使这个服务能够正常的运行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 package mainimport ( "context" "fmt" "go_distributed_system_study/log" "go_distributed_system_study/service" stlog "log" ) func main () { log.Run("./distribute.log" ) host, port := "localhost" , "4000" ctx, err := service.Start( context.Background(), "Log service" , host, port, log.RegisterHandler, ) if err != nil { stlog.Fatalln(err) } <-ctx.Done() fmt.Println("停止服务" ) }
测试 接着启动服务,并使用postman进行测试:
1 http://localhost:4000/log
输入任意文字,就会看到在根目录下,有一个日志文件生成了。
服务注册的基本逻辑 注册中心 首先需要去尝试编写一下,一个可以将服务都注册进去的注册中心。
先写一个数据结构,注册中心:
1 2 3 4 5 6 7 8 9 10 11 12 13 package registrytype Registration struct { ServiceName ServiceName ServiceURL string } type ServiceName string const ( LogService = ServiceName("LogService" ) )
注册中心包含了各个服务的名字的地址,紧接着,编写服务注册进去之后的逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 package registryimport ( "encoding/json" "log" "net/http" "sync" ) const ServerPort = ":3000" const ServicesURL = "http://localhost" + ServerPort + "/services" type registry struct { registrations []Registration mutex *sync.Mutex } func (registry *registry) add (reg Registration) error { registry.mutex.Lock() registry.registrations = append (registry.registrations, reg) registry.mutex.Unlock() return nil } var reg = registry{ registrations: make ([]Registration, 0 ), mutex: new (sync.Mutex), } type RegistryService struct {}func (s RegistryService) ServeHTTP (w http.ResponseWriter, r *http.Request) { log.Println("接受请求:" ) switch r.Method { case http.MethodPost: dec := json.NewDecoder(r.Body) var r Registration err := dec.Decode(&r) if err != nil { log.Println(err) w.WriteHeader(http.StatusBadRequest) return } log.Printf("增加服务:%v ,该服务的地址是:%s \n" , r.ServiceName, r.ServiceURL) err = reg.add(r) if err != nil { log.Println(err) w.WriteHeader(http.StatusBadRequest) return } default : w.WriteHeader(http.StatusMethodNotAllowed) return } }
在之后,需要让服务独立运行
独立服务 接着就需要将之前的服务,注册到服务中心中。
这就需要创建一个服务中心主要运行逻辑了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 package mainimport ( "context" "fmt" "go_distributed_system_study/registry" "log" "net/http" ) func main () { http.Handle("/services" , registry.RegistryService{}) ctx, cancel := context.WithCancel(context.Background()) defer cancel() var srv http.Server srv.Addr = registry.ServerPort go func () { log.Println(srv.ListenAndServe()) cancel() }() go func () { fmt.Printf("注册中心 的服务开始。按任意键停止. \n" ) var s string fmt.Scanln(&s) srv.Shutdown(ctx) cancel() }() <-ctx.Done() fmt.Println("结束服务注册" ) }
其实和日志服务的注册类似,没什么特别的,接着测试:
测试 1 http://localhost:3000/services
接着输入json
1 2 3 4 5 { "serviceName" : "study service" , "serviceURL" : " http://localhost/5000/study" }
接着显示服务注册成功:
1 2 3 注册中心 的服务开始。按任意键停止. 接受请求: 增加服务:study service ,该服务的//localhost/5000 /study
注册服务 微服务思想 首先微服务的基本含义是:注册中心是一个服务,然后其他的服务注册到注册中心,然后由主要控制台相互控制和调用。
那么现在会需要一个客户端:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 package registryimport ( "bytes" "encoding/json" "fmt" "net/http" ) func RegisterService (r Registration) error { buf := new (bytes.Buffer) enc := json.NewEncoder(buf) err := enc.Encode(r) if err != nil { return err } res, err := http.Post(ServicesURL, "application/json" , buf) if err != nil { return err } if res.StatusCode != http.StatusOK { return fmt.Errorf("服务注册失败 " +"状态码为: %v" , res.StatusCode) } return nil }
接着需要去改一下日志服务的逻辑,使得日志服务会主动去注册自己:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 package serviceimport ( "context" "fmt" "go_distributed_system_study/registry" "log" "net/http" ) func Start (ctx context.Context, host, port string , reg registry.Registration, registerHandlersFunc func () ) (context.Context, error) { registerHandlersFunc() ctx = startService(ctx, reg.ServiceName, host, port) err := registry.RegisterService(reg) if err != nil { return ctx, err } return ctx, nil } func startService (ctx context.Context, name registry.ServiceName, host string , port string ) context .Context { ctx, cancel := context.WithCancel(ctx) var server http.Server server.Addr = ":" + port go func () { log.Println(server.ListenAndServe()) cancel() }() go func () { fmt.Printf("%v 服务开始。按任意键停止. \n" , name) var s string fmt.Scanln(&s) server.Shutdown(ctx) cancel() }() return ctx }
这的registry.RegisterService(reg) 实际上会去调用client的func RegisterService(r Registration) ,这样会向注册中心发送一个post请求,去注册自己。
接着,要去改动日志服务的main函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 package mainimport ( "context" "fmt" "go_distributed_system_study/log" "go_distributed_system_study/registry" "go_distributed_system_study/service" stlog "log" ) func main () { log.Run("./distribute.log" ) host, port := "localhost" , "4000" serviceAddress := fmt.Sprintf("http://%s:%s" , host, port) r := registry.Registration{ ServiceName: "log service" , ServiceURL: serviceAddress, } ctx, err := service.Start( context.Background(), host, port, r, log.RegisterHandler, ) if err != nil { stlog.Fatalln(err) } <-ctx.Done() fmt.Println("停止服务" ) }
主要是增加了serviceAddress ,也就是说所有服务都会使用这同一个逻辑。
测试 紧接着两个服务连续启动,先启动注册中心,后启动日志逻辑,结果如下:
1 2 3 4 5 6 7 注册中心 的服务开始。按任意键停止. log service 服务开始。按任意键停止 接受请求: 增加服务:log service ,该服务的地址是:http://localhost:4000
很简单对吧,和Spring cloud的微服务简直一模一样。
取消注册 那么我们把微服务注册进去了,自然能够调用,但是怎么主动去取消微服务呢?它肯定不是说我自己把自己的微服务关了就行了,同时也需要通知注册中心。
修改注册中心 直接在注册中心加上一个方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 func (registry *registry) remove (url string ) error { for i := range reg.registrations { if reg.registrations[i].ServiceURL == url { registry.mutex.Lock() reg.registrations = append (reg.registrations[:i], reg.registrations[i+1 :]...) registry.mutex.Unlock() return nil } } return fmt.Errorf("服务地址未发现: %s " , url) }
之后直接在Switch里面增加一个情况Delete:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 case http.MethodDelete: payload, err := ioutil.ReadAll(r.Body) if err != nil { log.Println(err) w.WriteHeader(http.StatusInternalServerError) return } url := string (payload) log.Printf("移除服务: %s" , url) err = reg.remove(url) if err != nil { log.Println(err) w.WriteHeader(http.StatusInternalServerError) return }
代码很清晰,就是一模一样的移除服务,总体代码改动如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 package registryimport ( "encoding/json" "fmt" "io/ioutil" "log" "net/http" "sync" ) const ServerPort = ":3000" const ServicesURL = "http://localhost" + ServerPort + "/services" type registry struct { registrations []Registration mutex *sync.Mutex } func (registry *registry) add (reg Registration) error { registry.mutex.Lock() registry.registrations = append (registry.registrations, reg) registry.mutex.Unlock() return nil } func (registry *registry) remove (url string ) error { for i := range reg.registrations { if reg.registrations[i].ServiceURL == url { registry.mutex.Lock() reg.registrations = append (reg.registrations[:i], reg.registrations[:i+1 ]...) registry.mutex.Unlock() return nil } } return fmt.Errorf("服务地址未发现: %s " , url) } var reg = registry{ registrations: make ([]Registration, 0 ), mutex: new (sync.Mutex), } type RegistryService struct {}func (s RegistryService) ServeHTTP (w http.ResponseWriter, r *http.Request) { log.Println("接受请求:" ) switch r.Method { case http.MethodPost: dec := json.NewDecoder(r.Body) var r Registration err := dec.Decode(&r) if err != nil { log.Println(err) w.WriteHeader(http.StatusBadRequest) return } log.Printf("增加服务:%v ,该服务的地址是:%s \n" , r.ServiceName, r.ServiceURL) err = reg.add(r) if err != nil { log.Println(err) w.WriteHeader(http.StatusBadRequest) return } case http.MethodDelete: payload, err := ioutil.ReadAll(r.Body) if err != nil { log.Println(err) w.WriteHeader(http.StatusInternalServerError) return } url := string (payload) log.Printf("移除服务: %s" , url) err = reg.remove(url) if err != nil { log.Println(err) w.WriteHeader(http.StatusInternalServerError) return } default : w.WriteHeader(http.StatusMethodNotAllowed) return } }
注册中心取消服务的方法定义好了,那么也就需要在其他可注册服务的函数体中定义方法。为了进一步的解除耦合度,取消服务的方法和建立服务的方法一样,需要在client里面编写。
修改客户端 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 package registryimport ( "bytes" "encoding/json" "fmt" "net/http" ) func RegisterService (r Registration) error { buf := new (bytes.Buffer) enc := json.NewEncoder(buf) err := enc.Encode(r) if err != nil { return err } res, err := http.Post(ServicesURL, "application/json" , buf) if err != nil { return err } if res.StatusCode != http.StatusOK { return fmt.Errorf("服务注册失败 " +"状态码为: %v" , res.StatusCode) } return nil } func ShutdownService (url string ) error { req, err := http.NewRequest( http.MethodDelete, ServicesURL, bytes.NewBuffer([]byte (url))) if err != nil { return err } req.Header.Add("Content-Type" , "text/plain" ) res, err := http.DefaultClient.Do(req) if err != nil { return err } if res.StatusCode != http.StatusOK { return fmt.Errorf("服务取消失败,状态码为:%v" , res.StatusCode) } return nil }
增加了ShutdownService去结束这个服务。
紧接着,去开始服务注册的函数里边,进行取消注册的修改:
修改服务的注册功能 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 package serviceimport ( "context" "fmt" "go_distributed_system_study/registry" "log" "net/http" ) func Start (ctx context.Context, host, port string , reg registry.Registration, registerHandlersFunc func () ) (context.Context, error) { registerHandlersFunc() ctx = startService(ctx, reg.ServiceName, host, port) err := registry.RegisterService(reg) if err != nil { return ctx, err } return ctx, nil } func startService (ctx context.Context, name registry.ServiceName, host string , port string ) context .Context { ctx, cancel := context.WithCancel(ctx) var server http.Server server.Addr = ":" + port go func () { log.Println(server.ListenAndServe()) err := registry.ShutdownService(fmt.Sprintf("http://%s:%s" , host, port)) if err != nil { log.Println(err) } cancel() }() go func () { fmt.Printf("%v 服务开始。按任意键停止. \n" , name) var s string fmt.Scanln(&s) err := registry.ShutdownService(fmt.Sprintf("http://%s:%s" , host, port)) if err != nil { log.Println(err) } server.Shutdown(ctx) cancel() }() return ctx }
主要是两个goroutine的修改,使其具有取消服务的功能。
测试: 那么注册中心和服务的逻辑都修改好了,然后和上面的步骤一样,先启动注册中心,后启动日志逻辑,结果为:
1 2 3 4 5 6 7 8 9 10 11 12 注册中心 的服务开始。按任意键停止. 等待接受请求: 增加服务:log service ,该服务的地址是:http://localhost:4000 log service 服务开始。按任意键停止. q http: Server closed 停止服务 移除服务: http ://localhost :4000 http : Server closed
服务发现 前面的服务注册都是一对一的,还体现不了分布式的特点。接下来进行多服务注册,使得一个学生成绩的服务既要使用日志服务,也要注册到注册中心
业务服务 基本的数据结构与方法 首先要编写一个学生的基础信息的数据结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 package gradesimport ( "fmt" "sync" ) type Student struct { ID int FirstName string LastName string Grades []Grade } type Grade struct { Title string Type GradeType Score float32 } type GradeType string const ( GradeQuiz = GradeType("Quiz" ) GradeTest = GradeType("Test" ) GradeExam = GradeType("Exam" ) ) func (s Student) Average () float32 { var result float32 for _, grade := range s.Grades { result += grade.Score } return result / float32 (len (s.Grades)) } type Students []Studentfunc (ss Students) GetByID (id int ) (*Student, error) { for i := range ss { if ss[i].ID == id { return &ss[i], nil } } return nil , fmt.Errorf("学生的ID: %d 未找到" , id) } var ( students Students studentsMutex sync.Mutex )
接着肯定得有一些学生的数据,来做测试,这些数据一开始就会被加载进数据结构中,这暂时是用来代替数据库的方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 package gradesfunc init () { students = []Student{ { ID: 1 , FirstName: "Nick" , LastName: "Carter" , Grades: []Grade{ { Title: "Quiz 1" , Type: GradeQuiz, Score: 85 , }, { Title: "Final Exam" , Type: GradeExam, Score: 94 , }, { Title: "Quiz 2" , Type: GradeQuiz, Score: 82 , }, }, }, { ID: 2 , FirstName: "Roberto" , LastName: "Baggio" , Grades: []Grade{ { Title: "Quiz 1" , Type: GradeQuiz, Score: 100 , }, { Title: "Final Exam" , Type: GradeExam, Score: 100 , }, { Title: "Quiz 2" , Type: GradeQuiz, Score: 81 , }, }, }, { ID: 3 , FirstName: "Emma" , LastName: "Stone" , Grades: []Grade{ { Title: "Quiz 1" , Type: GradeQuiz, Score: 67 , }, { Title: "Final Exam" , Type: GradeExam, Score: 0 , }, { Title: "Quiz 2" , Type: GradeQuiz, Score: 75 , }, }, }, { ID: 4 , FirstName: "Rachel" , LastName: "McAdams" , Grades: []Grade{ { Title: "Quiz 1" , Type: GradeQuiz, Score: 98 , }, { Title: "Final Exam" , Type: GradeExam, Score: 99 , }, { Title: "Quiz 2" , Type: GradeQuiz, Score: 94 , }, }, }, { ID: 5 , FirstName: "Kelly" , LastName: "Clarkson" , Grades: []Grade{ { Title: "Quiz 1" , Type: GradeQuiz, Score: 95 , }, { Title: "Final Exam" , Type: GradeExam, Score: 100 , }, { Title: "Quiz 2" , Type: GradeQuiz, Score: 97 , }, }, }, } }
那么,也肯定要有server,才能正常的启动服务,需要去编写基本的逻辑,比如获取全部学生信息,根据ID进行信息搜索,增加学生信息等功能:
服务的逻辑 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 package gradesimport ( "bytes" "encoding/json" "fmt" "log" "net/http" "strconv" "strings" ) func RegisterHandlers () { handler := new (studentsHandler) http.Handle("/students" , handler) http.Handle("/students/" , handler) } type studentsHandler struct {}func (sh studentsHandler) ServeHTTP (w http.ResponseWriter, r *http.Request) { pathSegments := strings.Split(r.URL.Path, "/" ) switch len (pathSegments) { case 2 : sh.getAll(w, r) case 3 : id, err := strconv.Atoi(pathSegments[2 ]) if err != nil { w.WriteHeader(http.StatusNotFound) return } sh.getOne(w, r, id) case 4 : id, err := strconv.Atoi(pathSegments[2 ]) if err != nil { w.WriteHeader(http.StatusNotFound) return } sh.addGrade(w, r, id) default : w.WriteHeader(http.StatusNotFound) } } func (sh studentsHandler) getAll (w http.ResponseWriter, r *http.Request) { studentsMutex.Lock() defer studentsMutex.Unlock() data, err := sh.toJSON(students) if err != nil { w.WriteHeader(http.StatusInternalServerError) log.Println(err) return } w.Header().Add("Content-Type" , "application/json" ) w.Write(data) } func (sh studentsHandler) getOne (w http.ResponseWriter, r *http.Request, id int ) { studentsMutex.Lock() defer studentsMutex.Unlock() student, err := students.GetByID(id) if err != nil { w.WriteHeader(http.StatusNotFound) log.Println(err) return } data, err := sh.toJSON(student) if err != nil { w.WriteHeader(http.StatusInternalServerError) log.Printf("学生信息序列化失败: %q" , err) return } w.Header().Add("Content-Type" , "application/json" ) w.Write(data) } func (sh studentsHandler) addGrade (w http.ResponseWriter, r *http.Request, id int ) { studentsMutex.Lock() defer studentsMutex.Unlock() student, err := students.GetByID(id) if err != nil { w.WriteHeader(http.StatusNotFound) log.Println(err) return } var g Grade dec := json.NewDecoder(r.Body) err = dec.Decode(&g) if err != nil { w.WriteHeader(http.StatusBadRequest) log.Println(err) return } student.Grades = append (student.Grades, g) w.WriteHeader(http.StatusCreated) data, err := sh.toJSON(g) if err != nil { log.Println(err) return } w.Header().Add("Content-Type" , "applicaiton/json" ) w.Write(data) } func (sh studentsHandler) toJSON (obj interface {}) ([]byte , error) { var b bytes.Buffer enc := json.NewEncoder(&b) err := enc.Encode(obj) if err != nil { return nil , fmt.Errorf("学生信息序列化失败: %q" , err) } return b.Bytes(), nil }
这样的话,基础逻辑也已经完善了,接着就是在注册中心里增加服务:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package registrytype Registration struct { ServiceName ServiceName ServiceURL string } type ServiceName string const ( LogService = ServiceName("LogService" ) GradingService = ServiceName("GradingService" ) )
最后,得让web服务可以运行,在cmd文件夹下创建一个新的main函数,写入一样的逻辑代码:
服务启动器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 package mainimport ( "context" "go_distributed_system_study/grades" "go_distributed_system_study/registry" "go_distributed_system_study/service" "fmt" stlog "log" ) func main () { host, port := "localhost" , "6000" serviceAddress := fmt.Sprintf("http://%v:%v" , host, port) r := registry.Registration{ ServiceName: registry.GradingService, ServiceURL: serviceAddress, } ctx, err := service.Start(context.Background(), host, port, r, grades.RegisterHandlers) if err != nil { stlog.Fatal(err) } <-ctx.Done() fmt.Println("grading service 服务停止了" ) }
这么下来,这个业务服务也就完成了,他们可以互相不干扰的进行服务注册,但是现在grade服务还不能去调用日志服务。所以我们还需要服务发现。
服务发现 服务发现作用能让grade服务可以请求log服务
去引用日志服务 首先肯定是要给服务的数据结构增加一些基本信息,这样才能使得服务有这些基本的功能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 package registrytype Registration struct { ServiceName ServiceName ServiceURL string RequiredServices []ServiceName ServiceUpdateURL string } type ServiceName string const ( LogService = ServiceName("LogService" ) GradingService = ServiceName("GradingService" ) ) type patchEntry struct { Name ServiceName URL string } type patch struct { Added []patchEntry Removed []patchEntry }
接下来可以想想,一个服务如果还依赖着其他的服务。那么,当这个服务正要注册的时候,或者说要加入服务群体的时候。就会在:
1 func (registry *registry) add (reg Registration)
进行服务注册,那么这个时候如果服务还依赖其他服务,比如正要注册的grade服务还依赖log服务,这时候就正好可以去获取依赖。
修改后代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 package registryimport ( "bytes" "encoding/json" "fmt" "io/ioutil" "log" "net/http" "sync" ) const ServerPort = ":3000" const ServicesURL = "http://localhost" + ServerPort + "/services" type registry struct { registrations []Registration mutex *sync.RWMutex } func (registry *registry) add (reg Registration) error { registry.mutex.Lock() registry.registrations = append (registry.registrations, reg) registry.mutex.Unlock() err := registry.sendRequireServices(reg) return err } func (registry *registry) sendRequireServices (reg Registration) error { registry.mutex.RLock() defer registry.mutex.RUnlock() var p patch for _, serviceReg := range registry.registrations { for _, reqService := range reg.RequiredServices { if serviceReg.ServiceName == reqService { p.Added = append (p.Added, patchEntry{ Name: serviceReg.ServiceName, URL: serviceReg.ServiceURL, }) } } } err := registry.sendPatch(p, reg.ServiceUpdateURL) if err != nil { return err } return nil } func (r registry) sendPatch (p patch, url string ) error { d, err := json.Marshal(p) if err != nil { return err } _, err = http.Post(url, "application/json" , bytes.NewBuffer(d)) if err != nil { return err } return nil } func (registry *registry) remove (url string ) error { for i := range reg.registrations { if reg.registrations[i].ServiceURL == url { registry.mutex.Lock() reg.registrations = append (reg.registrations[:i], reg.registrations[:i+1 ]...) registry.mutex.Unlock() return nil } } return fmt.Errorf("服务地址未发现: %s " , url) } var reg = registry{ registrations: make ([]Registration, 0 ), mutex: new (sync.RWMutex), } type RegistryService struct {}func (s RegistryService) ServeHTTP (w http.ResponseWriter, r *http.Request) { log.Println("等待接受请求:" ) switch r.Method { case http.MethodPost: dec := json.NewDecoder(r.Body) var r Registration err := dec.Decode(&r) if err != nil { log.Println(err) w.WriteHeader(http.StatusBadRequest) return } log.Printf("增加服务:%v ,该服务的地址是:%s \n" , r.ServiceName, r.ServiceURL) err = reg.add(r) if err != nil { log.Println(err) w.WriteHeader(http.StatusBadRequest) return } case http.MethodDelete: payload, err := ioutil.ReadAll(r.Body) if err != nil { log.Println(err) w.WriteHeader(http.StatusInternalServerError) return } url := string (payload) log.Printf("移除服务: %s" , url) err = reg.remove(url) if err != nil { log.Println(err) w.WriteHeader(http.StatusInternalServerError) return } default : w.WriteHeader(http.StatusMethodNotAllowed) return } }
这是一个找到所需要服务并将其注册的过程。
接着,grade服务会向注册中心请求这些服务,但是注册中心也需要地方去存储这些请求的服务。
log服务就会向grade服务提供服务,那么会需要一些数据结构去存储:
1 2 3 4 5 6 7 type providers struct { services map [ServiceName][]string mutex *sync.RWMutex }
然后去实现它的逻辑,总体修改后代码如下:
服务的提供者 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 package registryimport ( "bytes" "encoding/json" "fmt" "log" "math/rand" "net/http" "net/url" "sync" ) func RegisterService (r Registration) error { serviceUpdateURL, err := url.Parse(r.ServiceUpdateURL) if err != nil { return err } http.Handle(serviceUpdateURL.Path, &serviceUpdateHandler{}) buf := new (bytes.Buffer) enc := json.NewEncoder(buf) err = enc.Encode(r) if err != nil { return err } res, err := http.Post(ServicesURL, "application/json" , buf) if err != nil { return err } if res.StatusCode != http.StatusOK { return fmt.Errorf("服务注册失败 " +"状态码为: %v" , res.StatusCode) } return nil } type serviceUpdateHandler struct {}func (suh serviceUpdateHandler) ServeHTTP (w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { w.WriteHeader(http.StatusMethodNotAllowed) return } dec := json.NewDecoder(r.Body) var p patch err := dec.Decode(&p) if err != nil { log.Println(err) w.WriteHeader(http.StatusBadRequest) return } prov.Update(p) } func ShutdownService (url string ) error { req, err := http.NewRequest( http.MethodDelete, ServicesURL, bytes.NewBuffer([]byte (url))) if err != nil { return err } req.Header.Add("Content-Type" , "text/plain" ) res, err := http.DefaultClient.Do(req) if err != nil { return err } if res.StatusCode != http.StatusOK { return fmt.Errorf("服务取消失败,状态码为:%v" , res.StatusCode) } return nil } type providers struct { services map [ServiceName][]string mutex *sync.RWMutex } var prov = providers{ services: make (map [ServiceName][]string ), mutex: new (sync.RWMutex), } func (p *providers) Update (pat patch) { p.mutex.Lock() defer p.mutex.Unlock() for _, patchEntry := range pat.Added { if _, ok := p.services[patchEntry.Name]; !ok { p.services[patchEntry.Name] = make ([]string , 0 ) } p.services[patchEntry.Name] = append (p.services[patchEntry.Name], patchEntry.URL) } for _, patchEntry := range pat.Removed { if providerURLs, ok := p.services[patchEntry.Name]; ok { for i := range providerURLs { if providerURLs[i] == patchEntry.URL { p.services[patchEntry.Name] = append (providerURLs[:i], providerURLs[i+1 :]...) } } } } } func (p providers) get (name ServiceName) (string , error) { providers, ok := p.services[name] if !ok { return "" , fmt.Errorf("没有可提供服务的提供商: %v" , name) } idx := int (rand.Float32() * float32 (len (providers))) return providers[idx], nil } func GetProvider (name ServiceName) (string , error) { return prov.get(name) }
客户端的client log服务现在有服务端的逻辑,但是客户端的服务想使用这个client还是比较麻烦的,所以还需要对log服务有一个自己的client:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 package logimport ( "bytes" "fmt" "go_distributed_system_study/registry" "net/http" stlog "log" ) func SetClientLogger (serviceURL string , clientService registry.ServiceName) { stlog.SetPrefix(fmt.Sprintf("[%v] - " , clientService)) stlog.SetFlags(0 ) stlog.SetOutput(&clientLogger{url: serviceURL}) } type clientLogger struct { url string } func (cl clientLogger) Write (data []byte ) (int , error) { b := bytes.NewBuffer([]byte (data)) res, err := http.Post(cl.url+"/log" , "text/plain" , b) if err != nil { return 0 , err } if res.StatusCode != http.StatusOK { return 0 , fmt.Errorf("Failed to send log message. Service responded with %d - %s" , res.StatusCode, res.Status) } return len (data), nil }
这样就可以让本地的日志服务写好日志后发送到服务器端保存
使main函数具有服务发现的功能 主要是使得两个启动器拥有新的功能:
grading service 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 package mainimport ( "context" "go_distributed_system_study/grades" "go_distributed_system_study/log" "go_distributed_system_study/registry" "go_distributed_system_study/service" "fmt" stlog "log" ) func main () { host, port := "localhost" , "6000" serviceAddress := fmt.Sprintf("http://%v:%v" , host, port) r := registry.Registration{ ServiceName: registry.GradingService, ServiceURL: serviceAddress, RequiredServices: []registry.ServiceName{registry.LogService}, ServiceUpdateURL: serviceAddress + "/services" , } ctx, err := service.Start(context.Background(), host, port, r, grades.RegisterHandlers) if err != nil { stlog.Fatal(err) } if logProvider, err := registry.GetProvider(registry.LogService); err == nil { fmt.Printf("发现日志服务: %s\n" , logProvider) log.SetClientLogger(logProvider, r.ServiceName) } <-ctx.Done() fmt.Println("grading service 服务停止了" ) }
log service 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 package mainimport ( "context" "fmt" "go_distributed_system_study/log" "go_distributed_system_study/registry" "go_distributed_system_study/service" stlog "log" ) func main () { log.Run("./distribute.log" ) host, port := "localhost" , "4000" serviceAddress := fmt.Sprintf("http://%s:%s" , host, port) r := registry.Registration{ ServiceName: registry.LogService, ServiceURL: serviceAddress, RequiredServices: make ([]registry.ServiceName, 0 ), ServiceUpdateURL: serviceAddress + "/services" , } ctx, err := service.Start( context.Background(), host, port, r, log.RegisterHandler, ) if err != nil { stlog.Fatalln(err) } <-ctx.Done() fmt.Println("停止服务" ) }
接下来便可以测试了。
测试 按照:registryservice,logservice,gradingservice的顺序启动,测试结果如:
1 2 3 4 5 6 7 8 9 10 注册中心 的服务开始。按任意键停止. 等待接受请求: 增加服务:LogService ,该服务的地址是:http://localhost:4000 等待接受请求: 增加服务:GradingService ,该服务的地址是:http://localhost:6000 LogService 服务开始。按任意键停止. GradingService 服务开始。按任意键停止. 发现日志服务: http://localhost:4000
这么一来就完成了。
依赖变化 重新发现服务 可以从上述的情况下看到一些不那么方便的点,一是:启动必须按照顺序来,不能随意。二是:当log服务下线后,再上线的话不会被再次发现。这都是服务极为脆弱的表现。那么解决这个问题的最好方法是:使服务具有依赖变化时进行通知的功能。
可以在服务中更改,使其具备通知的功能,主要是有notify函数,总体修改如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 package registryimport ( "bytes" "encoding/json" "fmt" "io/ioutil" "log" "net/http" "sync" ) const ServerPort = ":3000" const ServicesURL = "http://localhost" + ServerPort + "/services" type registry struct { registrations []Registration mutex *sync.RWMutex } func (registry *registry) add (reg Registration) error { registry.mutex.Lock() registry.registrations = append (registry.registrations, reg) registry.mutex.Unlock() err := registry.sendRequireServices(reg) registry.notify(patch{ Added: []patchEntry{ patchEntry{ Name: reg.ServiceName, URL: reg.ServiceURL, }, }, }) return err } func (registry *registry) sendRequireServices (reg Registration) error { registry.mutex.RLock() defer registry.mutex.RUnlock() var p patch for _, serviceReg := range registry.registrations { for _, reqService := range reg.RequiredServices { if serviceReg.ServiceName == reqService { p.Added = append (p.Added, patchEntry{ Name: serviceReg.ServiceName, URL: serviceReg.ServiceURL, }) } } } err := registry.sendPatch(p, reg.ServiceUpdateURL) if err != nil { return err } return nil } func (r registry) notify (fullPatch patch) { r.mutex.RLock() defer r.mutex.RUnlock() for _, reg := range r.registrations { go func (reg Registration) { for _, reqService := range reg.RequiredServices { p := patch{Added: []patchEntry{}, Removed: []patchEntry{}} sendUpdate := false for _, added := range fullPatch.Added { if added.Name == reqService { p.Added = append (p.Added, added) sendUpdate = true } } for _, removed := range fullPatch.Removed { if removed.Name == reqService { p.Removed = append (p.Removed, removed) sendUpdate = true } } if sendUpdate { err := r.sendPatch(p, reg.ServiceUpdateURL) if err != nil { log.Println(err) return } } } }(reg) } } func (r registry) sendPatch (p patch, url string ) error { d, err := json.Marshal(p) if err != nil { return err } _, err = http.Post(url, "application/json" , bytes.NewBuffer(d)) if err != nil { return err } return nil } func (registry *registry) remove (url string ) error { for i := range reg.registrations { if reg.registrations[i].ServiceURL == url { registry.mutex.Lock() reg.registrations = append (reg.registrations[:i], reg.registrations[:i+1 ]...) registry.mutex.Unlock() return nil } } return fmt.Errorf("服务地址未发现: %s " , url) } var reg = registry{ registrations: make ([]Registration, 0 ), mutex: new (sync.RWMutex), } type RegistryService struct {}func (s RegistryService) ServeHTTP (w http.ResponseWriter, r *http.Request) { log.Println("等待接受请求:" ) switch r.Method { case http.MethodPost: dec := json.NewDecoder(r.Body) var r Registration err := dec.Decode(&r) if err != nil { log.Println(err) w.WriteHeader(http.StatusBadRequest) return } log.Printf("增加服务:%v ,该服务的地址是:%s \n" , r.ServiceName, r.ServiceURL) err = reg.add(r) if err != nil { log.Println(err) w.WriteHeader(http.StatusBadRequest) return } case http.MethodDelete: payload, err := ioutil.ReadAll(r.Body) if err != nil { log.Println(err) w.WriteHeader(http.StatusInternalServerError) return } url := string (payload) log.Printf("移除服务: %s" , url) err = reg.remove(url) if err != nil { log.Println(err) w.WriteHeader(http.StatusInternalServerError) return } default : w.WriteHeader(http.StatusMethodNotAllowed) return } }
测试1 接着再进行测试,可以看到,当log服务下线后,重新上线时,grading 服务就能够发现log服务了。
1 2 3 4 收到更新: {[{LogService http://localhost:4000 } {LogService http://localhost:4000 }] []} 发现日志服务: http://localhost:4000 收到更新: {[{LogService http://localhost:4000 }] []} 收到更新: {[{LogService http://localhost:4000 }] []}
服务下线告知 接着也容易,把remove方法里面添加下线告知的功能就行了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 func (registry *registry) remove (url string ) error { for i := range reg.registrations { if reg.registrations[i].ServiceURL == url { registry.notify(patch{ Removed: []patchEntry{ { Name: registry.registrations[i].ServiceName, URL: registry.registrations[i].ServiceURL, }, }, }) registry.mutex.Lock() reg.registrations = append (reg.registrations[:i], reg.registrations[:i+1 ]...) registry.mutex.Unlock() return nil } } return fmt.Errorf("服务地址未发现: %s " , url) }
这样,这部分逻辑就完成了。接下来测试代码。
测试2 测试的步骤是先开启注册中心,再开启日志服务,后开始grade服务。然后使得日志服务停止,再重启。可以看到一系列的结果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 注册中心 的服务开始。按任意键停止. 等待接受请求: 增加服务:LogService ,该服务的地址是:http://localhost:4000 等待接受请求: 增加服务:GradingService ,该服务的地址是:http://localhost:6000 移除服务: http://localhost:4000 等待接受请求: 增加服务:LogService ,该服务的地址是:http://localhost:4000 LogService 服务开始。按任意键停止. 收到更新: {[] []} http: Server closed 停止服务 LogService 服务开始。按任意键停止GradingService 服务开始。按任意键停止.收到更新: {[{LogService http ://localhost :4000}] []} 发现日志服务: http ://localhost :4000 收到更新: {[] [{LogService http ://localhost :4000}]} 收到更新: {[{LogService http ://localhost :4000}] []}
使用解除了耦合的网络接口,这也是Spring Cloud的微服务思想,同时也是分布式的一种类型。所以说分布式也没什么神奇之处,最核心的一处在于:把本地接口转化为了网络接口 。能够理解这一过程,也就理解了分布式的思想。