【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
image.png

使用邮递员来调用API,会显示如下的图像。

    APIのURL:http://localhost:8080/getImage?path=2023-06-10/うちの雪の日の庭.jpg
image.png

画像上传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)。

image.png

调用API后,Cloud Storage上出现了一个新的「うちの雪の日の庭.jpg.webp」文件。该文件的大小比原始的jpg文件要小。

image.png
image.png

接下来,我试着上传PNG文件”开发界面.png”(使用Postman工具)。

image.png

调用API后,在Cloud Storage上出现了新的“开发画面.png.webp”。

image.png
image.png

画像上传(保持原始图片格式版本)的 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)。

image.png

当调用API后,在Cloud Storage中出现了新的“开发画面.png”。

image.png
image.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。

image.png

你可以使用Postman来进行POST API的功能验证。

广告
将在 10 秒后关闭
bannerAds