3142 字
16 分钟
Go 语言从零开始完整教程(小白也能看懂)

Go 语言从零开始完整教程(小白也能看懂)#

前面我们学了前端(JavaScript、TypeScript、Vue/React)。现在开始学后端。后端语言里,Go(也叫 Golang) 近几年特别火——很多大公司的核心服务(比如 Docker、Kubernetes、七牛、字节的部分服务)都用它写的。

这一篇从零开始,手把手带你入门 Go,最后写一个真正的 HTTP 服务器。


一、Go 是什么?为什么值得学?#

Go 是 Google 在 2009 年发布的一门编程语言,专门解决大型服务器软件的开发痛点。

Go 的三大优点:

  1. 性能高:编译型语言,接近 C 的速度,比 Python/Node.js 快得多
  2. 并发简单:天生的 goroutine 并发模型,写并发代码比别的语言容易十倍
  3. 部署简单:编译出来是一个独立的可执行文件,扔到服务器就能跑,不用装环境

对比一下(理解用,不用背):

语言类型适合场景特点
Python解释型脚本、AI、快速开发写起来快,跑起来慢
JavaScript/Node解释型Web 全栈前后端同语言
Go编译型后端服务、云原生快、稳、简单

学 Go 的最低要求: 有任意一门编程语言的基础(我们前面学的 JS 就够)。Go 的语法比 JS 更简单,但”思维”要从浏览器转向服务器。


二、安装 Go#

下载安装#

去 Go 官网(go.dev/dl)下载对应系统的安装包,一路点下一步即可。

装好后打开终端验证:

Terminal window
go version
# 输出类似:go version go1.24.0 linux/amd64

能看到版本号就是装好了。

验证开发环境:写第一个 Go 程序#

新建一个文件夹,比如 hello,在里面新建文件 main.go

package main // 每个程序必须有一个 main 包
import "fmt" // 引入标准库 fmt(格式化输出)
func main() { // 程序入口
fmt.Println("你好,Go!")
}

在终端运行:

Terminal window
go run main.go
# 输出:你好,Go!

两个命令要分清:

Terminal window
go run main.go # 直接运行(开发时用)
go build main.go # 编译成可执行文件(发布时用,会生成一个 main 可执行文件)

Go 的三个强制规则(新手最容易犯的错):

  1. { 必须和 func 同一行,不能换行
  2. 导入的包必须使用,没用就编译报错
  3. 声明的变量必须使用,没用就编译报错

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 是强类型语言,intstring 不能直接相加,必须转:

var age int = 18
fmt.Println("年龄:" + string(age)) // ❌ 错误:string(18) 不是转字符串!

正确转换:

var age int = 18
fmt.Println("年龄:" + strconv.Itoa(age)) // 用 strconv 包转字符串
// 输出:年龄:18
import "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 的区别:

  1. 条件不用加括号if score >= 90 而不是 if (score >= 90)
  2. { 必须跟在 if 后面同一行
  3. 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 := 0
for n < 5 {
n++
fmt.Println(n)
}
// 3. 无限循环(用 break 跳出)
count := 0
for {
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、3
part := nums[1:4]
fmt.Println(part) // [20 30 40]
// 遍历求和
sum := 0
for _, 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 true

map[string]int 的意思是:键是 string,值是 int。


八、函数#

基本写法:参数和返回值都要写类型#

package main
import "fmt"
// 两个参数都是 int,返回值是 int
func 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 项目是一个”模块”。在项目文件夹里执行:

Terminal window
go mod init myapp

会生成一个 go.mod 文件,记录项目名和依赖。

自定义包#

比如建一个 utils 文件夹,里面放 math.go

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)
}

运行:

Terminal window
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 后端最重要的搭档,把数据真正存起来。

Go 语言从零开始完整教程(小白也能看懂)
https://021028.xyz/posts/default/74/
作者
021028
发布于
2026-08-11
许可协议
CC BY-NC-SA 4.0