There's basically two ways I have seen this work. In one case, you have a "IO thing" stored somewhere in a struct.
type example struct {
backend db.DB // Never actually used the database library, so this MAY not be the right type name
...
}
Then your code uses that to call the methods on var.backend, and you can replace it with a test instance. This feels approximately like dependency injection, maybe? Or maybe just encapsulation?
And the other is that you pass your "IO" as a parameter. A pretty typical example would be fmt.Fprintf.
func Fprintf(out io.Writer, format string, args ...interface{}) (n int, e error)
It is of course limited, but all testing is limited. But, most "mocking" I have seen replaces the original type while the test is ongoing, and that specifically is pretty hard in Go. It is trivial to pass in a parameter, or set a struct slot, to something that fulfils a specific interface. And it is often quite useful. And, again, of course something that you may have to write tests for, to ensure that it does what you intend it to (especially if it is complex and needs to hold some state, which unfortunately sometimes happens).
And the other is that you pass your "IO" as a parameter. A pretty typical example would be fmt.Fprintf.
It is of course limited, but all testing is limited. But, most "mocking" I have seen replaces the original type while the test is ongoing, and that specifically is pretty hard in Go. It is trivial to pass in a parameter, or set a struct slot, to something that fulfils a specific interface. And it is often quite useful. And, again, of course something that you may have to write tests for, to ensure that it does what you intend it to (especially if it is complex and needs to hold some state, which unfortunately sometimes happens).