用Go语言编写一个web与分布式应用

使用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 main

import "net/http"

func main() {
//初始化一个函数,让其能对web应用进行响应
//第一个参数是路由地址,第二个参数是函数主要功能是收到请求并响应
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 main

import "net/http"

type myHandler struct {
}

//http.ResponseWriter用于表达响应的
// *http.Request 收到的请求,可以是get或post等等
func (m *myHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("hello, my lover"))
}

func main() {
//自制一个Handler
mh := myHandler{}

//等同于 http.ListenAndServe
server := http.Server{
Addr: "localhost:8888",
Handler: &mh, //Handler实际上是一个接口
//http.DefaultServeMux也是一个Handler,使用nil是默认使用
}
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 main

import "net/http"

type helloHandler struct {
}

//http.ResponseWriter用于表达响应的
// *http.Request 收到的请求,可以是get或post等等
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() {
//自制一个Handler
mh := helloHandler{}
a := aboutHandler{}

//等同于 http.ListenAndServe
server := http.Server{
Addr: "localhost:8888",
Handler: nil, //这样会默认使用DefaultServeMux
//http.DefaultServeMux也是一个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 main

import "net/http"

type helloHandler struct {
}

//http.ResponseWriter用于表达响应的
// *http.Request 收到的请求,可以是get或post等等
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() {
//自制一个Handler
mh := helloHandler{}
a := aboutHandler{}

//等同于 http.ListenAndServe
server := http.Server{
Addr: "localhost:8888",
Handler: nil, //这样会默认使用DefaultServeMux
//http.DefaultServeMux也是一个Handler,使用nil是默认使用
}
http.Handle("/hello", &mh) //相信到这里,就很简单了,直接望文生义即可
http.Handle("/about", &a)

//使用HandleFunc
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)

// ServeHTTP calls f(w, r).
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

  • func NotFoundHandler() Handler
    返回一个 handler,它给每个请求的响应都是“404 page not found”
  • func RedirectHandler(url string, code int) Handler
    返回一个 handler,它把每个请求使用给定的状态码跳转到指定的 URL。
    url,要跳转到的 URL
    code,跳转的状态码(3xx),常见的:StatusMovedPermanently、StatusFound 或 StatusSeeOther 等
  • func StripPrefix(prefix string, h handler) Handler
    返回一个 handler,它从请求 URL 中去掉指定的前缀,然后再调用另一个 handler。
    如果请求的 URL 与提供的前缀不符,那么 404
    略像中间件
    prefix,URL 将要被移除的字符串前缀
    h,是一个 handler,在移除字符串前缀之后,这个 handler 将会接收到请求
    修饰了另一个 Handler
  • func TimeoutHandler(h Handler, dt time.Duration, msg string) Handler
    返回一个 handler,它用来在指定时间内运行传入的 h。
    也相当于是一个修饰器
    h,将要被修饰的 handler
    dt,第一个 handler 允许的处理时间
    msg,如果超时,那么就把 msg 返回给请求,表示响应时间过长
  • func FileServer(root FileSystem) Handler
    返回一个 handler,使用基于 root 的文件系统来响应请求
    type FileSystem interface {
    Open(name string) (File, error)
    }
    使用时需要用到操作系统的文件系统,所以还需要委托给:
    type Dir string
    func (d Dir) Open(name string) (File, error)

代码演示:FileServer

1
http.ListenAndServe(":8888", http.FileServer(http.Dir("root")))

可以通过这个方法指定root这个根路径,这样打开文件的时候直接去root,直接找到需要的url路径,加载文件

请求类型

最基本的肯定是:

  • HTTP Request 和 HTTP Response(请求和响应)

  • 它们具有相同的结构:

    • ​ 请求(响应)行

    • ​ 0 个或多个 Header

    • ​ 空行

    • ​ 可选的消息体(Body)

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 Header

请求和响应(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

代码演示:Header
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
代码演示:Header
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() //得到一个map

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

控制台显示结果为:

1
2
[123 456]
panda

这表明 query[“id”] 是获取所有的值,而query.Get(“name”) 只会获取第一个值

Forms

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呢?

  1. 简单文本:表单 URL 编码
  2. 大量数据,例如上传文件:multipart-MIME,甚至可以把二进制数据通过选择 Base64 编码,来当作文本进行发送

Form字段

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 最常见的应用场景就是上传文件(例子):

  1. 首先调用 ParseMultipartForm 方法
  2. 从 File 字段获得 FileHeader,调用其 Open 方法来获得文件
  3. 可以使用 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) //1024表示一次性加载到内存的最大字节数
fileHeader := request.MultipartForm.File["upload"][0] //表示从upload字段获取值
file, err := fileHeader.Open()
if err == nil {
data, err := ioutil.ReadAll(file)
if err == nil {
fmt.Fprintln(writer, string(data))
}
}

}

字如其意,非常简单。但是这部分代码仍然有改进的空间,比如使用FormFile

FormFile()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func process(writer http.ResponseWriter, request *http.Request) {
//request.ParseMultipartForm(1024) //1024表示一次性加载到内存的最大字节数
//fileHeader := request.MultipartForm.File["upload"][0] //表示从upload字段获取值
//file, err := fileHeader.Open()

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 方法

WriteHeader 方法接收一个整数类型(HTTP 状态码)作为参数,并把它作为 HTTP 响应的状态码返回
如果该方法没有显式调用,那么在第一次调用 Write 方法前,会隐式的调用 WriteHeader(http.StatusOK),所以 WriteHeader 主要用来发送错误类的 HTTP 状态码
调用完 WriteHeader 方法之后,仍然可以写入到 ResponseWriter,但无法再修改 header 了

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

使用模板引擎
  1. 解析模板源(可以是字符串或模板文件),从而创建一个解析好的 模板的 struct
  2. 执行解析好的模板,并传入 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") //去解析一个模板文件,一个也在这个包下的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{}
//value 是函数
//可以有任意数量的参数
//返回单个值的函数或返回一个值+一个错误的函数

/*
创建一个 FuncMap(map 类型)。
key 是函数名
value 就是函数
把 FuncMap 附加到模板
*/

常见用法: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。

https://www.jianshu.com/p/7e745fefb8af

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 main

import (
"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
}

//操作一:执行数据操作语句
/*
sql:="insert into stu values (2,'berry')"
result,_:=db.Exec(sql) //执行SQL语句
n,_:=result.RowsAffected(); //获取受影响的记录数
fmt.Println("受影响的记录数是",n)
*/

//操作二:执行预处理
/*
stu:=[2][2] string{{"3","ketty"},{"4","rose"}}
stmt,_:=db.Prepare("insert into stu values (?,?)") //获取预处理语句对象
for _,s:=range stu{
stmt.Exec(s[0],s[1]) //调用预处理语句
}
*/

//操作三:单行查询
/*
var id,name string
rows:=db.QueryRow("select * from stu where id=4") //获取一行数据
rows.Scan(&id,&name) //将rows中的数据存到id,name中
fmt.Println(id,"--",name)
*/

//操作四:多行查询
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 controller

import "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 controller

import "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 controller

//RegisterRoutes 用于注册
func 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) //现在JSON就在这个body里边了,并且需要进行解码
company := Company{}
err := dec.Decode(&company) //将密码解码到company这个变量里边
if err != nil { //如果它不为空
log.Println(err.Error()) //打印错误
writer.WriteHeader(http.StatusInternalServerError) //500error
return
}
//否则将客户端传过来的数据转化为json重新返回给客户端
enc := json.NewEncoder(writer)
err = enc.Encode(company)
if err != nil {
log.Println(err.Error()) //打印错误
writer.WriteHeader(http.StatusInternalServerError) //500error
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
//handler 如果是 nil:DefaultServeMux

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) {
// 在 next handler 之前做一些事情
m.Next.ServeHTTP(w, r)
// 在 next handler 之后做一些事情
}

途中可以定义中间件需要做的事情。

中间件的用途

  1. Logging,日志
  2. 安全,身份认证
  3. 请求超时,减少资源消耗
  4. 响应压缩,提升效率

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package middleware

import "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) //401未授权
}
}

