banner
cos

cos

愿热情永存,愿热爱不灭,愿生活无憾
github
tg_channel
bilibili

Getting Started with Go Language (Part 1) Environment Configuration and Basic Syntax | Training Camp

The third Youth Training Camp of Byte is a back-end special session, and the class has started. I'm happily taking notes!
The class detailed the basic syntax of Go, along with some summaries from my own reading of the Go programming Bible, leading to this article. I feel there are many commonalities with JS and C/C++.

Content sourced from: The Go Programming Language and the third Youth Training Camp course.
Course source code wangkechun/go-by-example

Introduction to Go Language and Installation#

What is Go Language#

  • High performance, high concurrency
  • Rich standard library
  • Complete toolchain
  • Static linking
  • Fast compilation
  • Cross-platform
  • Garbage collection

In summary, it balances the performance of C/C++ while having the simplicity and completeness of standard libraries found in languages like Python.

Installation#

  1. Visit https://go.dev/, click Download, download the installation package for your platform, and install it.
  2. If you cannot access the above website, you can alternatively visit https://studygolang.com/dl to download and install.
  3. If accessing GitHub is slow, it is recommended to configure the Go mod proxy. Refer to the description in https://goproxy.cn/ for configuration, which can greatly speed up the download of third-party dependencies.
  • Install Go plugin for vscode
  • GoLand A new IDE from JetBrains series, dddd
    image.png

You can easily log in to experience the course's example project on GitHub Dashboard — Gitpod (So good, I'm crying)

Basic Data Types#

Integer#

Similar to C++, integers are divided into signed and unsigned types. Signed integers include:

  • int8, int16, int32, and int64
  • Corresponding to 8-bit, 16-bit, 32-bit, and 64-bit signed integers
  • uint8, uint16, uint32, and uint64 correspond to unsigned integers
  • Additionally, there are two types corresponding to the specific CPU platform's machine word size: int and uint, where int is the most widely used numeric type. Both types have the same size: 32 or 64 bits.
    • Different compilers may produce different sizes even on the same hardware platform.
  • The Unicode character type rune is equivalent to int32, typically used to represent a Unicode code point. These two names can be used interchangeably.
  • byte is an equivalent type of uint8, generally used to emphasize that the value is raw data rather than a small integer.
  • The uintptr type does not specify a specific bit size but is sufficient to hold a pointer. It is only needed in low-level programming, especially where Go interacts with C language libraries or operating system interfaces. We will see similar examples in the unsafe package section in Chapter 13.

You can print numbers in binary format using the %b parameter of the Printf function, and control the output base format with %d, %o, or %x, which is similar to formatted output in C.

var x uint8 = 1<<1 | 1<<5

fmt.Printf("%08b\n", x) // "00100010", the set {1, 5}

o := 0666
fmt.Printf("%d %[1]o %#[1]o\n", o) // "438 666 0666"

x := int64(0xdeadbeef)
fmt.Printf("%d %[1]x %#[1]x %#[1]X\n", x)
// Output:
// 3735928559 deadbeef 0xdeadbeef 0XDEADBEEF

ascii := 'a'
unicode := ''
newline := '\n'
fmt.Printf("%d %[1]c %[1]q\n", ascii)   // "97 a 'a'"
fmt.Printf("%d %[1]c %[1]q\n", unicode) // "22269 国 '国'"

In the above example, when the Printf format string contains multiple % parameters, it will include the corresponding number of additional operands. However, the [1] modifier after % tells the Printf function to use the first operand again.

  • The # modifier after % tells Printf to generate 0, 0x, or 0X prefixes when outputting with %o, %x, or %X.
  • Characters can be printed using the %c parameter, or using the %q parameter to print characters with single quotes.

The built-in len function returns a signed int, which can handle reverse loops as shown in the example below.

medals := []string{"gold", "silver", "bronze"}
for i := len(medals) - 1; i >= 0; i-- {
    fmt.Println(medals[i]) // "bronze", "silver", "gold"
}

Floating Point#

Go has floating point types float32 and float64.

