Golang基础知识

项目基础配置

初始化一个go项目

创建一个新的go项目的命令一般如下

# mkdir my-gin-project
# cd my-gin-project
# go mod init my-gin-project

my-gin-project 这里一般是项目的完整信息,例如:github.com/your_user_name/project_namez

具体执行及输出

# go mod init github.com/yourusername/myapp

go: creating new go.mod: module github.com/yourusername/myapp
go: to add module requirements and sums:
	go mod tidy
执行完成之后,会在当前目录下生产go.mod这个文件

下面详细讲一下go mod init的作用

  • 在Go中,一个模块是一组相关的Go包,它们作为一个单元一起进行版本控制。
  • 通常,在项目目录的根目录下使用go mod init命令来创建一个新模块或将现有项目初始化为一个模块。
  • 也就是说,这一个项目就是一个模块,定义好了之后,方便其他项目导入你这个模块,其他项目按照这种规范定义之后,也方便你去导入其他的模块
  • 模块路径是您的模块的唯一标识符,通常基于一个唯一代表您项目的URL。这有助于确保您模块的包是全局唯一的,并且可以被其他项目获取和导入。
  • 初始化模块之后,会在目录下生成一个go.mod文件
  • 我们在后续安装其他模块使用go get命令向您的模块添加依赖。

当你后续在Go代码中从这些依赖导入包时,Go工具链将自动下载并管理所需的包。

go.mod文件,类似python中的requirement.txt文件,主要都是用于

  • 依赖管理
  • 依赖的版本控制

 

开发过程中,如何安装第三方包

使用 go get 命令来下载和安装第三方包。这个命令会自动将依赖项添加到 go.mod 文件中
命令如下:
# go get github.com/gorilla/mux

常用搭配参数:

  • -u:更新包到最新的次要版本或修订版本。
  • -u=patch:仅更新到最新的修订版本(例如,用于修复安全漏洞的小版本)。
  • package@version:获取指定版本的包,例如 go get github.com/gin-gonic/gin@v1.7.4。
安装完成后,在你的 Go 代码中通过 import 语句导入包,然后就可以使用包提供的功能了。
package main


import (
"github.com/gorilla/mux"
)


func main() {
r := mux.NewRouter()
// ... 使用 r 配置路由
}

部署时,怎么安装main.go中的依赖包

使用场景:

  • 当我们要把代码移植到例如服务器上,服务器上需要安装代码中import的依赖包,才能正常运行项目
  • 我们在main.go中导入的模块,需要更新至go.mod中

上面的输出中有提示,使用:go mod tidy命令

例如:

# go mod tidy
go: finding module for package github.com/gin-gonic/gin
go: downloading github.com/gin-gonic/gin v1.11.0
go: found github.com/gin-gonic/gin in github.com/gin-gonic/gin v1.11.0
go: downloading github.com/gin-contrib/sse v1.1.0
go: downloading golang.org/x/net v0.42.0
go: downloading github.com/quic-go/quic-go v0.54.0
go: downloading github.com/stretchr/testify v1.11.1
go: downloading google.golang.org/protobuf v1.36.9
go: downloading github.com/go-playground/validator/v10 v10.27.0
go: downloading github.com/goccy/go-yaml v1.18.0
go: downloading github.com/bytedance/sonic v1.14.0
go: downloading github.com/pelletier/go-toml/v2 v2.2.4
go: downloading github.com/ugorji/go/codec v1.3.0
go: downloading golang.org/x/sys v0.35.0
go: downloading github.com/pmezard/go-difflib v1.0.0
go: downloading github.com/davecgh/go-spew v1.1.1
go: downloading github.com/gabriel-vasile/mimetype v1.4.8
go: downloading golang.org/x/crypto v0.40.0
go: downloading golang.org/x/text v0.27.0
go: downloading github.com/quic-go/qpack v0.5.1
go: downloading go.uber.org/mock v0.5.0
go: downloading github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421
go: downloading golang.org/x/tools v0.34.0
go: downloading golang.org/x/mod v0.25.0
go: downloading github.com/cloudwego/base64x v0.1.6
go: downloading golang.org/x/arch v0.20.0
go: downloading github.com/bytedance/sonic/loader v0.3.0
go: downloading github.com/klauspost/cpuid/v2 v2.3.0
go: downloading golang.org/x/sync v0.16.0
go: downloading github.com/go-playground/assert/v2 v2.2.0
go: downloading github.com/google/go-cmp v0.7.0

 

这个命令的作用是:

  • 整理并同步Go模块依赖,确保 go.mod 文件和 go.sum 文件与项目源代码中的 import 语句完全一致
  • 简单说就是:
    • 将项目使用到的依赖添加进go.mod文件中
    • 删除无用的依赖:go.mod文件中记录了,但是项目代码中没有任何地方import它。

最佳实践和常见使用场景

  • 在提交代码前:这是一个非常重要的习惯。在 git commit 之前运行一下 go mod tidy,可以确保你的版本控制库里记录的 go.mod 和 go.sum 文件是干净、准确的,反映了项目当前真实的依赖状态。
  • 在拉取别人的代码后:当你从 Git 上拉取(pull)了队友的更新后,他们的代码可能引入了新的依赖。运行 go mod tidy 可以确保你的本地环境下载所有必需的依赖,并保持与团队一致的依赖列表。
  • 在 CI/CD 流水线中:很多持续集成/持续部署流程中,会有一个步骤是运行 go mod tidy,然后检查 go.mod 和 go.sum 文件是否有变动。如果有变动,则说明开发者忘记在提交前运行此命令,CI 流程可以失败并提醒开发者。这能保证项目依赖的声明始终是准确的。

 

初始化相关命令的总结

命令
作用
go mod init
初始化一个新的模块,创建
go.mod
 文件。
go get
添加、升级、降级或移除一个特定的依赖。
go mod tidy
整理依赖,确保 go.mod 与代码的 import 完全匹配。
go mod verify
验证依赖是否自下载后未被修改。
go list -m all
查看当前模块的所有依赖(包括间接依赖)。

 

go项目的启动

开发测试时的启动

1、直接运行(最常用)

# 运行单个文件
go run main.go

# 运行整个项目(自动查找main包)
go run .

# 运行指定包
go run ./cmd/server

2、构建成二进制包之后再运行

# 构建可执行文件
go build -o myapp main.go

# 运行构建的文件
./myapp

3、使用热重载工具

安装并使用 air 或 fresh 实现代码变更时自动重启

# 安装 air
go install github.com/cosmtrek/air@latest

# 在项目根目录运行(会自动查找 .air.toml 配置)
air

# 或者使用 fresh
go get github.com/pilu/fresh
fresh

.air.toml配置文件示例

root = "."
testdata_dir = "testdata"
tmp_dir = "tmp"

[build]
args_bin = []
cmd = "go build -o ./tmp/main ."
delay = 1000
exclude_dir = ["assets", "tmp", "vendor", "testdata"]
include_ext = ["go", "tpl", "tmpl", "html"]
include_file = []
kill_delay = "0s"
log = "build-errors.log"
poll = false
poll_interval = 0
rerun = false
send_interrupt = false
stop_on_root = false

[color]
app = ""
build = "yellow"
main = "magenta"
runner = "green"
watcher = "cyan"

[log]
time = false

[misc]
clean_on_exit = false

4、补充-启动时设置临时环境变量

例如:

# Linux/Mac
export DATABASE_URL="postgres://user:pass@localhost:5432/db"
export DEBUG="true"
go run main.go

# Windows
set DATABASE_URL="postgres://user:pass@localhost:5432/db"
set DEBUG="true"
go run main.go

Linux-生产环境启动

1、直接运行二进制文件

# 构建生产版本(去除调试信息,优化大小)
go build -ldflags="-s -w" -o myapp main.go

# 运行
./myapp

2、使用系统服务(Systemd)

