Go Language Fundamentals
Go Language Fundamentals
Go (or Golang) is a statically typed, compiled language designed at Google. It's known for its simplicity, excellent performance, and first-class support for concurrency. This guide covers the fundamentals you need to start writing Go.
Hello, World!
Every Go program starts with a package declaration. The main package is special - it defines an executable program:
package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
}
# Run the program
go run main.go
# Build an executable
go build -o myapp main.go
./myapp
Variables and Types
Go is statically typed, but type inference makes declarations concise:
package main
import "fmt"
func main() {
// Explicit type declaration
var name string = "Alice"
var age int = 30
// Type inference (preferred)
count := 42
price := 19.99
isActive := true
// Multiple declarations
var x, y, z int = 1, 2, 3
a, b := "hello", "world"
// Constants
const MaxItems = 100
const (
StatusPending = iota // 0
StatusActive // 1
StatusComplete // 2
)
// Zero values (Go initializes variables to zero values)
var i int // 0
var s string // ""
var p *int // nil
var slice []int // nil
fmt.Println(name, age, count, price, isActive)
}
Functions
Go functions can return multiple values, making error handling explicit and clean:
package main
import (
"errors"
"fmt"
)
// Multiple return values
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
// Named return values (useful for documentation)
func rectangle(w, h float64) (area, perimeter float64) {
area = w * h
perimeter = 2 * (w + h)
return // naked return uses named values
}
// Variadic function
func sum(nums ...int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
// Function as a value
func apply(fn func(int) int, value int) int {
return fn(value)
}
func main() {
result, err := divide(10, 2)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Result:", result) // 5
area, perim := rectangle(3, 4)
fmt.Printf("Area: %.2f, Perimeter: %.2f\n", area, perim)
fmt.Println(sum(1, 2, 3, 4, 5)) // 15
double := func(x int) int { return x * 2 }
fmt.Println(apply(double, 5)) // 10
}
Structs and Methods
Go uses structs instead of classes. Methods are defined separately from struct declarations:
package main
import "fmt"
// Struct definition
type User struct {
ID int
Name string
Email string
IsActive bool
}
// Method with value receiver (doesn't modify the struct)
func (u User) FullName() string {
return u.Name
}
// Method with pointer receiver (can modify the struct)
func (u *User) Deactivate() {
u.IsActive = false
}
// Constructor function (Go convention)
func NewUser(id int, name, email string) *User {
return &User{
ID: id,
Name: name,
Email: email,
IsActive: true,
}
}
func main() {
// Create struct instances
user1 := User{ID: 1, Name: "Alice", Email: "alice@example.com"}
user2 := NewUser(2, "Bob", "bob@example.com")
fmt.Println(user1.FullName()) // Alice
fmt.Println(user2.IsActive) // true
user2.Deactivate()
fmt.Println(user2.IsActive) // false
}
Interfaces
Go interfaces are implicit - any type that implements the required methods satisfies the interface:
package main
import "fmt"
// Interface definition
type Writer interface {
Write(data []byte) (int, error)
}
type Reader interface {
Read(p []byte) (int, error)
}
// Composed interface
type ReadWriter interface {
Reader
Writer
}
// Any type with a Write method satisfies Writer
type ConsoleWriter struct{}
func (cw ConsoleWriter) Write(data []byte) (int, error) {
n, err := fmt.Print(string(data))
return n, err
}
// Function accepting interface
func writeData(w Writer, data string) {
w.Write([]byte(data))
}
func main() {
cw := ConsoleWriter{}
writeData(cw, "Hello, interfaces!\n")
}
Goroutines and Channels
Go's killer feature is goroutines - lightweight threads managed by the Go runtime. Channels provide safe communication between goroutines:
package main
import (
"fmt"
"net/http"
"time"
)
// Goroutine function
func fetchURL(url string, ch chan<- string) {
start := time.Now()
resp, err := http.Get(url)
if err != nil {
ch <- fmt.Sprintf("%s: error - %v", url, err)
return
}
defer resp.Body.Close()
elapsed := time.Since(start)
ch <- fmt.Sprintf("%s: %d (%v)", url, resp.StatusCode, elapsed)
}
func main() {
urls := []string{
"https://www.google.com",
"https://www.github.com",
"https://www.golang.org",
}
// Buffered channel
ch := make(chan string, len(urls))
// Launch goroutines
for _, url := range urls {
go fetchURL(url, ch) // 'go' keyword launches a goroutine
}
// Collect results
for range urls {
fmt.Println(<-ch) // Receive from channel
}
}
Concurrency deep dive
Channels are typed conduits for sending and receiving values. They're the preferred way to communicate between goroutines.
Buffered channels (make(chan string, 10)) allow sending without an immediate receiver, up to the buffer size.
Select statement lets you wait on multiple channel operations:
go
select {
case msg := <-ch1:
fmt.Println(msg)
case ch2 <- value:
fmt.Println("sent")
case <-time.After(1 * time.Second):
fmt.Println("timeout")
}
Error Handling
Go handles errors explicitly through return values, not exceptions:
package main
import (
"errors"
"fmt"
"os"
)
// Custom error type
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("%s: %s", e.Field, e.Message)
}
// Function that returns an error
func readConfig(path string) ([]byte, error) {
data, err := os.ReadFile(path)
if err != nil {
// Wrap errors with context
return nil, fmt.Errorf("failed to read config: %w", err)
}
return data, nil
}
func main() {
data, err := readConfig("config.json")
if err != nil {
// Check for specific error types
if errors.Is(err, os.ErrNotExist) {
fmt.Println("Config file not found, using defaults")
} else {
fmt.Printf("Error: %v\n", err)
os.Exit(1)
}
}
fmt.Printf("Config loaded: %d bytes\n", len(data))
}
Key Takeaways
- Go is simple by design - there's usually one obvious way to do things
- Use := for short variable declarations within functions
- Functions can return multiple values; use this for error handling
- Interfaces are satisfied implicitly - very powerful for testing
- Goroutines + channels = safe, easy concurrency
- Error handling is explicit - check errors, don't ignore them
- Use 'go fmt' to format code - all Go code looks the same
- The standard library is excellent - explore it before reaching for packages
Related Posts
Introduction to Machine Learning with Python
Start your ML journey with Python and scikit-learn. Build your first machine learning models with practical examples.
1 min read
Docker for Developers: A Practical Guide
Everything you need to know about Docker for local development. From Dockerfiles to Docker Compose, master containerization.
1 min read
Building Interactive UIs with Django and HTMX
Learn how to create dynamic, JavaScript-free interfaces using Django and HTMX. Build modern web apps without the complexity of SPAs.
1 min read
10 Tailwind CSS Tips You Need to Know
Level up your Tailwind CSS skills with these 10 practical tips. From responsive design to custom configurations, become a Tailwind power user.
1 min read
Comments
Log in to leave a comment.
Log In
Loading comments...