Their range limits can be found in the math package.

  • The constant math.MaxFloat32 represents the maximum value that float32 can represent, approximately 3.4e38; the corresponding constant math.MaxFloat64 is approximately 1.8e308. The minimum values they can represent are approximately 1.4e-45 and 4.9e-324, respectively.
  • Using the %g parameter of the Printf function to print floating-point numbers will adopt a more compact representation and provide sufficient precision. However, for tabular data, using %e (with exponent) or %f may be more appropriate. All three printing formats can specify printing width and control printing precision.
for x := 0; x < 8; x++ {
    fmt.Printf("x = %d e^x = %8.3f\n", x, math.Exp(float64(x)))
}
// x = 0       e^x =    1.000
// x = 1       e^x =    2.718
// x = 2       e^x =    7.389
// x = 3       e^x =   20.086
// x = 4       e^x =   54.598
// x = 5       e^x =  148.413
// x = 6       e^x =  403.429
// x = 7       e^x = 1096.633

In addition to providing a large number of commonly used mathematical functions, the math package also provides the creation and testing of special values defined in the IEEE754 floating-point standard: positive infinity and negative infinity Inf -Inf, used to represent numbers that are too large to fit and results of division by zero; and NaN (Not a Number), generally used to represent invalid division operation results, such as 0/0 or Sqrt(-1).

var z float64
fmt.Println(z, -z, 1/z, -1/z, z/z) // "0 -0 +Inf -Inf NaN"
  • Go's NaN is similar to JS in that it is not equal to any number, including itself, and can be tested using math.IsNaN to check if a number is NaN.
nan := math.NaN()
fmt.Println(nan == nan, nan < nan, nan > nan) // "false false false"

Complex Numbers#

Go provides two precision complex number types: complex64 and complex128, corresponding to float32 and float64 floating-point precision, respectively. The built-in complex function is used to construct complex numbers, while the built-in real and imag functions return the real part and imaginary part of the complex number, respectively.

var x complex128 = complex(1, 2) // 1+2i
var y complex128 = complex(3, 4) // 3+4i
fmt.Println(x*y)                 // "(-5+10i)"
fmt.Println(real(x*y))           // "-5"
fmt.Println(imag(x*y))           // "10"

If a floating-point literal or a decimal integer literal is followed by an i, such as 3.141592i or 2i, it will form a complex number's imaginary part, with the complex number's real part being 0:

fmt.Println(1i * 1i) // "(-1+0i)", i^2 = -1

A complex constant can be normally added to another ordinary numeric constant.

fmt.Println(1i * 1i) // "(-1+0i)", i^2 = -1

The math/cmplx package provides many functions for complex number processing, such as the square root function and power function for complex numbers.

fmt.Println(cmplx.Sqrt(-1)) // "(0+1i)"

Boolean#

true or false, nothing much to say about this.

String#

The string type in Go, string, is an immutable string, similar to JS but different from C++.

Immutability means that it is safe for two strings to share the same underlying data, making copying any length of string inexpensive. Similarly, an operation on a string s and its corresponding substring slice s[7:] can safely share the same memory, so string slice operations are also inexpensive. In both cases, there is no need to allocate new memory.

The i-th byte in a string is not necessarily the i-th character of the string, as non-ASCII characters in UTF8 encoding may require two or more bytes.

s[i:j] generates a new string starting from the i-th byte of the original string s to the j-th byte (excluding j itself). The new string generated will contain j-i bytes.

  • Both i and j can be omitted; when omitted, 0 is used as the starting position, and len(s) is used as the ending position.
fmt.Println(s[0:5]) // "hello"
fmt.Println(s[:5]) // "hello"
fmt.Println(s[7:]) // "world"
fmt.Println(s[:])  // "hello, world"

The + operator concatenates two strings to construct a new string:

fmt.Println("goodbye" + s[5:]) // "goodbye, world"

String comparison is done by comparing byte by byte, and the comparison result is in the natural encoding order of the strings.

Go source files are always encoded in UTF8, and Go's text strings are also processed in UTF8, so we can write Unicode code points in string literals.

A raw string literal is as follows, using backticks instead of double quotes.

const GoUsage = `Go is a tool for managing Go source code.
Usage:
    go command [arguments]
...`

