Como implementar upload de arquivo HTTP em Go | Suporte para net/http, multipart e S3
Usando as bibliotecas padrão do Go <code>net/http</code> e <code>mime/multipart</code>, você pode implementar upload de arquivo sem pacotes externos. Este artigo explora em detalhes a implementação em nível de produção, desde a análise do corpo multipart com <code>r.ParseMultipartForm()</code>, obtenção de arquivo com <code>r.FormFile()</code>, restrições de tamanho de arquivo, validação de tipo MIME, até upload para S3 usando AWS SDK v2.
Recepção de requisição multipart com net/http
Para receber solicitações <code>multipart/form-data</code>, chame <code>r.ParseMultipartForm(maxMemory)</code>. <code>maxMemory</code> é o número máximo de bytes mantidos na memória, e o excedente é gravado em um arquivo temporário.
package main
import (
"fmt"
"io"
"log"
"net/http"
"os"
"path/filepath"
"time"
)
func uploadHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
return
}
// 最大10MBをメモリに保持し、超過分は /tmp に一時保存
const maxMemory = 10 << 20 // 10MB
if err := r.ParseMultipartForm(maxMemory); err != nil {
http.Error(w, "フォームの解析に失敗しました: "+err.Error(), http.StatusBadRequest)
return
}
// フォームの文字列フィールドを取得
title := r.FormValue("title")
fmt.Printf("title: %s\n", title)
// ファイルを取得
file, header, err := r.FormFile("file")
if err != nil {
http.Error(w, "ファイルの取得に失敗しました: "+err.Error(), http.StatusBadRequest)
return
}
defer file.Close()
fmt.Printf("ファイル名: %s\n", header.Filename)
fmt.Printf("ファイルサイズ: %d bytes\n", header.Size)
fmt.Printf("Content-Type: %s\n", header.Header.Get("Content-Type"))
// 保存先のパスを生成
timestamp := time.Now().UnixNano()
safeFilename := filepath.Base(header.Filename) // ディレクトリトラバーサル対策
destPath := fmt.Sprintf("./uploads/%d_%s", timestamp, safeFilename)
// ファイルを保存
dst, err := os.Create(destPath)
if err != nil {
http.Error(w, "ファイルの作成に失敗しました", http.StatusInternalServerError)
return
}
defer dst.Close()
if _, err := io.Copy(dst, file); err != nil {
http.Error(w, "ファイルの書き込みに失敗しました", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"success":true,"filename":%q}`, safeFilename)
}
func main() {
os.MkdirAll("./uploads", 0755)
http.HandleFunc("/upload", uploadHandler)
log.Println("サーバー起動: :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
Implementação do limite de tamanho de arquivo
Usar <code>http.MaxBytesReader</code> permite limitar a quantidade de leitura do corpo da solicitação. Como o argumento de <code>ParseMultipartForm</code> sozinho não pode limitar o tamanho total da solicitação, a combinação com <code>MaxBytesReader</code> é importante.
package handler
import (
"errors"
"net/http"
)
const (
MaxUploadSize = 50 << 20 // 50MB
MaxMemory = 10 << 20 // 10MB(メモリに保持する最大量)
)
func uploadHandler(w http.ResponseWriter, r *http.Request) {
// リクエストボディ全体のサイズを制限
r.Body = http.MaxBytesReader(w, r.Body, MaxUploadSize)
if err := r.ParseMultipartForm(MaxMemory); err != nil {
var maxBytesErr *http.MaxBytesError
if errors.As(err, &maxBytesErr) {
http.Error(w, fmt.Sprintf("ファイルサイズが上限(%dMB)を超えています。", MaxUploadSize>>20), http.StatusRequestEntityTooLarge)
} else {
http.Error(w, "不正なリクエストです。", http.StatusBadRequest)
}
return
}
file, header, err := r.FormFile("file")
if err != nil {
http.Error(w, "ファイルの取得に失敗しました。", http.StatusBadRequest)
return
}
defer file.Close()
// ヘッダーのサイズも確認(ParseMultipartForm 後でも確認可能)
if header.Size > MaxUploadSize {
http.Error(w, "ファイルが大きすぎます。", http.StatusRequestEntityTooLarge)
return
}
// ... 以降の処理
}
Verificação de tipo MIME
O cabeçalho <code>Content-Type</code> declarado pelo cliente pode ser falsificado. É seguro ler os primeiros bytes do conteúdo do arquivo real (número mágico) e fazer a verificação com <code>http.DetectContentType</code>.
package handler
import (
"fmt"
"mime/multipart"
"net/http"
)
var allowedMIMETypes = map[string]bool{
"image/jpeg": true,
"image/png": true,
"image/gif": true,
"image/webp": true,
"application/pdf": true,
"application/zip": true,
}
// detectMIMEType はファイルの先頭512バイトを読んでMIMEタイプを判定します
func detectMIMEType(file multipart.File) (string, error) {
// 先頭512バイトを読む(http.DetectContentType は最大512バイトを使用)
buf := make([]byte, 512)
n, err := file.Read(buf)
if err != nil && err.Error() != "EOF" {
return "", fmt.Errorf("ファイルの読み込みに失敗しました: %w", err)
}
// ファイルポインタを先頭に戻す(後続の読み込みのため)
if seeker, ok := file.(interface{ Seek(int64, int) (int64, error) }); ok {
if _, err := seeker.Seek(0, 0); err != nil {
return "", fmt.Errorf("シークに失敗しました: %w", err)
}
}
mimeType := http.DetectContentType(buf[:n])
return mimeType, nil
}
func validateFile(file multipart.File, header *multipart.FileHeader) error {
// 実際のMIMEタイプを検出
mimeType, err := detectMIMEType(file)
if err != nil {
return err
}
if !allowedMIMETypes[mimeType] {
return fmt.Errorf("許可されていないファイル形式です(%s)", mimeType)
}
return nil
}
Upload do S3 usando AWS SDK v2
O AWS SDK v2 para Go é significativamente melhorado em relação ao SDK v1, com suporte a contexto e divisão de módulos como características principais. Upload multipart é processado automaticamente pelo <code>s3manager.Upload</code>.
go get github.com/aws/aws-sdk-go-v2/config
go get github.com/aws/aws-sdk-go-v2/service/s3
go get github.com/aws/aws-sdk-go-v2/feature/s3/manager
package s3upload
import (
"context"
"fmt"
"io"
"mime/multipart"
"os"
"time"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/feature/s3/manager"
"github.com/aws/aws-sdk-go-v2/service/s3"
)
type S3Uploader struct {
uploader *manager.Uploader
bucket string
}
func NewS3Uploader(ctx context.Context) (*S3Uploader, error) {
// 環境変数(AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION)から設定を読み込む
cfg, err := config.LoadDefaultConfig(ctx,
config.WithRegion(os.Getenv("AWS_REGION")),
)
if err != nil {
return nil, fmt.Errorf("AWS 設定の読み込みに失敗しました: %w", err)
}
client := s3.NewFromConfig(cfg)
uploader := manager.NewUploader(client, func(u *manager.Uploader) {
u.PartSize = 10 * 1024 * 1024 // 10MB パートサイズ
u.Concurrency = 3 // 並列アップロード数
})
return &S3Uploader{
uploader: uploader,
bucket: os.Getenv("AWS_S3_BUCKET"),
}, nil
}
func (u *S3Uploader) Upload(ctx context.Context, file io.Reader, header *multipart.FileHeader, mimeType string) (string, error) {
// ユニークなキーを生成
timestamp := time.Now().UnixNano()
key := fmt.Sprintf("uploads/%d_%s", timestamp, header.Filename)
result, err := u.uploader.Upload(ctx, &s3.PutObjectInput{
Bucket: aws.String(u.bucket),
Key: aws.String(key),
Body: file,
ContentType: aws.String(mimeType),
// ACL を使わずバケットポリシーで制御する場合は省略
})
if err != nil {
return "", fmt.Errorf("S3 へのアップロードに失敗しました: %w", err)
}
return result.Location, nil
}
// Presigned URL の生成(プライベートファイルへの一時アクセス)
func GeneratePresignedURL(ctx context.Context, bucket, key string, expiry time.Duration) (string, error) {
cfg, err := config.LoadDefaultConfig(ctx)
if err != nil {
return "", err
}
client := s3.NewFromConfig(cfg)
presigner := s3.NewPresignClient(client)
req, err := presigner.PresignGetObject(ctx, &s3.GetObjectInput{
Bucket: aws.String(bucket),
Key: aws.String(key),
}, s3.WithPresignExpires(expiry))
if err != nil {
return "", fmt.Errorf("Presigned URL の生成に失敗しました: %w", err)
}
return req.URL, nil
}
Exemplo de implementação com o framework Echo
Usando o framework web <code>Echo</code>, é possível configurar roteamento e middleware de forma simples. O processamento de upload de arquivo funciona da mesma forma que <code>net/http</code> padrão.
package main
import (
"net/http"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
func main() {
e := echo.New()
e.Use(middleware.Logger())
e.Use(middleware.Recover())
e.POST("/upload", func(c echo.Context) error {
// マルチパートフォームを解析(最大32MBまでメモリに保持)
if err := c.Request().ParseMultipartForm(32 << 20); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
}
file, err := c.FormFile("file")
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "ファイルが見つかりません"})
}
src, err := file.Open()
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
defer src.Close()
// MIMEタイプの検証
mimeType, err := detectMIMEType(src)
if err != nil || !allowedMIMETypes[mimeType] {
return c.JSON(http.StatusUnsupportedMediaType, map[string]string{
"error": "許可されていないファイル形式です",
})
}
return c.JSON(http.StatusOK, map[string]interface{}{
"filename": file.Filename,
"size": file.Size,
"mime": mimeType,
})
})
e.Logger.Fatal(e.Start(":8080"))
}
Arquivo de teste disponível para usar neste artigo (gratuito)
- → <a href="/ja/files/images/png/1mb/" class="text-primary-600 dark:text-primary-400 hover:underline">Imagem PNG de teste (1MB)</a> — Para teste de validação de tipo MIME e <code>http.DetectContentType</code>
- → <a href="/ja/files/pdf/1mb/" class="text-primary-600 dark:text-primary-400 hover:underline">Arquivo de teste PDF (1MB)</a> — Para confirmação de upload S3 e processamento multipart
- → <a href="/ja/files/zip/1mb/" class="text-primary-600 dark:text-primary-400 hover:underline">Arquivo de teste ZIP (1MB)</a> — Para testes de valor limite de restrição de tamanho de arquivo
Artigos relacionados
- → <a href="/ja/blog/s3-upload-limit/" class="text-primary-600 dark:text-primary-400 hover:underline">Resumo dos limites de upload de arquivos do AWS S3 e CloudFront</a>
- → <a href="/ja/blog/file-validation-checklist/" class="text-primary-600 dark:text-primary-400 hover:underline">Lista de verificação de implementação de validação de arquivo para formulários web</a>
- → <a href="/ja/blog/laravel-file-upload/" class="text-primary-600 dark:text-primary-400 hover:underline">Guia de implementação de upload de arquivos no Laravel | Validação, Storage e suporte S3</a>