Go download image
last modified October 30, 2026
In this article we show how to download an image file in Go. A download is an
HTTP GET request whose response body is copied to a file or decoded in memory.
The examples use the net/http, io and
image packages from the standard library.
All examples were compiled and run with Go 1.27.0. The image server runs on localhost, so the output does not depend on a public site.
The demo site
The examples download PNG files from a static file server on port 8099. Any
static server works; the simplest one is the http.server module of
Python, started in a directory that contains the images.
The files are small PNG images of 120 by 80 pixels. The server reports the size and the MIME type of each file, which the client can check before writing anything to disk.
$ ls icon.png logo.png photo.png $ python3 -m http.server 8099 Serving HTTP on 0.0.0.0 port 8099 ...
Downloading a file
http.Get sends the request and returns a response. The body is an
io.Reader, and io.Copy streams it into the file
without holding the whole image in memory.
package main
import (
"fmt"
"io"
"net/http"
"os"
)
func main() {
url := "http://localhost:8099/logo.png"
res, err := http.Get(url)
if err != nil {
fmt.Println(err)
return
}
defer res.Body.Close()
fmt.Println(res.StatusCode, res.Header.Get("Content-Type"), res.ContentLength)
f, err := os.Create("logo.png")
if err != nil {
fmt.Println(err)
return
}
defer f.Close()
n, err := io.Copy(f, res.Body)
if err != nil {
fmt.Println(err)
return
}
fmt.Println("bytes:", n)
}
The response reports status 200, the MIME type image/png and a
content length of 219 bytes. io.Copy returns the number of bytes
written, which matches the content length.
$ go run main.go 200 image/png 219 bytes: 219 $ file logo.png logo.png: PNG image data, 120 x 80, 8-bit/color RGB, non-interlaced
Failed responses
http.Get returns an error only when the request cannot be
completed. A 404 response is a successful HTTP exchange, so the error is
nil and the body contains an error page. The status code must be
checked explicitly.
package main
import (
"fmt"
"net/http"
)
func check(url string) {
res, err := http.Get(url)
if err != nil {
fmt.Println(err)
return
}
defer res.Body.Close()
fmt.Printf("%-40s err=%v status=%q\n", url, err, res.Status)
if res.StatusCode != http.StatusOK {
fmt.Println("unexpected status:", res.Status)
return
}
fmt.Println("ok")
}
func main() {
check("http://localhost:8099/logo.png")
check("http://localhost:8099/missing.png")
}
The missing file produces 404 File not found, the reason phrase
used by the Python server. Without the guard, the error page would be saved
under the name of the image.
$ go run main.go http://localhost:8099/logo.png err=<nil> status="200 OK" ok http://localhost:8099/missing.png err=<nil> status="404 File not found" unexpected status: 404 File not found
Choosing the file name
The last element of the URL path is a reasonable default name.
url.Parse separates the path from the query string, and
path.Base returns the final element.
package main
import (
"fmt"
"net/url"
"path"
)
func filename(raw string) (string, error) {
u, err := url.Parse(raw)
if err != nil {
return "", err
}
name := path.Base(u.Path)
if name == "." || name == "/" || name == "" {
return "", fmt.Errorf("no file name in %q", raw)
}
return name, nil
}
func main() {
for _, raw := range []string{
"http://localhost:8099/logo.png?size=large",
"http://localhost:8099/images/photo.png",
"http://localhost:8099/",
} {
name, err := filename(raw)
fmt.Printf("%-46s %q %v\n", raw, name, err)
}
}
The query string is dropped, so ?size=large does not become part
of the file name. A URL that ends with a slash has no file name and is
rejected. A name taken from a remote server should also be cleaned with
filepath.Base before it is joined to a local directory, so that a
path such as ../../etc/passwd cannot escape.
$ go run main.go http://localhost:8099/logo.png?size=large "logo.png" <nil> http://localhost:8099/images/photo.png "photo.png" <nil> http://localhost:8099/ "" no file name in "http://localhost:8099/"
Timeouts
http.Get uses the default client, which has no timeout. A
production download should use an http.Client with an explicit
timeout, so a stalled server cannot block the program forever.
package main
import (
"fmt"
"net/http"
"time"
)
func main() {
client := &http.Client{Timeout: time.Nanosecond}
res, err := client.Get("http://localhost:8099/logo.png")
if err != nil {
fmt.Println(err)
return
}
defer res.Body.Close()
fmt.Println(res.Status)
}
The timeout is deliberately tiny to force the failure. Real code uses a few
seconds, for example 10 * time.Second. The error wraps
context.DeadlineExceeded and can be tested with
errors.Is.
$ go run main.go Get "http://localhost:8099/logo.png": context deadline exceeded (Client.Timeout exceeded while awaiting headers)
Limiting the response size
A server can send an arbitrarily large body. Reading one byte more than the
allowed maximum shows whether the limit was exceeded, and
io.LimitReader stops the transfer at that point.
package main
import (
"fmt"
"io"
"net/http"
)
func download(url string, limit int64) ([]byte, error) {
res, err := http.Get(url)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected status: %s", res.Status)
}
data, err := io.ReadAll(io.LimitReader(res.Body, limit+1))
if err != nil {
return nil, err
}
if int64(len(data)) > limit {
return nil, fmt.Errorf("response is larger than %d bytes", limit)
}
return data, nil
}
func main() {
data, err := download("http://localhost:8099/logo.png", 1000)
fmt.Println(len(data), err)
_, err = download("http://localhost:8099/logo.png", 100)
fmt.Println(err)
}
The first call reads the whole 219-byte image. The second one hits the limit and is rejected, even though only the extra byte was read. This pattern is useful when the image is decoded in memory.
$ go run main.go 219 <nil> response is larger than 100 bytes
Decoding the image in memory
Sometimes the image is needed as an image.Image rather than as a
file. image.Decode reads the format from the content, not from the
file extension, and returns the format name and the dimensions.
package main
import (
"bytes"
"fmt"
"image"
_ "image/png"
"io"
"net/http"
)
func main() {
res, err := http.Get("http://localhost:8099/logo.png")
if err != nil {
fmt.Println(err)
return
}
defer res.Body.Close()
data, err := io.ReadAll(res.Body)
if err != nil {
fmt.Println(err)
return
}
img, format, err := image.Decode(bytes.NewReader(data))
if err != nil {
fmt.Println(err)
return
}
fmt.Println("format:", format)
fmt.Println("bounds:", img.Bounds())
fmt.Println("size: ", len(data), "bytes")
}
The blank import registers the PNG decoder. Without it,
image.Decode fails with image: unknown format; the
same error appears when an error page is decoded instead of an image.
$ go run main.go format: png bounds: (0,0)-(120,80) size: 219 bytes
Downloading several images
Each download can run in its own goroutine. A sync.WaitGroup
waits for all of them, and a slice indexed by the position of the URL keeps
the output in a fixed order.
package main
import (
"fmt"
"io"
"net/http"
"os"
"path"
"sync"
)
func download(url string) (int64, error) {
res, err := http.Get(url)
if err != nil {
return 0, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return 0, fmt.Errorf("%s: %s", url, res.Status)
}
f, err := os.Create(path.Base(res.Request.URL.Path))
if err != nil {
return 0, err
}
defer f.Close()
return io.Copy(f, res.Body)
}
func main() {
urls := []string{
"http://localhost:8099/logo.png",
"http://localhost:8099/icon.png",
"http://localhost:8099/photo.png",
}
var wg sync.WaitGroup
results := make([]string, len(urls))
for i, url := range urls {
wg.Add(1)
go func() {
defer wg.Done()
n, err := download(url)
if err != nil {
results[i] = fmt.Sprintf("%-10s error: %v", path.Base(url), err)
return
}
results[i] = fmt.Sprintf("%-10s %d bytes", path.Base(url), n)
}()
}
wg.Wait()
for _, r := range results {
fmt.Println(r)
}
}
The closure reads i and url directly. Since Go 1.22
each iteration has its own variables, so the goroutines see the values of their
own iteration. Before Go 1.22 the loop variables were shared and the arguments
had to be passed to the function.
$ go run main.go logo.png 219 bytes icon.png 219 bytes photo.png 219 bytes
Serving an image from Go
The other side of the transfer is a small HTTP server.
http.ServeFile sends the file and sets the content type, the
content length and the Accept-Ranges header for partial requests.
package main
import (
"log"
"net/http"
)
func main() {
http.HandleFunc("/image", func(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "logo.png")
})
log.Println("listening on http://localhost:8090")
log.Fatal(http.ListenAndServe(":8090", nil))
}
The server needs logo.png in its working directory. A HEAD
request shows the headers without transferring the body. The client from the
first section works unchanged with the new URL.
$ go run main.go & 2026/10/30 20:03:42 listening on http://localhost:8090 $ curl -I http://localhost:8090/image HTTP/1.1 200 OK Accept-Ranges: bytes Content-Length: 219 Content-Type: image/png $ sed -i 's|localhost:8099/logo.png|localhost:8090/image|' main.go $ go run main.go 200 image/png 219 bytes: 219
Downloading with Req
The third-party req client wraps the same steps into one call.
SetOutputFile writes the body directly to the named file.
package main
import (
"fmt"
"os"
"time"
"github.com/imroc/req/v3"
)
func main() {
client := req.C().SetTimeout(10 * time.Second)
res, err := client.R().SetOutputFile("logo.png").Get("http://localhost:8099/logo.png")
if err != nil {
fmt.Println(err)
return
}
info, err := os.Stat("logo.png")
if err != nil {
fmt.Println(err)
return
}
fmt.Println(res.Status, info.Size())
}
The example uses req v3.61.0. The response status and the size of the written file confirm the download; error handling is still required, and the file is created by the library.
$ go run main.go 200 OK 219
Source
Go net/http package - reference
Go image package - reference
Req HTTP client - Github page
In this article we have downloaded images in Go. We streamed a response into a file, checked the status code, derived the file name from the URL, set a client timeout, limited the response size, decoded the image in memory, downloaded several files concurrently, served an image from a Go server, and used the Req client for a shorter version of the same task.
Author
List all Go tutorials.