In a raw string literal, there are no escape operations; all content is taken literally, including backspaces and newlines, so a raw string literal in a program can span multiple lines.

  • It is not possible to write a backtick directly inside a raw string literal; it can be escaped using octal or hexadecimal or concatenated with a string constant using + "`".
  • The only special handling is that it will remove carriage returns to ensure that the values are the same across all platforms, including those systems that also include carriage returns in text files.

Windows systems will include both carriage returns and newlines in text files.

Here are some string methods:

package main

import (
	"fmt"
	"strings"
)

func main() {
	a := "hello"
	fmt.Println(strings.Contains(a, "ll"))                // true
	fmt.Println(strings.Count(a, "l"))                    // 2
	fmt.Println(strings.HasPrefix(a, "he"))               // true
	fmt.Println(strings.HasSuffix(a, "llo"))              // true
	fmt.Println(strings.Index(a, "ll"))                   // 2
	fmt.Println(strings.Join([]string{"he", "llo"}, "-")) // he-llo
	fmt.Println(strings.Repeat(a, 2))                     // hellohello
	fmt.Println(strings.Replace(a, "e", "E", -1))         // hEllo
	fmt.Println(strings.Split("a-b-c", "-"))              // [a b c]
	fmt.Println(strings.ToLower(a))                       // hello
	fmt.Println(strings.ToUpper(a))                       // HELLO
	fmt.Println(len(a))                                   // 5
	b := "你好"
	fmt.Println(len(b)) // 6
}

In Go, you can easily use %v to print variables of any type, without needing to distinguish between numeric strings, and you can use %+v to print detailed results, while %#v provides even more detail.

package main

import "fmt"

type point struct {
	x, y int
}

func main() {
	s := "hello"
	n := 123
	p := point{1, 2}
	fmt.Println(s, n) // hello 123
	fmt.Println(p)    // {1 2}

	fmt.Printf("s=%v\n", s)  // s=hello
	fmt.Printf("n=%v\n", n)  // n=123
	fmt.Printf("p=%v\n", p)  // p={1 2}
	fmt.Printf("p=%+v\n", p) // p={x:1 y:2}
	fmt.Printf("p=%#v\n", p) // p=main.point{x:1, y:2}

	f := 3.141592653
	fmt.Println(f)          // 3.141592653
	fmt.Printf("%.2f\n", f) // 3.14
}

String and Number Conversion#

In Go, conversions between strings and numeric types are handled in the strconv package, which is an abbreviation for string convert. You can use ParseInt or ParseFloat to parse a string. You can also use Atoi to convert a decimal string to a number. You can use Itoa to convert a number to a string.

  • If the input is invalid, these functions will return an error except for Itoa.
package main

import (
	"fmt"
	"strconv"
)

func main() {
	f, _ := strconv.ParseFloat("1.234", 64)
	fmt.Println(f) // 1.234

	n, _ := strconv.ParseInt("111", 10, 64)
	fmt.Println(n) // 111

	n, _ = strconv.ParseInt("0x1000", 0, 64)
	fmt.Println(n) // 4096

	n2, _ := strconv.Atoi("123")
	fmt.Println(n2) // 123

	n2, err := strconv.Atoi("AAA")
	fmt.Println(n2, err) // 0 strconv.Atoi: parsing "AAA": invalid syntax

	n3 := strconv.Itoa(123) // This does not return an error
	fmt.Println(n3) // 123
}

Constants#

Like constants in other languages, the value of a constant cannot be modified and must be initialized. When declaring constants in bulk, all except the first initialization expression can be omitted; if omitted, the previous constant's initialization expression is used, as shown below:

const pi = 3.14159 // approximately; math.Pi is a better approximation
const (
    e  = 2.71828182845904523536028747135266249775724709369995957496696763
    pi = 3.14159265358979323846264338327950288419716939937510582097494459
)
const (
    a = 1
    b
    c = 2
    d
)

iota Constant Generator#

Similar to the enumeration type Enum in C/C++!!

Constant declarations can use the iota constant generator for initialization, which is used to generate a set of constants initialized with similar rules without having to write the initialization expression on each line. In a const declaration statement, iota will be set to 0 on the line of the first declared constant, and then incremented by one on each line with a constant declaration.

type Weekday int
const (
    Sunday Weekday = iota
    Monday
    Tuesday
    Wednesday
    Thursday
    Friday
    Saturday
)
// Sunday corresponds to 0 
// Monday corresponds to 1 
// ....
// Saturday corresponds to 6

