Features Tutorials

Debugging with GoLand – Getting Started

Updated and validated on January 17, 2022. You can find more tutorials on how to debug Go programs here. You may also refer to the Debugging section of our Help documentation.

  1. Debugging with GoLand – Getting Started (this post)
  2. Debugging with GoLand – Essentials
  3. Debugging with GoLand – Advanced Debugging features
  4. Debugging with GoLand – Windows minidumps

Debugging is an essential part of any modern application lifecycle. It’s not only useful for finding bugs as programmers often use debuggers to see and understand what happens in a new codebase they have to work with or when learning a new language.

There are two styles of debugging which people prefer:

  • print statements: which is logging as your code executes various steps that might run
  • using a debugger such as Delve, either directly or via an IDE: this gives more control over the execution flow, more capabilities to see what the code does which may not have been included in the original print statement, or even changing values during the application runtime or going back and forward with the execution of the application.

In this series we’ll focus on the second option, using an IDE to debug an application.

As you noticed from the description above, doing so provides a lot more control and capabilities to find bugs and as such this article is broken down in several sections:

And after a look at all the above scenarios, we’ll see how GoLand handles them so that you can have the same set of features listed below regardless of where your application is running:

  • the basics of debugging
    • controlling the execution flow
    • evaluating expressions
    • watching custom values
    • changing variable values
  • working with breakpoints

The IDE also supports debugging core dumps produced on Linux and using Mozilla’s rr reversible debugger on Linux. We’ll see these features in upcoming, separate blog posts.

We’ll use a simple web server in all the above applications, but these can be applied to any kind of application, CLI tools, GUI applications, etc.

We’ll use Go Modules, but the default GOPATH using any other dependency management form can work just as well.

Create the application using the Go modules type, and make sure you have Go 1.11+ or newer.

The application can be found below or in this repository.

package main

import (
	"fmt"
	"log"
	"net/http"
	"os"
	"time"

	"github.com/gorilla/mux"
)

const (
	readTimeout  = 5
	writeTimeout = 10
	idleTimeout  = 120
)

func indexHandler(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "text/plain")
	returnStatus := http.StatusOK
	w.WriteHeader(returnStatus)
	message := fmt.Sprintf("Hello %s!", r.UserAgent())
	w.Write([]byte(message))
}

func main() {
	serverAddress := ":8080"
	l := log.New(os.Stdout, "sample-srv ", log.LstdFlags|log.Lshortfile)
	m := mux.NewRouter()

	m.HandleFunc("/", indexHandler)

	srv := &http.Server{
		Addr:         serverAddress,
		ReadTimeout:  readTimeout * time.Second,
		WriteTimeout: writeTimeout * time.Second,
		IdleTimeout:  idleTimeout * time.Second,
		Handler:      m,
	}

	l.Println("server started")
	if err := srv.ListenAndServe(); err != nil {
		panic(err)
	}
}

And we can also create a test file like this:

package main

import (
	"net/http"
	"net/http/httptest"
	"testing"
)

func TestIndexHandler(t *testing.T) {
	tests := []struct {
		name           string
		r              *http.Request
		w              *httptest.ResponseRecorder
		expectedStatus int
	}{
		{
			name:           "good",
			r:              httptest.NewRequest("GET", "/", nil),
			w:              httptest.NewRecorder(),
			expectedStatus: http.StatusOK,
		},
	}
	for _, test := range tests {
		test := test
		t.Run(test.name, func(t *testing.T) {
			indexHandler(test.w, test.r)
			if test.w.Code != test.expectedStatus {
				t.Errorf("Failed to produce expected status code %d, got %d", test.expectedStatus, test.w.Code)
			}
		})
	}
}

For Go Modules enabled applications, you can invoke the Alt+Enter shortcut then Sync packages of <my project>. For non-Go Modules enabled applications, you can invoke the Alt+Enter shortcut then go get -t <missing dependency>.

One final information that we should note is that the debugging experience is also impacted by the Go version being used to compile the target program. With each Go version released, the Go Team works to add more debugging information and improve the quality of the existing one, and this can be seen when switching from an older version such as Go 1.8 to Go 1.9 or in a more dramatic way, from Go 1.8 to Go 1.11. So, the newer the Go version you can use, the better your experience will be.

Now that all our code is in place let’s start debugging it!

Debugging an application

To debug our application, we can click on the green triangle and then select Debug ‘go build main.go’. Alternatively, we can right-click on a folder and choose Debug | go build <project name>.

debugging an application

Debugging tests

This can be done similarly to debugging an application. GoLand recognizes tests from the standard testing package, gocheck, and testify frameworks, so these actions are available for all of them straight from the editor window.

