diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 1a7de59..9e5c4fe 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -23,18 +23,15 @@ jobs:
         go-version: ${{ matrix.go-version }}
     - name: Checkout code
       uses: actions/checkout@v2
-    - name: Format
-      if: matrix.go-version >= '1.17'
-      run: diff -u <(echo -n) <(gofmt -d .)
+    - name: Run static checks
+      uses: golangci/golangci-lint-action@v2
+      with:
+        version: v1.42.1
+        args: --config=.golangci.yml --verbose
+        skip-go-installation: true
+        skip-pkg-cache: true
+        skip-build-cache: true
     - name: Build
       run: go build
-    - name: Vet
-      run: go vet
-    - name: Install and run staticcheck
-      if: matrix.go-version >= '1.17'
-      run: |
-        go install honnef.co/go/tools/cmd/staticcheck@latest
-        staticcheck -version
-        staticcheck -- ./...
     - name: Run unit tests
       run: go test -v -race -cover
diff --git a/.golangci.yml b/.golangci.yml
new file mode 100644
index 0000000..8dcba28
--- /dev/null
+++ b/.golangci.yml
@@ -0,0 +1,54 @@
+# See https://golangci-lint.run/usage/configuration/ for available options.
+# Also https://github.com/cilium/cilium/blob/master/.golangci.yaml as a
+# reference.
+linters:
+  disable-all: true
+  enable:
+    - asciicheck
+    - deadcode
+    - dogsled
+    - durationcheck
+    - errcheck
+    - errname
+    - errorlint
+    - exhaustive
+    - exportloopref
+    - forcetypeassert
+    - godot
+    - goerr113
+    - gofmt
+    - goimports
+    - gosec
+    - gosimple
+    - govet
+    - ifshort
+    - ineffassign
+    - misspell
+    - nestif
+    - nilerr
+    - prealloc
+    - predeclared
+    - revive
+    - rowserrcheck
+    - staticcheck
+    - structcheck
+    - thelper
+    - typecheck
+    - unconvert
+    - unparam
+    - unused
+    - varcheck
+
+linters-settings:
+  gosimple:
+    go: "1.17"
+  govet:
+    enable-all: true
+    disable:
+      - fieldalignment
+  staticcheck:
+    go: "1.17"
+  stylecheck:
+    go: "1.17"
+  unused:
+    go: "1.17"
diff --git a/workerpool_test.go b/workerpool_test.go
index b50b5bc..5d50855 100644
--- a/workerpool_test.go
+++ b/workerpool_test.go
@@ -16,6 +16,7 @@ package workerpool_test
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"runtime"
 	"sync"
@@ -181,11 +182,14 @@ func TestWorkerPool(t *testing.T) {
 	go func() {
 		id := fmt.Sprintf("task #%2d", numTasks-1)
 		ready <- struct{}{}
-		wp.Submit(id, func(_ context.Context) error {
+		err := wp.Submit(id, func(_ context.Context) error {
 			defer wg.Done()
 			done <- struct{}{}
 			return nil
 		})
+		if err != nil {
+			t.Errorf("failed to submit task '%s': %v", id, err)
+		}
 		sc <- struct{}{}
 	}()
 
@@ -282,12 +286,12 @@ func TestConcurrentDrain(t *testing.T) {
 	<-ready
 	time.Sleep(10 * time.Millisecond)
 
-	if err := wp.Submit("", nil); err != workerpool.ErrDraining {
+	if err := wp.Submit("", nil); !errors.Is(err, workerpool.ErrDraining) {
 		t.Errorf("submit: got '%v', want '%v'", err, workerpool.ErrDraining)
 	}
 
 	results, err := wp.Drain()
-	if err != workerpool.ErrDraining {
+	if !errors.Is(err, workerpool.ErrDraining) {
 		t.Errorf("drain: got '%v', want '%v'", err, workerpool.ErrDraining)
 	}
 	if results != nil {
@@ -318,7 +322,7 @@ func TestWorkerPoolDrainAfterClose(t *testing.T) {
 	wp := workerpool.New(runtime.NumCPU())
 	wp.Close()
 	tasks, err := wp.Drain()
-	if err != workerpool.ErrClosed {
+	if !errors.Is(err, workerpool.ErrClosed) {
 		t.Errorf("got %v; want %v", err, workerpool.ErrClosed)
 	}
 	if tasks != nil {
@@ -329,7 +333,7 @@ func TestWorkerPoolDrainAfterClose(t *testing.T) {
 func TestWorkerPoolSubmitAfterClose(t *testing.T) {
 	wp := workerpool.New(runtime.NumCPU())
 	wp.Close()
-	if err := wp.Submit("dummy", nil); err != workerpool.ErrClosed {
+	if err := wp.Submit("dummy", nil); !errors.Is(err, workerpool.ErrClosed) {
 		t.Fatalf("got %v; want %v", err, workerpool.ErrClosed)
 	}
 }
@@ -343,10 +347,10 @@ func TestWorkerPoolManyClose(t *testing.T) {
 	}
 
 	// calling Close() more than once should always return an error.
-	if err := wp.Close(); err != workerpool.ErrClosed {
+	if err := wp.Close(); !errors.Is(err, workerpool.ErrClosed) {
 		t.Fatalf("got %v; want %v", err, workerpool.ErrClosed)
 	}
-	if err := wp.Close(); err != workerpool.ErrClosed {
+	if err := wp.Close(); !errors.Is(err, workerpool.ErrClosed) {
 		t.Fatalf("got %v; want %v", err, workerpool.ErrClosed)
 	}
 }
@@ -361,12 +365,15 @@ func TestWorkerPoolClose(t *testing.T) {
 	wg.Add(n)
 	for i := 0; i < n; i++ {
 		id := fmt.Sprintf("task #%2d", i)
-		wp.Submit(id, func(ctx context.Context) error {
+		err := wp.Submit(id, func(ctx context.Context) error {
 			working <- struct{}{}
 			<-ctx.Done()
 			wg.Done()
 			return ctx.Err()
 		})
+		if err != nil {
+			t.Errorf("failed to submit task '%s': %v", id, err)
+		}
 	}
 
 	// ensure n workers are busy