优秀的编程知识分享平台

网站首页 > 技术文章 正文

golang 微服务(2) Restful 服务(golang 服务端)

nanyue 2024-11-02 12:18:59 技术文章 4 ℃

Restful 服务

我们之前搭建了一个简单 Http 服务,接下来我们更进一步,restful 服务,有关 restful 表示 represent a representational state transfer ,大部分人因为 restful 就是在 http 请求中使用数据格式 json 格式。restful 更像一种模式一种规范,通过实现 restful 规范的 http 请求无论设计人员和开发人员更容易理解和使用,也可以说一种协议,遵循 restful 定义规则来设计出会说话 api。将增删改查这些动作有 http 请求 method (作为动词)来表意,而不是通过路由上来定义。
在 restful 请求和返回数据格式都使用 json 格式,json 好处是便于序列化和反序列化。


图玩具

今天我们分享不想玩具,而是一个真正产品级别项目,希望大家有耐心给我一起 coding 下去,同时我相信您也会不虚此行。我们先定义数据,还是老规矩先从简单我们在文件定义数据,而不是上来就引入数据库结构如下,我们现在最热衷两门语言。


定义数据结构

这里我们定义 Tut(教程)作为数据,数据有以下字段,分别是ID,教程名称、描述和价格等。


type Tut struct {
	ID          int
	Name        string
	Description string
	Price       float32
	Category    string
	CreatedOn   string
	UpdatedOn   string
	DeletedOn   string
}

为了演示 resetful 构建,我们暂时模拟出一些数据,并没有引入数据库,这是因为今天主角是 Restful 服务而非数据库存储。


var tutList = []*Tut{
	&Tut{
		ID:          1,
		Name:        "golang",
		Description: "golang basic",
		Price:       2.45,
		Category:    "web",
		CreatedOn:   time.Now().UTC().String(),
		UpdatedOn:   time.Now().UTC().String(),
	},
	&Tut{
		ID:          2,
		Name:        "rust",
		Description: "rust blockchain",
		Price:       1.99,
		Category:    "blockchain",
		CreatedOn:   time.Now().UTC().String(),
		UpdatedOn:   time.Now().UTC().String(),
	},
}

然后就定义路由的处理程序来将数据返回给客户端。


创建 Tuts 结构体

我们这里定义 Tuts 结构体,这个结构体对应 Tut 结构体(类)的集合,可以理解为表,我们对 Tut 集合操作都是由,


type Tuts struct {
	l *log.Logger
}

随着开发时间,接触业务逐渐复杂,越来越感觉到日志重要性,以及设计好的日志系统对运维人员会有多大帮助,好的日志系统是健壮系统的一个保证。结构体中我们将日志作为结构体属性由外部传入。


定创建 Tuts 的方法


func NewTuts(l *log.Logger) *Tuts {
	return &Tuts{l}
}


实现 ServeHTTP 方法

让结构体实现了 ServeHTTP 方法,从而结构体就是 Handler 接口类型,这点一个点如果是从面向对象语言走出来的程序员还需要花一些时间和精力来理解。一旦理解就发现这种语言设计就是顺理成章,设计巧妙。


func (t *Tuts) ServeHTTP(rw http.ResponseWriter, r *http.Request) {

}

接下来我们创建 NewServeMux 替换 http 提供默认 ServeMux 路由管理器,使用 NewTuts 创建一个路由 Handler 因为 Tuts 结构体实现了 ServeHTTP 方法所以就可以做完参数传入到 Handle 方法来处理路由 / 的请求。


    tutsHandler := handlers.NewTuts(l)
    goodbyeHandler := handlers.NewGoodbye(l)

    sm := http.NewServeMux()
    sm.Handle("/", tutsHandler)
    sm.Handle("/goodbye", goodbyeHandler)


序列化

使用 json 包提供 Marshal 方法来对于结构体进行序列化为 json 格式,这个方法会返回一个json格式字符串和一个错误 error 值,我们现在已经熟悉了 go 编程 style 我们需要 error 进行处理。


func (t *Tuts) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
	lt := data.GetTuts()
	//Marshal 返回值 ([]byte, error),
	d, err := json.Marshal(lt)
	if err != nil {
		http.Error(rw, "Unable to marshal json", http.StatusInternalServerError)
	}
	rw.Write(d)
}

在终端运行 curl 命令来访问服务查看 json 解析是否已经将结构体解析 json 格式


curl localhost:4600


