This article will teach you how to upload single and multiple files on a Golang web server. The tutorial focuses on image upload, but the concepts can be extended and used for any file type.
When building APIs for either Web, Mobile, or Desktop apps, very often, the need to make provision for file upload arises. Therefore, knowing how to upload and process files in Golang will give you more confidence when searching for jobs.
At the end of this comprehensive guide, you should know how to:
- Upload a single image and store it on the disk
- Upload multiple images and store them on the disk
- Upload and resize a single image before storing it on the disk
- Upload and resize multiple images before storing them on the disk
More practice:
- How to Implement Two-factor Authentication (2FA) in React.js
- How to Implement Two-factor Authentication (2FA) in Node.js
- Two-factor Authentication (2FA) in FastAPI and Python
- How to Implement (2FA) Two-factor Authentication in Golang
Prerequisites
While this tutorial is designed to be beginner friendly, these are the prerequisites the reader is expected to possess:
- Have the latest version of Golang installed on your system
- Have basic knowledge of Golang
- Have a basic understanding of API architectures
- Basic knowledge of Gin Gonic will be beneficial
Run the Golang File Upload Project Locally
- If you don’t have Golang installed, visit https://go.dev/doc/install to download and install the binary executable on your machine.
- Download or clone the Upload Single and Multiple Files in Golang source code from https://github.com/wpcodevo/file_upload_golang
- Open the integrated terminal in your IDE and run
go mod tidy
to install all the necessary packages. - Start the Gin Gonic HTTP server with
go run main.go
or use the https://github.com/cosmtrek/air library if you need the hot-reloading feature. - Open an API testing software or visit
http://localhost:8000
to upload the files.
Step 1 – Setup the Golang Project
To get started, navigate to the location you would like to set up the project and create a folder named file_upload_golang
. Once the directory has been created, open it with an IDE or text editor like VS Code.
mkdir file_upload_golang && cd file_upload_golang && code .
Next, initialize the Golang project by running:
go mod init github.com/:github_username/name_of_project
Now that you’ve created the project module, install the Gin Gonic package with this command. Gin Gonic is an HTTP web framework written in Go.
go get -u github.com/gin-gonic/gin
Next, create a main.go
file and add the following code snippets:
main.go
package main
import (
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
router.GET("/healthchecker", func(ctx *gin.Context) {
ctx.JSON(200, gin.H{"status": "success", "message": "How to Upload Single and Multiple Files in Golang"})
})
router.Run(":8000")
}
The above code will create a Gin Gonic engine instance, add a health checker route to the middleware pipeline and start the HTTP server on port 8000. Start the Gin server by running go run main.go
.
Open a new tab in your browser and visit http://localhost:8000/healthchecker
to see the JSON object sent by the Golang API.
Congrats if you have made it this far. We will continue by creating the file upload components with HTML, CSS, and JavaScript. After that, we will use Gin Gonic to parse and render the HTML markup.
Step 2 – Create the File Upload Components
Below is a preview of the file upload components. The HTML markup will have three components:
- Image preview area
- Single file upload component
- Multiple file upload component
To spin up the file upload components, create a templates/index.html
file and add the following HTML markup.
templates/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<style>
.file-upload {
width: 100%;
display: flex;
align-items: flex-start;
justify-content: center;
}
.file-upload .file-upload__area {
width: 600px;
min-height: 200px;
display: flex;
flex-direction: column;
gap: 10px;
align-items: center;
justify-content: center;
border: 2px dashed #ccc;
margin-top: 40px;
}
.file-preview-container {
display: flex;
justify-content: center;
margin: 10px auto 20px;
}
.file-preview {
display: grid;
max-width: 1000px;
grid-template-columns: 1fr 1fr 1fr 1fr;
gap: 20px;
}
.file-preview .file-preview__el {
border: 2px dashed rgb(112, 102, 245);
padding: 5px;
max-height: 200px;
}
.file-preview .file-preview__el .file-preview__img {
width: 100%;
height: 100%;
object-fit: cover;
}
</style>
<title>File upload in Golang</title>
</head>
<body>
<div class="file-preview-container">
<div class="file-preview" id="file-preview"></div>
</div>
<div class="file-upload">
<div class="file-upload__area">
<h3>Single File Upload</h3>
<input type="file" name="" id="single_file_upload" max="1" />
</div>
</div>
<div class="file-upload">
<div class="file-upload__area">
<h3>Multiple File Upload</h3>
<input type="file" name="" id="mulitple_files_upload" multiple />
</div>
</div>
<script>
const SERVER_ENDPOINT = "http://localhost:8000";
const IMAGE_PREVIEW = document.getElementById("file-preview");
// Upload Single Image
document
.getElementById("single_file_upload")
.addEventListener("change", async (event) => {
try {
let formData = new FormData();
formData.append("image", event.target.files[0]);
const data = await fetch(`${SERVER_ENDPOINT}/upload/single`, {
body: formData,
method: "POST",
}).then((res) => res.json());
const imgHolder = document.createElement("div");
const imgElement = document.createElement("img");
imgHolder.classList.add("file-preview__el");
imgElement.classList.add("file-preview__img");
imgElement.src = data.filepath;
imgHolder.appendChild(imgElement);
IMAGE_PREVIEW.appendChild(imgHolder);
} catch (error) {
alert(error.message);
}
});
// Mulitple File Upload
document
.getElementById("mulitple_files_upload")
.addEventListener("change", async (event) => {
try {
let formData = new FormData();
for (let key in event.target.files) {
formData.append("images", event.target.files[key]);
}
const data = await fetch(`${SERVER_ENDPOINT}/upload/multiple`, {
body: formData,
method: "POST",
}).then((res) => res.json());
data.filepaths.forEach((filepath) => {
const imgHolder = document.createElement("div");
const imgElement = document.createElement("img");
imgHolder.classList.add("file-preview__el");
imgElement.classList.add("file-preview__img");
imgElement.src = filepath;
imgHolder.appendChild(imgElement);
IMAGE_PREVIEW.appendChild(imgHolder);
});
} catch (error) {
alert(error.message);
}
});
</script>
</body>
</html>
Quite a lot is happening in the above code, let’s break it down:
- First, we created a basic markup that contains the file upload components and some basic styling to improve the visual appearance of the components.
- Then, we wrote some logic to upload single and multiple files with JavaScript.
- Finally, we wrote some logic with JavaScript to dynamically display the images after the files have been saved to the disk.
Now that we have the HTML template, let’s write some code to render index.html
on the root route. Open main.go
file and replace its content with the code below.
main.go
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
router.LoadHTMLGlob("templates/*")
router.GET("/healthchecker", func(ctx *gin.Context) {
ctx.JSON(200, gin.H{"status": "success", "message": "How to Upload Single and Multiple Files in Golang"})
})
router.GET("/", func(ctx *gin.Context) {
ctx.HTML(http.StatusOK, "index.html", nil)
})
router.Run(":8000")
}
After that, start the Gin HTTP server with go run main.go
and open http://localhost:8000/
in a new tab to see the rendered HTML.
Let’s go ahead and write the code required for uploading and processing the files.
Step 3 – Upload a Single File in Golang
In this section, you will create a route function to retrieve the uploaded file and store it in the filesystem. Handling file upload in Golang can be a little challenging especially when you are a beginner. Luckily, Gin provides a simple way to handle uploaded files and store them on the disk.
To handle a single file, we will use Gin’s .FromFile()
method that’s available on the context object to retrieve the uploaded file from the multi-part content.
main.go
func uploadSingleFile(ctx *gin.Context) {
file, header, err := ctx.Request.FormFile("image")
if err != nil {
ctx.String(http.StatusBadRequest, fmt.Sprintf("file err : %s", err.Error()))
return
}
fileExt := filepath.Ext(header.Filename)
originalFileName := strings.TrimSuffix(filepath.Base(header.Filename), filepath.Ext(header.Filename))
now := time.Now()
filename := strings.ReplaceAll(strings.ToLower(originalFileName), " ", "-") + "-" + fmt.Sprintf("%v", now.Unix()) + fileExt
filePath := "http://localhost:8000/images/single/" + filename
out, err := os.Create("public/single/" + filename)
if err != nil {
log.Fatal(err)
}
defer out.Close()
_, err = io.Copy(out, file)
if err != nil {
log.Fatal(err)
}
ctx.JSON(http.StatusOK, gin.H{"filepath": filePath})
}
Let’s evaluate the above code. First, we attempt to retrieve the file with the name “image” from the multi-part content and check if the file is received successfully. To avoid conflicts with the file names, we appended a Unix time to the original file name, called the os.Create()
method to create the file in the public/single/
directory, and used an if statement to check if the file has been created.
After that, we called the io.Copy()
method to copy the uploaded file to the file system at the specified destination. If everything goes well, and the file was stored on the disk, we will return the file path to the client.
I didn’t use Gin’s
ctx.SaveUploadedFile(file, dst)
method because it had some limitations.
Step 4 – Resize a Single File in Golang
Now let’s handle the case where we want to process the uploaded file before storing it in the file system. Since we are dealing with images, we will use the https://github.com/disintegration/imaging package to process the uploaded image before storing it on the disk.
To begin, open your terminal and install the Imaging package.
go get -u github.com/disintegration/imaging
The Imaging package provides a bunch of image processing functions but we will only use the .Resize()
function to reduce the complexity of the project. Before we can resize the image, we first need to retrieve the file from the multi-part content with the .FormFile()
method, generate a Unix time, and append it to the filename.
After that, we will call the image.Decode()
method to decode the file from multipart.File
to image.Image
. If everything goes on well, we will resize the image to a specific aspect ratio by utilizing the imaging.Resize()
function.
Read the Imaging documentation for more details about the various resampling filters supported by the
imaging.Resize()
function.
Once the image has been resized and there is no error, we will call the imaging.Save()
method to save the image on the disk at the specified destination.
main.go
func uploadResizeSingleFile(ctx *gin.Context) {
file, header, err := ctx.Request.FormFile("image")
if err != nil {
ctx.String(http.StatusBadRequest, fmt.Sprintf("file err : %s", err.Error()))
return
}
fileExt := filepath.Ext(header.Filename)
originalFileName := strings.TrimSuffix(filepath.Base(header.Filename), filepath.Ext(header.Filename))
now := time.Now()
filename := strings.ReplaceAll(strings.ToLower(originalFileName), " ", "-") + "-" + fmt.Sprintf("%v", now.Unix()) + fileExt
filePath := "http://localhost:8000/images/single/" + filename
imageFile, _, err := image.Decode(file)
if err != nil {
log.Fatal(err)
}
src := imaging.Resize(imageFile, 1000, 0, imaging.Lanczos)
err = imaging.Save(src, fmt.Sprintf("public/single/%v", filename))
if err != nil {
log.Fatalf("failed to save image: %v", err)
}
ctx.JSON(http.StatusOK, gin.H{"filepath": filePath})
}
Step 5 – Upload Multiple Files in Golang
Now let’s handle the case where the client uploads multiple files to the server. To do that, we will create a route function that will extract the files from the multipart content, modify the origin filenames, and save the files to the filesystem. We can manually parse and iterate over each file but Gin provides a ctx.MultipartForm()
function that can parse the multipart form and the uploaded files.
main.go
func uploadMultipleFile(ctx *gin.Context) {
form, _ := ctx.MultipartForm()
files := form.File["images"]
filePaths := []string{}
for _, file := range files {
fileExt := filepath.Ext(file.Filename)
originalFileName := strings.TrimSuffix(filepath.Base(file.Filename), filepath.Ext(file.Filename))
now := time.Now()
filename := strings.ReplaceAll(strings.ToLower(originalFileName), " ", "-") + "-" + fmt.Sprintf("%v", now.Unix()) + fileExt
filePath := "http://localhost:8000/images/multiple/" + filename
filePaths = append(filePaths, filePath)
out, err := os.Create("./public/multiple/" + filename)
if err != nil {
log.Fatal(err)
}
defer out.Close()
readerFile, _ := file.Open()
_, err = io.Copy(out, readerFile)
if err != nil {
log.Fatal(err)
}
}
ctx.JSON(http.StatusOK, gin.H{"filepath": filePaths})
}
We parsed the uploaded files by calling Gin’s ctx.MultipartForm()
method and stored the result in a form variable. Then, we extracted the multipart content with the name images from form.File[]
, looped through the list of files, modified the filenames, and stored the files on the disk.
If everything goes well and the files were stored in the filesystem, a slice holding the file paths will be returned to the client.
Step 6- Resize Multiple Files in Golang
This route handler is similar to the uploadMultipleFile
function but this time we will resize the images before storing them in the filesystem. After looping through the multipart files, Imaging‘s image.Decode() method will be called to decode the images from multipart.File
to image.Image
.
If the images were decoded successfully, the imaging.Resize() method will be evoked to crop each image before the imaging.Save()
method will be called to save the file to the filesystem.
main.go
func uploadResizeMultipleFile(ctx *gin.Context) {
form, _ := ctx.MultipartForm()
files := form.File["images"]
filePaths := []string{}
for _, file := range files {
fileExt := filepath.Ext(file.Filename)
originalFileName := strings.TrimSuffix(filepath.Base(file.Filename), filepath.Ext(file.Filename))
now := time.Now()
filename := strings.ReplaceAll(strings.ToLower(originalFileName), " ", "-") + "-" + fmt.Sprintf("%v", now.Unix()) + fileExt
filePath := "http://localhost:8000/images/multiple/" + filename
filePaths = append(filePaths, filePath)
readerFile, _ := file.Open()
imageFile, _, err := image.Decode(readerFile)
if err != nil {
log.Fatal(err)
}
src := imaging.Resize(imageFile, 1000, 0, imaging.Lanczos)
err = imaging.Save(src, fmt.Sprintf("public/multiple/%v", filename))
if err != nil {
log.Fatalf("failed to save image: %v", err)
}
}
ctx.JSON(http.StatusOK, gin.H{"filepath": filePaths})
}
Step 7 – Register the Routes
Now let’s create routes to evoke the route handlers. The API will have four routes:
/
– Renders the HTML template on the root route/images
– A route for accessing the files stored in the filesystem/upload/single
– Upload a single file/upload/multiple
– Uploads multiple files
Because the files are to be stored on the disk, we need to create folders that represent the folder structure we passed to the os.Create()
and imaging.Save()
methods. To do that, we will utilize an init function to create the folders if they don’t exists before the main function gets called.
main.go
package main
import (
"errors"
"fmt"
"image"
"io"
"log"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/disintegration/imaging"
)
func uploadResizeSingleFile(ctx *gin.Context) {
file, header, err := ctx.Request.FormFile("image")
if err != nil {
ctx.String(http.StatusBadRequest, fmt.Sprintf("file err : %s", err.Error()))
return
}
fileExt := filepath.Ext(header.Filename)
originalFileName := strings.TrimSuffix(filepath.Base(header.Filename), filepath.Ext(header.Filename))
now := time.Now()
filename := strings.ReplaceAll(strings.ToLower(originalFileName), " ", "-") + "-" + fmt.Sprintf("%v", now.Unix()) + fileExt
filePath := "http://localhost:8000/images/single/" + filename
imageFile, _, err := image.Decode(file)
if err != nil {
log.Fatal(err)
}
src := imaging.Resize(imageFile, 1000, 0, imaging.Lanczos)
err = imaging.Save(src, fmt.Sprintf("public/single/%v", filename))
if err != nil {
log.Fatalf("failed to save image: %v", err)
}
ctx.JSON(http.StatusOK, gin.H{"filepath": filePath})
}
func uploadSingleFile(ctx *gin.Context) {
file, header, err := ctx.Request.FormFile("image")
if err != nil {
ctx.String(http.StatusBadRequest, fmt.Sprintf("file err : %s", err.Error()))
return
}
fileExt := filepath.Ext(header.Filename)
originalFileName := strings.TrimSuffix(filepath.Base(header.Filename), filepath.Ext(header.Filename))
now := time.Now()
filename := strings.ReplaceAll(strings.ToLower(originalFileName), " ", "-") + "-" + fmt.Sprintf("%v", now.Unix()) + fileExt
filePath := "http://localhost:8000/images/single/" + filename
out, err := os.Create("public/single/" + filename)
if err != nil {
log.Fatal(err)
}
defer out.Close()
_, err = io.Copy(out, file)
if err != nil {
log.Fatal(err)
}
ctx.JSON(http.StatusOK, gin.H{"filepath": filePath})
}
func uploadResizeMultipleFile(ctx *gin.Context) {
form, _ := ctx.MultipartForm()
files := form.File["images"]
filePaths := []string{}
for _, file := range files {
fileExt := filepath.Ext(file.Filename)
originalFileName := strings.TrimSuffix(filepath.Base(file.Filename), filepath.Ext(file.Filename))
now := time.Now()
filename := strings.ReplaceAll(strings.ToLower(originalFileName), " ", "-") + "-" + fmt.Sprintf("%v", now.Unix()) + fileExt
filePath := "http://localhost:8000/images/multiple/" + filename
filePaths = append(filePaths, filePath)
readerFile, _ := file.Open()
imageFile, _, err := image.Decode(readerFile)
if err != nil {
log.Fatal(err)
}
src := imaging.Resize(imageFile, 1000, 0, imaging.Lanczos)
err = imaging.Save(src, fmt.Sprintf("public/multiple/%v", filename))
if err != nil {
log.Fatalf("failed to save image: %v", err)
}
}
ctx.JSON(http.StatusOK, gin.H{"filepaths": filePaths})
}
func uploadMultipleFile(ctx *gin.Context) {
form, _ := ctx.MultipartForm()
files := form.File["images"]
filePaths := []string{}
for _, file := range files {
fileExt := filepath.Ext(file.Filename)
originalFileName := strings.TrimSuffix(filepath.Base(file.Filename), filepath.Ext(file.Filename))
now := time.Now()
filename := strings.ReplaceAll(strings.ToLower(originalFileName), " ", "-") + "-" + fmt.Sprintf("%v", now.Unix()) + fileExt
filePath := "http://localhost:8000/images/multiple/" + filename
filePaths = append(filePaths, filePath)
out, err := os.Create("./public/multiple/" + filename)
if err != nil {
log.Fatal(err)
}
defer out.Close()
readerFile, _ := file.Open()
_, err = io.Copy(out, readerFile)
if err != nil {
log.Fatal(err)
}
}
ctx.JSON(http.StatusOK, gin.H{"filepath": filePaths})
}
func init() {
if _, err := os.Stat("public/single"); errors.Is(err, os.ErrNotExist) {
err := os.MkdirAll("public/single", os.ModePerm)
if err != nil {
log.Println(err)
}
}
if _, err := os.Stat("public/multiple"); errors.Is(err, os.ErrNotExist) {
err := os.MkdirAll("public/multiple", os.ModePerm)
if err != nil {
log.Println(err)
}
}
}
func main() {
router := gin.Default()
router.LoadHTMLGlob("templates/*")
router.GET("/healthchecker", func(ctx *gin.Context) {
ctx.JSON(200, gin.H{"status": "success", "message": "How to Upload Single and Multiple Files in Golang"})
})
router.POST("/upload/single", uploadResizeSingleFile)
router.POST("/upload/multiple", uploadResizeMultipleFile)
router.StaticFS("/images", http.Dir("public"))
router.GET("/", func(ctx *gin.Context) {
ctx.HTML(http.StatusOK, "index.html", nil)
})
router.Run(":8000")
}
After implementing all the above code, start the Gin HTTP server by running go run main.go
.
Conclusion
Congrats! You have made it to the end, and I hope you’ve learned how to upload and process files in Golang. Take it up as a challenge, add more file-processing features, and perhaps do something even more awesome with the knowledge obtained from this tutorial.
You can find the complete code on GitHub.