1. go-micro体验

 1package main
 2
 3import (
 4	"fmt"
 5	"github.com/micro/go-micro/web"
 6	"net/http"
 7)
 8
 9func main(){
10	service := web.NewService(web.Address("127.0.0.1:8000"))
11	// 使用http原生的handlefunc
12	service.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
13		w.Write([]byte("hello world"))
14	})
15	err := service.Run()
16	if err != nil {
17		fmt.Println(err)
18	}
19}

测试结果

1curl http://127.0.0.1:8000
2hello world

2. 使用web框架gin

 1package main
 2
 3import (
 4	"github.com/gin-gonic/gin"
 5	"github.com/micro/go-micro/web"
 6	"net/http"
 7)
 8
 9func main() {
10	r := gin.Default()
11	r.Handle("GET", "/", func(c *gin.Context) {
12		c.JSON(http.StatusOK, gin.H{
13			"code": http.StatusOK,
14		})
15	})
16	service := web.NewService(
17		web.Name("demo_service"),
18		web.Address("127.0.0.1:8000"),
19		web.Handler(r),
20		)
21	service.Run()
22}

测试结果

1curl http://127.0.0.1:8000
2{"code":200}

3. 使用consul作为服务发现

这里需要你的consul是正常运行的,不然连接不上consul程序会直接退出。

最简单的方法启动单机版本的consul

1consul agent -dev

main.go

 1package main
 2
 3import (
 4	"github.com/gin-gonic/gin"
 5	"github.com/micro/go-micro/registry"
 6	"github.com/micro/go-micro/web"
 7	"github.com/micro/go-plugins/registry/consul"
 8	"net/http"
 9)
10
11// consul 服务注册
12func main(){
13	consulReg := consul.NewRegistry(
14		registry.Addrs("127.0.0.1:8500"))
15
16	r := gin.Default()
17	r.Handle("GET", "/", func(c *gin.Context) {
18		c.JSON(http.StatusOK, gin.H{
19			"code": http.StatusOK,
20		})
21	})
22	service := web.NewService(
23		web.Address("127.0.0.1:8000"),
24		web.Handler(r),
25		web.Registry(consulReg),
26	)
27	service.Run()
28}

运行之后服务并没有不一样,只是可以通过consul发现到这个服务.

consul-服务发现

4. 调用函数返回json数据

main.go

 1package main
 2
 3import (
 4	"github.com/gin-gonic/gin"
 5	"github.com/micro/go-micro/registry"
 6	"github.com/micro/go-micro/web"
 7	"github.com/micro/go-plugins/registry/consul"
 8	"net/http"
 9)
10
11// 调用函数返回json数据
12func main(){
13
14	consulReg := consul.NewRegistry(
15		registry.Addrs("127.0.0.1:8500"))
16
17	r := gin.Default()
18	// 路由分组
19	v1Group := r.Group("/v1")
20	{
21		v1Group.Handle("GET", "/prods", func(c *gin.Context) {
22			c.JSON(http.StatusOK, NewProdList(5))
23		})
24	}
25
26	service := web.NewService(
27		web.Name("ProdSrv"),
28		web.Address("127.0.0.1:8000"),
29		web.Handler(r),
30		web.Registry(consulReg),
31	)
32	service.Run()
33}

prod.go

 1package main
 2
 3import "strconv"
 4
 5type ProdModel struct {
 6	ProdId int
 7	ProdName string
 8}
 9
10func NewProd(id int, name string) *ProdModel{
11	return &ProdModel{ProdId:id, ProdName:name}
12}
13
14func NewProdList(n int) []*ProdModel{
15	ret := make([]*ProdModel,0)
16	for i:=0 ; i<n ; i++ {
17		ret = append(ret, NewProd(100+i, "Prod"+strconv.Itoa(i)))
18	}
19	return  ret
20}

运行

1go mod init
2go run main.go prod.go

访问测试

1curl http://127.0.0.1:8000/v1/prods
2[{"ProdId":100,"ProdName":"Prod0"},{"ProdId":101,"ProdName":"Prod1"},{"ProdId":102,"ProdName":"Prod2"},{"ProdId":103,"ProdName":"Prod3"},{"ProdId":104,"ProdName":"Prod4"}]

micro-prod

5. consul服务发现

前提是要启动consul并运行前面的ProdSrv服务

 1package main
 2
 3import (
 4	"github.com/micro/go-micro/client/selector"
 5	"github.com/micro/go-micro/registry"
 6	"github.com/micro/go-plugins/registry/consul"
 7	"log"
 8)
 9