[{"ID":1,"Name":"golang","Description":"golang basic","Price":2.45,"Category":"web","CreatedOn":"2020-05-01 03:42:41.044691 +0000 UTC","UpdatedOn":"2020-05-01 03:42:41.044693 +0000 UTC","DeletedOn":""},{"ID":2,"Name":"rust","Description":"rust blockchain","Price":1.99,"Category":"blockchain","CreatedOn":"2020-05-01 03:42:41.044694 +0000 UTC","UpdatedOn":"2020-05-01 03:42:41.044696 +0000 UTC","DeletedOn":""}]

在返回数据项 ID 或者 Name 这些字段首字母都是大写的,从 javascript 和 java 走出来的程序员会有点怪怪感觉,可能 csharp 就不会有这样感觉,因为csharp 方法名就是大写开头,我们没有接触过 csharp,在 go 语言字段和方法名大小开头表示共有属性或方法,我们想要解析为小写(遵循驼峰命名规范)


type Tut struct {
	ID          int     `json:"id"`
	Name        string  `json:"name"`
	Description string  `json:"description"`
	Price       float32 `json:"price"`
	Category    string  `json:"category"`
	CreatedOn   string  `json:"-"`
	UpdatedOn   string  `json:"-"`
	DeletedOn   string  `json:"-"`
}

再次通过终端输出,查看效果。


[{"id":1,"name":"golang","description":"golang basic","price":2.45,"category":"web"},{"id":2,"name":"rust","description":"rustblockchain","price":1.99,"category":"blockchain"}](


序列化和反序列化

在 go 语言中 json 包具有 Encode 和 Decode 包, 我们可以自己通过编码和解码器来自己实现一个 json 序列化和反序列方法。为什么我们要自己实现 json 解析器呢,这是为了以后通过提高效率,通过协程来处理一些大型数据结构的解析。


func NewEncoder(w io.Writer) *Encoder

调用 NewEncoder 的 Encoder ,NewEncoder 需要传入一个 io.Writer 接口,我们 http 方法就可以对的一个实现了 io.Writer 接口的


func (enc *Encoder) Encode(v interface{}) error

Encode 将结构体编码为符合 JSON 格式字节流数据,我们在处理程序可以得到 io.Writer


//编码器(序列化)
func (t *Tuts) ToJSON(w io.Writer) error {
	//获取json解码器
	e := json.NewEncoder(w)
	//调用解码器的解码方法来对 tuts 对象进行解码
	return e.Encode(t)
}

//解码器(反序列化)
func (t *Tut) FromJSON(r io.Reader) error {
	e := json.NewDecoder(r)
	return e.Decode(t)
}


实现 Restful 服务


func (t *Tuts) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
	if r.Method == http.MethodGet {
		t.getTuts(rw, r)
		return
	}
	//处理更新数据 POST
	if r.Method == http.MethodPost {
		t.addTut(rw, r)
		return
	}
	//处理 PUT 请求
	if r.Method == http.MethodPut {
		t.l.Println("Handle PUT Tuts")
		//正则解析器
		reg := regexp.MustCompile(`/([0-9]+)`)
		g := reg.FindAllStringSubmatch(r.URL.Path, -1)

		if len(g) != 1 {
			http.Error(rw, "Invalid URI", http.StatusBadRequest)
			return
		}

		if len(g[0]) != 2 {
			http.Error(rw, "Invalid URI", http.StatusBadRequest)
			return
		}
		idString := g[0][1]
		id, err := strconv.Atoi(idString)

		if err != nil {
			http.Error(rw, "Invalid URI", http.StatusBadRequest)
			return
		}
		t.l.Println("got id", id)

		t.updateTuts(id, rw, r)
	}

	rw.WriteHeader(http.StatusMethodNotAllowed)
}


r.Method == http.MethodPost

通过判断请求方式,来进行不同的动作,添加、更新和查询数据。我们是通过 http method 动词来表意进行区分,而不是在 url 中显示指明要进行动作。这里看起来 code 不那么优雅,接下来使用 Gollira 进行重构。在 PUT 中我们要做的是在如路由localhost:4600/id 来根据 id 对来更新符合条件 id 的数据进行更新。所以这里要相对复杂一下,PUT 做的更新动作,更新会分解为查找和替换两个动作,所以相对复杂一些。


reg := regexp.MustCompile(`/([0-9]+)`)
g := reg.FindAllStringSubmatch(r.URL.Path, -1)

if len(g) != 1 {
	http.Error(rw, "Invalid URI", http.StatusBadRequest)
	return
}

if len(g[0]) != 2 {
	http.Error(rw, "Invalid URI", http.StatusBadRequest)
	return
}
idString := g[0][1]
id, err := strconv.Atoi(idString)


func (re*Regexp) FindAllStringSubmatch(s string,n int)[][]string

这个方法返回 [][]string,
MustCompile 相对于 Compile 少一个返回值 err,取而代之 MustCompile 会抛出一个异常。会返回为一个二维 string 的数组,数组中会列出所有匹配内容,下面我们通过代码演示一下


