Claran's blog

怎么能成为Go学长,不行不行!(※也不是不行?!)

引言

最近做了网盘相关项目,了解一下一般情况下常用的一些文件存储方法:用IO库进行的本地磁盘存储,需要付费但是方便安全的OSS云存储(阿里云等),以及今天的主角的-MinIO对象存储系统

可以说,MinIO就是可以存储文件的“数据库”

在本质上,OSS云存储也只不过是在企业云服务器上部署的MinIO或本地存储方式,MinIO同样也是经过包装过的直接访问本地磁盘的存储方式

概述和特点

MinIO的独特之处,在于其完全云原生高性能轻量级完全开源的设计哲学,内部完全由Go语言实现,它重新定义了企业级对象存储的构建和运行方式

再简单点说,MinIO的独特之处在于,它将对象存储的门槛降到了最低(轻量 & 开源),同时将性能拉到了最高,专为云原生容器化环境而生

如同之前所说,MinIO是一种完美的对象存储系统,那么MinIO和其他存储方式有什么区别呢?

这里我们就要聚焦到MinIO的设计理念S3标准的云原生

独特的对象和K-V化存储(Object & Bucket)

MinIO的对象化存储,存的不只是文件的数据,还有文件的标识符Key和文件的元数据Value

K-V

在minIO的存储设计里,所有需要存储的文件本质上都是一个对象(Object)

我们都知道,无论是最简单的文本文件TXT,还是动辄几GB的视频文件或文件流,本质上都是由字节流byte[])构成的,而MinIO存储的正是这种字节流(byte[]),因此MinIO基本能够存储所有文件类型

更重要的是,MinIO将每个对象都通过一个唯一的“键”寻址,并与元数据绑定,这也是S3的协议标准

存储桶(Bucket)

在MinIO中,这个K-v设计中的这个Key可以是MyApp/Project1/storage/1.txt,但本质上这个Key是一个标识符,而非真实的目录层级

因此,与传统的树状文件系统不同的是,MinIO独特的KV式设计,让他的对象存储变成了一个巨大的**“存储池”**,而非依赖于路径下层层进深的树状结构。

所有对象都放在一个巨大的Bucket)里,通过唯一的键Key直接定位

这种设计让存储的拓展性变得无比简单,让人无须担心目录树的深度或复杂度

S3

在说云原生之前,我们需要先了解一下S3这一概念

S3 是 Amazon Simple Storage Service 的简称,是亚马逊AWS云提供的一项核心对象存储服务,核心结构也为B-O(Bucket-Object)式存储服务,具有高耐久性可用性,使用标准的RESTful API进行上传,下载和管理操作

由于S3出色的设计,可靠的稳定性,S3的API接口已经成为对象存储领域的行业标准协议。无数应用程序,工具或开源项目都原生支持S3协议

云原生

云原生意味着MinIO不仅采用了云原生架构原则,也可以运行在云服务器上

MinIO本身可以看作一个提供S3 API的微服务,用户可以通过K8S,用yaml文档声明式地定义和部署一个MinIO集群以实现自动化运维,且可以根据自身情况,动态规划MinIO中的Pod实例数量来契合个人的项目数据

也正是因为MinIO的S3支持和他强大的工具生态,MinIO才得以成为对象存储的杀手锏

MinIO的使用

我们可以在自己的项目里运用MinIO存储

快速开始

1. 拉取并运行Docker镜像

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 拉取最新的MinIO镜像
docker pull minio/minio

# 运行 MinIO 容器
# 将主机上的 `~/minio/data` 目录挂载到容器内,用于持久化存储数据
# 将主机上的 `~/minio/config` 目录挂载到容器内,用于存储配置
# 默认控制台端口为 9001,API端口为 9000
docker run -p 9000:9000 -p 9001:9001 \
--name minio \
-v ~/minio/data:/data \
-v ~/minio/config:/root/.minio \
-e "MINIO_ROOT_USER=admin" \
-e "MINIO_ROOT_PASSWORD=yourstrongpassword" \
minio/minio server /data --console-address ":9001"

2. MinIO管理控制台

运行后,可以通过访问http://localhost:9001,使用上述账号密码登录MinIO管理控制台

3. 安装客户端库

1
go get github.com/minio/minio-go/v7

4. 初始化客户端库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main

import (
"log"
"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
)

func main() {
// 配置参数
endpoint := "localhost:9000"
accessKeyID := "admin" // 即 MINIO_ROOT_USER
secretAccessKey := "yourstrongpassword" // 即 MINIO_ROOT_PASSWORD
useSSL := false // 使用http/https

// 初始化客户端
minioClient, err := minio.New(endpoint, &minio.Options{
Creds: credentials.NewStaticV4(accessKeyID, secretAccessKey, ""),
Secure: useSSL,
})
if err != nil {
log.Fatalln("初始化客户端失败:", err)
}
log.Printf("%#v\n", minioClient) // 打印客户端信息,确认连接成功
}

API实例

初始化客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
  ctx := context.Background()
endpoint := "localhost:9000"
accessKeyID := "your-access-key"
secretAccessKey := "your-secret-key"
useSSL := false

