从 TypeScript 到 Go - 入门篇

0 阅读17分钟

🚀 从 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!

  1. 创建一个新的项目目录并进入:
mkdir my-go-app && cd my-go-app
  1. 初始化 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,负责定义模块和管理依赖。

  1. 创建 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 引用类型

  • 值类型intfloatstringbool、数组(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 中 forwhile 的所有功能。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 中,如果你想在函数内修改一个外部的值类型变量(如 structint),就必须传递它的指针。

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 + methodstruct 用来组织数据,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 强大的标准库足以应对许多场景,这里介绍两个由字节跳动开源的库 ggsonic。安装依赖:

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

  1. todo-api 目录下运行服务:

      go run .
      ```
    
    
  2. 打开另一个终端,使用 curl 来测试:

    1. 创建一个 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}
       ```
      
      
    2. 获取所有 ToDo

       curl http://localhost:8080/todos
       # 返回一个包含刚才创建的 ToDoJSON 数组
       # > [{"id":1,"title":"Learn Go","is_done":false}]
       ```
      
      
    3. 删除一个 ToDo(假设 ID 是 1):

       curl -X DELETE http://localhost:8080/todos/1
       ```
      
      
    4. 再次获取所有 ToDo

       curl http://localhost:8080/todos
       # 返回空数组 []
       # > []
       ```
      

结语

从 TS 到 Go,你跨越的不仅仅是一门语言,更是一种新的编程范式和工程哲学。希望这篇入门指南能成为你 Go 旅程的坚实第一步。如果你也喜欢通过看案例学习,在此推荐一个开源网站 Go By Example,期望对你有帮助。