func main() {
	fmt.Println("hello regexp")
	url := "http://localhost:4600/12"
	reg := regexp.MustCompile(`/([0-9]+)`)
	g := reg.FindAllStringSubmatch(url, -1)
	fmt.Printf("%v\n", g)
}


[[/12 12]]


url := "http://localhost:4600/12/1"


[[/12 12] [/1 1]]


func (t *Tuts) getTuts(rw http.ResponseWriter, r *http.Request) {
	t.l.Println("Handle GET products")
	lt := data.GetTuts()
	//Marshal 返回值 ([]byte, error),
	// d, err := json.Marshal(lt)
	err := lt.ToJSON(rw)
	if err != nil {
		http.Error(rw, "Unable to marshal json", http.StatusInternalServerError)
	}
	// rw.Write(d)

}

不满足我们想要 PUT、POST 或 GET 请求可以通过代码在头部给出信息说明对该 http 动作并没有处理


rw.WriteHeader(http.StatusMethodNotAllowed)

因为还没有定义 DELETE 方法所以执行下面 curl 语句时,头部会返回HTTP/1.1 405 Method Not Allowed信息。


curl localhost:4600 -X DELETE -v |jq


HTTP/1.1 405 Method Not Allowed


//解码器(反序列化)
func (t *Tuts) FromJSON(r io.Reader) error {
	e := json.NewDecoder(r)
	return e.Decode(t)
}


添加 Tut


func (t *Tuts) addTut(rw http.ResponseWriter, r *http.Request) {
	t.l.Println("Handle POST Tuts")
	tut := &data.Tut{}
	err := tut.FromJSON(r.Body)
	if err != nil {
		http.Error(rw, "Unable to unmarshal json", http.StatusBadRequest)
	}

	// t.l.Printf("tut:%#v", tut)
	data.AddTut(tut)
	//转换数据为tut
}


curl localhost:4600 -d '{"name":"react","description":"nice react tut"}'


[{"id":1,"name":"golang","description":"golang basic","price":2.45,"category":"web"},{"id":2,"name":"rust","description":"rust
blockchain","price":1.99,"category":"blockchain"},{"id":3,"name":"react","description":"nice react tut","price":0,"category":""
}]


	if r.Method == http.MethodPut {
		t.l.Println("Handle PUT Tuts")
		//正则解析器
		reg := regexp.MustCompile(`/([0-9]+)`)
		g := reg.FindAllStringSubmatch(r.URL.Path, -1)

		if len(g) != 1 {
			http.Error(rw, "Invalid URI", http.StatusBadRequest)
			return
		}

		if len(g[0]) != 2 {
			http.Error(rw, "Invalid URI", http.StatusBadRequest)
			return
		}
		idString := g[0][1]
		id, err := strconv.Atoi(idString)

		if err != nil {
			http.Error(rw, "Invalid URI", http.StatusBadRequest)
			return
		}
		t.l.Println("got id", id)
    }


添加 Tut 到集合


func AddTut(t *Tut) {
	t.ID = getNextID()
	tutList = append(tutList, t)
}


生成新的 ID


func getNextID() int {
	lt := tutList[len(tutList)-1]
	return lt.ID + 1
}


更新 Tut 方法


func (t *Tuts) updateTuts(id int, rw http.ResponseWriter, r *http.Request) {
	t.l.Println("Handle PUT Tuts")
	tut := &data.Tut{}
	err := tut.FromJSON(r.Body)
	if err != nil {
		http.Error(rw, "Unable to unmarshal json", http.StatusBadRequest)
	}

	err = data.UpdateTut(id, tut)
	if err == data.ErrTutNotFound {
		http.Error(rw, "tut not found", http.StatusNotFound)
		return
	}

	if err != nil {
		http.Error(rw, "tut not found", http.StatusInternalServerError)
		return
	}

}


更新 Tut 集合


func UpdateTut(id int, t *Tut) error {
	//根据 id 判断是否存在
	_, pos, err := findTut(id)
	if err != nil {
		return err
	}
	t.ID = id
	tutList[pos] = t

	return nil
}


查询 Tut(通过ID)


var ErrTutNotFound = fmt.Errorf("Tut not found")

func findTut(id int) (*Tut, int, error) {
	for l, t := range tutList {
		if t.ID == id {
			return t, l, nil
		}
	}

	return nil, -1, ErrTutNotFound
}



curl localhost:4600/1 -XPUT  -d '{"name":"react","description":"nice react tut"}'


curl localhost:4600
[{"id":1,"name":"react","description":"nice react tut","price":0,"category":""},{"id":2,"name":"rust","d
最近发表
标签列表