How to implement HTTP file upload in Go | net/http, multipart, and S3 support
Using Go's standard library net/http and mime/multipart, you can implement file uploads without external packages. This article provides a detailed explanation of production-level implementation, from parsing multipart bodies with r.ParseMultipartForm(), retrieving files with r.FormFile(), enforcing file size limits, validating MIME types, to S3 uploads using AWS SDK v2.
Receiving multipart requests with net/http
To receive a multipart/form-data request, call r.ParseMultipartForm(maxMemory). maxMemory is the maximum bytes to keep in memory; excess is written to temporary files.
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))
}
Implementation of File Size Limits
Using http.MaxBytesReader allows you to limit the amount of request body read. Since the argument to ParseMultipartForm alone cannot limit the overall request size, combining it with MaxBytesReader is important.
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
}
// ... 以降の処理
}
MIME Type Verification
The Content-Type header declared by the client can be forged. It is safer to read the leading bytes (magic number) of the actual file content and determine the type using http.DetectContentType.
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
}
S3 Upload Using AWS SDK v2
Go's AWS SDK v2 is significantly improved over SDK v1, featuring context support and modular organization. Multipart uploads are handled automatically by s3manager.Upload.
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
}
Implementation Example with Echo Framework
Using the web framework Echo allows you to configure routing and middleware simply. File upload handling works the same way as the standard net/http.
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"))
}
❓ Frequently Asked Questions
Can I implement file upload in Go without any external packages?
How do I validate MIME types safely in Go?
What changed between AWS SDK v1 and v2 for Go?
Test files for this article (free)
- → PNG Test Image (1MB) — For MIME type validation and
http.DetectContentTypetesting - → PDF Test File (1MB) — For verifying S3 upload and multipart processing
- → ZIP test file (1MB) — for boundary testing of file size limits