编写一个简单的 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 关键字。使用 breakcontinue 分别终止循环或跳过本次循环。

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

虽然以上代码看着很像继承,但实际上并不是。

CarPlane 本质上是嵌合了 Item ,这被成为隐式组合。

两者本质上其实是多了一个名为 Item 的匿名属性。

.Move() 在编译时会被编译器转换为 .Item.Move()

模块化

Go 语言中,使用 package 关键字声明一个包。

一个包可以有多个文件。

Go 语言中没有 publicprivate 等访问修饰符。

取而代之的是首字母的大小写。

通过控制首字母的大小写来决定是否可被外部访问。

  • 首字母大写代表可以在包外部被访问。
  • 首字母小写则只能在包内部进行访问。

需要注意,同一个文件夹下的 .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

下面是 Gingo.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.modmodule 字段对应的 repo 后,将项目上传即可供他人使用。

Go 语言通过语义化版本(Semantic Versioning,格式为 vX.Y.Z)来管理模块版本。

执行 go get gitub.com/username/repo 后:

如果仓库具有 vX.Y.Z 的 Tag,那么将获取最新的那个 Tag 版本。

否则将使用默认分支的最新提交,并生成一个伪版本来进行管理。


编写一个简单的 go 项目
http://www.inksha.com/archives/bian-xie-yi-ge-jian-dan-de-go-xiang-mu
作者
inksha
发布于
2025年05月25日
许可协议