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.
chmod
on a Windows file, it just does nothing, without raising an exception.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.
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 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".
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".
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.
"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.
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.
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.
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.
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.
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.
We write tools in C and C++ when it’s necessary (like for high-efficiency, high-speed code at the system level).
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.
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.
.then(func1).then(func2).then(func3)
in JavaScript, where each function gets the output of the previous function).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
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.
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)
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.
C# seems to be just a much more fun and feature rich language to use, especially when it comes to eliminating boilerplate.
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.
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.
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 by if 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 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.
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.
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.
No:
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
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)
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
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
sqlh
is essentially following my specific pain points when using database/sql
defer
instead of try-finally
- It's just different syntax.import "github.com/some-user-or-org/some-go-module/some-go-package-that-you-need"
requirements.txt
is Go’s go.mod
file.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.
var ( ToBe bool = false MaxInt uint64 = 1<<64 - 1 z complex128 = cmplx.Sqrt(-5 + 12i) ) |
const
keyword and =, they cannot be declared using the := syntax. Example: const Pi = 3.14
while
keyword, you just use for
with no "init" or "post" statements.while True:
in Python is just for {
).if perr, ok := err.(*os.PathError); ok {
switch
statement, it doesn't do fall-through, it only runs the code in the matching block.switch
statement that should be compared to a value in each case
statement or you can just do "switch" by itself and then have each case
statement be a boolean comparison/function/whatever.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.defer
is how Go accomplishes the same thing as the with open(...):
context manager block in Python.main
and have a function named main
.Hello World:
package main import "fmt" func main() { fmt.Println("Hi there!") } |
go run <files-to-compile>
to run your code.:=
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).type deck []string
func (d deck) print(<args go here>) { <code goes here> }
my_deck.print()
cards := [<size>]string
cards := [<size>]string{<put-initial-contents-here-if-you-want>}
cards := []string{<put-initial-contents-here-if-you-want>}
cards = append(cards, newElement)
[]byte
is read as "slice byte", not "array of bytes".len()
to get the length of a slice.for i, card := range cards { <do-stuff> }
for _, card
[]byte("Hello World!")
When importing multiple packages in a file, use this syntax:
import ( "package name one in quotes" "package name two in quotes" ) |
nil
, not null
or None
.Println(err)
to print an error.os
package function Exit()
with an argument of 1
which indicates that something went wrong._test.go
and then run go test
t *testing.T
t.Errorf("Description of problem")
to notify the test handler of a failure.type Card struct { Value string Suit string } type Deck struct { Cards []Card } deck := Deck{Cards: []Card{{Value: "Ace", Suit: "Spades"}}} |
&
operator: myStructMemoryAddress := &myStruct
*
operator: myStruct := *myStructMemoryAddress
*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.my_map := map[string]string { }
for keyVar, valueVar := range myMap {
Example interface:
type myInterface interface { myRequiredFunction(string) string } |
os.Stdout
would work as a Writer
type: os.Stdout
is of type File
, and type File
has a function of type Write
, and since we know that any type that implements that function (including the required parameter types and return types) is of type Writer
, we therefore know that os.Stdout
would be accepted by io.Copy()
.go
keyword before the call: go doMyThing()
c := make(chan string)
->
func(<args>) { myCodeGoesHere() }
package main import "fmt" func main() { fmt.Println("Hi there!") } |
go run main.go
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.main
.func
in our hello world program is short for "function". Functions in Go work just like functions in other languages.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
Cards
package that simulates a deck of playing cards.cards
.main.go
file.Add some boilerplate code:
package main func main() { } |
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 type string
will ever be assigned to this variable."Ace of Spades"
is the initial value we want to assign to the variable.bool
, string
, int
, float64
:=
syntax:string
in the variable definition if you're assigning an initial value of that type.card := "Ace of Spades"
:=
syntax when assigning a new variable. If we're assigning a new value to an existing variable, we don't include the colon.:=
a second time, you'll get an error message.He starts with the following code:
package main import "fmt" func main() { card := "Ace of Spades" fmt.Println(card) } |
main()
.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" } |
too many arguments to return. have (string), want ()
.func newCard() string {
, adding the word string
.string
in the function definition to int
, and shows how that pops up an error on the return
line letting you know you're returning the wrong type of value.cards := []string{}
strings
(plural).{"Ace of Diamonds", newCard()}
cards = append(cards, newElement)
To iterate over a slice use range
:
for i, card := range cards { fmt.Println(i, card)} |
:=
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.Cards
project in earnest.Deck
class and use it to create a Deck
instance variable. The instance would have deck-related methods attached to it.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.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.deck.go
we'll create the new type: type deck []string
deck
in our code should be treated as being exactly the same thing as if it saw []string
(a slice of type string
).[]string
where it appears in our main.go
file with deck
.deck.go
:go run main.go deck.go
To define a receiver function, use this syntax:
func (d deck) print() {} |
cards.print()
cardSuits
and cardValues
slices and then use two for-loops to create all the different combinations.deal
function. It's going to return two decks: a "hand" deck and a "remaining" deck.[0:2
]
is the first two elements.func deal(d deck, handSize int) (deck, deck) { }
hand, remainingDeck := deal(cards, 5)
saveToFile()
.WriteFile
function in the ioutil
package to write to the hard drive.[]byte
which makes me wonder how people distinguish between an array and a slice in function definitions.WriteFile
function.[]byte("Hello World!")
toString
that turns a deck into a string. We will make it a receiver function.toString
function we first convert the deck
object to a slice of string: []string(d)
strings
default package, specifically the Join
function.To import multiple packages we use this syntax:
import ( "package name one in quotes" "package name two in quotes" ) |
saveToFile()
receiver function.filename
that WriteFile
takes as an argument.error
since that's something that can be returned from the WriteFile
function we're going to use.permission
parameter to WriteFile
we'll use 0666.ioutil
is a subpackage of the io
package.newDeckFromFile()
function.nil
instead of null
or None
.nil
is to add an if statement to add code to handle the case when the value is nil
.os
package function Exit()
with an argument of 1
which indicates that something went wrong.Split()
function in the strings
package to split up the comma-joined list of cards that we load from the file: s = strings.Split(string(bs), ",")
deck(s)
newDeckFromFile()
and confirms it works.Intn()
function in the rand
subpackage of the math
package to generate random numbers.shuffle()
function a receiver function that modifies the deck we call it from.len()
to get the length of a slice.d[i], d[newPosition] = d[newPosition], d[i]
shuffle()
function is that the last four elements in our deck are always the same.Rand
in the docs. It's "a source of random numbers". When we create an object of this type with the New()
function, we must specify a source (which is of type Source
).Source
is "a source of uniformly-distributed pseudo-random int64 values in the range 0-2^62".Source
we need to pass in a randomly-chosen int64
.UnixNano()
in the time
package: time.Now().UnixNano()
.mod
file. To create one, run go mod init cards
_test.go
go test
package <your-package-name>
newDeck
function there are three things he can think of:_test.go
file, and for each function in that file that we want to test, we create a function named Test<YourFunctionName>
.TestSaveToDeckAndNewDeckFromFile
if
statements to check the output.t *testing.T
which is our test handler.t.Errorf("Write a description of the problem here")
("Your message: %v", len(d))
if d[0] != "Ace of Spades" { t.Errorf("...") }
if d[len(d) - 1] != "Four of Clubs" { t.Errorf("...") }
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"._decktesting
file we'll be using both before running and after running.Remove
function in the os
package.Remove
function can return an error, but we can ignore that for our purposes.TestSaveToDeckAndNewDeckFromFile
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 said cards.deal(5)
it would look like we're modifying the cards
slice by removing 5 cards.t *testing.T
argument to each of our tests, but we don't know yet what the *
is for./structs
and a new main.go
person
struct with a firstName
field and a lastName
field.person
object with syntax like the following: alex := person("Alex", "Anderson")
Println
the struct you'll get a list of the values.var alex person
null
or undefined
%+v
in our Println
call to get a printout of both the keys and values of the struct.var
syntax when the coders want the instantiated struct to use the default values of the struct.alex.firsname = 'new name'
contactInfo
struct that is used for a contact
attribute of a person
struct.He goes through an example of creating a person
struct and having an embedded definition of a contactInfo
struct:
jim := person{ firstName: "Jim", lastName: "Party", contactInfo: contactInfo{ email: "jim@gmail.com", zipCode: 94000, }, } |
contactInfo: contactInfo
you can just have contactInfo
.func (p person) print() { }
updateName(newName string)
receiver function and shows that it doesn't actually modify the object that calls the updateName()
function. This is a segue into the next section.person
object, the computer takes that object and puts it at a particular address (cubby) in RAM.person
struct to the updateName
receiver function.jimPointer := &jim
updateName
to instead be called by the pointer: jimPointer.updateName("jimmy")
updateName
function definition: func (pointerToPerson *person) updateName(newFirstName string) { }
updateName
that changes the name: (*pointerToPerson).firstName = newFirstName
&
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".*
when seen in a type description vs. *
when seen on a 'normal' line of code.*
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.*
on a line of code like *pointerToPerson.updateName("jimmy")
then it is being used as an operator and is actively dereferencing the pointer.jimPointer := &jim; jimPointer.updateName("jimmy")
we can just keep it as jim.updateName("jimmy")
and it will work (it will pass to the receiver function by reference) as long as the receiver is type pointerToPerson *person
The syntax for defining a map is map[key_type]value_type
my_map := map[string]string { } |
To create an empty map:
var colors map[string]string or colors := make(map[string]string) |
colors["white"] = "#ffffff"
delete(mapName, keyName)
To iterate over a map:
for keyVar, valueVar := range myMap { } |
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?englishBot
struct and a spanishBot
struct.getGreeting
receiver function that returns a greeting in the bot's language, and a printGreeting
receiver function.printGreeting
function will probably just do something like Println(bot.getGreeting())
getGreeting
functions' implementations will be very different, but the printGreeting
functions will have very similar logic.englishBot
struct (no fields), the spanishBot
struct (no fields), and getGreeting
receiver functions for each that return different strings.func (englishBot) getGreeting() string { }
He defines the interface:
type bot interface { getGreeting() string } |
He defines the generic form of printGreeting
:
func printGreeting(b bot) { fmt.Println(b.getGreeting()) } |
getGreeting
that returns a string, you are now an honorary member of type 'bot', and you now get access to the function printGreeting
."http
directory to hold the project code, and creates a main.go
file.http
package in the official Go docs and sees how to create a GET request, and then navigates to the type Response
section of the docs to arrive at the section dedicated to the GET
function.http://)
when making a request.Println
the response object and explains that while this would work in many other languages, it won't work in Go.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.Response
type has a Body
property of type io.ReadCloser
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 just Reader
and Closer
Reader
type and sees it's an interface that needs to define a read
function that takes a slice of bytes and returns an int
and an error.ReadCloser
that just had Reader
and Closer
fields? It's a way for us to define an interface in terms of another; what it means is, "in order to satisfy the ReadCloser
interface, you need to satisfy the Reader
interface and the Closer
interface".printGreeting
function).Reader
interface all over Go.Reader
interface needs to implement a Read
function.Read
function works is that the code that wants to call ReaderObject.Read()
passes it a byte slice, and then the Reader takes its source of data and pushes it to that provided byte slice.Read()
is the number of bytes that were put into the slice.Read()
function.bs := make([]byte, 99999)
Read()
function isn't set up to automatically resize the slice if it's already full. (NW: Ugh, what a nightmare.)resp.Body.Read(bs)
fmt.Println(string(bs))
io.Copy(os.Stdout, resp.Body)
Writer
interface which does the opposite of the Reader
interface: it takes a slice of bytes and writes it to some form of output (HTTP request, text file, etc.).Writer
interface and sees that to satisfy it, a type needs to implement a Write
function.Copy()
function and sees that it requires two arguments: first, something that implements the Writer
interface, and secondly something that implements the Reader
interface.os.Stdout
would work as a Writer
type: os.Stdout
is of type File
, and type File
has a function of type Write
, and since we know that any type that implements that function (including the required parameter types and return types) is of type Writer
, we therefore know that os.Stdout
would be accepted by io.Copy()
.io.Copy()
so we can understand how we'd implement the Writer
interface in our own type.Copy
function in the code, and it shows the source code in a little window.Copy
and a new tab opens to the source code.Copy
function immediately passes the arguments it receives to a copyBuffer
function, and navigates to that function's source code.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 the Reader
interface).Reader
and piping it to the Writer
32kb at a time until the Reader
doesn't have anything left to give.Writer
interface.Writer
and pass it to io.Copy()
He creates an empty logWriter
type with the necessary Write
function:
type logWriter struct{} (...) func (logWriter) Write(bs []byte) (int, error) {} |
Write
function just return 1, nil
but not do anything else. He runs the code (using a logWriter object with io.Copy
) and shows that it doesn't raise any error.He then does a working implementation of Write
:
func (logWriter) Write(bs []byte) (int, error) { fmt.Println(string(bs)) return len(bs), nil } |
getArea()
receiver function.shape
interface that requires a getArea()
receiver function and provides a printArea()
function.os.Args
is a slice of string that has the command line arguments passed to our program.checkLink
that takes a link and makes an HTTP request.http.Get(link)
call, the main Go routine is blocked from continuing until it receives a response.go
keyword in front of the checkLink
function to have it run in parallel in a new Go routine: go checkLink(link)
checkLink
project code we'd written to work with Go routines by just adding the keyword go
in front of the call to checkLink(link)
c := make(chan string)
string
can be whatever type you want.func checkLink(link string, c chan string) {
<-
operator: myChannel <- 5
myNumber <- myChannel
fmt.Println(<- myChannel)
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.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.fmt.Println(<- c)
line after his first one and runs the code again and sees two messages in the output.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.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.Println
statements.Println
calls, we're going to use a for loop: for i := 0; i < len(links); i++ {
for {
This is the full code for the repeating part:
for { go checkLink(<- c, c) } |
<-c
is receiving links sent into the channel by finished routines and then immediately passing them to a new routine.An equivalent syntax for our loop in the previous section is:
for l := range c { checkLink(l, c) } |
range c
will wait for a value to arrive on the channel c
time.Second
is of type Duration
time.Sleep(5 * time.Second)
time.Sleep()
call to the checkLink()
function but then points out that this kind of "pollutes" the nature of checkLink()
His proposed solution is to instead have the go
call initiate a function literal that contains the call to time.Sleep()
and the call to checkLink()
:
for l := range c { go func() { time.Sleep(5 * time.Second) checkLink(l, c) }() } |
checkLink()
saying "range variable l captured by func literal".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) } |
go version
to check your version of Go.go mod init snippetbox.nathanwailes.com
in the project directory and it should create a go.mod
file.main.go
file.mux.HandleFunc(url, handler)
to add a new mapping of URL to handler.http.ListenAndServe(port, servemux)
to start the web server./snippet/view
and /snippet/create
httpNotFound
curl
commands he has in his book, I just need to have it as curl.exe
because "on Windows, curl is an alias for Invoke-WebRequest in PowerShell".int()
is strconv.Atoi()
fmt.Printf
function takes an io.Writer
object and we passed it an http.ResponseWriter
object instead, but it's fine because it satisfies the io.Writer
interface.cmd
, internal
, and ui
directories.cmd
holds app-specific code.web
subdirectory.cli
subdirectory (NW: would this be separately-compiled or just a new Go file?).main.go
file and a handlers.go
file.go run ./cmd/web
internal
holds potentially-reusable, non-app-specific code like validation code and database models.internal
can only be imported by code within the parent of that directory.ui
holds HTML, CSS, images, etc.