For example, if you only use S3, it is premature abstraction to accept an interface for something that may not be S3. Just accept the S3 client itself as input.
Then the S3 client can be designed to be testable by itself by having the lowest-level dependencies (ie, network calls) stubbed out. For example, it can take a fake implementation that has hard-coded S3 URLs mapped to blobs. Everything that tests code with S3 simply has to pre-populate a list of URLs and blobs they need for the test, which itself can be centralized or distributed as necessary depending on the way the code is organized.
Generally, I/O is great level to use an interface and to stub out. Network, disk, etc. Then if you have good dependency injection practicies, it becomes fairly easy to use real structs in testing and to avoid interfaces purely for testing.
Related reading from the Google style guide, but focused specifically on the transport layer: https://google.github.io/styleguide/go/best-practices.html#u...