1、创建系统服务文件 /etc/systemd/system/myapp.service

[Unit]
Description=My Go Application
After=network.target

[Service]
Type=simple
User=appuser
Group=appgroup
WorkingDirectory=/opt/myapp
ExecStart=/opt/myapp/myapp
Restart=always
RestartSec=10
StandardOutput=syslog
StandardError=syslog
SyslogIdentifier=myapp

# 环境变量
Environment=GIN_MODE=release
Environment=PORT=8080
Environment=DATABASE_URL=postgres://user:pass@localhost:5432/prod_db

[Install]
WantedBy=multi-user.target

2、设置并启动服务

# sudo systemctl daemon-reload
# sudo systemctl enable myapp
# sudo systemctl start myapp
# sudo systemctl status myapp

3、使用使用进程管理器(Supervisor)

1、安装并配置 Supervisor

# 安装
sudo apt-get install supervisor

# 创建配置文件: /etc/supervisor/conf.d/myapp.conf
配置文件内容:
[program:myapp]
command=/opt/myapp/myapp
directory=/opt/myapp
user=appuser
autostart=true
autorestart=true
startretries=3
stderr_logfile=/var/log/myapp.err.log
stdout_logfile=/var/log/myapp.out.log
environment=GIN_MODE=release,PORT=8080

2、设置并启动服务

# sudo supervisorctl reread
# sudo supervisorctl update
# sudo supervisorctl start myapp

加载环境变量配置文件

1、创建env文件

# cat .env

PORT=8080
DATABASE_URL=postgres://user:pass@localhost:5432/prod_db
REDIS_URL=redis://localhost:6379
DEBUG=false
LOG_LEVEL=info

2、代码中使用env配置文件

package main

import (
    "log"
    "os"
    "github.com/joho/godotenv"
)

func main() {
    // 加载 .env 文件
    if err := godotenv.Load(); err != nil {
        log.Println("No .env file found")
    }
    
    port := os.Getenv("PORT")
    if port == "" {
        port = "8080"
    }
    
    // 启动应用
    // ...
}

 

Systemd和Supervisor的区别?

核心特性对比表

特性
Systemd
Supervisor
定位
系统初始化系统和服务管理器
进程控制系统
管理范围
系统级服务
应用级进程
依赖管理
强大的依赖关系管理
简单的进程顺序启动
日志管理
集成 journald 系统日志
文件输出或 syslog
配置复杂度
相对复杂
简单直观
资源控制
完整的 cgroups 支持
基本的资源限制
启动时机
系统启动时
系统启动后手动或自动
适用场景
系统服务、守护进程
应用进程、开发环境

下面开始详细讲解

【systemd配置示例】

# /etc/systemd/system/myapp.service
[Unit]
Description=My Go Application
# 依赖关系管理
After=network.target postgresql.service
Requires=postgresql.service

[Service]
Type=notify  # 支持服务状态通知
# 资源限制
MemoryLimit=100M
CPUQuota=50%
# 安全配置
NoNewPrivileges=yes
PrivateTmp=yes
# 重启策略
Restart=on-failure
RestartSec=5s

[Install]
WantedBy=multi-user.target

【supervisor配置示例】

; /etc/supervisor/conf.d/myapp.conf
[program:myapp]
command=/opt/myapp/myapp
; 简单的进程管理
process_name=%(program_name)s
numprocs=1
; 基本资源限制
priority=999
autostart=true
; 重启策略
autorestart=unexpected
startretries=3
; 日志输出
stdout_logfile=/var/log/myapp.out.log
stderr_logfile=/var/log/myapp.err.log

systemd适合的场景

1、系统关键服务

[Unit]
Description=Database Service
After=syslog.target network.target
Wants=network.target

[Service]
Type=forking
PIDFile=/var/run/postgresql/12-main.pid
ExecStart=/usr/lib/postgresql/12/bin/postgresql start
ExecReload=/bin/kill -HUP $MAINPID
ExecStop=/bin/kill -s TERM $MAINPID

[Install]
WantedBy=multi-user.target

2、需要资源限制的服务

[Service]
MemoryLimit=512M
CPUQuota=80%
IOWeight=100
BlockIOWeight=100
# 安全沙箱
ProtectSystem=strict
ReadWritePaths=/var/lib/myapp /var/log/myapp

supervisor适合的场景

1、应用进程管理

[program:webapp]
command=/opt/myapp/web-server
directory=/opt/myapp
user=www-data
environment=PORT=8080,ENV=production

[program:worker]
command=/opt/myapp/worker-process
directory=/opt/myapp
user=www-data
environment=QUEUE=high,ENV=production

[group:appgroup]
programs=webapp,worker

 

systemd-线上环境标准配置案例

# /etc/systemd/system/api.service
[Unit]
Description=Go API Server
Documentation=https://github.com/mycompany/api
After=network.target postgresql.service redis.service
Wants=postgresql.service redis.service

[Service]
Type=exec
User=api
Group=api
WorkingDirectory=/opt/api
ExecStart=/opt/api/server
ExecReload=/bin/kill -HUP $MAINPID
Restart=always
RestartSec=5

# 资源限制
MemoryLimit=1G
CPUQuota=100%
LimitNOFILE=65536

# 安全加固
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
ProtectHome=yes
ReadWritePaths=/opt/api /var/log/api

# 环境变量
Environment=GIN_MODE=release
Environment=PORT=8080
EnvironmentFile=/etc/api/config

StandardOutput=journal
StandardError=journal
SyslogIdentifier=api

[Install]
WantedBy=multi-user.target

supervisor-线上环境标准配置案例

; /etc/supervisor/conf.d/ecommerce.conf

; Web 服务器
[program:web-frontend]
command=/opt/ecommerce/web-server -port=8080
directory=/opt/ecommerce
user=ecom
autostart=true
autorestart=true
startretries=3
stdout_logfile=/var/log/ecommerce/web.out.log
stderr_logfile=/var/log/ecommerce/web.err.log
environment=GIN_MODE=release

; 工作进程
[program:order-worker]
command=/opt/ecommerce/order-worker -queues=high,medium
directory=/opt/ecommerce
user=ecom
autostart=true
autorestart=true
startretries=3
stdout_logfile=/var/log/ecommerce/worker.out.log
stderr_logfile=/var/log/ecommerce/worker.err.log

; 定时任务
[program:cron-worker]
command=/opt/ecommerce/cron-worker
directory=/opt/ecommerce
user=ecom
autostart=true
autorestart=true
startretries=3
stdout_logfile=/var/log/ecommerce/cron.out.log
stderr_logfile=/var/log/ecommerce/cron.err.log

; 进程组
[group:ecommerce]
programs=web-frontend,order-worker,cron-worker

 

管理命令汇总

【systemd命令】

# 服务管理
sudo systemctl start api
sudo systemctl stop api
sudo systemctl restart api
sudo systemctl reload api

# 状态查看
sudo systemctl status api
sudo journalctl -u api -f  # 查看日志
sudo systemctl is-enabled api

# 故障诊断
sudo systemctl daemon-reload
sudo systemctl reset-failed api

【supervisor命令】

# 进程管理
sudo supervisorctl start ecommerce:*
sudo supervisorctl stop ecommerce:web-frontend
sudo supervisorctl restart all

# 状态查看
sudo supervisorctl status
sudo supervisorctl tail ecommerce:web-frontend
sudo supervisorctl tail -f ecommerce:web-frontend

# 配置重载
sudo supervisorctl reread
sudo supervisorctl update

 

K8S环境的启动

容器镜像构建

1、dockerfile文件

# 构建阶段
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o main .

# 运行阶段
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/main .
COPY --from=builder /app/.env .

# 使用非root用户运行
RUN adduser -D -u 10001 appuser
USER appuser

EXPOSE 8080
CMD ["./main"]

dockerfile文件中的关键字段说明