For other frameworks you may need to configure a custom test runner by going to Run | Edit Configurations… and specifying additional arguments either in the Go tool arguments or in Program arguments, depending on where the custom library you are using needs those arguments.

Debugging a running application on the local machine

There are cases where you would want to debug an application that’s launched outside of the IDE. One of such cases is when the application runs on the local machine. To run this using the debugger, then open the project in the IDE and select Attach to Process…

If it’s the first time you are using this feature, then the IDE will ask you to download a small utility program named gops, available at https://github.com/google/gops. This program helps the IDE find Go process running on your machine. Then invoke the Attach to Process… feature again.

You’ll see a list of all Go projects running on your computer, so who knows, maybe you’ll even discover new ones. Select the one that you want to debug from the list, and the debugger will attach to the process, and you can start your debugging session.

To ensure that the debugging session is successful and you can debug the application without issues, all you need to do is to compile your application with a special flag. The IDE will add these flags automatically for the other configuration types, so these are necessary only when compiling the application manually.

If you are running with Go 1.10 or newer, you need to add -gcflags="all=-N -l" to the go build command. If you are running with Go 1.9 or older, then you need to add -gcflags="-N -l" to the go build command.

Important note! Some people also use the -ldflags="all=-w" or -ldflags="-w", depending on the Go version being used. This is incompatible with debugging the application as it strips out the necessary DWARF information needed for Delve to work. As such the application will not be able to be debugged.

A similar problem will be encountered when using symbolic links, or symlinks, on operating systems and file systems which support this feature. Due to an incompatibility between the Go toolchain, Delve, and the IDE, using symlinks is currently not compatible with debugging an application.

Attach to process – ⌥⇧ F5 (Ctrl + Alt + F5 for Windows/Linux)

Debugging a running application on a remote machine

Lastly, this case is a more complex one, at least for now. This debugging session type allows you to connect the IDE to a remote target and debug a process running there. By remote target, we can consider containers running on the local machine remote targets or actual servers either on-premise or in the cloud.

Much like with running against applications running on your local machine, you have to be careful about the compiler flags that you use to compile the application.
After that, you need to compile Delve with the same Go version and host/target as your application as there might be slight differences between the various operating systems which could cause the debugging session not to work as expected.

You should also make sure that if you are using $GOPATH, the project is compiled using the same relative path to $GOPATH. For example: if your project is available under github.com/JetBrains/go-sample, then both on the machine with the IDE and in the machine that compiles the application, the application is under $GOPATH/src/github.com/JetBrains/go-sample, $GOPATH can be different between these two machines. Then the IDE can automatically map the sources between the local and remote machine.

Then, when you deploy your application, also deploy the copy of Delve that was compiled earlier, and you have two options to launch the debugger there:

  • let the debugger run the process for you: if you choose this option, you need to run dlv --listen=:2345 --headless=true --api-version=2 exec ./application. Also note that if you use any firewall or containers, then you’ll need to expose the port 2345 in those configurations. The port number can be any random one you need, not just 2345 as long as it’s free on the host machine.
  • attaching to the process: you need to run dlv --listen=:2345 --headless=true --api-version=2 attach <pid> where <pid> is the process id of your application.

After all of this is done, the final step is to connect your IDE to the remote debugger. You can do so by going to Run | Edit Configurations… | + | Go Remote and configuring the host and port your remote debugger is listening on.

You can use the container definition from the Dockerfile in our repository.

FROM golang:1.17-alpine3.15 as build-env

ENV CGO_ENABLED 0

# Allow Go to retreive the dependencies for the build step
RUN apk add --no-cache git

WORKDIR /goland-debugging/
ADD . /goland-debugging/

RUN go build -o /goland-debugging/srv .

# Get Delve from a GOPATH not from a Go Modules project
WORKDIR /go/src/
RUN go install github.com/go-delve/delve/cmd/dlv@latest

# final stage
FROM alpine:3.15.0

WORKDIR /
COPY --from=build-env /goland-debugging/srv /
COPY --from=build-env /go/bin/dlv /

EXPOSE 8080 40000

CMD ["/dlv", "--listen=:40000", "--headless=true", "--api-version=2", "exec", "/srv"]

Please note that in this Dockerfile, the project is named goland-debugging, but you should change this folder name to match the name of the project you created.

When running the Docker container, you also need to specify --security-opt="apparmor=unconfined" --cap-add=SYS_PTRACE arguments to it. If you do it from the command line, these are arguments to the docker run command. If you do it from the IDE, these options must be placed in the Run options field.

This is what your Run configuration should look like.

The final step is to connect your IDE to the remote debugger.

That’s it for today. In the next article in the series, we’ll learn how to use the various features that are available to us during one of the debugging scenarios described above. Please let us know your feedback in the comments section below, on Twitter, or open an issue on our issue tracker.

image description