// 初始化客户端
minioClient, err := minio.New(endpoint, &minio.Options{
Creds: credentials.NewStaticV4(accessKeyID, secretAccessKey, ""),
Secure: useSSL,
})
if err != nil {
log.Fatalln(err)
}

创建存储桶

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 创建一个存储桶(如果不存在)
bucketName := "my-documents"
location := "us-east-1" // 对MinIO来说,这个值通常无关紧要

err = minioClient.MakeBucket(ctx, bucketName, minio.MakeBucketOptions{Region: location})
if err != nil {
// 检查存储桶是否已存在
exists, errBucketExists := minioClient.BucketExists(ctx, bucketName)
if errBucketExists == nil && exists {
log.Printf("存储桶 %s 已存在\n", bucketName)
} else {
log.Fatalln("创建存储桶失败:", err)
}
} else {
log.Printf("成功创建存储桶 %s\n", bucketName)
}

上传文件

1
2
3
4
5
6
7
8
9
10
// 3. 上传一个文件
objectName := "example.jpg"
filePath := "./local-image.jpg"
contentType := "image/jpeg"

info, err := minioClient.FPutObject(ctx, bucketName, objectName, filePath, minio.PutObjectOptions{ContentType: contentType})
if err != nil {
log.Fatalln("上传文件失败:", err)
}
log.Printf("成功上传 %s,大小:%d bytes, ETag: %s\n", objectName, info.Size, info.ETag)

生成URL

1
2
3
4
5
6
7
// 4. 生成一个用于临时访问文件的预签名URL(有效期7天)
expiry := 7 * 24 * 60 * 60 // 秒
presignedURL, err := minioClient.PresignedGetObject(ctx, bucketName, objectName, time.Duration(expiry)*time.Second, nil)
if err != nil {
log.Fatalln("生成预签名URL失败:", err)
}
log.Println("您可以通过此链接临时访问文件:", presignedURL.String())

注意事项

  1. 启用HTTPS以加密数据传输
  2. 可以在控制台通过Access Key为不同程序创建具有特定策略(policy)的子账户,避免直接使用ROOT账户密钥
  3. 数据持久化:使用Docker或K8S不是时,务必通过卷(Volume)或持久化卷声明(PVC)将/data目录挂载到宿主机或网络存储,避免重启容器造成数据丢失
  4. 桶命名(bucket_name)限制: 3-63字符,只允许 小写字母,数字,点号.,连字符-,不能包含相邻的句点..或与连字符相邻的句点.-/-.
  5. 对象命名(object_name): 建议使用 ASCⅡ可打印字符,在Windows环境使用时,不允许包含对操作系统有歧义的字符 “./“

项目实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
package minIO

import (
"bytes"
"context"
"fmt"
"io"
"mime"
"os"
"path"
"path/filepath"

"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
"go.uber.org/zap"
)

type MinIOClient struct {
Client *minio.Client
BucketName string
}

func NewMinIOClient(endpoint, accessKeyID, secretAccessKey, bucketName, DefaultAvatarPath string) (*MinIOClient, error) {
//初始化minIOClient
minioClient, err := minio.New(endpoint, &minio.Options{
Creds: credentials.NewStaticV4(accessKeyID, secretAccessKey, ""),
Secure: false, // http
})
if err != nil {
return nil, fmt.Errorf("初始化minIO客户端失败: %v", err)
}

//验证bucket是否存在
exist, err := minioClient.BucketExists(context.Background(), bucketName)
if !exist {
err = minioClient.MakeBucket(context.Background(), bucketName, minio.MakeBucketOptions{})
if err != nil {
return nil, fmt.Errorf("新建bucket失败: %v", err)
}
}
if err != nil {
return nil, fmt.Errorf("验证minIO_bucket失败: %v", err)
}

//上传默认头像
Path := filepath.Join(".", DefaultAvatarPath)
DefaultAvatar, err := os.Open(Path)
if err != nil {
fmt.Printf("获取默认头像失败:%v\n", err)
}
DefaultAvatarData, _ := io.ReadAll(DefaultAvatar)
ext := path.Ext(DefaultAvatarPath)
reader := bytes.NewReader(DefaultAvatarData)
size := int64(len(DefaultAvatarData))
mimeType := mime.TypeByExtension(ext)
opts := minio.PutObjectOptions{ContentType: mimeType}
_, err = minioClient.PutObject(context.Background(), bucketName, DefaultAvatarPath, reader, size, opts)
if err != nil {
zap.S().Errorf("上传默认头像失败:%v\n", err)
fmt.Printf("上传默认头像失败:%v\n", err)
}

return &MinIOClient{
Client: minioClient,
BucketName: bucketName,
}, nil
}

func (m *MinIOClient) Save(ctx context.Context, objectName string, data []byte, ext string) error {
//捕获数据
//mimeType := mime.TypeByExtension(ext)
mimeType := mime.TypeByExtension(ext)
reader := bytes.NewReader(data)
size := int64(len(data))
//Options
opts := minio.PutObjectOptions{ContentType: mimeType}

//保存文件
//PutObject(ctx context.Context, bucketName, objectName string, reader io.Reader, objectSize int64,opts PutObjectOptions) (info UploadInfo, err error)
_, err := m.Client.PutObject(ctx, m.BucketName, objectName, reader, size, opts)
if err != nil {
zap.S().Errorf("保存到minIO失败: %v", err)
return fmt.Errorf("保存到minIO失败: %v", err)
}

return nil
}

