What is the best way to load different interface implementations at runtime in Go? (original) (raw)

November 11, 2025, 12:36pm 1

Hi everyone 😀
Quick question about Go, and I’d love your practical opinions/war stories.

Background / goal
I have a core repo (call it repo A) that defines a FileService interface that will be used throughout my application:

type FileService interface {
    Upload(path string, data []byte) (url string, err error)
    Delete(url string) error
}

Concrete implementations should live in a separate repo (call it repo B): e.g. azure_fileservice and s3_fileservice. A deployer should be able to pick which implementation to use via configuration at runtime. Ideally the app in repo A would load the chosen implementation on startup.

My current idea
Use Go’s plugin mechanism: build each implementation as a .so plugin and load the chosen plugin at runtime using plugin.Open() and a well-known exported symbol (e.g. New or NewFileService). That way repo A stays unchanged and deployers can drop-in a plugin file for the impl they want.

I need to know whether you think this is feasible and is the best way to handle such a setup, or if there are better approaches out there. Please share your thoughts and experiences. I’d really appreciate hearing what’s worked (or not worked) for you.

Thank you in advance! 😀

mje (Jeff Emanuel) November 11, 2025, 4:12pm 2

You could, but plugin package - plugin - Go Packages. I’ve never used plugins because of these, mostly the first one. If you can live with some tighter coupling, then make your implementations modules and import all of them in the main, and instantiate the desired implementation based on the configuration. You trade-off a little flexibility and danger, for less flexibility and safety.

Dean_Davidson (Dean Davidson) November 11, 2025, 4:20pm 3

What’s your motivation here? Are you trying to make smaller binary sizes? From the docs:

For these reasons, many users decide that traditional interprocess communication (IPC) mechanisms such as sockets, pipes, remote procedure call (RPC), shared memory mappings, or file system operations may be more suitable despite the performance overheads.

It might be better to just import the libraries you need. There’s some discussion here:

Editing to add: gRPC might be a good option for you:

Andrew_Stratton (Andrew Stratton) November 12, 2025, 4:43pm 4

If you only have a few alternatives, you could use build flags, which does mean compiling (or running) a version for a specific implementation - this saves on run time resources, e.g.

  1. add package (directory) fileservice
  2. add s3_fileservice.go with
//go:build s3
...
package fileservice
...
  1. similarly for azure_fileservice.go
  2. – likely you will also have a fileservice.go that is for both
  3. Use import "fileservice" to import
  4. use command line, e.g. go build -o s3.exe -tags=s3 .
  5. – or for VS code, you can add build tags to your compile/run, e.g.
"configurations": [
    {
        "name": "S3",
            "type": "go",
            "buildFlags": "-tags='s3'",
...

N.B. this is compile time, not runtime, but does allow you to have lighter weight (and safer?) executables which only include the relevant code.

There’s more on build tags here - go command - cmd/go - Go Packages but it’s not great for your own tags - also try here - Decoding Go Language Source Code: A Deep Dive into go: Directives and the Use of Automation Tools | Xinwei Xiong(cubxxw)'s English blog

Build tags can be combined using || and && and also !

Hope this helps - Andy

Sandamini (Sandamini) November 15, 2025, 3:36pm 5

Will look into this. Thank you friend :saluting_face:

Sandamini (Sandamini) November 15, 2025, 3:44pm 6

Thank you for looking into this :saluting_face:
Actually, the core code in repo A is an open-source framework. Inside this framework we have use cases where we need things like a FileService, UserService, etc. We’ve already built different implementations for these services to make developers’ lives easier, and we also want to give them the flexibility to plug in their own implementations if needed.

The problem is that we can’t keep all these implementations in the same repo; it would introduce a lot of dead code, and developers would end up deleting or modifying the core repo just to add their own service. So that’s the motivation behind this setup: I’m looking for a cleaner, maintainable way to support swappable implementations. 🙂

gRPC is an option, but my concern is that we’ll end up with a lot of tiny services that each do very simple, straightforward tasks. Wdyt?

Sandamini (Sandamini) November 15, 2025, 3:49pm 7

Thank you for looking into this :saluting_face:

is this an approach similar to Wire (Google’s DI Framework) ? blog/wire

Andrew_Stratton (Andrew Stratton) November 17, 2025, 10:13am 8

is this an approach similar to Wire (Google’s DI Framework) ? blog/wire

I would say not - it’s a Go compiler flag used to include/exclude source files for compilation (build).

I would identify this is as an idiomatic programming approach - depending on the Golang compiler.

Dependency Injection (DI) is a Design Pattern that allows you to define the requirements for a dependency as specify your own/existing implementation. The separation of concerns/responsibilities is explicit and tends to fit well with OO development. There is a lot more on DI that I can’t cover here - anything I give here is ‘my own take’.

e.g. with build tags - I have a ‘local’ build tag for packages to include when running on a local PC as opposed to in the cloud. These packages allow for connecting with usb devices. I have a ‘server_local.go’ that includes

//go:build local
package server

import (
    gamepad "quando/internal/server/devices/gamepad"
)

func init() {
    go gamepad.CheckChanged()
}

This means that when the local build flag is passed on compile, then the server will start the gamepad (usb) handler. Otherwise, it isn’t even compiled. But there is no interface or even agreed way of working - unlike DI.

It really depends on what you are trying to build. DI tends to match offering a framework, e.g. that developers extend; DI is likely the more enterprise/professional approach. Build tags are good for creating executables with different builds, e.g. you might have a debug/deploy builds. They also suit different platforms, e.g. having different code for Windows/Linux/MacOS, or different micro controller boards (e.g. using TinyGo), or docker vs local install, etc.

I hope this helps - Andy

Dean_Davidson (Dean Davidson) November 17, 2025, 7:28pm 9

Is there a reason you wouldn’t just follow the same pattern database/sql uses? That supports a lot of drivers developed by 3rd party devs:

You could do something similar and if somebody wants to use some 3rd party thing you can make them import it. There’s a Register func:

https://cs.opensource.google/go/go/+/refs/tags/go1.25.4:src/database/sql/sql.go;l=56

And here’s an example of jackc/pgx registering itself:

You could have anybody who wants to create a plugin implement an interface and use a similar Register approach.