Advanced Go Training
Advanced Go Training was designed with the experienced developer in mind. It assumes that you have already learned the day to day Go concepts, and are ready to master concepts such as Concurrency and advanced testing methodologies. It finishes with a comprehensive tour of the pprof tool and how to profile your Go code and determine how to make it more performant.
Length
3 days (with an optional 4th day). Each day is 4 hours long including a 15 minute break.Class Size
Our classes are priced for small and large classes. We offer classes starting at only five students, up to 100 students. We recognize that each company has specific needs and budgets.
For pricing, fill out our contact us form and you'll receive an automated reply with our current rates.
Target Audience
- You have been doing daily Go development for 3-6 months and want to master more advanced topics.
- You are looking to learn advanced concurrency patterns.
- You want to learn more advanced Go testing patterns, such as asynchronous testing, mocking, stubbing, etc.
- You want to learn how to profile your Go code and improve performance.
Prerequisites
- Familiarity and comfort navigating and basic file manipulation at the command line.
- Familiarity and comfort with a modern code editor, including creating and modifying files and projects.
- You have 3 to 6 months of daily Go experience.
- You have at least 1 year of experience with other modern development languages such as Java, C#, Swift, JavaScript, Python, Rust, etc.
- Familiarity with basic programming concepts and structures such as variables, loops, conditionals, etc.
- Computers should be capable of modern software development, such as access to install and run binaries, install a code editor, etc. Full instructions referenced here: preparing your environment for Go development. It may be necessary for them to have root/admin access to their computer.
Recommended Preparation
- Install and configure an editor for Go.
- Have a functioning Go environment installed with Go 1.13 or later.
- Sign up for a Github account if you don't already have one.
- Install GraphViz (needed for generating Go profiles).
Suggested Followup Learning
Expected Outcomes
- Students will be able understand and create advanced concurrent Go patterns.
- Students will understand how to create tests for asynchronous (concurrent) code.
- Students will be able to stub out Go structures for better unit testing.
- Students will understand the Go test ecosystem, as well as how to configure and run their Go tests efficiently.
- Students will be able to profile and improve performance of Go code.
Course Details
Day One
Automating Testing in Go: Using go test and Integrating with Continuous Integration Pipelines
Automating testing is crucial for maintaining code quality and reducing bugs during the development process. This chapter will teach you how to efficiently run and automate tests in Go using the `go test` command, alongside various useful options such as running specific tests, enabling verbose outputs, handling race conditions, and running tests in parallel. Additionally, you’ll learn how to incorporate tests into Continuous Integration (CI) pipelines for robust and automated feedback on your codebase. By the end, you’ll understand how to structure your tests for fast and effective validation.
Boosting Code Coverage: Tools and Techniques for Comprehensive Testing in Go
This chapter explores advanced tools and techniques for increasing your test coverage and improving overall code quality. You will learn how to generate code coverage reports, interpret coverage statistics, and utilize heatmaps to identify untested code. Additionally, we'll cover isolating specific tests, testing in parallel, and how to leverage Go’s built-in coverage tools to maximize testing efficiency. By the end of this chapter, you'll be equipped to improve your Go project's coverage and maintain high testing standards.
Break
Tea/Coffee Break.
Example Tests
No project is complete without great documentation. Example tests are a great way to not only document your code, but ensure that the examples you use always work as they are actually a test in addition to the documentation. Example tests are tests that document the use of your package. They are also runnable as tests to ensure that your examples are always error free. <code src="./src/examples/strutil/strutil_test.go" section="simple"></code> Example tests are all over the standard library. Here are a list of examples from the strings package:
Isolating Dependencies in Go Tests: Techniques for Creating Reliable Stubs and Fakes
In Go, decoupling components and isolating dependencies during testing can be easily achieved using interfaces. This chapter explores how to create stubs and fakes to test functionality independently and ensure reliability without the need for external dependencies. By simulating different behaviors in your components, you can improve test accuracy and efficiency. You'll learn how to replace real implementations with stubs in unit tests, handle various scenarios like errors and success cases, and maintain high test coverage. With these techniques, you can create more maintainable and flexible test suites for your Go applications.
Testing HTTP Handlers with Net/HTTP: Writing Tests for HTTP Services and Utilizing the net/http/httptest Package
In this chapter, we dive into testing HTTP handlers in Go, leveraging the `net/http/httptest` package. Testing HTTP handlers can be approached in two distinct styles: Unit Style Testing and Integration Style Testing. Each style serves different purposes in ensuring that your web applications work as expected. We will explore both approaches, demonstrating how to mock HTTP requests, simulate responses, and write efficient tests for your web services. Additionally, you'll learn how to create test servers, utilize middleware, and handle request bodies and form data. By the end of this module, you’ll have the skills to write comprehensive and reliable tests for Go-based web applications.
Day Two
Testing Asynchronous and Concurrent Code: Techniques for Reliable and Efficient Testing
Testing asynchronous and concurrent code introduces unique challenges, such as dealing with unknown execution times and ensuring test reliability in multi-threaded environments. This chapter focuses on how to efficiently test asynchronous operations, such as database writes, service queues, and distributed system calls, using Go's concurrency primitives like goroutines and channels. We will cover key techniques for handling these complexities in your tests, including timeouts, retries, and using channels to synchronize test execution. By the end of this chapter, you'll know how to design tests that effectively handle non-blocking, asynchronous processes while maintaining test stability and minimizing bloated test durations. This chapter assumes familiarity with basic concurrency concepts in Go, including the `httptest` package, channels, and goroutines.
Enhancing Go Testing with Advanced Tooling
This chapter introduces advanced tools and techniques to improve testing in Go. It covers topics such as: - **Comparing Complex Structures:** A deeper look into comparing values in Go, going beyond `reflect.DeepEqual`, which often leads to unexpected results, and exploring better alternatives like `go-cmp`. - **Using the `testing/quick` Package:** This section explains how to automate tests for a wide variety of input scenarios by leveraging the `testing/quick` package, which allows for rapid generation of test cases with minimal setup.
Workflow Automation
Workflow automation can significantly improve developer productivity. This chapter will show how to implement some very simple, lightweight automation to automatically run tests and coverage. File Watchers are small tools to watch a directory and rerun a command when certain files change. They are great for automatically running compile/lint/test/coverage/etc tasks and for reloading your application when the code changes. * [Reflex](https://github.com/cespare/reflex) - Mac/Linux * [Watcher](https://github.com/radovskyb/watcher) - Windows
Break
Tea/Coffee Break.
Handling IO in Go Tests: Effective Techniques for Testing IO Operations and External System Interactions
Handling input/output (IO) operations is a crucial part of many software applications, especially when interacting with files, network connections, or other external systems. Testing such interactions presents unique challenges, such as dealing with external dependencies, ensuring proper data flow, and maintaining test isolation. In this chapter, we will explore how to design Go programs to be easily testable without creating actual files or relying on real external systems. We’ll focus on using Go's `io.Reader` and `io.Writer` interfaces to abstract away IO operations, making tests faster, more reliable, and more isolated. Additionally, we’ll explore common patterns, utility types, and functions from Go's standard library to streamline testing for IO-heavy code.
Benchmarking
Benchmarking is an essential tool in Go for evaluating the performance of your code. This chapter introduces you to the powerful tools Go provides for benchmarking and performance analysis, allowing you to measure the efficiency of your code with precision. You will learn how to establish baseline performance, write effective benchmarks, and compare results to see the impact of optimizations. This chapter also delves into common pitfalls in benchmarking and how to avoid them, ensuring that your performance tests yield reliable, consistent results. Finally, we will explore advanced topics such as tracking memory allocations, handling compiler optimizations, and using tools like `benchstat` to analyze and compare benchmark data. By the end of this chapter, you'll have a solid foundation for systematically improving the performance of your Go applications.
Day Three
Profiling Go Applications for Performance Optimization
Profiling is a critical aspect of performance tuning in software development. In this chapter, we dive deep into the various profiling tools that Go provides, focusing on how to identify performance bottlenecks and optimize code effectively. You will learn how to generate CPU, memory, block, mutex, and thread creation profiles, and how to use interactive tools like `pprof` to analyze these profiles. Additionally, the chapter covers advanced topics like generating profiles from live applications, using profiling in benchmarks, and visualizing performance data with tools such as flame graphs. By the end of this chapter, you'll have the knowledge to pinpoint inefficient code and make informed decisions on how to enhance the performance of your Go applications.
Optimizing Go Services for High Performance
In this chapter, we delve into the advanced techniques and best practices for optimizing Go services to achieve better performance and efficiency. While tools like `pprof` and benchmarks are essential, the real value comes from identifying and addressing common coding inefficiencies that impact runtime performance. This chapter equips you with the skills to fine-tune your Go code by spotting these pitfalls, leveraging compiler tricks, and minimizing resource usage without sacrificing code clarity.
Break
Tea/Coffee Break.
Tracing
The execution trace captures a wide range of execution events such as goroutine creation/blocking/unblocking, syscall enter/exit/block, GC-related events, changes of heap size, processor start/stop, etc. A precise nanosecond-precision timestamp and a stack trace is captured for most events. The generated trace can be interpreted using `go tool trace`. This chapter will walk through how to enable tracing, use the trace tool, and spot performance issues. Go ships with several profiling tools, such as CPU, memory, and block profilers. Additionally, they have GC and scheduler tracers and a heap dumper. While each of these tools provide answers to many questions, they do it by "aggregating" information in many cases.
Optional Day
Concurrency Fundamentals in Go: Understanding Go's approach to concurrency with goroutines
This chapter introduces Go's unique approach to concurrency, focusing on goroutines as lightweight, independent functions that enable efficient concurrent programming. You will explore how Go's concurrency model encourages communication through channels rather than shared memory, making code faster, more robust, and easier to scale. The chapter also covers the differences between concurrency and parallelism, the role of Go's scheduler, and how channels simplify communication between goroutines. Additionally, you’ll get a glimpse into the potential pitfalls of data races and Go’s powerful race detector.
Leveraging the Sync Package for Concurrency Control: Utilizing the sync package to handle complex synchronization problems
In this chapter, we will explore how Go's `sync` package provides powerful tools to manage synchronization between goroutines, ensuring safe access to shared memory. You will learn how to utilize synchronization primitives like `WaitGroups`, `Mutexes`, `RWMutexes`, `sync.Once`, `sync.Cond`, and `Semaphores` to coordinate concurrent operations effectively. Additionally, we will cover common concurrency issues such as race conditions, deadlocks, and bugs, and demonstrate how to detect and fix them using Go's race detector and other tools. We will also explore advanced patterns including one-time initialization, condition variables, and controlling concurrency with semaphores, comparing different approaches for throttling concurrent operations. This chapter will equip you with the knowledge to handle complex concurrency problems, making your Go programs more robust and efficient.
Break
Tea/Coffee Break.
Concurrency With Channels: Safe Communication Between Goroutines
This chapter delves into Go's powerful concurrency mechanism using channels to enable safe communication between goroutines. You will learn how to implement basic and advanced channel usage, including buffered and unbuffered channels, how to signal completion, and gracefully shut down applications. The chapter will guide you through practical examples and best practices for avoiding common concurrency pitfalls, making it easier to build robust, concurrent applications that scale efficiently. Channels are a typed conduit through which you can send and receive values.
Understanding the Context Package: Managing request-scoped data, deadlines, and cancellation with the context package
The `context` package in Go is essential for managing request-scoped data, timeouts, and cancellations across various parts of your application. This chapter provides an in-depth look into how Go's context package helps simplify managing concurrent operations. You'll learn how to propagate values like user data across goroutines, handle timeouts to ensure efficient resource management, and gracefully cancel operations when required. By the end, you’ll understand how to avoid common pitfalls and implement the `context` package to build robust, scalable Go applications.
Prerequisites
Testing Basics: Writing and Running Unit Tests
This chapter introduces the fundamentals of testing in Go, with a focus on writing and running unit tests using the built-in `testing` package. You'll learn how to structure tests, the importance of naming conventions, and how to utilize the `*testing.T` type to create effective test cases. Key concepts such as the difference between `Error` and `Fatal`, crafting meaningful failure messages, and organizing internal vs. external tests are explored. Additionally, we'll cover useful helper functions, alternative testing packages, and provide practical examples to help you develop reliable, maintainable tests for your Go applications.
Table-Driven Testing for Flexible and Scalable Tests
Table-driven testing is a powerful and efficient way to create clean, reusable, and scalable tests in Go. By defining test cases as data in tables, you can minimize duplication and easily extend test coverage by adding new cases. This chapter covers how to implement table-driven tests, demonstrating how to isolate test logic, reduce redundancy, and ensure your tests remain organized. You will also learn advanced techniques like running sub-tests, parallelizing table-driven tests, and handling setup and teardown logic for enhanced performance and clarity
Interfaces and Polymorphism in Go: Understanding Go's approach to interfaces and dynamic behavior in type systems
Interfaces in Go provide a way to specify the behavior of an object: `If something can do this, then it can be used here`. This chapter will take a look at how to use interfaces to abstract that behavior. Concepts such as the `Empty Interface`, satisfying multiple interfaces, and asserting for behavior will be covered. Additionally, this chapter will cover the difference between `value` and `pointer` receivers and how they affect the ability to satisfy an interface. > The larger the interface, the weaker the abstraction. -- Rob Pike
Embedding and Composition for Code Reuse: Using Go’s embedding feature for cleaner, more modular code
This chapter delves into Go's powerful embedding and composition mechanisms, which allow developers to build cleaner, modular, and reusable code without traditional inheritance. Instead of subclassing, Go enables embedding types within structs and interfaces, promoting fields and methods automatically. You will explore how embedding works, how method promotion simplifies code, and how to handle collisions and method overriding effectively. This chapter also covers how embedding can be used to satisfy interfaces, making your code more versatile and maintainable through practical examples.
Error Handling and Best Practices (Dealing with errors, panics, recover)
Go's approach to error handling is simple but powerful, emphasizing clear control flow over error-prone constructs like `try/catch`. In this chapter, you will learn how to effectively manage errors in Go using idiomatic patterns. The chapter covers basic error handling, creating and returning errors, and best practices such as wrapping errors for better context. You will also explore advanced topics like custom error types, sentinel errors, error chain traversal, modern error inspection with `errors.Is()` and `errors.As()`, custom unmarshal error handling, and the use of Go's `panic` and `recover` for managing unexpected failures. By the end, you'll have a deep understanding of how Go's error model leads to more reliable and maintainable code, while learning when and how to use recovery mechanisms for graceful error handling.
Logging
In Go (golang) release 1.21, the [slog](https://pkg.go.dev/slog) package will be added to the standard library. It includes many useful features such as structured logging as well as level logging. In this article, we will talk about the history of logging in Go, the challenges faced, and how the new `slog` package will help address those challenges. In this chapter, we'll cover the following topics: - Introduce the Slog package that is released in Go 1.21 - Cover the major features being introduced with this logging package