Golang基础知识
- Golang
- 2024-03-21
- 1533热度
- 0评论
项目基础配置
初始化一个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 init的作用
- 在Go中,一个模块是一组相关的Go包,它们作为一个单元一起进行版本控制。
- 通常,在项目目录的根目录下使用go mod init命令来创建一个新模块或将现有项目初始化为一个模块。
- 也就是说,这一个项目就是一个模块,定义好了之后,方便其他项目导入你这个模块,其他项目按照这种规范定义之后,也方便你去导入其他的模块
- 模块路径是您的模块的唯一标识符,通常基于一个唯一代表您项目的URL。这有助于确保您模块的包是全局唯一的,并且可以被其他项目获取和导入。
- 初始化模块之后,会在目录下生成一个go.mod文件
- 我们在后续安装其他模块使用go get命令向您的模块添加依赖。
当你后续在Go代码中从这些依赖导入包时,Go工具链将自动下载并管理所需的包。
go.mod文件,类似python中的requirement.txt文件,主要都是用于
- 依赖管理
- 依赖的版本控制
开发过程中,如何安装第三方包
# go get github.com/gorilla/mux
常用搭配参数:
- -u:更新包到最新的次要版本或修订版本。
- -u=patch:仅更新到最新的修订版本(例如,用于修复安全漏洞的小版本)。
- package@version:获取指定版本的包,例如 go get github.com/gin-gonic/gin@v1.7.4。
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函数吗?
- 可以有多个主包和多个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"}
【变量的零值,默认值】
|
类型
|
零值
|
|
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:字符串类型,表示文本。int,int8,int16,int32,int64:整数类型,有不同的位宽。uint,uint8,uint16,uint32,uint64:无符号整数类型。byte:字节类型,是int8的别名,表示 8 位整数。rune: rune 类型,是int32的别名,表示 Unicode 码点。float32,float64:浮点数类型。complex64,complex128:复数类型。
复合数据类型
- 结构体(
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是接口的名称。Method1,Method2, ... 是接口中定义的方法签名,包括方法名、参数列表和返回类型。
接口的特性
- 类型断言:接口类型的变量可以存储任何实现了接口声明方法的值。要访问接口变量中存储的具体值,需要使用类型断言。
- 空接口:如果一个接口没有声明任何方法,它被称为空接口,用
interface{}表示。任何类型的值都可以赋给空接口类型的变量。 - 类型实现接口:如果一个类型实现了接口中的所有方法,那么这个类型就实现了该接口。在 Go 中,类型实现接口是隐式的,不需要显式声明。
- 接口的多重实现:一个类型可以实现多个接口。
类型和接口类型的联系
- 实现(Implementation):任何具体类型(如结构体、数组、切片等)都可以实现一个或多个接口。当一个类型拥有了接口声明的所有方法时,它就实现了该接口。这种实现是隐式的,不需要显式声明。
- 赋值(Assignment):在 Go 中,如果一个类型
T实现了接口I,那么T类型的变量可以被赋值给I类型的变量。这是因为T已经具备了I接口所需的所有方法。 - 多态性(Polymorphism):接口类型使得我们可以编写更加通用的代码。通过接口,我们可以编写处理未知类型的函数,只要这些类型实现了相应的接口。这种多态性是 Go 语言强大的特性之一。
- 类型断言(Type Assertion):当我们有一个接口类型的变量时,我们可以使用类型断言来检查和转换它所持有的具体类型。这是接口和具体类型之间联系的一个重要方面,它允许我们在运行时确定和访问接口变量中存储的具体类型。
具体示例1:
假设我们有一个接口 Reader,它定义了一个 Read 方法:
type Reader interface {
Read(p []byte) (n int, err error)
}
现在,我们有两个不同的类型 File 和 StringReader,它们都实现了 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) {
// 实现字符串读取的代码
}
在这个例子中,File 和 StringReader 都实现了 Reader 接口,因为它们都有 Read 方法。现在我们可以创建一个 Reader 类型的变量,并将其分别赋值为 File 或 StringReader 类型的实例:
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接口。自定义错误类型通常用于封装更具体的业务逻辑错误信息。
