golang 开发高效、语法简洁,且天生擅长网络服务和高并发场景。以后开发 Web 渗透测试工具估计免不了要和 golang 打交道。今天写了一篇记录,尝试说清一些 golang 的特点。内容聚焦在 golang 的包管理模式上,golang 的包管理非常清晰,与其他语言有比较大的区别。


从新建项目到 “hello, world”

项目初始化:

mkdir hello-go
cd hello-go

初始化 Go 模块(生成 go.mod

go mod init hello-go

编写 main.go。在项目根目录下创建 main.go

// main.go
package main

import "fmt"

func main() {
    fmt.Println("Hello, World!")
}

本地运行测试(可选)

go run .
# 或
go run main.go
#  Hello, World!

打包成可执行文件。会在当前目录生成一个名为 hello(Linux/macOS)或 hello.exe(Windows)的可执行文件。

go build -o hello .

# 如果需要跨平台打包(交叉编译),则可以执行命令:
GOOS=linux GOARCH=amd64 go build -o hello .  # Linux
GOOS=windows GOARCH=amd64 go build -o hello.exe .  # Windows
GOOS=darwin GOARCH=amd64 go build -o hello .  # macOS

清理(可选):

rm hello        # Linux/macOS
# 或
del hello.exe   # Windows

以上就是 golang 项目的基本创建流程。


基本 .go 文件格式

一个 .go 文件 = package 声明 + import + Code。其中 package 是唯一强制项。

// Package greeting provides simple greeting functions.
package greeting

import (
    "fmt"
    "time"

    "example.com/myproject/config"
)

const defaultName = "World"

var logEnabled = true

// Message represents a greeting message.
type Message struct {
    Text string
    When time.Time
}

// SayHello returns a greeting message.
func SayHello(name string) Message {
    msg := Message{
        Text: fmt.Sprintf("Hello, %s!", name),
        When: time.Now(),
    }
    if logEnabled {
        fmt.Printf("Generated message at %v\n", msg.When)
    }
    return msg
}
  1. 包注释。通常出现在包的第一个文件顶部,用于描述整个包的功能。包注释不是 .go 文件的必要部分,但推荐写。

  2. 包声明 (package package_name) 是每个 .go 文件必须要有的部分,且必须是每个 .go 文件的第一个非注释、非空行。包名应为小写、简洁、无下划线(如 http, json, main)。

  3. 包导入 import。如果代码中使用了其他包的导出标识符(如 fmt.Println),就必须导入。支持单行导入分组导入两种写法:

    // 单行导入
    import "fmt"
    
    // 分组导入(推荐),通常将标准库、第三方库、本地包分段(用空行隔开)
    import (
        "fmt"
        "net/http"
    
        "github.com/gin-gonic/gin"
    )
    
  4. 声明区。全局作用域的声明(变量、常量、类型)。顺序无严格要求,但社区惯例通常是:

    const > var > type > func
    
  5. 函数和方法。是 .go 文件的主体。如果是 main 包则包中必须有一个 func main() 作为入口点。


golang 包管理

项目(Project)、模组(Module)、包(Package)

项目(Project)、模组(Module)、包(Package) 这三个概念是嵌套关系。

myproject/
└── Module/
     ├── Package1/
     │   ├── .go
     │   └── .go
     ├── Package2/
     ├── Packagen/
     └── go.mod

一个项目通常只包括一个模组 (Module)。若一个项目不同服务高度解耦,则可以将每个服务封装为单独的模组。模组代表一个版本化的代码仓库。一个模块包含一个或多个包,并通过 go.mod 文件定义其模块路径和依赖关系。

模组的初始化命令为:

go mod init <module_name>

这个命令会在执行路径下生成 go.mod 文件,起到版本控制等作用。

module <模块路径>

go <Go 版本>

require (
    <依赖模块路径> <版本>
    ...
)

replace <原模块路径> => <替换路径或本地目录>

exclude <模块路径> <版本>

包 (Package) 是 Go 语言中代码组织的基本单位。一个包是一组 .go 文件的集合,这些文件必须声明相同的 package <name>,用于封装变量、函数、类型等。一般而言,包名和所在文件夹的名称保持一致

package package1

import (
    "example.com/myproject/internal/api"
    "example.com/myproject/pkg/logger"
)

func main() {
    api.HandleRequest()
    logger.Info("started")
}

可以使用 go buildgo vetgo fmt 命令来起到语法检查的目的。


package main 与 func main

这一小节着重说明下 package mainmain.gofunc main() 的关系。

func main() 必须唯一声明在 package main 中。func main() 是程序的入口点 (entry point)。意思是,func main 只能出现在 package main 中,并且一个 package main 有且只能有一个 func main。下面的写法是错误的:

// 默认包声明与目录同名
myproject/
└── main/
     ├── main.go (func main)
     └── tmp.go (func main)

那么 main.go 又与 package mainfunc main 有什么关系呢?答案是没关系。Go 依赖包声明,而非 .go 文件名,来识别 package main。而 func main() 并不一定要定义在 main.go 中,因此下面的结构是可行的(但不推荐):

// 默认包声明与目录同名
myproject/
└── main/
     ├── main.go (func log)
     └── tmp.go (func main)

上面的项目在被 build 时,程序入口为 tmp.gofunc main() 而非 main.go

再进一步,golang 只规定了 一个 package main 只能有一个 func main但没有规定一个项目/模块只能有一个 package main。因此下面的结构是合法的(多命令项目):

myproject/
├── go.mod
├── cmd/
   ├── server/
      └── main.go    package main + func main()   编译为 myproject-server
   └── cli/
       └── main.go    package main + func main()   编译为 myproject-cli
└── internal/
    └── utils/
        └── helper.go  package utils

因为 servercli 是两个独立的 package main。只要是 package main 就可以有自己的 func main。并且这两个 package 可分别编译:

go build -o server ./cmd/server
go build -o cli    ./cmd/cli

main 函数本身也具备一些特点:

  1. 必须在 package main 中;
  2. func main 无参数、无返回值;
  3. 不能被其他代码调用。

func init()

每个包可以有多个 func init()。按依赖顺序,在 func main() 之前自动执行。常用于初始化全局变量、注册驱动等。无法被显式调用。

// mathutil.go
package mathutil

import "fmt"

var MaxValue int

func init() {
    fmt.Println("Initializing mathutil package...")
    MaxValue = 1000
}

func Clamp(x int) int {
    if x > MaxValue {
        return MaxValue
    }
    return x
}

有关 func init() 这里不展开讨论。


包管理总结

  1. 一个 module 中可以有多个 package。有的 packagepackage main,有的没有。
  2. package mainpackage 代表一个可执行程序,可编译为可执行文件。没有 package mainpackage 代表一个库 (Library),能被其他包导入使用(提供函数/类型等);
  3. 每个 package main 有且仅有一个 func mainfunc main 没有参数和返回值,且不能被其他代码调用。唯一的作用是作为程序运行的入口 (entry point);
  4. package 实际上就是一个文件夹目录,逻辑上一个 package 以不同的 .go 文件为载体。func init用于包的初始化,在 func main之前自动执行;
  5. 每个 package 可以有多个 func init
  6. main.go 只是个文件名称,除此以外没有任何功能上的含义。
myapp/                          // 🟢 Module 根目录 (有 go.mod)
├── go.mod                      // 📜 module 声明:module example.com/myapp
├── cmd/                        // (约定俗成的多命令目录)
   ├── server/                 // 🟡 Package #1 (目录 = 包)
      ├── server.go           //     package server + server 功能代码     
      └── main.go             //     package main + func main()  → 可执行程序 A
   └── cli/                    // 🟡 Package #2
       ├── client.go           //     package cli + client 功能代码
       └── main.go             //     package main + func main()  → 可执行程序 B
├── internal/                   // 私有库 (仅本 module 可用)
   ├── auth/                   // 🟡 package #3
      ├── user.go             //     package auth
      └── jwt.go              //     同属 package auth (同目录)
   └── storage/                // 🟡 Package #4
       └── db.go               //     package storage
├── api/                        // 公共库 (可被外部导入)
   └── handler.go              //     package api
└── main.go                     // 🟡 Package #5 (根目录也可放 main)
                                //     package main + func main() → 可执行程序 C (不推荐与 cmd/ 混用)

需要注意的是 golang 没有任何 “嵌套包” 或 “父子包” 概念。所以一定要注意避免包嵌套的情况,保持代码组织管理的纯粹性。


工具链

Go 工具链 (Go Toolchain) 是 Go 语言官方提供的一套命令行工具集合,用于项目管理、构建、测试、格式化、依赖管理等开发全流程。它内置于 Go 发行版中,安装 Go 后即可直接使用(如 go buildgo test 等)。

go build       // 编译 Go 程序(生成可执行文件或包)
go run         // 编译并立即运行 Go 程序(适合快速测试)
go test        // 运行单元测试和基准测试
go fmt         // 自动格式化 Go 代码(遵循官方风格)
go mod         // 管理 Go 模块(初始化、同步、校验等)
go get         // 获取/升级依赖模块
go install     // 编译并安装可执行文件到 $GOBIN
go vet         // 静态分析代码,检查可疑结构
go doc         // 查看包或函数的文档
go list        // 列出包、模块或依赖信息
go clean       // 清理构建缓存和临时文件
go work        // (Go 1.18+) 管理多模块工作区

特别推荐 go fmt 代码格式化命令:

go fmt ./...            // 格式化所有 Go 文件

所有命令可通过 go help <command> 查看详细用法,例如:go help build


golang 语法特点

注释

单行注释 //

// 这是一个行注释
x := 42  // 行尾注释

多行注释

/*
这是一个多行注释
可以跨越多行
*/

官方强烈推荐使用 // 行注释,即使是多行也用多个 //


print

在 Go 语言中,没有内置的 print 关键字,但标准库提供了多个用于输出的函数。

最常用:fmt 包(格式化输出)

  1. fmt.Println。输出到 os.Stdout,带换行的打印。自动在参数间加空格,末尾加换行符 \n。支持任意类型(调用其 String() 方法或默认格式)

    fmt.Println("Hello", "World")        // 输出: Hello World\n
    fmt.Println(42, true, []int{1,2,3})  // 输出: 42 true [1 2 3]\n
    
  2. fmt.Print。输出到 os.Stdout,不带换行,参数间不自动加空格,需手动控制格式。

    fmt.Print("Hello")
    fmt.Print("World")  // 输出: HelloWorld(无空格、无换行)
    
  3. fmt.Printf。输出到 os.Stdout,格式化输出(类似 C 的 printf

    name := "Alice"
    age := 30
    fmt.Printf("Name: %s, Age: %d\n", name, age)
    // 输出: Name: Alice, Age: 30
    

    常用格式动词:

    动词 说明 示例
    %v 默认格式 fmt.Printf("%v", x)
    %+v 结构体显示字段名 fmt.Printf("%+v", user)
    %#v Go 语法表示(可复制回代码) fmt.Printf("%#v", []int{1,2})[]int{1, 2}
    %T 类型 fmt.Printf("%T", x)int
    %s 字符串 "hello"
    %d 十进制整数 42
    %x 十六进制 2a
    %f 浮点数 3.141592
    %t 布尔值 true

    推荐:优先用 %v,结构体用 %+v,调试用 %#v

其他打印方式

  1. printlnprint(内置函数,输出到 stderr,仅用于调试!官方文档明确要求 “不要在生产代码中使用!”)

    print("hello")   // 输出到 stderr,无空格、无换行
    println("world") // 输出 + 换行
    
  2. log 包(带时间戳的日志输出),输出到 stderr,适合写日志,不适合普通打印

    import "log"
    
    log.Println("This is a log message")
    log.Printf("Error: %v", err)
    
  3. 直接写 os.Stdout / os.Stderr。底层操作,一般不需要,fmt 内部就是基于这些实现的。

    import "os"
    
    os.Stdout.Write([]byte("hello\n"))
    

记住:在 Go 中,fmt 是你打印输出的唯一朋友。


golang 的其他特点

  1. nil 不是一个值,而是一个 “无” 状态。类似于其他语言中的 NULL,但用途更受限、语义更明确。

    // nil 只能赋给下面 6 种类型:
    var p *int = nil            // 指针 *T
    var s []string = nil        // 切片 []T
    var m map[string]int = nil  // 映射 map[K]V
    var c chan bool = nil       // 通道 chan T
    var f func() = nil          // 函数 func(...)
    var i interface{} = nil     // 接口 interface{}
    
  2. 函数可以返回多个值

    func divide(a, b float64) (float64, error) {
        if b == 0 {
            // 这也是 Go 错误处理的核心机制(替代异常)
            return 0, errors.New("division by zero")
        }
        return a / b, nil
    }
    
    result, err := divide(10, 2)
    
  3. 没有隐式类型转换

    var a int = 10
    var b float64 = a  // 编译错误!
    var b float64 = float64(a)  // 必须显式转换
    
  4. iota 枚举常量生成器

    type Status int
    
    const (
        Pending Status = iota  // 0
        Approved               // 1
        Rejected               // 2
        // iota 在 const 块中从 0 开始自增
        // 是 Go 实现枚举的惯用方式
    )
    
  5. 没有类 (Class),但有结构体 + 方法。Go 是面向对象的,但没有 class 关键字

    type Person struct {
        Name string
    }
    
    func (p Person) Greet() {
        fmt.Println("Hello, I'm", p.Name)
    }
    

    通过 struct + method 实现封装。没有继承,但支持组合(embedding):

    type Employee struct {
        Person  // 匿名字段,实现“组合”
        ID int
    }
    e := Employee{Person: Person{Name: "Bob"}, ID: 123}
    e.Greet() // 可直接调用 Person 的方法
    
  6. 方法可以定义在任何类型上(包括自定义类型)

    type MyInt int
    
    func (m MyInt) Double() MyInt {
        return m * 2
    }
    
    x := MyInt(5)
    fmt.Println(x.Double()) // 10
    

    不能为内置类型(如 intstring)直接定义方法,必须先定义新类型。方法接收者可以是值或指针。

  7. ... 用于变参和切片展开

    func sum(nums ...int) int {  // 变参
        total := 0
        for _, n := range nums {
            total += n
        }
        return total
    }
    
    numbers := []int{1, 2, 3}
    result := sum(numbers...)  // 切片展开
    
  8. 空白标识符 _,用于丢弃不需要的值:

    _, err := someFunc()
    if err != nil { /* ... */ }