Golang Web服务与数据格式解析指南
Golang Web服务与数据格式解析指南 大家好,
我即将创建我的第一个Web服务,并且需要确认如何在请求特定JSON格式的用户界面(UI)与以不同方式组织数据的关系型数据库之间管理数据。
为了稍作说明,我将举一个简单的例子,一个过度简化的酒店数据库:

一个酒店有一些字段,它有房间和图片,这里没什么特别的。
为了反序列化来自酒店数据库的SQL响应,我会创建这些简单的结构体…
type Hotel struct {
HotelId string `json:"hotel_id"`
Name string `json:"name"`
Address string `json:"address"`
NbRooms int32 `json:"nb_rooms"`
Pictures []Picture `json:"pictures"`
Rooms []Room `json:"hotel_rooms"`
}
type Picture struct {
Url string `json:"url"`
Order string `json:"order"`
Description string `json:"description"`
}
type Room struct {
RoomNb string `json:"room_nb"`
Capacity string `json:"name"`
Surface string `json:"surface"`
Price float32 `json:"price"`
}
…另一方面,当用户界面执行请求来填充其视图时,它只希望检索必要的数据,并且格式可能并不总是与上述结构体匹配。
例如,我们可以想象用户界面希望获取可用的酒店(取决于日期、价格等),但只想要一些特定信息,比如名称、地址、房间数量,仅此而已。出于某种原因,用户界面不希望获取任何类型的图片或关于酒店的更多信息。
{
"hotel_list":[
{
"hotel_id": "0001",
"name": "The Nice Hotel",
"address": "...",
"nb_rooms": 4,
},
{
"hotel_id": "0001",
"name": "The Nice Hotel",
"address": "..." "nb_rooms": 4,
}
]
为了满足用户界面的需求,酒店API将需要其他类型的结构体来匹配用户界面的数据要求:
var hotelList = []HotelListItem{}
type HotelListItem struct {
HotelId string `json:"hotel_id"`
Name string `json:"name"`
Address string `json:"address"`
NbRooms int32 `json:"nb_rooms"`
}
这意味着模型将负责将发送到数据库的SELECT请求的结构体结果,转换为适合用户界面期望的响应的结构体。换句话说,预订服务的“领域”部分不仅包含CRUD方法,还包含用于完成这项工作的转换方法。
你们对这些假设有什么看法?
我们可以想象,“转换”函数的数量可能会增长并占用大量空间,因为用户界面正在构建数百个不同的视图,这将使数据传输对象(DTO)变得越来越大……即使我们可以想象有几个视图使用相同的JSON响应格式,在这种情况下是否有其他策略可以实施?
这里有一个链接指向一个图表,稍作说明:
如果您能推荐关于这个主题的读物,我将不胜感激。
非常感谢您的帮助
更多关于Golang Web服务与数据格式解析指南的实战教程也可以访问 https://www.itying.com/category-94-b0.html
“我们可以想象,‘转换’函数的数量可能会增长并占用大量空间,因为UI正在构建数百种不同的视图,这将使DTO变得越来越大……即使我们可以想象多个视图使用相同的JSON响应格式,在这种情况下是否有其他策略可以实施?”
听起来你真正想要的——但可能没有意识到——是实现一个GraphQL服务器。
GraphQL的设计目的正是为了解决你刚才提到的问题,即客户端可以根据其提供的GraphQL查询来决定输出数据的外观。
如果你不熟悉GraphQL,我刚刚搜索到的这个链接或许能帮助你:
希望这能有所帮助。
更多关于Golang Web服务与数据格式解析指南的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
BigBoulard:
函数可能会变得庞大并占用大量空间,因为用户界面正在构建数百个不同的视图,这会使数据传输对象(DTO)变得越来越大……即使我们可以设想多个视图使用相同的JSON响应格式,在这种情况下是否有其他策略可以实施?
有一些技术可以减少对结构体和视图的需求。可以看看 SQL 驱动 sqlx,它不需要任何结构体。还有一些方法可以减少 API 中的端点数量。我创建了一些测试,也许至少能给你提供另一个视角。如果记录很多,使用 AJAX 可以减少页面闪烁。
// 示例代码:使用 sqlx 查询数据库
func queryUsers(db *sqlx.DB) error {
var users []map[string]interface{}
err := db.Select(&users, "SELECT * FROM users")
if err != nil {
return err
}
// 处理 users...
return nil
}
可能有四到五种情况,用户界面需要获取酒店信息,但同时还需要其他不同的信息……我在想,这难道不应该是API中“服务”部分(在控制器、服务、领域这种架构中)的工作吗?例如,酒店服务可以向酒店领域请求一些数据,并根据传入的HTTP请求,选择性地从其他领域(如“客房”领域或“餐厅”领域)获取其他信息,并负责汇总来自这些领域的所有数据,然后创建HTTP响应和JSON负载。你觉得呢?在我看来,这应该符合关注点分离原则。我们在现实生活中的API项目中会这样做吗?
是的——这取决于你的前端。我通常倾向于让我的API调用稍微通用一些,然后由前端框架组合多次调用来生成特定页面所需的所有数据。谷歌有一个通用的设计指南,其中包含一些他们偏离标准方法而使用自定义方法的例子:
自定义方法 | Cloud API | Google Cloud
![]()
虽然不完全是全面的,但我认为你提到的一些问题可能会通过这个得到解决。如果你想的话,可以通读那个API设计指南。它包含很多有用的信息,并且是与平台无关的。
你好 @Dean_Davidson,
感谢分享你的经验。为了提供更多细节,我实际上在使用 pgx。
在我看来,这个 API 中有 3 种类型的结构体:
- 用于接收 JSON 负载数据(反序列化)的请求结构体
- 用于接收 SQL 查询响应的响应结构体
- 可选的……用于生成返回给客户端的 JSON 响应的结构体。这一点曾是我的疑问,因为有时这些结构体可能就是第 2 点中的那些,但我不确定是否总是如此。如果没必要的话,我不想返回酒店的所有字段,因为我们不想耗尽客户端的电池,对吧 🙂
为了进一步说明,API 需要能够处理许多不同的传入查询。因此,为了加快实现速度,POST 和 PATCH 请求包含了需要添加或更新的内容的动态定义,例如:
POST /api/hotel
{
[{
field:"name"
value:"The Madison Hotel"
},
{
field:"address",
value:"21 E 27th St NYC"
}]
}
在这里,我将 JSON 反序列化到结构体中
type HotelReq struct{
Fields []FieldValue
}
type FieldValue struct{
Field string `json:"field" binding:"required"`
Value interface{} `json:"value" binding:"required"`
}
然后,我使用一点反射来创建插入或更新的字符串请求,并提交给数据库客户端。
INSERT INTO HOTEL(name, address) values ('The Madison Hotel', '21 E 27th St NYC')
更新操作同理。
现在我需要处理查询部分(并且我想我需要添加一些检查,比如检查提供的字段是否存在以及特殊字符……以避免任何 SQL 注入)(暂且不谈认证服务器,那是另一个话题 :D)。可能会有 4 到 5 种情况,UI 想要获取酒店信息,但同时需要其他不同的信息……考虑到这一点……我在想,这会不会是 API 的“服务”部分(在控制器、服务、领域这种架构中)应该完成的工作。例如,酒店服务可以向酒店领域请求一些数据,并可选地(取决于传入的 HTTP 请求)向其他领域(如“房间”领域或另一个“餐厅”领域)请求其他信息,并负责从这些领域收集所有数据,然后创建 HTTP 响应以及 JSON 负载。你觉得呢?在我看来,这应该遵循 SoC(关注点分离)原则。在实际的 API 项目中,我们会这样做吗?
非常感谢。
我曾使用过与您描述的 XListItem 类型结构体类似的方法。以下是我通常采用的做法:对于简单的 CRUD 类型操作,例如检索单个酒店及其关联数据,直接使用模型并生成 SQL(可以在项目中手动编写,也可以使用 Gorm 或 SQLX)。对于任何相对高级的操作,我通常更倾向于手动编写 SQL。因此,在您的示例中,我会为 Hotel、Picture 和 Room 生成 SQL。以 Gorm 类型为例:
func GetHotel(hotelID string) Hotel {
var hotel Hotel
db.First(&hotel, 1)
return hotel
}
但是,对于 HotelListItem,我可能会希望编写自定义查询,并直接将其扫描到 HotelListItem 结构体切片中。同样以 Gorm 类型为例:
// GetHotelList 根据条件获取酒店列表项。借鉴其他语言的经验,
// 如果我想在更具体的 UI 上下文中考虑这些列表项,有时会将它们视为 ViewModel。
// 例如 `HotelSearchItem` 和 `HotelDetailView` 或类似的概念。
func GetHotelList(priceMax int64, capacityMin int32) []HotelListItem {
result := hotelList = []HotelListItem{}
db.Raw(`
select
h.id as hotel_id,
h.name,
h.address,
r.room_nb
from hotel h
join room r on r.hotel_id = h.id
where r.price <= ?
and r.capacity >= ?`, priceMax, capacityMin).Scan(&result)
return result
}
为什么这样做?
- 因为我通常不信任 ORM 处理过多的连接或任何过于复杂的查询(尽管这个例子并不复杂,但通常我的查询比这更复杂)。Gorm 生成的 SQL 比我用过的任何其他 ORM 都更合理,但对于复杂查询,你往往需要与 ORM 作斗争。
- 因为这使得手动调整非 CRUD 查询变得容易。这在小型应用中并不重要,但在大型生产应用中却非常重要(至少根据我的经验;您的结果可能有所不同!)。
- 这里没有任何魔法,对于任何未来的开发者来说,正在发生什么以及如何调整都非常明显。如果需要,我可以直接将 SQL 复制/粘贴到编辑器中运行。
其他选项包括:
- 为所有内容创建数据库视图/存储过程,并建立与之对应的模型。您可以使用 volatiletech/sqlboiler 来生成它们。
- 您也可以研究一下 Go GraphQL 实现。我在这方面经验较少。
在Golang Web服务中处理不同数据格式是常见需求。以下是针对你场景的专业实现方案:
1. 核心解决方案:使用DTO模式
你的方法基本正确,但可以优化转换逻辑。以下是改进的实现:
// 领域模型(数据库结构)
type Hotel struct {
HotelID string `db:"hotel_id"`
Name string `db:"name"`
Address string `db:"address"`
NbRooms int32 `db:"nb_rooms"`
Pictures []Picture `db:"-"`
Rooms []Room `db:"-"`
}
// DTO结构体
type HotelListItemDTO struct {
HotelID string `json:"hotel_id"`
Name string `json:"name"`
Address string `json:"address"`
NbRooms int32 `json:"nb_rooms"`
}
type HotelDetailDTO struct {
HotelID string `json:"hotel_id"`
Name string `json:"name"`
Address string `json:"address"`
NbRooms int32 `json:"nb_rooms"`
Pictures []PictureDTO `json:"pictures"`
Rooms []RoomListItem `json:"rooms"`
}
// 转换方法
func (h *Hotel) ToListItemDTO() HotelListItemDTO {
return HotelListItemDTO{
HotelID: h.HotelID,
Name: h.Name,
Address: h.Address,
NbRooms: h.NbRooms,
}
}
func (h *Hotel) ToDetailDTO() HotelDetailDTO {
pictureDTOs := make([]PictureDTO, len(h.Pictures))
for i, p := range h.Pictures {
pictureDTOs[i] = p.ToDTO()
}
roomItems := make([]RoomListItem, len(h.Rooms))
for i, r := range h.Rooms {
roomItems[i] = r.ToListItem()
}
return HotelDetailDTO{
HotelID: h.HotelID,
Name: h.Name,
Address: h.Address,
NbRooms: h.NbRooms,
Pictures: pictureDTOs,
Rooms: roomItems,
}
}
2. 使用组合减少重复代码
// 基础结构体
type HotelBase struct {
HotelID string `json:"hotel_id"`
Name string `json:"name"`
Address string `json:"address"`
NbRooms int32 `json:"nb_rooms"`
}
// 组合DTO
type HotelListItemDTO struct {
HotelBase
}
type HotelDetailDTO struct {
HotelBase
Pictures []PictureDTO `json:"pictures,omitempty"`
Rooms []RoomListItem `json:"rooms,omitempty"`
}
// 使用嵌入
type HotelSearchResult struct {
HotelBase
AvailableRooms int32 `json:"available_rooms"`
MinPrice float32 `json:"min_price"`
}
3. 使用函数式转换器
type HotelTransformer func(*Hotel) interface{}
var transformers = map[string]HotelTransformer{
"list": transformToList,
"detail": transformToDetail,
"search": transformToSearch,
}
func transformToList(h *Hotel) interface{} {
return HotelListItemDTO{
HotelID: h.HotelID,
Name: h.Name,
Address: h.Address,
NbRooms: h.NbRooms,
}
}
func transformToDetail(h *Hotel) interface{} {
return HotelDetailDTO{
HotelID: h.HotelID,
Name: h.Name,
Address: h.Address,
NbRooms: h.NbRooms,
Pictures: transformPictures(h.Pictures),
Rooms: transformRooms(h.Rooms),
}
}
// 在handler中使用
func GetHotels(w http.ResponseWriter, r *http.Request) {
viewType := r.URL.Query().Get("view")
transformer, exists := transformers[viewType]
if !exists {
transformer = transformers["list"]
}
hotels := fetchHotelsFromDB()
result := make([]interface{}, len(hotels))
for i, h := range hotels {
result[i] = transformer(h)
}
json.NewEncoder(w).Encode(map[string]interface{}{
"hotel_list": result,
})
}
4. 使用结构体标签控制JSON输出
type HotelResponse struct {
HotelID string `json:"hotel_id"`
Name string `json:"name"`
Address string `json:"address,omitempty"` // 为空时不输出
NbRooms int32 `json:"nb_rooms"`
Pictures []Picture `json:"pictures,omitempty"`
Rooms []Room `json:"rooms,omitempty"`
// 使用自定义标签控制不同视图
IncludePictures bool `json:"-" query:"include_pictures"`
IncludeRooms bool `json:"-" query:"include_rooms"`
}
// 自定义MarshalJSON
func (h HotelResponse) MarshalJSON() ([]byte, error) {
type Alias HotelResponse
aux := &struct {
*Alias
Pictures []Picture `json:"pictures,omitempty"`
Rooms []Room `json:"rooms,omitempty"`
}{
Alias: (*Alias)(&h),
}
if h.IncludePictures {
aux.Pictures = h.Pictures
}
if h.IncludeRooms {
aux.Rooms = h.Rooms
}
return json.Marshal(aux)
}
5. 使用SQL查询优化
// 根据视图类型构建查询
func buildHotelQuery(viewType string) string {
switch viewType {
case "list":
return `SELECT hotel_id, name, address, nb_rooms FROM hotels`
case "detail":
return `
SELECT h.hotel_id, h.name, h.address, h.nb_rooms,
p.url, p.order, p.description,
r.room_nb, r.capacity, r.surface, r.price
FROM hotels h
LEFT JOIN pictures p ON h.hotel_id = p.hotel_id
LEFT JOIN rooms r ON h.hotel_id = r.hotel_id
`
default:
return `SELECT hotel_id, name FROM hotels`
}
}
// 使用sqlx扫描到不同结构体
func fetchHotels(viewType string) (interface{}, error) {
query := buildHotelQuery(viewType)
switch viewType {
case "list":
var hotels []HotelListItemDTO
err := db.Select(&hotels, query)
return hotels, err
case "detail":
var hotels []HotelDetailDTO
err := db.Select(&hotels, query)
return hotels, err
default:
var hotels []HotelBase
err := db.Select(&hotels, query)
return hotels, err
}
}
6. 使用代码生成工具
对于大量DTO,可以考虑使用代码生成:
// 使用go:generate指令
//go:generate dto-generator -type=Hotel -output=hotel_dto.go
// 生成的代码示例
type HotelListResponse struct {
Hotels []HotelListItem `json:"hotels"`
Total int `json:"total"`
Page int `json:"page"`
}
type HotelSearchResponse struct {
Hotels []HotelSearchItem `json:"hotels"`
Filters map[string]interface{} `json:"filters"`
}
这些方案提供了可扩展的方式来处理不同UI视图的数据格式需求。DTO模式在Golang Web开发中是标准实践,结合结构体组合和转换器模式可以有效管理复杂度。