10// consul 服务发现 selector随机选择
11func main() {
12	// consul连接句柄
13	consulReg := consul.NewRegistry(
14		registry.Addrs("127.0.0.1:8500"))
15
16	// 获取服务
17	getService, err := consulReg.GetService("ProdSrv")
18	if err != nil {
19		log.Fatalf("get service failed, err:%v\n", err)
20		return
21	}
22
23	next := selector.Random(getService)
24
25	node, err := next()
26	if err != nil {
27		log.Fatalln(err)
28		return
29	}
30
31	// 打印服务节点信息
32	log.Println(node.Id, node.Address, node.Metadata)
33}
34

测试结果

12020-03-20 14:37:44.934162 I | 03f124a4-eee9-4349-890d-ed35b49c872a 127.0.0.1:8000 map[]

6. 改造成通过命令行参数启动

为了方便测试,需要在同一个服务同一台机器上运行多个,这个实现起来很简单。初始化之后增加Init(),并取消固定端口设置。然后运行的时候通过--server_address指定端口或者MICRO_SERVER_ADDRESS环境变量进行设置。

 1func main(){
 2
 3	consulReg := consul.NewRegistry(
 4		registry.Addrs("127.0.0.1:8500"))
 5
 6	r := gin.Default()
 7	// 路由分组
 8	v1Group := r.Group("/v1")
 9	{
10		v1Group.Handle("GET", "/prods", func(c *gin.Context) {
11			c.JSON(http.StatusOK, NewProdList(5))
12		})
13	}
14
15	service := web.NewService(
16		web.Name("ProdSrv"),
17		web.Handler(r),
18		web.Registry(consulReg),
19	)
20
21	service.Init()
22	service.Run()
23}

例如同时运行2份

1go run main.go prod.go --server_address  127.0.0.1:8000
2go run main.go prod.go --server_address  127.0.0.1:8001

7. 使用轮询调度算法调度服务

前面的服务已经运行了2份副本,我们现在使用轮询调度算法来调度

 1package main
 2
 3import (
 4	"github.com/micro/go-micro/client/selector"
 5	"github.com/micro/go-micro/registry"
 6	"github.com/micro/go-plugins/registry/consul"
 7	"log"
 8	"time"
 9)
10
11// consul 通过轮询获取服务
12// 并测试其中一个节点关闭时不影响业务
13func main() {
14	// consul连接句柄
15	consulReg := consul.NewRegistry(
16		registry.Addrs("127.0.0.1:8500"))
17	for{
18		// 获取服务
19		getService, err := consulReg.GetService("ProdSrv")
20		if err != nil {
21			log.Fatalf("get service failed, err:%v\n", err)
22			return
23		}
24
25		next := selector.RoundRobin(getService)
26
27		node, err := next()
28		if err != nil {
29			log.Fatalln(err)
30			return
31		}
32
33		log.Println(node.Address)
34		time.Sleep(time.Second)
35	}
36}

测试输出结果

12020-03-20 14:48:47.048770 I | 127.0.0.1:8000
22020-03-20 14:48:48.057258 I | 127.0.0.1:8000
32020-03-20 14:48:49.063769 I | 127.0.0.1:8001
42020-03-20 14:48:50.072566 I | 127.0.0.1:8001
52020-03-20 14:48:51.081194 I | 127.0.0.1:8000
62020-03-20 14:48:52.087654 I | 127.0.0.1:8000
72020-03-20 14:48:53.092245 I | 127.0.0.1:8001
82020-03-20 14:48:54.097805 I | 127.0.0.1:8000
92020-03-20 14:48:55.103272 I | 127.0.0.1:8000

8. 服务的基本调用

 1package main
 2
 3import (
 4	"github.com/micro/go-micro/client/selector"
 5	"github.com/micro/go-micro/registry"
 6	"github.com/micro/go-plugins/registry/consul"
 7	"io/ioutil"
 8	"log"
 9	"net/http"
10)
11
12// 基本方式调用后端服务
13func callAPI(addr, path, method string) (string, error) {
14	req, _ := http.NewRequest(method, "http://"+addr+path, nil)
15	client := http.DefaultClient
16	res, err := client.Do(req)
17	if err != nil {
18		return "", err
19	}
20	defer res.Body.Close()
21	buf, err := ioutil.ReadAll(res.Body)
22	if err != nil {
23		return "", err
24	}
25	return string(buf), nil
26}
27
28func main() {
29	// consul连接句柄
30	consulReg := consul.NewRegistry(
31		registry.Addrs("127.0.0.1:8500"))
32
33	// 获取服务
34	getService, err := consulReg.GetService("ProdSrv")
35	if err != nil {
36		log.Fatalf("get service failed, err:%v\n", err)
37		return
38	}
39
40	next := selector.RoundRobin(getService)
41
42	node, err := next()
43	if err != nil {
44		log.Fatalln(err)
45		return
46	}
47
48	res, err := callAPI(node.Address, "/v1/prods", "GET")
49	if err != nil {
50		log.Fatal(err)
51		return
52	}
53	log.Println(res)
54}

