[7天实战入门Go语言后端] Day 1:Go 基础入门——环境、语法、错误处理与并发

关键词 (后端实战常用):Go 入门、fmt、Println、变量与函数、结构体、多返回值、返回指针、error、err 命名、错误包装、errors.Iserrors.As、goroutine、channel、WaitGroup、select、ready、context、Go 并发

获取实战代码 :如需在本地跑通本文示例,请克隆仓库 WenSongWang/go-quickstart-7days,本文示例在 day1 目录,克隆后在项目根目录执行下文中的命令即可。

阅读说明 :本文包含 Day 1 四个示例的完整代码 与逐段解读,仅读博客即可学完当日内容,无需打开项目。若再克隆仓库动手跑一遍,理解会更牢。


一、本篇目标

学完本文并跑通本目录下的示例代码,你将掌握:

模块 内容
环境 安装 Go、go run / go build
语法 变量、函数、结构体、多返回值、error
错误处理 errors.Is / errors.As、错误包装 fmt.Errorf("%w", err)(面试常问)
并发 goroutine、channel、sync.WaitGroupselectcontext 取消与超时(面试高频)

二、Go 下载与安装

若本机还未安装 Go,按下面步骤即可(建议 Go 1.21+)。

步骤 说明
1. 下载 打开官网 https://go.dev/dl/,按系统选安装包:Windows 选 .msi,macOS 选 .pkg.tar.gz,Linux 选 .tar.gz
2. 安装 Windows :双击 .msi 按提示安装(默认会写入 PATH)。macOS :双击 .pkg 或解压 .tar.gz/usr/local/goLinux :解压到 /usr/local,例如 sudo tar -C /usr/local -xzf go1.21.x.linux-amd64.tar.gz
3. 配置 PATH 安装程序通常已配置。若终端找不到 go,把 Go 的 bin 目录加入环境变量:Windows 在「环境变量」里加,Mac/Linux 在 ~/.bashrc~/.zshrc 里加 export PATH=$PATH:/usr/local/go/bin
4. 验证 新开终端执行:go version,能输出版本号即成功(如 go1.21.12 windows/amd64go1.22.x 等均可)。再执行 go env GOPATH 可看工作目录(可选)。

本系列所有命令均在项目根目录 (即 go-quickstart-7days 下)执行。

三、前置要求

  • 已完成上面「Go 下载与安装」或本机已有 Go 1.21+go version 可输出版本号)。
  • 若只读博客 :无其他要求。若想跑代码:需克隆本仓库并进入项目根目录。

四、示例与知识点(先混个眼熟)

示例目录 主要知识点
hello/ 最简程序:package mainmain()fmt.Println
basics/ 变量与短声明、结构体、*返回 User 指针 、多返回值 + error、if err != nilerr 常规命名
errors/ 错误包装 %werrors.Is / errors.As、自定义错误类型
concurrency/ goroutine、channel、sync.WaitGroup打印顺序不固定ready channel(与 Done() 区分)、select 超时、context

五、语法学习(实战常用,对应示例代码)

以下为写后端时最常碰到 的语法,与示例对应关系如下,可结合 day1/ 下四个示例对照阅读。