You can also combine complex expressions with iota, as shown in the example: each constant corresponds to the expression 1 << iota, which is consecutive powers of 2.

type Flags uint

const (
    FlagUp Flags = 1 << iota // is up
    FlagBroadcast            // supports broadcast access capability
    FlagLoopback             // is a loopback interface
    FlagPointToPoint         // belongs to a point-to-point link
    FlagMulticast            // supports multicast access capability
)

fmt.Println(FlagUp, FlagBroadcast, FlagLoopback, FlagPointToPoint, FlagMulticast)
// 1 2 4 8 16

Untyped Constants#

Many constants do not have a specific underlying type. The Go compiler provides arithmetic operations with higher precision than the underlying types for these untyped numeric constants; you can think of it as having at least 256 bits of arithmetic precision. There are six types of untyped constants: untyped boolean, untyped integer, untyped character, untyped floating-point, untyped complex, and untyped string.

Only constants can be untyped. When an untyped constant is assigned to a variable, the untyped constant will be implicitly converted to the corresponding type if the conversion is valid.

  • For variables declared without explicit types (including short variable declarations), the form of the constant will implicitly determine the default type of the variable,
    • Untyped integer constants convert to int, which has an uncertain memory size, while untyped floating-point and complex constants convert to memory size defined float64 and complex128.

Program Structure#

https://books.studygolang.com/gopl-zh/ch2/ch2.html

Declaration and Variables#

var#

The general syntax is as follows:

var variableName type = expression

If the type is omitted, it is automatically inferred from the expression. If the expression is empty, the variable is initialized with zero value (therefore, in Go, there are no uninitialized variables).

TypeZero Value
Numeric0
Booleanfalse
String""
Aggregate types like arrays or structsnil

You can declare a group of variables in a single declaration statement, or declare and initialize a group of variables with a set of initialization expressions.

var i, j, k int     // int int int
var b, f, s = true, 2.3, "hello" // bool float64 string

A group of variables can also be initialized by calling a function that returns multiple return values:

var f, err = os.Open(name) // os.Open returns a file and an error

Short Variable Declaration :=#

Variables are declared in the form name := expression, with the variable's type automatically inferred from the expression.

  • Due to its concise and flexible nature, short variable declarations are widely used for most local variable declarations and initializations.
  • The var form of declaration statements is often used where explicit variable types need to be specified, or where the initial value is irrelevant because the variable will be reassigned later.
i := 100                  // int
i, j := 0, 1              // int int
var boiling float64 = 100 // a float64
var names []string
var err error
  • The short variable declaration statement will only perform assignment for variables that have already been declared in the same lexical scope.
  • If the variable is declared in an outer lexical scope, the short variable declaration statement will redeclare a new variable in the current lexical scope.

Pointers#

Similar to C, you can take the address with the & operator and dereference with *.

x := 1
p := &x         // p, of type *int, points to x
fmt.Println(*p) // "1"
*p = 2          // equivalent to x = 2
fmt.Println(x)  // "2"

The zero value of any pointer type is nil.

  • If p points to a valid variable, then p != nil tests true.
  • Two pointers are equal only if they point to the same variable or are all nil.

new Function#

new(T) creates an anonymous variable of type T, initialized to the zero value of type T, and returns the variable's address, with the returned pointer type being *T.

  • In Go, new is a predefined function, not a keyword! So it can be redefined.
p := new(int)   // p, *int type, points to an anonymous int variable
fmt.Println(*p) // "0"
*p = 2          // Set the value of the int anonymous variable to 2
fmt.Println(*p) // "2"

Increment/Decrement Operations#

The increment statement i++ adds 1 to i; this is equivalent to i += 1 and i = i + 1. The corresponding decrement operation is i--, which subtracts 1 from i. They are statements, unlike in other C-like languages where they are expressions.

  • Therefore, j = i++ is illegal, and both ++ and -- can only be placed after the variable name, making --i also illegal.

Type type#

Similar to an enhanced version of typeof in C++, the form is as follows:

type typeName underlyingType