测试结果

12020-03-20 14:56:22.368768 I | [{"ProdId":100,"ProdName":"Prod0"},{"ProdId":101,"ProdName":"Prod1"},{"ProdId":102,"ProdName":"Prod2"},{"ProdId":103,"ProdName":"Prod3"},{"ProdId":104,"ProdName":"Prod4"}]

9. 使用http插件调用后端服务

这里对直接服务端的返回进行了改造,data返回具体的数据,数据请求方式换成POST

1v1Group := r.Group("/v1")
2	{
3		v1Group.Handle("POST", "/prods", func(c *gin.Context) {
4			c.JSON(http.StatusOK,gin.H{
5				"data": NewProdList(2),
6			} )
7		})
8	}

client.go

 1package main
 2
 3import (
 4	"context"
 5	"github.com/micro/go-micro/client"
 6	"github.com/micro/go-micro/client/selector"
 7	"github.com/micro/go-micro/registry"
 8	"github.com/micro/go-plugins/registry/consul"
 9	"log"
10	"github.com/micro/go-plugins/client/http"
11)
12
13func callAPI(s selector.Selector){
14	myClient := http.NewClient(
15		client.Selector(s),
16		client.ContentType("application/json"),
17		)
18	req := myClient.NewRequest("ProdSrv","/v1/prods",map[string]string{})
19	var rsp map[string]interface{}
20	err := myClient.Call(context.Background(), req, &rsp)
21	if err != nil {
22		log.Fatal(err)
23		return
24	}
25	log.Println(rsp["data"])
26}
27
28
29func main() {
30	// consul连接句柄
31	consulReg := consul.NewRegistry(
32		registry.Addrs("127.0.0.1:8500"))
33
34	sel := selector.NewSelector(
35		selector.Registry(consulReg),
36		selector.SetStrategy(selector.RoundRobin),
37		)
38	callAPI(sel)
39
40}

测试结果

12020-03-20 15:00:03.329088 I | [map[ProdId:100 ProdName:Prod0] map[ProdId:101 ProdName:Prod1]]

10. 带参数调用服务

上面的示例是直接请求不带参数的,真实的应用场景往往都是根据不同的请求参数来返回想要的结果。这就需要对服务端进行改造。

 1func main(){
 2
 3	consulReg := consul.NewRegistry(
 4		registry.Addrs("127.0.0.1:8500"))
 5
 6	r := gin.Default()
 7	// 路由分组
 8	v1Group := r.Group("/v1")
 9	{
10		v1Group.Handle("POST", "/prods", func(c *gin.Context) {
11			var pr ProdsRequest
12			// 给默认值
13			err := c.Bind(&pr)
14			if err != nil || pr.Size <=0 {
15				log.Println(err)
16				pr = ProdsRequest{Size: 2}
17			}
18			c.JSON(http.StatusOK,gin.H{
19				"data": NewProdList(pr.Size),
20			} )
21		})
22	}
23
24	service := web.NewService(
25		web.Name("ProdSrv"),
26		web.Handler(r),
27		web.Registry(consulReg),
28	)
29
30	service.Init()
31	service.Run()
32}
33

prod.go

 1package main
 2
 3import "strconv"
 4
 5type ProdModel struct {
 6	ProdId int
 7	ProdName string
 8}
 9
10type ProdsRequest struct {
11	Size int `form:"size"`
12}
13
14func NewProd(id int, name string) *ProdModel{
15	return &ProdModel{ProdId:id, ProdName:name}
16}
17
18func NewProdList(n int) []*ProdModel{
19	ret := make([]*ProdModel,0)
20	for i:=0 ; i<n ; i++ {
21		ret = append(ret, NewProd(100+i, "Prod"+strconv.Itoa(i)))
22	}
23	return  ret
24}

