golang模拟API服务的尝试

636 阅读5分钟

「这是我参与11月更文挑战的第8天,活动详情查看:2021最后一次更文挑战」。

模拟API服务的工具有很多,不过内网部署的好像不是很多。
学了几天 golang, 想用 golang 也折腾一个。
视野格局能力有限,抛砖引玉一下。

简单需求:

  1. 对于请求(http.Reqeust) 里 的 Method 和 URL,指定不同的模拟响应头和响应文件(这里使用 json)。
  2. 读取响应文件里的内容,返回给请求端。

本文相关代码:hellogo/mock at main · bettersun/hellogo (github.com)

模拟服务选项和模拟服务信息

模拟服务选项

// Option 选项
type Option struct {
	Port      string     `yaml:"port"`         // 端口
	UseMock   bool       `yaml:"useMock"`      // true: 使用模拟服务 false: 不使用
	MockInfos []MockInfo `yaml:"requestInfos"` // 模拟服务信息
}

主要定义模拟服务的端口、否使用模拟服务和模拟服务信息。
模拟服务信息是模拟服务的主要内容,是一个切片。

模拟服务信息

// MockInfo 模拟服务信息
type MockInfo struct {
	Method   string            `yaml:"method"`   // 请求方法
	URL      string            `yaml:"url"`      // URL
	Header   map[string]string `yaml:"header"`   // 响应头
	JsonFile string            `yaml:"jsonFile"` // 响应内容 json 文件
}

请求信息里,设置 请求方法 和 URL,是为了匹配请求过来的方法和 URL。
匹配之后,使用模拟服务信息里的响应头和响应内容json文件返回响应给请求端。

golang的HTTP服务

// Mock 模拟服务
//   option: 选项
func Mock(option Option) {
	log.Println("mock")

	// 监听端口
	port := ":" + option.Port
	server = http.Server{
		Addr: port,
	}

	log.Println(fmt.Sprintf("服务运行中... 端口[%v]", option.Port))
	http.ListenAndServe(port, http.HandlerFunc(DoHandle))
}

感觉golang的 HTTP 服务写起来很省事,比 Java 的简单多了。
这里的 DoHandle 是一个 http.HandlerFunc(),研究了好久,才知道有这种写法。

http.ListenAndServe(port, http.HandlerFunc(DoHandle))

网上和很多书里,大多是下面这种写法:

http.HandleFunc("/hello", hello)

这种写法需要每个 URL 都去匹配了,不知道 URL 的前提下,根本不知道怎么匹配。

响应函数

// DoHandle 响应函数
func DoHandle(w http.ResponseWriter, r *http.Request) {
	if VerOption.UseMock {
		doMock(w, r, VerOption)
	} else {
		// 转发到真正的API
	}
}

这里加了一个判断,用了全局公共变量:模拟服务选项。

// VerOption 全局公共变量:模拟服务选项
var VerOption Option

全局变量应该控制使用,因为http.HandlerFunc()的参数是固定的,其它需要设定的选项就只能通过全局变量来控制了。
如果有什么其它好方法,不吝赐教。

响应函数里只是把参数的 http.ResponseWriter 和 *http.Request 都传递给实际的处理函数,顺便把全局变量的模拟服务选项也传递给实际的处理函数。

响应函数里加了一个判断,使用模拟服务时,调用真正的处理函数;不使用模拟服务时(现在没有实际编码),可以转发到真正的 API,即简单代理一下。

模拟服务

