Frontier Software

Packages

Successful design is based on a principle known since the days of Julius Caesar: Divide and conquer. — Structured Design, Edward Yourdon and Larry L. Constantine

Go programs are organized into packages. A package is a collection of source files in the same directory that are compiled together. Functions, types, variables, and constants defined in one source file are visible to all other source files within the same package. — How to Write Go Code

Fahrenheit/Celsius Table

As per K&R tradition, programming basics such variable assignment, types (int vs float), for and while loops, conditionals… are introduced by converting Fahrenheit to Celsius.

I’m going to use this simple example to create a function separate from main, housed in a subdirectory. Something that confused me is f2c is the module while ftoc is the package, whose path is f2c/ftoc, ie module/package.

My tree for this example:

f2c
├── f2c_test.go
├── ftoc
│   ├── ftoc.go
│   └── ftoc_test.go
├── go.mod
└── main.go

go.mod

Produced by go mod init f2c

module f2c

go 1.22.2

main.go

/*
My f2c package is based on K&R's Fahrenheit to Celsius tutorial.
*/
package main

import (
	"fmt"

	"f2c/ftoc"
)

const lower = -40.0
const upper = 300.0
const step = 20.0

/*
A comment above main gets ignored.
*/
func main() {
	fahr := lower
	for fahr <= upper {
		fmt.Printf("%3.0f\t%6.1f\n", fahr, ftoc.FToC(fahr))
		fahr = fahr + step
	}
}

ftoc/ftoc.go

/*
Comment above the package declaration is put into "Overview" by godoc.
*/
package ftoc

/*
Comment above the function declaration
*/
func FToC(f float64) float64 {
/*
Comment below the function declaration is ignored.
*/
	return (5.0 / 9.0) * (f - 32.0)
}

f2c_test.go

package main

// How do I get this into my documentation?
func Example() {
	main()
	// Output:
	// -40	 -40.0
	// -20	 -28.9
	//   0	 -17.8
	//  20	  -6.7
	//  40	   4.4
	//  60	  15.6
	//  80	  26.7
	// 100	  37.8
	// 120	  48.9
	// 140	  60.0
	// 160	  71.1
	// 180	  82.2
	// 200	  93.3
	// 220	 104.4
	// 240	 115.6
	// 260	 126.7
	// 280	 137.8
	// 300	 148.9
}

ftoc/ftoc_test.go

Here I’m moving to a TestXxx style test.

An advantage of seperating auxiliary functions from the main.go file is we can test them independently.

package ftoc

import (
	"fmt"
	"testing"
)

func TestFToC(t *testing.T) {
	var tests = []struct {
		fahrenheit float64
		celsius    float64
	}{
		{-40.0, -40.0},
		{-20.0, -28.888889},
		{0.0, -17.777778},
		{20.0, -6.666667},
		{40.0, 4.444444},
		{60.0, 15.555556},
		{80.0, 26.666667},
		{100.0, 37.777778},
		{120.0, 48.888889},
		{140.0, 60.0},
		{160.0, 71.111111},
		{180.0, 82.222222},
		{200.0, 93.333333},
		{220.0, 104.444444},
		{240.0, 115.555556},
		{260.0, 126.666667},
		{280.0, 137.777778},
		{300.0, 148.888889},
	}
	for _, test := range tests {
		got := FToC(test.fahrenheit)
		if fmt.Sprintf("%6.2f", got) != fmt.Sprintf("%6.2f", test.celsius) {
			t.Errorf("FToC(%f) = %f; want %f", test.fahrenheit, got, test.celsius)
		}
	}
}

// This appears in the docs as a collapsible Example.
func ExampleFToC() {
	fmt.Printf("%6.2f", FToC(-40.0))
	// Output:
	// -40.00
}

This example lends itself to Table-Driven Tests. That to avoid floating comparison errors I’d need to convert want and got into strings wasn’t obvious to me.

By naming the test ExampleFToC it automatically gets added to the documentation.

Assignment

Introducing assignment into our programming language leads us into a thicket of difficult conceptual issues. — Structure and Interpretation of Computer Programs

As in C, all variables must be declared before they are used, usually at the beginning of the function before any executable statements.

The differences from the original C example in K&R:

  1. In go, type declarations come at the end instead of the beginning.
  2. There is only a for loop which can be made to act just like C’s while.
  3. I put ; at the end of each statement which go fmt fahrenheit_celsius.go removed.

Symbolic constants

#define	LOWER	0	/* lower limit of table */
#define	UPPER	300	/* upper limit */
#define	STEP	20	/* step size */

Go’s const is fairly identical to C’s #define. Not sure if Go has an all upercase convention, since that would export constants even if I don’t want to.

:= vs =

Instead of declaring fahr and celsius as typed variables C-style, Go allows variables to be introduced anywhere (as in Bash, JavaScript, Python…) using the := operator which is used in Hugo. Later reassignment of these variables requires the = operator.

package main

import (
	"fmt"
)

const lower = -40
const upper = 300
const step = 20

func main() {
	fahr := lower
	for fahr <= upper {
		celsius := 5 * (fahr - 32) / 9
		fmt.Printf("%d\t%d\n", fahr, celsius)
		fahr = fahr + step
	}
}

The short variable declaration operator := can only be used within functions, not if variables and constants are defined outside of functions:

Type inference

A snag with the := short variable declaration operator is it infers int, so if I want the table to be float, I have to declare it:

package main

import (
	"fmt"
)

const lower = -40
const upper = 300
const step = 20

func main() {
	var fahr, celsius float64
	fahr = lower
	for fahr <= upper {
		celsius = (5.0 / 9.0) * (fahr - 32.0)
		fmt.Printf("%f\t%f\n", fahr, celsius)
		fahr = fahr + step
	}
}

Using Go’s for in a more conventional way:

package main

import (
	"fmt"
)

const lower = -40
const upper = 300
const step = 20

func main() {
	var fahr, celsius float64
	for fahr = lower; fahr <= upper; fahr = fahr + step {
		celsius = (5.0 / 9.0) * (fahr - 32.0)
		fmt.Printf("%f\t%f\n", fahr, celsius)
	}
}