Go 语言从零开始完整教程(小白也能看懂)
前面我们学了前端(JavaScript、TypeScript、Vue/React)。现在开始学后端。后端语言里,Go(也叫 Golang) 近几年特别火——很多大公司的核心服务(比如 Docker、Kubernetes、七牛、字节的部分服务)都用它写的。
这一篇从零开始,手把手带你入门 Go,最后写一个真正的 HTTP 服务器。
一、Go 是什么?为什么值得学?
Go 是 Google 在 2009 年发布的一门编程语言,专门解决大型服务器软件的开发痛点。
Go 的三大优点:
- 性能高:编译型语言,接近 C 的速度,比 Python/Node.js 快得多
- 并发简单:天生的 goroutine 并发模型,写并发代码比别的语言容易十倍
- 部署简单:编译出来是一个独立的可执行文件,扔到服务器就能跑,不用装环境
对比一下(理解用,不用背):
| 语言 | 类型 | 适合场景 | 特点 |
|---|---|---|---|
| Python | 解释型 | 脚本、AI、快速开发 | 写起来快,跑起来慢 |
| JavaScript/Node | 解释型 | Web 全栈 | 前后端同语言 |
| Go | 编译型 | 后端服务、云原生 | 快、稳、简单 |
学 Go 的最低要求: 有任意一门编程语言的基础(我们前面学的 JS 就够)。Go 的语法比 JS 更简单,但”思维”要从浏览器转向服务器。
二、安装 Go
下载安装
去 Go 官网(go.dev/dl)下载对应系统的安装包,一路点下一步即可。
装好后打开终端验证:
go version# 输出类似:go version go1.24.0 linux/amd64能看到版本号就是装好了。
验证开发环境:写第一个 Go 程序
新建一个文件夹,比如 hello,在里面新建文件 main.go:
package main // 每个程序必须有一个 main 包
import "fmt" // 引入标准库 fmt(格式化输出)
func main() { // 程序入口 fmt.Println("你好,Go!")}在终端运行:
go run main.go# 输出:你好,Go!两个命令要分清:
go run main.go # 直接运行(开发时用)go build main.go # 编译成可执行文件(发布时用,会生成一个 main 可执行文件)Go 的三个强制规则(新手最容易犯的错):
{必须和func同一行,不能换行- 导入的包必须使用,没用就编译报错
- 声明的变量必须使用,没用就编译报错
Go 编译器很严格,编译期就把错误拦下来,这反而是好事。
三、变量和数据类型
声明变量:三种写法
package main
import "fmt"
func main() { // 写法一:var + 类型 var name string = "小明"
// 写法二:省略类型(Go 自动推断) var age = 18
// 写法三::= 简写(最常用,只能在函数内部用) city := "北京"
fmt.Println(name, age, city) // 小明 18 北京}重点: 在 Go 里,:= 是”声明并赋值”的简写,看到 := 就知道这是个新变量。
常用类型
var score int = 95 // 整数var price float64 = 3.14 // 小数var name string = "小明" // 字符串var isOK bool = true // 布尔var nothing = 0 // 推断为 int注意:Go 是强类型语言,int 和 string 不能直接相加,必须转:
var age int = 18fmt.Println("年龄:" + string(age)) // ❌ 错误:string(18) 不是转字符串!正确转换:
var age int = 18fmt.Println("年龄:" + strconv.Itoa(age)) // 用 strconv 包转字符串// 输出:年龄:18import "strconv"
// 字符串 → 数字num, err := strconv.Atoi("42") // num = 42
// 数字 → 字符串s := strconv.Itoa(42) // s = "42"四、条件判断:if / else
package main
import "fmt"
func main() { score := 85
if score >= 90 { fmt.Println("优秀") } else if score >= 60 { fmt.Println("及格") } else { fmt.Println("不及格") }}Go 和 JS 的区别:
- 条件不用加括号:
if score >= 90而不是if (score >= 90) {必须跟在 if 后面同一行- if 条件里可以直接声明变量:
if score := 85; score >= 60 { fmt.Println("及格", score) // 这个 score 只在 if 里能用}五、循环:只有 for,没有 while
Go 只有一个循环关键字:for。但用不同的写法,可以表示所有循环。
三种 for 写法
// 1. 经典计数循环(像 JS 的 for)for i := 1; i <= 5; i++ { fmt.Println(i)}
// 2. 像 while 的循环n := 0for n < 5 { n++ fmt.Println(n)}
// 3. 无限循环(用 break 跳出)count := 0for { count++ if count >= 3 { break // 跳出循环 } fmt.Println("第", count, "次")}遍历数组/切片:for range
names := []string{"小明", "小红", "小刚"}
for index, name := range names { fmt.Println(index, name)}// 输出:// 0 小明// 1 小红// 2 小刚六、数组和切片:动态数组
Go 的数组长度是固定的,一般都用切片(slice)——长度可以变化的数组。
// 数组:长度固定var arr [3]int = [3]int{1, 2, 3}
// 切片:长度可变(最常用)nums := []int{10, 20, 30}
// 追加元素nums = append(nums, 40)fmt.Println(nums) // [10 20 30 40]切片的常用操作
nums := []int{10, 20, 30, 40, 50}
// 切片截取:左闭右开 [1:4] 取下标 1、2、3part := nums[1:4]fmt.Println(part) // [20 30 40]
// 遍历求和sum := 0for _, n := range nums { sum += n}fmt.Println(sum) // 150注意 _ 下划线:Go 里变量声明了必须用,但遍历时不想用 index,就用 _ 占位(读作”忽略”)。
七、Map:键值对(像 JS 的对象)
// 声明并赋值student := map[string]int{ "小明": 95, "小红": 88,}
// 取值fmt.Println(student["小明"]) // 95
// 添加/修改student["小刚"] = 76
// 删除delete(student, "小红")
// 判断 key 是否存在score, exists := student["小明"]fmt.Println(score, exists) // 95 truemap[string]int 的意思是:键是 string,值是 int。
八、函数
基本写法:参数和返回值都要写类型
package main
import "fmt"
// 两个参数都是 int,返回值是 intfunc add(a int, b int) int { return a + b}
func main() { result := add(3, 5) fmt.Println(result) // 8}多个返回值(Go 的特色)
Go 函数可以一次返回多个值,最常见的用途是返回结果 + 错误:
func divide(a, b int) (int, error) { if b == 0 { return 0, fmt.Errorf("除数不能为 0") } return a / b, nil // nil 表示没有错误}
func main() { result, err := divide(10, 0) if err != nil { fmt.Println("出错了:", err) // 出错了:除数不能为 0 return } fmt.Println(result)}这是 Go 的风格: 每个可能出错的函数都返回 error,调用方必须检查。代码啰嗦一点,但错误永远不会被悄悄忽略。
九、结构体 struct:自定义数据类型
Go 没有 class,用 struct 来定义”对象”的形状:
package main
import "fmt"
// 定义一个 Student 结构体type Student struct { Name string Age int Score float64}
func main() { // 创建结构体实例 s1 := Student{ Name: "小明", Age: 18, Score: 95.5, }
fmt.Println(s1.Name) // 小明 fmt.Println(s1.Score) // 95.5
// 修改字段 s1.Score = 98 fmt.Println(s1) // {小明 18 98}}给结构体绑定方法
type Student struct { Name string Score float64}
// 绑定方法:在 func 和函数名之间加 (s Student)func (s Student) SayHi() { fmt.Printf("你好,我是%s,成绩%.1f分\n", s.Name, s.Score)}
func main() { s := Student{Name: "小红", Score: 88} s.SayHi() // 你好,我是小红,成绩88.0分}和 JS 对比: 这就像给 class 加了一个方法,只是 Go 的写法更直接。
十、指针:Go 里最难懂的概念(用比喻讲)
指针就是变量的地址——“这个变量的数据放在内存的哪里”。
想象快递柜:每个柜子有编号(地址),包裹(数据)放在柜子里。指针就是”柜子编号”,通过编号能打开柜子拿包裹。
func main() { age := 18
// & 取地址,* 取值 p := &age // p 是 age 的地址(指针) fmt.Println(p) // 0xc000012098(一串内存地址,每次运行不同)
fmt.Println(*p) // 18(通过地址取到值) *p = 20 // 通过地址修改值 fmt.Println(age) // 20(原来的变量也变了)}为什么需要指针?看这个经典问题
func changeName(name string) { name = "改了"}
func main() { name := "小明" changeName(name) fmt.Println(name) // 小明(没变!函数里改的是副本)}函数参数默认是值传递——复制一份进去,函数里改的是副本,外面不受影响。想真正修改外部变量,就传指针:
func changeName(name *string) { *name = "改了"}
func main() { name := "小明" changeName(&name) fmt.Println(name) // 改了(这次真的变了)}初学阶段记住: 需要”函数内部修改外部变量”时用指针;正常读取数据不用。
十一、错误处理:Go 的哲学
Go 不用 try/catch,而是显式返回错误:
package main
import ( "errors" "fmt")
func checkAge(age int) error { if age < 0 { return errors.New("年龄不能为负数") } if age < 18 { return errors.New("未成年人") } return nil // 没有错误}
func main() { err := checkAge(-5) if err != nil { fmt.Println("错误:", err) // 错误:年龄不能为负数 }}惯例: 函数能出错就返回 (结果, error),调用方用 if err != nil 检查。这是 Go 代码里出现频率最高的写法。
十二、并发:goroutine 和 channel(Go 的王牌)
goroutine:轻量级线程
在函数调用前加一个 go 关键字,就开了一个 goroutine,函数会在后台并发执行:
package main
import ( "fmt" "time")
func printMsg(msg string) { for i := 0; i < 3; i++ { fmt.Println(msg, i) time.Sleep(100 * time.Millisecond) // 睡 0.1 秒 }}
func main() { go printMsg("协程A") // 后台并发执行 go printMsg("协程B") time.Sleep(500 * time.Millisecond) // 等主函数别提前退出 fmt.Println("主函数结束")}两个 printMsg 会同时打印,顺序交错。这就是并发。
channel:协程之间通信
多个 goroutine 之间传数据,用 channel(管道)。它像一个传送带:一边放,一边取。
package main
import "fmt"
func main() { // 创建一个 int 类型的管道 ch := make(chan int)
// 开一个 goroutine:往管道里放数据 go func() { ch <- 42 // 放进管道 }()
// 主函数:从管道取出 value := <-ch fmt.Println(value) // 42}ch <- 42 是放,<-ch 是取。放和取会”互相等待”——没放进去时取的人等着,没取走时放的人等着。这个特性让并发代码天然安全,不会数据打架。
并发经典场景:并发请求多个网址
package main
import ( "fmt" "time")
func fetch(url string, ch chan string) { time.Sleep(500 * time.Millisecond) // 模拟请求耗时 ch <- url + " 请求完成"}
func main() { ch := make(chan string)
urls := []string{"a.com", "b.com", "c.com"} for _, url := range urls { go fetch(url, ch) // 三个请求同时发 }
// 收三次结果 for i := 0; i < len(urls); i++ { fmt.Println(<-ch) }}三个请求同时发起,总共约 0.5 秒就全部完成(如果是串行要 1.5 秒)。这就是并发的威力。
十三、包和模块:组织代码
Go 模块(go.mod)
每个 Go 项目是一个”模块”。在项目文件夹里执行:
go mod init myapp会生成一个 go.mod 文件,记录项目名和依赖。
自定义包
比如建一个 utils 文件夹,里面放 math.go:
package utils
func Double(n int) int { return n * 2}注意: 函数名大写开头表示”公开”(别的包能用),小写开头是”私有”(只能本包用)。
在 main.go 里使用:
package main
import ( "fmt" "myapp/utils")
func main() { fmt.Println(utils.Double(21)) // 42}十四、实战:写一个 HTTP 服务器
Go 的标准库自带 HTTP 服务器功能,不需要装任何框架:
package main
import ( "encoding/json" "fmt" "net/http")
// 定义一个用户结构体type User struct { ID int `json:"id"` Name string `json:"name"`}
func main() { // 路由一:首页 http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "你好,Go 服务器!") })
// 路由二:返回 JSON 数据(模拟后端接口) http.HandleFunc("/api/user", func(w http.ResponseWriter, r *http.Request) { user := User{ID: 1, Name: "小明"}
w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(user) // 把结构体转成 JSON 返回 })
fmt.Println("服务器启动:http://localhost:8080") http.ListenAndServe(":8080", nil)}运行:
go run main.go浏览器打开 http://localhost:8080 看到”你好,Go 服务器!“;打开 http://localhost:8080/api/user 看到 JSON:
{"id":1,"name":"小明"}你现在写的就是一个真正的后端接口! 它和你前面学的 fetch 正好能对上:
// 前端 JS 请求这个接口let res = await fetch("http://localhost:8080/api/user");let user = await res.json();console.log(user.name); // 小明前后端打通了——前端用 fetch 请求,后端用 Go 提供数据。
十五、总结
这一篇我们从零学会了 Go:
- Go 是什么:Google 出品的高性能后端语言,快、简单、并发强
- 安装与运行:go run 运行,go build 编译
- 基础语法:变量(:=)、类型、if/for、数组切片、map
- 函数:多返回值、错误处理哲学
- 结构体 struct:自定义数据类型 + 方法
- 指针:地址与值传递
- 错误处理:if err != nil 检查
- 并发:goroutine + channel
- HTTP 服务器:标准库直接写后端接口
下一篇学 PostgreSQL(PGSQL)——Go 后端最重要的搭档,把数据真正存起来。