For example, two types are declared: Celsius and Fahrenheit, corresponding to different temperature units.

  • The underlying data type determines its internal structure and representation.
  • Although they have the same underlying type float64, they are different data types, so they cannot be compared or mixed in an expression.
  • Type conversion does not change the value itself but changes its semantics.
import "fmt"

type Celsius float64    // Celsius temperature
type Fahrenheit float64 // Fahrenheit temperature

const (
    AbsoluteZeroC Celsius = -273.15 // Absolute zero
    FreezingC     Celsius = 0       // Freezing point temperature
    BoilingC      Celsius = 100     // Boiling water temperature
)

func CToF(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) }

func FToC(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) }

The comparison operators == and < can also be used to compare a named type variable with another variable of the same type or values of unnamed types with the same underlying type. However, if two values have different types, they cannot be compared directly:

var c Celsius
var f Fahrenheit
fmt.Println(c == 0)          // "true"
fmt.Println(f >= 0)          // "true"
fmt.Println(c == f)          // compile error: type mismatch
fmt.Println(c == Celsius(f)) // "true"! Type conversion does not change the value.

Named types can also define new behaviors for the values of that type. These behaviors are represented as a set of functions associated with that type, which we call the method set of the type (detailed in Chapter 6).

In the following declaration statement, the parameter c of type Celsius appears before the function name, indicating that a method named String is declared for the Celsius type, which returns a string representation of the object c with the °C temperature unit:

func (c Celsius) String() string { return fmt.Sprintf("%g°C", c) }

Many types define a String method because when using the printing methods of the fmt package, the result returned by the String method of that type will be prioritized for printing.

c := FToC(212.0)
fmt.Println(c.String()) // "100°C"
fmt.Printf("%v\n", c)   // "100°C"; no need to call String explicitly
fmt.Printf("%s\n", c)   // "100°C"
fmt.Println(c)          // "100°C"
fmt.Printf("%g\n", c)   // "100"; does not call String
fmt.Println(float64(c)) // "100"; does not call String

Looping for#

Command Line Arguments · The Go Programming Language

In Go, there are no while or do while loops, only one for loop.
The syntax is as follows:

for initialization; condition; post {
    // zero or more statements
}

The three parts of the for loop do not need to be surrounded by parentheses. Braces are mandatory, and the left brace must be on the same line as the post statement.

  • The initialization statement is optional and is executed before the loop starts. If initialization exists, it must be a simple statement (simple statement), such as a short variable declaration, increment statement, assignment statement, or function call.
  • The condition is a boolean expression whose value is evaluated at the start of each iteration of the loop. If it is true, the loop body statements are executed.
  • The post statement is executed after each execution of the loop body, and then the condition is evaluated again. When the value of condition is false, the loop ends.

Each of the three parts of the for loop can be omitted. If both initialization and post are omitted, it becomes a while loop. Semicolons can also be omitted; if all three parts are omitted, it becomes an infinite loop, which can be exited using break:

i := 1
for {
        fmt.Println("loop")
        break
}
for j := 7; j < 9; j++ {
        fmt.Println(j)
}

for n := 0; n < 5; n++ {
        if n%2 == 0 {
                continue
        }
        fmt.Println(n)
}
for i <= 3 {
        fmt.Println(i)
        i = i + 1
}

Branching Structure#

if else#

The if in Go is similar to Python, with no parentheses, but must be followed by braces.

if 7%2 == 0 {
        fmt.Println("7 is even")
} else {
        fmt.Println("7 is odd")
}

if 8%4 == 0 {
        fmt.Println("8 is divisible by 4")
}

if num := 9; num < 0 {
        fmt.Println(num, "is negative")
} else if num < 10 {
        fmt.Println(num, "has 1 digit")
} else {
        fmt.Println(num, "has multiple digits")
}

switch#

The switch structure in Go is similar to C++. However, there are many differences:

  • The variable name after switch should not have parentheses.
  • In C++, if you do not add break to switch cases, it will continue to run all cases below. In Go, you do not need to add break.
  • The switch in Go is more powerful; it can use any variable type and can even replace any if else statements.

You can omit any variable after switch and write conditional branches in the case. This makes the code logic clearer compared to using multiple if else statements.

package main

import (
	"fmt"
	"time"
)