client.go

 1package main
 2
 3import (
 4	"context"
 5	"github.com/micro/go-micro/client"
 6	"github.com/micro/go-micro/client/selector"
 7	"github.com/micro/go-micro/registry"
 8	"github.com/micro/go-plugins/registry/consul"
 9	"log"
10	"github.com/micro/go-plugins/client/http"
11)
12
13// consul 通过轮询获取服务
14// 使用插件 调用http api 带参数调用
15func callAPI(s selector.Selector){
16	myClient := http.NewClient(
17		client.Selector(s),
18		client.ContentType("application/json"),
19		)
20	req := myClient.NewRequest("ProdSrv","/v1/prods",map[string]interface{}{"size": 4})
21	var rsp map[string]interface{}
22	err := myClient.Call(context.Background(), req, &rsp)
23	if err != nil {
24		log.Fatal(err)
25		return
26	}
27	log.Println(rsp["data"])
28}
29
30
31func main() {
32	// consul连接句柄
33	consulReg := consul.NewRegistry(
34		registry.Addrs("127.0.0.1:8500"))
35
36	sel := selector.NewSelector(
37		selector.Registry(consulReg),
38		selector.SetStrategy(selector.RoundRobin),
39		)
40	callAPI(sel)
41
42}

测试结果上面我填的4,返回4条记录

12020-03-20 15:08:27.950250 I | [map[ProdId:100 ProdName:Prod0] map[ProdId:101 ProdName:Prod1] map[ProdId:102 ProdName:Prod2] map[ProdId:103 ProdName:Prod3]]

11. 引入protobuf生成请求响应模型

新建一个models目录用于存放protobuf生成的文件,在models目录下新建protos目录用于存放proto文件。

proto文件内容如下:

 1syntax = "proto3";
 2
 3// 生成的包名
 4package models;
 5
 6// 商品模型
 7message ProdModel{
 8    int32 ProdId = 1;
 9    string ProdName = 2;
10}
11
12// 请求
13message ProdRequest{
14    int32 size = 1;
15}
16
17// 响应
18message ProdListResponse{
19    repeated ProdModel data = 1;
20}

使用protoc生成

1cd models/protos && \
2protoc --micro_out=../ --go_out=../ *.proto && cd -

修改调用端的服务,改成通过生成的models包下的进行调用

 1package main
 2
 3import (
 4	"context"
 5	"github.com/micro/go-micro/client"
 6	"github.com/micro/go-micro/client/selector"
 7	"github.com/micro/go-micro/registry"
 8	"github.com/micro/go-plugins/registry/consul"
 9	"log"
10	"github.com/micro/go-plugins/client/http"
11	"micro_demo/models"
12)
13
14// consul 通过轮询获取服务
15// 调用http api 引入protobuf生成请求响应模型
16func callAPI(s selector.Selector){
17	myClient := http.NewClient(
18		client.Selector(s),
19		client.ContentType("application/json"),
20		)
21	req := myClient.NewRequest("ProdSrv","/v1/prods",
22		models.ProdRequest{Size:6})
23	var rsp models.ProdListResponse
24	err := myClient.Call(context.Background(), req, &rsp)
25	if err != nil {
26		log.Fatal(err)
27		return
28	}
29	log.Println(rsp.GetData())
30}
31
32func main() {
33	// consul连接句柄
34	consulReg := consul.NewRegistry(
35		registry.Addrs("127.0.0.1:8500"))
36
37	sel := selector.NewSelector(
38		selector.Registry(consulReg),
39		selector.SetStrategy(selector.RoundRobin),
40		)
41	callAPI(sel)
42
43}

效果跟上面的一样。

12. 处理参数模型中json tag不一致的问题

比如之前定义的ProdId,ProdName可能在json中不叫这个名字,可能是pid,pname等等,这时候需要修改protobuf生成的json tag,但是因为是自动生成的,下次改动了proto文件重新生成又覆盖掉了。这个时候我们需要用到第三方包github.com/favadi/protoc-go-inject-tag

处理方式

  1. proto文件中加入// @inject_tag:标记

     1syntax = "proto3";
     2
     3package models;
     4
     5message ProdModel{
     6    // @inject_tag: json:"pid"
     7    int32 ProdId = 1;
     8    // @inject_tag: json:"pname"
     9    string ProdName = 2;
    10}
    11
    12message ProdRequest{
    13    int32 size = 1;
    14}
    15
    16message ProdListResponse{
    17    repeated ProdModel data = 1;
    18}
    
  2. 生成go文件,需要带上protoc-go-inject-tag参数

    1cd models/protos && \
    2protoc --micro_out=../ --go_out=../ prods.proto && \
    3protoc-go-inject-tag --input=../prods.pb.go
    4cd -
    

这个时候就能愉快的调用后端服务了。