Nathan Wailes - Blog - GitHub - LinkedIn - Patreon - Reddit - Stack Overflow - Twitter - YouTube
Go (Programming Language)
Random things I'm learning
- Apparently Go devs don't like people calling the language "Golang". They want it called "Go". I had a guy on Stack Overflow correct me.
- "There are no reference types in Go."
Reviews of the language
- Google searches to find reviews:
- is go a bad language
- 2015.04.21 - Evan Miller - Four Days of Go
- 2017.08.22 - Michal Konarski - 5 things about programming I learned with Go
- HN discussion
- It is possible to have both dynamic-like syntax and static safety.
- Go is not an object-oriented language. But it does have interfaces. And they are pretty much the same as these you can find in Java or C++. They have names and define a set of function signatures.
- Then we have Go’s equivalent of classes - structs. Structs are simple things that bundle together attributes.
- We can add a function to a struct.
- Go uses a concept called “automatic interface implementation”. A struct containing all methods defined in the interface automatically fulfills it. There is no implements keyword. Isn’t that cool? A friend of mine even likes to call it “a statically typed duck typing”. Thanks to that feature and type inference that allows us to omit the type of a variable while defining, we can feel like we’re working in a dynamically typed language. But here we get the safety of a typed system too.
- It’s better to compose than inherit.
- if we want to mitigate the risk of getting lost inside the dark forest of code complexity we need to avoid inheritance and prefer composition instead.
- Go doesn’t support inheritance at all.
- There is a feature called embedding. Every method in an embedded interface is accessible directly on the struct that the interface is embedded in.
- Channels and goroutines are powerful way to solve problems involving concurrency.
- Don’t communicate by sharing memory, share memory by communicating.
- Instead of using locks to control access to a shared resource, Go coders can simply use channels to pass around its pointer. Then only a goroutine that holds the pointer can use it and make modifications to the shared structure.
- Think about exceptions like they were regular return values.
- there is nothing exceptional in exceptions. They are usually just one of possible return values from a function....it’s good to think about exceptions like they were regular return values. Don’t pretend that they just won’t occur.
- Go FAQ - Why are you creating a new language?
- We wanted the ease of programming of an interpreted, dynamically typed language...
- ...and the efficiency and safety of a statically typed, compiled language.
- Finally, working with Go is intended to be fast: it should take at most a few seconds to build a large executable on a single computer.
- FasterThanLime
- This guy seems to have switched from Go to Rust.
- I want off Mr. Golang's Wild Ride
- Basically he seems to say that when you use Go you can run into problems that Rust will protect you from.
- By now, everybody knows Go doesn't have generics, which makes a lot of problems impossible to model accurately (instead, you have to fall back to reflection, which is extremely unsafe, and the API is very error-prone), error handling is wonky (even with your pick of the third-party libraries that add context or stack traces), package management took a while to arrive, etc.
Over and over, every piece of documentation for the Go language markets it as "simple". This is a lie. Or rather, it's a half-truth that conveniently covers up the fact that, when you make something simple, you move complexity elsewhere. Computers, operating systems, networks are a hot mess. They're barely manageable, even if you know a decent amount about what you're doing. Nine out of ten software engineers agree: it's a miracle anything works at all. So all the complexity is swept under the rug. Hidden from view, but not solved.
- Examples:
- Go's file API will make up a value of the Mode (Linux file permissions, e.g. 755, 666) when you run it on Windows.
- If you use Go to try to run
chmod
on a Windows file, it just does nothing, without raising an exception. - Trying to handle all the possible failure modes when uploading a large file via an HTTP request is extremely frustrating.
Go says "don't worry about encodings! things are probably utf-8". Except when they aren't. And paths aren't.
Of course there's a learning curve [with Rust]. Of course there's more concepts involved than just throwing for loops at byte slices and seeing what sticks, like the Go library does. But the result is a high-performance, reliable and type-safe library. It's worth it.
The Go way is to half-ass things. The Go way is to patch things up until they sorta kinda work, in the name of simplicity.
Over and over, Go is a victim of its own mantra - "simplicity". It constantly takes power away from its users, reserving it for itself. It constantly lies about how complicated real-world systems are, and optimize for the 90% case, ignoring correctness. It is a minefield of subtle gotchas that have very real implications - everything looks simple on the surface, but nothing is. (...) This fake "simplicity" runs deep in the Go ecosystem. Rust has the opposite problem - things look scary at first, but it's for a good reason. The problems tackled have inherent complexity, and it takes some effort to model them appropriately.
- I've lost count of the amount of incidents directly caused by poor error handling, or Go default values.
- I'm tired of being woken up due to the same classes of preventable errors, all the time.
- you cannot replace Go piecemeal once it has taken hold in a codebase
- If you're looking to reduce the whole discourse to "X vs Y", (...) it really is "specifying behavior that should be allowed (and rejecting everything else)" [Rust] vs "manually checking that everything is fine in a thousand tiny steps" [Go], which inevitably results in missed combinations because the human brain is not designed to hold graphs that big.
- Lies we tell ourselves to keep using Golang
- HN discussion: https://news.ycombinator.com/item?id=34188528
- Summary: The Go compiler doesn't catch some very common sources of bugs caused by uninitialized values being given default values by Go, so you need to add code to check for it, which clutters the codebase.
- The author is a platypus
- It doesn't take much skill to notice a problem. (...) feedback ought to be taken under advisement no matter who it comes from.
- Mom smokes, so it's probably okay
- A piece from company X on "how they used technology Y", will very rarely reflect the true cost of adopting that technology. (...) This kind of blog doesn't lend itself to coming out and admitting that mistakes were made. It's supposed to make the company look good.
A lot of the pain in the [TailScale] netaddr.IP article is caused by:
Go not having sum types — making it really awkward to have a type that is "either an IPv4 address or an IPv6 address"
Go choosing which data structures you need — in this case, it's the one-size-fits-all slice, for which you pay 24 bytes on 64-bit machines.
Go not letting you do operator overloading, harkening back to the Java days where a == b isn't the same as a.equals(b)
Go's lack of support for immutable data — the only way to prevent something from being mutated is to only hand out copies of it, and to be very careful to not mutate it in the code that actually has access to the inner bits.
Go's unwillingness to let you make an opaque "newtype". The only way to do it is to make a separate package and use interfaces for indirection, which is costly and awkward.
And yet Tailscale is using it. Are they wrong? Not necessarily! Because their team is made up of a bunch of Go experts. (...) They know how Go works deep down (something Go marketing pinky-swears you never need to worry about, why do you ask?), so if they hit edge cases, they can dive into it, fix it, and wait for their fix to be upstreamed (if ever). But chances are, this is not you. This is not your org. You are not Google either, and you cannot afford to build a whole new type system on top of Go just to make your project (Kubernetes) work at all.
- Go is not adequate for production services unless your shop is literally made up of Go experts (Tailscale) or you have infinite money to spend on engineering costs (Google).
- The good parts
Go is a pretty good async runtime, with opinionated defaults, a state-of-the-art garbage collector, and tooling that would make C developers jealous. This also describes Node.js, and I believe it also describes "modern Java".
- Go's tooling around package management, refactoring, cross-compiling, etc., is easy to pick up and easy to love. Until you reach some of the arbitrary limitations that simply do not matter to the Go team, and then you're on your own.
Evidently, the Go team didn't want to design a language. What they really liked was their async runtime. And they wanted to be able to implement TCP, and HTTP, and TLS, and HTTP/2, and DNS, etc., on top of it. And then web services on top of all of that. And so they didn't. They didn't design a language. It sorta just "happened".
- Just like C, it doesn't concern itself with error handling at all. Everything is a big furry ball of mutable state, and it's on you to add ifs and elses to VERY CAREFULLY (and very manually) ensure that you do not propagate invalid data.
- Go is an island
The Go toolchain does not use the [tools that other languages use] in the interest of interoperability. Go is closer to closed-world languages than it is to C or C++. Even Node.js, Python and Ruby are not as hostile to FFI. To a large extent, this is a feature: being different is the point.
And it comes with its benefits. Being able to profile the internals of the TLS and HTTP stacks the same way you do your business logic is fantastic. (Whereas in dynamic languages, the stack trace stops at OpenSSL).
But it comes at a terrible cost, too. (...) It makes it extremely hard to integrate Go with anything else, whether it's upstream (calling C from Go) or downstream (calling Go from Ruby). (...) Calling Go from anything involves shoving the whole Go runtime (GC included) into whatever you're running: expect a very large static library and all the operational burden of running Go code as a regular executable.
After spending years doing those FFI dances in both directions, I've reached the conclusion that the only good boundary with Go is a network boundary. Integrating with Go is relatively painless if you can afford to pay the latency cost of doing RPC over TCP.
- All or nothing (so let's do nothing)
- Go's compiler won't complain if you initialize a struct without specifying all the attributes, it'll just use the default Go values (0 / False / ""). But that can lead to bugs.
- len(nil) returns zero rather than something like a TypeError like it would in Python. So again, the compiler won't protect you from a significant potential source of bugs.
"Zero values have meaning" is naive, and clearly untrue when you consider the inputs of, like... almost everything. There's so many situations when values need to be "one of these known options, and nothing else", and that's where sum types come in (in Rust, that's enums). And Go's response to that is: just be careful. Just like C's response before it.
- "Rust is perfect and you're all idiots"
The success of Go is due in large part to it having batteries included and opinionated defaults. The success of Rust is due in large part to it being easy to adopt piecemeal and playing nice with others.
- The most hardcore Rust users are the most vocal about issues like build times, the lack of certain language features (I just want GATs!), and all the other shortcomings everyone else is also talking about.
- we could have literally no decent alternative [to Go], and it would still be worth talking about.
- You can, at great cost, write extremely careful Go code that stays far away from stringly-typed values and constantly checks invariants — you just get no help from the compiler whatsoever.
- Go as a prototyping/starter language
Go is an easy language to pick up, and a lot of folks have learned it by now, so it's easy to recruit Go developers, so we can get lots of them on the cheap and just uhhh prototype a few systems? And then later when things get hard (as they always do at scale) we'll either rewrite it to something else, or we'll bring in experts, we'll figure something out.
- Except there is no such thing as throwaway code.
- All the Go pitfalls, all the things the language and compiler doesn't help you prevent, are an issue for everyone, fresh or experienced. Linters help some, but can never do quite as much as compiler for languages that took these problems seriously to begin with. And they slow down development, cutting into the "fast development" promise.
All the complexity that doesn't live in the language now lives in your codebase. All the invariants you don't have to spell out using types, you now have to spell out using code: the signal-to-noise ratio of your (very large) codebases is extremely poor. Because it has been decided that abstractions are for academics and fools, and all you really need is slices and maps and channels and funcs and structs, it becomes extremely hard to follow what any program is doing at a high level, because everywhere you look, you get bogged down in imperative code doing trivial data manipulation or error propagation.
- function signatures don't tell you much of anything (does this mutate data? does it hold onto it? is a zero value there okay? does it start a goroutine? can that channel be nil? what types can I really pass for this interface{} param?)
- The very reason I don't consider Go a language "suitable for beginners" is precisely that its compiler accepts so much code that is very clearly wrong.
- we are doomed to be woken up in the middle of the night, over and over again, because some nil value slipped in where it never should have.
- Here's a list of lies we tell ourselves to keep using Golang:
- Others use it, so it must be good for us too
- Everyone who has concerns about it is an elitist jerk
- Its attractive async runtime and GC make up for everything else
- Every language design flaw is ok in isolation, and ok in aggregate too
- We can overcome these by "just being careful" or adding more linters/eyeballs
- Because it's easy to write, it's easy to develop production software with
- Because the language is simple, everything else is, too
- We can do just a little of it, or just at first, or we can move away from it easily
- We can always rewrite it later
- Yager.io - Why Go Is Not Good
- Uber
- Search Google for "site:https://www.uber.com/blog/ go engineering"
- The Uber Engineering Tech Stack, Part I: The Foundation
At the lower levels, Uber’s engineers primarily write in Python, Node.js, Go, and Java. We started with two main languages: Node.js for the Marketplace team, and Python for everyone else. These first languages still power most services running at Uber today.
- Uber's Marketplace team runs the platform that matches drivers with riders for our ridesharing business and determines dynamic pricing. (Source)
We adopted Go and Java for high performance reasons. We provide first-class support for these languages. Java takes advantage of the open source ecosystem and integrates with external technologies, like Hadoop and other analytics tools. Go gives us efficiency, simplicity, and runtime speed.
We rip out and replace older Python code as we break up the original code base into microservices. An asynchronous programming model gives us better throughput. We use Tornado with Python, but Go’s native support for concurrency is ideal for most new performance-critical services.
- So this would seem to suggest a course of action where you create your MVP with Python and then switch things over to Go as you gain traction...? On the other hand, I've seen people say developing a backend in Go isn't slower than in Python.
We write tools in C and C++ when it’s necessary (like for high-efficiency, high-speed code at the system level).
- How We Built Uber Engineering’s Highest Query per Second Service Using Go
Looking back, we are extremely happy with our decision to Go for it and write our service in a new language. The highlights:
High developer productivity. Go typically takes just a few days for a C++, Java or Node.js developer to learn, and the code is easy to maintain. (Thanks to static typing, no more guessing and unpleasant surprises).
High performance in throughput and latency. In our main data center serving non-China traffic alone, this service handled a peak load of 170k QPS with 40 machines running at 35% CPU usage on NYE 2015. The response time was < 5 ms at 95th percentile, and < 50 ms at the 99th percentile.
Super reliable. This service has had 99.99% uptime since inception. The only downtime was caused by beginner programming errors and a file descriptor leak bug in a third party library. Importantly, we haven’t seen any issues with Go’s runtime.
- Data Race Patterns in Go
- https://stackoverflow.blog/2020/11/02/go-golang-learn-fast-programming-languages/
- I didn't find this blog post particularly well-written.
- Comments:
I did a 9 month project in Go last year. By the end of it I could see some benefits in Go. The main one I saw was how the language constraints help avoid complex models. What you get used to however, is just how much copy and paste and boilerplate exist in a Go project. It was hard to get used to stateful local variables, which experienced programmers know are a source of bugs, Go forces their use while in Java for example you can effectively use final local variables.
Go is verbose, but what I found at the end of 9 months of Go full time was that you start to read go in large stanzas and not line by line. There are very common ways of writing code that while verbose they become so common that your eye can read them quickly and spot oddities.
At the start however this is very uncomfortable. I also became used to copy and pasting without feeling bad, Go just encourages it. I would never write code like that in another language.Overall go is good IMO but the packages, go routines and advanced language features are not as simple to learn and use effectively as you make out.
- The main problem is that the code is very difficult to read. The error handling is hiding logic. When I see a methods implemented in GO they full of noise of checking errors. Those checks hide the valuable logic.
Just watch this https://vimeo.com/97344498- In the talk the guy says that the nice simplicity of code when it's only considering the "happy path" gets marred by lots of error-handling code, making it hard to understand what the function is trying to do.
- He says that in a functional language you can avoid this by combining errors into a single "failure" path.
- Basically he uses something like a Python decorator to modify his functions to take an optional "Failure" argument that, if it's present, will bypass the function and move to the next function in the chain (he's using F#, a functional programming language, so he's doing something like
.then(func1).then(func2).then(func3)
in JavaScript, where each function gets the output of the previous function).
- Go is not a replacement for C. (...) Only an idiot would use Go for performance heavy projects like OSes or game engines.
- i'd like to love go, but the syntax of anything that goes beyond "hello world" is just ugly as hell when coming from a C-style background (C, C++, C#, Java, JavaScript etc.). simply unreadable.
- it's verboisty is initally annoying but also why it's commpeting at an enterpise level. The verbosity adds clarity for future maintainers of unkown skill level. Working on Ruby or PHP projects where previous developers have tried to be "clever" making everything magic, you have a hard time writing obfuscated code in Go.
- Got in to Golang about a year ago and much like Java, I like the strong typed ideals and I like the verbosity. (...) when I need to be able to code across OSX, Linux and Windows with a single tool/util then Golanf is "good enough"
- the blog post does not mention the low memory footprint and fast startup time of Go applications inside containers. These properties really impressed me about Go.
- https://bitfieldconsulting.com/golang/rust-vs-go
- https://betterprogramming.pub/early-impressions-of-go-from-a-rust-programmer-f4fd1074c410
Go vs. other languages
Why Go was developed / what the goals of the language were
- https://thenextweb.com/news/python-vs-go-syndication
- Go at Google: Language Design in the Service of Software Engineering.
- TODO: Read through this
- The computing landscape today is almost unrelated to the environment in which the languages being used, mostly C++, Java, and Python, had been created.
- The problems introduced by [the following] were being worked around rather than addressed head-on.
- multicore processors,
- networked systems,
- massive computation clusters, and
- the web programming model
- Moreover, the scale has changed:
- today's server programs comprise tens of millions of lines of code,
- are worked on by hundreds or even thousands of programmers, and
- are updated literally every day.
- To make matters worse, build times, even on large compilation clusters, have stretched to many minutes, even hours.
- The problems introduced by [the following] were being worked around rather than addressed head-on.
- What programming languages Google's servers were using at the time: "servers mostly in C++ and lots of Java and Python for the other pieces"
the properties Go does have address the issues that make large-scale software development difficult. These issues include:
slow builds
uncontrolled dependencies
each programmer using a different subset of the language
poor program understanding (code hard to read, poorly documented, and so on)
duplication of effort
cost of updates
version skew
difficulty of writing automatic tools
cross-language builds
- "we have had extensive experience tracking down build and test failures caused by cross-language builds where a Python snippet embedded in another language, for instance through a SWIG invocation, is subtly and invisibly broken by a change in the indentation of the surrounding code. Our position is therefore that, although spaces for indentation is nice for small programs, it doesn't scale well, and the bigger and more heterogeneous the code base, the more trouble it can cause. It is better to forgo convenience for safety and dependability, so Go has brace-bounded blocks."
- "The primary considerations for any language to succeed in this context are:
- It must work at scale, for large programs with large numbers of dependencies, with large teams of programmers working on them.
- It must be familiar, roughly C-like."
Go vs. dynamically-typed languages (Python, Ruby)
Advantages of Go
- Go's static typing makes it easier to catch bugs, easier to refactor, and easier to collaborate in large teams and large codebases, because you get notified immediately when some change you've made isn't going to work with code elsewhere in the codebase.
- "Python is a dynamically typed language, and as such it can present, er, challenges for working on large programs, in large teams. A quick example, is that if you make a function in Python to call, and call it from a few places, you’ll find that if you change the number of parameters, or types of parameters that the function takes, there is no compile time error, only a runtime error. Now that’s no big deal on a small program of only a few hundred or even few thousand lines, but once you go up to hundreds of thousands of lines, or millions of lines, working with hundreds of other people, it’s a major problem. Statically typed language like Go turn that runtime error into a compile time error, and it’ll point out each and every time that function is called with the wrong parameters. This is a huge difference, when working on medium or large scale projects." (Source)
- "The largest dynamically-typed codebase I have worked on thus far is GitLab, which consists of about 700 000 lines of code, of which about 420 000 lines are written in Ruby. (...) After working on it for little over six years, I firmly believe a statically typed language would have saved us a ton of effort and more importantly bugs. We've had a lot of bugs over the years along the lines of "Under this weird condition we call non-existing method X on type Y". (...) As much as I dislike Go, it's probably the language I would pick if I were to write GitLab today; at least when it finally gets generics. As much as I would like to pick Rust, I feel it just isn't quite there yet for web development, and the compile times would likely be through the roof. I'm also not a fan of the async APIs in Rust, whereas with Go this is baked into the language (as it should be in my opinion)." (Source)
- Go is faster than Python, both because it's compiled and because it handles concurrency better.
- "Python is very good at making programming simple and easy, but it suffers a performance hit compared to compiled languages. Compiled languages such as C or C++ are very fast, but they're not as simple and easy to use as Python. Go aims to be almost as easy to use as Python while being compiled, and almost as fast as traditional compiled languages. It's also very good at concurrency by design, which is its main strength."
- Maybe it’s a messaging, caching, computational-heavy data analysis, command line interface, logging or monitoring. I don’t know what label to give it, but over the course of my career, as systems continue to grow in complexity and as concurrency frequently measures in the tens of thousands, there’s clearly been a growing need for custom infrastructure-type systems. You can build such systems with Ruby or Python or something else (and many people do), but these types of systems can benefit from a more rigid type system and greater performance. (The Little Go Book)
- Go programs can be easier to run on other machines because they're self-contained executables rather than requiring the machine already has the language installed.
- "You don’t have to worry if your users have Ruby or the JVM installed, and if so, what version. For this reason, Go is becoming increasingly popular as a language for command-line interface programs and other types of utility programs you need to distribute (e.g., a log collector)." (The Little Go Book)
- "in my opinion Go is a much better option for distributing a program across multiple platforms" (Source)
- "Generally my determining factor for if I want to pick python or go for a project is "how many platforms will this have to be distributed to and will humans be using it?". If the answer is "at all" I generally tend to reach for go. I know there's ways to statically compile python and I've done that for a few things, but go is extremely handy for distribution." (Source)
Problems with Go
You'll have to do a lot of things by hand, or with a worse/unreliable package, that you wouldn't if you were using another language with a full-featured framework.
- "The language is simple meaning it doesn’t offer you any of the niceties other languages do. (...) The frameworks are similarly minimal, there is nothing similar to rails or express or django or spring. You’ll have to do a thousand things by hand that you wouldn’t with those languages." (Source)
- "Batteries included: argparse, pty, locale, configparser, json, uuid, http. All of these excellent modules are used by asciinema and are part of Python’s standard library. Python stdlib’s quality and stability guarantees are order of magnitude higher than of unversioned Go libs from Github. (...) Casting int32 to int64 to… gets old fast." (Source)
You end up with a lot more code.
"you can use Go to build websites (and many people do), but I still prefer, by a wide margin, the expressiveness of Node or Ruby for such systems." (The Little Go Book)
"Error handling is simple but it’s a PITA for anybody coming from another language. It’s annoying, verbose, and makes code hard to read. The language is simple meaning it doesn’t offer you any of the niceties other languages do. Everything you do takes five to ten times more typing than Python or typescript or Ruby or kotlin. (...) Frankly I would not even recommend go for a web back end. It’s a lot of pain for very little gain. (...) The pain is at least ten times more code you’ll have to write, read, understand and maintain." (Source)
"Python is high level language while Go is lower level language (I think it’s fair to say it’s C 2.0). (...)
if err != nil {
gets old [fast]. (...) Go’s static type system with type inference and functions as first class citizens felt like a nice bonus. In reality, the lack of generics forces you to write lots of boilerplate and repetitive code. 20 lines of boilerplate, imperative code is not simpler and easier to understand (like some Go defendants claim) than 2 lines of higher level code because it adds noise to the essence of algorithm. When reading code you don’t need that level of granularity in most cases." (Source)- "Lets talk about serialization of JSON (or anything, for that matter) into structs and how having to write a struct for anything you plan on serializing into a go object is tedious. I love go a lot but holy shit am I not a fan of having to write out cooky nested structs for JSON." [Other person replies:] "This is my second big complaint. I have a huge list of API endpoints to interact with and the company isn't exactly consistent with the output. It's also an undocumented API. You can see where this is going..." (Source)
- Counter-point: There are people on Reddit who say they don't feel it's any slower to work in Go than in Django.
List of links saying Go is better than Python/Ruby
- https://josvisser.substack.com/p/why-python-is-terrible
- The number one reason I get for people wanting to use Python is that it is "easy to use". Apart from the fact that this is not true, it is also a fallacy that shows a complete lack of understanding of how Total Cost of Ownership (TCO) works. For any serious piece of software that makes it to production, 80% or more of the cost is in the phase after initial development. That's the phase where we run, debug, add features, and refactor. Who cares that the first 20% was twice as fast if the other 80% is a hellhole of debugging and finding problems that any compiler from the 1960s could have caught?
Personally I switched all of my casual programming to Go. It's almost as easy to write as Python, is type safe, has a fast build system, and generates highly optimized native code binaries. Of course Go is not perfect (hint: no programming language is), but it is much better than Python if you want to write code that is reliable, fast, and where you have at least some chance at debugging and refactoring when the code has sprawled out of control.
List of links from people saying Go isn't so great
- https://www.yosefk.com/blog/things-from-python-id-miss-in-go.html
- HN discussion: https://news.ycombinator.com/item?id=7872153
- https://blog.asciinema.org/post/and-now-for-something-completely-different/
- https://news.ycombinator.com/item?id=11320908
- [Regarding an article titled "10M Concurrent Websockets" from goroutines.com:] Sorry, but this is pointless. And I have a feeling that you could do better even with a scripting language like Perl, Python, Ruby and a single-threaded event loop. [Later person comments:] Yes, exactly. Which is why we switched from Go to Python (using twisted), and dropped our memory use substantially for holding open hundreds of thousands (and soon millions) of websocket connections. M:N schedulers and the primitives they use for their 'lightweight' threading are always going to use more memory than a single-threaded event loop.
- C# is a great language, the real issue is .NET (the framework). (...) The long term viability of a .NET project is very fragile has they are constantly breaking things from version to version and updated version can appear as quick as 1-2 years after.
Go vs. other statically-typed languages (C#, Java)
Go vs. C#
- r/golang - c# vs go
- in my opinion the biggest problem with .net isn’t the language, but rather its association with widows. Managing large projects on windows just sucks, really. (...) The other thing about .net is the community. (...) I interview a lot of .net devs and they just seem to have a kind of Microsoft centric tunnel vision. If MS didn’t publish it, it doesn’t exist.
- We used Go for some customized ETL automation. It reminded me of Node.js. While it did the job, I moved to a different company rather quickly because I missed C#. I missed Entity & LINQ as well as all the other great features of .Net that are built-in at your disposal. Between the two, I believe C# is best for enterprise apps as long as you know how to design the entire stack well.
C# seems to be just a much more fun and feature rich language to use, especially when it comes to eliminating boilerplate.
- One main advantage with Go (and Rust), both being "new" languages, is the lack of inheritance and choosing composition over inheritance, which I think is one of the main things that messes up codes in Java and C#.
- We went with golang and language wise, it felt like it was a downgrade coming from c#. Although i do like the fast compile times, channels and goroutines, and small binary output.
- C# has more features for sure. It’s also has a huge ecosystem. (...) Many of C# APIs just work better. For example, the File API. There’s nothing in golang that compares to LINQ (...) I also like how you can just inject services in C# versus golang. It has a much better dependency injection system and it’s not even close. (...) C# is much, much more mature than golang. (...) One thing I really like about golang is the support for grpc and protobuff. (...) It’s easier to understand go code because it’s so simple and straightforward. I also prefer error over exceptions being thrown. Not having try/catch and always checking for errors also helps you not swallow exceptions. Because goroutines only use 2kb, you require less resources to run large systems where as in c# it’s almost 1mb
- Go literally has the best implementation of concurrency I've ever seen, that's like 50% of the reason I use Go. Go is kind of known as the fancy new concurrency language. (...) Basically Go achieves the results of high-level C++ code while being easy to use all the way from creating a project to deployment. And that's a hell of an achievement.
If you’re working high level services and business logic - use C#. Only consider golang if the task at hand also makes you consider c/c++. (...) their implementation of pointers is super confusing unless you've really mastered pointers in C/C++ (...) In short Golang is great at what it was created for: infrastructure code. I'm not sure it's so good at the other things, and it has way to many gotchas and hidden pitfalls to be "kind" to a new coder - take the mechanism of the pointer within a slice, for example. I'd point new coders to JavaScript, first - to get them familiar with non-strictly typed OO, first, and then move towards typescript. From there, I'd move them towards a more "concrete" backend language, like C#, or even C or C++. Heck - even Python. (...) In addition to learning Golang's syntax, you must also learn what is idiomatic, and why - which is a lot to swallow for someone who is just trying to understand what a variable is.
- r/csharp - c# vs go
- As much as I love Go it's strengths are kind of in niche areas. It's great to have an executable without a big runtime but the coverage of .Net Core and C# for just about everything you would ever want to develop is hard to beat. I've spent a lot of time learning Go, Elixir and Rust in the last few years and still end up using the .Net ecosystem for most solutions.
- the only C# limitations I'm aware of are related to mobile dev.
- C# has a heavier and more complicated runtime and has MUCH larger assemblies for self-contained deployments. Go also has a lower latency GC. This difference has let Go eat a bit into C#'s market for serverless functions, densely hosted and short duration microservices, and command line tools.
- When I started looking, I just realized that Go doesn't have the strength in numbers that I see built around C#.
- I'm not sure most people here understand how difficult Macs are to integrate into an existing Windows based IT infrastructure [Later person comments:] I work (currently) in a bank and there are plenty of backend devs using Macbooks.
I have my first project in Go right now after a long time in C#, and to me it would be hard to be enthusiastic about this change. A lot of the more civilized niceties are just not there. Quick build times and small binary sizes are nice. Being close to the metal might be nice, depending on what you're doing. But most else about it feels tedious.
- There's actually a chance that Google will eventually stop working on Go, as they are known for doing that sort of thing. (...) The Go team is something like 17 people. Let's say that pay them $1M a year, it wouldn't be a $20M project based on just salary. But .Net is $4B project and has been for 20 years now. (...) Since Go has less then 1% of 1% of the market today, there's some risk in adopting it.
- .NET hands down. Between the (usually) well written standard library and constant stream of useful language improvements, nothing else comes close.
- I found [Go] incredibly tedious to write
- I had to use Go for a Grafana backend, it was an atrocious experience. It just isn't there for productivity imo.
Channels and goroutines are kinda nice, but don't really give you more than similar constructs in c#.
Defer is.... idk.. not really any better than finally. It keeps cleanup with declarations, which is nice, but out of order from an idiomatic perspective, which isn't.
Error handling is atrocious and really demonstrates the reality of what the "just use tuples/options" crowd want. Every fucking call is
res, err = ...
followed byif err != nil return nil, err
kinda shit -unless you use the whole panic/recover setup which is clunky af.No fucking generics. This is a huge pain in the ass for dealing with reactive extensions. Worse, the opinionated formatter will turn an inline cast of like
.struct{foo,bar}
into FOUR lines. [Generics were added to Go in November 2022]
I like [Go] for small things. Little apps and programs that patch together code that needs to be performant. I just cannot see how go can stretch to enterprise like c#.
[Go's] core strength make it a good choice for only fraction of use cases compared to languages like C#. (...) For example, while you technically can create a client/UI app in Go, C# would be a much better choice for basically all platforms: web, desktop, mobile.... On the other hand, anything server related, ie anything that processes a lot of different requests in parallel, Go doesn't just 'look good', it outshines because of the lightweight nature of goroutines
- I prefer more expressive languages that allow the writer to pursue a more optimal balance of readability and conciseness than you get with Go.
I did C#/Windows for about 15 years as my primary language. Around 7 years ago I moved to Go/Linux. I still do some C# as my company has legacy applications in it. Personally, I love Go/Linux and I find it very frustrating having to pick up Windows/C# stuff now. Everything is just quicker, leaner, more explicit and ‘mechanical’. C#/Windows feels bloated, over abstracted, full of fluff and indirection. A couple of people have commented that you can do more in C# but I’m not sure what they’re referring to specifically. (...) C#/Windows is all IOC, DI, abstract factory patterns and blah blah blah.
- Although in performance tests that I have seen, C# REST API performance in terms of request/sec can be nearly identical, the resource consumption is far greater than Go.
- I did not like working with Go at all. There’s tons of help online for things on C# but I struggled to get help with Go online.
I basically worked for about 10 years in the .NET/C# world starting from my first internship out of college and switched jobs about 6 months ago. I am a C# fanboy and for the most part have really enjoyed learning and working in Go.
Things I like:
Very explicit "style guide" -- there's a "Go"-way of doing just about everything. Which makes writing quality code that's easy to maintain much easier than if you were making the opposite switch (Go →C#).
Way less verbose than c# (this can backfire at times, but in general it's nice)
Fast and lean - hard to overemphasize this: everything from builds to running tests is just very very streamlined
Implicit interfaces - love this feature
Testing in Go is much more integrated and feels simpler to do than in C#
Static typing and autocompletion just like we're used to and love in the C# world
Things I don't like:
Package management is not as easy as in C#, sometimes you run into weird dependency chain issues
LINQ/Lambdas -- in my current job I find myself working with collections and databases way less than my previous jobs, so I haven't really had to look into this, but creating for loops to iterate over everything does feel a bit weird sometimes. Although, I will say that this has the side effect of making code more readable. We've all seen some lambda/linq-statement horrors and I don't miss having to decipher those.
Concerns I'd Have if I was in your position:
UI development - I don't work with UI, but it definitely feels like Go's wheelhouse is backend development. If you guys have frontend products, you're going to have a bad time migrating that to Go, I think.
Like I mentioned earlier, there's a "Go"-way of doing things, this is great when you have people around you that know the language and you have their code to look at, but if you're switching as a company and everyone is a newbie, your code might be a mess.
- I’ve professionally written go at my last job and now professionally write c# at my current job. In my extremely limited scope, go was great for small, lightweight processes which are compiled into a single executables. IMO, serverless (although we didn’t use it) is about the perfect use case for go. Anything else, it’s a total toss up that I’d lean towards c#.
Go vs. Java
How to best use Go
IDEs
- GoLand by Jetbrains
- I love PyCharm so I'm inclined to use the Jetbrains IDE for Go. A bunch of people on Reddit seem to say it's the best Go IDE available.
- Apparently the Beta versions are free / don't require a subscription when they're active: https://www.jetbrains.com/go/nextversion/
- https://medium.com/@dlsniper/make-the-most-out-of-goland-cb8977443242
- Reviews / mentions:
- Most our Go devs use Goland (Jetbrains) (Source)
- VS Code
- A lot of people seem to like VSCode because it's free, but when I used it I found it annoying how much setup I needed to do, and I just remember finding the UI clunkier than PyCharm.
Lists of libraries/tools
Application structure
- https://go.dev/doc/modules/layout
- https://github.com/avelino/awesome-go?tab=readme-ov-file#project-layout
- https://www.calhoun.io/flat-application-structure/
- https://www.calhoun.io/why-cant-we-settle-on-a-single-application-structure-in-go/
- https://eli.thegreenplace.net/2019/simple-go-project-layout-with-modules/
Web frameworks
Working with databases / ORMs
- 2019 - Ask any Go developer and they will tell you how painful it is to write programs that talk to SQL databases. Go feels like it isn't even half as productive compared to working with toolkits like SQLAlchemy, Diesel, Hibernate or ActiveRecord. The existing tools in the Go ecosystem force application developers to hand-write mapping functions or litter their code with unsafe empty interfaces. (src)
- Should you use an ORM with Go?
- https://www.reddit.com/r/golang/comments/t3bp79/a_good_orm_for_golang/
Yes:
I've worked on implementing a bigger project in go (4 devs, 2 years of work) following the "don't use an ORM in golang" advice and we severely regret it. In this project, more than 9/10 queries are simple CRUD statements that could have been completely solved by using an ORM. They took a long time to write and there have been multiple bugs in that part of the application. The rest are a mix of queries that could have been realized with a simple query builder or by writing simple SQL statements, utilizing the underlaying database's (pgsql) native features. tl;dr: if you use a good ORM, it'll work with you, not against you. it can save you time and a lot of boilerplate code.
- ORMs are very beneficial and they take care of a lot of things you need everywhere and for free. It doesn't mean you have to use all of the ORM's features, but just using it as a mapping and validation framework is a win. They also force a way to deal with transactions, entity caching etc which is very helpful and may benefit performance greatly. (...) Almost always it's a mixed bag with any technology.
- I don’t understand the hate for ORM. A good ORM can reverse-engineer your db into entities for your favorite coding language, giving you type safety and thus the ability to refactor without concern for all the magic strings in your SQL everywhere. How is this not a win?
No:
- From my experience, ORM-generated SQL-queries much more slower than raw SQL-queries or builder-generated queries (squirrel, for example). ORMs are useful to develop a student project or if you have a task to develop a backend application in one month. Maybe it will save your time, yeah. But it will be a headache in the real production world, when you will try to optimize your query.
- In my case - [GORM is] super memory and cpu intensive (but we handle more than 30m req/day).
- I don't like ORMs because they introduce a lot of API overhead and often leave you high and dry or struggling some point down the line with something that would have been trivial had you not introduced an ORM. (...) my requirements for a database package are:
Doesn't introduce a lot of new or unnecessary types a la sqlx or most builders
Works close to database/sql and maintains your access to it
Handles the tedium of SELECT scanning
Handles the tedium of basic CRUD or model operations
- I write my SQL queries by hand and let sqlc or pggen generate them.
TLDR: with ORMs it's still easy to make problems the compiler can't check: The Go community has produced higher-level libraries (github.com/jmoiron/sqlx) and ORMs (github.com/jinzhu/gorm) to solve these issues. However, higher-level libraries still require manual mapping via query text and struct tags that, if incorrect, will only fail at runtime. ORMs do away with much of the manual mapping but require you to write your queries now in a pseudo-sql DSL that basically reinvents SQL in a set of Go function calls. With either approach, it is still trivial to make errors that the compiler can't check. As a Go programmer, have you ever:
Mixed up the order of the arguments when invoking the query so they didn't match up with the SQL text
Updated the name of a column in one query both not another
Mistyped the name of a column in a query
Changed the number of arguments in a query but forgot to pass the additional values
Changed the type of a column but forgot to change the type in your code?
(src)
- https://www.reddit.com/r/golang/comments/t3bp79/a_good_orm_for_golang/
- Which ORM / SQL Builder should you use?
- ORMs
- SQL Builders
- goqu
- goqu is an expressive SQL builder and executor
While goqu may support the scanning of rows into structs it is not intended to be used as an ORM; if you are looking for common ORM features like associations, or hooks I would recommend looking at some of the great ORM libraries such as gorm or hood
- sqlc
- Introducing sqlc - this is a good blog post
- How to use sqlc in 3 steps
- You write SQL queries
- You run sqlc to generate Go code that presents type-safe interfaces to those queries
- You write application code that calls the methods sqlc generates
- sqlh / SQL Helper
- Seems like an abandoned project.
- Author explanation of why he created it
Doesn't introduce a lot of new or unnecessary types a la sqlx or most builders
Works close to database/sql and maintains your access to it
Handles the tedium of SELECT scanning
Handles the tedium of basic CRUD or model operations
- The development of
sqlh
is essentially following my specific pain points when usingdatabase/sql
- goqu
Testing
Authentication
- https://www.jetbrains.com/guide/go/tutorials/authentication-for-go-apps/auth/
- This was a helpful guide.
- Let's Go Further
- He implements bearer-token-based authentication, where you give the user a token (short string) that you then need to look up in the database for every request to see which user it was associated with. The Jetbrains guide above explains that JWT-based authentication can avoid the db lookup but is more complicated to set up.
- Discussions
Cool Go projects
Learning resources
- Aggregations of learning resources
- Intros to the language
- A Tour of Go (the official Go tour) - See later on this page
- Official 'How to Write Go Code' tutorial
- Learn X in Y Minutes - Go
- How I Start - Go
- Learn Go with Tests
- Go by Example
- Go for Python Programmers (unfinished)
- Go vs. Python for Senior Developers
- This is great.
- Summary:
- Similarities to Python:
- A large standard library.
- Automatic memory management.
- A huge ecosystem (libraries, frameworks, IDEs, and other tooling).
- Differences to Python:
- Go is a compiled language
- Better performance
- Easier deployment and cross-compilation
- Go is statically-typed
- Go supports call by reference (via pointers)
- "Python is call-by-reference for dicts/lists/objects but doesn't support call-by-reference for primitives." But so what? I haven't run into a situation where that mattered.
- Go doesn't have classes or "class implements interface" syntax
- Go (officially) does not support sub-classing / inheritance
- Go does not have exceptions
- Go uses
defer
instead oftry-finally
- It's just different syntax. - Go has decentralized dependency management
- import statements in Go typically look like this:
import "github.com/some-user-or-org/some-go-module/some-go-package-that-you-need"
- The equivalent to Python’s
requirements.txt
is Go’sgo.mod
file.
- import statements in Go typically look like this:
- Go has excellent support for concurrency
- "Goroutines are a bit like threads, but much more efficient"
- "Python’s support for concurrency is abysmal. While you can have multiple threads, they won’t make proper use of your CPU cores (due to the Global Interpreter Lock). If you want to use multiple CPU cores, you must fork extra processes."
- Go comes with built-in, opinionated formatting
- Similarities to Python:
- YouTube - Google I/O 2010 - Go Programming
- Creating web apps
- Let's Go (see below)
- Let's Go Further (see below)
- Web Development with Go by Jon Calhoun
- Intermediate topics
A Tour of Go
- A Tour of Go (the official Go tour)
- I'm going through this after having gone through the Udemy course "Go: The Complete Developer's Guide".
- Summary of stuff I want to remember from the tour:
- Packages, variables, and functions
- gofmt is a tool that can automatically format Go code.
- The term for when you have a bunch of imports with one "import" statement and all within a single pair of parentheses is a "factored import statement".
- A name (of a variable, function, etc.) is exported from a package (i.e. public) if it begins with a capital letter.
- Types are listed after the variable name (e.g. myVar int), which is different from C (int myVar); this blog post explains why: https://go.dev/blog/declaration-syntax
- The
var
statement declares a list of variables; as in function argument lists, the type is last. Example:var c, python, java bool
Outside a function, every statement begins with a keyword (var, func, and so on) and so the := construct is not available.
- Variable declarations may be "factored" into blocks, as with import statements.
var ( ToBe bool = false MaxInt uint64 = 1<<64 - 1 z complex128 = cmplx.Sqrt(-5 + 12i) )
- Variables declared without an explicit initial value are given their "zero value" rather than None or nil. 0 for ints, False for bools, empty string for strings.
- Constants are declared with the
const
keyword and =, they cannot be declared using the := syntax. Example:const Pi = 3.14
- In Go, numeric constants have high precision and don't initially have a set type. Their type is determined based on the context in which they're used. This means if a constant is used in a situation where an int is expected, it becomes an int. If it's used where a float64 is expected, it becomes a float64. This flexibility allows for more generic and adaptable code, reducing the need for type conversions and making the constants more versatile.
- Flow control statements: for, if, else, switch and defer
- There's no
while
keyword, you just usefor
with no "init" or "post" statements. - You can even omit the check to do an infinite loop (so the equivalent of
while True:
in Python is justfor {
). - New to me: for and if statements can start with a short statement to execute before the condition is checked, like this:
if perr, ok := err.(*os.PathError); ok {
- NW: I looked up why someone might want to do this and it's because a variable defined in that statement will have its scope limited to that block: https://stackoverflow.com/questions/24317951/why-use-a-statement-inside-an-if-statement
- Variables declared inside an if short statement are also available inside any of the else blocks.
- Go has a
switch
statement, it doesn't do fall-through, it only runs the code in the matching block. - You can either define a value in the
switch
statement that should be compared to a value in eachcase
statement or you can just do "switch" by itself and then have eachcase
statement be a boolean comparison/function/whatever.- They say the latter can be a clean way to do long if/else chains.
- New to me: A
defer
statement defers the execution of a function until the surrounding function returns. The deferred call's arguments are evaluated immediately, but the function call is not executed until the surrounding function returns.- If you have more than one defer statement, they get put on a stack that get called in LIFO order.
- More info: https://go.dev/blog/defer-panic-and-recover "Defer is commonly used to simplify functions that perform various clean-up actions." For example, "By introducing defer statements we can ensure that files are always closed".
- So basically
defer
is how Go accomplishes the same thing as thewith open(...):
context manager block in Python. - "Deferred functions may read and assign to the returning function’s named return values." "This is convenient for modifying the error return value of a function"
- So basically
- There's no
- More types: structs, slices, and maps.
- Packages, variables, and functions
Go: The Complete Developer's Guide
- https://www.udemy.com/go-the-complete-developers-guide/learn/v4/content
- This was the highest-rated intro to Go on Udemy at the time I bought the course, but it looks like a bunch more good ones have been released / upvoted since then.
- I liked it, I would recommend it to anyone who wants to learn Go.
- I think the main thing I found less-than-ideal about it was that I'm already an experienced Python developer and so I found the pace slower than it needed to be for me. But I can see how this pace would be perfect for someone who isn't as familiar with programming. So basically it doesn't leave anyone behind. But I feel like I would've benefited from seeing him digging through some more-complicated programs, like the Axis & Allies simulator, to get a sense of how to work with stuff like that.
Summary of key ideas from the course I want to remember
- An executable package (in contrast to a non-executable library) must be named
main
and have a function namedmain
. Hello World:
package main import "fmt" func main() { fmt.Println("Hi there!") }
- Use
go run <files-to-compile>
to run your code. - The
:=
syntax is an abbreviated syntax used when defining a variable for the first time; it has the variable infer its type from its initial value (which must be assigned on the same line). - Instead of using the Python syntax for creating classes, you create types (which have no inherent methods attached to them), and then to create methods you create receiver functions (functions which only accept the new type as one of their arguments).
- Example type declaration:
type deck []string
- Example receiver declaration:
func (d deck) print(<args go here>) { <code goes here> }
- Note how the receiver part is in front of the function name and separate from the parameter list.
- Example receiver usage:
my_deck.print()
- Example type declaration:
- A lot of learning Go is learning how the various packages in the standard library work.
- Go has arrays (fixed-length) and slices (variable length, similar to Python lists):
- Create an empty array:
cards := [<size>]string
- Create an array with initial values:
cards := [<size>]string{<put-initial-contents-here-if-you-want>}
- Create an array with initial values:
- To create a slice just omit the <size>:
cards := []string{<put-initial-contents-here-if-you-want>}
- Append to a slice:
cards = append(cards, newElement)
- Append to a slice:
- Selecting a subarray/subslice works just like Python.
- The code
[]byte
is read as "slice byte", not "array of bytes". - Arrays are rarely used directly; slices are used 99% of the time for lists of items.
- We can use
len()
to get the length of a slice.
- Create an empty array:
- For loop syntax:
for i, card := range cards { <do-stuff> }
- If you're not going to use the iterator replace its name with an underscore:
for _, card
- If you're not going to use the iterator replace its name with an underscore:
- Unlike Python, you don't need to add import statements for stuff that's defined in the same package but in separate files.
- To convert between types, write the type you want and put the object to convert in parentheses after it:
[]byte("Hello World!")
- This is similar to Python, where you have str(), int(), class constructors like MyClass(), etc.
When importing multiple packages in a file, use this syntax:
import ( "package name one in quotes" "package name two in quotes" )
- Golang uses
nil
, notnull
orNone
. - You can do
Println(err)
to print an error. - To immediately quit a program we can call the
os
package functionExit()
with an argument of1
which indicates that something went wrong. - Random number generation is kind of complicated in Go.
- Testing:
- Make a file that ends in
_test.go
and then rungo test
- The first argument to every test function should be
t *testing.T
- Call
t.Errorf("Description of problem")
to notify the test handler of a failure.
- Make a file that ends in
- Structs are like dicts or named tuples or classes-without-methods in Python: a collection of attributes describing a single thing.
- The values can be changed afterwards, unlike named tuples.
- The fields can't be added to afterwards, unlike a dict.
- How structs are different from Go "types":
- Most types you'll be creating in Go will be structs, but a type doesn't have to be a struct; it can be a slice, primitive type, etc.
- So to use structs, you'll generally have a line of code that defines a new type as being a struct, and then you'll have another line of code that instantiates the new type and assigns it to a variable:
type Card struct { Value string Suit string } type Deck struct { Cards []Card } deck := Deck{Cards: []Card{{Value: "Ace", Suit: "Spades"}}}
- How values are passed to functions:
- Go is a "pass-by-value" language by default. It will copy the object and pass the copy to a function.
- You can use pointers to pass the original object to a function rather than a copy.
- To create a pointer use the
&
operator:myStructMemoryAddress := &myStruct
- To dereference a pointer use the
*
operator:myStruct := *myStructMemoryAddress
- When you see * in a type name (for example
*person
), it isn't being used as an operator, but as part of the name ("pointer-to-X"). So any variable of that type will be a pointer instead of a dereferenced pointer.- This could be very confusing for some people.
- If we define a receiver function of type pointer-to-X, Go will allow us to call it either with a pointer or with the referenced type itself.
- This is helpful when we want a function to modify the type instance it's called on, because it lets us skip defining a pointer variable before calling the function.
- To create a pointer use the
- Slices and maps are "reference types" and are tricky because the reference type instance itself is a different thing in memory from the underlying data. So if you don't want a function to modify the original underlying data, you need to first make a copy of the reference type object in such a way that also copies the underlying data before sending it to the function.
- Structs are not reference types, but if a struct contains a reference type (slice or map) as one of its attributes, it'll have this issue, so you'd need to do a deep copy of the struct to avoid this problem.
- Maps:
- They're like dicts in Python, except:
- The keys and values must all be of the same type within their respective set (so, all keys need to be the same type as each other, and all values need to be the same type as each other).
- Defining a map:
my_map := map[string]string { }
- Iterating over a map:
for keyVar, valueVar := range myMap {
- They're like dicts in Python, except:
- Interfaces
- Interfaces allow us to write functions that can be used by multiple different types.
Example interface:
type myInterface interface { myRequiredFunction(string) string }
- Any type that has all of the functions specified in the interface definition (including exactly matching the input types and return types) will thereby be treated by the Go compiler as a member of the interface type as well, and be able to use any functions defined for that interface type.
- In Go, interface types can't ever be directly instantiated, whereas "concrete types" are those that can be.
- The Reader interface is a common interface in Go; it lets us take input from any of various sources as a slice of bytes.
- He goes through the line of reasoning that would lead us to understand that
os.Stdout
would work as aWriter
type:os.Stdout
is of typeFile
, and typeFile
has a function of typeWrite
, and since we know that any type that implements that function (including the required parameter types and return types) is of typeWriter
, we therefore know thatos.Stdout
would be accepted byio.Copy()
.- He describes knowing how to do this as "hard-won knowledge".
- Go routines & channels
- Go routines
- They're like Python threads (in that you can communicate between them) except they can be assigned to multiple cores like when doing multiprocessing (my understanding is that Python threads need to be on the same processor to be able to communicate to each other). So basically they have the ease-of-communication of Python multithreading but the speed of Python multiprocessing.
- To run a function as a Go routine just add the
go
keyword before the call:go doMyThing()
- Channels
- Channels are a way to communicate between Go routines; think of them as a messaging system.
- To create a channel:
c := make(chan string)
- Channels need to specify the type of the messages they'll be handling.
- To push or pop a message to/from a channel, use the <- operator: myNumber <- myChannel
- Always pointing to the left, never
->
- Always pointing to the left, never
- Receiving a message from a channel is a blocking operation (the program will wait until a message arrives).
- Go routines
- Function literals are equivalent to anonymous functions in JavaScript or lambda functions in Python and are straightforward to define:
func(<args>) { myCodeGoesHere() }
Section 1: Getting Started
How to Get Help
- No video, just text.
- There are three ways to get help:
- Post in the Q&A discussion
- Email me: <email address redacted>
- Message me on Udemy.
Link to Completed Code
- No video, just text.
- You can find the completed version of all the examples in this course here: https://github.com/StephenGrider/GoCasts
Environmental Setup
- We are going to install Go, then we'll install VSCode (an IDE), then we'll configure VSCode, then we'll write some code.
- He highly recommends trying VSCode because it has the best integration with Go.
- To install Go, go to golang.org/dl
- Just keep hitting "Next" for the installer, there's nothing we need to configure.
VSCode Installation
- To verify Go was installed correctly, start a terminal / command prompt window and type "go" and hit Enter. You should see a help message show up on the screen.
- To install VSCode, go to code.visualstudio.com
Go Support in VSCode
- VSCode doesn't start out with any support for Go.
- To add this support, go to View → Extensions, and search for "go".
- Download the extension with the title "Go", the description "Rich Go language support..." and over 1 million downloads.
- Restart VSC.
- Make sure you have a code editor window open (do File → New File if you don't).
- In the bottom right corner, click "Plaintext" and switch it to "Go", and in the same place where "Plaintext" showed up you'll see an error saying "Analysis tools missing". Click the error message and then click "Install" on the dialogs that pop up.
Section 2: A Simple Start
Boring Ol' Hello World
- We're going to start with a simple "Hello World" example in this video but in the next few videos we're going to do a deep dive on how Go is working behind the scenes.
- He goes to File → Open and creates a new folder named "helloworld" to house this project.
- NW: In my editor I didn't have an "Open" option, just "Open File" and "Open Folder", so I chose the latter.
- You should see the folder show up in the left sidebar (the "Explorer tab").
- He clicks a little "Add file" button next to the name of his project in the Explorer tab and names it "main.go".
- He writes out the code and says we'll discuss each line in-depth in the next few videos:
package main import "fmt" func main() { fmt.Println("Hi there!") }
- He says you must use double-quotes, not single quotes, in both the import statement and the Println statement.
Five Important Questions
- He studied the Hello World program ahead of time and came up with five basic questions we can ask that will give us a good sense of what the program is doing:
- How do we run the code in our project?
- What does 'package main' mean?
- What does 'import "fmt"' mean?
- What's that 'func' thing?
- How is the main.go file organized?
- He then starts answering question 1.
- He switches to a terminal navigated to his project folder.
- He runs
go run main.go
- NW: When I tried this I ran into a minor problem in that my code had not been auto-saved like it is in PyCharm, so it didn't work until I figured out that that was happening.
- He shows several other Golang CLI commands that we'll be using:
go run
is used to compile and immediately execute one or two files, without generating an executable file.go build
compiles, but does not execute, go files. It will generate an executable file.go fmt
formats all of the code in each file in the current directory.go install
compiles and 'installs' a package.go get
downloads the raw source code of someone else's package.go test
runs any tests associated with the current project.
Go Packages
- We're going to talk about what "package" means and why we chose the name "main" for the package.
- What a package is:
- A package can be thought of as a project or workspace.
- A package can have many Go source code files in it.
- Every file that belongs to a package must have its first line declare the package it is a part of.
- As for why we chose the name "main" for the package:
- There are two types of packages: executable packages and reusable packages. Executable packages generate an executable we can run. Reusable packages are used as helpers / libraries.
- The name of the package that you use determines whether you're creating an executable or reusable package. In particular, the word "main" for a package is what designates a package as executable. Any other name for a package will create a reusable package.
- An executable package must have a function called
main
.
Import Statements
- The import statement is used to give our package access to code contained in another package.
- "fmt" is a standard library included with Go. It stands for "format". It's used for printing out information.
- We can import both standard packages that are included with Go as well as packages that are not official and have been authored by other individuals.
- We can learn more about the standard packages by going to golang.org/pkg
- We are going to be looking at these official docs a lot, because a lot of learning Go is learning how these standard packages work.
File Organization
- The
func
in our hello world program is short for "function". Functions in Go work just like functions in other languages. - The way a file is organized always follows the same pattern:
- At the very top we have a "package ..." declaration.
- Then we'll import other packages we need.
- Then we'll declare functions.
How to Access Course Diagrams
- No video, just text.
How to use the course diagrams:
Go to https://github.com/StephenGrider/GoCasts/tree/master/diagrams
Open the folder containing the set of diagrams you want to edit
Click on the ‘.xml’ file
Click the ‘raw’ button
Copy the URL
Go to https://www.draw.io/
On the ‘Save Diagrams To…’ window click ‘Decide later’ at the bottom
Click ‘File’ -> ‘Import From’ -> ‘URL’
Paste the link to the XML file
Section 3: Deeper Into Go
Project Overview
- We're going to create a
Cards
package that simulates a deck of playing cards. - We're going to create the following functions:
- newDecks - Create a list of playing cards, just an array of strings.
- shuffle
- deal - I'll get one list for the 'hand' and a second one for the remaining cards.
- saveToFile
- newDeckFromFile
New Project Folder
- Create a new folder named
cards
. - Create a
main.go
file. Add some boilerplate code:
package main func main() { }
Variable Declarations
- We're going to play with Go a bit first to get a sense of the language.
- I want to create a variable in our
main()
function and assign it a string that represents a playing card, and then print out that string.Add the following code to
main()
:var card string = "Ace of Spades"fmt.Println(card)
var
is short for "variable" and lets Go know that we're going to create a new variable.card
is the name of the variable.string
is telling the Go compiler that only a value of typestring
will ever be assigned to this variable."Ace of Spades"
is the initial value we want to assign to the variable.
- Golang is a "statically typed language", like C++ and Java, and in contrast to "dynamically typed languages" like JavaScript, Ruby, and Python.
- He opens the Chrome console to show how JavaScript will let you assign a number to a variable and then later assign a string to the same variable.
- Some basic Go types:
bool
,string
,int
,float64
- The
:=
syntax:- He points out that the IDE shows a warning letting you know that you don't need to include
string
in the variable definition if you're assigning an initial value of that type. - An alternative way to define the variable is
card := "Ace of Spades"
- We only use the
:=
syntax when assigning a new variable. If we're assigning a new value to an existing variable, we don't include the colon. - If you accidentally use
:=
a second time, you'll get an error message.
- He points out that the IDE shows a warning letting you know that you don't need to include
Functions and Return Types
He starts with the following code:
package main import "fmt" func main() { card := "Ace of Spades" fmt.Println(card) }
- He wants to modify this code to create a new function that will return the value of the card to be created, instead of just assigning the value directly in
main()
. - To return a value, just use
return <value>.
He modifies the code to look like this:
package main import "fmt" func main() { card := newCard() fmt.Println(card) } func newCard() { return "Five of Diamonds" }
- He gets an error on the return statement saying
too many arguments to return. have (string), want ()
. - This error is happening because our function definition is saying we don't expect this function to return anything, but we are returning something (a string).
- To fix the error we update the function definition to read:
func newCard() string {
, adding the wordstring
. - He tries changing the
string
in the function definition toint
, and shows how that pops up an error on thereturn
line letting you know you're returning the wrong type of value.
Slices and For Loops
- Go has two basic data structures for handling lists of records: Array and Slice.
- an array is a fixed-length list of things.
- a slice is an array that can grow or shrink, like a Python list.
- Both arrays and slices must be defined with a single particular data type; every element must be of the same type.
- To declare a slice, we use the following syntax:
cards := []string{}
- Note he doesn't write
strings
(plural). - You can place whatever initial records you want to include in the curly braces:
{"Ace of Diamonds", newCard()}
- Note he doesn't write
- To add a new value to a slice:
cards = append(cards, newElement)
- This returns a new slice. It doesn't modify the existing slice.
To iterate over a slice use
range
:for i, card := range cards { fmt.Println(i, card)}
- We use the
:=
syntax for defining the variables (which we earlier saw should only be used the first time you define a variable) because Go "throws away" the variables at the end of every loop.
- We use the
- In the section "Reference vs Value Types" later in the course he writes that arrays are rarely used directly; slices are used "99% of the time for lists of elements".
OO Approach vs. Go Approach
- We're now going to start on our
Cards
project in earnest. - We're going to see how we might create this project in an OO language and then see how we do it in Go.
- Go is not an object-oriented programming language. There's no idea of "classes" in Go.
- In an OO language like Python we would define a
Deck
class and use it to create aDeck
instance variable. The instance would have deck-related methods attached to it. - In Go what we do instead is create a new type:
type deck []string
, and then to create deck-related functions we create "functions with a receiver" that only work with that new type. We'll talk more about this later. - We're going to aim to have the following file structure in our package:
main.go
- Contains the code used to create and manipulate a deck.deck.go
- Contains the code that describes what a deck is and how it works.deck_test.go
- Contains code that automatically tests the deck.
Custom Type Declarations
- In
deck.go
we'll create the new type:type deck []string
- This is telling Go that the identifier
deck
in our code should be treated as being exactly the same thing as if it saw[]string
(a slice of typestring
).
- This is telling Go that the identifier
- Note: we don't use the terms "extends" or "subclass" in Go.
- We can now replace
[]string
where it appears in ourmain.go
file withdeck
. - When we run the program now, we need to include
deck.go
:go run main.go deck.go
To define a receiver function, use this syntax:
func (d deck) print() {}
- To use the defined receiver function, use it as if it was a method in Python:
cards.print()
Receiver Functions
- When we define a receiver function, any variable in our package of the specified type will get access to that function. So basically it works like a Python class method.
- The receiver syntax "(d deck)" has two parts: a variable name and a type. So in our example, "d" is a variable name we'll be able to use in our method, and "deck" is the required type of that variable.
- You can think of the receiver variable as being very similar to the word "this" or "self" in JavaScript and Python.
- By convention in Go we never use the word "this" or "self"; we always pick a variable name that refers to the actual object.
- By convention in Go we always use a one or two letter abbreviation as the receiver variable name.
- Personally he's not convinced it's a great idea to use one or two letter variables names as it could be confusing, but he follows the convention.
- Officially in Go we never use the terms "class" and "method" even though you can think about this way of defining types with receivers as being similar to those.
Creating a New Deck
- We're going to implement the newDeck function.
- We need to annotate the function definition with the type that the function returns, we put it after the param list: func newDeck() deck { }
- We're not going to add a receiver because we'll want to call this function when we don't already have a deck object to work with.
- Each card is going to be a string of the form "<Value> of <Suit>", like "Ace of Spades".
- To create the cards we're going to create
cardSuits
andcardValues
slices and then use two for-loops to create all the different combinations. - This code should look very familiar if you've programmed before. He's seen some people feel intimidated when switching to Go but the language honestly doesn't have a lot of special features. You can put together good programs without a lot of study of the language.
- We don't use the iterator variables in our for loops and so we see a warning in the IDE. By convention in Go we replace the names of unused variable definitions in for loops with underscores.
Slice Range Syntax
- We're now going to implement the
deal
function. It's going to return two decks: a "hand" deck and a "remaining" deck. - Selecting a subset from a slice works just like Python:
- it's zero indexed
- the second index is not included in the result. So
[0:2
]
is the first two elements. - We can leave off either of the two numbers.
Multiple Return Values
- He spends a few minutes repeating information from previous lessons.
- To declare that a function returns multiple values, wrap them in parentheses:
func deal(d deck, handSize int) (deck, deck) { }
- Unlike Python, you don't need to add import statements for stuff that's defined in the same package but in separate files.
- Receiving multiple values from a function call works just like in Python: just have comma-separated variable names:
hand, remainingDeck := deal(cards, 5)
Byte Slices
- Now we're going to implement
saveToFile()
. - We're going to use the
WriteFile
function in theioutil
package to write to the hard drive. - He uses the word "slice byte" when the code says
[]byte
which makes me wonder how people distinguish between an array and a slice in function definitions. - Whenever you head "slice byte" he wants you to think "string of characters".
Deck to String
- We need to convert our card strings to bytes to be able to use the
WriteFile
function. - To do this conversion we'll do "type conversion". We just write the type we want, add parentheses, and then put the object we want to convert in the parentheses:
[]byte("Hello World!")
- To convert our deck to a slice of bytes, we're going to take our deck, which is a slice of strings, and join the elements into a single string, and then convert that single string into a slice of bytes.
- We're going to make a helper function
toString
that turns a deck into a string. We will make it a receiver function.
Joining a Slice of Strings
- In our
toString
function we first convert thedeck
object to a slice of string:[]string(d)
- To join the elements of the slice of strings, we're going to use the
strings
default package, specifically theJoin
function. To import multiple packages we use this syntax:
import ( "package name one in quotes" "package name two in quotes" )
Saving Data to the Hard Drive
- We're now going to implement a
saveToFile()
receiver function. - We'll allow the user of our function to specify the
filename
thatWriteFile
takes as an argument. - We're going to say that it returns an
error
since that's something that can be returned from theWriteFile
function we're going to use. - For the
permission
parameter toWriteFile
we'll use 0666. - You can see in the import statement that
ioutil
is a subpackage of theio
package. - He runs the code and confirms that it writes a file to the hard drive.
Reading From the Hard Drive
- We're now going to implement a
newDeckFromFile()
function. - Golang uses
nil
instead ofnull
orNone
. - A common pattern in Go code after calling a function that can return
nil
is to add an if statement to add code to handle the case when the value isnil
. - He spends a few minutes explaining two different options for handling an error when trying to load a file: 1) return an empty deck, or 2) quit the program.
- To immediately quit the program we can call the
os
package functionExit()
with an argument of1
which indicates that something went wrong.
Error Handling
- We'll use the
Split()
function in thestrings
package to split up the comma-joined list of cards that we load from the file:s = strings.Split(string(bs), ",")
- He converts the list of string to a deck with
deck(s)
- He tries running
newDeckFromFile()
and confirms it works.
Shuffling a Deck
- Golang doesn't have any built-in function for shuffling a slice.
- To implement our own shuffle function, we will iterate over the length of the deck, and for each index we'll generate a random different index in the deck and swap the two cards at those indices.
- We can use the
Intn()
function in therand
subpackage of themath
package to generate random numbers. - We'll make the
shuffle()
function a receiver function that modifies the deck we call it from. - We can use
len()
to get the length of a slice. - We can do a one-line swap like this:
d[i], d[newPosition] = d[newPosition], d[i]
- One weird thing about our initial implementation of the
shuffle()
function is that the last four elements in our deck are always the same.
Random Number Generation
- The reason we're getting the same random results every time is that by default, the Golang random number generator always uses the same seed.
- It's kind of hard to figure out how to change the seed just by reading the docs.
- Let's take a look at the type
Rand
in the docs. It's "a source of random numbers". When we create an object of this type with theNew()
function, we must specify a source (which is of typeSource
). - A
Source
is "a source of uniformly-distributed pseudo-random int64 values in the range 0-2^62". - To create a new
Source
we need to pass in a randomly-chosenint64
. - Again, a lot of learning Go is just learning to navigate the documentation around the standard packages.
- To generate an int64 that'll be different every time, we'll use the current time with
UnixNano()
in thetime
package:time.Now().UnixNano()
Creating a go.mod file
- To run tests we need a
.mod
file. To create one, rungo mod init cards
Testing With Go
- Go testing is not like using testing frameworks in other languages. Go has a very limited number of functions to help us test our code.
- To make a test in Go, make a file that ends in
_test.go
- To run all tests in a package run
go test
- In your test file remember to start it with
package <your-package-name>
- VS Code will detect that you've created a test file and will put links at the top of the file to run all the tests in the package or just the tests in that file.
Writing Useful Tests
- A common question in testing is: how do we know what to test? With Go this ends up being straightforward to answer.
- Basically you want to try to find some easy assertions that should be true of the output of your functions and that capture attributes of the output you care about.
- For the
newDeck
function there are three things he can think of:- The returned deck should have the expected number of items.
- The first card of the deck should be the expected first card.
- The last card should be the expected last card.
- For each file we want to test we create a
_test.go
file, and for each function in that file that we want to test, we create a function namedTest<YourFunctionName>
. - But we don't have to have one test function for each of our functions. For example, we can test both saving a deck to a file and reading a deck from a file in a single test function:
TestSaveToDeckAndNewDeckFromFile
- Our tests can often follow a similar pattern:
- Get the return value from the function we're testing.
- Have various
if
statements to check the output. - If we find a problem, tell the test handler that something went wrong.
- The first (and only?) argument to a test function should be
t *testing.T
which is our test handler. - To notify our test handler that something went wrong, use
t.Errorf("Write a description of the problem here")
- To include variables in the error description, use string formatting with the percentage sign:
("Your message: %v", len(d))
Asserting Elements in a Slice
- He just implements the checks he talked about in the last lesson:
if d[0] != "Ace of Spades" { t.Errorf("...") }
if d[len(d) - 1] != "Four of Clubs" { t.Errorf("...") }
- Unlike the test frameworks you see in other languages,
go test
doesn't know how many tests we wrote, so you won't see an output that says something like "Ran 60 tests, 5 failed".
Testing File IO
- He's now going to test our functions that save data to a file and load data from a file.
- When writing tests with Go we need to make sure we write code that will do any necessary clean-up in all cases. Go doesn't handle cleanup for us.
- In our case, we'll have our code attempt to delete the
_decktesting
file we'll be using both before running and after running. - So, how do we delete a file? Let's check the Golang standard library docs. It's the
Remove
function in theos
package. - The
Remove
function can return an error, but we can ignore that for our purposes. - Our test function will be named
TestSaveToDeckAndNewDeckFromFile
- We're using a long name because it will make it easier for us to find it in our code if it causes an error in the future. (This doesn't make any sense to me if all your tests are in one file but I could see it making sense if you have tests across multiple files and want to avoid using duplicate test names).
- He just has a single assertion, that the deck he gets from loading the saved file has the expected number of items.
- When writing a test you should make a change to the assertion to make sure that it actually fails when it should.
Project Review
- He summarizes the steps he took to get the Deck project done.
- There are some weird things about the project he wants to point out.
- For the
deal
function we didn't use a receiver. The reason is that it might create some ambiguity about what it does. If we had code that saidcards.deal(5)
it would look like we're modifying thecards
slice by removing 5 cards. - We pass in the
t *testing.T
argument to each of our tests, but we don't know yet what the*
is for. - These two things are actually at the core of the next thing he wants to talk about.
- For the
Section 4: Organizing Data With Structs
Structs in Go
- When creating our Deck class it would've been awkward to access just the suit or just the number since they're joined into a single string.
- A struct is like a collection of properties that are related.
- So it's like a JavaScript object or a Python dictionary (or really a named tuple).
- He creates a new folder called
/structs
and a newmain.go
Defining Structs
- We're going to create a simple project / demo to show how to use a struct.
- Whenever you use a struct you need to first define the type and then you can create an object of that type.
- He gives an example of a
person
struct with afirstName
field and alastName
field. - Note we don't have any colons or commas in the definition.
Declaring Structs
- We can define a new
person
object with syntax like the following:alex := person("Alex", "Anderson")
- By default, if you omit the field names Go will assume the provided arguments match the order of the struct definition.
- Personally he can't stand that Go allows this.
- The other way to create an instance of a struct is to specify the keys like in Python.
- If you
Println
the struct you'll get a list of the values.
Updating Struct Values
- Another way o create an instance is with
var alex person
- If you don't specify the values, Go will assume what it calls a "zero value": an empty string, 0, or false, depending on the type.
- This is different from JavaScript where such fields would have a value of
null
orundefined
- We can use
%+v
in ourPrintln
call to get a printout of both the keys and values of the struct. - You'll sometimes see the
var
syntax when the coders want the instantiated struct to use the default values of the struct. - To update the value in a struct we can just do
alex.firsname = 'new name'
Embedding Structs
- You can have attributes in structs that are themselves structs.
- Example: a
contactInfo
struct that is used for acontact
attribute of aperson
struct.
- Example: a
He goes through an example of creating a
person
struct and having an embedded definition of acontactInfo
struct:jim := person{ firstName: "Jim", lastName: "Party", contactInfo: contactInfo{ email: "jim@gmail.com", zipCode: 94000, }, }
- Every line within the definition must have a comma at the end.
Structs with Receiver Functions
- When writing a struct definition, you can leave off the field name and just specify the value, and the field name will be the same as the name of the type of the value.
- Example: instead of
contactInfo: contactInfo
you can just havecontactInfo
.
- Example: instead of
- This might seem like a minor thing but it'll be important later when we look at code reuse with Go.
- Defining a receiver function works the same way as when doing it for a type:
func (p person) print() { }
- He creates an
updateName(newName string)
receiver function and shows that it doesn't actually modify the object that calls theupdateName()
function. This is a segue into the next section.
Pass By Value
- This discussion is going to revolve around the concept of 'pointers'.
- Pointers in Go are relatively straightforward.
- We should talk about how RAM on your computer works: it's like a bunch of cubbies, each of which has an address.
- When we create a
person
object, the computer takes that object and puts it at a particular address (cubby) in RAM. - Go is a "pass by value" language by default. It will copy the entire object when passing it to the function being called.
Structs with Pointers
- He reiterates / summarizes what he discussed in the previous section.
- He's going to change the code we've been working on to make it pass the value of the
person
struct to theupdateName
receiver function.- The first thing is that he adds this line of code:
jimPointer := &jim
- He then changes the call to
updateName
to instead be called by the pointer:jimPointer.updateName("jimmy")
- He updates the
updateName
function definition:func (pointerToPerson *person) updateName(newFirstName string) { }
- He updates the line of code in
updateName
that changes the name:(*pointerToPerson).firstName = newFirstName
- The first thing is that he adds this line of code:
- He's going to explain what is going on with this new code in the next section.
Pointer Operations
- The
&
is an operator.&variable
means "give me the memory address that this variable is pointing at". *
is also an operator.*variable
means "give me the value that this memory address is pointing at".- A very important distinction you need to understand that can be very confusing in Go is the difference between
*
when seen in a type description vs.*
when seen on a 'normal' line of code.- He thinks this is one of the most confusing things about pointers. When you see a star in front of a type it means something completely different than when you see a star in front of an actual instance of a pointer.
- When you see
*
in a type description (for example*person
), it isn't being used as an operator. So any variable you see being assigned to something of that type description is still just a pointer instead of a dereferenced pointer. - When you see
*
on a line of code like*pointerToPerson.updateName("jimmy")
then it is being used as an operator and is actively dereferencing the pointer.
- So we're working with two different kinds of variables: variables that produce an address to a particular type of data structure, and variables that produce an actual value of a particular type of data structure.
- He spends a few minutes reiterating / summarizing the above ideas.
- This is revision 1 of our understanding of pointers.
Pointer Shortcut
- With Go, if we define a receiver function of type pointer-to-X, Go will allow us to call it either with a pointer or with the referenced type itself. Basically letting us skip defining a pointer variable.
- So instead of needing to do
jimPointer := &jim; jimPointer.updateName("jimmy")
we can just keep it asjim.updateName("jimmy")
and it will work (it will pass to the receiver function by reference) as long as the receiver is typepointerToPerson *person
Gotchas With Pointers
- He creates an example of creating a string slice, passing it to a function, modifying the first element of the slice within that function, and then doing a Println of the slice outside of the function. Given what we said before about Go being pass-by-value, we would expect that the slice would not have been modified.
Reference vs. Value Types
- We rarely use arrays directly; we almost always use slices when we want to have lists of items.
- Slices actually store the items in an array behind the scenes.
- When we create a slice, Go is actually creating two separate data structures:
- the first is what we call a slice. It has a pointer to the underlying array, a capacity number, and a length number.
- The second is the underlying array.
- So what's happening when we pass a slice to a function is that Go is still behaving the same: it's still passing by value. But it's copying the slice and passing the slice by value, but the underlying array is not being copied because it's not what is being passed.
- In Go, slices aren't the only data structure that behave this way. There are "value types" and "reference types".
- Value types: int, float, string, bool, structs - You need to use a pointer to modify this value from a function.
- Reference types: slices, maps, channels, pointers, functions
- He writes "Don't worry about pointers with these". You don't need to use a pointer to modify the values from a function.
- NW: My question: What happens if you grow one of these reference types within the function to such a degree that they need to reference a new array?
- A: I asked about this here and apparently the answer is that this is a potential problem for slices, but not for other "reference types" because there's another level of separation between the underlying array and the root type, and actually there are no "reference types" in Go (see links at the top of this wiki page).
Section 5: Maps
What's a Map?
- A map is a collection of key-value pairs, like an object in JavaScript or dict in Python.
- The keys must all be of the same type.
- The values must all be of the same type.
The syntax for defining a map is
map[key_type]value_type
my_map := map[string]string { }
Manipulating Maps
To create an empty map:
var colors map[string]string or colors := make(map[string]string)
- To add a key-value pair:
colors["white"] = "#ffffff"
- You can't access keys using dot notation as with structs because the keys don't have to be strings.
- To delete an entry:
delete(mapName, keyName)
Iterating Over Maps
To iterate over a map:
for keyVar, valueVar := range myMap { }
Differences Between Maps and Structs
- Keys in a map don't need to be a string.
- All values must be of the same type in a map.
- You can't update the keys of a struct; you need to define them all in your code.
- There's no built-in way to iterate over the keys of struct like how you can with a map.
- A struct is used to represent a single thing that has multiple attributes, whereas a map is used as a collection of different things.
- We need to use pointers to update a struct from within a function, but we don't need to do that with maps. So maps are a "reference type".
- In his experience, when writing professional Go code you'll end up using structs a lot more than maps.
Section 6: Interfaces
Purpose of Interfaces
- We're going to look at the code we've written through this course and see an issue with it that interfaces help solve.
- He gives an example of the
shuffle()
receiver function we created, and how the logic in it seems fairly generic. But what if we want to use it with a slice of a different type (other than string)? Do we need to copy-paste all that code?- This is one of the problems that interfaces helps us solve.
- We're going to write a "bad version" of a program without interfaces, then update it to use interfaces.
- We're going to create a chatbot program. It'll have an
englishBot
struct and aspanishBot
struct.- Both bots are going to have a
getGreeting
receiver function that returns a greeting in the bot's language, and aprintGreeting
receiver function. - The
printGreeting
function will probably just do something likePrintln(bot.getGreeting())
- The
getGreeting
functions' implementations will be very different, but theprintGreeting
functions will have very similar logic.
- Both bots are going to have a
Problems Without Interfaces
- He creates a new project and creates the
englishBot
struct (no fields), thespanishBot
struct (no fields), andgetGreeting
receiver functions for each that return different strings. - If your receiver function isn't going to use the received object, you can omit its variable name and just specify the type:
func (englishBot) getGreeting() string { }
- Go doesn't support overloading: you can't have functions with identical names but different parameters.
Interfaces in Practice
- In this video he's going to do the refactor and then discuss what he did.
He defines the interface:
type bot interface { getGreeting() string }
He defines the generic form of
printGreeting
:func printGreeting(b bot) { fmt.Println(b.getGreeting()) }
- He has a good "plain English" explanation of what the interface definition is saying: "Hey, every type in this program, pay attention: our program now has a new type called 'bot'. If you're a type in this program with a receiver function named
getGreeting
that returns a string, you are now an honorary member of type 'bot', and you now get access to the functionprintGreeting
."- When he showed his refactor I was wondering how the connection was happening between the new interface and the existing types, and this explanation answered my question.
Rules of Interfaces
- In our interface definition we list all of the functions that we expect the matching types to have along with their expected argument types and expected return types.
- He mainly makes the point that you can specify multiple parameter types and multiple functions within an interface, all of which need to be matched exactly for a type to qualify as the interface type.
- In TypeScript we have the terms "concrete type" and "interface type". You can directly instantiate a concrete type, but you can't directly instantiate an interface type.
Extra Interface Notes
- Interfaces are not "generic types" like you'd see in some other languages. Go doesn't have support for generic types.
- Interfaces are implicit. You don't need to write code that explicitly connects an interface and the qualifying types.
- It's nice because the code is shorter, but it can make it harder to know what interfaces a given type implements.
- Interfaces are a "contract to help us manage types". But they're not some kind of guarantee that your implementation of that interface is going to be correct. If you feed the interface garbage, you'll get garbage back.
- Interfaces are tough to use and understand. At the beginning, just focus on trying to understand the standard library documentation when it says it expects an interface. Later, when you're comfortable with them, you can think about writing your own.
The HTTP Package
- Next we're going to take a look at a more-realistic example of using interfaces: working with stuff from the standard library.
- Our program will make an HTTP request to google.com and print the response to the terminal.
- He creates a new
http
directory to hold the project code, and creates amain.go
file. - He navigates to the
http
package in the official Go docs and sees how to create a GET request, and then navigates to thetype Response
section of the docs to arrive at the section dedicated to theGET
function. - You have to include the protocol (
http://)
when making a request. - He writes code that will just
Println
the response object and explains that while this would work in many other languages, it won't work in Go. - He runs the code and seems to get something like a slice of header information, but no actual HTML.
Reading the Docs
- He reads the official docs on the
Response
type and sees that the stuff we were getting in the output last time seem to be the first few fields of the type. - He sees that the
Response
type has aBody
property of typeio.ReadCloser
- He checks the docs for the
ReadCloser
type and sees that it's an interface, and that it looks weird, because it doesn't seem to have a list of functions with their required argument types and return types, but instead justReader
andCloser
- He reads the docs on the
Reader
type and sees it's an interface that needs to define aread
function that takes a slice of bytes and returns anint
and an error. - You can end up going down a rabbit hole when trying to make sense of the docs.
More Interface Syntax
- First we're going to talk about why an interface was used as a type inside a struct: what it means is that we can assign any type we want to that field that satisfies that interface.
- Next: why did we see that weird syntax in the definition of
ReadCloser
that just hadReader
andCloser
fields? It's a way for us to define an interface in terms of another; what it means is, "in order to satisfy theReadCloser
interface, you need to satisfy theReader
interface and theCloser
interface".
Interface Review
- Quick review of what interfaces are and why we care about them: (he summarizes the bot example and how interfaces allowed us to re-use the
printGreeting
function).
The Reader Interface
- You're going to see this
Reader
interface all over Go. - He gives an example of how the Reader interface is useful (and how interfaces are useful): Go programs could receive input from a variety of sources: HTTP requests, text files on the hard drive, image files, users entering information on the command line, or even data from an analog sensor plugged into the computer. We could imagine that these could all return different data types, and therefore without interfaces we'd need to have separate "print" functions for each of them.
- The Reader interface allows us to take the input from all of these different sources and get a slice of bytes as a result.
- So you can think of the Reader interface as being like an "adapter".
More on the Reader Interface
- Something that implements the
Reader
interface needs to implement aRead
function. - The request body has already implemented this for us.
- The way the
Read
function works is that the code that wants to callReaderObject.Read()
passes it a byte slice, and then the Reader takes its source of data and pushes it to that provided byte slice.- This may seem weird you if you're used to not needing to supply the data structure that the output will be put into.
- The integer that gets returned from
Read()
is the number of bytes that were put into the slice.
Working with the Read Function
- Let's try to actually write some code that uses the
Read()
function. - To create the slice he does this:
bs := make([]byte, 99999)
- This initializes the slice so that its underlying array can immediately handle the specified number of bytes.
- We have to do this because the
Read()
function isn't set up to automatically resize the slice if it's already full. (NW: Ugh, what a nightmare.)
- To load the HTTP response into the byte slice he does this:
resp.Body.Read(bs)
- To print it out he does
fmt.Println(string(bs))
- In the next video we'll see a way to simplify this code we wrote.
The Writer Interface
- Let's simplify the code from the last section: He replaces the three lines of code he wrote in the previous section with just this:
io.Copy(os.Stdout, resp.Body)
- How did this work? First, you need to understand that Go has a
Writer
interface which does the opposite of theReader
interface: it takes a slice of bytes and writes it to some form of output (HTTP request, text file, etc.).
The io.Copy Function
- He navigates to the official docs on the
Writer
interface and sees that to satisfy it, a type needs to implement aWrite
function. - He then navigates to the docs on the
Copy()
function and sees that it requires two arguments: first, something that implements theWriter
interface, and secondly something that implements theReader
interface. - He then goes through the line of reasoning that would lead us to understand that
os.Stdout
would work as aWriter
type:os.Stdout
is of typeFile
, and typeFile
has a function of typeWrite
, and since we know that any type that implements that function (including the required parameter types and return types) is of typeWriter
, we therefore know thatos.Stdout
would be accepted byio.Copy()
. - He describes this as "hard-won knowledge", suggesting there was no easily-available help out there on the Internet that made this stuff clear to him.
- "It took me a lot of time when I was learning Go to understand all this stuff."
- He says that even though this stuff seems "nasty", it will save you "so much time when you go write your own code".
- Next, as a way to get more experience with interfaces, we'll take a look at the implementation of
io.Copy()
so we can understand how we'd implement theWriter
interface in our own type.
The Implementation of io.Copy
- He holds down the Command key (OSX, maybe Ctrl on Windows) and hovers over the
Copy
function in the code, and it shows the source code in a little window. - He then clicks on
Copy
and a new tab opens to the source code. - He sees that the
Copy
function immediately passes the arguments it receives to acopyBuffer
function, and navigates to that function's source code. - He points out in the source code the line that has the
copyBuffer
function doing what we did manually in an earlier section: it creates an empty 32kb byte slice to contain information from the input source (whatever is implementing theReader
interface). - He then shows how the function is just looping pulling information from the
Reader
and piping it to theWriter
32kb at a time until theReader
doesn't have anything left to give. - Next, let's try to create our own type that implements the
Writer
interface.
A Custom Writer
- We're going to try to create our own custom
Writer
and pass it toio.Copy()
He creates an empty
logWriter
type with the necessaryWrite
function:type logWriter struct{} (...) func (logWriter) Write(bs []byte) (int, error) {}
- He points out (as he did earlier in the course) that there's nothing about the "interface" feature of Go that guarantees that a given type will actually do the intended action of the interface. It just guarantees the input and output types.
- As an example, he has the
Write
function just return1, nil
but not do anything else. He runs the code (using a logWriter object withio.Copy
) and shows that it doesn't raise any error.
- As an example, he has the
He then does a working implementation of
Write
:func (logWriter) Write(bs []byte) (int, error) { fmt.Println(string(bs)) return len(bs), nil }
- He says interfaces are "some of the really top-end, heavy Go stuff", so it's normal if it feels tough.
Assignment: Interfaces
- Create two struct types: triangle and square, both should have a
getArea()
receiver function. - Add a
shape
interface that requires a getArea()
receiver function and provides aprintArea()
function.
Assignment: Hard Mode Interfaces
- Create a program that reads a file from the hard drive and prints out the contents to the terminal.
- The filename should be provided as a command-line argument.
os.Args
is a slice of string that has the command line arguments passed to our program.- The first element will be the path to the compiled executable.
Section 7: Channels and Go Routines
Website Status Checker
- Channels and Go Routines are both used for writing concurrent programs.
- We're going to build a program without using these features and then improve the program by rewriting it to use these features.
- The program we'll build will make HTTP requests to a list of sites periodically to check if they're online or not.
- Our first approach will be to iterate one-at-a-time through a slice of strings of the URLs we want to check.
- He implements the "outer" code of the function that loops through the list of strings.
Printing Site Status
- He writes a function
checkLink
that takes a link and makes an HTTP request. - He runs the program and sees the output coming out one site at a time.
Serial Link Checking
- It seems in our first approach there's a bit of a delay between when we receive the output for each site.
- This is because we're needing to wait for the HTTP response for each site we make a request for before we can move on to handle the next site.
- If we had many, many links, we might having to wait a long time between when we could check a given website.
- We could use Go Routines to run these requests in parallel.
Go Routines
- First we'll discuss the theory of Go Routines, and then we'll actually implement them.
- When we run a Go program, Go creates a Go Routine that runs our code line-by-line.
- When it runs the
http.Get(link)
call, the main Go routine is blocked from continuing until it receives a response. - To fix this, we'll add a
go
keyword in front of thecheckLink
function to have it run in parallel in a new Go routine:go checkLink(link)
- When the child routine hits a blocking function, control is passed to other Go routines that are unblocked.
- In the next section we'll look at some of the edge cases we can run into with Go routines.
Theory of Go Routines
- Let's talk about what Go routines are doing on our machine when we run them.
- Behind the scenes, there's something called the "Go scheduler". By default it works with one CPU on our machine, even if you have multiple cores.
- What's important to understand is that only one Go routine is ever running at any given time.
- The purpose of the Go scheduler is to monitor the status of each Go routine and switch the 'active' one depending on which ones are blocked and unblocked.
- If you have multiple CPUs, the scheduler will assign routines to different CPUs to make them run truly in parallel.
- In Go there is a saying you'll run into a lot: "concurrency is not parallelism". Parallelism is when you have code for the same program running at the exact same time on different CPUs. Concurrency is when you can start new work before having totally finished old work.
- Child go routines aren't given 100% exactly the same treatment as the main Go routine.
Channels
- He updates the
checkLink
project code we'd written to work with Go routines by just adding the keywordgo
in front of the call tocheckLink(link)
- He runs the code and there's no output (we'd expect to see the URLs we're checking).
- He explains that in our existing code, when the main routine finishes creating all the child routines, it doesn't see anything else for it to do, so it exits entirely. It doesn't care that the child routines haven't finished their work.
- We're going to fix this by using channels. Channels are a way to communicate between Go routines, and they're the only way to communicate between Go routines.
- In our case, we're going to create one channel that will let our main routine know when the child routines have finished their work.
- You can think of the channels as working like text/instant messaging: you can send data into a channel and it'll automatically get sent to all other routines that have access to that channel.
- A channel is an actual value in Go that you can pass around between functions like any data structure: int, string, struct.
- You must specify a type of data that the channel will be passing around.
Channel Implementation
- To create a channel:
c := make(chan string)
string
can be whatever type you want.
- For a function to be able to make use of the channel, you have to pass the channel variable to that function.
- When you list a channel in a function's list of arguments, you also need to list the type of data that the channel expects:
func checkLink(link string, c chan string) {
- To send data into a channel use the
<-
operator:myChannel <- 5
- To receive data from a channel use the same operator:
myNumber <- myChannel
- The routine will wait for a value to be sent into the channel.
- You don't have to assign it to a variable, for example you can do
fmt.Println(<- myChannel)
- He updates the
checkLink
project code we've been working on to just send a string message into the channel from each child go routine after the GET request, and then waiting for a single message in the main routine. - He runs the code and notes that we only see one message in the terminal before the program exits, and says we'll discuss why this is happening in the next section.
Blocking Channels
- He steps through the code, explaining that the main routine stops at the
fmt.Println(<- c)
line of code that receives string messages from the child routines, and then when it receives a message it runs that line of code and exits.- He stresses that the main thing to understand is that receiving data from a channel is a blocking operation in the same way that an HTTP request is.
- He shows a timeline diagram he created to show how the main routine stops when it has finished creating all the child routines and then restarts when it receives a message in the channel from the child routine that had sent a request to google.com.
- He adds another
fmt.Println(<- c)
line after his first one and runs the code again and sees two messages in the output.- He shows a new timeline diagram showing how the main routine would go to sleep after printing the first string and wake up when receiving the second string through the channel.
- He adds three more
Println
calls, bringing the number of calls up to the same number of links, and runs the code again and sees all of the expected messages in the output. - He adds one more
Println
call and sees how all of the expected output messages get printed out but then the program doesn't exit because it's waiting for one more that will never come. - In the next section we'll see how to print out all of the messages without copy-pasting
Println
statements.
Receiving Messages
- Instead of copy-pasting the
Println
calls, we're going to use a for loop:for i := 0; i < len(links); i++ {
Repeating Routines
- We're now going to update our program to ping each website repeatedly instead of only once.
- To have an infinite loop use this syntax:
for {
This is the full code for the repeating part:
for { go checkLink(<- c, c) }
- This loop runs after our initial loop through the links. The
<-c
is receiving links sent into the channel by finished routines and then immediately passing them to a new routine. - He runs the code and we can see how quickly it's querying each site.
- In the next section we'll add a gap of time between each request for a given site.
Alternative Loop Syntax
An equivalent syntax for our loop in the previous section is:
for l := range c { checkLink(l, c) }
- The
range c
will wait for a value to arrive on the channelc
- This syntax might make it easier for other coders to understand that the for loop is iterating over values coming in from the channel.
Sleeping a Routine
- He navigates the official docs to find the 'Sleep' function.
- 'Sleep' takes a 'Duration', so he navigates to the docs to see what that is. He sees that a
time.Second
is of typeDuration
- The final code:
time.Sleep(5 * time.Second)
- He points out that having the sleep statement in the main routine wouldn't achieve the parallel behavior we want.
Function Literals
- He initially moves the
time.Sleep()
call to thecheckLink()
function but then points out that this kind of "pollutes" the nature ofcheckLink()
His proposed solution is to instead have the
go
call initiate a function literal that contains the call totime.Sleep()
and the call tocheckLink()
:for l := range c { go func() { time.Sleep(5 * time.Second) checkLink(l, c) }() }
Channels Gotcha!
- He notes that we're getting a warning underneath the call to
checkLink()
saying "range variable l captured by func literal". - When he runs the code, we see that after the initial loop through the sites, all of the HTTP calls seem to be going to facebook.com. So something is wrong.
- He explains that what's happening is that the function literal is running within a child routine and is referencing the actual value of l (not a copy), which is also being referenced and updated by the main routine.
The solution is to pass the for loop variable as an argument to the function literal:
for l := range c { go func(link string) { time.Sleep(5 * time.Second) checkLink(link, c) }(l) }
The Little Go Book
- https://www.openmymind.net/The-Little-Go-Book/
- I found this book recommended in this review of Let's Go: "if you never use Golang before. I don't suggest to read this directly because there is no Golang 101 to cover. I suggest you can start with 'The Little Go Book'"
Let's Go
- https://lets-go.alexedwards.net/
- Thoughts:
- This book and the next one seem to be split up in such a way that you'll want to buy both of them if you want to use Go for the back-end of a web app.
- Summary:
- 1. Introduction - He recommends using the HTML version of the book b/c you can copy code more easily and navigate the chapters more easily. The book has you create "Snippetbox", a Pastebin type app.
- 1.1. Prerequisites - Use the Tour of Go or Little Book of Go to get familiar with the syntax. Use
go version
to check your version of Go.
- 1.1. Prerequisites - Use the Tour of Go or Little Book of Go to get familiar with the syntax. Use
- 2. Foundations
- 2.1. Project setup and creating a module
- The project's module path should be globally unique to avoid conflicts with other imported packages, so something like snippetbox.nathanwailes.com.
- Create a module by running
go mod init snippetbox.nathanwailes.com
in the project directory and it should create ago.mod
file. - Snippetbox: You create and run a "Hello World"
main.go
file.
- 2.2. Web application basics
- You need three things: a handler, a router ("servemux"), and a web server.
- A servemux is just a mapping of URL patterns to handlers.
- You use `http.NewServeMux()` to create a new servemux, and
mux.HandleFunc(url, handler)
to add a new mapping of URL to handler.
- You use `http.NewServeMux()` to create a new servemux, and
http.ListenAndServe(port, servemux)
to start the web server.- An amazing thing about Go is that you don't need Nginx/Apache.
- Snippetbox: You create the simplest possible web app, with just a single route that returns a text response.
- NW: I think I'm noticing that the app seems to take longer to compile than a Python app would take to start.
- 2.3. Routing requests
- Snippetbox: You add two new routes that each show different text:
/snippet/view
and/snippet/create
- Servemux URL patterns can be either "fixed paths" or "subtree paths". Subtree paths end with a trailing slash and act like a catch-all.
- He shows how to do an "if" check on the "/" subtree path to have missing paths return a 404 response: check r.URL.Path and then use
httpNotFound
- Longer paths always take precedence over shorter ones; the order in which they're registered doesn't matter (unlike JavaScript and Flask IIRC).
- The web server is smart enough to automatically redirect requests missing slashes to the version with the slash, when appropriate.
- You can list full host names in the URL patterns as if it was an Apache config.
- Go has a default servemux instance but it's not recommended to use it for security reasons.
- Important: Go's servemux doesn't support routing based on the request method, doesn't support variables in URLs, doesn't support regex URL patterns.
- Snippetbox: You add two new routes that each show different text:
- 2.4. Customizing HTTP headers
- My thoughts:
- On my computer I can run the
curl
commands he has in his book, I just need to have it ascurl.exe
because "on Windows, curl is an alias for Invoke-WebRequest in PowerShell". - I'm definitely noticing that compiling is slower than starting a Flask app; I wonder if there is a way to speed it up when reloading an app I've already compiled before.
- On my computer I can run the
- You can use r.Method to check the request method (GET, POST, etc.)
- Just FYI, not the normal way of doing things:
- You can use w.WriteHeader() to write response headers. You can just give it an HTTP status code.
- You can use w.Header().Set(<key>, <value>) to add a new header to the "response header map".
- The normal way to handle errors is with http.Error(w, "Some message", <some HTTP status code>)
- The pattern of passing http.ResponseWriter to other functions is very common in Go
- Normally you don't use strings and integers for the HTTP methods and status code, you use constants. For example http.MethodPost and http.StatusMethodNotAllowed. Full list: https://pkg.go.dev/net/http#pkg-constants
- Go automatically sets the Content-Type header but it can't distinguish JSON from plaintext, so you'll want to set it manually if you're sending JSON: w.Header().Set("Content-Type", "application/json").
- My thoughts:
- 2.5. URL query strings
- You can use r.URL.Query().Get() to get the value of a query parameter.
- The equivalent of Python's
int()
isstrconv.Atoi()
- He introduces the idea of interfaces by pointing out that the
fmt.Printf
function takes anio.Writer
object and we passed it anhttp.ResponseWriter
object instead, but it's fine because it satisfies theio.Writer
interface.
- 2.6. Project structure and organization
- There's no single recommended project structure.
- Don't over-complicate things. Add structure/complexity as needed.
- For this project we'll use this structure: https://go.dev/doc/modules/layout#server-project
- He creates
cmd
,internal
, andui
directories.cmd
holds app-specific code.- We'll put the web app code in a
web
subdirectory. - We could add CLI executables in a
cli
subdirectory (NW: would this be separately-compiled or just a new Go file?). - He splits the existing code into a
main.go
file and ahandlers.go
file. - To run the code we do
go run ./cmd/web
- We'll put the web app code in a
internal
holds potentially-reusable, non-app-specific code like validation code and database models.- This name is significant to the Go compiler: anything in a directory named
internal
can only be imported by code within the parent of that directory. - This is like the leading underscore in Python to designate something as private / not part of the API.
- This name is significant to the Go compiler: anything in a directory named
ui
holds HTML, CSS, images, etc.
- 2.7. HTML templating and inheritance
- 2.8. Serving static files
- 2.9. The http.Handler interface
- 2.1. Project setup and creating a module
- 3. Configuration and error handling
- 3.1. Managing configuration settings
- 3.2. Structured logging
- 3.3. Dependency injection
- 3.4. Centralized error handling
- 3.5. Isolating the application routes
- 4. Database-driven responses
- 4.1. Setting up MySQL
- 4.2. Installing a database driver
- 4.3. Modules and reproducible builds
- 4.4. Creating a database connection pool
- 4.5. Designing a database model
- 4.6. Executing SQL statements
- 4.7. Single-record SQL queries
- 4.8. Multiple-record SQL queries
- 4.9. Transactions and other details
- 5. Dynamic HTML templates
- 5.1. Displaying dynamic data
- 5.2. Template actions and functions
- 5.3. Caching templates
- 5.4. Catching runtime errors
- 5.5. Common dynamic data
- 5.6. Custom template functions
- 6. Middleware
- 6.1. How middleware works
- 6.2. Setting security headers
- 6.3. Request logging
- 6.4. Panic recovery
- 6.5. Composable middleware chains
- 7. Advanced routing
- 7.1. Choosing a router
- 7.2. Clean URLs and method-based routing
- 8. Processing forms
- 8.1. Setting up an HTML form
- 8.2. Parsing form data
- 8.3. Validating form data
- 8.4. Displaying errors and repopulating fields
- 8.5. Creating validation helpers
- 8.6. Automatic form parsing
- 9. Stateful HTTP
- 9.1. Choosing a session manager
- 9.2. Setting up the session manager
- 9.3. Working with session data
- 10. Server and security improvements
- 10.1. The http.Server struct
- 10.2. The server error log
- 10.3. Generating a self-signed TLS certificate
- 10.4. Running a HTTPS server
- 10.5. Configuring HTTPS settings
- 10.6. Connection timeouts
- 11. User authentication
- 11.1. Routes setup
- 11.2. Creating a users model
- 11.3. User signup and password encryption
- 11.4. User login
- 11.5. User logout
- 11.6. User authorization
- 11.7. CSRF protection
- 12. Using request context
- 12.1. How request context works
- 12.2. Request context for authentication/authorization
- 13. File embedding
- 13.1. Embedding static files
- 13.2. Embedding HTML templates
- 14. Testing
- 14.1. Unit testing and sub-tests
- 14.2. Testing HTTP handlers and middleware
- 14.3. End-to-end testing
- 14.4. Customizing how tests run
- 14.5. Mocking dependencies
- 14.6. Testing HTML forms
- 14.7. Integration testing
- 14.8. Profiling test coverage
- 15. Conclusion
- 16. Further reading and useful links
- 1. Introduction - He recommends using the HTML version of the book b/c you can copy code more easily and navigate the chapters more easily. The book has you create "Snippetbox", a Pastebin type app.
Let's Go Further
- https://lets-go-further.alexedwards.net/
- Covers more-advanced web development topics than the "Let's Go" book.
- Seems more focused on building an API than on building a template-based site.
- Topics include: handling JSON, db migrations, rate limiting, sending emails, auth via JWTs, CORS, deploying to Digital Ocean