func main() {
	a := 2
	switch a {
	case 1:
		fmt.Println("one")
	case 2:
		fmt.Println("two")
	case 3:
		fmt.Println("three")
	case 4, 5:
		fmt.Println("four or five")
	default:
		fmt.Println("other")
	}

	t := time.Now()
	switch {
	case t.Hour() < 12:
		fmt.Println("It's before noon")
	default:
		fmt.Println("It's after noon")
	}
}

Process Information#

In Go, we can use os.argv to get the command line arguments specified when executing the program. For example, when we compile a binary file, command, followed by abcd to start, the output will be that os.argv will be a slice of length 5, with the first member representing the name of the binary itself. We can use os.Getenv to read environment variables. exec

package main

import (
	"fmt"
	"os"
	"os/exec"
)

func main() {
	// go run example/20-env/main.go a b c d
	fmt.Println(os.Args)           // [/var/folders/8p/n34xxfnx38dg8bv_x8l62t_m0000gn/T/go-build3406981276/b001/exe/main a b c d]
	fmt.Println(os.Getenv("PATH")) // /usr/local/go/bin...
	fmt.Println(os.Setenv("AA", "BB"))

	buf, err := exec.Command("grep", "127.0.0.1", "/etc/hosts").CombinedOutput()
	if err != nil {
		panic(err)
	}
	fmt.Println(string(buf)) // 127.0.0.1       localhost
}

Composite Data Types#

Arrays#

An array is a sequence of fixed length consisting of elements of a specific type. An array can consist of zero or more elements. Because the length of the array is fixed, arrays are rarely used directly in Go. The corresponding type to an array is Slice, which is a dynamic sequence that can grow and shrink, and slice functionality is more flexible, but to understand how slices work, you need to first understand arrays.

package main

import "fmt"

func main() {

	var a [5]int
	a[4] = 100
	fmt.Println("get:", a[2])
	fmt.Println("len:", len(a))

	b := [5]int{1, 2, 3, 4, 5}
	fmt.Println(b)

	var twoD [2][3]int
	for i := 0; i < 2; i++ {
		for j := 0; j < 3; j++ {
			twoD[i][j] = i + j
		}
	}
	fmt.Println("2d: ", twoD)
}

Slices#

Slices differ from arrays in that they can change length arbitrarily and have richer operations.

  • Use make to create a slice, and you can access values like an array.
  • Use append to add elements. Note that the usage of append is similar to concat in JS, returning a new array, and the result of append is assigned to the original array.
  • Slices can also be dynamically initialized with a specified length. len(s)
  • Slices support slicing operations like Python, for example, s[2:5] represents extracting elements from the second to the fifth position, excluding the fifth element. However, unlike Python, negative indexing is not supported here.
package main

import "fmt"

func main() {
	s := make([]string, 3)
	s[0] = "a"
	s[1] = "b"
	s[2] = "c"
	fmt.Println("get:", s[2])   // c
	fmt.Println("len:", len(s)) // 3

	s = append(s, "d")
	s = append(s, "e", "f")
	fmt.Println(s) // [a b c d e f]

	c := make([]string, len(s))
	copy(c, s)
	fmt.Println(c) // [a b c d e f]

	fmt.Println(s[2:5]) // [c d e]
	fmt.Println(s[:5])  // [a b c d e]
	fmt.Println(s[2:])  // [c d e f]

	good := []string{"g", "o", "o", "d"}
	fmt.Println(good) // [g o o d]
}

Maps#

map is the most frequently used data structure in practical use.

  • You can create an empty map using make, which requires two types: the type of key and the type of value.
    • map[string]int indicates that the key type is string and the value type is int.
  • The value retrieval and insertion in map are similar to STL maps in C++, and can be done directly. m[key] m[key] = value.
  • You can use delete to remove key-value pairs from it.
  • In Go, map is completely unordered, and the output during traversal will not be in alphabetical order or insertion order, but rather in random order.
package main

import "fmt"

func main() {
	m := make(map[string]int)
	m["one"] = 1
	m["two"] = 2
	fmt.Println(m)           // map[one:1 two:2]
	fmt.Println(len(m))      // 2
	fmt.Println(m["one"])    // 1
	fmt.Println(m["unknow"]) // 0

	r, ok := m["unknow"]
	fmt.Println(r, ok) // 0 false

	delete(m, "one")

	m2 := map[string]int{"one": 1, "two": 2}
	var m3 = map[string]int{"one": 1, "two": 2}
	fmt.Println(m2, m3)
}

