|
Go at Google |
|
SPLASH, Tucson, Oct 25, 2012 |
|
|
|
Rob Pike |
|
Google, Inc. |
|
https://go.dev |
|
|
|
* Preamble |
|
|
|
* What is Go? |
|
|
|
Go is: |
|
|
|
- open source |
|
- concurrent |
|
- garbage-collected |
|
- efficient |
|
- scalable |
|
- simple |
|
- fun |
|
- boring (to some) |
|
|
|
.link / go.dev |
|
|
|
* History |
|
|
|
Design began in late 2007. |
|
|
|
Key players: |
|
|
|
- Robert Griesemer, Rob Pike, Ken Thompson |
|
- Later: Ian Lance Taylor, Russ Cox |
|
|
|
Became open source in November 2009. |
|
|
|
Developed entirely in the open; very active community. |
|
Language stable as of Go 1, early 2012. |
|
|
|
* Go at Google |
|
|
|
Go is a programming language designed by Google to help solve Google's problems. |
|
|
|
Google has big problems. |
|
|
|
* Big hardware |
|
|
|
.image splash/datacenter.jpg |
|
|
|
* Big software |
|
|
|
- C++ (mostly) for servers, plus lots of Java and Python |
|
- thousands of engineers |
|
- gazillions of lines of code |
|
- distributed build system |
|
- one tree |
|
|
|
And of course: |
|
|
|
- zillions of machines, which we treat as a modest number of compute clusters |
|
|
|
Development at Google can be slow, often clumsy. |
|
|
|
But it _is_ effective. |
|
|
|
* The reason for Go |
|
|
|
Goals: |
|
|
|
- eliminate slowness |
|
- eliminate clumsiness |
|
- improve effectiveness |
|
- maintain (even improve) scale |
|
|
|
Go was designed by and for people who write—and read and debug and maintain—large software systems. |
|
|
|
Go's purpose is _not_ research into programming language design. |
|
|
|
Go's purpose is to make its designers' programming lives better. |
|
|
|
* Today's theme |
|
|
|
A talk about software engineering more than language design. |
|
|
|
To be more accurate: |
|
|
|
- Language design in the service of software engineering. |
|
|
|
In short: |
|
|
|
- How does a language help software engineering? |
|
|
|
* Features? |
|
|
|
Reaction upon launch: |
|
My favorite feature isn't in Go! Go Sucks! |
|
|
|
This misses the point. |
|
|
|
* Pain |
|
|
|
What makes large-scale development hard with C++ or Java (at least): |
|
|
|
- slow builds |
|
- uncontrolled dependencies |
|
- each programmer using a different subset of the language |
|
- poor program understanding (documentation, etc.) |
|
- duplication of effort |
|
- cost of updates |
|
- version skew |
|
- difficulty of automation (auto rewriters etc.): tooling |
|
- cross-language builds |
|
|
|
Language _features_ don't usually address these. |
|
|
|
* Focus |
|
|
|
In the design of Go, we tried to focus on solutions to _these_ problems. |
|
|
|
Example: indentation for structure vs. C-like braces |
|
|
|
* Dependencies in C and C++ |
|
|
|
* A personal history of dependencies in C |
|
|
|
`#ifdef` |
|
|
|
`#ifndef` "guards": |
|
|
|
#ifndef _SYS_STAT_H_ |
|
|
|
1984: `<sys/stat.h>` times 37 |
|
|
|
ANSI C and `#ifndef` guards: |
|
|
|
- dependencies accumulate |
|
- throw includes at the program until it compiles |
|
- no way to know what can be removed |
|
|
|
* A personal history of dependencies in Plan 9's C |
|
|
|
Plan 9, another take: |
|
|
|
- no `#ifdefs` (or `#ifndefs`) |
|
- documentation and topological sort |
|
- easy to find out what can be removed |
|
|
|
Need to document dependencies, but much faster compilation. |
|
|
|
In short: |
|
|
|
- _ANSI_C_made_a_costly_mistake_ in requiring `#ifndef` guards. |
|
|
|
* A personal history of dependencies in C++ |
|
|
|
C++ exacerbated the problem: |
|
|
|
- one `#include` file per class |
|
- code (not just declarations) in `#include` files |
|
- `#ifndef` guards persist |
|
|
|
2004: Mike Burrows and Chubby: `<xxx>` times 37,000 |
|
|
|
1984: Tom Cargill and pi |
|
|
|
* A personal history of dependencies at Google |
|
|
|
Plan 9 demo: a story |
|
|
|
Early Google: one `Makefile` |
|
|
|
2003: `Makefile` generated from per-directory `BUILD` files |
|
|
|
- explicit dependencies |
|
- 40% smaller binaries |
|
|
|
Dependencies still not checkable! |
|
|
|
* Result |
|
|
|
To build a large Google binary on a single computer is impractical. |
|
|
|
In 2007, instrumented the build of a major Google binary: |
|
|
|
- 2000 files |
|
- 4.2 megabytes |
|
- 8 gigabytes delivered to compiler |
|
- 2000 bytes sent to compiler for every C++ source byte |
|
- it's real work too: `<string>` for example |
|
- hours to build |
|
|
|
* Tools can help |
|
|
|
New distributed build system: |
|
|
|
- no more `Makefile` (still uses `BUILD` files) |
|
- many buildbots |
|
- much caching |
|
- much complexity (a large program in its own right) |
|
|
|
Even with Google's massive distributed build system, a large build still takes minutes. |
|
(In 2007 that binary took 45 minutes; today, 27 minutes.) |
|
|
|
Poor quality of life. |
|
|
|
* Enter Go |
|
|
|
While that build runs, we have time to think. |
|
|
|
Want a language to improve the quality of life. |
|
|
|
And dependencies are only one such problem.... |
|
|
|
* Primary considerations |
|
|
|
Must work at scale: |
|
|
|
- large programs |
|
- large teams |
|
- large number of dependencies |
|
|
|
Must be familiar, roughly C-like |
|
|
|
* Modernize |
|
|
|
The old ways are _old_. |
|
|
|
Go should be: |
|
|
|
- suitable for multicore machines |
|
- suitable for networked machines |
|
- suitable for web stuff |
|
|
|
* The design of Go |
|
|
|
From a software engineering perspective. |
|
|
|
* Dependencies in Go |
|
|
|
* Dependencies |
|
|
|
Dependencies are defined (syntactically) in the language. |
|
|
|
Explicit, clear, computable. |
|
|
|
import "encoding/json" |
|
|
|
Unused dependencies cause error at compile time. |
|
|
|
Efficient: dependencies traversed once per source file... |
|
|
|
* Hoisting dependencies |
|
|
|
Consider: |
|
`A` imports `B` imports `C` but `A` does not directly import `C`. |
|
|
|
The object code for `B` includes all the information about `C` needed to import `B`. |
|
Therefore in `A` the line |
|
|
|
import "B" |
|
|
|
does not require the compiler to read `C` when compiling `A`. |
|
|
|
Also, the object files are designed so the "export" information comes first; compiler doing import does not need to read whole file. |
|
|
|
Exponentially less data read than with `#include` files. |
|
|
|
With Go in Google, about 40X fanout (recall C++ was 2000x) |
|
Plus in C++ it's general code that must be parsed; in Go it's just export data. |
|
|
|
* No circular imports |
|
|
|
Circular imports are illegal in Go. |
|
|
|
The big picture in a nutshell: |
|
|
|
- occasional minor pain, |
|
- but great reduction in annoyance overall |
|
- structural typing makes it less important than with type hierarchies |
|
- keeps us honest! |
|
|
|
Forces clear demarcation between packages. |
|
|
|
Simplifies compilation, linking, initialization. |
|
|
|
* API design |
|
|
|
Through the design of the standard library, great effort spent on controlling dependencies. |
|
|
|
It can be better to copy a little code than to pull in a big library for one function. |
|
(A test in the system build complains if new core dependencies arise.) |
|
|
|
Dependency hygiene trumps code reuse. |
|
|
|
Example: |
|
The (low-level) `net` package has own `itoa` to avoid dependency on the big formatted I/O package. |
|
|
|
* Packages |
|
|
|
* Packages |
|
|
|
Every Go source file, e.g. `"encoding/json/json.go"`, starts |
|
|
|
package json |
|
|
|
where `json` is the "package name", an identifier. |
|
Package names are usually concise. |
|
|
|
To use a package, need to identify it by path: |
|
|
|
import "encoding/json" |
|
|
|
And then the package name is used to qualify items from package: |
|
|
|
var dec = json.NewDecoder(reader) |
|
|
|
Clarity: can always tell if name is local to package from its syntax: `Name` vs. `pkg.Name`. |
|
(More on this later.) |
|
|
|
Package combines properties of library, name space, and module. |
|
|
|
* Package paths are unique, not package names |
|
|
|
The path is `"encoding/json"` but the package name is `json`. |
|
The path identifies the package and must be unique. |
|
Project or company name at root of name space. |
|
|
|
import "google/base/go/log" |
|
|
|
Package name might not be unique; can be overridden. These are both `package`log`: |
|
|
|
import "log" // Standard package |
|
import googlelog "google/base/go/log" // Google-specific package |
|
|
|
Every company might have its own `log` package; no need to make the package name unique. |
|
|
|
Another: there are many `server` packages in Google's code base. |
|
|
|
* Remote packages |
|
|
|
Package path syntax works with remote repositories. |
|
The import path is just a string. |
|
|
|
Can be a file, can be a URL: |
|
|
|
go get github.com/4ad/doozer // Command to fetch package |
|
|
|
import "github.com/4ad/doozer" // Doozer client's import statement |
|
|
|
var client doozer.Conn // Client's use of package |
|
|
|
* Go's Syntax |
|
|
|
* Syntax |
|
|
|
Syntax is not important... |
|
|
|
- unless you're programming |
|
- or writing tools |
|
|
|
Tooling is essential, so Go has a clean syntax. |
|
Not super small, just clean: |
|
|
|
- regular (mostly) |
|
- only 25 keywords |
|
- straightforward to parse (no type-specific context required) |
|
- easy to predict, reason about |
|
|
|
* Declarations |
|
|
|
Uses Pascal/Modula-style syntax: name before type, more type keywords. |
|
|
|
var fn func([]int) int |
|
type T struct { a, b int } |
|
|
|
not |
|
|
|
int (*fn)(int[]); |
|
struct T { int a, b; } |
|
|
|
Easier to parse—no symbol table needed. Tools become easier to write. |
|
|
|
One nice effect: can drop `var` and derive type of variable from expression: |
|
|
|
var buf *bytes.Buffer = bytes.NewBuffer(x) // explicit |
|
buf := bytes.NewBuffer(x) // derived |
|
|
|
For more information: |
|
|
|
.link /s/decl-syntax go.dev/s/decl-syntax |
|
|
|
* Function syntax |
|
|
|
Function on type `T`: |
|
|
|
func Abs(t T) float64 |
|
|
|
Method of type `T`: |
|
|
|
func (t T) Abs() float64 |
|
|
|
Variable (closure) of type `T`: |
|
|
|
negAbs := func(t T) float64 { return -Abs(t) } |
|
|
|
In Go, functions can return multiple values. Common case: `error`. |
|
|
|
func ReadByte() (c byte, err error) |
|
|
|
c, err := ReadByte() |
|
if err != nil { ... } |
|
|
|
More about errors later. |
|
|
|
* No default arguments |
|
|
|
Go does not support default function arguments. |
|
|
|
Why not? |
|
|
|
- too easy to throw in defaulted args to fix design problems |
|
- encourages too many args |
|
- too hard to understand the effect of the fn for different combinations of args |
|
|
|
Extra verbosity may happen but that encourages extra thought about names. |
|
|
|
Related: Go has easy-to-use, type-safe support for variadic functions. |
|
|
|
* Naming |
|
|
|
* Export syntax |
|
|
|
Simple rule: |
|
|
|
- upper case initial letter: `Name` is visible to clients of package |
|
- otherwise: `name` (or `_Name`) is not visible to clients of package |
|
|
|
Applies to variables, types, functions, methods, constants, fields.... |
|
|
|
That Is It. |
|
|
|
Not an easy decision. |
|
One of the most important things about the language. |
|
|
|
Can see the visibility of an identifier without discovering the declaration. |
|
|
|
Clarity. |
|
|
|
* Scope |
|
|
|
Go has very simple scope hierarchy: |
|
|
|
- universe |
|
- package |
|
- file (for imports only) |
|
- function |
|
- block |
|
|
|
* Locality of naming |
|
|
|
Nuances: |
|
|
|
- no implicit `this` in methods (receiver is explicit); always see `rcvr.Field` |
|
- package qualifier always present for imported names |
|
- (first component of) every name is always declared in current package |
|
|
|
No surprises when importing: |
|
|
|
- adding an exported name to my package cannot break your package! |
|
|
|
Names do not leak across boundaries. |
|
|
|
In C, C++, Java the name `y` could refer to anything |
|
In Go, `y` (or even `Y`) is always defined within the package. |
|
In Go, `x.Y` is clear: find `x` locally, `Y` belongs to it. |
|
|
|
* Function and method lookup |
|
|
|
Method lookup by name only, not type. |
|
A type cannot have two methods with the same name, ever. |
|
Easy to identify which function/method is referred to. |
|
Simple implementation, simpler program, fewer surprises. |
|
|
|
Given a method `x.M`, there's only ever one `M` associated with `x`. |
|
|
|
* Semantics |
|
|
|
* Basics |
|
|
|
Generally C-like: |
|
|
|
- statically typed |
|
- procedural |
|
- compiled |
|
- pointers etc. |
|
|
|
Should feel familiar to programmers from the C family. |
|
|
|
* But... |
|
|
|
Many small changes in the aid of robustness: |
|
|
|
- no pointer arithmetic |
|
- no implicit numeric conversions |
|
- array bounds checking |
|
- no type aliases |
|
- `++` and `--` as statements not expressions |
|
- assignment not an expression |
|
- legal (encouraged even) to take address of stack variable |
|
- many more |
|
|
|
Plus some big ones... |
|
|
|
* Bigger things |
|
|
|
Some elements of Go step farther from C, even C++ and Java: |
|
|
|
- concurrency |
|
- garbage collection |
|
- interface types |
|
- reflection |
|
- type switches |
|
|
|
* Concurrency |
|
|
|
* Concurrency |
|
|
|
Important to modern computing environment. |
|
Not well served by C++ or even Java. |
|
|
|
Go embodies a variant of CSP with first-class channels. |
|
|
|
Why CSP? |
|
|
|
- The rest of the language can be ordinary and familiar. |
|
|
|
Must be able to couple concurrency with computation. |
|
|
|
Example: concurrency and cryptography. |
|
|
|
* CSP is practical |
|
|
|
For a web server, the canonical Go program, the model is a great fit. |
|
|
|
Go _enables_ simple, safe concurrent programming. |
|
It doesn't _forbid_ bad programming. |
|
|
|
Focus on _composition_ of regular code. |
|
|
|
Caveat: not purely memory safe; sharing is legal. |
|
Passing a pointer over a channel is idiomatic. |
|
|
|
Experience shows this is a practical design. |
|
|
|
* Garbage collection |
|
|
|
* The need for garbage collection |
|
|
|
Too much programming in C and C++ is about memory allocation. |
|
But also the design revolves too much about memory management. |
|
Leaky abstractions, leaky dependencies. |
|
|
|
Go has garbage collection, only. |
|
|
|
Needed for concurrency: tracking ownership too hard otherwise. |
|
Important for abstraction: separate behavior from resource management. |
|
A key part of scalability: APIs remain local. |
|
|
|
Use of the language is much simpler because of GC. |
|
Adds run-time cost, latency, complexity to the implementation. |
|
|
|
Day 1 design decision. |
|
|
|
* Garbage collection in Go |
|
|
|
A garbage-collected systems language is heresy! |
|
Experience with Java: Uncontrollable cost, too much tuning. |
|
|
|
But Go is different. |
|
Go lets you limit allocation by controlling memory layout. |
|
|
|
Example: |
|
|
|
type X struct { |
|
a, b, c int |
|
buf [256]byte |
|
} |
|
|
|
Example: Custom arena allocator with free list. |
|
|
|
* Interior pointers |
|
|
|
Early decision: allow interior pointers such as `X.buf` from previous slide. |
|
|
|
Tradeoff: Affects which GC algorithms that can be used, but in return reduces pressure on the collector. |
|
|
|
Gives the _programmer_ tools to control GC overhead. |
|
|
|
Experience, compared to Java, shows it has significant effect on memory pressure. |
|
|
|
GC remains an active subject. |
|
Current design: parallel mark-and-sweep. |
|
With care to use memory wisely, works well in production. |
|
|
|
* Interfaces |
|
|
|
Composition not inheritance |
|
|
|
* Object orientation and big software |
|
|
|
Go is object-oriented. |
|
Go does not have classes or subtype inheritance. |
|
|
|
What does this mean? |
|
|
|
* No type hierarchy |
|
|
|
O-O is important because it provides uniformity of interface. |
|
Outrageous example: the Plan 9 kernel. |
|
|
|
Problem: subtype inheritance encourages _non-uniform_ interfaces. |
|
|
|
* O-O and program evolution |
|
|
|
Design by type inheritance oversold. |
|
Generates brittle code. |
|
Early decisions hard to change, often poorly informed. |
|
Makes every programmer an interface designer. |
|
(Plan 9 was built around a single interface everything needed to satisfy.) |
|
|
|
Therefore encourages overdesign early on: predict every eventuality. |
|
Exacerbates the problem, complicates designs. |
|
|
|
* Go: interface composition |
|
|
|
In Go an interface is _just_ a set of methods: |
|
|
|
type Hash interface { |
|
Write(p []byte) (n int, err error) |
|
Sum(b []byte) []byte |
|
Reset() |
|
Size() int |
|
BlockSize() int |
|
} |
|
|
|
No `implements` declaration. |
|
All hash implementations satisfy this implicitly. (Statically checked.) |
|
|
|
* Interfaces in practice: composition |
|
|
|
Tend to be small: one or two methods are common. |
|
|
|
Composition falls out trivially. Easy example, from package `io`: |
|
|
|
type Reader interface { |
|
Read(p []byte) (n int, err error) |
|
} |
|
|
|
`Reader` (plus the complementary `Writer`) makes it easy to chain: |
|
|
|
- files, buffers, networks, encryptors, compressors, GIF, JPEG, PNG, ... |
|
|
|
Dependency structure is not a hierarchy; these also implement other interfaces. |
|
|
|
Growth through composition is _natural_, does not need to be pre-declared. |
|
|
|
And that growth can be _ad_hoc_ and linear. |
|
|
|
* Compose with functions, not methods |
|
|
|
Hard to overstate the effect that Go's interfaces have on program design. |
|
|
|
One big effect: functions with interface arguments. |
|
|
|
func ReadAll(r io.Reader) ([]byte, error) |
|
|
|
Wrappers: |
|
|
|
func LoggingReader(r io.Reader) io.Reader |
|
func LimitingReader(r io.Reader, n int64) io.Reader |
|
func ErrorInjector(r io.Reader) io.Reader |
|
|
|
The designs are nothing like hierarchical, subtype-inherited methods. |
|
Much looser, organic, decoupled, independent. |
|
|
|
* Errors |
|
|
|
* Error handling |
|
|
|
Multiple function return values inform the design for handling errors. |
|
|
|
Go has no `try-catch` control structures for exceptions. |
|
Return `error` instead: built-in interface type that can "stringify" itself: |
|
|
|
type error interface { Error() string } |
|
|
|
Clear and simple. |
|
|
|
Philosophy: |
|
|
|
Forces you think about errors—and deal with them—when they arise. |
|
Errors are _normal_. Errors are _not_exceptional_. |
|
Use the existing language to compute based on them. |
|
Don't need a sublanguage that treats them as exceptional. |
|
|
|
Result is better code (if more verbose). |
|
|
|
* (OK, not all errors are normal. But most are.) |
|
|
|
.image splash/fire.jpg |
|
|
|
* Tools |
|
|
|
* Tools |
|
|
|
Software engineering requires tools. |
|
|
|
Go's syntax, package design, naming, etc. make tools easy to write. |
|
|
|
Standard library includes lexer and parser; type checker nearly done. |
|
|
|
* Gofmt |
|
|
|
Always intended to do automatic code formatting. |
|
Eliminates an entire class of argument. |
|
Runs as a "presubmit" to the code repositories. |
|
|
|
Training: |
|
|
|
- The community has always seen `gofmt` output. |
|
|
|
Sharing: |
|
|
|
- Uniformity of presentation simplifies sharing. |
|
|
|
Scaling: |
|
|
|
- Less time spent on formatting, more on content. |
|
|
|
Often cited as one of Go's best features. |
|
|
|
* Gofmt and other tools |
|
|
|
Surprise: The existence of `gofmt` enabled _semantic_ tools: |
|
Can rewrite the tree; `gofmt` will clean up output. |
|
|
|
Examples: |
|
|
|
- `gofmt`-r`'a[b:len(a)]`->`a[b:]'` |
|
- `gofix` |
|
|
|
And good front-end libraries enable ancillary tools: |
|
|
|
- `godoc` |
|
- `go`get`, `go`build`, etc. |
|
- `api` |
|
|
|
* Gofix |
|
|
|
The `gofix` tool allowed us to make sweeping changes to APIs and language features leading up to the release of Go 1. |
|
|
|
- change to map deletion syntax |
|
- new time API |
|
- many more |
|
|
|
Also allows us to _update_ code even if the old code still works. |
|
|
|
Recent example: |
|
|
|
Changed Go's protocol buffer implementation to use getter functions; updated _all_ Google Go code to use them with `gofix`. |
|
|
|
* Conclusion |
|
|
|
* Go at Google |
|
|
|
Go's use is growing inside Google. |
|
|
|
Several big services use it: |
|
|
|
- golang.org |
|
- youtube.com |
|
- dl.google.com |
|
|
|
Many small ones do, many using Google App Engine. |
|
|
|
* Go outside Google |
|
|
|
Many outside companies use it, including: |
|
|
|
- BBC Worldwide |
|
- Canonical |
|
- Heroku |
|
- Nokia |
|
- SoundCloud |
|
|
|
* What's wrong? |
|
|
|
Not enough experience yet to know if Go is truly successful. |
|
Not enough big programs. |
|
|
|
Some minor details wrong. Examples: |
|
|
|
- declarations still too fussy |
|
- `nil` is overloaded |
|
- lots of library details |
|
|
|
`Gofix` and `gofmt` gave us the opportunity to fix many problems, ranging from eliminating semicolons to redesigning the `time` package. |
|
But we're still learning (and the language is frozen for now). |
|
|
|
The implementation still needs work, the run-time system in particular. |
|
|
|
But all indicators are positive. |
|
|
|
* Summary |
|
|
|
Software engineering guided the design. |
|
But a productive, fun language resulted because that design enabled productivity. |
|
|
|
Clear dependencies |
|
Clear syntax |
|
Clear semantics |
|
Composition not inheritance |
|
Simplicity of model (GC, concurrency) |
|
Easy tooling (the `go` tool, `gofmt`, `godoc`, `gofix`) |
|
|
|
* Try it! |
|
|
|
.link / go.dev |
|
|
|
.image splash/appenginegophercolor.jpg |
|
|