1、FROM golang:1.21-alpine AS builder

这里选的是官方维护的 Go 1.21 版 Alpine Linux,体积只有几十 MB,AS builder 给这一阶段起名叫 builder,后面运行阶段可以 COPY --from=builder 来拿编译好的文件。

这里是dockerfile的多阶段构建,也可以说是dockerfile的流水线机制

多阶段构建机制:

  • 语法:FROM <基础镜像> AS <阶段名>
  • 作用:告诉 Docker新开一条流水线,构建一个临时镜像,并给它起个别名
  • 多阶段构建的关键点
    • 每个阶段彼此完全隔离,就像两个独立的 Dockerfile
    • 前面的阶段可以“被后面阶段引用”,但不会进入最终镜像
    • 只有最后一条 FROM 生成的镜像,才是 docker build 最终打出来的那个

整体过程如下:

宿主机文件系统
     │
     ├─→ 阶段1:golang:1.21-alpine → 编译出 /app/main
     │                        ↑
     │                        │ COPY --from=builder
     └─→ 阶段2:alpine:latest ←──────────────┘
                 +ca-certificates
                 +非root用户
                 +main 二进制
                 =
               最终镜像(20 MB)

机制总结:

  • 构建阶段生成的镜像”只是中间产物,用完即走
  • 运行阶段再拿一个干净的 alpine,把编译好的文件拷进来,成为最终交付物
  • 这样既保留编译环境,又让线上镜像最小、最安全。

2、COPY go.mod go.sum ./

这里这么写的作用是:

  • 只先把依赖描述文件拷进来,不拷源码
  • 利用 Docker 的层缓存,只要 go.mod/go.sum 没变,下一行的 go mod download 也就不会重新执行,大幅节省构建时间

3、RUN go mod download

作用是:

  • 根据 go.mod中的依赖包清单,拉取第三方依赖,并生成 /go/pkg/mod 缓存
  • 因为上一步已经保证 mod/sum 文件存在, 所以这里能成功解析版本
  • 拉好的依赖留在这一层,后续如果只有源码改动,那么不会导致依赖重复下载

4、COPY . .

把项目的源码拷贝进工作目录中

这2个点分别代表的是什么?

第一个 . 宿主机 的“构建上下文目录”(docker build 时 -f 参数所在的目录,或者 docker build 末尾那个路径)

第二个 . 容器内 的当前工作目录,也就是第 2 行 WORKDIR /app 创建出来的 /app。

所以,COPY . .的意思是:

  • 把“你执行 docker build 命令时所在的整个目录(含子目录,但受 .dockerignore 影响)”原样拷到容器的 /app 目录下
  • 两个点虽然写法一样,但一个在外(宿主机),一个在内(容器)

 

5、RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o main .

说明:

  • CGO_ENABLED=0 关闭 CGO,生成静态链接的可执行文件,运行时无外部 C 库依赖,
    这样才能直接放到纯 alpine 镜像里跑。
  • GOOS=linux 显式指定目标 OS,防止在 Mac/Win 上交叉构建时误用。
  • -ldflags="-s -w" 去掉符号表和调试信息,体积可再小 20~30%。
  • 最终产出 /app/main 单文件。

然后我们再详细讲解一下,-ldflags="-s -w" 到底扔掉了什么?

段名
作用
体积举例
.symtab(符号表)
调试器/perf 做函数名回溯、栈追踪时用
几 MB
.debug_*(DWARF 调试信息)
gdb、delve 下断点、打印变量、行号映射
十几 MB

说明:

  • -s 让链接器直接不生成 .symtab 段。
  • -w 让链接器不生成任何 DWARF 调试段。
  • 结果:可执行文件体积立刻瘦一圈,且运行时内存也省一点。
  • 代价:不能用 gdb/delve 调试这个二进制;panic 栈里只剩地址,没有函数名(线上通常用 addr2line 配合带符号的单独文件再还原)。

6、FROM alpine:latest

说明:重新拉一个干净的 alpine 作为“运行底座”,体积只有 5 MB 左右。
注意 latest 标签会随时间漂移,生产环境最好指定 alpine:3.18 这类具体版本。

 

7、RUN apk --no-cache add ca-certificates

安装 CA 证书包,使程序能对外做 HTTPS 调用。--no-cache 不把包索引落盘,使镜像更小

 

8、COPY --from=builder /app/main .
把构建阶段编出来的静态可执行文件 main 拷到当前目录(即 /root/)

 

9、COPY --from=builder /app/.env .

作用和上面一样:把源码仓库里的 .env 文件(环境变量配置模板)也拷进来。

如果程序启动时默认读取 ./.env,这一行就能保证它找到配置。

注意:若 .env 含敏感信息,更安全的做法是在运行时通过 docker run -e 或 K8s Secret 注入,而不是打进镜像。

 

10、RUN adduser -D -u 10001 appuser
创建一个非 root 用户:

  • - D 不创建家目录(没必要)
  • - u 10001 显式指定 UID,方便 K8s PodSecurityContext 或 OpenShift 的受限 SCC 匹配。
    很多集群强制禁止容器以 0 号用户运行,这一行就是合规手段。

 

11、USER appuser
把后续指令(包括容器启动时的 CMD)的默认 UID 切到 appuser,进程将以最小权限运行。

 

12、EXPOSE 8080
声明容器监听的端口,只是文档性质,不会真的发布端口。

运行容器时仍需 docker run -p 8080:8080 来做宿主机映射。

 

13、CMD ["./main"]

容器启动的默认命令:执行当前目录下的 main 可执行文件。

因为 WORKDIR 是 /root,所以路径就是 /root/main。

使用 JSON 数组写法,让 Docker 直接 exec,而非 shell 包裹,能正确接收 SIGTERM,优雅终止

 

补充-镜像缓存层机制

针对这部分

COPY go.mod go.sum ./
RUN go mod download
COPY . .

这里的作用是:如果只有源码改动,那么不会导致依赖重复下载,可以减少构建时间

详细讲解

  • 这是Docker镜像的缓存机制,它不会去看文件的真实内容,而是看上一层的文件哈希,一层影响下一层
  • 如果 COPY go.mod go.sum ./ 这一层计算出的层哈希与本地缓存中已存在的层哈希完全一致,Docker 会直接复用已有缓存层
  • 于是 RUN go mod download 的整个文件系统输入(上一层的哈希)也没有变化,它自己的缓存键也就命中,这条 RUN 指令不会被真正执行,而是直接拿缓存里的那一层挂载上来

因此,如果将

COPY go.mod go.sum ./
COPY . .

这2条进行合并,也就是第3行为COPY . .

那么只要有代码的变动,哪怕go.mod依赖没有变化,那么这一层的文件哈希变化了,那么RUN go mod download 这一层就必然要重新执行

这就会增加构建时间

 

补充-CMD的两种写法

两种写法对比

写法
名称
实际被 Docker 翻译成什么
示例
CMD ["./main", "arg1"]
exec 形式
(JSON 数组)
execve("/root/main", ["./main", "arg1"], envp)
直接替换进程
CMD ./main arg1
shell 形式
(字符串)
CMD ["/bin/sh", "-c", "./main arg1"]
先启/bin/sh,sh 再启 main

生命周期的差异如下

  • exec 形式
    • PID 1 就是 ./main 本身
    • → 当你 docker stop 时,Docker 把 SIGTERM 直接发给 PID 1(main 进程)。
    • → main 里只要注册了 signal.Notify 就能收到,做清理、回滚事务、优雅退出。
  • shell 形式
    • PID 1 是 /bin/sh,main 只是 sh 的子进程(PID ≠ 1)
    • → docker stop 发的 SIGTERM 被 sh 吃掉;sh 默认不转发信号给子进程。
    • → 10 s(默认)后 sh 还在,Docker 再发 SIGKILL,整个容器被强杀,main 没机会清理。
    • → 结果:日志没刷完、连接没断开、数据库事务可能挂起。

