Packages & visibility

Station 1.11 13 min Interchange: namespace & public / private & PSR-4 → packages

PHP organises code with namespaces, classes and the PSR-4 autoloader, and controls access with public, protected and private on every property and method. Go replaces all of that with two rules. A package is a directory, and a name that starts with a capital letter is visible outside it.

A package is a directory

Every .go file starts with package something, and every file in one directory must say the same thing. Together they're one package. Split it across ten files or one: to Go, it's the same package.

Files in a package never import each other. They simply share everything: every function, type, constant and variable, in any file, is visible to all the others. There's no file-level privacy, and no require.

To use another package, you import it by its import path: the module path from go.mod (lesson 0.4) followed by the directory.

import "example.com/tube/board"

board.Row(d) // the last part of the path is the name
flowchart TB
  accTitle: package main importing package board
  accDescr: package main, in the module root, imports example.com/tube/board. That lets it use board's exported names, Row and Width. board's lower-case names, pad and filler, stay invisible to main.
  m["package main"]
  m -->|"can use"| ex["board.Row, board.Width<br/>exported"]
  m -.-x|"can't see"| pr["board.pad, board.filler<br/>private to board"]

There's no autoloader and no use statement doing the work behind the scenes. Every package a file uses is listed at the top of that file, and an unused one doesn't compile (0.2).

Capital letters are public

package board

const Width = 12   // exported
const filler = " " // private to board

func Row(dest string) string { /* ... */ }
func pad(s string) string    { /* ... */ }

That's the whole visibility system. A name that starts with an upper-case letter is exported, so other packages can use it as board.Row. A lower-case name is visible only inside its own package. The rule applies to everything with a name: functions, types, constants, variables, struct fields and, in Unit 3, methods.

Mind the gap

"Private" means private to the package, not to a type. PHP's private hides a property from everything outside its class. In Go, any code in the same package can read a lower-case field. And Go has no protected at all, because there's no inheritance to protect things from (3.4). If something must be hidden from the rest of the package, it needs a package of its own.

Fields follow the same rule, and this is the one that catches people. A struct called Departure can be exported while its dest field isn't. Code outside the package then gets unknown field Dest in struct literal of type board.Departure, but does have unexported dest. The standard library plays by the same rules, so encoding/json silently skips lower-case fields as well. You'll meet that in 7.3.

Go code tends not to write getters for plain data. If a field should be readable outside the package, export it. Methods that guard a value, with rules about what it may hold, come in Unit 3.

Naming a package

Package names are short, lower case and one word: board, strconv, http. No underscores, no Utils, no Helpers.

PHPBoard\BoardService::boardRow()
Go, week oneboard.BoardRow()
Go, month twoboard.Row()
The package already said "board". Once is enough.

Callers always write the package name in front, so don't repeat it in what you export. board.BoardRow stutters. board.Row reads properly. If you're used to App\Services\Board\BoardService, this takes some unlearning, but your call sites get much shorter.

Symfony corner

Symfony's autowiring scans src/, registers each class as a service and injects them by type, so a controller can ask for a BoardFormatter and simply get one. Go has no container and no scanning. A package is used only when something imports it, and main builds whatever each part needs and passes it in, in plain code, as 0.2 promised.

Tip

No import cycles. If board imports timetable, then timetable can't import board, directly or by a longer route. The build fails with import cycle not allowed. PHP's autoloader never minded. When you hit this, it usually means the shared piece belongs in a third package that both can import. Lesson 5.1 covers project layout, including the special internal/ directory.

Recap

  • A package is every .go file in one directory. Files in a package share everything, without imports.
  • Import by path, the module path plus the directory, and refer to the package by its last part: board.Row.
  • A capital first letter exports a name. Lower case is private to the package. There's no protected.
  • Struct fields follow the same rule, and lower-case fields are invisible to other packages, encoding/json included.
  • Short package names, no stutter, and no import cycles.

1public and private

PHPvisibility.php
<?php
namespace App\Board;

final class Formatter
{
    public const WIDTH = 12;
    private const FILLER = ' ';

    public function row(string $dest): string { /* ... */ }
    private function pad(string $s): string { /* ... */ }
}
GOvisibility.go
package board

const Width = 12   // exported: Width
const filler = " " // package-private

func Row(dest string) string { /* ... */ }
func pad(s string) string    { /* ... */ }

Mind the gap

No keywords. A capital first letter exports a name from its package. Anything lower case is private to the package, not to a class. There's no protected.

2Namespaces and imports

PHPimports.php
<?php
namespace App\Console;

use App\Board\Formatter; // autoloaded via PSR-4

echo (new Formatter())->row('Epping');
GOimports.go
package main

import (
	"fmt"

	"example.com/tube/board" // module path + directory
)

func main() {
	fmt.Println(board.Row("Epping"))
}

Mind the gap

No autoloader. You import a package by its path, the module path from go.mod plus the directory, then use its short name: board.Row.

3Fields follow the same rule

PHPfields.php
<?php
final class Departure
{
    public function __construct(
        public string $dest,
        private int $platform,
    ) {}
}

$d = new Departure('Epping', 3);
echo $d->dest;     // fine
echo $d->platform; // Error: private
GOfields.go
type Departure struct {
	Dest     string // exported
	platform int    // board package only
}

// in package main:
d := board.Departure{Dest: "Epping"}
fmt.Println(d.Dest)     // fine
fmt.Println(d.platform) // compile error

Mind the gap

A lower-case struct field is invisible outside its package. That includes encoding/json, which silently skips it (lesson 7.3).