Getting Started on Heroku with Go
Introduction
Complete this tutorial to deploy a sample Go app to Cedar, the legacy generation of the Heroku platform. To deploy the app to the Fir generation, only available to Heroku Private Spaces, follow this guide instead.
The tutorial assumes that you have:
- A verified Heroku Account
- An Eco dynos plan subscription (recommended)
- Go 1.27 or better installed locally (Installation instructions available here)
- Postgres installed locally
Using dynos and databases to complete this tutorial counts towards your usage. We recommend using our low-cost plans to complete this tutorial. Eligible students can apply for platform credits through our new Heroku for GitHub Students program.
Set Up
Install the Heroku Command Line Interface (CLI). Use the CLI to manage and scale your app, provision add-ons, view your logs, and run your app locally.
The Heroku CLI requires Git, the popular version control system. If you don’t already have Git installed, complete the following before proceeding:
Download and run the installer for your platform:
Download the appropriate installer for your Windows installation:
You can find more installation options for the Heroku CLI here.
After installation, you can use the heroku command from your command shell.
To log in to the Heroku CLI, use the heroku login command:
$ heroku login
heroku: Press any key to open up the browser to login or q to exit:
Opening browser to https://cli-auth.heroku.com/auth/cli/browser/***
heroku: Waiting for login...
Logging in... done
Logged in as me@example.com
This command opens your web browser to the Heroku login page. If your browser is already logged in to Heroku, click the Log In button on the page.
This authentication is required for the heroku and git commands to work correctly.
If you have any problems installing or using the Heroku CLI, see the main Heroku CLI article for advice and troubleshooting steps.
If you’re behind a firewall that uses a proxy to connect with external HTTP/HTTPS services, set the HTTP_PROXY or HTTPS_PROXY environment variables in your local development environment before running the heroku command.
Clone the Sample App
If you’re new to Heroku, it’s recommended that you complete this tutorial using the Heroku-provided sample application.
To deploy an existing application, follow this article instead.
Clone the sample application to get a local version of the code. Execute these commands in your local command shell or terminal:
$ git clone https://github.com/heroku/go-getting-started.git
$ cd go-getting-started
You now have a functioning Git repository that contains a simple application. It includes a go.mod file, which is used by Go and Go’s module dependency system.
Create Your App
Using a dyno and a database to complete this tutorial counts towards your usage. Delete your app, and database as soon as you’re done to control costs.
By default, apps use Eco dynos if you’re subscribed to Eco. Otherwise, it defaults to Basic dynos. The Eco dynos plan is shared across all Eco dynos in your account. It’s recommended if you plan on deploying many small apps to Heroku. Learn more here. Eligible students can apply for platform credits through our Heroku for GitHub Students program.
Create an app on Heroku to prepare the platform to receive your source code:
$ heroku create --stack heroku-26
Creating app... done, evening-lowlands-26749, stack is heroku-26
https://evening-lowlands-26749-ffbc0e97091e.herokuapp.com/ | https://git.heroku.com/evening-lowlands-26749.git
When you create an app, a Git remote named heroku is also created and added to your local repository configuration. Git remotes are versions of your repository that live on other servers. You can deploy your app by pushing code to that special Heroku-hosted remote associated with your app.
Heroku generates a random name for your app, in this case, evening-lowlands-26749. You can specify your own app name.
Deploy the App
Deploy your code. This command pushes the main branch of the sample repo to your heroku remote, which then deploys to Heroku:
$ git push heroku main
remote: Updated 19 paths from 9789aa2
remote: Compressing source files... done.
remote: Building source:
remote:
remote: -----> Building on the Heroku-26 stack
remote: -----> Determining which buildpack to use for this app
remote: -----> Go app detected
remote: -----> Detected go modules via go.mod
remote: -----> Detected Module Name: github.com/heroku/go-getting-started
remote: -----> New Go Version, clearing old cache
remote: -----> Installing go1.27.2
remote: -----> Fetching go1.27.2.linux-amd64.tar.gz
remote: -----> Determining packages to install
remote: go: downloading github.com/gin-gonic/gin v1.12.0
remote: go: downloading github.com/heroku/x v0.6.2
remote: go: downloading github.com/gin-contrib/sse v1.1.2
remote: go: downloading github.com/mattn/go-isatty v0.0.24
remote: go: downloading github.com/quic-go/quic-go v0.63.0
remote: go: downloading golang.org/x/net v0.59.0
remote: go: downloading github.com/go-playground/validator/v10 v10.30.5
remote: go: downloading github.com/goccy/go-yaml v1.19.2
remote: go: downloading github.com/pelletier/go-toml/v2 v2.4.3
remote: go: downloading github.com/ugorji/go/codec v1.3.2
remote: go: downloading go.mongodb.org/mongo-driver/v2 v2.9.1
remote: go: downloading google.golang.org/protobuf v1.36.12
remote: go: downloading golang.org/x/sys v0.48.0
remote: go: downloading github.com/quic-go/qpack v0.6.0
remote: go: downloading github.com/gabriel-vasile/mimetype v1.4.15
remote: go: downloading github.com/go-playground/universal-translator v0.18.2
remote: go: downloading github.com/leodido/go-urn v1.5.0
remote: go: downloading golang.org/x/crypto v0.57.0
remote: go: downloading golang.org/x/text v0.42.0
remote: go: downloading github.com/go-playground/locales v0.14.2
remote: -----> Detected the following main packages to install:
remote: github.com/heroku/go-getting-started
remote: -----> Running: go install -v -tags heroku github.com/heroku/go-getting-started
remote: internal/goarch
remote: internal/unsafeheader
remote: internal/byteorder
remote: internal/cpu
remote: internal/coverage/rtcov
remote: internal/abi
remote: internal/godebugs
remote: internal/bytealg
remote: internal/chacha8rand
remote: internal/goexperiment
remote: internal/goos
remote: internal/profilerecord
remote: internal/runtime/atomic
remote: internal/runtime/syscall/linux
...
remote: github.com/quic-go/quic-go/internal/utils/linkedlist
remote: golang.org/x/crypto/chacha20poly1305
remote: golang.org/x/net/bpf
remote: github.com/quic-go/quic-go/qlog
remote: golang.org/x/net/internal/socket
remote: golang.org/x/text/transform
remote: golang.org/x/text/unicode/bidi
remote: github.com/quic-go/quic-go/internal/congestion
remote: github.com/quic-go/quic-go/internal/handshake
remote: github.com/quic-go/quic-go/internal/ackhandler
remote: golang.org/x/net/ipv4
remote: golang.org/x/net/ipv6
remote: golang.org/x/text/secure/bidirule
remote: golang.org/x/text/unicode/norm
remote: log/slog/internal/buffer
remote: github.com/quic-go/quic-go
remote: log/slog
remote: golang.org/x/net/internal/httpsfv
remote: golang.org/x/net/idna
remote: golang.org/x/net/http/httpguts
remote: golang.org/x/net/internal/httpcommon
remote: github.com/gin-gonic/gin/internal/fs
remote: net/rpc
remote: github.com/gin-contrib/sse
remote: golang.org/x/net/http2
remote: net/http/httputil
remote: github.com/ugorji/go/codec
remote: github.com/heroku/x/hmetrics
remote: github.com/heroku/x/hmetrics/onload
remote: github.com/quic-go/quic-go/http3/qlog
remote: golang.org/x/net/http2/h2c
remote: github.com/quic-go/quic-go/http3
remote: github.com/gin-gonic/gin/render
remote: github.com/gin-gonic/gin/binding
remote: github.com/gin-gonic/gin
remote: github.com/heroku/go-getting-started
remote: -----> Installed the following binaries:
remote: ./bin/go-getting-started
remote: -----> Discovering process types
remote: Procfile declares types -> web
remote:
remote: -----> Compressing...
remote: Done: 15.9M
remote: -----> Launching...
remote: Released v3
remote: https://evening-lowlands-26749-ffbc0e97091e.herokuapp.com/ deployed to Heroku
remote:
remote: Verifying deploy... done.
To https://git.heroku.com/evening-lowlands-26749.git
* [new branch] main -> main
Visit the app at the URL shown in the logs. As a shortcut, you can also open the website as follows:
$ heroku open
Understanding the Procfile
Use a Procfile, a text file in the root directory of your application, to explicitly declare what command to execute to start your app.
The Procfile in the example app looks like this:
web: go-getting-started
This Procfile declares a single process type, web, and the command needed to run it. The name web is important here. It declares that this process type is attached to Heroku’s HTTP routing stack and receives web traffic when deployed. The command used here, go-getting-started is the compiled binary of the getting started app. The heroku build process makes this compiled binary available on the $PATH.
A Procfile can contain additional process types. For example, you can declare a background worker process that processes items off a queue.
View Logs
Heroku treats logs as streams of time-ordered events, aggregated from the output streams of all your app and Heroku components. Heroku provides a single stream for all events.
View information about your running app by using one of the logging commands, heroku logs --tail:
$ heroku logs --tail
2026-10-16T12:34:07.957075+00:00 heroku[web.1]: Starting process with command `go-getting-started`
2026-10-16T12:34:08.934803+00:00 app[web.1]: [GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
2026-10-16T12:34:08.934841+00:00 app[web.1]: - using env: export GIN_MODE=release
2026-10-16T12:34:08.934841+00:00 app[web.1]: - using code: gin.SetMode(gin.ReleaseMode)
2026-10-16T12:34:08.934842+00:00 app[web.1]:
2026-10-16T12:34:08.934927+00:00 app[web.1]: [GIN-debug] Loaded HTML Templates (4):
2026-10-16T12:34:08.934927+00:00 app[web.1]: -
2026-10-16T12:34:08.934928+00:00 app[web.1]: - header.tmpl.html
2026-10-16T12:34:08.934928+00:00 app[web.1]: - index.tmpl.html
2026-10-16T12:34:08.934928+00:00 app[web.1]: - nav.tmpl.html
2026-10-16T12:34:08.934929+00:00 app[web.1]:
2026-10-16T12:34:08.934942+00:00 app[web.1]: [GIN-debug] GET /static/*filepath --> github.com/gin-gonic/gin.(*RouterGroup).createStaticHandler.func1 (2 handlers)
2026-10-16T12:34:08.934943+00:00 app[web.1]: [GIN-debug] HEAD /static/*filepath --> github.com/gin-gonic/gin.(*RouterGroup).createStaticHandler.func1 (2 handlers)
2026-10-16T12:34:08.934944+00:00 app[web.1]: [GIN-debug] GET / --> main.main.func1 (2 handlers)
2026-10-16T12:34:08.935023+00:00 app[web.1]: [GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.
2026-10-16T12:34:08.935023+00:00 app[web.1]: Please check https://github.com/gin-gonic/gin/blob/master/docs/doc.md#dont-trust-all-proxies for details.
2026-10-16T12:34:08.935024+00:00 app[web.1]: [GIN-debug] Listening and serving HTTP on :49723
2026-10-16T12:34:09.284920+00:00 heroku[web.1]: State changed from starting to up
2026-10-16T12:34:31.873613+00:00 app[web.1]: [GIN] 2026/10/16 - 12:34:31 | 200 | 434.818µs | 20.29.157.48 | GET "/"
2026-10-16T12:34:31.874029+00:00 heroku[router]: at=info method=GET path="/" host=evening-lowlands-26749-ffbc0e97091e.herokuapp.com request_id=ef45e1ba-018d-2946-65e0-dae2db60d673 fwd="123.456.789.0" dyno=web.1 connect=1ms service=1ms status=200 bytes=8826 protocol=http2.0 tls=true tls_version=tls1.3
To generate more log messages, refresh the app in your browser.
To stop streaming the logs, press Control+C.
Scale the App
After deploying the sample app, it automatically runs on a single web dyno. Think of a dyno as a lightweight container that runs the command specified in the Procfile.
You can check how many dynos are running by using the ps command:
$ heroku ps
=== web (Basic): go-getting-started (1)
web.1: up 2026/10/16 12:34:09 +0000 (~ 29s ago)
Scaling an app on Heroku is equivalent to changing the number of running dynos. Scale the number of web dynos to zero:
$ heroku ps:scale web=0
$ heroku ps:wait
Access the app again by hitting refresh in your browser, or heroku open to open it in a web tab. You get an error message because you no longer have web dynos available to serve requests.
Scale it up again:
$ heroku ps:scale web=1
$ heroku ps:wait
By default, apps use Eco dynos if you’re subscribed to Eco. Otherwise, it defaults to Basic dynos. The Eco dynos plan is shared across all Eco dynos in your account and is recommended if you plan on deploying many small apps to Heroku. Eco dynos sleep if they don’t receive any traffic for half an hour. This sleep behavior causes a few seconds delay for the first request upon waking. Eco dynos consume from a monthly, account-level quota of eco dyno hours. As long as you haven’t exhausted the quota, your apps can continue to run.
To avoid dyno sleeping, upgrade to a Basic or higher dyno type as described in the Dyno Types article. Upgrading to at least Standard dynos allows you to scale up to multiple dynos per process type.
Start a Console
You can run a command, typically scripts and applications that are part of your app, in a one-off dyno using the heroku run command. You can also run an interactive bash session in your app’s environment:
$ heroku run bash
Running bash on evening-lowlands-26749... starting, run.6250
Running bash on evening-lowlands-26749... connecting, run.6250
Running bash on evening-lowlands-26749... up, run.6250
~ $ ls -A
.env .gitignore CODE_OF_CONDUCT.md README.md bin main.go
.gitattributes .heroku LICENSE SECURITY.md go.mod static
.github .profile.d Procfile app.json go.sum templates
~ $
~ $ exit
exit
If you receive an error, Error connecting to process, configure your firewall.
Type exit to exit the shell.
Declare App Dependencies
Heroku recognizes an app as being written in Go by the existence of a go.mod file in the root directory. The demo app you deployed already has a go.mod file, and it looks something like this:
module github.com/heroku/go-getting-started
go 1.27
require (
github.com/gin-gonic/gin v1.12.0
github.com/heroku/x v0.6.2
)
require (
github.com/bytedance/gopkg v0.1.4 // indirect
github.com/bytedance/sonic v1.15.4 // indirect
github.com/bytedance/sonic/loader v0.5.2 // indirect
github.com/cloudwego/base64x v0.1.7 // indirect
github.com/gabriel-vasile/mimetype v1.4.15 // indirect
github.com/gin-contrib/sse v1.1.2 // indirect
github.com/go-playground/locales v0.14.2 // indirect
github.com/go-playground/universal-translator v0.18.2 // indirect
github.com/go-playground/validator/v10 v10.30.5 // indirect
github.com/goccy/go-json v0.11.1 // indirect
github.com/goccy/go-yaml v1.19.2 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.4.0 // indirect
github.com/leodido/go-urn v1.5.0 // indirect
github.com/mattn/go-isatty v0.0.24 // 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.4.3 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.63.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.2 // indirect
go.mongodb.org/mongo-driver/v2 v2.9.1 // indirect
golang.org/x/arch v0.31.0 // indirect
golang.org/x/crypto v0.57.0 // indirect
golang.org/x/net v0.59.0 // indirect
golang.org/x/sys v0.48.0 // indirect
golang.org/x/text v0.42.0 // indirect
google.golang.org/protobuf v1.36.12 // indirect
)
The go.mod file is used by Go tool and specifies both the dependencies that are required to build your application and the build configuration Heroku should use to compile the application. This Go app has a few dependencies, primarily on Gin, a HTTP web framework.
When an app is deployed, Heroku reads this file, installs an appropriate Go version and compiles your code using go install ..
Run the app locally
Running apps locally in your own dev environment requires a little more effort. Go is a compiled language and you must compile the application and ensure it available on your $PATH.
First, ensure you have $GOPATH/bin on your path:
$ export GOPATH="$HOME/go"
$ export PATH="$GOPATH/bin:$PATH"
These environment variables are commonly used by Go developers. They may be something you want to persist to your environment by
including them your .zshrc, .bash_profile.
Then compile and install the program to your $GOPATH/bin directory.
$ go install .
go: downloading go1.27.0 (linux/amd64)
go: downloading github.com/gin-gonic/gin v1.12.0
go: downloading github.com/heroku/x v0.6.2
go: downloading github.com/gin-contrib/sse v1.1.2
go: downloading github.com/mattn/go-isatty v0.0.24
go: downloading github.com/quic-go/quic-go v0.63.0
go: downloading golang.org/x/net v0.59.0
go: downloading github.com/go-playground/validator/v10 v10.30.5
go: downloading github.com/goccy/go-yaml v1.19.2
go: downloading github.com/pelletier/go-toml/v2 v2.4.3
go: downloading github.com/ugorji/go/codec v1.3.2
go: downloading go.mongodb.org/mongo-driver/v2 v2.9.1
go: downloading google.golang.org/protobuf v1.36.12
go: downloading golang.org/x/sys v0.48.0
go: downloading github.com/gabriel-vasile/mimetype v1.4.15
go: downloading github.com/go-playground/universal-translator v0.18.2
go: downloading github.com/leodido/go-urn v1.5.0
go: downloading golang.org/x/crypto v0.57.0
go: downloading golang.org/x/text v0.42.0
go: downloading github.com/quic-go/qpack v0.6.0
go: downloading github.com/go-playground/locales v0.14.2
Now start your application locally using the heroku local command, which was installed as part of the Heroku CLI:
$ heroku local web --port 5006
[OKAY] Loaded ENV .env File as KEY=VALUE Format
12:38:22 PM web.1 | [GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
12:38:22 PM web.1 | - using env: export GIN_MODE=release
12:38:22 PM web.1 | - using code: gin.SetMode(gin.ReleaseMode)
12:38:22 PM web.1 | [GIN-debug] Loaded HTML Templates (4):
12:38:22 PM web.1 | - nav.tmpl.html
12:38:22 PM web.1 | -
12:38:22 PM web.1 | - header.tmpl.html
12:38:22 PM web.1 | - index.tmpl.html
12:38:22 PM web.1 | [GIN-debug] GET /static/*filepath --> github.com/gin-gonic/gin.(*RouterGroup).createStaticHandler.func1 (2 handlers)
12:38:22 PM web.1 | [GIN-debug] HEAD /static/*filepath --> github.com/gin-gonic/gin.(*RouterGroup).createStaticHandler.func1 (2 handlers)
12:38:22 PM web.1 | [GIN-debug] GET / --> main.main.func1 (2 handlers)
12:38:22 PM web.1 | [GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.
12:38:22 PM web.1 | Please check https://github.com/gin-gonic/gin/blob/master/docs/doc.md#dont-trust-all-proxies for details.
12:38:22 PM web.1 | [GIN-debug] Listening and serving HTTP on :5006
On Windows you will need to do two things before being able to run `heroku local`:
- Run `go build -o bin/go-getting-started.exe -v` instead of the command listed above.
- Alter Procfile so it's contents are: `web: bin\go-getting-started.exe` instead of what is in the checkout. Don't commit changes to Procfile though, otherwise your application's web process won't be able to start on Heroku.
Just like Heroku, heroku local examines the Procfile to determine what to run.
Open http://localhost:5006 with your web browser. You should see your app running locally.
To stop the app from running locally, go back to your terminal window and press Ctrl+C to exit.
Provision a Database
The sample app uses a database. Provision a Heroku Postgres database, an add-on available through the Elements Marketplace. Add-ons are cloud services that provide out-of-the-box additional services for your application, such as logging, monitoring, databases, and more.
An essential-0 Postgres size costs $5 a month, prorated to the minute. At the end of this tutorial, we prompt you to delete your app and its database to minimize costs.
$ heroku addons:create heroku-postgresql:essential-0
Creating heroku-postgresql:essential-0 on evening-lowlands-26749... ~$0.007/hour (max $5/month)
Database should be available soon
postgresql-trapezoidal-80403 is being created in the background. The app will restart when complete...
Run heroku addons:info postgresql-trapezoidal-80403 to check creation progress.
Run heroku addons:docs heroku-postgresql to view documentation.
You can wait for the database to provision by running this command:
$ heroku pg:wait
After that command exits, your Heroku app can access the Postgres database. The DATABASE_URL environment variable stores your credentials, which your app is configured to connect to. You can see all the add-ons provisioned with the addons command:
$ heroku addons
Add-on Plan Price Max Price State
-------------------------------------------------------------------------------------------------
heroku-postgresql (postgresql-trapezoidal-80403) essential-0 ~$0.007/hour $5/month created
└─ as DATABASE
The table above shows add-ons and the attachments to the current app (evening-lowlands-26749) or other apps.
Push Local Changes
In this step you’ll learn how to propagate a local change to the application through to Heroku. As an example, you’ll modify the application to add an additional dependency and the code to use it.
Dependencies are managed with the Go tool.
Let’s add a route to the application that will use the database we just created.
We’ll need the pgx library to interact with
the database. Since this dependency is not already used by your application we
need to tell go to fetch a copy of the dependency:
$ go get github.com/jackc/pgx/v5
go: downloading github.com/jackc/pgx/v5 v5.11.0
go: downloading github.com/jackc/pgx v3.6.2+incompatible
go: downloading github.com/jackc/pgpassfile v1.0.0
go: downloading github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761
go: added github.com/jackc/pgpassfile v1.0.0
go: added github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761
go: added github.com/jackc/pgx/v5 v5.11.0
This does 3 things:
- Downloads
pgxand any of its dependencies to the module cache. - Records the pgx dependency, and its dependencies in
go.mod. - Records a cryptographic sum of pgx and its dependencies in
go.sum
Next, let’s introduce a new route, /db, which will track timestamps of
requests to that endpoint. Modify main.go so that it uses pgx by adding
"github.com/jackc/pgx/v5/stdlib" to the list of imports.
In file main.go, on line 8 add:
_ "github.com/jackc/pgx/v5/stdlib"
Sync your module files with the code’s imports, so that go.mod and go.sum list the exact dependencies your application uses:
$ go mod tidy
go: downloading github.com/stretchr/testify v1.12.1
go: downloading github.com/json-iterator/go v1.1.12
go: downloading github.com/modern-go/reflect2 v1.0.2
go: downloading github.com/bytedance/sonic v1.15.4
go: downloading github.com/goccy/go-json v0.11.1
go: downloading github.com/quic-go/go-ossfuzz-seeds v0.1.0
go: downloading go.uber.org/mock v0.6.0
go: downloading github.com/google/go-cmp v0.7.0
go: downloading golang.org/x/sync v0.23.0
go: downloading github.com/jackc/puddle/v2 v2.2.2
go: downloading github.com/go-playground/assert/v2 v2.2.0
go: downloading github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd
go: downloading github.com/davecgh/go-spew v1.1.1
go: downloading go.yaml.in/yaml/v3 v3.0.5
go: downloading golang.org/x/arch v0.31.0
go: downloading github.com/bytedance/gopkg v0.1.4
go: downloading github.com/cloudwego/base64x v0.1.7
go: downloading github.com/bytedance/sonic/loader v0.5.2
go: downloading github.com/klauspost/cpuid/v2 v2.4.0
go: downloading github.com/twitchyliquid64/golang-asm v0.15.1
Now let’s open a connection pool to the database defined by the
$DATABASE_URL environment variable. sql.Open doesn’t open any connections
itself, so we call db.PingContext to confirm the database is reachable before
the app starts serving requests.
In file main.go, on line 19 add:
db, err := sql.Open("pgx", os.Getenv("DATABASE_URL"))
if err != nil {
log.Fatalf("Error opening database: %q", err)
}
if err := db.PingContext(context.Background()); err != nil {
log.Fatalf("Error connecting to database: %q", err)
}
db.SetMaxOpenConns(5)
In this code, SetMaxOpenConns caps the number of connections each dyno will create. Heroku’s PostgreSQL plans have limits on the number of total connections. You can calculate the number of connections by multiplying your dyno count by your maximum open connection value. When your dyno count would push the total past the plan limit, move to a larger plan or route connections through Heroku Postgres connection pooling, which lets many dynos share a smaller set of database connections.
Now add a dbFunc to the app.
At the end of main.go add:
func dbFunc(db *sql.DB) gin.HandlerFunc {
return func(c *gin.Context) {
if _, err := db.ExecContext(c.Request.Context(), "CREATE TABLE IF NOT EXISTS ticks (tick timestamp)"); err != nil {
c.String(http.StatusInternalServerError,
fmt.Sprintf("Error creating database table: %q", err))
return
}
if _, err := db.ExecContext(c.Request.Context(), "INSERT INTO ticks VALUES (now())"); err != nil {
c.String(http.StatusInternalServerError,
fmt.Sprintf("Error incrementing tick: %q", err))
return
}
rows, err := db.QueryContext(c.Request.Context(), "SELECT tick FROM ticks")
if err != nil {
c.String(http.StatusInternalServerError,
fmt.Sprintf("Error reading ticks: %q", err))
return
}
defer rows.Close()
var b strings.Builder
for rows.Next() {
var tick time.Time
if err := rows.Scan(&tick); err != nil {
c.String(http.StatusInternalServerError,
fmt.Sprintf("Error scanning ticks: %q", err))
return
}
fmt.Fprintf(&b, "Read from DB: %s\n", tick.String())
}
if err := rows.Err(); err != nil {
c.String(http.StatusInternalServerError,
fmt.Sprintf("Error iterating ticks: %q", err))
return
}
c.String(http.StatusOK, b.String())
}
}
Then register the new function with the router.
In file main.go, on line 31 add:
router.GET("/db", dbFunc(db))
The additional code requires new packages from Go’s standard library.
You can import them automatically with goimports. Install goimports if you
haven’t already with:
$ go install golang.org/x/tools/cmd/goimports@latest
go: downloading golang.org/x/tools v0.52.0
go: downloading golang.org/x/telemetry v0.0.0-20261009151841-97e8ea77aa2a
go: downloading golang.org/x/mod v0.42.0
go: downloading golang.org/x/sync v0.24.0
Then add the new imports to main.go:
$ goimports -w main.go
main.go should now look like this:
package main
import (
"context"
"database/sql"
"fmt"
"log"
"net/http"
"os"
"strings"
"time"
"github.com/gin-gonic/gin"
_ "github.com/heroku/x/hmetrics/onload"
_ "github.com/jackc/pgx/v5/stdlib"
)
func main() {
port := os.Getenv("PORT")
if port == "" {
log.Fatal("$PORT must be set")
}
db, err := sql.Open("pgx", os.Getenv("DATABASE_URL"))
if err != nil {
log.Fatalf("Error opening database: %q", err)
}
if err := db.PingContext(context.Background()); err != nil {
log.Fatalf("Error connecting to database: %q", err)
}
db.SetMaxOpenConns(5)
router := gin.New()
router.Use(gin.Logger())
router.GET("/db", dbFunc(db))
router.LoadHTMLGlob("templates/*.tmpl.html")
router.Static("/static", "static")
router.GET("/", func(c *gin.Context) {
c.HTML(http.StatusOK, "index.tmpl.html", nil)
})
router.Run(":" + port)
}
func dbFunc(db *sql.DB) gin.HandlerFunc {
return func(c *gin.Context) {
if _, err := db.ExecContext(c.Request.Context(), "CREATE TABLE IF NOT EXISTS ticks (tick timestamp)"); err != nil {
c.String(http.StatusInternalServerError,
fmt.Sprintf("Error creating database table: %q", err))
return
}
if _, err := db.ExecContext(c.Request.Context(), "INSERT INTO ticks VALUES (now())"); err != nil {
c.String(http.StatusInternalServerError,
fmt.Sprintf("Error incrementing tick: %q", err))
return
}
rows, err := db.QueryContext(c.Request.Context(), "SELECT tick FROM ticks")
if err != nil {
c.String(http.StatusInternalServerError,
fmt.Sprintf("Error reading ticks: %q", err))
return
}
defer rows.Close()
var b strings.Builder
for rows.Next() {
var tick time.Time
if err := rows.Scan(&tick); err != nil {
c.String(http.StatusInternalServerError,
fmt.Sprintf("Error scanning ticks: %q", err))
return
}
fmt.Fprintf(&b, "Read from DB: %s\n", tick.String())
}
if err := rows.Err(); err != nil {
c.String(http.StatusInternalServerError,
fmt.Sprintf("Error iterating ticks: %q", err))
return
}
c.String(http.StatusOK, b.String())
}
}
Now that your code changes are complete, recompile and install the updated
binary so it’s available on your $PATH and heroku local runs your new /db
route:
$ go install .
In order to test the new code locally, we’ll want a local postgres database. If you don’t have postgres installed locally, follow these instructions. Then create a database for use with our app:
$ createdb go-getting-started
Now test the local changes using the local database:
$ heroku local --port=5006
Visit your application at http://localhost:5006/db. If your changes worked, you see a log of timestamp. Refreshing the page should add more timestamps to the log.
Now deploy this local change to Heroku.
Almost every deploy to Heroku follows this same pattern. First, add the modified files to the local Git repository:
$ git add .
Now commit the changes to the repository:
$ git commit -m "Added db route"
[main 3ace3ef] Added db route
3 files changed, 74 insertions(+)
Now deploy as before:
$ git push heroku main
...
Finally, check that everything is working:
$ heroku open db
Provision a Logging Add-on
Add-ons are third-party cloud services that provide out-of-the-box additional services for your application, from persistence through logging to monitoring and more.
By default, Heroku stores 1500 lines of logs from your application, but the full log stream is available as a service. Several add-on providers have logging services that provide things such as log persistence, search, and email and SMS alerts.
In this step, you provision one of these logging add-ons, Papertrail, using its free plan.
Provision the Papertrail logging add-on:
$ heroku addons:create papertrail
Creating papertrail on evening-lowlands-26749... free
Provisioning has been successful
Created papertrail-regular-24869
Run heroku addons:docs papertrail to view documentation.
The add-on is now deployed and configured for your app. You can list add-ons for your app with this command:
$ heroku addons
Add-on Plan Price Max Price State
-------------------------------------------------------------------------------------------------
heroku-postgresql (postgresql-trapezoidal-80403) essential-0 ~$0.007/hour $5/month created
└─ as DATABASE
papertrail (papertrail-regular-24869) choklad free free created
└─ as PAPERTRAIL
The table above shows add-ons and the attachments to the current app (evening-lowlands-26749) or other apps.
To see this particular add-on in action, visit your application’s Heroku URL a few times. Each visit generates more log messages, which get routed to the Papertrail add-on. Visit the Papertrail console to see the log messages:
$ heroku addons:open papertrail
Your browser opens up a Papertrail web console, showing the latest log events. The interface lets you search and set up alerts.
Define Config Vars
Heroku lets you externalize configuration, storing data such as encryption keys or external resource addresses in config vars.
At runtime, config vars are exposed as environment variables to the application.
Your application is already reading one config var, the $PORT config var.
$PORT is automatically set by Heroku on web dynos.
Let’s explore how to use user-set config vars in your Go application.
Modify main.go and add a repeatHandler function that returns
Hello from Go! the number of times specified by the value of the REPEAT
environment variable.
At the end of main.go add:
func repeatHandler(r int) gin.HandlerFunc {
return func(c *gin.Context) {
var builder strings.Builder
for range r {
builder.WriteString("Hello from Go!\n")
}
c.String(http.StatusOK, builder.String())
}
}
Next, let’s read the $REPEAT environment variable.
In file main.go, on line 32 add:
repeatStr := os.Getenv("REPEAT")
repeat, err := strconv.Atoi(repeatStr)
if err != nil {
log.Printf("Error converting $REPEAT to an int: %q - Using default\n", err)
repeat = 5
}
Now register the handler function with the router.
In file main.go, on line 42 add:
router.GET("/repeat", repeatHandler(repeat))
Now run goimports to automatically bring in the standard library packages
that are now in use:
$ goimports -w main.go
The updated main.go will look like this:
package main
import (
"context"
"database/sql"
"fmt"
"log"
"net/http"
"os"
"strconv"
"strings"
"time"
"github.com/gin-gonic/gin"
_ "github.com/heroku/x/hmetrics/onload"
_ "github.com/jackc/pgx/v5/stdlib"
)
func main() {
port := os.Getenv("PORT")
if port == "" {
log.Fatal("$PORT must be set")
}
db, err := sql.Open("pgx", os.Getenv("DATABASE_URL"))
if err != nil {
log.Fatalf("Error opening database: %q", err)
}
if err := db.PingContext(context.Background()); err != nil {
log.Fatalf("Error connecting to database: %q", err)
}
repeatStr := os.Getenv("REPEAT")
repeat, err := strconv.Atoi(repeatStr)
if err != nil {
log.Printf("Error converting $REPEAT to an int: %q - Using default\n", err)
repeat = 5
}
db.SetMaxOpenConns(5)
router := gin.New()
router.GET("/repeat", repeatHandler(repeat))
router.Use(gin.Logger())
router.GET("/db", dbFunc(db))
router.LoadHTMLGlob("templates/*.tmpl.html")
router.Static("/static", "static")
router.GET("/", func(c *gin.Context) {
c.HTML(http.StatusOK, "index.tmpl.html", nil)
})
router.Run(":" + port)
}
func dbFunc(db *sql.DB) gin.HandlerFunc {
return func(c *gin.Context) {
if _, err := db.ExecContext(c.Request.Context(), "CREATE TABLE IF NOT EXISTS ticks (tick timestamp)"); err != nil {
c.String(http.StatusInternalServerError,
fmt.Sprintf("Error creating database table: %q", err))
return
}
if _, err := db.ExecContext(c.Request.Context(), "INSERT INTO ticks VALUES (now())"); err != nil {
c.String(http.StatusInternalServerError,
fmt.Sprintf("Error incrementing tick: %q", err))
return
}
rows, err := db.QueryContext(c.Request.Context(), "SELECT tick FROM ticks")
if err != nil {
c.String(http.StatusInternalServerError,
fmt.Sprintf("Error reading ticks: %q", err))
return
}
defer rows.Close()
var b strings.Builder
for rows.Next() {
var tick time.Time
if err := rows.Scan(&tick); err != nil {
c.String(http.StatusInternalServerError,
fmt.Sprintf("Error scanning ticks: %q", err))
return
}
fmt.Fprintf(&b, "Read from DB: %s\n", tick.String())
}
if err := rows.Err(); err != nil {
c.String(http.StatusInternalServerError,
fmt.Sprintf("Error iterating ticks: %q", err))
return
}
c.String(http.StatusOK, b.String())
}
}
func repeatHandler(r int) gin.HandlerFunc {
return func(c *gin.Context) {
var builder strings.Builder
for range r {
builder.WriteString("Hello from Go!\n")
}
c.String(http.StatusOK, builder.String())
}
}
The heroku local command automatically sets up the environment based on the
contents of the .env file in your local directory. In the top level directory
of your sample project, there’s already a .env file that contains:
REPEAT=10
Recompile the app and run it:
$ go install .
$ heroku local --port=5006
When you access the /repeat route on the app at http://localhost:5006/repeat
you’ll see “Hello from Go!” ten times.
To set the config var on Heroku, execute the following:
$ heroku config:set REPEAT=10
Setting REPEAT and restarting evening-lowlands-26749... done, v5
REPEAT: 10
View the app’s config vars using heroku config:
$ heroku config | grep REPEAT
REPEAT: 10
Deploy the changes to heroku using what you learned, and try it out by visiting
the /repeat handler of your application:
$ git add .
$ git commit -m "Added configurable repeat"
$ git push heroku main
$ heroku open repeat
Delete Your App
Remove the app from your account. We only charge you for the resources you used.
This action permanently deletes your application and any add-ons attached to it.
$ heroku apps:destroy
You can confirm that your app is gone with this command:
$ heroku apps --all
Next Steps
You now know how to configure and deploy a Go app, view logs, and start a console.
To learn more, see: