【2023-06〜2023-08】参加团队开发记录:(1) 使用Go+Gin创建图像下载/上传的API
从2023年6月开始,在一个在线沙龙上开始了团队开发,并且我为了学习目的参与其中(将于2023年8月结束)。
我加入了第三团队的后端成员。
第三团队的后端决定使用Go+Gin。
在参与团队开发的同时,我希望能将我的学习历程记录成文章。
这个系列的链接
-
- チーム開発参加の記録【2023-06~2023-08】(1) Go+Ginで画像をダウンロード/アップロードするAPIを作る
-
- チーム開発参加の記録【2023-06~2023-08】(2) sqlc + jackc/pgx/v5(v5.4.0)を使ってみた
-
- チーム開発参加の記録【2023-06~2023-08】(3) sqlc + jackc/pgx/v5(v5.4.1)からPostgreSQLの複合型の配列を使ってみた
-
- チーム開発参加の記録【2023-06~2023-08】(4) sqlc + jackc/pgx/v5 からPostgreSQLの複合型の配列を更新してみた
-
- チーム開発参加の記録【2023-06~2023-08】(5) gocronでスケジュール処理し、定期的にバッチジョブを起動してみた
- チーム開発参加の記録【2023-06~2023-08】(6) PostgreSQLの複合型の配列の更新について、もう少し煮詰める
※ 本文中的源代码主要是为了学习和验证目的而编写的,不适用于直接用于产品上,目标是追求能够使用的质量。
在本篇文章中所做的事情是
-
- Go+Ginで、画像ファイルをダウンロード/アップロードするAPIを作成します。
-
- アップロードするAPIは2種類作成します。
GoCVを使用してWebP形式に変換してアップロードするAPI
画像形式を変換せずにそのままアップロードするAPI
アップロード先はCloud Storageとします。
アプリのデプロイ先はCloud Runとします。
源代码
首先,我们将检查每个API,然后决定是否将整个源代码放在后面。
画像下载API(GET)
-
- クエリパラメータ
path:Cloud Storageのpathを指定
func getImage(c *gin.Context) {
ctx := context.Background()
// クエリパラメータ
path := c.Query("path")
// Cloud Storageのclientとbucket
client, err := storage.NewClient(ctx)
if err != nil {
c.JSON(http.StatusInternalServerError, httpError{Error: err.Error()})
return
}
defer client.Close()
bucket := client.Bucket(bucketName)
// Cloud Storageから画像read
obj := bucket.Object(path)
reader, err := obj.NewReader(ctx)
if err != nil {
c.JSON(http.StatusInternalServerError, httpError{Error: err.Error()})
return
}
defer reader.Close()
c.DataFromReader(http.StatusOK, reader.Attrs.Size, reader.Attrs.ContentType, reader, nil)
}
为了测试这个API的运行,我事先在Cloud Storage中准备了我拍摄的照片文件。
- path:2023-06-10/うちの雪の日の庭.jpg
使用邮递员来调用API,会显示如下的图像。
- APIのURL:http://localhost:8080/getImage?path=2023-06-10/うちの雪の日の庭.jpg
画像上传API:转换为WebP格式(POST)的版本。
-
- リクエストボディ
dir:Cloud Storageの格納先ディレクトリ
uploadFile:画像ファイル
type postWebpRequest struct {
Dir string `form:"dir"`
}
type postWebpResponse struct {
Message string `form:"message"`
Path string `form:"path"`
}
func postWebp(c *gin.Context) {
ctx := context.Background()
// リクエストボディ(uploadFile以外)
var request postWebpRequest
err := c.Bind(&request)
if err != nil {
c.JSON(http.StatusInternalServerError, httpError{Error: err.Error()})
return
}
// リクエストボディ(uploadFile)
fi, uploadedFile, err := c.Request.FormFile("uploadFile")
if err != nil {
c.JSON(http.StatusInternalServerError, httpError{Error: err.Error()})
return
}
defer fi.Close()
// Cloud Storageのclientとbucket
client, err := storage.NewClient(ctx)
if err != nil {
c.JSON(http.StatusInternalServerError, httpError{Error: err.Error()})
return
}
defer client.Close()
bucket := client.Bucket(bucketName)
// ファイルをバイナリ形式で読み込み
buf := bytes.NewBuffer(nil)
if _, err := io.Copy(buf, fi); err != nil {
c.JSON(http.StatusInternalServerError, httpError{Error: err.Error()})
return
}
// バイナリを画像形式に変換
img, err := gocv.IMDecode(buf.Bytes(), gocv.IMReadColor)
if err != nil {
c.JSON(http.StatusInternalServerError, httpError{Error: err.Error()})
return
}
// 画像をWebP形式に変換。圧縮率80にしました。
webp, err := gocv.IMEncodeWithParams(".webp", img, []int{gocv.IMWriteWebpQuality, 80})
if err != nil {
c.JSON(http.StatusInternalServerError, httpError{Error: err.Error()})
return
}
// アップロード先pathの調整(画像ファイル名に拡張子「.webp」を追加)
dir := request.Dir
if 0 < len(dir) {
if dir[0] == '/' {
dir = dir[1:]
}
}
if 0 < len(dir) {
if dir[len(dir)-1] == '/' {
dir = dir[:len(dir)-1]
}
}
var path string
if 0 < len(dir) {
path = dir + "/" + uploadedFile.Filename + ".webp"
} else {
path = uploadedFile.Filename + ".webp"
}
// WebP画像をCloud Storageにwrite
obj := bucket.Object(path)
writer := obj.NewWriter(ctx)
if _, err := writer.Write(webp.GetBytes()); err != nil {
c.JSON(http.StatusInternalServerError, httpError{Error: err.Error()})
return
}
if err := writer.Close(); err != nil {
c.JSON(http.StatusInternalServerError, httpError{Error: err.Error()})
return
}
c.JSON(http.StatusOK, postWebpResponse{
Message: "file uploaded successfully",
Path: writer.Attrs().Name,
})
}
使用这个API,首先尝试上传jpeg文件“我们雪天的花园.jpg”(使用Postman)。
调用API后,Cloud Storage上出现了一个新的「うちの雪の日の庭.jpg.webp」文件。该文件的大小比原始的jpg文件要小。
接下来,我试着上传PNG文件”开发界面.png”(使用Postman工具)。
调用API后,在Cloud Storage上出现了新的“开发画面.png.webp”。
画像上传(保持原始图片格式版本)的 POST API
这个API是用来上传WebP格式的图片的,但也可以使用其他格式的图片。
-
- リクエストボディ
dir:Cloud Storageの格納先ディレクトリ
uploadFile:画像ファイル
type postImageRequest struct {
Dir string `form:"dir"`
}
type postImageResponse struct {
Message string `form:"message"`
Path string `form:"path"`
}
func postImage(c *gin.Context) {
ctx := context.Background()
// リクエストボディ(uploadFile以外)
var request postImageRequest
err := c.Bind(&request)
if err != nil {
c.JSON(http.StatusInternalServerError, httpError{Error: err.Error()})
return
}
// リクエストボディ(uploadFile)
fi, uploadedFile, err := c.Request.FormFile("uploadFile")
if err != nil {
c.JSON(http.StatusInternalServerError, httpError{Error: err.Error()})
return
}
defer fi.Close()
// Cloud Storageのclientとbucket
client, err := storage.NewClient(ctx)
if err != nil {
c.JSON(http.StatusInternalServerError, httpError{Error: err.Error()})
return
}
defer client.Close()
bucket := client.Bucket(bucketName)
// アップロード先pathの調整
dir := request.Dir
if 0 < len(dir) {
if dir[0] == '/' {
dir = dir[1:]
}
}
if 0 < len(dir) {
if dir[len(dir)-1] == '/' {
dir = dir[:len(dir)-1]
}
}
var path string
if 0 < len(dir) {
path = dir + "/" + uploadedFile.Filename
} else {
path = uploadedFile.Filename
}
// 画像をCloud Storageにwrite
obj := bucket.Object(path)
writer := obj.NewWriter(ctx)
if _, err := io.Copy(writer, fi); err != nil {
c.JSON(http.StatusInternalServerError, httpError{Error: err.Error()})
return
}
if err := writer.Close(); err != nil {
c.JSON(http.StatusInternalServerError, httpError{Error: err.Error()})
return
}
c.JSON(http.StatusOK, postImageResponse{
Message: "file uploaded successfully",
Path: writer.Attrs().Name,
})
}
我将使用这个API来上传一个名为 “开发画面.png” 的PNG文件(使用Postman)。
当调用API后,在Cloud Storage中出现了新的“开发画面.png”。
整个源代码
主要的.go文件
package main
import (
"bytes"
"cloud.google.com/go/storage"
"context"
"github.com/gin-gonic/gin"
"gocv.io/x/gocv"
"io"
"net/http"
)
type httpError struct {
Error string `json:"error"`
}
var bucketName string = "your_bucket_0"
func healthCheck(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "ok",
})
}
func getImage(c *gin.Context) {
ctx := context.Background()
// クエリパラメータ
path := c.Query("path")
// Cloud Storageのclientとbucket
client, err := storage.NewClient(ctx)
if err != nil {
c.JSON(http.StatusInternalServerError, httpError{Error: err.Error()})
return
}
defer client.Close()
bucket := client.Bucket(bucketName)
// Cloud Storageから画像read
obj := bucket.Object(path)
reader, err := obj.NewReader(ctx)
if err != nil {
c.JSON(http.StatusInternalServerError, httpError{Error: err.Error()})
return
}
defer reader.Close()
c.DataFromReader(http.StatusOK, reader.Attrs.Size, reader.Attrs.ContentType, reader, nil)
}
type postWebpRequest struct {
Dir string `form:"dir"`
}
type postWebpResponse struct {
Message string `form:"message"`
Path string `form:"path"`
}
func postWebp(c *gin.Context) {
ctx := context.Background()
// リクエストボディ(uploadFile以外)
var request postWebpRequest
err := c.Bind(&request)
if err != nil {
c.JSON(http.StatusInternalServerError, httpError{Error: err.Error()})
return
}
// リクエストボディ(uploadFile)
fi, uploadedFile, err := c.Request.FormFile("uploadFile")
if err != nil {
c.JSON(http.StatusInternalServerError, httpError{Error: err.Error()})
return
}
defer fi.Close()
// Cloud Storageのclientとbucket
client, err := storage.NewClient(ctx)
if err != nil {
c.JSON(http.StatusInternalServerError, httpError{Error: err.Error()})
return
}
defer client.Close()
bucket := client.Bucket(bucketName)
// ファイルをバイナリ形式で読み込み
buf := bytes.NewBuffer(nil)
if _, err := io.Copy(buf, fi); err != nil {
c.JSON(http.StatusInternalServerError, httpError{Error: err.Error()})
return
}
// バイナリを画像形式に変換
img, err := gocv.IMDecode(buf.Bytes(), gocv.IMReadColor)
if err != nil {
c.JSON(http.StatusInternalServerError, httpError{Error: err.Error()})
return
}
// 画像をWebP形式に変換。圧縮率80にしました。
webp, err := gocv.IMEncodeWithParams(".webp", img, []int{gocv.IMWriteWebpQuality, 80})
if err != nil {
c.JSON(http.StatusInternalServerError, httpError{Error: err.Error()})
return
}
// アップロード先pathの調整(画像ファイル名に拡張子「.webp」を追加)
dir := request.Dir
if 0 < len(dir) {
if dir[0] == '/' {
dir = dir[1:]
}
}
if 0 < len(dir) {
if dir[len(dir)-1] == '/' {
dir = dir[:len(dir)-1]
}
}
var path string
if 0 < len(dir) {
path = dir + "/" + uploadedFile.Filename + ".webp"
} else {
path = uploadedFile.Filename + ".webp"
}
// WebP画像をCloud Storageにwrite
obj := bucket.Object(path)
writer := obj.NewWriter(ctx)
if _, err := writer.Write(webp.GetBytes()); err != nil {
c.JSON(http.StatusInternalServerError, httpError{Error: err.Error()})
return
}
if err := writer.Close(); err != nil {
c.JSON(http.StatusInternalServerError, httpError{Error: err.Error()})
return
}
c.JSON(http.StatusOK, postWebpResponse{
Message: "file uploaded successfully",
Path: writer.Attrs().Name,
})
}
type postImageRequest struct {
Dir string `form:"dir"`
}
type postImageResponse struct {
Message string `form:"message"`
Path string `form:"path"`
}
func postImage(c *gin.Context) {
ctx := context.Background()
// リクエストボディ(uploadFile以外)
var request postImageRequest
err := c.Bind(&request)
if err != nil {
c.JSON(http.StatusInternalServerError, httpError{Error: err.Error()})
return
}
// リクエストボディ(uploadFile)
fi, uploadedFile, err := c.Request.FormFile("uploadFile")
if err != nil {
c.JSON(http.StatusInternalServerError, httpError{Error: err.Error()})
return
}
defer fi.Close()
// Cloud Storageのclientとbucket
client, err := storage.NewClient(ctx)
if err != nil {
c.JSON(http.StatusInternalServerError, httpError{Error: err.Error()})
return
}
defer client.Close()
bucket := client.Bucket(bucketName)
// アップロード先pathの調整
dir := request.Dir
if 0 < len(dir) {
if dir[0] == '/' {
dir = dir[1:]
}
}
if 0 < len(dir) {
if dir[len(dir)-1] == '/' {
dir = dir[:len(dir)-1]
}
}
var path string
if 0 < len(dir) {
path = dir + "/" + uploadedFile.Filename
} else {
path = uploadedFile.Filename
}
// 画像をCloud Storageにwrite
obj := bucket.Object(path)
writer := obj.NewWriter(ctx)
if _, err := io.Copy(writer, fi); err != nil {
c.JSON(http.StatusInternalServerError, httpError{Error: err.Error()})
return
}
if err := writer.Close(); err != nil {
c.JSON(http.StatusInternalServerError, httpError{Error: err.Error()})
return
}
c.JSON(http.StatusOK, postImageResponse{
Message: "file uploaded successfully",
Path: writer.Attrs().Name,
})
}
func main() {
router := gin.Default()
router.GET("/", healthCheck)
router.GET("/getImage", getImage)
router.POST("/postWebp", postWebp)
router.POST("/postImage", postImage)
router.Run("0.0.0.0:8080")
}
go.mod -> go模块
module exercise_image
go 1.20
require (
cloud.google.com/go/storage v1.30.1
github.com/gin-gonic/gin v1.9.1
gocv.io/x/gocv v0.32.1
)
require (
cloud.google.com/go v0.110.0 // indirect
cloud.google.com/go/compute v1.18.0 // indirect
cloud.google.com/go/compute/metadata v0.2.3 // indirect
cloud.google.com/go/iam v0.12.0 // indirect
github.com/bytedance/sonic v1.9.1 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.14.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect
github.com/googleapis/gax-go/v2 v2.7.1 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect
go.opencensus.io v0.24.0 // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/crypto v0.9.0 // indirect
golang.org/x/net v0.10.0 // indirect
golang.org/x/oauth2 v0.6.0 // indirect
golang.org/x/sys v0.8.0 // indirect
golang.org/x/text v0.9.0 // indirect
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
google.golang.org/api v0.114.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20230320184635-7606e756e683 // indirect
google.golang.org/grpc v1.53.0 // indirect
google.golang.org/protobuf v1.30.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
将应用程序部署到Cloud Run。
将已构建的OpenCV容器映像推送到Docker Hub。
为了使用GoCV,我们需要从源代码构建OpenCV 4.7.0。由于构建过程需要一些时间,所以在构建完成后,我们将将Docker映像推送到Docker Hub,并在以后重复使用它们。我们将使用以下的Dockerfile。
FROM golang:1.20.5-bullseye
RUN apt-get update && apt-get install -y \
unzip \
cmake \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
RUN curl -SL https://github.com/opencv/opencv/archive/4.7.0.zip -o opencv-4.7.0.zip \
&& curl -SL https://github.com/opencv/opencv_contrib/archive/refs/tags/4.7.0.zip -o opencv_contrib-4.7.0.zip \
&& unzip opencv-4.7.0.zip \
&& unzip opencv_contrib-4.7.0.zip -d ./opencv-4.7.0/
RUN cd opencv-4.7.0 \
&& mkdir build \
&& cd build \
&& cmake -DOPENCV_GENERATE_PKGCONFIG=ON -DOPENCV_EXTRA_MODULES_PATH=../opencv_contrib-4.7.0/modules .. \
&& cmake --build . \
&& make \
&& make install \
&& ldconfig
我已经将以下内容推送到Docker Hub。
将应用部署到Cloud Run。
下一步是准备用于应用程序的Dockerfile。
使用在Docker Hub上上传的”kanedasmec/golang_opencv:1.20.5_4.7.0″镜像。
FROM kanedasmec/golang_opencv:1.20.5_4.7.0 as builder
ENV APP_HOME /app
WORKDIR $APP_HOME
COPY ./ ./
RUN go build main.go
FROM debian:bullseye-slim
LABEL version="0.1"
RUN apt-get update && apt-get install -y \
lsb-release \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
COPY --from=builder /usr/local/ /usr/local/
COPY --from=builder /app/main /app/
ENV APP_HOME /app
WORKDIR $APP_HOME
RUN ldconfig
EXPOSE 8080
CMD ["./main"]
使用这个Dockerfile来为Cloud Run进行构建。
在your_project中输入Google Cloud的项目名称。
在your_tag中设置标签(版本)。
gcloud builds submit --tag gcr.io/your_project/exercise:your_tag
如果构建成功,就部署到Cloud Run上。
最好调整每个参数等。
gcloud run deploy exercise \
--image gcr.io/your_project/exercise:your_tag \
--platform managed \
--cpu 1 \
--max-instances 1 \
--concurrency 1 \
--memory 1024Mi \
--timeout 3600 \
--allow-unauthenticated \
--region us-central1 \
--service-account your_identity
在部署完后,我尝试在浏览器中调用GET API。
你可以使用Postman来进行POST API的功能验证。