There's a great job a friend has turned you on to, but when you read the description your heart sinks:
... must have experience working with Go on a daily basis ...
You've used Go before and you kind of liked it, but that's not "working with it on a daily basis". You wonder if you could brush up a bit before the interview... but then you would be lying if you said you had the experience they were looking for. Never a good idea to lie about stuff like that.
Then you keep reading:
If you've been programming for a long time but are new to Go, that's OK! Apply anyway. Go is simple to learn and if you're the right fit, we'll train you.
I wish more companies would do this kind of thing! Anyway - you get the job and you promise to get up to speed on Go. So: what do you do?
Here's what I would have done 2 years ago:
- Look for Go tutorials on YouTube.
- Ask a friend for a book recommendation.
- Ask as many people as I could for a course recommendation.
From there, I would pick two things and jump in. Today, however, is a different story.
Claude, Show Me Go
Claude Sonnet is an amazing LLM and is my go-to for basically everything. I pay for a monthly subscription and it's worth every penny, and this isn't just for generating code! In fact, that's rarely what I use it for.
I have it write me utility scripts, create outlines and help me think through complex problems. I even have it proof blog posts by running the code blocks in the markdown.
For learning Go, I would start with a simple set of instructions in CLAUDE.md
(you can add this to .github/copilot-instructions.md
if you're using Copilot:
# Copilot Instructions
I'm teaching myself Go by _doing_ things. I want to understand from a basic level all the way through deployment.
I have 28 years of programming experience. I'm most familiar with JavaScript so comparisons to Go are useful. I want to learn Go idioms and best practices.
I learn by doing and building.
Don't do things for me, give me the commands to run in the tutorial itself.
Put all lessons in the `/tutorials` directory as markdown documents.
## The Tutorial
Center the tutorials on a B2B invoicing system using gRPC and Protobuf. I want to bill my clients and send them invoices using Stripe, the online payment processor.
I want to know _why_ you're using the commands given. I want to know _what_ the code does. Use comments in the code, and terse explanations. I'll ask questions if I don't understand.
## Tone and Response
Don't be effusive, be terse and direct. Don't embellish or make things up, just be factual and cite your sources.
## Pace
Create one lesson at a time and put recommendations for the next lesson at the bottom.
I don't know Protobuf or gRPC either, but I figure why not? Let's keep it real and grounded as it's rare to just create an application without some other technologies involved.
Let's create the first lesson by adding the prompt:
Create the first lesson
And off we go...
You Get What You Ask For
It's critical that you spend time with the instructions up front as I have here. Consider how you learn, the tone you want to work with, and the subject matter. Use bullet points to keep things as concise as possible, and also know you can adjust as time goes on.
The most important part of these instructions, however, is this line:
I'll ask questions if I don't understand.
That, right there, is what makes AI-based learning superior to books and videos.
Here's what Claude made for me. I'll break it in to sections:
# Tutorial 1: Go Project Setup and Protobuf Basics
## Why Go Modules?
Go modules solve dependency management, similar to npm for Node.js. Unlike JavaScript's flat dependency tree, Go modules use semantic versioning and provide reproducible builds through `go.sum` checksums.
Initialize your Go project:
```bash
go mod init github.com/rob/grpc-invoicing
```
This creates `go.mod` - your dependency manifest. The module path serves as the import prefix for packages within your project.
## Why These Dependencies?
gRPC requires multiple packages because Go's standard library doesn't include HTTP/2 or protobuf support:
- `grpc`: Core gRPC runtime for client/server communication
- `protobuf/*`: Runtime libraries for generated protobuf code
- `protoc-gen-go*`: Code generators that convert `.proto` files to Go structs and interfaces
Install the dependencies:
```bash
go get google.golang.org/grpc
go get google.golang.org/protobuf/reflect/protoreflect
go get google.golang.org/protobuf/runtime/protoimpl
go get google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
go get google.golang.org/protobuf/cmd/protoc-gen-go@latest
```
Notice the way Claude is using Node for comparison here? That's another reason why learning like this is superior: it's tailored to my experience.
I'll skip over the center of the document as it gets into Protobuf stuff and gRPC basics, and I want to keep this post on-target. I will summarize by saying that every bit of code is explained along with the concepts behind.
It's also full of bugs.
Learning From Crap Code
I know many of you are thinking that I'm headed for disaster here, and that it's inevitable that I will learn the "wrong way", or write code that sucks or is unmaintainable.
I will counter that with:
- Yes, I know.
- Code in most tutorials (especially YouTube) is probably worse.
- I actually learn better if I have to debug something.
That last bit is surprisingly important. When is the last time you watched an online course where the instructor said "here's some code to implement thing A. Give it a try!" and you give it a try only to find it didn't work. Then, in the next lesson, the instructor says "did you find the bug? How did you fix it? What you ran into is a classic issue with JavaScript called 'scoping'..."
The exact same thing happened to me with Claude's tutorial, but the best part was I had Claude help me fix it! From linting to compiler errors - I was able to ask Claude "why isn't this working?" and receive a pretty compelling answer.
Soon enough I began to understand Go's errors a bit more, and then look out for them on my own. Little things like misspelling the path to a module on import, incorrectly handling null
errors (ick), and so on.
Then things got complicated.
Learning the Idioms
I knew the code I was writing was about to get complex, because Go is full of idioms that I don't fully understand. I mean I know what pointers are but I haven't had to think about passing something by reference vs. by value since ... I think my VB days.
That's when I had to write this function:
```go
// CreateInvoice handles new invoice creation
func (s *InvoicingServer) CreateInvoice(ctx context.Context, req *pb.CreateInvoiceRequest) (*pb.CreateInvoiceResponse, error) {
// Input validation
if req.Client == nil {
return nil, status.Error(codes.InvalidArgument, "client is required")
}
if len(req.LineItems) == 0 {
return nil, status.Error(codes.InvalidArgument, "at least one line item is required")
}
// Generate unique ID (use proper UUID in production)
invoiceID := fmt.Sprintf("inv_%d", time.Now().Unix())
//... rest of code clipped
I could recognize a few things here:
- The
*
means "here comes a pointer". - The
if
statements at the top are the ubiquitousnull
checks that are everywhere. - The
:=
operator is variable assignment + type inference in one go.
But... what is func
doing here? There's a pair of parentheses as a first "argument", then the name of the function CreateInvoice
, then another argument list, followed by a third pair of parentheses with another pointer.
Baffling.
So I highlighted the declaration and, using Copilot, asked:
What is happening with #selection?
In VS Code, Copilot will see the variable #selection
and use it as part of the prompt.
The answer I got back was really good, explaining what "method receivers" are in Go, and why they're useful (tacking a function on to a struct). This didn't make much sense to me - why not just declare the method on the struct declaration?
As the answer was coming back, I figured it out on my own (I think): structs are data structures, not classes. There are no classes in Go, so it doesn't make sense to think of them that way. A method receiver is like a mixin in Ruby that extends functionality.
I don't know if that's completely true, but when I asked Claude if that sounded right it basically confirmed what I was thinking.
Making My Own Adventure
As I went on, I realized that some things needed a bit more emphasis than others. Go idioms, for instance, and good naming. Understanding what the standard project directories are (bin
, cmd
, internal
, e.g.) and what to put where, when. Also testing! I wanted to know how a "Go Pro" would test the code I was writing, and what things they would focus on and truly care about.
To that end, I prompted:
I want to test this function but I want to do it in a way that my boss would think an experienced Gopher wrote it. Show me that, and explain the choices.
Copilot (using Claude Sonnet) created a test file for me and then ran it. I was happy when the test passed, but I wanted to know why it did what it did. So I prompted it again by asking explain why you created this file where you did and what idioms you followed
.
Here's the answer:

As for the idioms:

There are 7 more points here, but I'm clipping to keep things readable.
The point is this: when is the last time you read a language tutorial where the author discussed testing idioms? I know there are some books out there that do just this - but then you get into testing topics which become a distraction. The author will then write a followup book like "Testing in Language X".
I want to know this stuff upfront! The great thing is, using Claude, I get to choose my own adventure and get as sidetracked as I like, jumping down rabbit holes and learning the things that interest me right when they come up.
This Is the Way
I'm no fan of "This Thing is Dead" posts, but in this case, it seems utterly obvious to me after using it to learn things the way I have. And yes, I'm completely aware that there might be a few things here and there that are flat wrong and that I'll need to ask another human for verification.
Here's what I think of that:

Every online course I've taken has had wrong information. Every book and online course I have created has had wrong information too! It's just the way it is - you can hire a massive team of editors and get fact-checked for days, but in the end someone will find a thing you claimed and counter it.
Going in to this kind of thing with eyes open and knowing "there be dragons" on the horizon is the key. Challenging yourself to prove that what you're doing is correct only reinforces what you've learned.
If you don't have another human to talk to, use a different LLM to review the things you've been doing.
For instance: here's what Gemini thinks of Claude Sonnet's test code (using Gemini CLI with Gemini 2.5-pro):
✦ Based on my review, invoicing_test.go provides a solid foundation for testing the gRPC client, but
there are several areas where it could be improved to align better with Go best practices.
What's Good
* Table-Driven Tests: The use of a slice of structs ([]struct{...}) to define test cases
(TestInvoicingClient_CreateInvoice) is excellent. This approach makes the tests clean, readable,
and easy to extend with new scenarios.
* Mocking: The mockInvoicingServiceClient is a good example of dependency injection. It correctly
implements the service client interface, allowing you to test the client's logic in isolation
without making actual gRPC calls.
* Context Testing: The test correctly verifies that a context with a deadline is passed to the gRPC
client method. This is a crucial best practice for any networked application to avoid hanging
requests.
* Variety of Cases: The tests cover a good range of scenarios, including the "happy path" (successful
creation) and multiple types of gRPC errors (internal, timeout, invalid argument).
* Benchmarking: The inclusion of a benchmark function (BenchmarkInvoicingClient_CreateInvoice) is a
proactive approach to performance testing.
Right on - that's a good start! So what about the bad?
Areas for Improvement
1. Fragile Error Checking: The tests compare error messages using err.Error() != tt.expectedError.
This is brittle and can cause tests to fail if an error message is slightly reworded. A better
practice is to inspect the gRPC status code. For example, instead of checking for the full string
"rpc error: code = Internal desc = database connection failed", you should check that
status.Code(err) == codes.Internal.
2. Incomplete Edge Case Tests: The TestInvoicingClient_CreateInvoice_EdgeCases function is not very
effective. The comments // This would depend on server-side validation indicate a dependency on the
server's behavior. For unit tests, the client itself should ideally perform basic validation (e.g.,
checking for a nil client or empty line items) before making a remote call. The current test only
confirms an error is returned, not the correct error for the specific invalid input.
3. Unused Mock Methods: The mock client implements GetInvoice, ListInvoices, and UpdateInvoiceStatus,
but they all return a "mock not implemented" error. There are no corresponding tests for these
methods, leaving that functionality untested.
4. Verbose Assertions: The success case in TestInvoicingClient_CreateInvoice manually compares each
field of the resulting Invoice struct. This is verbose and can be tedious to maintain. Using a
library like google/go-cmp or testify/assert would make these comparisons more robust and provide
much clearer diffs when a test fails.
In summary, it's a good start, but the tests could be more robust and maintainable by improving the
error and data validation strategies and by completing the test coverage for the entire client
surface.
If you, like me, might not fully understand what the issue is with the improvements, just ask!

I clipped off the rest of the response as it was long, but as you can see, even by going through a review with a different LLM, we're learning a ton!
What do you think? Am I being a bit too "extra" with this post title, or can you see what I'm seeing on the horizon?
Would love to hear your thoughts! As always, hit Reply or join up and leave a comment.
This post was 100% written by hand, by me, as all my posts are.