语法点 说明 对应示例
package main 可执行程序必须用 main 包,入口函数为 main() hello、basics、errors、concurrency
import 导入标准库或第三方包,如 "fmt""errors" 所有示例
fmt / Println / Printf 终端打印;Println 打一行,Printf%d%s 等占位符 hello、basics
变量 var 变量名 类型 = 值 或短声明 变量名 := 值(类型推断) basics
函数 func 函数名(参数) 返回值类型 { ... },多返回值用 (A, B) basics、errors、concurrency
结构体 type 名称 struct { 字段 类型 },用于请求/响应、实体等 basics(User
*返回指针 T 出错时只能返回 nil,只有指针能表示"没有";先判 err 再使用返回值(重点 basics(FindUser 返回 *User)
多返回值 + error Go 惯例:(结果, error),调用处 if err != nil { ... }err 是常规变量名,非关键字 basics、errors
错误包装 fmt.Errorf("描述: %w", err) 保留错误链,便于 errors.Is/errors.As errors
errors.Is(err, 目标) 判断是否为目标 sentinel 错误(包装后仍可匹配) errors
errors.As(err, &目标) 将错误断言为具体类型(如 *ValidationError errors
goroutine go 函数() 启动轻量级并发;多个 goroutine 打印顺序不固定 concurrency
channel make(chan int, 缓冲)close(ch)for v := range ch;信号 channel 用 chan struct{} concurrency
sync.WaitGroup Add(1)Done()Wait() 等待多个 goroutine 结束(Done 是方法名,与变量名区分) concurrency
select 多路复用 channel,常与 time.After 做超时;示例里用 ready 避免与 wg.Done() 混淆 concurrency
context context.WithTimeoutctx.Done()、取消/超时传递(面试高频);可改 doWithContext 里时间观察"超时"分支 concurrency

建议 :先看本表;若跑代码,再打开对应目录的 main.go 逐行对照。


六、核心概念与最小示例(不看代码也能懂)

下面用极短代码 + 一句话把几个最容易懵的点说清,只看博客即可建立印象。

为什么函数经常返回指针(*User)而不是值(User)?

因为出错时没有"结果"可返回,只能返回 nil;只有指针类型才有 nil,值类型没有。所以"查用户"这类函数会写成:

go 复制代码
func FindUser(id int) (*User, error) {
    if id <= 0 {
        return nil, errors.New("invalid id")  // 出错:第一个返回值用 nil
    }
    return &User{ID: id, Name: "张三"}, nil   // 正常:返回指针和 nil 错误
}

调用方必须先判断 err,再使用第一个返回值(否则可能是 nil,直接访问会 panic):

go 复制代码
u, err := FindUser(1)
if err != nil {
    return  // 或打日志
}
fmt.Println(u.Name)  // 这里 u 一定非 nil,安全

多返回值与 err 的常规写法

Go 里没有 try-catch,错误通过最后一个返回值 传出来;约定俗成把这个变量命名为 err(不是关键字)。写法固定成:

go 复制代码
u, err := FindUser(1)
if err != nil {
    // 处理错误
    return
}
// 用 u

错误包装:%w 与 errors.Is

在已有错误外包一层说明时,用 %w 可以把"里面的错误"保留住,后面还能用 errors.Is 认出是哪种错:

go 复制代码
return fmt.Errorf("查询用户: %w", ErrNotFound)   // 包装
// 调用方
if errors.Is(err, ErrNotFound) {
    // 知道是"未找到"
}

goroutine 与 channel 一句话

  • goroutinego 函数() 让这个函数在"后台"跑,不卡住当前代码;多个 goroutine 同时跑,所以打印顺序不固定是正常的。
  • channel :在多个 goroutine 之间传数据,ch <- 1 发,<-ch 收;close(ch) 后接收方 for v := range ch 会收完已有数据后结束。
  • select :在多个 channel 上"等谁先到",常配合 time.After(100*time.Millisecond) 做超时:要么业务先完成,要么超时走另一分支。
  • context :给"整条请求链路"设一个死线 (如 3 秒),下游查 DB、调接口都能收到"时间到了别干了"的信号,通过 ctx.Done() 判断。

七、Day 1 示例代码全文与逐段解读(只看博客即可对照学习)

下面把四个示例的完整代码贴出,并分段做简短解读。读者无需打开项目,按顺序看下去即可把 Day 1 学完。


示例 1:hello------最简程序

完整代码(即 day1/hello/main.go,含注释):

go 复制代码
package main   // 可执行程序必须是 main 包

import "fmt"   // 导入标准库 fmt,用于在终端打印

func main() {  // 程序入口,从 main 开始执行
	fmt.Println("Hello, Go! 7天入门后端开发")  // 打印一行到终端并换行
}

解读:

  • package main:可执行程序必须是 main 包。
  • import "fmt":导入标准库 fmt,用于在终端打印。
  • func main():程序入口,从 main 开始执行。
  • fmt.Println(...):打印一行字符串到终端并换行。运行后终端会输出:Hello, Go! 7天入门后端开发

示例 2:basics------变量、结构体、返回指针、多返回值与 err

完整代码(即 day1/basics/main.go,含注释):

go 复制代码
package main

import (
	"errors"
	"fmt"
)

// User 结构体:把多个字段绑在一起,表示"用户"(后端常用来表示请求/响应或数据库一行)
type User struct {
	ID   int
	Name string
}

// FindUser 返回 *User(指针)而不是 User:出错时只能返回 nil,只有指针才有 nil;调用方要先判 err 再用 u
func FindUser(id int) (*User, error) {
	if id <= 0 {
		return nil, errors.New("invalid user id")  // 出错:第一个返回值用 nil
	}
	return &User{ID: id, Name: "张三"}, nil       // 正常:& 取地址得到 *User
}

func main() {
	var a int = 1   // 显式类型
	b := 2          // 短声明,类型自动推断
	fmt.Println("a+b =", a+b)

	u, err := FindUser(1)   // err 是常规命名(非关键字);u 是 *User
	if err != nil {         // 必须先判断 err,再使用 u
		fmt.Println("错误:", err)
		return
	}
	fmt.Printf("用户: ID=%d Name=%s\n", u.ID, u.Name)  // %d 数字 %s 字符串 \n 换行

	_, err = FindUser(-1)   // _ 忽略第一个返回值,只关心 err
	if err != nil {
		fmt.Println("预期错误:", err)
	}
}

解读:

  • User 结构体:把 ID、Name 绑成一种类型,表示"用户";后端里常用来表示请求/响应或数据库一行。
  • *FindUser 返回 User :出错时没有用户可返回,只能返回 nil,只有指针类型才有 nil;正常时返回 &User{...}, nil。调用方必须先 if err != nil 再使用 u,否则 u 可能为 nil 会 panic。
  • var a / b :=var a int = 1 显式类型,b := 2 短声明、类型推断。
  • u, err := FindUser(1) :err 是 Go 里错误返回值的常规命名(非关键字)。先判 err,再用 u;fmt.Printf%d 数字、%s 字符串、\n 换行。
  • _, err = FindUser(-1) :用 _ 忽略第一个返回值,只关心 err;故意传 -1 触发错误分支,打印"预期错误"。

示例 3:errors------错误包装、errors.Iserrors.As

完整代码(即 day1/errors/main.go,含注释):

go 复制代码
package main

import (
	"errors"
	"fmt"
)

// 定义"已知错误",方便后面用 errors.Is 判断
var ErrNotFound = errors.New("not found")
var ErrInvalidID = errors.New("invalid id")

// ValidationError 自定义错误类型,带字段,便于 errors.As 取出具体信息
type ValidationError struct {
	Field string
}

// 实现 Error() 后 *ValidationError 就是一种 error
func (e *ValidationError) Error() string {
	return "validation error: " + e.Field
}

// 用 %w 包装底层错误,保留错误链,这样 errors.Is 即使包了一层也能匹配
func FindUser(id int) error {
	if id <= 0 {
		return fmt.Errorf("find user: %w", ErrInvalidID)
	}
	if id > 100 {
		return fmt.Errorf("find user: %w", ErrNotFound)
	}
	if id == 50 {
		return fmt.Errorf("find user: %w", &ValidationError{Field: "id"})
	}
	return nil
}

func main() {
	// errors.Is:判断是不是某个已知错误(包装后仍可认)
	err := FindUser(-1)
	if errors.Is(err, ErrInvalidID) {
		fmt.Println("检测到 ErrInvalidID:", err)
	}

	err = FindUser(101)
	if errors.Is(err, ErrNotFound) {
		fmt.Println("检测到 ErrNotFound:", err)
	}

	// errors.As:把错误转成具体类型,取出里面的字段
	err = FindUser(50)
	var valErr *ValidationError
	if errors.As(err, &valErr) {
		fmt.Println("校验错误字段:", valErr.Field)
	}
}

解读:

  • ErrNotFound / ErrInvalidID :用 errors.New 定义"已知错误",方便在调用方用 errors.Is 统一判断。
  • ValidationError :自定义错误类型,带 Field 字段;实现 Error() string 后就是一种 error,便于 errors.As 取出具体信息。
  • fmt.Errorf("...: %w", err) :用 %w 包装底层错误,保留错误链,这样 errors.Is(err, ErrNotFound) 即使包了一层也能匹配。
  • errors.Is(err, ErrInvalidID):判断 err 是不是某个已知错误(包装后仍可认)。
  • errors.As(err, &valErr):把 err 转成 *ValidationError,成功时 valErr.Field 即"哪个字段校验失败"。面试常问 Is/As 区别。

示例 4:concurrency------goroutine、channel、WaitGroup、select、context

完整代码(即 day1/concurrency/main.go,含注释):

go 复制代码
package main

import (
	"context"
	"fmt"
	"sync"
	"time"
)

func main() {
	// ---------- 1. goroutine + channel:后台发数据,主流程收 ----------
	ch := make(chan int, 2)   // 带缓冲的 channel,能存 2 个 int
	go func() {
		ch <- 1
		ch <- 2
		close(ch)              // 发完后关闭,for range 会收完已有的就结束
	}()
	for v := range ch {        // 一直从 ch 收,直到 ch 被 close
		fmt.Println("channel:", v)
	}

	// ---------- 2. sync.WaitGroup:等 3 个 goroutine 都干完再继续;打印顺序不固定是正常的 ----------
	var wg sync.WaitGroup
	for i := 0; i < 3; i++ {
		wg.Add(1)               // 要等的任务 +1
		go func(id int) {       // 把 i 传进去,避免闭包捕获同一个 i
			defer wg.Done()     // 函数退出前把任务数 -1(Done 是方法名,不是变量)
			time.Sleep(10 * time.Millisecond)
			fmt.Println("worker", id)
		}(i)
	}
	wg.Wait()                  // 阻塞直到 Add 和 Done 次数相等
	fmt.Println("all workers done")

	// ---------- 3. select:多路等待,谁先到执行谁;变量名用 ready 避免和 wg.Done() 混淆 ----------
	ready := make(chan struct{})   // 空结构体 channel 只当"信号"用
	go func() {
		time.Sleep(50 * time.Millisecond)
		close(ready)               // 关闭后 <-ready 会收到信号
	}()
	select {
	case <-ready:
		fmt.Println("done")       // 50ms 内收到 ready,走这里
	case <-time.After(100 * time.Millisecond):
		fmt.Println("timeout")    // 100ms 还没收到 ready,走这里(超时)
	}

	// ---------- 4. context:20ms 后自动取消;defer cancel() 释放内部计时器 ----------
	ctx, cancel := context.WithTimeout(context.Background(), 20*time.Millisecond)
	defer cancel()
	result := doWithContext(ctx)
	fmt.Println("context result:", result)
}

// select 等两路:ctx 超时 或 10ms"完成";10 < 20 所以通常返回 "ok";若改成 30ms 会先超时返回 "cancelled"
func doWithContext(ctx context.Context) string {
	select {
	case <-ctx.Done():
		return "cancelled: " + ctx.Err().Error()
	case <-time.After(10 * time.Millisecond):
		return "ok"
	}
}

解读:

  • 1. goroutine + channelmake(chan int, 2) 带缓冲的 channel;go func(){ ch<-1; ch<-2; close(ch) }() 在后台发数据,主流程 for v := range ch 收完 1、2 后因 ch 已 close 而结束。
  • 2. WaitGroupwg.Add(1)defer wg.Done()wg.Wait() 等待 3 个 goroutine 都结束。go func(id int)(i) 把 i 传进去,避免闭包捕获同一个 i。多个 goroutine 并发执行,"worker 0/1/2" 打印顺序不固定是正常的。
  • 3. select :变量名用 ready 而不是 done,避免和上面的 wg.Done() 方法混淆。ready 在 50ms 后被 close,主流程 select 等 <-ready 或 100ms 超时;通常 50ms 先到会打印 "done"。
  • 4. contextcontext.WithTimeout(context.Background(), 20*time.Millisecond) 创建 20ms 后取消的 context;defer cancel() 释放内部计时器。doWithContext 里 select 等两路:ctx.Done()(超时/取消)或 10ms 后"完成";因 10ms < 20ms,通常返回 "ok"。若把 10 改成 30,会先超时走 ctx.Done() 返回 "cancelled"。

八、运行当天代码

本日示例用 go run 即可(边编译边运行,不生成可执行文件)。若想生成可执行文件 再用命令行运行,可用 go build

方式一:go run(推荐,适合学习)

项目根目录执行:

bash 复制代码
go run ./day1/hello
go run ./day1/basics
go run ./day1/errors
go run ./day1/concurrency

方式二:go build(生成可执行文件)

在项目根目录执行后,当前目录会多出 hellobasicserrorsconcurrency(Windows 下为 hello.exe 等),可直接双击或命令行运行:

bash 复制代码
go build -o hello ./day1/hello
go build -o basics ./day1/basics
go build -o errors ./day1/errors
go build -o concurrency ./day1/concurrency
./hello        # Linux/macOS;Windows 下为 hello.exe
./basics
# ...

建议按顺序运行:先 hellobasicserrorsconcurrency,对应「入门 → 语法 → 错误处理 → 并发」。

九、学习建议

  1. 先跑再改 :每个示例都只有少量代码,跑通后改一改(变量名、返回值、错误信息),再 go run 看效果。
  2. 重点看basics返回 *User 指针 的原因;errors 里的错误包装与 errors.Is/errors.Asconcurrency 里的 goroutine、channel、context,面试经常问到。
  3. 易混淆点concurrency 里 "worker 0/1/2" 打印顺序不固定是正常的;select 示例用的变量名是 ready (与 wg.Done() 方法区分);context 示例可把 doWithContext 里的 10ms 改成 30ms,观察"超时"分支。
  4. 遇到报错先看终端提示,再对照本目录下的 .go 源码理解。

十、小结

Day 1 打好「环境 + 语法 + 错误 + 并发」基础,后面 Day 2 的 HTTP、Day 5 的 context 中间件都会用到。只看本文也能建立正确概念;若再克隆仓库把四个示例跑一遍,理解会更牢,再进入 Day 2 会更顺。

相关推荐
~央千澈~2 小时前
抖音弹幕游戏开发之第4集:第一个WebSocket连接·优雅草云桧·卓伊凡
开发语言·python·php
WHS-_-20222 小时前
Sensing in Bistatic ISAC Systems With Clock Asynchronism
开发语言·php
IvanCodes2 小时前
十、C语言文件与标准 I/O
c语言·开发语言
新缸中之脑2 小时前
SaaS 大灭绝
开发语言·ios·swift
娇娇乔木2 小时前
模块十四--String/StringBuilder--尚硅谷Javase笔记总结
java·开发语言
脏脏a2 小时前
【C++篇】面向对象编程的三大特性:深入解析继承机制
开发语言·c++·继承·组合
csdn2015_2 小时前
mybatisplus 获得新增id
java·开发语言·mybatis
ghie90902 小时前
差速转向移动机器人基于速度的动力学模型与自适应控制器 MATLAB实现
开发语言·matlab
❀͜͡傀儡师2 小时前
SpringBoot渗透扫描Scan工具
java·spring boot·后端