// 模拟服务
func doMock(w http.ResponseWriter, r *http.Request, option Option) {
	statusCode := http.StatusOK
	var info RequestInfo

	// 模拟服务通过 请求方法和 URL 来匹配
	// 当 请求 的 请求方法和URL 与 模拟服务选项 的 请求信息 的 请求方法和URL 一致时,
	// 使用 模拟服务选项 的 请求信息 的 Json 文件返回响应内容。
	isMatch := false
	reqMethodUrl := fmt.Sprintf("%s_%s", r.Method, r.URL.String())
	for _, item := range option.RequestInfos {
		infoMethodUrl := fmt.Sprintf("%s_%s", item.Method, item.URL)
		if reqMethodUrl == infoMethodUrl {
			isMatch = true
			info = item
			break
		}
	}

	// 没有匹配的模拟服务,返回404
	if !isMatch {
		statusCode = http.StatusNotFound
		w.WriteHeader(statusCode)
	}

	// 响应头
	for k, v := range info.Header {
		w.Header().Set(k, v)
	}

	// 响应文件
	exist := false
	_, err := os.Stat(info.JsonFile)
	if err == nil || os.IsExist(err) {
		exist = true
	}

	// json 文件不存在
	if !exist {
		log.Printf("IsExist Error: %v\n", err)
		statusCode = http.StatusInternalServerError
	}

	// 读取 json 文件
	b, err := ioutil.ReadFile(info.JsonFile)
	if err != nil {
		log.Printf("ReadFile Error: %v\n", err)
		statusCode = http.StatusInternalServerError
	}

	// 响应状态码,必须放在w.Header().Set(k, v)之后
	w.WriteHeader(statusCode)

	isGzip := false
	contentEncoding, ok := info.Header[headerContentEncoding]
	if ok {
		if strings.Contains(contentEncoding, "gzip") {
			isGzip = true
		}
	}

	// 响应
	// gzip 压缩(不同的压缩需要不同的处理对应)
	if isGzip {
		//gzip压缩
		buffer := new(bytes.Buffer)
		gw := gzip.NewWriter(buffer)
		// 写入 json 文件的字节
		gw.Write(b)
		// 需要 Flush()
		gw.Flush()

		w.Write(buffer.Bytes())
	} else {
		w.Write(b)
	}
}

大体的处理流程:

  1. 接收到的请求,如上面所说的,先去匹配模拟服务信息。匹配不上,返回404
  2. 匹配之后,设置响应头,然后读取响应内容 json 文件。读取出错,返回500。
  3. 响应状态码设置为200。
  4. 读取响应内容 json 文件的数据后,根据具体响应头的内容处理读取的数据。
    这里是有一个 gzip 的处理,可根据具体情况再扩展。
    注:gzip 处理中,gw.Write(b)写入字节数据后,需要 Flush() 一下。
  5. 写入响应数据。

启动模拟服务例

在 main() 里定义好 模拟服务信息 和 模拟服务选项。

如下例: 定义响应头,这里没有每个模拟服务信息各自定义响应头,用了同一个响应头。
然后指定模拟服务要响应的请求方法和URL 的响应体和响应内容 json 文件。
设置到模拟服务选项后,就可以启动模拟服务了。

这里的响应头、模拟服务信息和模拟服务选项的成员变量值,都可以改成从外部文件读取。

package main

import (
	"hellogo/mock"
	"net/http"
)

func main() {
	// 响应头
	header := make(map[string]string, 0)
	header["Access-Control-Allow-Origin"] = "*"
	header["Content-Encoding"] = "gzip"
	header["Content-Type"] = "application/json;charset=UTF-8"

	// 模拟服务信息切片
	var mockInfos []mock.MockInfo

	// 模拟服务信息1
	var mockInfo1 mock.MockInfo
	mockInfo1.Header = header
	mockInfo1.Method = http.MethodGet
	mockInfo1.URL = "/hello"
	mockInfo1.JsonFile = "/Users/xxx/Documents/Develop/github.com/bettersun/hellogo/mock/command/hello.json"

	// 模拟服务信息2
	var mockInfo2 mock.MockInfo
	mockInfo2.Header = header
	mockInfo2.Method = http.MethodGet
	mockInfo2.URL = "/welcome"
	mockInfo2.JsonFile = "/Users/xxx/Documents/Develop/github.com/bettersun/hellogo/mock/command/welcome.json"

	// 添加到模拟服务信息切片
	mockInfos = append(mockInfos, mockInfo1)
	mockInfos = append(mockInfos, mockInfo2)

	// 模拟服务选项
	mock.VerOption.UseMock = true
	mock.VerOption.Port = "9012"
	mock.VerOption.MockInfos = mockInfos
	
	// 启动模拟服务
	mock.Mock(mock.VerOption)
}

hello.json

{
  "message": "hello, world."
}

welcome.json

{
  "message": "welcome"
}

测试一下

mock.gif

响应内容为 json 文件里的内容,这样就完成了一个简单的模拟 API 服务。

具体的模拟服务信息和模拟服务选项的内容都可以改为从外部文件读取,然后还可以结合GUI,就能扩展成一个比较方便的模拟 API 服务工具。
可以在真实的 API 开发出来之前,先给前端的小伙伴们用着。

复杂的情况(其它的压缩、加密、其它类型的响应内容等)还需要单独再编码,大体就是基于上面的思路进行扩展。