Go后端学习
前言
现在Golang似乎也是后端开发的热门语言了,前后端开发如果不熟悉的话连接口都写不了吧,为了不被时代淘汰,必须要学学了。
Golang结构
Golang的结构主要是包括模块,包,以及函数。
- **Package(包)**是 Go 组织代码的基本单位。通常,一个目录对应一个 package;同一包可以由多个 .go 文件组成,文件中可以定义函数、类型、方法、常量和变量。包通常按功能或职责组织。
- **Module(模块)**是 Go 管理依赖和版本的单位,可以包含一个或多个 package。每个 module 由根目录中的 go.mod 文件定义;go.mod 所在的目录就是 module 根目录。
go.mod 中的 module 声明模块路径,这个路径也是模块内 package 导入路径的前缀;go 声明模块使用的 Go 语言版本;require 声明模块依赖的其他 module 及其版本。
.go 文件中的 import 则指定当前文件要使用的 package。简单来说,require 管理模块依赖,import 管理具体使用的包。
涉及的知识页面一,页面二,页面三。
数据格式
编码与解码
与前端类似,Go语言使用JavaScript对象处理数据,所以对于数据传输时需要先编码为JSON处理,通过使用 encoding/json/v2 包,可以方便地在 Go 程序中读写 JSON。其中,对于需要传输的数据,我们使用Marshal函数将数据编码为 JSON;而对于前端传入的JSON数据,我们需要使用Unmarshal函数将其解码为Go值。
需要注意的是,编码和解码数据也需要遵守相关规定:
- 只有能够表示为有效 JSON 的数据结构才能被编码:
- 指针会被编码为它指向的值;如果指针是 nil,则编码为 null。
- JSON 对象的键只能是字符串。因此,Go 的 map 键必须能编码为 JSON 字符串,例如 map[string]T。其中 T 可以是任何受 JSON 包支持的 Go 类型。
- channel、复数和函数类型不能被编码。
- 不支持循环引用的数据结构。
JSON 包只会读取 struct 中已导出的字段(类似于前端的type类型),也就是名称以大写字母开头的字段。因此,输出 JSON 中只会包含 struct 的已导出字段。
1 | type Message struct { |
此外,可以使用any来作为通用容器承接任意数据,JSON 包使用 map[string]any 和 []any 保存任意 JSON 对象和数组;它可以将任何有效的 JSON 数据解码到普通的 any 值中。默认使用的 Go 具体类型如下:
- JSON 布尔值对应 bool
- JSON 数字对应 float64
- JSON 字符串对应 string
- JSON null 对应 nil
所以不知道某段JSON数据的结构时,我们可以使用Unmarshal 将它解码到 any 值中。
1 | var f any |
使用
使用时,我们可以通过断言来访问其中具体存放的类型,或使用条件判断来根据不同的数据类型来做输出。
1 | // 用类型断言取得它实际存放的 map[string]any |
初始化逻辑
Go在初始化后不会先分配底层数组,等真正需要保存元素时再分配,这样避免为没用到的字段预留空间。此时Go的切片零值是 nil,可以直接使用;当你对它 append 时,Go才会按需分配底层数组,对于指针和其他初始化分配也保持这种机制。
下面例子展示将数据解码到 FamilyMember 值中,结果会符合预期。不过仔细看会发现一个值得注意的细节:我们用 var 创建了 FamilyMember struct,然后将它的指针传给 Unmarshal。此时,Parents 字段还是一个 nil 切片。而需要填充 Parents时,Unmarshal 在后台分配了新的切片。
1 | type FamilyMember struct { |
流式编码与解码
io.Reader 和 io.Writer 接口在 Go 中应用广泛,可以通过流的方式访问 HTTP 连接、WebSocket 或文件等资源。
MarshalWrite 和 UnmarshalRead 函数可以直接将数据编码到流中,或从流中解码数据,无需先用一个中间的 []byte 保存整条消息。
1 | // 形式定义 |
自定义编码与解码
有时,默认的编码方式不适合你的类型。我们可以自定义一个编码与解码过程。
后续遇到了再详细解释。
总结
本节涉及的知识页面。
连接数据库
后端使用上必不可少的步骤就是连接并访问数据库,这里以MySQL数据库为例,展示Golang连接并访问数据库的流程。
前置条件
首先需要一个可以运行的MySQL数据库,还需要数据库的使用权限,最后我们需要为Golang后端安装对应的连接包database/sql以及数据库驱动github.com/go-sql-driver/mysql。
连接和访问
第一步,在MySQL中创建一个数据库,创建你想使用的表。
这过于简单,省略详细过程了。
第二步,需要先导入驱动,虽然database/sql包可以提供访问SQL数据库的通用接口,但其本身不包括对应数据库的驱动,所以我们需要先安装github.com/go-sql-driver/mysql驱动。
1 | 1.在需要用到 SQL 交互的 Go 源码中添加 MySQL 驱动的 import,import "github.com/go-sql-driver/mysql" |
第三步,使用database/sql包提供数据库连接池,调用 sql.Open 可以创建一个数据库句柄 db 。
1 | db, err := sql.Open("mysql", "user:password@/dbname") |
db也就是数据库句柄,可以理解为程序访问数据库的管理入口,它通常不是一条数据库连接,而是一个并发安全的连接池管理对象:它负责按需创建、复用和回收底层连接。“句柄”是对这种管理入口的泛称。
简单说则是当前 Go 后端通过 db(*sql.DB)统一管理数据库连接池;后端执行数据库操作时,db 会按需提供并复用连接。
第四步,一个简单的访问Go源码如下,对于的解释在代码中。
1 | package main |
rows.Next() 返回 false 表示没有取得下一行。这通常是因为结果集正常读完了;也可能是读取过程中出了错。单看这个 false,无法区分这两种情况。rows.Err() 用来区分:
- rows.Err() == nil:遍历正常结束,没有发现读取错误。
- rows.Err() != nil:遍历中发生了读取错误,例如连接问题。
当然也有仅限单行读取的查询操作,使用 QueryRow,它会返回一个结果对象。
1 | // albumByID 查询指定编号的唱片。 |
存入数据
使用 Exec 可以执行不需要返回查询结果的 SQL,例如插入、更新或删除数据:
1 | // addAlbum 向数据库添加一张唱片。 |
简单的SQL数据库连接示例就到这里结束了,本节涉及的知识页面。
开发 RESTful API
这里我们使用 Go 和 Gin Web Framework(Gin)编写一个简单的 RESTful API 服务。Gin 能帮助你处理 Web 开发中的常见工作,例如路由请求、读取请求信息,以及把数据转换为 JSON 响应。
设计API路由时,需要按照功能对应区分不同的GET/POST接口方法。
这里我们先安装gin,在项目根目录执行go get github.com/gin-gonic/gin,然后在代码中导入 Gin。
预置数据格式
为了方便展示过程,我们先不结合数据库做,而是设置一些保存在内存中的预置数据。
1 | // album 表示一张唱片的数据。 |
GET接口与请求
现在数据已经创建,我们需要从前端发送请求到后端获取数据,具体的流程展示如下:
1 | 前端发送 GET /albums |
这里我们先定义后端请求入口,,在main.go中设置路由并启动服务,如下:
1 | func main() { |
gin.Default() 创建一个默认路由器。router.GET 将 GET /albums 请求交给 getAlbums 处理。这里传入的是函数本身 getAlbums,不是调用函数得到的结果 getAlbums()。
当然,在这之前我们需要引入依赖包,如下:
1 | package main |
在对应功能GET /albums时,我们应该返回所有唱片为JSON,处理请求的函数叫作 handler(处理函数)。教程中的处理函数如下:
1 | // getAlbums 将所有唱片作为 JSON 返回。 |
一般来讲,前端发送 HTTP 请求;Gin 收到请求后,根据请求方法和路径匹配路由,然后创建并填充这个请求对应的 *gin.Context,也就是本示例中的c,再把它传给 getAlbums。
进入函数时,c 已经包含了可供 handler 使用的请求和响应上下文,例如请求对象、路径参数、查询参数,以及写响应的方法。但 Gin 不会预先把请求体里的 JSON 自动绑定到某个业务结构体;需要 handler 显式调用 c.BindJSON(…) 才会解析。
但在当前例子中,getAlbums 不读取请求数据,只通过 c.IndentedJSON(…) 写入 HTTP 响应(这包含状态码和数据内容),因为当前数据已经存在本地而且没有设置查询条件。
那么这里我们启动服务发送请求就可以得到我们预设的数据:
1 | 启动服务 |
如果我们要加入参数,也很简单,我们只需要在前端传入查询条件,并在后端读取该条件查询即可,这里以唱片ID为例:
1 | // getAlbumByID 查找并返回 ID 与请求参数匹配的唱片。 |
这样c.Param("id") 会取得路由中的 id 参数。for _, a := range albums 遍历切片:_ 忽略元素下标,a 表示当前唱片。
如果找到唱片,就返回 JSON 和 200 OK。return 用来立即结束函数,避免继续遍历或执行后面的“未找到”逻辑。
如果没找到,程序返回 404 Not Found,并用 gin.H 构造一段 JSON 对象:
1 | {"message": "album not found"} |
然后记得添加路由router.GET("/albums/:id", getAlbumByID)。
然后再进行测试curl http://localhost:8080/albums/2。
Post接口与请求
这里我们通过新增唱片来模拟我们的POST接口任务。先创建路由于main.go:
1 | func main() { |
由于我们发送信息到后端,所以服务端要读取请求体中的 JSON,创建唱片数据并添加到列表中。也就是说我们需要从c中读取请求信息。
1 | // postAlbums 根据请求体中的 JSON 添加一张唱片。 |
绑定成功后,append 将新唱片加入 albums 切片。响应使用 http.StatusCreated,也就是 HTTP 201 Created,并返回刚添加的唱片。
同样地,启动服务后使用命令行模拟请求,我们可以添加一张唱片。
1 | curl http://localhost:8080/albums \ |
其中:
- –include 要求 curl 显示 HTTP 响应头。
- –header 声明请求体是 JSON。
- –request “POST” 指定 HTTP 方法。
- –data 提供要发送的 JSON 内容。
成功时,响应状态是 201 Created,响应体会包含新唱片的数据。之后再请求 GET /albums,可以看到新记录已经加入列表。
总结
这个接口开发实例可以按四个环节理解:
- 数据模型:album 结构体定义一张唱片包含哪些字段;JSON 标签控制字段在接口中的名称。
- 内存数据:albums 切片保存唱片。它暂时替代数据库,但服务重启后会恢复到代码中的初始数据。
- 处理函数:getAlbums、postAlbums 和 getAlbumByID 分别处理列表查询、添加和按 ID 查询。
- 路由:main 把 HTTP 方法和 URL 路径映射到处理函数,再启动监听 localhost:8080 的服务器。
参考的教程文档。





