Hard disagree.

    GO_SRCS=$(shell find src -name '*.go' -not -name '*_test.go)
    GO_TESTS=$(shell find src -name '*_test.go')

    .PHONY: run
    docker-run: target/image.target
        docker run example

    .PHONY: integration-test
    integration-test: target/integration-test.target

    .PHONY: test
    test: target/test.target

    target/bin: $(GO_SRCS)
        mkdir -p $(@D)
        GOOS=linux-amd64 go build -ldflags='-s -w' -o $@

    target/context.target: Dockerfile target/bin
        rm -fr $(@:.target=)
        mkdir -p $(@:.target=)
        cp Dockerfile target/bin $(@:.target=)
    
    target/image.target: target/context.target
        docker build -t example $(<:.target=)

    target/integration-test.target: target/image.target
        # TODO: test the docker image

    target/test.target: $(GO_SRCS) $(GO_TESTS)
        go test
What is your better thing?

(The answer is Bazel, but if you think Make is a "supercar".......)

Most (all?) of your Makefile targets don't represent actual files to 'make'.

You're using make to run a bunch of tasks, including where tasks may depend on other tasks having been run before.

A task-runner tool like `just` is better suited to this task. https://github.com/casey/just

`just` has some nice UX improvements over `make`. (e.g. doesn't require soft tabs, can list recipes out of the box, recipes can take command line arguments, supports .env files, can be run from any subdirectory).