这个是自定义的中间件类型,接着是主函数:

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

有这么两种方式去处理上下文

看看里面实际上是什么:

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 middleware

import (
"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) //修改context的超时判断
request.WithContext(ctx) //用我们自定义的context去代替
ch := make(chan struct{}) //意图在于,如果我们请求能够在3秒内完成的话,这个chan就会收到一个信号
go func() { //使用goroutine
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 main

import (
"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",
}
//time.Sleep(4*time.Second)
//测试用
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 8760h0m0s)
-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 main

import (
"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的情况下:

  1. 请求 header+body
  2. 响应 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 controller

import "net/http"

func registerHomeRoutes() {
http.HandleFunc("/home", func(writer http.ResponseWriter, request *http.Request) {
if pusher, ok := writer.(http.Pusher); ok { //先做类型断言,看看存不存在push
pusher.Push("/css/app.css", &http.PushOptions{ //如果为真,则说明支持server push
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 model

import "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 model

import "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!")
}
}
//如果测试通过,则会直接显示PASS
//如果测试不通过。结果如下:
/*
UN TestCompany_GetCompanyType
company_test.go:15: this is others!
--- FAIL: TestCompany_GetCompanyType (0.00s)

FAIL
*/

这便是测试的结果。

测试 Controller 层

  1. 为了尽量保证单元测试的隔离性,测试不要使用例如数据库、外部API、文件系统等外部资源。
  2. 模拟请求和响应
  3. 需要使用 net/http/httptest 提供的功能

这里有几个函数值得关注:

NewRequest 函数
1
2
3
4
5
6
func NewRequest(method, url string, body io.Reader) (*Request, error) 

//method:HTTP Method
//url:请求的 URL
//body:请求的 Body
//返回的 *Request 可以传递给 handler 函数
ResponseRecorder
1
2
3
4
5
6
7
8
type ResponseRecorder {
Code int // 状态码 200、500…
HeaderMap http.Header // 响应的 header
Body *bytes.Buffer // 响应的 body
Flushed bool // 缓存是否被 flush 了
}
//用来捕获从 handler 返回的响应,只是做记录
//可以用于测试断言
例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package controller

import (
"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",
}
//收到请求后使用JSON编码,并写到响应里面,并返回回去
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 controller

import (
"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) //传入Handler

result, _ := ioutil.ReadAll(w.Result().Body) //获取响应,提取body

c := model.Company{}
json.Unmarshal(result, &c) //解码并重新放入c这个变量里边

if c.ID != 123 { //然后判断
t.Error("this is a failed")
}
}

//通过就显示PASS
//否则显示:
/*
=== RUN TestHandleCompanyCorrect
company_test.go:23: this is a failed
--- FAIL: TestHandleCompanyCorrect (0.00s)

FAIL
*/

这还是比较方便的

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 log

//因为标准库也有一个log,所以可以起一个别名
import (
"io/ioutil"
stlog "log"
"net/http"
"os"
)

var log *stlog.Logger

//目的在于把日志写入文件系统
type fileLog string

func (fl fileLog) Write(data []byte) (int, error) {
//首先要打开文件,才能写入
//fl文件路径,os...表示没有则创造,只写,只附加,
f, err := os.OpenFile(string(fl), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600)
if err != nil {
return 0, err
}
//defer表示程序最后一定会执行的,这句的意思是必须把文件关闭
defer f.Close()
return f.Write(data)
}

//最后把log指向某个文件地址
func Run(destination string) {
log = stlog.New(fileLog(destination), "go", stlog.LstdFlags)
}

//注册一个Handler
func RegisterHandler() {
http.HandleFunc("/log", func(writer http.ResponseWriter, request *http.Request) {
switch request.Method {
case http.MethodPost: //如果请求是post
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 service

import (
"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具有取消的功能
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 main

import (
"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,
)
//如果有错误,就先执行标准库的log打印出结果
if err != nil {
stlog.Fatalln(err)
}
//接需要等待ctx的信号
//如果在启动服务器的时候出现了错误
//或者在按下任意键停止后,就会发送信号
<-ctx.Done()
//接受到信号后,就会继续
fmt.Println("停止服务")
}

测试

接着启动服务,并使用postman进行测试:

1
http://localhost:4000/log

输入任意文字,就会看到在根目录下,有一个日志文件生成了。

服务注册的基本逻辑

注册中心

首先需要去尝试编写一下,一个可以将服务都注册进去的注册中心。

先写一个数据结构,注册中心:

1
2
3
4
5
6
7
8
9
10
11
12
13
package registry

//注册中心
type 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 registry

import (
"encoding/json"
"log"
"net/http"
"sync"
)

const ServerPort = ":3000"
const ServicesURL = "http://localhost" + ServerPort + "/services"

type registry struct {
//这个slice,是动态变化的,而且多个线程可能会并发的进行访问,
//为了保证线程安全,需要加锁。
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),
}

//创建一个web服务
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 main

import (
"context"
"fmt"
"go_distributed_system_study/registry"
"log"
"net/http"
)

func main() {
//将之前的处理逻辑注册进去
http.Handle("/services", registry.RegistryService{})

//接下来的逻辑一样,需要有取消功能,当然其实你在ide中能直接打断,但在大型服务中,每秒都要运行。还是需要自定义取消功能的
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 registry

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

//这个函数的目的是给web service发送一个post请求
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 service

import (
"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)

//启动web服务之后注册:
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具有取消的功能
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 main

import (
"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,
)
//如果有错误,就先执行标准库的log打印出结果
if err != nil {
stlog.Fatalln(err)
}
//接需要等待ctx的信号
//如果在启动服务器的时候出现了错误
//或者在按下任意键停止后,就会发送信号
<-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 registry

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

const ServerPort = ":3000"
const ServicesURL = "http://localhost" + ServerPort + "/services"

type registry struct {
//这个slice,是动态变化的,而且多个线程可能会并发的进行访问,
//为了保证线程安全,需要加锁。
registrations []Registration
mutex *sync.Mutex
}

//在增加服务的时候是需要加锁的
//add方法表示增加服务
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),
}

//创建一个web服务
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 registry

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

//这个函数的目的是给web service发送一个post请求
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))) //把string转化为slice
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 service

import (
"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)

//启动web服务之后注册:
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具有取消的功能
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 grades

import (
"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))
}

//寻找学生 by ID
type Students []Student

func (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 grades

func 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 grades

import (
"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{}

//所以一个链接需要处理很多情况
// /students 分割后长度是2
// /students/{id} 分割后长度是3
// /students/{id}/grades 分割后长度是4
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
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()

//将学生信息全部转为JSON,返回给data,最后写入
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)
}

//根据ID,搜索学生信息
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
}
//上面的逻辑一样的,没什么好说

//接着要从URL中获取到要传达的学生成绩信息
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) //201

data, err := sh.toJSON(g)
if err != nil {
log.Println(err)
return
}
w.Header().Add("Content-Type", "applicaiton/json")
w.Write(data)
}

//转化为JSON
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 registry

//注册中心
type 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 main

import (
"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 registry

//注册中心
type Registration struct {
ServiceName ServiceName
ServiceURL string

//该服务所依赖的其他服务。用slice去保存
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 registry

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

const ServerPort = ":3000"
const ServicesURL = "http://localhost" + ServerPort + "/services"

type registry struct {
//这个slice,是动态变化的,而且多个线程可能会并发的进行访问,
//为了保证线程安全,需要加锁。
registrations []Registration
mutex *sync.RWMutex
}

//在增加服务的时候是需要加锁的
//add方法表示增加服务
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]...)
//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),
}

//创建一个web服务
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
//log服务会向多个服务提供服务
type providers struct {
//服务与服务的URL
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 registry

import (
"bytes"
"encoding/json"
"fmt"
"log"
"math/rand"
"net/http"
"net/url"
"sync"
)

//这个函数的目的是给web service发送一个post请求
func RegisterService(r Registration) error {

//服务注册中心要向URL更新一些信息
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
}
//fmt.Printf("收到更新: %v\n", p)
prov.Update(p)
}

//结束服务
func ShutdownService(url string) error {
req, err := http.NewRequest(
http.MethodDelete, ServicesURL,
bytes.NewBuffer([]byte(url))) //把string转化为slice
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
}

//log服务会向多个服务提供服务
type providers struct {
//服务与服务的URL
services map[ServiceName][]string
//互斥锁
mutex *sync.RWMutex
}

//接受到patch的时候,需要进行更新,
var prov = providers{
services: make(map[ServiceName][]string),
mutex: new(sync.RWMutex),
}

//实现
func (p *providers) Update(pat patch) {
//对传进来的patch更新provider
p.mutex.Lock()
defer p.mutex.Unlock()

//新增的情况
for _, patchEntry := range pat.Added {
//如果这个服务名目前还不存在,就创建新的slice
if _, ok := p.services[patchEntry.Name]; !ok {
p.services[patchEntry.Name] = make([]string, 0)
}
//如果存在的话,就在值后边附加URL
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:]...)
}
}
}
}
}