Range#

For a slice or a map, we can use range to quickly iterate, making the code more concise. When iterating over arrays, it will return two values: the first is the index, and the second is the value at that position. If we do not need the index, we can use the underscore _ to ignore it.

Go does not allow the use of unused local variables (local variables), as this will lead to compilation errors. The blank identifier (blank identifier), i.e., _ (the underscore), can be used to discard unnecessary loop indices while retaining the element values.

package main

import "fmt"

func main() {
	nums := []int{2, 3, 4}
	sum := 0
	for i, num := range nums {
		sum += num
		if num == 2 {
			fmt.Println("index:", i, "num:", num) // index: 0 num: 2
		}
	}
	fmt.Println(sum) // 9

	m := map[string]string{"a": "A", "b": "B"}
	for k, v := range m {
		fmt.Println(k, v) // b 8; a A
	}
	for k := range m {
		fmt.Println("key: ", k) // key:  a; key:  b
	}
	for _, v := range m {
		fmt.Println("value:", v) // value: A; value: B
	}
}

Structs#

A struct is a collection of fields with types. For example, the user struct contains two fields, name and password.

  • You can initialize a struct variable using the struct name, passing initial values for each field.
  • You can also specify initial values using key-value pairs, allowing you to initialize only a portion of the fields.
  • Structs also support pointers, allowing direct modification of the struct, which can avoid some overhead of copying large structs in certain situations.
package main

import "fmt"

type user struct {
	name     string
	password string
}

func main() {
	a := user{name: "wang", password: "1024"}
	b := user{"wang", "1024"}
	c := user{name: "wang"}
	c.password = "1024"
	var d user
	d.name = "wang"
	d.password = "1024"

	fmt.Println(a, b, c, d)                 // {wang 1024} {wang 1024} {wang 1024} {wang 1024}
	fmt.Println(checkPassword(a, "haha"))   // false
	fmt.Println(checkPassword2(&a, "haha")) // false
}

func checkPassword(u user, password string) bool {
	return u.password == password
}

func checkPassword2(u *user, password string) bool {
	return u.password == password
}

JSON#

JSON operations in Go are very simple.

  • For an existing struct, as long as the first letter of each field is uppercase, meaning it is public, the struct can be serialized into a JSON string using JSON.marshaler.

The JSON.marshaler returns the serialized value and error, as shown in the example
The default serialized string will have fields starting with uppercase letters. You can modify the output JSON field names using json tags and other syntax.

  • The serialized string can be deserialized back into an empty variable using JSON.unmarshaler.
package main

import (
	"encoding/json"
	"fmt"
)

type userInfo struct {
	Name  string
	Age   int `json:"age"`
	Hobby []string
}

func main() {
	a := userInfo{Name: "wang", Age: 18, Hobby: []string{"Golang", "TypeScript"}}
	buf, err := json.Marshal(a)
	if err != nil {
		panic(err)
	}
	fmt.Println(buf)         // [123 34 78 97...]
	fmt.Println(string(buf)) // {"Name":"wang","age":18,"Hobby":["Golang","TypeScript"]}

	buf, err = json.MarshalIndent(a, "", "\t")
	if err != nil {
		panic(err)
	}
	fmt.Println(string(buf))

	var b userInfo
	err = json.Unmarshal(buf, &b)
	if err != nil {
		panic(err)
	}
	fmt.Printf("%#v\n", b) // main.userInfo{Name:"wang", Age:18, Hobby:[]string{"Golang", "TypeScript"}}
}

Time Handling#

In Go, the most commonly used function is time.Now() to get the current time. You can also use time.Date to construct a time with a timezone, and there are many methods to get the year, month, day, hour, minute, and second of this time point.

  • You can use the Sub method to subtract two times and get a time duration.
  • The duration can also tell you how many hours, minutes, and seconds it has.
  • When interacting with certain systems, we often use timestamps. You can use .Unix to get the timestamp. time.Format time.Parse
package main

import (
	"fmt"
	"time"
)

