Golang基础知识-第一部分
- Golang
- 2024-03-21
- 2093热度
- 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"`
}
变量赋值进阶部分
基本赋值
代码示例如下
package main
import "fmt"
func main() {
// 单变量赋值
var num int
num = 100
fmt.Println(num) // 输出: 100
// 多变量赋值
var a, b int
a, b = 10, 20
fmt.Println(a, b) // 输出: 10 20
// 声明时直接赋值
var name string = "Go"
var version = 1.21
}
变量值交换
代码示例如下:
func swapExample() {
a, b := 1, 2
fmt.Printf("交换前: a=%d, b=%d\n", a, b) // 输出: 交换前: a=1, b=2
// 交换变量值
a, b = b, a
fmt.Printf("交换后: a=%d, b=%d\n", a, b) // 输出: 交换后: a=2, b=1
}
匿名变量赋值
代码示例如下:
func anonymousVariable() {
// 使用下划线 _ 忽略不需要的返回值
x, _, z := getThreeValues()
fmt.Println(x, z) // 只使用第一个和第三个返回值
// 从函数返回多个值时使用
result, err := someFunction()
if err != nil {
// 处理错误
}
// 使用result
}
func getThreeValues() (int, int, int) {
return 1, 2, 3
}
在调用函数,或者其他操作,会产生多个值的时候,可以使用下划线_进行占位,同时表示不获取这个值
注意:必须要使用_占位,不能留空,不然go解释器不知道你的意图
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)
}
}
常量赋值
代码示例如下:
const (
Pi = 3.14159
Size = 1024
Prefix = "GO_"
)
// iota 常量生成器
const (
Monday = iota + 1 // 1
Tuesday // 2
Wednesday // 3
)
指针变量赋值
代码示例如下:
func pointerAssignment() {
var num int = 42
var ptr *int = &num // & 取地址操作符
fmt.Println("num的值:", num) // 输出: 42
fmt.Println("num的地址:", &num) // 输出地址
fmt.Println("ptr指向的值:", *ptr) // * 解引用操作符,输出: 42
// 通过指针修改变量值
*ptr = 100
fmt.Println("修改后num的值:", num) // 输出: 100
}
复合类型赋值
代码示例如下:
func complexTypes() {
// 数组赋值
var arr [3]int = [3]int{1, 2, 3}
arr2 := [3]string{"a", "b", "c"}
// 切片赋值
slice := []int{1, 2, 3, 4, 5}
slice = append(slice, 6)
// 映射赋值
m := make(map[string]int)
m["apple"] = 5
m["banana"] = 3
// 结构体赋值
type Person struct {
Name string
Age int
}
p := Person{Name: "Bob", Age: 25}
}
struct结构体赋值
这部分单独介绍了,见上面部分
多返回值赋值
代码示例如下
func multipleReturn() (int, string, bool) {
return 100, "success", true
}
func useMultipleReturn() {
// 一次性接收多个返回值
code, message, status := multipleReturn()
fmt.Printf("code: %d, message: %s, status: %t\n", code, message, status)
// 只接收部分返回值
result, _ := multipleReturn() // 忽略第二个和第三个返回值
fmt.Println("result:", result)
}
类型断言赋值
代码示例如下
func typeAssertion() {
var i interface{} = "hello"
// 类型断言
s, ok := i.(string)
if ok {
fmt.Println("是字符串:", s)
}
// 如果断言失败会panic
// s2 := i.(int) // 这会panic
}
类型转换和切片字面量
简单来说:
- ()是类型转换
- {}是赋值
语法区别:
package main
import "fmt"
func main() {
str := "hello"
// 1. 类型转换:string → []byte
bytes1 := []byte(str) // 正确:类型转换
fmt.Printf("类型转换: %v\n", bytes1)
// 2. 切片字面量(错误写法)
// bytes2 := []byte{str} // 编译错误:不能把string赋值给[]byte的元素
// 3. 正确的切片字面量(元素是byte类型)
bytes3 := []byte{104, 101, 108, 108, 111} // "hello"的ASCII码
fmt.Printf("切片字面量: %v → %s\n", bytes3, string(bytes3))
}
常量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)。
数据类型
整体说明
在 Go 语言中,数据类型用于声明函数和变量
数据类型的作用:
- 把数据分成所需内存大小不同的数据
- 编程的时候需要用大数据的时候才需要申请大内存,就可以充分利用内存。
Go语言的数据类型可以分为四大类:基本类型、复合类型、引用类型和接口类型。
也就是说,在go中,所有的变量数据类型都是这几种
基本类型:
- 布尔类型
- 定义关键字:bool
- 这种类型的变量,它的值只能是:true或者false
- 一个简单的例子:var b bool = true
- 数值类型,或者叫数字类型
- 整数,这其中又可以分为有符号和无符号
- 有符号表示可以为负数
- 无符号都是正数≥0
- 并且有多种精度可供选择:int8、int16、int32、int64、uint8、uint16、uint32、uint64等等
- 有一些别名需要注意,例如byte是uint8的别名,rune是int32的别名,表示unicode的码点
- 还有特殊的例如uintptr,无符号整型,存放指针
- 浮点数,带小数点
- 复数类型,complex64 complex128
- 整数,这其中又可以分为有符号和无符号
- 字符串
- 注意,字符串修改需先转 []rune 或 []byte,再重新构造新字符串
- 字符类型和字符串类型的区别是什么?
复合类型:
- 数组
- 切片
- 映射map
- 结构体
引用类型:
- 指针
- 函数类型
- 通道Channel
接口类型:
- 接口
- 特殊接口:error 错误类型
下面进行详细的讲解
基本类型
布尔类型
代码示例如下
var b bool = true
var f bool = false
要点:
- bool类型的结果只能是true或者false,不会有其他情况
- 零值,也即默认值为:false
字符串
注意:
- 字符串本质上是只读的字节切片,只读,不可写
- 字节切片可读可写
代码示例
var s1 string = "Hello, 世界"
s2 := "Go语言"
// 字符串是不可变的字节序列
// 支持UTF-8编码
字符串的一些操作
// 长度
len := len("hello") // 5,注意这里表示的是字节,5个字节,如果是1个中文,那么会输出为3
// 索引访问
str := "hello"
ch := str[0] // 获取字节,不是字符
// 遍历
for i, r := range "hello" {
fmt.Printf("%d: %c\n", i, r) // r是rune类型
}
// 常用操作
strings.Contains(str, "ell") // true
strings.Split("a,b,c", ",") // ["a", "b", "c"]
strings.Join([]string{"a", "b"}, "-") // "a-b"
数值类型-整数
整数类型中的细分类型如下:
- 主要会分为有符号和无符号,区别表示是不是包含负数
- 注意下别名的使用,很多场景下会使用byte或者rune,要知道他们对应的实际上是uint8和int32
|
类型(别名)
|
范围
|
大小
|
|
int8
|
-128 到 127
|
8位
|
|
int16
|
-32768 到 32767
|
16位
|
|
int32 (rune)
|
-2147483648 到 2147483647
|
32位
|
|
int64
|
-2^63 到 2^63-1
|
64位
|
|
int
|
平台相关(32或64位)
|
|
|
uint8 (byte)
|
0 到 255
|
8位
|
|
uint16
|
0 到 65535
|
16位
|
|
uint32
|
0 到 4294967295
|
32位
|
|
uint64
|
0 到 2^64-1
|
64位
|
|
uint
|
平台相关(32或64位)
|
|
|
uintptr
|
存储指针的整数
|
代码示例如下:
var i1 int = 42
var i2 int8 = 127
var i3 uint = 100
var i4 byte = 255 // byte是uint8的别名
var i5 rune = '中' // rune是int32的别名,表示Unicode码点
【int、uint这2个特殊类型说明】
1、int和uint
它们属于基本整数类型家族:分别是int/uint的8/16/32/64 的“可变宽度”版本
宽度 = 当前机器的原生字长(native word size):
- 32 位系统,占4字节
- 64 位系统 ,占8字节
对照表如下:
|
维度
|
int
|
uint
|
|
字母含义
|
signed
|
unsigned
|
|
符号位
|
占 1 位
|
0 位,全位数据
|
|
范围(32 位)
|
-2³¹ … 2³¹-1
|
0 … 2³²-1
|
|
范围(64 位)
|
-2⁶³ … 2⁶³-1
|
0 … 2⁶⁴-1
|
|
零值
|
0
|
0
|
|
宽度规则
|
原生字长
(32 位=4 B,64 位=8 B)
|
同左
|
|
溢出行为
|
未定义(实际按补码回绕)
|
未定义(按 2ⁿ 回绕)
|
|
与浮点互转
|
允许,损失精度
|
允许,损失精度
|
|
与指针互转
|
禁止直接转指针
|
同左
|
|
默认类型推断
|
整型字面量
123
默认
int
|
无“默认 uint”一说
|
【uintptr特殊类型说明】
它是专为存储指针地址诞生的整数
宽度 = 当前机器的原生字长(native word size):
- 32 位系统,占4字节
- 64 位系统 ,占8字节
数值类型-浮点数
除了常规的小数点写法,还支持科学计数法的表示方式
代码示例
var f1 float32 = 3.14
var f2 float64 = 3.141592653589793 // 默认浮点类型
var f3 = 1.0e-10 // 科学计数法
数值类型-复数
注意:Go 只提供两种固定精度的复数,64位和128位
代码示例
var c1 complex64 = complex(5, 6) // 5 + 6i
var c2 complex128 = 3 + 4i
realPart := real(c1) // 获取实部
imagPart := imag(c1) // 获取虚部
复合类型
数组-Array
这里说的数组,是标准意义上的数字,也即:
- 固定长度
- 相同类型元素
代码示例
// 声明
var arr1 [3]int = [3]int{1, 2, 3}
arr2 := [...]int{1, 2, 3, 4} // 编译器计算长度
arr3 := [5]int{1: 10, 3: 30} // 指定索引初始化
// 访问
fmt.Println(arr1[0]) // 1
len := len(arr1) // 3
// 数组是值类型
arr4 := arr1 // 复制整个数组
arr4[0] = 100 // 不影响arr1
切片-Slice
切片是动态数组,是数组的引用
代码示例
// 创建
slice1 := []int{1, 2, 3}
slice2 := make([]int, 3, 5) // 长度3,容量5
var slice3 []int // nil切片
// 从数组创建
arr := [5]int{1, 2, 3, 4, 5}
slice4 := arr[1:3] // [2, 3] 左闭右开
slice5 := arr[:3] // [1, 2, 3]
slice6 := arr[2:] // [3, 4, 5]
// 操作
slice := []int{1, 2, 3}
slice = append(slice, 4, 5) // 追加元素
copy(slice2, slice1) // 复制切片
len := len(slice) // 长度
cap := cap(slice) // 容量
// 多维切片
matrix := [][]int{
{1, 2, 3},
{4, 5, 6},
}
数组和切片的区别是什么?
他们之间的主要区别汇总如下:
- 数组是值,切片是引用 ,这是他们最核心的区别
- 长度:
- 数组Array不可变
- 切片是动态数组,长度可变,可以动态增长
- 因此,数组是不能做append操作的。
// 数组的效果如下 // arr = append(arr, 4) // 编译错误:不能追加到数组 -
//切片的效果如下 slice := make([]int, 3, 5) // 长度3,容量5 fmt.Println(len(slice)) // 3 fmt.Println(cap(slice)) // 5 // 追加元素(长度增加) slice = append(slice, 4) // 长度变为4,容量仍是5 slice = append(slice, 5) // 长度变为5,容量仍是5 // 继续追加会触发扩容 slice = append(slice, 6) // 长度变为6,容量变为10(通常翻倍) fmt.Println(cap(slice)) // 10
- 赋值和传递的处理逻辑:
- 数组Arrray,会生成新的数组,原数组内容不会被影响
- 切片,大多数场景下,是一个引用关系,原切片内容会受到影响
- 零值,默认值
- 数组的零值根据声明的数据类型决定
-
var arr [3]int // 零值为[0 0 0],不是nil
- 切片的零值为:nil
-
var slice []int // 零值为nil
【赋值和传递行为的区别】
数组是值类型:复制整个数据
代码如下:
package main
import "fmt"
func main() {
// 数组赋值:复制整个数组
arr1 := [3]int{1, 2, 3}
arr2 := arr1 // 复制整个数组
arr2[0] = 100 // 修改副本
fmt.Println("arr1:", arr1) // [1 2 3] - 原数组不变
fmt.Println("arr2:", arr2) // [100 2 3] - 副本改变
// 函数传参也是复制
modifyArray(arr1)
fmt.Println("调用函数后 arr1:", arr1) // [1 2 3] - 仍然不变
}
func modifyArray(arr [3]int) {
arr[0] = 999 // 只修改局部副本
}
而切片是引用类型:复制切片头
- 如果不做append追加操作,那么切片类型在重新赋值,然后修改之后,会影响原始数据
- 如果重新赋值之后,做了append操作,那么底层会重新分配创建一个新的切片,这个时候后续才不影响
- 并且传递到函数中,函数体中对这个切片的操作,也会影响原始数据
- 总结来说:切片是一个引用关系,赋值、传递等行为,很容易会影响原始数据,使用的时候要特别小心
代码如下:
package main
import "fmt"
func main() {
// 切片赋值:复制切片头(指针、长度、容量)
slice1 := []int{1, 2, 3}
slice2 := slice1 // 复制切片头,共享底层数组
slice2[0] = 100 // 修改共享的底层数组
fmt.Println("slice1:", slice1) // [100 2 3] - 原切片也改变了!
fmt.Println("slice2:", slice2) // [100 2 3]
// 但是,追加元素可能导致底层数组重新分配
slice2 = append(slice2, 4, 5)
slice2[0] = 200
fmt.Println("追加后 slice1:", slice1) // [100 2 3] - 不变
fmt.Println("追加后 slice2:", slice2) // [200 2 3 4 5] - 新数组
// 函数传参
modifySlice(slice1)
fmt.Println("调用函数后 slice1:", slice1) // [999 2 3] - 被修改了!
}
func modifySlice(s []int) {
s[0] = 999 // 修改共享的底层数组
}
映射map
map数据类型是键值对的集合,类似python中的字典类型
代码示例:
// 创建
m1 := make(map[string]int)
m2 := map[string]int{
"apple": 5,
"banana": 10,
}
var m3 map[string]int // nil映射
// 操作
m1["apple"] = 10 // 插入/更新
value := m1["apple"] // 获取值
delete(m1, "apple") // 删除
// 检查键是否存在
value, ok := m1["apple"]
if ok {
// 键存在
}
// 遍历
for key, value := range m1 {
fmt.Println(key, value)
}
结构体
结构体是一种自定义类型,包含了多个字段
代码示例:
// 定义
type Person struct {
Name string
Age int
Address struct {
City string
Country string
}
}
// 初始化
p1 := Person{"Alice", 30, struct{City, Country string}{"Beijing", "China"}}
p2 := Person{
Name: "Bob",
Age: 25,
}
p3 := &Person{Name: "Charlie"} // 指针
// 匿名结构体
point := struct {
X, Y int
}{10, 20}
// 结构体标签
type User struct {
Name string `json:"name" xml:"name"`
Age int `json:"age" xml:"age"`
}
内置make函数讲解
先看一个真实的示例
m1 := make(map[string]int)
var m3 map[string]int
m1["a"] = 1 // 一切安好
m3["a"] = 1 // panic: assignment to entry in nil map 写入异常
功能介绍
make是Go语言的内置函数,专门用于创建和初始化引用类型的数据结构。这些引用类型包括:
- 切片 (slice)
- 映射 (map)
- 通道 (channel)
也就是说,make函数的目标对象就是这3种类型
语法:
make(T, args...)
其中:
- T:要创建的类型(必须是切片、映射或通道)
- args:根据类型而异的参数
3种类型使用方式说明
1、切片-Slice
代码示例:
// 语法1:指定长度和容量
slice1 := make([]int, 3, 5) // 长度=3,容量=5
// 语法2:只指定长度(容量=长度)
slice2 := make([]int, 3) // 长度=3,容量=3
// 语法3:创建空切片
slice3 := make([]int, 0, 10) // 长度=0,容量=10
创建之后,切片的内容
func demonstrateSliceMake() {
// 创建切片时会初始化元素为零值
slice := make([]int, 3) // [0, 0, 0]
// 对于结构体切片,每个元素都是结构体的零值
type Point struct{ X, Y int }
points := make([]Point, 2) // [{0, 0}, {0, 0}]
// 对于指针切片,每个元素都是nil
ptrSlice := make([]*int, 2) // [nil, nil]
}
2、映射-Map
代码示例:
// 语法1:不指定容量
m1 := make(map[string]int) // 创建空映射
// 语法2:指定初始容量
m2 := make(map[string]int, 100) // 预分配空间,容量≈100
// 语法3:初始化并添加元素
m3 := map[string]int{ // 字面量方式,不是用make
"apple": 5,
"banana": 3,
}
3、通道-Channel
代码示例:
// 语法1:无缓冲通道
ch1 := make(chan int) // 容量=0,同步通道
// 语法2:有缓冲通道
ch2 := make(chan string, 10) // 容量=10,异步通道
make方式和var方式的区别对比
make和var的区别是:
- var只是声明,在定义的时候,没有申请开辟出内存空间
- make额外做了初始化操作,申请了内存空间,可以直接赋值的时候,是可以写进去的
- 就像你要建造一个柜子,var只是声明说要造,但是make是真的造了个空柜子
- 所以后续写入的时候,var方式会失败,因为此时柜子并没有建造出来
- make方式是正常的,因为已经提前建造出来了
如果你var定义之后,要正式开始使用了,那么要执行:make命令,去申请内存地址
代码示例如下:
var m map[string]int // ① 只声明,目前是 nil
m = make(map[string]int) // ② 造柜子(非 nil)
m["a"] = 1 // ③ 现在可以随便写
所以通常情况下,我们在开始的时候就会使用make方式
var m = make(map[string]int) // 声明 + make 同时做
m["a"] = 1
完整的代码示例对比:
package main
import "fmt"
func main() {
// 使用make创建映射
m1 := make(map[string]int)
// 使用var声明映射
var m3 map[string]int
fmt.Println("=== 初始状态 ===")
fmt.Printf("m1 (make创建): %v, len=%d, nil? %v\n", m1, len(m1), m1 == nil)
fmt.Printf("m3 (var声明): %v, len=%d, nil? %v\n", m3, len(m3), m3 == nil)
fmt.Println("\n=== 尝试赋值 ===")
// m1可以正常赋值
m1["apple"] = 5
fmt.Printf("m1赋值后: %v, len=%d\n", m1, len(m1))
// m3赋值会panic!
// m3["banana"] = 3 // 运行时panic: assignment to entry in nil map
fmt.Println("\n=== 尝试读取 ===")
// 都可以读取(不会panic,返回零值)
v1, ok1 := m1["apple"]
v3, ok3 := m3["banana"]
fmt.Printf("m1[\"apple\"]: value=%d, exists? %v\n", v1, ok1) // 5, true
fmt.Printf("m3[\"banana\"]: value=%d, exists? %v\n", v3, ok3) // 0, false
fmt.Println("\n=== 尝试删除 ===")
// 都可以删除(不会panic)
delete(m1, "apple")
delete(m3, "banana") // 安全,即使映射为nil
fmt.Printf("删除后 m1: %v, len=%d\n", m1, len(m1))
fmt.Printf("删除后 m3: %v, len=%d\n", m3, len(m3))
fmt.Println("\n=== 迭代操作 ===")
// m1可以迭代(空映射,不执行循环体)
for k, v := range m1 {
fmt.Printf("m1迭代: %s=%d\n", k, v)
}
// m3也可以迭代(nil映射,不执行循环体)
for k, v := range m3 {
fmt.Printf("m3迭代: %s=%d\n", k, v)
}
}
make()函数使用总结
对比如下:
|
特性
|
make(map[string]int)
|
var m map[string]int
|
|
是否为nil
|
否
- 已初始化的空映射,头指针关联的是已申请的内存地址
|
是
- nil映射
|
|
可直接赋值
|
可以
|
不可以
(会panic)
|
|
可读取值
|
可以,返回零值
|
可以,返回零值
|
|
可删除键
|
可以
|
可以(安全)
|
|
可迭代
|
可以(空映射)
|
可以(nil映射,不执行循环体)
|
|
可比较
|
不能直接比较(除了与nil比较)
|
可以比较:
m == nil
为true
|
|
内存分配
|
已分配底层数据结构
|
未分配,指针为nil
|
|
使用前是否需要初始化
|
不需要,已初始化
|
需要,否则赋值会panic
|
引用类型
指针数据类型-Pointer
代码示例:
var x int = 10
var p *int = &x // p指向x
*p = 20 // 通过指针修改值
// new函数创建指针
p2 := new(int)
*p2 = 30
// 指针的指针
var pp **int = &p
函数类型-Function
代码示例:
// 函数类型
type AddFunc func(int, int) int
// 函数变量
var add AddFunc = func(a, b int) int {
return a + b
}
// 高阶函数
func calculate(a, b int, op func(int, int) int) int {
return op(a, b)
}
通道Channel
通道用于goroutine间通信
代码示例:
// 创建
ch1 := make(chan int) // 无缓冲通道
ch2 := make(chan string, 10) // 缓冲通道大小为10
// 发送接收
ch1 <- 42 // 发送
value := <-ch1 // 接收
// 关闭通道
close(ch1)
// select多路复用
select {
case msg := <-ch1:
fmt.Println(msg)
case ch2 <- "hello":
fmt.Println("sent")
default:
fmt.Println("no activity")
}
接口类型
接口interface类型的定义
接口类型的核心概念:
- 接口是一组方法签名的集合【这个一定要明白和理解】
- 它是一种抽象类型,它定义了对象的行为规范,而不关心具体实现
- 接口是 Go 实现多态和解耦的核心机制之一
- 任何类型只要实现了接口中的方法,就隐式地实现了这个接口
另外注意
- 在 Go 里,接口的方法集合在定义时就固定了;
- 函数返回什么接口,编译器就按那个固定集合去验身——
- 如果一个接口有3个方法,那么自定义类型在实现这个接口的时候,也要实现这3个方法,也不能少,但可以多,多的方法对接口满足性没有帮助
- 所以面对 error 你永远只需要关心 Error() string 这一个方法,不会多,也不会少。因为error里面只有这1个方法
例如:
type Speaker interface {
Speak() string
}
那么:任何类型只要实现了 Speak() 方法,就隐式地实现了 Speaker 这个接口。
并且,它的实现是隐式的,没有例如implements这种关键字,例如
type Dog struct{}
func (d Dog) Speak() string {
return "Woof!"
}
此时 Dog 类型就实现了 Speaker 接口,因为它实现了Speak()这个方法,并且返回值也一致。
常规接口-interface
代码示例:
// 定义接口
type Writer interface {
Write([]byte) (int, error)
}
// 实现接口
type FileWriter struct{}
func (f FileWriter) Write(data []byte) (int, error) {
// 实现
return len(data), nil
}
// 空接口
var any interface{}
any = 42
any = "hello"
// 类型断言
if str, ok := any.(string); ok {
fmt.Println(str)
}
// 类型开关
switch v := any.(type) {
case int:
fmt.Println("int:", v)
case string:
fmt.Println("string:", v)
default:
fmt.Println("unknown")
}
特殊类型
error接口类型
注意:
- error接口类型,永远只有一个方法,那即是Error() string
代码示例:
type error interface {
Error() string
}
// 创建错误
err := errors.New("something went wrong")
err2 := fmt.Errorf("error: %v", "file not found")
使用原生的error接口
一般有以下几种使用方式
- errors.New(string)
- fmt.Errorf(string)
这两种写法 并没有“自己实现” error 接口,而是直接使用了标准库已经实现好的类型;
可以看到在定义返回值的时候,是直接使用error,这表示使用的是原生error接口
它们返回的值天生就是 error,因此调用者照样能正常检查、判断、包装。
// 方式1: 使用 errors.New
func divide1(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("除数不能为零")
}
return a / b, nil
}
// 方式2: 使用 fmt.Errorf
func divide2(a, b float64) (float64, error) {
if b == 0 {
return 0, fmt.Errorf("除法错误: %.2f 除以 %.2f", a, b)
}
return a / b, nil
}
自定义实现error接口
标准语法:
func (接收者类型) Error() string { return "某字符串" }
在实际的程序开发中,除了上面那种可以直接使用error这个接口类型,也可以就指定的类型,重新去实现Error()方法,添加额外的信息
有这些注意事项
- 方法名必须叫 Error
- 签名必须是 func () string
- fmt.Println 只是“打印”,不会自动让类型变成 error
示例1-自定义error
type OpError struct {
Op string
Key string
Err error
}
// 在这里重新定义Error()方法,实现原生的error接口
func (e *OpError) Error() string {
return fmt.Sprintf("%s %s: %v", e.Op, e.Key, e.Err)
}
// 后续在使用这个类型的时候,输出error的时候,就会有额外的信息输出
func (e *OpError) Unwrap() error { return e.Err } // 加入错误链
示例2-推荐指针写法
// 自定义错误类型
type DivisionError struct {
Dividend float64
Divisor float64
Message string
}
func (e *DivisionError) Error() string {
return fmt.Sprintf("除法错误: %s (%.2f/%.2f)", e.Message, e.Dividend, e.Divisor)
}
func divide3(a, b float64) (float64, error) {
if b == 0 {
return 0, &DivisionError{
Dividend: a,
Divisor: b,
Message: "除数不能为零",
}
}
return a / b, nil
}
返回指针” (&DivisionError{…}) 是 Go 社区里最常用、最推荐 的做法
只所以使用返回指针的方式,而没有赋值之后再返回
例如
abc := DivisionError{
Dividend: a,
Divisor: b,
Message: "除数不能为零",
}
然后if b == 0 {
return 0,abc}
// 虽然这种方式也是正确的,但是通常不使用这种方式
是因为:
- 接口里存指针,避免一次结构体拷贝;
- 指针接收者才能修改内部状态(如果将来需要);
- 与 errors.New、fmt.Errorf 等标准库返回的 errors.errorString 保持一致风格,都是后面直接加字符串信息
类型别名和定义
代码示例:
// 类型定义
type MyInt int // 新类型,需要显式转换
var a MyInt = 10
var b int = int(a) // 需要转换
// 类型别名
type AliasInt = int // 只是别名,不需要转换
var c AliasInt = 20
var d int = c // 可以直接赋值
类型转换
代码示例:
// 显式类型转换
var i int = 42
var f float64 = float64(i)
var u uint = uint(f)
// 字符串转换
str := "123"
num, _ := strconv.Atoi(str) // 字符串转整数
str2 := strconv.Itoa(456) // 整数转字符串
fstr := strconv.FormatFloat(3.14, 'f', 2, 64)
类型推断
代码示例:
// 短变量声明
x := 42 // int
y := 3.14 // float64
z := "hello" // string
a := true // bool
// 函数返回类型推断
func createSlice() []int {
return []int{1, 2, 3} // 编译器推断返回类型
}
各类型的零值
汇总如下:
var i int // 0
var f float64 // 0.0
var b bool // false
var s string // ""
var p *int // nil
var slice []int // nil
var m map[string]int // nil
var ch chan int // nil
var iface interface{} // nil
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)
}
}
字符和字符串
代码示例如下:
func typeStrictness() {
// Go是强类型语言,每个值都有明确的类型
// 不同类型的变量
var b byte = 65 // byte (uint8)
var r rune = 'A' // rune (int32) 字符
var s string = "A" // string 字符串
// 不能混用
// var slice1 []byte = []byte{b} // 正确
// var slice2 []byte = []byte{r} // 编译错误: rune不能直接赋值给byte,因为r是有类型变量
// var slice3 []byte = []byte{s} // 编译错误: string不能直接赋值给byte
// 'A' 是无类型的字符常量,可以根据上下文决定类型
bytes1 := []byte{'A'} // 正确:'A' 被推断为 byte 类型,因为这里A没有实现被声明类型,属于无类型字符常量
// 需要显式类型转换
slice4 := []byte{byte(r)} // rune → byte
slice5 := []byte(s) // string → []byte(整体转换)
fmt.Printf("slice4: %v\n", slice4)
fmt.Printf("slice5: %v\n", slice5)
// 检查类型
fmt.Printf("b的类型: %T\n", b) // uint8
fmt.Printf("r的类型: %T\n", r) // int32
fmt.Printf("s的类型: %T\n", s) // string
}
字符串和字节切片
字符串是只读的字节切片
字节切片可读可写
[]byte-字节数组/字节切片
注意:byte这里是别名,实际是表示的是uint8这个类型,也就是这里等价于[]uint8
把“字节数组”这四个字拆成大白话:
- 字节 = 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:
text = '{"name":"bob"}' # str
data = text.encode('utf-8') # bytes → 就是 Go 的 []byte
data 在 Python 里也叫“字节串”,肉眼依旧能读,只是类型变成 bytes。
Go 的 json.Marshal 返回的就是这样的 UTF-8 字节串。
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) 变回人类可读文本。
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接口。自定义错误类型通常用于封装更具体的业务逻辑错误信息。