//然后还需要,根据服务的名称来找到它所依赖服务的url
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
}

//由于这个get方法是私有的,对外再套一个函数:
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 log

import (
"bytes"
"fmt"
"go_distributed_system_study/registry"
"net/http"

stlog "log"
)

//写日志,把日志写到server
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 main

import (
"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)
}

//在服务启动之后使用log服务
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 main

import (
"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,
)
//如果有错误,就先执行标准库的log打印出结果
if err != nil {
stlog.Fatalln(err)
}
//接需要等待ctx的信号
//如果在启动服务器的时候出现了错误
//或者在按下任意键停止后,就会发送信号
<-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 registry

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

const ServerPort = ":3000"
const ServicesURL = "http://localhost" + ServerPort + "/services"

type registry struct {
//这个slice,是动态变化的,而且多个线程可能会并发的进行访问,
//为了保证线程安全,需要加锁。
registrations []Registration
mutex *sync.RWMutex
}

//在增加服务的时候是需要加锁的
//add方法表示增加服务
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()

//看看服务的依赖在patch里面存不存在
//对已经注册的服务循环遍历
for _, reg := range r.registrations {
//并发的发出通知
go func(reg Registration) {
//对服务所需要的服务进行循环
for _, reqService := range reg.RequiredServices {
p := patch{Added: []patchEntry{}, Removed: []patchEntry{}}
//标志位,为TRUE表示有需要更新的地方
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]...)
//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),
}

//创建一个web服务
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]...)
//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的微服务思想,同时也是分布式的一种类型。所以说分布式也没什么神奇之处,最核心的一处在于:把本地接口转化为了网络接口。能够理解这一过程,也就理解了分布式的思想。