Go Language Modularization: Difference between revisions

From NovaOrdis Knowledge Base
Jump to navigation Jump to search
No edit summary
No edit summary
Line 157: Line 157:
A ''package'' is a unit of compiled code and a namespace: it consists in one or more source files that declare [[Go Concepts - Lexical Structure#Constants|constants]], [[Go Concepts - Lexical Structure#Variables|variables]], [[Go Concepts - The Type System#Type_Definition|types]] and [[Go Concepts - Functions#Function_Definition|functions]] belonging to the package and which are accessible in all files of the same package. Packages are Go's way of organizing and reusing code, according to the principle [[DRY|Don't Repeat Yourself]].  
A ''package'' is a unit of compiled code and a namespace: it consists in one or more source files that declare [[Go Concepts - Lexical Structure#Constants|constants]], [[Go Concepts - Lexical Structure#Variables|variables]], [[Go Concepts - The Type System#Type_Definition|types]] and [[Go Concepts - Functions#Function_Definition|functions]] belonging to the package and which are accessible in all files of the same package. Packages are Go's way of organizing and reusing code, according to the principle [[DRY|Don't Repeat Yourself]].  


Every Go source code must belong to a package, and it must declare the package it belongs to with the <tt>[[Go_Concepts_-_Packages#The_package_keyword|package]]</tt> keyword.
Every Go source code must belong to a package, and it must declare the package it belongs to with the <tt>[[Go_Language_Modularization#The_package_keyword|package]]</tt> keyword.


All source file declared as being part of the same package '''must''' be in the same file system directory. You cannot split a package across multiple directories.
All source file declared as being part of the same package '''must''' be in the same file system directory. You cannot split a package across multiple directories.

Revision as of 22:50, 6 September 2023

External

Internal

Overview

A standard organization of the files that are part of a project makes easier to share code with other people who also use the same standard. Go workspaces encourage such a standard.

Workspaces

The standard workspace layout is:

. 
├─ src
├─ pkg
└─ bin

This layout is recommended, but not enforced.

A workspace may contain multiple projects.

Define the relationship between workspace and the GOPATH variable.

Project

Packages

A package is a group of related source files. A package can be imported by other packages. Always, there must be one package called main, which produces an executable as result of its compilation. Other packages do not produce executables as result of their compilation.

Must the package live in a directory with the same name?


Idiomatic Package Conventions

Package names are generally given lowercase, single-word names. They should be short, concise and evocative, and it should provide context for their contents.

Package-exported names start with an uppercase character.

The contents of a package should be consistent with the name. If you start noticing that a package includes extra logic that has no relationship to the package name, consider exporting it as a separate one, or use a more descriptive package name.

Every package should have a comment describing its contents.

Also see:

Go Style

Declaring Packages

The main Package

The main package produces an executable as result of its compilation. The main package must have a function called main(), which is where the code execution starts. For more details see:

main()

The name of the source code file that declares the main() function does not matter. The following code, declared in a blue.go source file, placed in any directory, not necessarily in a main directory, produces an executable named blue:

package main

import "fmt"

func main() {
  fmt.Printf("hello\n")
}
go build ./blue.go
./blue
hello

Importing Packages

When a package is imported during compilation, the compiler searches directories specified by the GOROOT and GOPATH environment variables.

import Keyword

The import is a keyword use to access other packages.

import "fmt"
import (
	"bufio"
	"fmt"
	"os"
	"strings"
)

Relationship between Source Code Files and Packages

TODO: how do we add source code files to package? Is it a flat space? It is a hierarchy? What kind of names should the files use?

Standard library

Go comes with a set of "built-in" packages, which come with the Go development environment.

The content of the Standard library is available here: https://pkg.go.dev/std

The standard library is a good source of code examples, comments and style.

Standard library packages:

archive atomic bytes container database encoding errors
flag fmt io ioutil encoding/json hash log
math net os path reflect regexp runtime
slices sort strings strconv sync text/template time
unicode

Dependencies

Vendoring

"Vendoring" is the act of making a local copy of a third party package your project depends on. This copy is traditionally placed inside each project and then saved in the project repository.


Vendoring is using local copies of external dependencies to satisfy imports of those dependencies.

For a directory structure referred by GOPATH, code below a directory named "vendor" is importable only by code in the directory tree rooted at the parent of "vendor", and only using an import path that omits the prefix up to and including the vendor element.

Code in vendor directories deeper in the source tree shadows code in higher directories. Within the subtree rooted at foo, an import of "crash/bang" resolves to "foo/vendor/crash/bang", not the top-level "crash/bang". Code in vendor directories is not subject to import path checking.

When 'go get' checks out or updates a git repository, it now also updates submodules.

Vendor directories do not affect the placement of new repositories being checked out for the first time by 'go get': those are always placed in the main GOPATH, never in a vendor subtree.

Export

The functions and variable that start with a capital letter in a package get exported: somebody who imports the package can access the exported names, but not the "private" names, which start with lower case letter. "Hidden" names is also used in this situation. This is how encapsulation is implemented for packages.

In the following example:

package colors

var color string = "blue"

func GetColor() { 
  return color
}

the color variable is private to the package, but it can be accessed with the public GetColor() function.

Modules

Explain go.mod

Package as Namespace

Each package defines a namespace for its identifiers, which include variables, types (structs, interfaces), functions.

TO DEPLETE

External

Internal


Overview

A package is a unit of compiled code and a namespace: it consists in one or more source files that declare constants, variables, types and functions belonging to the package and which are accessible in all files of the same package. Packages are Go's way of organizing and reusing code, according to the principle Don't Repeat Yourself.

Every Go source code must belong to a package, and it must declare the package it belongs to with the package keyword.

All source file declared as being part of the same package must be in the same file system directory. You cannot split a package across multiple directories.

A directory cannot contain source files for more than one package (or the compiler will complain).

Identifiers declared by a package can be exported, and in order to refer those identifiers from other packages, the enclosing package must be imported and the identifiers must be prefixed with the name of the package:

package B

import "A"

...

func something() {

    A.SomeFunctionExportedByA()

}

Each package can be imported and used individually, so developers can import only the functionality they need.

The benefits offered by the package mechanism are:

  • Different namespaces allow for identifiers with the same name - as long as they belong to different namespaces. This allows keeping the identifier names short and succinct.
  • It speeds up the compiler by only requiring compilation of smaller chunks of the program. For example, when we use "fmt" we don't need to recompile it every time we change our program.

It is common in Go to see packages that are relatively small: they have a small API and perform a single task. This is normal and expected.

The package keyword

The package keyword is used in package declarations. All Go files must start with a package declaration, which consists start with the package keyword and it is followed by the package name:

package blue

...

Package Names

A package name is an identifier, which means it consists in a sequence of one more letter, digits and possibly underscores, where the first character is a letter. Even if we might be tempted to think about packages hierarchically, "project1/blue" is NOT a package name, even if, as we will see below, we will use "project1/blue" string literal to import the package.

Conventionally, the name of the directory that contains the package files should coincide with the name of the package. It is usually a good idea to follow the convention: while compiling the file, the compiler won't complain if the convention is not followed, but it will run into troubles if other packages that are referred do not follow the convention. For more details, see GOPATH.

Note that in order to import the package, the package name may not be sufficient and we may need to use a path fragment in the import statement: the string literal that follows the import keyword and the package name are two different things. For more details on importing packages, see import.

Package Name Conventions

  • A package name should coincide with the name of the directory that holds the files.
  • The package names should be short, concise, lowercase names.

Writing a Package

Place the package files under a directory hierarchy that makes it obvious where the package comes from, if imported. For example, if planning to write a reusable "blue" package as part of the "airplane" project, place the package source under:

$LOCAL_PROJECT_HOME/src/io/novaordis/airplane/blue

Thus the package will be imported as:

import io/novaordis/airplane/blue

Exported Identifiers

In order to be visible outside the declaring package, an identifier must be exported. This is achieved by any of the following is true:

  1. The identifier's first character is an upper case letter.
  2. The identifier is declared in the package block or it is a field name or a method name of an exported type.

An identifier that is not explicitly exported is unexported.

Note that identifiers, and not values, can be exported or unexported.

It is a good practice to only expose the package elements that we explicitly want other package to use, and hide everything else. This allows to change the hidden parts later, without affecting the package's consumers.

Note that just because an identifier is unexported, it does not mean other packages can't indirectly access it: for example a function can return the value of an unexported type and this value is accessible by an calling function, even if the calling function has been declared in a different package. This can be coupled with the short declaration operator (:=), which infers the type of the variable. A variable of that type cannot be explicitly declared with var.

struct Fields Export

The fields of an exported struct type can be exported or unexported on a field basis, by naming them with an upper case and respectively lower case letter.

Embedded Type Export

If the name of an inner type starts with a lower case, it is unexported even if its outer type is exported. Even if the name of the inner type is not exported, its fields may be individually exported, and thus accessible from outside the package.

For more details on embedded types see:

Type Embedding

Importing Packages

import

The "main" Package

A package named "main" that exposes a main() function is used by the Go linker as a start point for walking the package dependency graph when building the binary executable. All of the executable programs must have a package called "main", and their "main" package must have a main() function, otherwise the executable won't be built. More about the main() function, the main package and Go executables can be found here: compiling an executable.

Hierarchical Packages

TODO

Package Block

Each package has a package block containing all source text for that package. More about blocks is available here:

Blocks

Package and Encapsulation