I also did it for a Go app recently exactly like that.
It was mostly about putting the "imperative" part closer to main.go and putting business rules and things like that in the other files: Routing, serialization, validation, business rules, data transformation, command line argument parsing, configuration parsing, they all go in the "functional" part. Instantiating the HTTP server, reading argv, files and reading/writing to the database goes in the "imperative" part.
The major hurdle for me is frameworks and libraries that often want to be used in an imperative way. Some routers, for, instance could be purely functional, but they often want to instantiate the HTTP server themselves. Not a big issue in practice in Golang, though.
Also: the database. To keep purity in a simple way, you gotta wrap the imperative parts and use callback-ish structures. This part is often where I cheat. But with a proper abstraction it's great. I hate to say the forbidden word here, but a "monadic" abstraction you can have your cake and eat it. I never did it in Golang and I have no idea whether it would work, but, I did it in C# and it was a breeze.
I agree with your assessment about testing. In the end it was incredibly easy for me to get 100% coverage on the functional part.
If you're familiar with Node.js promises, you already know how it works. I just pass a hollow "DB" object to my controllers that can perform queries and commands, but instead of returning data imperatively, I use JS-like "promises" to chain the next steps. On the "success" callback you have the result of queries. At the end I will just return this "chain of statements" that wasn't executed yet. I only really run everything at the "imperative" layer, within a database transaction.
That's also similar to how Haskell IO works. If you desugar the "do" syntax, you get something like this. Of course I said "monadic" between quotes above because it doesn't follow the functor/monad laws, it's just a Fluent interface promise-thing tailor-made for that very small app.
A big issue is that this was in very small project. Promises are not exactly pretty, and the code is not the easiest to maintain if you don't know how they work, which is why I "cheated" on the Golang app. I think someone else smarter might be able to figure this problem out too, though :)
It was mostly about putting the "imperative" part closer to main.go and putting business rules and things like that in the other files: Routing, serialization, validation, business rules, data transformation, command line argument parsing, configuration parsing, they all go in the "functional" part. Instantiating the HTTP server, reading argv, files and reading/writing to the database goes in the "imperative" part.
The major hurdle for me is frameworks and libraries that often want to be used in an imperative way. Some routers, for, instance could be purely functional, but they often want to instantiate the HTTP server themselves. Not a big issue in practice in Golang, though.
Also: the database. To keep purity in a simple way, you gotta wrap the imperative parts and use callback-ish structures. This part is often where I cheat. But with a proper abstraction it's great. I hate to say the forbidden word here, but a "monadic" abstraction you can have your cake and eat it. I never did it in Golang and I have no idea whether it would work, but, I did it in C# and it was a breeze.
I agree with your assessment about testing. In the end it was incredibly easy for me to get 100% coverage on the functional part.