保存方式
在用户头像、背景图等功能下,需要保存用户提交的图片。
这时候就要考虑怎么把图片保存下来,一般来说,图片保存到数据库通常有以下几种方式:
1、将图片转换为字节流保存
将图片文件读取为字节流的形式,然后将字节流存储在数据库相应字段中。这种方式最为简单直接,适用于小型图片或需要频繁访问的场景
2、将图片保存在服务器上,数据库中只保存图片的路径
另一种常见的方式是将图片保存在服务器的文件系统中,然后在数据库中只保存图片的路径。这样可以有效的减轻数据库的负担,也方便管理和维护图片文件
3、使用Base64编码保存图片
将图片转换为Base64编码的字符串,然后将字符串保存在数据库中。这种方式适用于在前端直接显示出图片的场景,但是会增大数据库的负担
4、适用专门的图片存储服务
在图片数量太多或者其他情况下,可以选择专门的图片存储服务(如AWS S3、阿里云OSS等),把图片上传到这些服务中,然后数据库中保存图片的URL和标识符
Base64编码方式保存较为不常用,本文只给出字节流和图片路径的保存方式代码实现
关于图片存储服务,感兴趣可以去官网查看文档说明:
Amazon S3 云存储_对象存储_云存储服务-AWS云服务
对象存储 OSS_云存储服务_企业数据管理_存储-阿里云 (aliyun.com)
字节流保存
package main
import (
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql"
"os"
)
func main() {
// 连接数据库
db, err := sql.Open("mysql", "username:password@tcp(localhost:3306)/dbname")
if err != nil {
fmt.Println("连接数据库失败:", err)
return
}
defer db.Close()
// 读取图片文件
imagePath := "./image/img.png" // 图片的相对路径 也可以用绝对路径
imageFile, err := os.Open(imagePath)
if err != nil {
fmt.Println("打开图片文件失败:", err)
return
}
defer imageFile.Close()
// 转换为字节流
imageData, err := os.ReadFile(imagePath)
if err != nil {
fmt.Println("读取图片文件失败:", err)
}
// 把图片保存到数据库
insertQuery := "insert into images (image_data) values (?)"
_, err = db.Exec(insertQuery, imageData)
if err != nil {
fmt.Println("保存图片到数据库失败:", err)
return
}
fmt.Println("保存图片到数据库成功")
}
要保存到mysql数据库,首先要下载驱动
go get -u github.com/go-sql-driver/mysql
导入驱动包后,在Open方法中写上自己数据库的用户名和密码以及要连接的数据库
打开图片文件后读取为字节流,再保存到数据库中就可以了,要注意保存的字段类型需要是二进制类型(blob、mediumblob、longblob)
插入数据库成功后,可以打开图片:
这张图片的大小只有34k,非常小,所以可以采用这种方式存储,不会浪费多少性能
如果限制图片上传大小,就可以考虑这种保存方式
保存url
个人认为这是最合理的方案
如果贸然把图片保存到数据库中,在图片较大的情况下,会影响数据库的性能。因为数据库数据量增大后,操作数据的速度就会下降。在处理请求时大部分的时间会耗费在等待数据库将数据处理完毕。
在数据库中保存图片路径时,可以按照年月日去生成路径,用时间戳命名
比如在保存一张头像图片时,数据库中的路径就可以是
image/avatar/2024/03/08/1709863925265073900.jpg
这只是一个参考的实现方式,关键点在于要把图片分文件夹保存且保证图片名唯一
当一个文件夹的文件数量到达一定程度时,从文件夹中获取文件的速度就会越来越慢。为了保证速度,才要按照一定的规则去分散到多个目录中去。
为了保证每一张图片都有唯一的名称,就可以使用时间戳来生成
在并发量大的时候,可以把目录分的更详细,可以精确到每一个小时一个文件夹,并且提高时间戳的精度,例如有两个用户同时在上传图(没有绝对的同时),为了保证图片名称不同而不至于被覆盖,可以把时间戳精确到纳秒
示例代码:
package main
import (
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql"
"io"
"os"
"path/filepath"
"time"
)
func main() {
// 连接数据库
db, err := sql.Open("mysql", "username:password@tcp(localhost:3306)/dbname")
if err != nil {
fmt.Println("连接数据库失败:", err)
return
}
defer db.Close()
// 读取图片文件
imagePath := "./image/img.png" // 图片的相对路径 也可以用绝对路径
imageFile, err := os.Open(imagePath)
if err != nil {
fmt.Println("打开图片文件失败:", err)
return
}
defer imageFile.Close()
// 在image文件夹下再以当前年月日创建文件夹
folderPath := filepath.Join("image", time.Now().Format("2006/01/02"))
// 如果文件夹不存在,就创建一个
if _, err := os.Stat(folderPath); os.IsNotExist(err) {
err = os.MkdirAll(folderPath, os.ModePerm)
if err != nil {
fmt.Println("创建文件夹失败:", err)
return
}
}
// 获取当前时间戳,用于给图片命名
timestamp := time.Now().UnixNano()
fileName := filepath.Join(folderPath, fmt.Sprintf("%d.jpg", timestamp))
// 先创建文件,再复制到这个文件中
targetFile, err := os.Create(fileName)
// 保存文件
_, err = io.Copy(targetFile, imageFile)
if err != nil {
fmt.Println("保存文件失败:", err)
return
}
// 把路径保存到url中
sqlStr := "insert into images (image_url) values (?)"
_, err = db.Exec(sqlStr, fileName)
if err != nil {
fmt.Println("保存图片路径到数据库失败:", err)
return
}
}
保存的图片文件如下:
数据库中保存:
保存到数据库的时候前面最好不要有斜杠/
为了方便以后组装图片路径,图片路径不要写全
图片压缩
有一些场景下用户上传的图片会很大,比如拍照上传等等,这时候可以在保存图片之前先压缩(当然可以前端压缩好发到后端)
在go中对图片进行压缩,可以使用resize包,它提供了简单好用的图片压缩功能
要使用这个包,首先要下载
go get github.com/nfnt/resize
然后就可以使用这个包对图片进行压缩
package main
import (
"image"
"image/jpeg"
"log"
"os"
"github.com/nfnt/resize"
)
func main() {
// 打开原始图片文件
file, err := os.Open("original.jpg")
if err != nil {
log.Fatal(err)
}
defer file.Close()
// 解码原始图片文件
img, _, err := image.Decode(file)
if err != nil {
log.Fatal(err)
}
// 压缩图片到指定大小
newWidth := 200
newHeight := 0 // 0 表示按比例缩放
resizedImg := resize.Resize(uint(newWidth), uint(newHeight), img, resize.Lanczos3)
// 创建压缩后的图片文件
out, err := os.Create("compressed.jpg")
if err != nil {
log.Fatal(err)
}
defer out.Close()
// 将压缩后的图片写入文件
jpeg.Encode(out, resizedImg, nil)
log.Println("图片压缩完成")
}
使用resize库指明了压缩后图片的高度和宽度,然后再保存到文件中
总结
对于不同的场景,需要分析之后去选择解决方案,没有最好的方案,只有相对较好的。