🚀 从 TypeScript 到 Go:前端工程师的后端入门指南
这篇指南专为拥有 TypeScript 背景的你设计。我将跳过泛泛的编程入门知识,聚焦于 Go 的核心特性、与 TS 的差异,以及如何快速动手构建一个能跑起来的后端服务。我们的目标是:在 2 小时内,让你完成从 Go 环境配置到构建一个小型 JSON API 的全过程,为你开启后端开发的大门。
学习节奏建议(总计约 1.5 小时)
为了让你更有掌控感,我将整个学习路径拆分为几个时间块:
- 0–15 分钟:环境与第一个程序
- 15–45 分钟:核心语法与 TS 对比
- 45-60分钟:常用 三方库 介绍
- 60–90 分钟:构建 API 服务
让我们开始吧!
环境配置与第一个程序
目标:安装 Go,配置好开发环境,并成功运行你的第一个 Go 程序。
1. 安装与环境
首先,访问 Go 官方下载页面,下载并安装最新的稳定版(例如 1.24.x)。安装过程会自动帮你设置好 GOROOT(Go 的安装目录)和 PATH(命令行工具路径)。
安装完成后,打开终端,运行以下命令验证:
go version
如果能看到版本号,说明安装成功。然后配置下国内代理装包更快,作用类似 npm config set registry
go env -w GOPROXY="https://goproxy.cn|https://proxy.golang.org|direct"
接下来,了解几个环境变量:
-
go env: 这个命令可以查看所有 Go 相关的环境变量。GOPATH是你的工作区,用于存放非模块化项目的代码和工具。GOPROXY模块代理地址
2. 你的第一个 Go 程序:Hello, World!
- 创建一个新的项目目录并进入:
mkdir my-go-app && cd my-go-app
- 初始化 Go 模块。与 TS 项目需要
npm init类似,Go 项目使用go mod init来初始化模块。模块名通常是你的代码仓库路径,比如github.com/xx/xx。这里我们用一个示例名字:
go mod init example.com/my-go-app
这个命令会创建一个 go.mod 文件,它等同于 TS 项目中的 package.json,负责定义模块和管理依赖。
- 创建
main.go文件,并写入以下内容:
package main
import "fmt"
func main() {
fmt.Println("Hello, Frontend Gopher!")
}
代码解读:
package main: 每个可执行的 Go 程序都必须有一个main包。import "fmt": 引入标准库中的fmt包,用于格式化输入输出。func main(): 程序的入口函数。
3. 运行与构建
在终端中,你有几种方式运行它:
- 直接运行:
go run .或go run main.go。这个命令会编译并直接运行,适合开发阶段快速验证。 - 构建成 可执行文件:
go build .。这个命令会生成一个名为my-go-app(或my-go-app.exe)的二进制文件。你可以直接./my-go-app运行它,并且可以把它部署到任何兼容的服务器上,无需安装 Go 环境。
4. 常用 Go 命令
go mod init xx: 初始化 GO 模块go get xx: 添加依赖go mod tidy: 整理依赖,安装依赖并移除未使用的依赖go run ./main.go: 编译并运行 GO 程序
核心语法与 TS 对比
目标:快速掌握 Go 的核心语法,并通过与 TS 的对比,理解其设计哲学的异同。
1. 变量、常量与类型
Go 是静态强类型语言,但其类型推导能让代码很简洁。
package main
import "fmt"
func main() {
// 完整声明
var name string = "GO"
// 类型推导
var version = 1.0
// 短变量声明 (最常用,只能在函数内部使用)
isReady := true
// 常量
const Pi = 3.14159
fmt.Println(name, version, isReady, Pi)
}
Go
// 短变量声明
message := "Hello"
count := 100
// 常量
const baseURL = "https://api.example.com"
TypeScript
// let/const 声明
let message = "Hello";
const count = 100;
// 常量
const baseURL = "https://api.example.com";
实践要点:在函数内部,优先使用 := 进行短变量声明,它能让代码更紧凑。只有当你需要预先声明一个变量(比如在 main 外部)时,才使用 var。
2. 切片 (Slice) 与映射 (Map)
- 切片 (slice) :Go 的动态数组,对应 TS 的
Array。 - 映射 (map) :键值对集合,对应 TS 的
Map或普通对象{}。
package main
import "fmt"
func main() {
// 切片
a1 := make([]int, 1, 10) // 创建一个长度为 1,容量为 10 的 slice
a1[0] = 1
fmt.Println("Slice:", a1, len(a1), cap(a1))
a2 := []int{1, 2, 3} // 创建并初始化
a2 = append(a2, 4) // 追加元素, 需要接收返回值
fmt.Println("Slice:", a2, len(a2), cap(a2))
subA2 := a2[1:3] // 得到一个包含元素 a2[1]、a2[2] 的 slice
fmt.Println("Sub Slice:", subA2, len(subA2), cap(subA2))
// 映射
m1 := make(map[string]int) // 创建一个映射
m1["Go"] = 1
fmt.Println("Map1:", m1)
m2 := map[string]interface{}{ // 创建并初始化一个映射,interface{} = any类型
"id": 1024,
"names": []string{"Go", "TypeScript"},
"flag": true,
}
fmt.Println("Map2:", m2)
}
- Go的数组与切片:GO的数组类型需固定长度,初始化一个数组示例
b := [3]int{1, 2, 3}。数组几乎不会直接使用,切片是引用类型,指向某个底层数组的一段,赋值/传参会共享底层数组。 - 切片 扩容:
append时如果超出切片的容量 (cap),Go 会重新分配一个更大的底层数组,并将原数据复制过去,这可能导致两个切片不再共享同一个底层数组,所以append需要接收返回值。
Go
// --- Slice ---
// 创建与追加
s := []int{1, 2}
s = append(s, 3)
// --- Map ---
// 创建与赋值
m := make(map[string]int)
m["one"] = 1
m["two"] = 2
// 查找与存在性判断
val, ok := m["one"]
if ok {
// val == 1
}
TypeScript
// --- Array ---
// 创建与追加
const arr = [1, 2];
arr.push(3);
// --- Object ---
// 创建与赋值
const obj: { [key: string]: number } = {};
obj["one"] = 1;
obj.two = 2;
// 查找与存在性判断
if ("one" in obj) {
// obj["one"]
}
make or 语法糖 : 有初始值用语法糖,要控制容量/长度用 make。
值类型 vs 引用类型
- 值类型:
int、float、string、bool、数组(array)、结构体(struct)。当把它们赋值给新变量或作为函数参数传递时,Go 会复制整个数据。 - 引用类型:切片 (
slice)、映射 (map)、通道 (channel)、指针 (pointer)、接口(interface)、函数(func)。赋值或传递时,复制的是指向底层数据结构的指针或引用。
3. 分支(IF、Switch)
Go 的分支语句在语法上比 TS 更简洁,并且 if 语句支持初始化语句,这使得代码更局部化、更清晰。
Go: if 与 switch
package main
import "fmt"
func main() {
// if 包含一个初始化语句
// 变量 score 只在 if-else 作用域内有效
if score := 85; score >= 90 {
fmt.Println("优秀")
} else if score >= 60 {
fmt.Println("及格")
} else {
fmt.Println("不及格")
}
// fmt.Println(score) // 在这里访问 score 会编译错误
// switch 语句
day := 3
switch day {
case 1, 2, 3, 4, 5: // 多个 case
fmt.Println("工作日")
case 6, 7:
fmt.Println("周末")
default:
fmt.Println("无效日期")
}
}
TypeScript : if/else 与 switch
let score = 85;
if (score >= 90) {
console.log("优秀");
} else if (score >= 60) {
console.log("及格");
} else {
console.log("不及格");
}
// switch 语句,每个 case 后都需要 break
const day = 3;
switch (day) {
case 1:
case 2:
case 3:
case 4:
case 5:
console.log("工作日");
break; // 如果没有 break,会继续执行下一个 case
case 6:
case 7:
console.log("周末");
break;
default:
console.log("无效日期");
}
实践要点:Go Switch 的不同之处
- 无需
break:Go 的switch在匹配到一个case后会自动终止,这与需要手动break的 TS 不同。 - 多值匹配:Go 可以在一个
case写多个条件,语法更简洁。
4. 循环 (For)与遍历(Range)
Go 在循环设计上遵循极简主义:只有 for 关键字。但它通过不同的写法,可以实现 TS 中 for、while 的所有功能。range 关键字则提供了统一且强大的遍历方式。
Go: for 的百变形态
package main
import "fmt"
func main() {
// 1. 经典三段式 for 循环
for i := 0; i < 3; i++ {
fmt.Println("经典循环:", i)
}
// 2. "while" 风格
j := 0
for j < 3 {
fmt.Println("While 风格:", j)
j++
}
// 3. 无限循环 (配合 break 使用)
for {
fmt.Println("这是一个无限循环,但马上会 break")
break
}
// 4. for-range 遍历slice/数组
nums := []int{10, 20, 30}
for index, value := range nums {
fmt.Printf("索引: %d, 值: %d\n", index, value)
}
// 遍历 map (注意:顺序不保证)
kvs := map[string]string{"a": "apple", "b": "banana"}
for k, v := range kvs {
fmt.Printf("%s -> %s\n", k, v)
}
}
TypeScript : 多种循环方式
// 经典 for
for (let i = 0; i < 3; i++) {
console.log("经典循环:", i);
}
// while
let j = 0;
while(j < 3) {
console.log("While 风格:", j);
j++;
}
// forEach 遍历数组
nums.forEach((value, index) => {
console.log(`索引: ${index}, 值: ${value}`);
});
// 遍历 map
const kvs = { a: "apple", b: "banana" };
Object.entries(kvs).forEach(([k, v]) => {
console.log(`${k} -> ${v}`);
});
5. 函数(Func)
Go 的函数设计非常务实,原生支持多返回值,这与它的错误处理哲学紧密相连。同时,它也支持可变参数、命名返回值和闭包等现代语言特性。
Go: 函数特性展示
package main
import "fmt"
// 1. 多返回值 (常用于返回结果和 error)
func divide(a, b int) (int, error) {
if b == 0 {
return 0, fmt.Errorf("除数不能为零")
}
return a / b, nil
}
// 2. 可变参数
func sum(nums ...int) int {
total := 0
for _, num := range nums {
total += num
}
return total
}
// 3. 命名返回值
// (result 和 err 就像函数体内的局部变量)
func divideNamed(a, b int) (result int, err error) {
if b == 0 {
err = fmt.Errorf("除数不能为零")
// return 等价于 return result, err
return
}
result = a / b
// return 等价于 return result, err
return
}
// 4. 闭包&匿名函数
func makeGreeter(greeting string) func(string) string {
return func(name string) string {
return greeting + ", " + name
}
}
func main() {
// 多返回值
res, err := divide(10, 2)
if err == nil {
fmt.Println("结果:", res)
}
// 可变参数
fmt.Println("总和:", sum(1, 2, 3))
// 切片展开
fmt.Println("总和:", sum([]int{1, 2, 3}...))
// 闭包
englishGreeter := makeGreeter("Hello")
fmt.Println(englishGreeter("World"))
}
TypeScript : 函数与箭头函数
// 1. 单返回值 (错误通过 throw 或 Promise.reject)
function divide(a: number, b: number): number {
if (b === 0) {
throw new Error("除数不能为零");
}
return a / b;
}
// 2. Rest 参数
function sum(...nums: number[]): number {
return nums.reduce((total, num) => total + num, 0);
}
// 3. (无直接对应)
// 4. 闭包
function makeGreeter(greeting: string): (name: string) => string {
return (name: string): string => {
return `${greeting}, ${name}`;
};
}
try {
console.log("结果:", divide(10, 2));
} catch (e: any) {
console.error(e.message);
}
console.log("总和:", sum(1, 2, 3));
console.log("总和:", sum(...[1, 2, 3] ))
const englishGreeter = makeGreeter("Hello");
console.log(englishGreeter("World"));
TS vs Go:函数与错误处理回顾
- 错误处理:这是 Go 最具争议也最独特的特性之一。Go 没有
try...catch,而是将error作为一个普通的值返回。TS 的异常倾向于“让错误冒泡”,由上层统一捕获。Go 强制你在错误发生的地方就地处理,虽然写起来啰嗦,但提升了程序的健壮性。 - 返回值:Go 函数可以返回多个值,这使得
(value, error)模式成为一种惯例,强制调用者立即关注并处理潜在的错误。 - 命名返回值:Go 的命名返回值在函数签名处预先声明了返回变量,可以使代码(尤其是包含复杂逻辑的函数)更清晰。当函数很简单时,不使用它也完全可以。
- 可变参数与展开 运算符:
...Go与TS用法基本一致,语法略有差异。Go不能展开对象/结构体。
6. 指针(Pointer)
指针存储的是一个变量的内存地址,与 C 语言指针类似。在 TS 中,对象、数组等都是引用,你传递它们时,函数内外操作的是同一个底层数据;在 Go 中,如果你想在函数内修改一个外部的值类型变量(如 struct、int),就必须传递它的指针。
Go: 值传递 vs 指针 传递
package main
import "fmt"
func doubleValue(val int) {
val *= 2 // 修改的是 val 的副本
}
func doubleValueByPtr(ptr *int) {
*ptr *= 2 // *ptr 解引用,修改的是原始地址上的值
}
func main() {
num := 10
doubleValue(num)
fmt.Println("值传递后:", num) // 输出 10,未改变
doubleValueByPtr(&num) // &num 取地址
fmt.Println("指针传递后:", num) // 输出 20,已改变
}
TypeScript : 对象引用
function doubleValue(obj: { value: number }) {
// obj 是一个引用,指向外部的 myObject
obj.value *= 2;
}
const myObject = { value: 10 };
doubleValue(myObject);
console.log("函数调用后:", myObject.value); // 输出 20,已改变
取地址 ( & ) 与解引用 ( * ) :
&操作符用于获取一个变量的内存地址,得到一个指针。*操作符用在指针变量前,用于获取该指针指向地址上存储的值(解引用)。
7. 结构体 (Struct) 与方法 (Method)
Go 没有 class,它的等价物是 struct + method。struct 用来组织数据,method 用来定义行为。
package main
import "fmt"
// 定义一个结构体
type User struct {
ID int
Name string
}
// 为 User 类型定义一个方法(值接收者)
// (u User) 被称为“接收者”
func (u User) Greet() {
fmt.Printf("Hello, my name is %s", u.Name)
// 这里的修改不会影响 main 函数里的 user
u.Name = "Anonymous"
}
// 为 User 类型定义一个方法(指针接收者)
func (u *User) SetName(newName string) {
u.Name = newName
}
func main() {
user := User{ID: 1, Name: "Alice"}
user.Greet()
fmt.Println("After Greet:", user.Name) // 仍然是 "Alice"
user.SetName("Bob")
fmt.Println("After SetName:", user.Name) // 变成了 "Bob"
}
值/ 指针 接收者:func (u User) 是值接收者,方法内得到的是 u 的副本。func (u *User) 是指针接收者,方法内得到的是 u 的引用,可以修改原始值。实践中,大部分情况都推荐使用指针接收者。
Go: Struct + Method
type Rectangle struct {
width, height float64
}
func (r Rectangle) Area() float64 {
return r.width * r.height
}
TypeScript : Class
class Rectangle {
width: number;
height: number;
constructor(w: number, h: number) {
this.width = w;
this.height = h;
}
Area(): number {
return this.width * this.height;
}
}
易踩坑:在 TS 中,几乎所有对象(object, array)都是引用。在 Go 中,结构体 struct 是值类型。这意味着如果你直接传递一个结构体,函数内对它的修改不会影响外部原始的结构体。如果想修改,需要传递它的指针。
8. 接口 (Interface)
Go 接口是运行时的契约。任何类型,只要它实现了接口中定义的所有方法,就被认为实现了该接口。
package main
import "fmt"
// 定义一个接口
type Greeter interface {
Greet() string
}
// 定义两种类型
type EnglishSpeaker struct{}
type ChineseSpeaker struct{}
// EnglishSpeaker 实现了 Greet() 方法
func (e EnglishSpeaker) Greet() string {
return "Hello, world!"
}
// ChineseSpeaker 实现了 Greet() 方法
func (c ChineseSpeaker) Greet() string {
return "你好,世界!"
}
// 这个函数接受任何实现了 Greeter 接口的类型
func SayHello(g Greeter) {
fmt.Println(g.Greet())
}
func main() {
// 两种类型都可以被传入 SayHello 函数
SayHello(EnglishSpeaker{})
SayHello(ChineseSpeaker{})
}
TS vs Go:接口
- Go interface 是运行时的类型 (编译进二进制里);TS interface 主要是编译期类型提示 ,编译成 JS 以后接口本身不存在 。
- Go interface:只关心方法,不关心字段,接口里只能放方法;TS interface 更像“对象形状”描述,既能描述字段也能描述方法 ,常用来约束对象结构。
9. 嵌入(组合)(Embedding)
在后端开发中,一个非常常见的场景是:所有业务实体都有 ID、创建时间等公共字段。用 Go 的做法,是定义一个基础结构体,然后通过嵌入在其他结构体中复用,并获得“字段和方法提升”的效果:
package main
import (
"fmt"
"time"
)
// 基础结构
type BaseModel struct {
ID int `json:"id"`
CreatedAt time.Time `json:"created_at"`
}
// 基础结构方法
func (m BaseModel) PrintID() {
fmt.Println("BaseModel ID:", m.ID)
}
// User 通过嵌入 BaseModel 复用字段和方法
type User struct {
BaseModel // 匿名字段:字段和方法会被“提升”
Name string `json:"name"`
Role string `json:"role"`
}
func main() {
u := User{
BaseModel: BaseModel{ID: 42}, // 显式初始化
Name: "Go",
Role: "admin",
}
// 字段与方法提升:可以直接在 User 上调用 BaseModel 的字段与方法
fmt.Println(u.ID, u.BaseModel.ID) // 字段提升: 输出:42 42
u.PrintID() // 方法提升,等价显式调用
u.BaseModel.PrintID() // 显式调用
}
在 TS 中你会“User extends BaseModel”,通过继承拿到父类的字段和方法;在 Go 中没有 extends,只能通过匿名嵌入来实现类似的效果。这看起来很像“继承”,但本质是“组合”。
Go:通过组合获得“继承感”
type BaseModel struct {
ID int `json:"id"`
}
func (m BaseModel) PrintID() {
fmt.Println("BaseModel ID:", m.ID)
}
type User struct {
BaseModel
Name string
}
// User 自动拥有 BaseModel 的字段和方法
TypeScript :class + extends
class BaseModel {
id: number;
constructor(id: number) {
this.id = id;
}
printID() {
console.log("BaseModel ID:", this.id);
}
}
class User extends BaseModel {
name: string;
constructor(id: number, name: string) {
super(id);
this.name = name;
}
}
TS vs Go:面向对象
- 继承:TS 支持类和继承。Go 没有继承,它推崇组合优于继承。你可以通过在一个
struct中嵌入另一个struct来实现行为的复用。 - this:TS 的
this比较复杂,Go 的方法接收者(u User)是显式的,更清晰。
三方库使用
Go 强大的标准库足以应对许多场景,这里介绍两个由字节跳动开源的库 gg与sonic。安装依赖:
go get github.com/bytedance/gg
go get github.com/bytedance/sonic
1. gg: Go 版 Lodash
gg 是一个基础库,它提供了大量类似 Lodash 的工具函数,涵盖类型转换、数组操作、条件运算等。以 gg/gslice为例, 它提供了 Map, Filter, Reduce 等常见的数组处理函数。
Go: 使用 gg /gslice
package main
import (
"fmt"
"github.com/bytedance/gg/gslice"
"github.com/bytedance/sonic"
)
type Stage struct {
ID int `json:"id"`
Status string `json:"status"`
}
func main() {
stages := []Stage{
{ID: 3, Status: "success"},
{ID: 1, Status: "running"},
{ID: 2, Status: "running"},
}
// Sort
gslice.SortBy(stages, func(s1, s2 Stage) bool {
return s1.ID < s2.ID
})
fmt.Println(sonic.MarshalString(stages))
// Filter
runningStages := gslice.Filter(stages, func(s Stage) bool {
return s.Status == "running"
})
fmt.Println(sonic.MarshalString(runningStages))
// Map
stageStatus := gslice.Map(stages, func(s Stage) string {
return s.Status
})
fmt.Println(stageStatus)
}
TypeScript : 使用数组方法或 Lodash
const stages = [
{ id: 3, status: "success" },
{ id: 1, status: "running" },
{ id: 2, status: "running" }
];
// Sort
stages.sort((s1, s2) => s1.id - s2.id);
console.log(JSON.stringify(stages));
// Filter
const runningStages = stages.filter(s => s.status === "running");
console.log(JSON.stringify(runningStages));
// Map
const stageStatus = stages.map(s => s.status);
console.log(stageStatus);
2. Sonic: 极致性能的 JSON 库
sonic 是一个由字节跳动开源的高性能 JSON 库。
Go: 使用 Sonic
package main
import (
"fmt"
"github.com/bytedance/sonic"
)
type User struct {
Name string `json:"name"`
Age int `json:"age"`
}
func main() {
user := User{Name: "Sonic", Age: 1}
// Marshal: struct -> JSON string
jsonData, err := sonic.MarshalString(user)
if err != nil {
panic(err)
}
fmt.Println(jsonData) // 输出: {"name":"Sonic","age":1}
// Unmarshal: JSON string -> struct
var decodedUser User
jsonStr := `{"name":"Sonic","age":1}`
err = sonic.UnmarshalString(jsonStr, &decodedUser)
if err != nil {
panic(err)
}
fmt.Println(decodedUser.Name) // 输出: Sonic
}
TypeScript : 原生 JSON
interface User {
name: string;
age: number;
}
const user: User = { name: "Sonic", age: 1 };
// stringify: object -> JSON string
const jsonString = JSON.stringify(user);
console.log(jsonString);
// parse: JSON string -> object
const jsonStr = '{"name":"Sonic","age":1}';
const decodedUser: User = JSON.parse(jsonStr);
console.log(decodedUser.name);
构建 API 服务
目标:使用 Go 标准库 net/http 搭建一个完整的、可运行的、包含增删查的简单 TodoList API。
我们将创建一个简单的内存 To-Do List 服务。项目结构如下:
todo-api/
├── go.mod
├── main.go # 程序入口,设置路由
├── handlers.go # HTTP 请求处理器
└── types.go # 数据类型定义
1. 初始化项目
mkdir todo-api
cd todo-api
go mod init example.com/todo-api
touch main.go handlers.go types.go
2. 安装依赖
go get github.com/bytedance/gg
3. 定义数据类型 (types.go)
package main
// Todo 定义了任务的数据结构
// `json:"..."` 标签用于控制 JSON 序列化/反序列化时的字段名
type Todo struct {
ID int `json:"id"`
Title string `json:"title"`
IsDone bool `json:"is_done"`
}
易踩坑:json 标签中的字段名是 Go 与外部世界(如 JSON)的契约。更重要的是,要被 encoding/json 包访问到的字段,其首字母必须大写(即“导出”的)。如果字段首字母小写,json 包将无法访问它,导致序列化结果为空或反序列化失败。
4. 编写处理器 (handlers.go)
这里我们先用一个简单的内存 map 来存储数据。
package main
import (
"encoding/json"
"net/http"
"strconv"
"github.com/bytedance/gg/gmap"
)
// 使用 map 作为内存数据库
var (
todos = make(map[int]Todo)
nextID = 1
)
// getTodosHandler 返回所有待办事项
func getTodosHandler(w http.ResponseWriter, r *http.Request) {
todoList := gmap.Values(todos)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(todoList)
}
// createTodoHandler 创建一个新的待办事项
func createTodoHandler(w http.ResponseWriter, r *http.Request) {
var todo Todo
if err := json.NewDecoder(r.Body).Decode(&todo); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
todo.ID = nextID
nextID++
todos[todo.ID] = todo
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(todo)
}
// deleteTodoHandler 删除一个待办事项
func deleteTodoHandler(w http.ResponseWriter, r *http.Request) {
idStr := r.URL.Path[len("/todos/"):]
id, err := strconv.Atoi(idStr) // 转换 string 为 int
if err != nil {
http.Error(w, "Invalid ID", http.StatusBadRequest)
return
}
if _, ok := todos[id]; !ok {
http.Error(w, "Todo not found", http.StatusNotFound)
return
}
delete(todos, id)
w.WriteHeader(http.StatusOK)
}
5. 设置路由并启动服务 (main.go)
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
// 创建一个新的 ServeMux,它是一个 HTTP 请求路由器
mux := http.NewServeMux()
// 注册处理器
mux.HandleFunc("GET /todos", getTodosHandler)
mux.HandleFunc("POST /todos", createTodoHandler)
mux.HandleFunc("DELETE /todos/{id}", deleteTodoHandler) // Go 1.22+ 支持路径参数
fmt.Println("Server is running on :8080")
// 启动 HTTP 服务
if err := http.ListenAndServe(":8080", mux); err != nil {
log.Fatalf("could not listen on port 8080: %v", err)
}
}
6. 运行和测试 API
-
在
todo-api目录下运行服务:go run . ``` -
打开另一个终端,使用
curl来测试:-
创建一个 ToDo:
curl -X POST -H "Content-Type: application/json" -d '{"title": "Learn Go", "is_done": false}' http://localhost:8080/todos # 返回创建的 ToDo,包含 ID # > {"id":1,"title":"Learn Go","is_done":false} ``` -
获取所有 ToDo:
curl http://localhost:8080/todos # 返回一个包含刚才创建的 ToDo 的 JSON 数组 # > [{"id":1,"title":"Learn Go","is_done":false}] ``` -
删除一个 ToDo(假设 ID 是 1):
curl -X DELETE http://localhost:8080/todos/1 ``` -
再次获取所有 ToDo:
curl http://localhost:8080/todos # 返回空数组 [] # > [] ```
-
结语
从 TS 到 Go,你跨越的不仅仅是一门语言,更是一种新的编程范式和工程哲学。希望这篇入门指南能成为你 Go 旅程的坚实第一步。如果你也喜欢通过看案例学习,在此推荐一个开源网站 Go By Example,期望对你有帮助。