Skip to content
← 返回Go列表
知识专题

Go:从函数到 HTTP JSON 接口 ​

前置:文件与 JSON。一个服务进程持续监听端口,客户端发送请求,服务根据方法、路径与正文生成响应。先用标准库建立这些概念,再进入 Gin。Go 1.27.2;历史记录中 Windows/Linux 测试通过,本轮未启动服务或执行示例测试;macOS 仅作平台说明,本项目不执行平台运行验证。

请求、状态码与 Content-Type ​

本例协议:POST /greet,请求正文 {"name":"Go"};成功返回 200 和 {"message":"你好,Go"}。Content-Type 表示正文格式,JSON 响应使用 application/json; charset=utf-8。方法说明操作类别,路径定位资源;JSON 内容放 body,不塞进路径。

情形本例状态处理
合法 name200返回问候 JSON
空 name、坏 JSON、未知字段、多个 JSON 值、请求超过 4096 字节400返回文本错误,不继续执行业务
路径不存在404检查 URL
对 /greet 使用 GET405使用契约规定的 POST

这是教学接口自己的限制与错误选择;400 不等于服务器故障。标准库 http.Error 产生文本错误,下一章统一为 JSON 错误结构。仅设置请求头不等于内容已验证,必须实际解码并检查值。本例按 JSON 解码内容,没有强制检查请求的 Content-Type。

完整源码与启动 ​

使用上一章 examples/go/foundations 模块,在其中建 http-bridge/main.go,或直接使用仓库现有文件。完整源码如下:

http-bridge/main.go
go
package main

import (
	"encoding/json"
	"io"
	"log"
	"net/http"
	"strings"
	"time"
)

func handler() http.Handler {
	mux := http.NewServeMux()
	mux.HandleFunc("POST /greet", func(w http.ResponseWriter, r *http.Request) {
		var input struct {
			Name string `json:"name"`
		}
		decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 4096))
		decoder.DisallowUnknownFields()
		if err := decoder.Decode(&input); err != nil || strings.TrimSpace(input.Name) == "" {
			http.Error(w, "需要 name 字符串", 400)
			return
		}
		if decoder.Decode(new(any)) != io.EOF {
			http.Error(w, "只接受一个 JSON 对象", 400)
			return
		}
		w.Header().Set("Content-Type", "application/json; charset=utf-8")
		if err := json.NewEncoder(w).Encode(map[string]string{"message": "你好," + strings.TrimSpace(input.Name)}); err != nil {
			log.Print(err)
		}
	})
	return mux
}
func main() {
	server := &http.Server{Addr: "127.0.0.1:8081", Handler: handler(), ReadHeaderTimeout: 5 * time.Second}
	log.Fatal(server.ListenAndServe())
}

在模块根目录执行 go run ./http-bridge;服务监听 127.0.0.1:8081,没有启动输出也可以是正常监听。终端此时不会退出;另开终端发请求,结束后 Ctrl+C。127.0.0.1 只能从本机访问,本练习不要求公网开放端口。

PowerShell:

powershell
Invoke-RestMethod 'http://127.0.0.1:8081/greet' -Method Post -ContentType 'application/json' -Body '{"name":"Go"}'

Bash/Zsh:

bash
curl -i -X POST http://127.0.0.1:8081/greet -H 'Content-Type: application/json' -d '{"name":"Go"}'

预期 message 为“你好,Go”;curl 响应头有 200。请求 {} 应为 400。PowerShell 遇到非成功状态可能抛出异常,查看响应状态/正文即可,不能把客户端抛错误判为服务崩溃。

JSON 输入校验、httptest 与 connection refused 排查 ​

ServeMux 按方法和路径分发到函数;ResponseWriter 写响应,Request 提供请求信息。函数中先限制 body 大小,再解码到结构体,使用 DisallowUnknownFields 拒绝未知字段、检查 TrimSpace 后的 name,最后二次 Decode 必须得到 io.EOF,确保只存在一个 JSON 值。若得到另一个对象或其他错误都返回 400;单次 Decode 成功不代表后面没有第二个对象。

写响应前设置 Header;一旦写入正文或状态头,再改状态码可能已经无效。超时至少限制读取请求头的时间;完整服务还要评估 body、写入与空闲连接时间,下一章示例显式设置这些期限。

测试不需要开启端口:httptest.NewRequest 创建请求,NewRecorder 收集响应,直接调用 handler.ServeHTTP。它验证处理逻辑;实际网络监听、端口与进程重启需另测。

http-bridge/main_test.go
go
package main

import (
	"net/http/httptest"
	"strings"
	"testing"
)

func TestGreetingHTTP(t *testing.T) {
	for _, tc := range []struct {
		method, path, body string
		status             int
	}{{"POST", "/greet", `{"name":"Go"}`, 200}, {"POST", "/greet", `{"name":" "}`, 400}, {"POST", "/greet", `broken`, 400}, {"GET", "/greet", "", 405}, {"GET", "/missing", "", 404}} {
		t.Run(tc.method+tc.path+tc.body, func(t *testing.T) {
			w := httptest.NewRecorder()
			handler().ServeHTTP(w, httptest.NewRequest(tc.method, tc.path, strings.NewReader(tc.body)))
			if w.Code != tc.status {
				t.Fatal(w.Code, w.Body.String())
			}
			if tc.status == 200 && !strings.Contains(w.Body.String(), "你好,Go") {
				t.Fatal(w.Body.String())
			}
		})
	}
}

func TestRejectInvalidJSON(t *testing.T) {
	for name, body := range map[string]string{
		"unknown field":   `{"name":"Go","extra":1}`,
		"trailing object": `{"name":"Go"}{}`,
		"oversized":       `{"name":"` + strings.Repeat("x", 4096) + `"}`,
	} {
		t.Run(name, func(t *testing.T) {
			response := httptest.NewRecorder()
			handler().ServeHTTP(response, httptest.NewRequest("POST", "/greet", strings.NewReader(body)))
			if response.Code != 400 {
				t.Fatalf("invalid JSON returned %d, want 400", response.Code)
			}
		})
	}
}

在模块根目录运行 go test ./http-bridge -v。预期成功、空输入、坏 JSON、错误路径和错误方法的测试全部 PASS。connection refused 通常是服务没启动或端口写错;address already in use 则检查是否已有自己的服务在运行,停止那个进程或调整明确的配置,不随意结束其他程序。

练习与自检 ​

发送 {"name":" Go "},预期去掉空白后仍是“你好,Go”。再发送 {"name":"Go","extra":1}、{"name":"Go"}{},预期 400。解释:为什么 GET /greet 是 405,GET /missing 是 404?在现有 TestRejectInvalidJSON 中定位未知字段断言,再增加一个去除 name 首尾空白的成功用例。

参考解法

前者在 DisallowUnknownFields 解码时失败,后者在二次 Decode 时发现非 EOF。/greet 路径存在但不支持 GET,所以 405;/missing 没有匹配路由,所以 404。未知字段断言已在 TestRejectInvalidJSON 中;新增 method POST、path /greet、body 为带空白的 name、status 200,并断言响应 message 为“你好,Go”,使用同一个真实 handler。

学习顺序请使用本页底部的路线导航。核对依据:net/http、httptest、Gin 中文测试说明。

继续阅读