func main() {
	now := time.Now()
	fmt.Println(now) // 2022-05-07 13:12:03.7190528 +0800 CST m=+0.004990401
	t := time.Date(2022, 5, 7, 13, 25, 36, 0, time.UTC)
	t2 := time.Date(2022, 8, 12, 12, 30, 36, 0, time.UTC)
	fmt.Println(t)                                                  // 2022-05-07 13:25:36 +0000 UTC
	fmt.Println(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute()) // 2022 March 27 1 25
	fmt.Println(t.Format("2006-01-02 15:04:05"))                    // 2022-05-07 13:25:36
	diff := t2.Sub(t)
	fmt.Println(diff)                           // 2327h5m0s
	fmt.Println(diff.Minutes(), diff.Seconds()) // 139625 8.3775e+06
	t3, err := time.Parse("2006-01-02 15:04:05", "2022-05-07 13:25:36")
	if err != nil {
		panic(err)
	}
	fmt.Println(t3 == t)    // true
	fmt.Println(now.Unix()) // 1651900531
}

Functions#

Unlike many other languages, the variable types of function parameters in Go are postfixed. Go natively supports returning multiple values.

  • In practical business logic code, almost all functions return two values: the first is the return value, and the second is an error message. As shown in the example, exists.
package main

import "fmt"

func add(a int, b int) int {
	return a + b
}

func add2(a, b int) int {
	return a + b
}

func exists(m map[string]string, k string) (v string, ok bool) {
	v, ok = m[k]
	return v, ok
}

func main() {
	res := add(1, 2)
	fmt.Println(res) // 3

	v, ok := exists(map[string]string{"a": "A"}, "a")
	fmt.Println(v, ok) // A True
}

Error Handling#

Error handling in Go is done by using a separate return value to pass the error information.

  • By adding an error to the return value type of a function, it indicates that this function may return an error. Therefore, in the function implementation, you need to return two values simultaneously.
  • When an error occurs, you can return nil and an error. If there is no error, return the original result and nil.
package main

import (
	"errors"
	"fmt"
)

type user struct {
	name     string
	password string
}

func findUser(users []user, name string) (v *user, err error) {
	for _, u := range users {
		if u.name == name {
			return &u, nil
		}
	}
	return nil, errors.New("not found")
}

func main() {
	u, err := findUser([]user{{"wang", "1024"}}, "wang")
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(u.name) // wang

	if u, err := findUser([]user{{"wang", "1024"}}, "li"); err != nil {
		fmt.Println(err) // not found
		return
	} else {
		fmt.Println(u.name)
	}
}

Several code generation tools mentioned in class:

After-Class Exercises#

  1. Modify the final code of the first example guessing game to use fmt.Scanf to simplify the implementation.
package main

import (
	"fmt"
	"math/rand"
	"time"
)

func main() {
	maxNum := 100
	rand.Seed(time.Now().UnixNano())
	secretNumber := rand.Intn(maxNum)
	// fmt.Println("The secret number is ", secretNumber)

	fmt.Println("Please input your guess")
	//reader := bufio.NewReader(os.Stdin)
	for {
		//input, err := reader.ReadString('\n')
		var guess int
		_, err := fmt.Scanf("%d", &guess)
		fmt.Scanf("%*c")    // consume newline
		if err != nil {
			fmt.Println("An error occurred while reading input. Please try again", err)
			continue
		}
		//input = strings.TrimSuffix(input, "\n")
		if err != nil {
			fmt.Println("Invalid input. Please enter an integer value")
			continue
		}
		fmt.Println("Your guess is", guess)
		if guess > secretNumber {
			fmt.Println("Your guess is bigger than the secret number. Please try again")
		} else if guess < secretNumber {
			fmt.Println("Your guess is smaller than the secret number. Please try again")
		} else {
			fmt.Println("Correct, you Legend!")
			break
		}
	}
}
  1. Modify the final code of the second example command line dictionary to add support for another translation engine.
  1. Based on the previous step, modify the code to implement parallel requests to two translation engines to improve response speed.

Summary and Insights#

The class detailed the basic syntax of Go, along with some summaries from my own reading of the Go programming Bible, leading to this article. I feel there are many commonalities with JS and C/C++.

Content sourced from: The Go Programming Language and the third Youth Training Camp course.

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.