编写一个简单的 go 项目
Go 简介
Go(又称 Golang)是由 Google 开源的一门编译型、静态类型、并发友好的编程语言。它旨在提高软件开发效率,特别是在大规模系统和基础设施软件中表现优异。
Go 的语言特点如下:
- 编译速度快,接近于脚本开发体验。
- 语法简洁清晰,仅有 25 个关键字,平易上手。
- 内建并发支持,通过 goroutine 和 channel 就可以实现并发编程。
- 支持跨平台编译,一次编写,多平台部署。
- 标准库丰富,涵盖大多数常用功能。
- 支持模块化管理,让版本管理和依赖更加稳定可靠。
Go 的应用场景:
- Web 应用和 API 服务。
- 命令行工具。
- 云原生和微服务。
- 网络服务与中间件。
Go 鼓励通过构建包,来组织代码和模块复用,你可以编写自己的工具库,算法模块或通用逻辑,并在多个项目中复用,并开源给他人使用。
接下来,我们将一步步创建一个简单的 Go 包,涵盖从项目初始化到测试、发布的完整流程,帮助你快速上手 Go 的项目开发。
Go 语言快速上手
安装环境
在官网下载 Go 环境。
安装完毕后,在控制台执行 go version,正常打印版本号就是安装成功了。
$ go version
go version go1.24.3 windows/386
Go 示例
Go 语言的代码文件采用的是 .go 后缀名。
以下是一个简单的 Go 语言示例。
package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
}
package main将这份文件声明为main包。每个 Go 程序都必须有一个main包。import "fmt"导入 Go 标准库fmt。func main()声明一个名为main的函数,main函数是 Go 程序的入口。fmt.Println("Hello, World!")调用fmt模块的Println方法打印输出。
新建一份名为 main.go 的文件,将示例代码复制进去,随后打开命令行,执行 go run main.go 就能够执行这段 Go 代码。
$ go run main.go
Hello, World!
基本语法
变量声明
变量,用于存储数据并使用。
// Go 语言中,使用 var 声明变量,变量类型后置
var name string = "ZhangSan"
// 使用海象运算符可以简略声明并自动推导变量类型
age := 18
// 使用 const 声明一个常量
const Pi = 3.14
函数
用于封装一段代码,让代码可复用而不需要重写,代码将根据参数的不同得出不同的结果。
// 使用 func 定义一个函数
// Go 语言中的函数没有默认参数
func add(num1 int, num2 int) int {
return num1 + num2
}
// 函数可以返回另一个函数
// 返回的函数会使用到外部函数的变量
// 通常这个函数被称为闭包
func create_queue() func(num int) []int {
queue := []int{}
return func(num int) []int {
queue = append(queue, num)
return queue
}
}
条件语句
Go 语言的 else 必须紧跟在 if 的花括号后面,不允许换行,否则编译器会报错。
if age > 20 {
fmt.Println("条件一通过")
} else if age > 18 {
fmt.Println("条件二通过")
} else {
fmt.Println("条件都不通过")
}
switch 语句会自上往下逐个匹配 case ,直到匹配成功。
case 默认自带 break 无需手动增加,如果需要执行下一个 case 则使用 fallthrough 关键字。
num := 3
switch num {
case 1:
fmt.Println("One")
case 2:
fmt.Println("Two")
case 3:
fmt.Println("Three")
default:
fmt.Println("Unknown")
}
switch {
case num > 0:
fmt.Println("num > 0")
case num < 0:
fmt.Println("num < 0")
}
循环
Go 语言中,循环只有 for 关键字。使用 break ,continue 分别终止循环或跳过本次循环。
for i := 0; i < 5; i++ {
fmt.Println(i)
}
for {
fmt.Println("死循环")
}
数组和切片
// 数组 长度固定 值类型 复制时会复制整份数据,所有副本互不干扰
arr := [3]int { 1, 2, 3 }
// 切片 长度不固定 引用类型 复制时只会复制指针,所有副本共享同一份数据
slice := []int { 1, 2, 3, 4 }
字典
dict := map[string]int { "a": 1, "b": 2 }
a := dict["a"]
结构体
type User struct {
// json:"name" 是结构体标签
// 可以理解为其他语言中的元数据
// 多个标签直接以空格分割, 例如 `json:"name" description:"名称"`
Name string `json:"name"`
Age int `json:"age"`
}
u := User {
Name: "ZhangSan",
Age: 18
}
name := u.Name
age := u.Age
遍历
使用 range 关键字可以对字典,数组等进行遍历取值。
func main() {
array := []int{1, 2, 3, 4, 5}
dict := map[string]int{
"one": 1,
"two": 2,
"three": 3,
"four": 4,
}
channel := make(chan int)
go func() {
channel <- 1
channel <- 2
channel <- 3
channel <- 4
channel <- 5
close(channel)
}()
for index, value := range array {
println(index, "=", value)
}
for key, value := range dict {
println(key, "=", value)
}
for value := range channel {
println(value)
}
}
接口
type People interface {
Eat(food string) string
Drink(drink string) string
}
接口不用手动继承,只要实现了某个接口的方法签名,那么 Go 就会认为实现了该接口。
type People interface {
Eat(food string) string
Drink(drink string) string
}
type ZhangSan struct{}
func (p ZhangSan) Eat(food string) string {
return "eat " + food
}
func (p ZhangSan) Drink(drink string) string {
return "drink " + drink
}
错误处理
Go 语言中没有异常机制,而是采用显式返回错误的方式。
func divide(a, b int) (int, error) {
if b == 0 {
return 0, fmt.Errorf("除数不能为 0")
}
return a / b, nil
}
func main() {
result, err := divide(10, 0)
if err != nil {
fmt.Println("错误:", err)
} else {
fmt.Println("结果:", result)
}
}
并发
Go 原生支持并发,采用 go 关键字开启线程。通过创建 channel 在线程之间传递消息。
通过操作符 <- 为通道输入输出消息。
通道在左侧,则为写入数据:ch <- 1 。
通道在右侧,则为输出数据:num := <- ch 。
package main
import (
"fmt" // 用于输入输出
"strconv" // 用于字符串转数字
"time" // 用于时间相关操作
)
const EXIT_NUM = -127 // 退出数字
const EXIT_MSG = "exit" // 退出消息
// 计算函数
// input: 输入数字
// output: 输出数字
// 功能: 从输入通道读取数字, 乘以2, 写入输出通道
// 如果等待 3 秒没有输入, 写入退出数字, 退出程序
func compute(input chan int, output chan int) {
for {
select {
case n := <-input:
output <- n * 2
case <-time.After(3 * time.Second):
output <- EXIT_NUM
}
}
}
// 读取函数
// label: 提示信息
// input: 输入通道
// 功能: 从标准输入读取一行, 写入输入通道
// 如果读取失败, 打印错误信息
func readline(label string, input chan string) {
fmt.Println(label)
var line string
_, err := fmt.Scanln(&line)
if err != nil {
fmt.Println(err)
}
input <- line
}
func main() {
input := make(chan int) // 输入通道
output := make(chan int) // 输出通道
user_input := make(chan string) // 用户输入通道
fmt.Println("程序开始")
fmt.Println("输入数字, 程序会将数字乘以2")
fmt.Println(fmt.Sprintf("输入 %s 退出程序", EXIT_MSG))
// 用于判断是否有输入
has_input := false
// 启动计算函数线程
go compute(input, output)
for {
// 如果没有输入, 启动读取函数线程
if !has_input {
go readline("请输入数字", user_input)
}
select {
// 如果有输入, 从用户输入通道读取输入, 写入输入通道
case content := <-user_input:
fmt.Println("输入: ", content)
if content == EXIT_MSG {
fmt.Println("退出程序")
return
}
// 尝试转换用户输入为数字
num, err := strconv.Atoi(content)
if err != nil {
fmt.Println("输入错误")
continue
}
// 如果转换成功, 写入输入通道
input <- num
// 标记有输入
has_input = true
// 如果有输出, 从输出通道读取输出, 打印输出
case result := <-output:
// 如果输出为退出数字, 打印退出信息, 退出程序
if result == EXIT_NUM {
fmt.Println("等待输入超时")
return
}
fmt.Println("输出: ", result)
// 标记没有输入
has_input = false
}
}
}
引用传递
在 Go 中,参数默认是值拷贝的,在一些参数庞大的情况下,会导致巨大开销。
使用 & 用来引用数据,使用 * 可以解引用数据。
// 会复制一份新的数据, 不会修改原数据
func change_user_info_copy(info User) User {
info.Name = "changed"
info.Age = 18
return info
}
// 会修改原数据
func change_user_info(info *User) {
info.Name = "changed"
info.Age = 18
}
func main() {
raw_user := User{
Name: "test",
Age: 12,
}
fmt.Println("原始数据: ", raw_user)
changed_user := change_user_info_copy(raw_user)
fmt.Println("复制后的数据: ", changed_user)
fmt.Println("原始数据: ", raw_user)
change_user_info(&raw_user)
fmt.Println("修改后的数据: ", raw_user)
fmt.Println("原始数据: ", raw_user)
}
忽略值
使用下划线 _ 可以忽略值。
常用于返回多个值但只需要一个的情况下。
可以重复使用,不会冲突。
func info() (string, int) {
return "John", 30
}
func main() {
username1, _ := info()
username2, _ := info()
username3, _ := info()
username4, _ := info()
println(username1, username2, username3, username4)
}
延迟
使用 defer 关键字可以将操作推迟到函数结束时。
常用于资源操作,防止没有及时释放资源。
func read_file(path string) (string, error) {
// 打开文件
file, err := os.Open(path)
if err != nil {
return "", err
}
// 将文件关闭操作延迟到函数结束时执行
defer file.Close()
// 读取文件内容
buffer := make([]byte, 1024)
n, err := file.Read(buffer)
if err != nil {
return "", err
}
return string(buffer[:n]), nil
}
继承
Go 语言中没有传统 OOP 语言的继承,取而代之的是组合。
package main
type Item struct {
Name string
}
type Action interface {
Move()
}
type Car struct {
Item
}
type Plane struct {
Item
}
func (item Item) Move() {
println(item.Name, "move")
}
func main() {
car := Car{Item{"Car"}}
plane := Plane{Item{"Plane"}}
car.Move()
plane.Move()
}
虽然以上代码看着很像继承,但实际上并不是。
Car 和 Plane 本质上是嵌合了 Item ,这被成为隐式组合。
两者本质上其实是多了一个名为 Item 的匿名属性。
.Move() 在编译时会被编译器转换为 .Item.Move() 。
模块化
Go 语言中,使用 package 关键字声明一个包。
一个包可以有多个文件。
Go 语言中没有 public,private 等访问修饰符。
取而代之的是首字母的大小写。
通过控制首字母的大小写来决定是否可被外部访问。
- 首字母大写代表可以在包外部被访问。
- 首字母小写则只能在包内部进行访问。
需要注意,同一个文件夹下的 .go 文件只能是同一个 package。
// /m/m1.go
package m
var (
name1 = "m1"
)
func M1() string {
return name1
}
// /m/m2.go
package m
var (
name2 = "m2"
)
func M2() string {
return name2
}
// /m/m3.go
package m
var (
name3 = "m3"
)
func M3() string {
return name3
}
// /main.go
package main
import (
"github.com/username/repo/m"
)
func main() {
m.M1()
m.M2()
m.M3()
}
初始化项目
执行 go mod init <模块路径> ,即可初始化一个 Go 项目。
其中,<模块路径> 通常为类似 github.com/username/repo 的格式。这种命名方式源于 Go 的模块管理机制,它要求使用唯一的模块路径,并推荐使用代码托管平台(如 GitHub)上的完整仓库地址,以便于模块的依赖管理和版本控制。
例如 Go 的后端框架 Gin,它的模块路径就是 github.com/gin-gonic/gin,安装命令为 go get -u github.com/gin-gonic/gin 。
初始化之后,会在执行命令的路径下生成一个 go.mod 文件。它相当于前端项目的 package.json,Rust 项目的 Cargo.toml 。
下面是 Gin 的 go.mod 的例子:
module github.com/gin-gonic/gin
go 1.23.0
require (
github.com/bytedance/sonic v1.13.2
github.com/gin-contrib/sse v1.1.0
github.com/go-playground/validator/v10 v10.26.0
github.com/goccy/go-json v0.10.2
github.com/json-iterator/go v1.1.12
github.com/mattn/go-isatty v0.0.20
github.com/pelletier/go-toml/v2 v2.2.4
github.com/quic-go/quic-go v0.51.0
github.com/stretchr/testify v1.10.0
github.com/ugorji/go/codec v1.2.12
golang.org/x/net v0.40.0
google.golang.org/protobuf v1.36.6
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/bytedance/sonic/loader v0.2.4 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
github.com/klauspost/cpuid/v2 v2.0.9 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/onsi/ginkgo/v2 v2.9.5 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
go.uber.org/mock v0.5.0 // indirect
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
golang.org/x/crypto v0.38.0 // indirect
golang.org/x/mod v0.18.0 // indirect
golang.org/x/sync v0.14.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.25.0 // indirect
golang.org/x/tools v0.22.0 // indirect
)
module 字段记录这个模块的模块路径。
go 字段则是表示这个模块所需要的 Go 版本。
require 字段内容项是项目依赖,带有 // indirect 后缀的则是依赖的依赖。
需要注意的是,模块路径并非必须为 github.com/username/repo 格式,只要是唯一的标识符就可以。
如果没有发布到网络供其他人使用的打算,那么使用什么名称都可以。
以下是一个简单的例子:
module example
go 1.24.3
编写模块
首先初始化一个项目。
随便编写一些内容。
// /math/add.go
package math
func Add(a int, b int) int {
sum := a + b
return sum
}
// /print/print.go
package print
import "fmt"
func ExamplePrint(word string, n int) {
for i := 0; i < n; i++ {
fmt.Println(word)
}
}
// /main.go
package main
import (
math "github.com/username/repo/math"
print "github.com/username/repo/print"
)
func main() {
print.ExamplePrint("hello world", 3)
math.Add(1, 2)
}
编写测试文件
Go 语言自带测试功能。
测试文件的命名要求以 _test.go 结尾。
且测试函数也有要求:
- 单元测试函数命名要求符合
TestXxx格式。参数必须是*testing.T类型。 - 基准测试函数命名要求符合
BenchmarkXxx格式。参数必须是*testing.B类型。 - 示例函数命名要求符合
ExampleXxxx其中,Xxxx必须是对应示例函数的函数名称。且没有函数参数。
// /math/add_test.go
package math
import "testing"
func TestOneAddTwo(t *testing.T) {
num1 := 1
num2 := 2
expected := 3
actual := Add(num1, num2)
if actual != expected {
t.Errorf("Add(%d, %d) = %d; expected %d", num1, num2, actual, expected)
}
}
// /print/print_test.go
package print
import (
"bytes"
"io"
"os"
"testing"
)
func TestPrintThreeHelloWorld(t *testing.T) {
old := os.Stdout
defer func() { os.Stdout = old }()
// 创建一个 bytes.Buffer 来捕获输出
var buf bytes.Buffer
r, w, _ := os.Pipe()
os.Stdout = w
ExamplePrint("hello world", 3)
// 关闭写入端并读取输出
w.Close()
io.Copy(&buf, r)
output := buf.String()
expected := "hello world\nhello world\nhello world\n"
if output != expected {
t.Errorf("输出不一致,期望: %q,实际: %q", expected, output)
}
}
func ExampleExamplePrint() {
ExamplePrint("hello world", 3)
// Output:
// hello world
// hello world
// hello world
}
编写完毕后,执行 go test ./... 即可进行测试。
./... 是 Go 的路径通配符,表示当前目录与其所有子目录。
执行命令时,Go 会遍历当前目录和所有子目录,找到所有 _test.go 结尾文件。
编写命令行命令
新建 /cmd/xxx 目录,其中,xxx 就是你希望在命令行使用的命令名称。
以下是一个简单的示例:
// /cmd/cwd/main.go
package main
import (
"fmt"
"log"
"os"
)
func main() {
dir, err := os.Getwd()
if err != nil {
log.Fatalf("get working directory failed: %v", err)
}
fmt.Println("current working directory: ", dir)
}
在发布包之后,运行 go install github.com/username/repo 后,就能够安装命令行命令了。
发布包
在 GitHub (或其他平台)上创建于 go.mod 中 module 字段对应的 repo 后,将项目上传即可供他人使用。
Go 语言通过语义化版本(Semantic Versioning,格式为 vX.Y.Z)来管理模块版本。
执行 go get gitub.com/username/repo 后:
如果仓库具有 vX.Y.Z 的 Tag,那么将获取最新的那个 Tag 版本。
否则将使用默认分支的最新提交,并生成一个伪版本来进行管理。