func (m *MinIOClient) Delete(ctx context.Context, objectName string) error {
//RemoveObject(ctx context.Context, bucketName, objectName string, opts minio.RemoveObjectOptions) error
err := m.Client.RemoveObject(ctx, m.BucketName, objectName, minio.RemoveObjectOptions{})
if err != nil {
zap.S().Errorf("从minIO删除文件失败: %v", err)
return fmt.Errorf("从minIO删除文件失败: %v", err)
}

return nil
}

func (m *MinIOClient) Update(ctx context.Context, objectName string, data []byte, ext string) error {
return m.Save(ctx, objectName, data, ext)
}

func (m *MinIOClient) Exists(ctx context.Context, objectName string) (bool, error) {
//StatObject(ctx context.Context, bucketName, objectName string, opts StatObjectOptions) (ObjectInfo, error)
//NoSuchKey
_, err := m.Client.StatObject(ctx, m.BucketName, objectName, minio.StatObjectOptions{})
if err != nil {
if errType, ok := err.(minio.ErrorResponse); ok && errType.Code == "NoSuchKey" {
return false, nil
}
return false, fmt.Errorf("检查文件失败: %v", err)
}
return true, nil
}

func (m *MinIOClient) GetBytes(ctx context.Context, objectName string) ([]byte, error) {
//GetObject(ctx context.Context, bucketName, objectName string, opts GetObjectOptions) (*Object, error)
obj, err := m.Client.GetObject(ctx, m.BucketName, objectName, minio.GetObjectOptions{})
if err != nil {
return nil, fmt.Errorf("获取文件失败: %v", err)
}
defer obj.Close()

//检查文件是否存在
if _, err := obj.Stat(); err != nil {
return nil, fmt.Errorf("文件不存在: %v", err)
}

//读取数据
data, err := io.ReadAll(obj)
if err != nil {
return nil, fmt.Errorf("读取文件失败: %v", err)
}

return data, nil
}

func (m *MinIOClient) GetStream(ctx context.Context, objectName string) (io.ReadCloser, error) { //GetObject(ctx context.Context, bucketName, objectName string, opts GetObjectOptions) (*Object, error)
obj, err := m.Client.GetObject(ctx, m.BucketName, objectName, minio.GetObjectOptions{})
if err != nil {
return nil, fmt.Errorf("获取文件失败: %v", err)
}
//流式传输不关闭obj,直接返回

return obj, nil
}

2025过去了,也代表着我的大一上生活结束了

那么我的大一上,或者说2025,干了啥呢?


前半年的苦逼高三先按下不表,高考的目标本来也是邮砖的计科的,结果高考分数比预估的低了很多😭,
还好惆怅之余还是捡漏到了你邮的一个半科班,快哉快哉

然后的暑假自然也是爽快地玩了三个月


之后就到了九月份开学,初入重邮就经历了军训,依旧无所事事


到了十月初,红蓝的招新会开始了,原本来重邮就希望能进红蓝(高三时重邮招生宣传办的老师使劲鼓吹),我就去了解了一下
红蓝和各个部门,最后选择了后端开发,在后面选择了学习golang,开始了大学写代码之旅
在这期间,我先是过完了java和go的基础语法内容,然后放弃了java选择了go语言,并搭了一个网站当个人博客用,之后继续学习了go接口和并发,完整地过完了基础


十一月,我主要接触并学习了web编程,不得不说初入web相关还是感觉到很困惑和复杂的,要边补全计网基础内容,边明确前后端分工职责,还要学习gin相关新知识
和ai斗智斗勇。

除此之外,还学了写了jwt,协程池和mysql数据库,并开始写一些最基础的crud项目

在这个阶段,我也开始结识到了同届的各位大佬-mo神汪神T神(我喜欢你),认识到了相比之下的不足,也感受到了相当的压力


十二月的前几天,我粗略地过了一下linux和docker的相关内容后,学习了redis作缓存和一些缓存策略,同时意识到了八股的存在,恐怖如斯

学习完这些,也就差不多算学完了计划里大一上应该学的内容

不过照理说我更应该继续往后了解的,不过我放缓了脚步,同时开始写一个网盘crud项目,持续了两周直到期末周到来


一月,期末周开始,到考试结束,甚至说到现在,是我最恶堕的时候,打着备战期末的名头没写代码,复习文化课也是摸鱼耍
,高数也没好好学,直接摆烂准备补考

我忏悔😭😭😭,虽然这个坏习惯大概一辈子也改不掉😭,我可是高中天天,甚至每节课偷偷玩手机,还不写作业的人啊😭
如此安于现状且享乐主义😭我忏悔😭


至此一月即将结束,终末地也毕业没了肝的欲望,我终于滚回来继续写代码里

希望日后蒸蒸日上,寒假规划什么的放在随笔7里了


祝好!

0%