整体流程为:

docker stop 发送 SIGTERM
┌─────────────┐        ┌─────────────┐        ┌─────────────┐
│ docker-cli  │ ---->  │  container  │ ---->  │  PID 1      │
└─────────────┘        └─────────────┘        └─────────────┘
exec 形式:PID 1 = ./main        (信号自己处理)
shell 形式:PID 1 = /bin/sh      (信号被 sh 吃掉,main 收不到)

 

【必须使用shell形式的场景】

当你不得不写 shell 脚本时,例如需要管道、重定向、多命令组合时,可以在脚本里用 shell 自带的 exec 命令:

#!/bin/sh
echo "starting"
exec ./main arg1          # ← 这一行

功能:当前 shell 进程自己“变身”成 `./main”,不再产生新子进程,PID 保持不变

在dockerfile中的写法:

CMD ["/bin/sh", "-c", "echo starting; exec ./main arg1"]

使用这种方式,最终 PID 1 仍是 main,信号也能收到。

 

推送至镜像仓库

# docker build -t myregistry/myapp:v1.0.0 .
# docker push myregistry/myapp:v1.0.0

 

deployment-yaml文件编辑

文件内容如下:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
  labels:
    app: myapp
spec:
  replicas: 3
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
    spec:
      containers:
      - name: myapp
        image: myregistry/myapp:v1.0.0
        ports:
        - containerPort: 8080
        env:
        - name: PORT
          value: "8080"
        - name: DATABASE_URL
          valueFrom:
            secretKeyRef:
              name: app-secrets
              key: database-url
        - name: LOG_LEVEL
          value: "info"
        resources:
          requests:
            memory: "64Mi"
            cpu: "50m"
          limits:
            memory: "128Mi"
            cpu: "100m"
        livenessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /ready
            port: 8080
          initialDelaySeconds: 5
          periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
  name: myapp-service
spec:
  selector:
    app: myapp
  ports:
  - port: 80
    targetPort: 8080
  type: LoadBalancer

 

相关configmap配置

配置如下:

configmap.yaml 文件内容:

apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
data:
  LOG_LEVEL: "info"
  MAX_CONNECTIONS: "100"

secret.yaml 文件内容:

apiVersion: v1
kind: Secret
metadata:
  name: app-secrets
type: Opaque
data:
  database-url: <base64-encoded-connection-string>
  api-key: <base64-encoded-api-key>

 

main函数和主包

golang项目的结构

一个项目结构示例如下:

myproject/
├── cmd/                    # 可执行程序入口
│   ├── server/            # 服务器主程序
│   │   └── main.go
│   └── cli/               # 命令行工具
│       └── main.go
├── internal/              # 私有包(仅项目内部使用)
│   ├── handlers/         # HTTP处理器
│   ├── models/           # 数据模型
│   └── database/         # 数据库操作
├── pkg/                  # 公共库包(可被外部引用)
│   ├── utils/            # 工具函数
│   └── config/           # 配置管理
├── api/                  # API定义(protobuf、swagger等)
├── web/                  # 前端资源
├── configs/              # 配置文件
├── scripts/              # 构建脚本
├── go.mod
└── go.sum

 

什么是主包?

主包:
  • main package
  • 可以理解为是一个模块,main模块,是go项目中最核心的模块
  • 只要一个源文件的第一行是 package main,它就属于主包
  • 可以有多个代码文件属于同一个模块,只要它的第一行是 package main
  • 这些代码文件的名称可以自定义,例如main.go,abc.go,只要符合上面的规范
  • 这些代码文件可以不在同一个目录下,例如上面的cmd/server 和cmd/cli
  • 但是一个目录下的所有go文件,只能属于同一个包名,也就是说第一行必须都一样
主包的特性:
  • 只有主包,可以被编译为可执行的二进制文件
  • package main 的包,go build 会生成可执行文件(Windows 下是 .exe,Unix 下是无后缀的二进制),对其它包名,go build 只生成 .a 静态库,不会得到能直接运行的程序
  • 主包不需要遵循 Go Modules 的导入路径规则;它通常放在仓库根目录或 cmd/xxx 子目录,不会被别的包 import
  • 也就是说,package main 是一个树形结构下的根节点包,仅供编译成可执行文件,而不是库
  • 主包是Go用来生成可执行文件的起点,里面必须包含func main(){}

 

什么是main函数?

在 Go 里,main 函数是:

func main() {
// 程序从这里开始运行
}

main函数特性:

  • 它只是一个普通函数,但名字和签名被 go runtime 特殊对待
  • main函数是主包的入口函数,服务启动后加载的初始点
  • 必须是 package main 里的函数,其它非main包中写 func main() 编译器会报错
  • main函数不能有参数,也不能有返回值,也就是,函数定义是固定格式的:func main(){...}
  • 进程启动后,runtime 初始化完毕会自动调用它;执行到 main 结束,程序就退出,因此它也叫入口函数(entry point)。
  • 一句话:main 函数就是 Go 可执行程序的“大门”,程序从这里开始,也在这里结束。
  • 如果主包中,没有这个函数,编译会报错:runtime.main_main·f: function main is undeclared in the main package
案例展示
项目结构如下:
myapp/
├── cmd/
│   ├── api-server/
│   │   ├── main.go
│   │   └── routes.go
│   └── worker/
│       └── main.go
└── go.mod

运行程序

# 启动API服务器
go run ./cmd/api-server

# 启动后台任务处理器  
go run ./cmd/worker

可以有多个主包和main函数吗?

问题:在一个go项目中,可以有多个主包和多个main函数吗??
先说结论:
  • 可以有多个主包和多个main函数
  • 但是需要在不同的目录下(目录下的代码文件,必须属于同一个包,在这里,第一行必须都是package main)
  • 一个主包目录下,只能有一个main()函数,哪怕有10个代码文件,也只能有1个代码文件中定义main()函数

go中包的解析机制

1、包边界
说明:

  • 每个目录都是一个独立的包
  • 不同目录的包即使都叫 main,也是不同的包
  • Go工具链按目录来识别和编译包

2、编译过程

例如,当你运行 go run ./cmd/cli 时:

# Go只编译指定目录下的main包
go run ./cmd/cli
↓
Go工具只查看 ./cmd/cli 目录下的文件
↓
发现这是 main 包,有 main 函数
↓
编译并运行这个特定的main包

3、案例验证

项目结构如下:

test-project/
├── cmd/
│   ├── app1/
│   │   └── main.go
│   └── app2/
│       └── main.go
└── go.mod

其中的文件内容为:

// cmd/app1/main.go
package main
import "fmt"
func main() { fmt.Println("我是App1") }

// cmd/app2/main.go  
package main
import "fmt"
func main() { fmt.Println("我是App2") }

运行测试

$ go run ./cmd/app1
我是App1

$ go run ./cmd/app2  
我是App2

多主包和main函数设计原因

1、多程序项目支持

一个项目可能包含多个独立的可执行程序,例如:

  • Web API服务器
  • 后台任务处理器
  • 命令行工具
  • 管理界面

2、代码共享

公共代码放在 pkg/ 或 internal/ 中,被多个main包共享:

myapp/
├── cmd/
│   ├── api-server/main.go    # 使用 shared 包
│   └── worker/main.go        # 使用 shared 包
├── internal/
│   └── shared/               # 共享代码
│       ├── database.go
│       └── config.go

3、独立部署

每个main包,可以单独构建和部署

这里和第1点类似,一个go项目,可能集成了多种不同的业务功能

# 分别构建
go build -o bin/api-server ./cmd/api-server
go build -o bin/worker ./cmd/worker

# 分别部署
./bin/api-server  # 只部署API服务
./bin/worker      # 只部署后台任务

 

重要规则总结

总结如下:

  • go的处理逻辑是以目录为单元,一个目录=1个包
  • 不同目录的main包是相互独立的
  • 每个main包必须,并且有且仅有一个main函数
  • go run 指定的是包含main包的目录路径
  • 同一个目录下的多个文件共享同一个包作用域

 

 

基础知识

待补充

 

变量的声明赋值和使用

变量的作用域和生命周期

局部变量

func localVariables() {
    // 函数内声明的变量,作用域仅限于函数内部
    localVar := "我是局部变量"
    fmt.Println(localVar)
    
    {
        // 代码块内的变量,作用域仅限于该代码块
        blockVar := "我在代码块内"
        fmt.Println(blockVar)
    }
    // fmt.Println(blockVar) // 这里会编译错误,blockVar不可见
}

包全局变量

package main

import "fmt"

// 全局变量,在包内可见
var globalCount int = 0

func increment() {
    globalCount++
    fmt.Println("全局计数:", globalCount)
}

func main() {
    increment() // 输出: 全局计数: 1
    increment() // 输出: 全局计数: 2
}

 

 

var声明介绍

在 Go(Golang)中,变量声明有几种方式,而 var 是最传统、最通用的声明方式之一,这个章节先讲var,系统性地介绍它在变量声明中的作用和用法

var声明的作用及特点:

  • var 用于显式声明一个或多个变量,并可以指定类型和初始值
  • 使用 var 声明变量时,需要显式指定变量的类型(或使用类型推断)
  • 如果不进行初始化赋值,只定义变量,那么必须要指定数据类型
  • 包级别下,只能写声明语句,不能写赋值语句,赋值语句必须放在函数体内
  • 如果想在包级别下赋值一个变量,那么只能使用带有初始值的变量声明,进行声明并赋值,例如var abc int=4

语法格式如下:

# 声明变量并指定类型(不初始化)
var name string
变量 name 的类型是 string,默认值为空字符串 ""。

# 声明变量并初始化(指定类型)
var age int = 25

# 声明变量并初始化(类型可省略)
var age = 30
类型由编译器自动推断为 int

# 声明多个变量
var x, y int     //只声明,不初始化
var a, b = 1, "hello" //声明并初始化赋值

# 分组声明,也叫声明变量块
var (
     name string = "gopher"
     age int = 10
     ok bool = true
)

分组声明实际上就是把多条var声明语句进行合并,这可以让代码更紧凑,易读

 

特别注意,包级别下,不能进行变量赋值,代码示例如下:

// 不允许
var qq_test int
qq_test = 42    //这个是赋值语句,不能在包全局中,只能放在函数体中

// 允许
var abc int = 42
var dgh = 88

类型推断是什么?举例说明:

// 显式写类型
var x int = 42

// 类型推断:编译器看见 42,就知道 x 是 int
var x = 42
var s = "hello" // 推断为 string
var pi = 3.14 // 推断为 float64(默认浮点类型)
var ok = true // 推断为 bool
var nums = []int{1,2} // 推断为 []int

var nums = []int{1,2} 这个讲解

右边是一个复合字面量(composite literal),它天生就带着类型信息:

  • []int → 元素类型为 int 的切片
  • {1,2} → 初始两个元素

也就是说,这个变量是一个列表,列表中的元素是int类型

如果不使用类型推断,显性指定的话,是下面的写法

var nums []int = []int{1,2}

如果声明和赋值分开的话是这样的
var nums []int
nums = []int{1,2} //注意,这个不能写在包内,只能写在函数体中

可以再举一些例子,性质都是类似的

var a = []int{1,2}     // 推断为 []int(切片)
var b = [2]int{1,2}    // 推断为 [2]int(数组,长度是类型的一部分)
var c = map[string]int{"go":1, "rust":2} // 数据类型推断为 map[string]int

问题:为什么 string 前面有 [],而 int 没有?

因为 map[K]V 的语法规定:

  • K(键)必须是可比较类型——string 可以,切片 []int 不行,也可以是其他非string类型
  • V(值)可以是任意类型——int、[]int 甚至另一个 map 都行。

可比较类型(合法 K)

  • 布尔、整数、浮点、复数、指针、接口
  • string
  • 结构体(所有字段都可比较)
  • 数组(元素类型可比较)
  • 通道(channel)
  • 以上类型的嵌套组合

不可比较类型(非法 K)

  • 切片 []T
  • 映射 map[T]U
  • 函数 func(...)
  • 含上述不可比较字段的结构体 / 数组

代码示例

// int 做键
var m1 map[int]string = map[int]string{1:"one", 2:"two"}

// 结构体做键
type Point struct{ X, Y int }
var m2 map[Point]bool = map[Point]bool{{0,0}:true, {1,1}:false}

// 数组做键
var m3 map[[2]int]string = map[[2]int]string{{1,2}:"a", {3,4}:"b"}

 

 

【变量的零值,默认值】

Go 中所有变量在声明后都会被赋予一个零值,具体取决于类型
类型
零值
int
0
float64
0.0
string
""
bool
false
指针、切片、map、函数、channel
nil

在 Go(Golang)中:

  • 没有 null 这个关键字
  • 用 nil 表示“空值”或“零值”,它就是 Go 里的 “null”

nil 是 Go 的预定义标识符,表示指针、切片、映射、通道、函数、接口的“零值”。

例如:

var p *int = nil        // 空指针
var s []int = nil       // 空切片
var m map[string]int = nil // 空映射
var c chan int = nil    // 空通道
var f func() = nil      // 空函数变量
var i interface{} = nil // 空接口

nil 不能用于:

  • 基本类型(如 int, float64, bool, string)
  • 结构体(struct)

例如:

var x int = nil //编译错误:cannot use nil as int value

 

【汇总代码示例】

var globalVar int   // 包级变量,零值为 0
var qq_test int = 4 //指定数据类型
var hh_test = 4     // 类型推断为 int

func type_test() {
	var localVar string // 函数级变量,零值为 ""
	var pi = 3.14       // 类型推断为 float64
	var x, y int = 1, 2 // 多变量初始化
	var (
		name = "alice"
		age  = 25
	)

	fmt.Println(globalVar, qq_test, hh_test, localVar, pi, x, y, name, age)
}

 

:=短变量声明

短变量声明特性:

  • 只能在函数体中使用,不能在包层级使用
  • 会自动进行类型推断,不需要像var声明一样,先明确定义出这个变量的数据类型

代码示例如下:

func shortDeclaration() {
    // 使用 := 进行短变量声明(只能在函数内使用)
    name := "Charlie"
    age := 35
    weight := 75.2
    hobbies := []string{"reading", "swimming"}
    
    fmt.Println(name, age, weight, hobbies)

    // 短变量声明的多变量形式 
    x, y, z := 1, "hello", true //自动进行类型推断
    
    // 短变量声明可以用于已有变量的重新赋值(至少有一个新变量)
    age, country := 36, "USA"    //此时,age从35更新至了36
    fmt.Println(age, country)
}

结构体类型的声明

常规用法

代码示例如下:

type Person struct {
    // 这些字段在创建结构体实例时自动声明
    Name    string
    Age     int
    Email   string
    Scores  []int
}

func structDeclaration() {
    // 创建结构体实例时,字段自动声明并初始化
    p := Person{
        Name:   "Charlie",
        Age:    28,
        Email:  "charlie@example.com",
        Scores: []int{95, 87, 92},
    }
    
    fmt.Printf("%+v\n", p)
    
    // 访问已声明的字段
    p.Age = 29
    fmt.Println("更新年龄:", p.Age)
}

 

结构体嵌套

代码示例如下:

type Logger struct {
    Level string
}
type Server struct {
    Logger          // 没有字段名,称“匿名字段”/“内嵌字段”
    Addr   string
}

效果:

  • 不需要写字段名,只需要写已经定义好的结构体的名称即可
  • 外层结构体直接拥有内嵌类型的全部字段和方法。
  • 满足接口时自动“升级”为那个接口(Go 的“鸭子类型”加速)。
  • 可以用 s.Logger.Level 显式访问,也可以简写为 s.Level。

示例

s := Server{Addr: ":8080"}
s.Level = "debug"          // 等价于 s.Logger.Level = "debug"

在实际情况中,还会有多级嵌套和嵌套指针类型,示例如下

type Pool struct {
    *sync.Mutex   // 内嵌指针类型
    conn []net.Conn
}

p := &Pool{}
p.Lock()          // 直接调用 sync.Mutex 的方法

 

结构体标签

代码示例

type BaseWebhookData struct {
	Alerts            []string          	`json:"alerts"`     // alerts list
	CommonAnnotations map[string]string 	`json:"commonAnnotations"`
	CommonLabels      map[string]string 	`json:"commonLabels"`
	GroupLabels       string            	`json:"groupLabels"`
	Receiver          string            	`json:"receiver"`
	Status            string            	`json:"status"`
	Version           string            	`json:"version"`
}

后面的`json:"version"` 这种是结构体标签(Struct Tag),它不是给 Go 自己看的,而是给反射库(或其他代码)看的元数据。

这里的作用是:

  • 告诉 Go 标准库里的 encoding/json 包
  • 在把这个结构体序列化成 JSON 或从 JSON 反序列化时,把字段名 Version 映射成 JSON 中的 "version"(小写)。

也就是说:

  • webhook中接受到的json数据,字段名称是version
  • 我进行反序列化(把这个json数据,映射成go对象)时,要进行关联,关联到Version字段
  • 进行输出的时候,输出的json数据中,字段是version

 

代码示例:

Marshal(编码,Go → JSON)
Unmarshal(解码,JSON → Go)

1、序列化(Marshal)

把 Go 值 变成 JSON 字符串(便于存储、网络传输)

type User struct {
    ID   int    `json:"id"`
    Name string `json:"name"`
}

u := User{ID: 1, Name: "Alice"}
data, err := json.Marshal(u)   // []byte(`{"id":1,"name":"Alice"}`)

2、反序列化(Unmarshal)

把 JSON 字符串 还原成 Go 值(便于程序内部使用)

var u2 User
err := json.Unmarshal(data, &u2)   // u2 == User{ID:1, Name:"Alice"}

一句话总结

  • 序列化 = Go 对象 → JSON 字节流
  • 反序列化 = JSON 字节流 → Go 对象

 

可见性-首字母大小写

决定在包外是否可见

说明

  • 字段名、方法名、类型名 首字母大写 → 包外可见(exported)。
  • 首字母小写 → 仅限当前包(unexported)。
  • 这是 Go 唯一的访问控制机制,没有 public/private 关键字。

代码示例如下:

type user struct {   // 包外看不到这个类型
    Name string      // 包外能看到 Name
    age  int         // 包外看不到 age
}

 

结构体组合示例

代码示例如下:

1、嵌套接口,直接满足接口

type ReadOnlyFile struct {
    io.Reader        // 内嵌接口
    Name      string
}

var rf ReadOnlyFile
io.Copy(os.Stdout, rf) // rf 自动满足 io.Reader

2、标签,嵌套,指针

type Model struct {
    ID        uint   `gorm:"primaryKey" json:"id"`
    CreatedAt time.Time
}

type User struct {
    *Model               // 内嵌指针结构体
    Username string      `json:"username" gorm:"unique"`
}

 

变量赋值进阶部分

基本赋值

 

变量值交换

匿名变量赋值

 

for循环中使用匿名变量案例

func loopDeclarations() {
    numbers := []int{1, 2, 3, 4, 5}
    
    // for 循环中声明的变量 i
    for i := 0; i < len(numbers); i++ {
        fmt.Printf("numbers[%d] = %d\n", i, numbers[i])
    }
    
    // range 循环中的索引和值变量
    for index, value := range numbers {
        fmt.Printf("索引 %d: 值 %d\n", index, value)
    }
    
    // 只使用值
    for _, value := range numbers {
        fmt.Println("值:", value)
    }
}

 

指针变量赋值

复合类型赋值

struct结构体赋值

多返回值赋值

类型断言赋值

 

常量const介绍

const 用于声明常量,即在编译期就确定、运行期不可修改的值。

是否需要“定义类型”?——答案是:不需要显式写类型,但每一个常量都有类型(要么是默认的无类型常量,要么是显式指定的有类型常量)。

语法如下:

# 单常量:
const pi = 3.1415926          // 无类型浮点常量,默认类型 float64
const e float32 = 2.71828     // 显式指定类型 float32

# 多常量:
const (
    Monday = 1          // 无类型整型常量,默认 int
    Tuesday            // 省略值,继承上一行的表达式:Tuesday = 1
    Wednesday = 3
    Pi = 3.1415
)

无类型(untyped)常量

  • 只要声明时没有显式写类型,Go 就把它当作“无类型常量”。
  • 无类型常量精度更高(因为整数最大 256 bit,浮点 512 bit)。

 

 

数据类型

指针数据类型

 

整体说明

 

golang中的结构体

结构体struct,也是属于上面说的数据类型中的一种,属于派生类型中的一种类型

error数据类型

函数的返回值中,最后1个,一般都会是这个,用来判断是否异常

例如:

func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, fmt.Errorf("division by zero")
    }
    return a / b, nil
}

我的自定义函数中,设置的返回值类型分别为float64和error,这里的error是数据类型

 

interface{}数据类型

例如

var something interface{} = "hello world"

完整的

func typeAssertionDeclaration() {
    var something interface{} = "hello world"
    
    // 类型断言同时声明新变量
    if str, ok := something.(string); ok {
        fmt.Printf("是字符串: %s\n", str)
    } else {
        fmt.Println("不是字符串")
    }
    
    // 另一种类型断言方式
    something = 42
    switch v := something.(type) {
    case string:
        fmt.Printf("字符串: %s\n", v)
    case int:
        fmt.Printf("整数: %d\n", v)
    default:
        fmt.Printf("未知类型: %T\n", v)
    }
}

 

字节数组

把“字节数组”这四个字拆成大白话:
字节 = 0~255 的整数
数组 = 一段连续内存
合起来:一段连续的小整数,每个整数代表 1 个字节(8 bit)。
为什么 Go 的 json.Marshal 不直接返回“字符串”,而返回“字节数组”?
JSON 文本最终要写到网络/磁盘,那些接口只认字节流(socket、文件描述符、HTTP body …)。
字符串在 Go 里是不可变的只读视图,而字节切片 []byte 可以任意切片、追加、直接写入 io.Writer,零拷贝、少一次分配。
→ 性能 + 通用性 更好。
“文本”与“字节”在 Go 是两个不同类型:
string → 只读文本
[]byte → 可读可写的原始数据
官方库把选择权留给你:
只想看一眼 → string(b)
想继续传 → 直接用 []byte 写走,避免再编码一次。

它是不是“二进制”?
不是传统意义上的“二进制协议”(不是 protobuf、不是 gzip)。
它只是UTF-8 编码的文本,但以字节形式存在。
类比 Python:
Python
复制
text = '{"name":"bob"}' # str
data = text.encode('utf-8') # bytes → 就是 Go 的 []byte
data 在 Python 里也叫“字节串”,肉眼依旧能读,只是类型变成 bytes。
Go 的 json.Marshal 返回的就是这样的 UTF-8 字节串。
肉眼验证
go
复制
b, _ := json.Marshal(map[string]string{"name": "bob"})
fmt.Println(b) // [123 34 110 97 109 101 34 58 34 98 111 98 34 125]
fmt.Printf("%s\n", b) // {"name":"bob"} ← 一模一样的人类文本
[123, 34, ...] 就是字符 {, ", n, a... 的 UTF-8 码点,完全可读,只是被拆成了字节。
一句话总结
“字节数组” = 连续的小整数(0~255),
json.Marshal 返回的是 UTF-8 编码后的 JSON 文本字节流,
不是晦涩的二进制协议,而是“披着字节外衣的字符串”,方便你直接往网络/文件里扔,也随时 string(b) 变回人类可读文本。

 

go的指针处理

可以直接使用指针对象调用方法等

type Pool struct {
    *sync.Mutex   // 内嵌指针类型
    conn []net.Conn
}

p := &Pool{}
p.Lock()          // 直接调用 sync.Mutex 的方法

 

 

go的异常处理

 

JSON数据的处理

代码示例:

Marshal(编码,Go → JSON)
Unmarshal(解码,JSON → Go)

1、序列化(Marshal)

把 Go 值 变成 JSON 字符串(便于存储、网络传输)

type User struct {
    ID   int    `json:"id"`
    Name string `json:"name"`
}

u := User{ID: 1, Name: "Alice"}
data, err := json.Marshal(u)   // []byte(`{"id":1,"name":"Alice"}`)

2、反序列化(Unmarshal)

把 JSON 字符串 还原成 Go 值(便于程序内部使用)

var u2 User
err := json.Unmarshal(data, &u2)   // u2 == User{ID:1, Name:"Alice"}

一句话总结

  • 序列化 = Go 对象 → JSON 字节流
  • 反序列化 = JSON 字节流 → Go 对象

 

 

 

Go中的流程控制

for循环

if判断

switch

 

文件处理

日志处理

发送HTTP请求-net/http模块专项

 

与操作系统交互

例如执行Linux命令

 

go的多线程

 

go的并发处理

 

go的类,继承等

 

常用第三方模块

内置模块

例如

	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"os/exec"
	"strings"

 

正则处理

输出excel

 

golang的函数

函数定义


在 Go 语言中,函数通过 func 关键字定义,其基本语法如下:

func functionName(parameterList) (resultList) {
    // 函数体
}
  • func 是定义函数的关键字。
  • functionName 是函数的名称。
  • parameterList 是函数参数的列表,包括参数的类型和名称。参数列表为空时,表示该函数不接受任何参数。
  • resultList 是函数返回值的列表,包括返回值的类型。如果没有返回值,则使用 void 或者省略括号。Go 语言支持命名返回值,这样可以直接在函数体中通过返回值的名称返回结果,而不需要显式声明返回语句。
  • // 函数体 是函数执行的代码块。

函数案例

无参数和返回值的函数

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

func main() {
    sayHello() // 调用函数
}

带参数的函数

func add(a int, b int) int {
    return a + b
}

func main() {
    sum := add(5, 3)
    fmt.Println("The sum is:", sum)
}

带返回值的函数

func multiply(a, b int) (result int) {
    result = a * b
    return // 命名返回值,这里也可以省略 return 语句
}

func main() {
    product := multiply(7, 8)
    fmt.Println("The product is:", product)
}

变参函数

Go 语言支持变参函数,即函数可以接受不定数量的参数。

func sum(numbers ...int) (total int) {
    for _, number := range numbers {
        total += number
    }
    return
}

func main() {
    total := sum(1, 2, 3, 4, 5)
    fmt.Println("The total is:", total)
}

定义默认值的函数

func calculate(a, b int, operation string) int {
    switch operation {
    case "add":
        return a + b
    case "subtract":
        return a - b
    default:
        return 0
    }
}

func main() {
    result := calculate(10, 5, "add")
    fmt.Println(result) 
}

匿名函数(Lambda)

Go 语言中的匿名函数也称为闭包,可以在运行时定义,通常用作回调函数或高阶函数的参数。

func printNumbers(f func(int)) {
    for i := 0; i < 5; i++ {
        f(i)
    }
}

func main() {
    printNumbers(func(i int) {
        fmt.Println(i)
    })
}

带错误返回的函数

在 Go 语言中,返回 error 类型的函数通常是那些可能会在执行过程中遇到错误情况的函数。error 是 Go 标准库中的一个接口类型,定义如下:

type error interface {
    Error() string
}

这个接口要求实现一个 Error() 方法,该方法返回一个描述错误的字符串。当函数执行时,如果遇到无法恢复的错误状态,它会返回一个实现了 error 接口的具体类型值,以及通常是一个成功的结果值。调用者可以通过检查 error 返回值来确定函数是否成功执行。

示例:带错误返回的函数

下面是一个简单的示例,展示了一个可能返回 error 的函数:

package main

import (
    "fmt"
    "os"
)

// 创建一个可能返回错误的函数
func createFile(filename string) (*os.File, error) {
    file, err := os.Create(filename)
    if err != nil {
        // 如果创建文件时出现错误,返回 nil 和错误信息
        return nil, err
    }
    // 如果成功创建文件,返回文件对象和 nil
    return file, nil
}

func main() {
    // 使用函数并处理可能的错误
    file, err := createFile("example.txt")
    if err != nil {
        fmt.Println("Error creating file:", err.Error())
    } else {
        defer file.Close() // 确保文件在使用后关闭
        fmt.Println("File created successfully.")
    }
}

在这个例子中,createFile 函数尝试创建一个新文件,并返回一个 *os.File 类型的文件对象和一个 error 类型的错误。如果在创建文件的过程中发生错误,os.Create 会返回一个非 nil 的错误对象,createFile 函数随后将这个错误对象返回给调用者。

错误处理

在 Go 语言中,错误处理是通过检查函数返回的 error 值来完成的。如果 error 值不是 nil,则表示函数执行过程中遇到了问题。调用者有责任处理这些错误,通常是通过记录日志、尝试恢复、向用户报告错误或根据错误类型执行其他逻辑。

错误处理是 Go 语言编程中的一个重要部分,它有助于编写健壮、可靠的代码。

 

特殊说明-结构体方法声明

案例

func (w WhiteCat) eat() {
	fmt.Println("白猫在吃饭,它的名字叫", w.name)
}

在Golang中,这种定义函数的方法是结构体方法的声明。结构体方法允许你为结构体类型的实例关联函数行为。这种方式在面向对象编程语言中很常见,它允许你模拟面向对象编程中的“类”和“对象”的概念,尽管Go语言本身是静态类型的、并发支持的编程语言,并不是传统意义上的面向对象语言。

在上面代码片段中

  • func 是Go语言中定义函数的关键字。
  • (w WhiteCat) 表示这是一个结构体方法,WhiteCat 是接收者(receiver)的类型,w 是接收者的名称。这意味着这个方法与WhiteCat类型的每个实例相关联。
  • eat 是这个方法的名称,它表示这个方法是WhiteCat类型的一个行为或动作。

WhiteCat类型的一个实例调用eat方法时,它会执行方法体中的代码,即打印出一条消息,告诉别人这只白猫的名字。

完整的代码如下:

package main

import "fmt"

// Duck 定义一个猫接口
type Cat interface {
	eat()
	run()
}

// WhiteCat 定义一个白猫结构体
type WhiteCat struct {
	name string
	age  int
	sex  string
}

// BlackCat 定义一个黑猫结构体
type BlackCat struct {
	name string
	age  int
	sex  string
}

// 让白猫和黑猫实现接口中的所有方法,就叫实现该接口
// 让白猫实现 Cat 接口
func (w WhiteCat) eat() {
	fmt.Println("白猫在吃饭,它的名字叫", w.name)
}

func (w WhiteCat) run() {
	fmt.Println("白猫在走路,它的名字叫", w.name)
}

// 让黑猫实现 Cat 接口
func (b BlackCat) eat() {
	fmt.Println("黑猫在喝汤,它的名字叫", b.name)
}

func (b BlackCat) run() {
	fmt.Println("黑猫在散步,它的名字叫", b.name)
}

func main() {
	var cat Cat
	cat = WhiteCat{"小白", 5, "雄性"} // 把我的对象赋值给一个接口类型,就可以实现多态的效果
	fmt.Println(cat)

	// cat 现在他是一个接口,它只能取方法,不能取出属性了。
	cat.eat()
	cat.run()
}

输出内容是:
{小白 5 雄性}
白猫在吃饭,它的名字叫 小白
白猫在走路,它的名字叫 小白

类型和接口类型

在 Go 语言中,类型和接口类型之间存在着紧密的联系,但它们是两个不同的概念。下面我将详细解释它们之间的关系:

类型(Type)

在 Go 语言中,"类型"(Type)是指一组具有相同名称和属性的数据的集合。类型可以是基本数据类型(如 int, float64, bool, string 等),复合数据类型(如结构体 struct),切片 slice,映射 map,通道 channel,函数 func,接口 interface 、指针类型等。

每种类型都有其特定的操作和行为。例如,int 类型可以进行整数运算,string 类型可以进行字符串拼接和搜索操作,而结构体可以包含多种不同类型的字段,形成复杂的数据结构。

基本数据类型

Go 语言的基本数据类型包括:

  • bool:布尔类型,表示 true 或 false
  • string:字符串类型,表示文本。
  • intint8int16int32int64:整数类型,有不同的位宽。
  • uintuint8uint16uint32uint64:无符号整数类型。
  • byte:字节类型,是 int8 的别名,表示 8 位整数。
  • rune: rune 类型,是 int32 的别名,表示 Unicode 码点。
  • float32float64:浮点数类型。
  • complex64complex128:复数类型。

复合数据类型

  • 结构体(struct:可以包含多个不同类型的字段,用于表示复杂的数据结构。
  • 数组(array:固定长度的序列,所有元素类型相同。
  • 切片(slice:动态长度的序列,基于数组,但可以改变大小。
  • 映射(map:键值对集合,每个键唯一对应一个值。
  • 通道(channel:用于在不同的 Goroutine 之间传递数据的通信机制。
  • 接口(interface:定义了一组方法的集合,任何实现了这些方法的类型都实现了该接口。

函数类型

Go 语言中的函数也是类型的一种。函数类型由其参数列表和返回值列表决定。

指针类型

每种基本类型都有一个对应的指针类型,表示对该类型的引用。例如,*int 是指向 int 类型的指针。

类型转换

在 Go 语言中,可以使用类型转换来将一个类型的值转换为另一个类型的值。类型转换的语法是 T(表达式),其中 T 是目标类型,而 表达式 是要转换的值。

类型断言

类型断言用于接口类型,用于检查和转换接口变量中存储的具体类型。语法是 接口名.(目标类型)

类型是 Go 语言中的核心概念之一,理解不同类型的行为和操作对于编写高效、可靠的 Go 代码至关重要。

 

接口类型(Interface Type)

接口类型是一种特殊类型的类型,它定义了一组方法的签名,但不包含具体的实现。接口类型的主要作用是提供了一种方式,使得不同类型的对象可以以统一的方式处理。如果一个类型实现了接口中声明的所有方法,那么这个类型就实现了该接口。

接口的定义

接口通过 interface 关键字定义,其基本语法如下:

type InterfaceName interface {
    Method1(Parameters) ReturnType1
    Method2(Parameters) ReturnType2
    // ...
}
  • InterfaceName 是接口的名称。
  • Method1Method2, ... 是接口中定义的方法签名,包括方法名、参数列表和返回类型。

接口的特性

  1. 类型断言:接口类型的变量可以存储任何实现了接口声明方法的值。要访问接口变量中存储的具体值,需要使用类型断言。
  2. 空接口:如果一个接口没有声明任何方法,它被称为空接口,用 interface{} 表示。任何类型的值都可以赋给空接口类型的变量。
  3. 类型实现接口:如果一个类型实现了接口中的所有方法,那么这个类型就实现了该接口。在 Go 中,类型实现接口是隐式的,不需要显式声明。
  4. 接口的多重实现:一个类型可以实现多个接口。

类型和接口类型的联系

  1. 实现(Implementation):任何具体类型(如结构体、数组、切片等)都可以实现一个或多个接口。当一个类型拥有了接口声明的所有方法时,它就实现了该接口。这种实现是隐式的,不需要显式声明。
  2. 赋值(Assignment):在 Go 中,如果一个类型 T 实现了接口 I,那么 T 类型的变量可以被赋值给 I 类型的变量。这是因为 T 已经具备了 I 接口所需的所有方法。
  3. 多态性(Polymorphism):接口类型使得我们可以编写更加通用的代码。通过接口,我们可以编写处理未知类型的函数,只要这些类型实现了相应的接口。这种多态性是 Go 语言强大的特性之一。
  4. 类型断言(Type Assertion):当我们有一个接口类型的变量时,我们可以使用类型断言来检查和转换它所持有的具体类型。这是接口和具体类型之间联系的一个重要方面,它允许我们在运行时确定和访问接口变量中存储的具体类型。

具体示例1:

假设我们有一个接口 Reader,它定义了一个 Read 方法:

type Reader interface {
    Read(p []byte) (n int, err error)
}

现在,我们有两个不同的类型 FileStringReader,它们都实现了 Read 方法:

type File struct{}

func (f *File) Read(p []byte) (n int, err error) {
    // 实现文件读取的代码
}

type StringReader struct {
    s string
}

func (sr *StringReader) Read(p []byte) (n int, err error) {
    // 实现字符串读取的代码
}

在这个例子中,FileStringReader 都实现了 Reader 接口,因为它们都有 Read 方法。现在我们可以创建一个 Reader 类型的变量,并将其分别赋值为 FileStringReader 类型的实例:

var reader Reader

file := &File{}
reader = file // file 实现了 Reader 接口,所以可以赋值给 Reader 类型的变量

stringReader := &StringReader{}
reader = stringReader // stringReader 也实现了 Reader 接口,同样可以赋值给 Reader 类型的变量

通过这种方式,我们可以编写通用的函数来处理任何实现了 Reader 接口的类型,而不需要关心具体的实现细节。这就是类型和接口类型之间的联系,以及它们如何共同工作以支持多态性和类型安全。

具体示例2:见上方函数部分中的“特殊说明-结构体方法声明”

总结来看,接口类型的定义和实现,可以类似实现面向对象编程中的“类”和“对象”的概念,定义一个接口就好比定义一个类,后面对接口的实现,就好比创建了一个对象。

具体示例3:error处理

在Go语言中,error是一个内置的接口类型,它定义了错误处理的基本方法。error接口有一个方法:

type error interface {
    Error() string
}

任何实现了Error() string方法的类型都可以被用作错误值。当一个函数返回error类型时,它通常

是返回了一个实现了error接口的值,这样调用者就可以通过调用Error()方法来获取错误的描述性信息。

下面是一个简单的例子,展示了如何自定义一个错误类型,并确保它实现了error接口:

package main

import (
    "fmt"
)

// MyError 是一个自定义的错误类型,实现了 error 接口
type MyError struct {
    Message string
}

// 实现 error 接口的 Error() 方法
func (e MyError) Error() string {
    return e.Message
}

func main() {
    // 创建一个自定义的错误实例
    myErr := MyError{Message: "Custom error occurred"}

    // 打印错误信息
    fmt.Println(myErr.Error()) // 输出: Custom error occurred
}

在这个例子中,MyError是一个结构体,它包含一个Message字段用于存储错误信息。MyError类型通过实现Error() string方法来满足error接口的要求。这样,当我们创建MyError的实例并调用Error()方法时,它将返回存储在Message字段中的错误信息。

在实际编程中,通常会使用标准库中提供的错误类型,如ioutil包中的io.EOF(文件结束错误)或者os包中的os.ErrNotExist(文件或目录不存在错误),这些错误类型都已经实现了error接口。自定义错误类型通常用于封装更具体的业务逻辑错误信息。