Coursera上Golang专项课程1:Getting Started with Go 学习笔记,University of California, Irvine授课

Getting Started with Go

本文是学习 https://www.coursera.org/learn/golang-getting-started/ 这门课的学习笔记,如有侵权,请联系删除。
Course Certificate
在这里插入图片描述

这门课是University of California, Irvine发布的。

在这里插入图片描述

Week 01: Getting Started with Go

Module 1: Getting Started with Go

This first module gets you started with Go. You’ll learn about the advantages of using Go and begin exploring the language’s features. Midway through the module, you’ll take a break from “theory” and install the Go programming environment on your computer. At the end of the module, you’ll write a simple program that displays “Hello, World” on your screen.

Learning Objectives


  • Describe the basic features of a compiled, object-oriented programming language.
  • Download and install the Go development environment on your computer.
  • Write and implement a Go program that displays “Hello, World!” on your screen.

Module 1 Overview

Go,通常被称为Golang,是一种由Google开发的开源编程语言。它的设计目标是提高开发人员的生产力,特别是在大型项目中。以下是一些关于Go语言的主要特点和优势:

  1. 简洁性和易读性: Go语言的语法简洁清晰,易于理解和学习。它摒弃了一些复杂的特性,例如继承和类型系统中的一些复杂机制,使得代码更加简洁易读。

  2. 并发支持: Go语言内置了并发支持,通过goroutines和channels实现。Goroutines是一种轻量级的线程,可以轻松创建成千上万个goroutines,并通过channels进行通信,这使得编写并发程序变得更加容易。

  3. 性能: Go语言被设计成能够在多核和分布式系统上高效运行。它的编译器和运行时系统都被优化,使得Go程序具有良好的性能。

  4. 内置工具支持: Go语言自带了丰富的工具集,例如测试框架、性能分析工具等,这些工具使得开发和调试过程更加便捷。

  5. 静态类型和类型推断: Go语言是静态类型的,但同时也支持类型推断,这意味着在大多数情况下不需要显式地指定变量的类型。

  6. 跨平台支持: Go语言支持跨平台开发,可以在多种操作系统上编译和运行,包括Linux、Windows和macOS等。

  7. 垃圾回收: Go语言具有自动内存管理,使用了一种称为垃圾回收的机制来管理内存,开发人员不需要手动管理内存。

  8. 丰富的标准库: Go语言提供了丰富的标准库,涵盖了各种领域,包括网络、文本处理、加密等,这些库使得开发人员能够快速构建各种类型的应用程序。

总的来说,Go语言是一种简洁、高效、易用的编程语言,特别适合构建高并发、可扩展的网络服务和分布式系统。由于其优秀的性能和便利的并发支持,Go语言在云计算、网络服务、容器等领域得到了广泛的应用。

M1.1.1 - Why Should I Learn Go? (Advantages of Go)

在这里插入图片描述

编译型语言和解释性语言是两种不同的编程语言类型,它们在代码执行方式、性能和开发流程等方面存在显著差异。

  1. 编译型语言:

    • 编译型语言的代码在运行之前需要经过编译器的编译过程,将源代码转换成目标机器能够执行的机器代码或者中间代码。
    • 编译型语言的编译过程会生成与目标机器相关的二进制文件,这些文件通常是可执行文件或者库文件,可以直接在目标机器上运行,而不需要再次编译。
    • 典型的编译型语言包括C、C++、Go等。
  2. 解释性语言:

    • 解释性语言的代码不需要经过编译器的编译过程,而是由解释器逐行解释执行。
    • 解释性语言的代码在运行时会逐行被解释器解析并执行,而不会生成独立的可执行文件。
    • 解释性语言通常需要在运行时存在解释器或虚拟机,解释器会将源代码转换成机器代码或者中间代码并执行。
    • 典型的解释性语言包括Python、JavaScript、Ruby等。

主要区别:

  • 执行方式: 编译型语言在运行之前需要将源代码编译成机器码或中间码,而解释性语言则是在运行时逐行解释执行源代码。
  • 性能: 由于编译型语言在编译时进行了优化,因此通常执行速度更快,而解释性语言由于需要在运行时进行解释执行,执行速度相对较慢。
  • 开发流程: 编译型语言的开发流程通常包括编写代码、编译、链接、运行等步骤,而解释性语言的开发流程则更加简单,只需编写代码然后直接运行即可。

在选择编程语言时,开发者需要根据项目的需求、性能要求、开发效率等因素来决定使用编译型语言还是解释性语言。

M1.1.2 - Objects

在这里插入图片描述

Go语言是一种支持面向对象编程(OOP)的编程语言,但它与传统的面向对象语言(如Java、C++)有所不同。在Go语言中,虽然没有类和继承的概念,但是可以通过结构体和方法来实现面向对象的特性。以下是Go语言中面向对象编程的主要概念和特性:

  1. 结构体(Structures): 在Go语言中,结构体用于定义自定义的数据类型,它可以包含零个或多个字段(成员变量)。结构体可以看作是一种简化的类,用于封装数据。
type Person struct {
    Name string
    Age  int
}
  1. 方法(Methods): 方法是与结构体关联的函数,它允许在结构体上执行特定的操作。通过在函数名之前添加接收者(receiver),可以将函数绑定到特定的结构体上。
func (p *Person) SayHello() {
    fmt.Printf("Hello, my name is %s and I'm %d years old.\n", p.Name, p.Age)
}
  1. 封装(Encapsulation): 封装是面向对象编程的核心概念之一,它指的是将数据和对数据的操作封装在一起,对外部隐藏具体实现细节。在Go语言中,通过将字段声明为结构体的私有成员(小写字母开头),可以实现封装。
type Person struct {
    name string // 小写字母开头的字段为私有成员
    age  int
}
  1. 继承与组合(Inheritance and Composition): Go语言中没有显式的继承概念,但可以通过组合来实现类似继承的效果。通过在结构体中嵌入其他结构体,可以实现复用现有代码的功能。
type Employee struct {
    Person // 匿名字段
    Salary float64
}
  1. 多态(Polymorphism): 多态是指相同的方法调用可以在不同类型的对象上产生不同的行为。在Go语言中,通过接口实现多态。如果一个对象实现了某个接口定义的所有方法,那么该对象就实现了该接口。
type Speaker interface {
    Speak()
}

虽然Go语言中的面向对象编程与传统的面向对象语言有所不同,但它仍然提供了一种简单而灵活的方式来组织和管理代码,使得开发人员能够轻松地构建和维护复杂的程序。

M1.1.3 - Concurrency

在这里插入图片描述

Go语言通过goroutines和channels提供了强大的并发编程支持,使得编写并发程序变得简单而又高效。以下是Go语言中并发编程的主要特性和概念:

  1. Goroutines(协程): Goroutines是Go语言中轻量级的线程,由Go运行时(runtime)管理。与传统的线程相比,Goroutines的创建和销毁开销较小,可以在程序中轻松创建成千上万个Goroutines。通过关键字go可以启动一个新的Goroutine。
func main() {
    go sayHello() // 启动一个新的Goroutine
    fmt.Println("Main function")
}

func sayHello() {
    fmt.Println("Hello from Goroutine")
}
  1. Channels(通道): Channels是用于在Goroutines之间进行通信的管道。它提供了一种同步的机制,用于保证数据在Goroutines之间的安全传输。通过make函数创建通道,使用<-运算符发送和接收数据。
func main() {
    ch := make(chan string) // 创建一个字符串类型的通道

    go func() {
        ch <- "Hello from Goroutine" // 发送数据到通道
    }()

    msg := <-ch // 从通道接收数据
    fmt.Println(msg)
}
  1. Select语句: Select语句用于处理多个通道的操作,它可以使程序在多个通道之间进行选择操作。当多个通道都准备好时,Select语句会随机选择一个执行。
func main() {
    ch1 := make(chan string)
    ch2 := make(chan string)

    go func() {
        ch1 <- "Hello"
    }()

    go func() {
        ch2 <- "World"
    }()

    select {
    case msg1 := <-ch1:
        fmt.Println("Message from ch1:", msg1)
    case msg2 := <-ch2:
        fmt.Println("Message from ch2:", msg2)
    }
}
  1. 互斥锁(Mutex): 在并发程序中,多个Goroutines同时访问共享资源可能会导致数据竞争和不确定行为。互斥锁是一种常用的同步机制,用于保护共享资源的访问,确保同时只有一个Goroutine可以访问该资源。
import (
    "sync"
)

var counter int
var mutex sync.Mutex

func main() {
    for i := 0; i < 10; i++ {
        go incrementCounter()
    }
}

func incrementCounter() {
    mutex.Lock()
    defer mutex.Unlock()
    counter++
    fmt.Println("Counter:", counter)
}

通过这些特性,Go语言使得并发编程变得简单而又高效,开发人员可以轻松地编写并发程序,处理并发问题,构建高效的并发应用。

M1.2.1 - Installing Go

WSL中安装go环境

M1.2.2 - Workspaces & Packages

在Go语言中,正确的术语是"workspace"(工作区)和"packages"(包)。

  1. 工作区(workspace):
    Go语言的工作区是一个目录结构,包含了Go代码文件和相关的辅助文件。工作区的目录结构通常包括三个主要目录:

    • src:用于存放源代码文件(.go文件)的目录。
    • pkg:用于存放编译后生成的包文件(.a文件)的目录。
    • bin:用于存放可执行文件的目录,当我们使用go install命令编译安装一个Go程序时,生成的可执行文件会被放在这里。
  2. 包(packages):
    包是Go语言中用于组织和管理代码的基本单元。每个Go源代码文件都属于一个包,每个包都有一个唯一的包名。包可以包含函数、变量、常量和类型的定义等内容。包名通常与包所在的目录名称相对应。

    Go语言的标准库本身就是由多个包组成的,例如fmt包用于格式化输入输出,net/http包用于处理HTTP请求等。

在工作区中,通常会有一个顶级的目录用于存放所有的Go代码,称为src目录。每个项目通常会有一个独立的目录,其中包含一个或多个包,以及相关的测试文件和文档。在这些包中,可以根据需要进行引用和组织,以便在项目中进行复用和管理。

工作区和包是Go语言中非常重要的概念,它们帮助开发者组织代码、管理依赖、编译程序,并且遵循了Go语言的约定和惯例。

M1.2.3 - Go Tool

Go语言提供了一系列强大的工具,用于开发、构建、测试和管理Go代码。这些工具通常被称为"Go工具链"(Go tools),包括以下几个主要工具:

  1. go命令:
    go命令是Go语言的主要命令行工具,用于管理Go代码的开发、构建、测试和安装等任务。它提供了许多子命令,例如build用于编译代码,test用于运行测试,install用于安装程序等。

  2. go build:
    go build命令用于编译Go程序,将源代码文件编译成可执行文件或库文件。如果没有指定输出文件名,则默认将生成一个可执行文件,文件名与当前目录的最后一个元素相同。

  3. go install:
    go install命令用于编译和安装Go程序,将编译后的可执行文件安装到bin目录下(默认为$GOPATH/bin)。它还可以安装并编译库文件到pkg目录下(默认为$GOPATH/pkg)。

  4. go run:
    go run命令用于编译和运行Go程序,它会临时编译程序并直接运行,而不会生成可执行文件。这对于简单的程序很方便,可以避免生成额外的可执行文件。

  5. go test:
    go test命令用于运行Go程序中的测试代码,它会自动查找并运行包含测试函数的文件,并输出测试结果。测试函数通常以Test开头,并接受一个*testing.T类型的参数。

  6. go fmt:
    go fmt命令用于格式化Go源代码文件,使其符合Go语言的代码风格规范。它会自动调整缩进、空格、换行等格式,并将代码格式化为标准的风格。

  7. go get:
    go get命令用于获取远程代码包并安装到本地工作区中。它会自动下载指定的代码包,并将其安装到$GOPATH/src目录下,然后编译并安装其中的可执行文件或库文件。

  8. go vet:
    go vet命令用于静态分析Go程序,查找代码中可能的错误或潜在问题。它会检查代码中的常见错误模式,例如使用未初始化的变量、函数调用参数不匹配等,并提供警告或建议。

除了以上列出的常用工具外,Go语言还提供了许多其他有用的工具,如go doc用于查看文档,go generate用于自动生成代码等。这些工具使得Go语言开发变得更加便捷和高效。

M1.3.1 - Variables

在Go语言中,变量用于存储数据值。它们是用来容纳信息的容器,可以在程序中进行操作或引用。以下是Go语言中变量的概述:

  1. 声明: 在Go中,变量必须在使用之前进行声明。声明变量的基本语法是:

    var 变量名 数据类型
    

    例如:

    var age int
    
  2. 初始化: 变量可以同时声明和初始化。例如:

    var name string = "John"
    

    在Go中,变量的类型也可以从赋值推断出来。这称为类型推断:

    var message = "Hello, World!"
    
  3. 短变量声明: Go还支持在函数内部声明和初始化变量的简短语法:

    变量名 :=

    例如:

    age := 25
    

    这种短变量声明语法通常在函数内部声明变量时使用。

  4. 零值: 如果变量声明时没有初始化,它将根据类型被分配一个零值。例如,数值类型(如int和float64)的零值为0,字符串的零值为"",布尔类型的零值为false。

  5. 常量: 常量是声明后其值不可改变的变量。使用const关键字声明:

    const pi = 3.14
    
  6. 作用域: Go中的变量有作用域,定义了它们在程序中的可访问范围。在函数内声明的变量只能在该函数内部访问(局部作用域),而在任何函数外部声明的变量(包级别)都可以在整个包中访问(包作用域)。

  7. 命名规范: Go遵循驼峰命名法来命名变量。变量名应该具有描述性和有意义,有助于使代码更具可读性。

  8. 数据类型: Go支持各种数据类型的变量,包括:

    • 基本类型:int、float64、bool、string等。
    • 复合类型:数组、切片、映射、结构体等。
    • 指针类型:*int、*string等。

总的来说,Go语言中的变量是存储和操作数据的基本构建块。了解如何声明、初始化和使用变量是编写高效Go代码的关键。

M1.3.2 - Variable Initialization

在Go语言中,类型别名(Type Alias)是为现有类型创建一个新的名称,而不创建新的类型。类型别名提供了一种简单的方式来增强代码的可读性,并可以在不修改现有代码的情况下为类型提供更具描述性的名称。

以下是一个使用结构体的例子来说明类型别名的概念:

package main

import "fmt"

// 定义结构体
type Person struct {
    Name    string
    Age     int
}

// 定义类型别名
type Employee Person

func main() {
    // 创建一个 Person 类型的变量
    p1 := Person{"Alice", 30}

    // 创建一个 Employee 类型的变量
    e1 := Employee{"Bob", 25}

    fmt.Println("Person:", p1)
    fmt.Println("Employee:", e1)
}

在上面的代码中,我们定义了一个名为 Person 的结构体,包含了姓名和年龄两个字段。然后,我们使用类型别名 Employee 来创建了一个新的类型,它的底层类型是 Person。因此,Employee 类型和 Person 类型是等价的,它们具有相同的字段和方法。

main() 函数中,我们分别创建了一个 Person 类型的变量 p1 和一个 Employee 类型的变量 e1。尽管 p1e1 是不同的类型,但它们都具有相同的结构体字段,并且可以使用相同的方法。这体现了类型别名的灵活性和可重用性,同时增加了代码的可读性和理解性。

Variable initialization in Go refers to the process of assigning an initial value to a variable when it is declared. This ensures that the variable has a defined starting value before it is used in the program. There are several ways to initialize variables in Go:

  1. Explicit Initialization:
    Variables can be explicitly initialized by specifying their initial values when they are declared. This is done by using the assignment operator (=) after the variable name.

    var age int = 30
    
  2. Type Inference:
    In Go, the type of a variable can be inferred from the assigned value. In such cases, you can omit the type declaration and let the Go compiler determine the type based on the assigned value.

    var name = "John"
    
  3. Short Variable Declaration:
    Go provides a shorthand syntax for declaring and initializing variables, known as short variable declaration. This is done by using the := operator.

    age := 25
    

    In this example, Go infers the type of age based on the value 25, and declares it as an int.

  4. Zero Value Initialization:
    If a variable is declared without an explicit initialization, Go automatically assigns a default zero value to it based on its type. For example, numeric types are initialized to 0, strings to an empty string "", and boolean types to false.

    var count int  // count is initialized to 0
    var message string  // message is initialized to ""
    
  5. Multiple Variable Initialization:
    Go also allows initialization of multiple variables in a single line.

    var x, y int = 10, 20
    

    Here, x is initialized to 10 and y is initialized to 20.

Variable initialization is an important aspect of Go programming as it ensures that variables have valid starting values before they are used in computations or other operations within the program.

Module 1 Activity

$ cat test.go
package main

import "fmt"

func main() {
        fmt.Println("Hello, world!")
}

Module 1 Quiz

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

Week 02: Basic Data Types

Now that you’ve set up your programming environment and written a test program, you’re ready to dive into data types. This module introduces data types in Go and gives you practice writing routines that manipulate different kinds of data objects, including floating-point numbers and strings.

Learning Objectives


  • Describe the fundamental data types in Go.
  • Explain how program flow is controlled in Go.
  • Describe the process of garbage collection.
  • Write and implement a Go program that manipulates floating-point numbers and performs truncation.
  • Write and implement a Go program that manipulates strings.

Module 2 Overview

M2.1.1 - Pointers

"ampersand"指的是英文中的 “&” 符号

在Go语言中,指针(Pointer)是一种特殊的变量,它存储了另一个变量的内存地址。通过指针,可以直接访问或修改该变量的值,而无需拷贝整个变量的数据。

以下是关于Go语言指针的一些重要概念和用法:

  1. 声明指针: 指针变量声明时使用 * 符号来表示。例如,var ptr *int 声明了一个指向 int 类型的指针变量。

  2. 获取变量地址: 使用 & 符号来获取变量的地址。例如,var num int = 10,则 &num 表示 num 变量的地址。

  3. 访问指针指向的值: 使用 * 符号来访问指针指向的值。例如,*ptr 表示指针 ptr 指向的变量的值。

  4. 空指针: 指针变量默认值为 nil,表示空指针。空指针不指向任何有效的内存地址。

  5. 指针的零值: 未初始化的指针默认值也是 nil

  6. 通过指针修改变量的值: 使用指针可以直接修改变量的值。例如,*ptr = 20 将修改指针 ptr 所指向的变量的值为 20

  7. 指针的传递: 在函数调用中,可以通过将指针作为参数传递给函数,从而在函数内部直接修改变量的值。

指针在Go语言中被广泛应用于数据结构和函数调用等场景中,能够提高程序的性能和灵活性。但需要注意,使用指针时要小心处理,避免出现空指针引用和内存泄漏等问题。

在这里插入图片描述

在Go语言中,new 是一个内置函数,用于动态分配内存并返回指向该内存的指针。new 函数的签名如下:

func new(Type) *Type

new 函数接受一个类型作为参数,并返回一个指向该类型的零值的指针。这个指针可以用于访问和修改分配的零值。

下面是 new 函数的一些特点和用法:

  1. 分配内存: new 函数会在堆上为指定类型的变量分配内存空间。它返回一个指向分配的内存的指针。

  2. 初始化零值: new 函数返回的指针指向的内存会被初始化为对应类型的零值。对于数值类型,零值是 0;对于字符串类型,零值是空字符串 "";对于引用类型(如指针、slice、map、函数、接口和通道),零值是 nil

  3. 返回指针: new 函数返回的是指向所分配内存的指针,而不是分配的实际值。

下面是一个使用 new 函数的示例:

package main

import "fmt"

func main() {
    // 使用new函数动态分配一个整数的内存,并返回指向该整数的指针
    ptr := new(int)

    // 打印指针所指向的零值
    fmt.Println("Value of pointer:", *ptr) // 输出: Value of pointer: 0
}

在上面的示例中,new(int) 分配了一个 int 类型的零值,并返回一个指向该零值的指针。在 fmt.Println 中,我们通过 *ptr 来获取指针所指向的值,这里输出的是该 int 类型的零值 0

虽然 new 函数可以用来分配内存并返回指针,但在实际应用中,使用 new 函数的场景相对较少,因为在 Go 语言中,通常使用直接声明变量的方式来分配内存。

M2.1.2 - Variable Scope

在Go语言中,作用域规则遵循以下几个基本原则:

  1. 包级作用域(Package-level Scope):
    在一个包(package)中声明的变量、常量、类型和函数具有包级作用域。这意味着它们可以在同一个包中的任何地方被访问,但不能被其他包直接访问。包级作用域的标识符可以在整个包中的任何文件中使用。

  2. 函数级作用域(Function-level Scope):
    在函数内部声明的变量和常量具有函数级作用域。这意味着它们只能在声明它们的函数内部使用,超出函数范围就无法访问。每次函数调用都会创建一个新的作用域。

  3. 代码块作用域(Block-level Scope):
    在代码块(由 {} 包围的区域)内声明的变量具有代码块级作用域。这意味着它们只能在该代码块内部使用,超出代码块范围就无法访问。例如,在 ifforswitch 语句中声明的变量就具有代码块级作用域。

在Go语言中,作用域规则非常清晰且严格执行,有助于避免命名冲突和提高代码的可读性和可维护性。正确理解和掌握作用域规则对于编写高效、可靠的Go代码至关重要。

在Go语言中,“block”(代码块)是由一对花括号 {} 包围的代码区域。代码块可以包含一个或多个语句,并且可以嵌套在其他代码块中。在Go中,代码块用于创建局部作用域,限定变量的生命周期和可见性。

以下是关于Go语言中代码块的一些重要特点和用法:

  1. 作用域: 代码块内部声明的变量和常量具有代码块级作用域。这意味着它们只能在该代码块内部使用,超出代码块范围就无法访问。这种作用域规则有助于避免变量命名冲突和提高代码的可读性。

  2. 控制流语句: 在Go语言中,诸如 ifforswitch 等控制流语句都可以创建代码块。在这些语句中,代码块内的语句根据条件进行执行。

    • if 语句中,代码块根据条件的真假执行对应的语句。
    • for 循环中,代码块内的语句将被重复执行,直到循环条件为假。
    • switch 语句中,每个 case 子句都可以包含一个代码块,用于处理特定的情况。
  3. 函数体: 在Go语言中,函数体也是一个代码块。函数体内部的语句将在函数被调用时执行。函数体中声明的变量和常量具有函数级作用域,只能在函数内部使用。

  4. 匿名函数: 匿名函数也是一个代码块,它可以直接在其他代码中定义和使用,而不需要提前声明函数名。匿名函数常常用于在函数内部定义局部函数,或者作为函数参数传递给其他函数。

在Go语言中,代码块是一种重要的语言结构,它提供了一种便捷的方式来组织和控制程序的逻辑结构。通过合理使用代码块,可以提高代码的可读性、可维护性和可扩展性。

Lexical Scoping

在Go语言中,词法作用域(Lexical Scoping)是一种作用域规则,它确定了在源代码中的声明点和作用域位置之间的关系。词法作用域是一种静态作用域,它在编译时就已经确定,而不是在运行时确定。

在词法作用域中,一个变量的作用域由它在源代码中的声明位置所决定。换句话说,一个变量的作用域是由它所在的代码块或函数定义的位置决定的。这意味着在同一个作用域中,内部代码块可以访问外部代码块中声明的变量,而外部代码块无法访问内部代码块中声明的变量。

以下是词法作用域的一些重要特点:

  1. 变量可见性: 在词法作用域中,一个变量在其声明的代码块内是可见的,但在其声明之前或之后的代码块中是不可见的。这意味着一个代码块中的变量可以被该代码块内的所有子代码块访问,但无法被父级或兄弟级代码块访问。

  2. 闭包: 词法作用域的另一个重要特点是它支持闭包(Closure)。闭包是指一个函数捕获了其外部变量的引用,即使在该函数的定义之后,也可以在后续的调用中使用这些变量。由于词法作用域的存在,闭包可以在声明时捕获其外部变量,并在调用时访问这些变量的值。

  3. 嵌套函数: 在Go语言中,函数可以嵌套在其他函数内部定义。在嵌套函数中,内部函数可以访问外部函数的变量。这种特性在词法作用域下得到了实现,因为内部函数可以捕获外部函数的局部变量,并在整个外部函数的生命周期内使用这些变量。

词法作用域是一种常见的作用域规则,在很多编程语言中都得到了广泛的应用。它使得代码的可读性和可维护性得到了提高,并且在函数式编程中的闭包等特性的实现中起着重要作用。

下面是一个使用Go语言的代码示例,演示了词法作用域(Lexical Scoping)的概念:

package main

import "fmt"

// 外部函数
func outer() {
    // 外部函数的局部变量
    outerVar := 10
    
    // 内部函数
    inner := func() {
        // 内部函数中访问外部函数的局部变量
        fmt.Println("Inner function accessing outerVar:", outerVar)
    }

    // 调用内部函数
    inner()
}

func main() {
    // 调用外部函数
    outer()
}

在这个示例中,我们定义了一个外部函数 outer(),在函数内部声明了一个局部变量 outerVar。然后,我们定义了一个内部函数 inner(),在内部函数中访问了外部函数的局部变量 outerVar

通过这个示例,可以看出内部函数 inner() 可以访问外部函数 outer() 的局部变量 outerVar。这就是词法作用域的特性,内部函数在声明时就捕获了外部函数的局部变量,并在后续的调用中可以使用这些变量。

运行这个程序,将会输出:

Inner function accessing outerVar: 10

这表明内部函数成功地访问了外部函数的局部变量。这个示例清晰地展示了词法作用域在Go语言中的实现方式。

M2.1.3 - Deallocating Memory

Deallocating Space

在Go语言中,Stack(栈)和Heap(堆)是两种内存分配和管理的方式,它们有不同的特点和用途。

  1. Stack(栈):

    • 栈是一种后进先出(LIFO)的数据结构,用于存储函数调用期间的局部变量、函数参数、函数返回地址等数据。
    • 栈内存是有限的,它的大小通常受到操作系统的限制,每个线程都有自己的栈空间。
    • 栈内存的分配和释放是由编译器自动管理的,它们的生命周期与函数的调用和返回相关联,因此它们具有很高的效率。
    • 栈内存的使用通常用于存储较小的数据,具有较短的生命周期,以及需要快速分配和释放的情况。
  2. Heap(堆):

    • 堆是一种动态分配内存的方式,用于存储程序运行期间动态创建的变量、对象、数据结构等。
    • 堆内存的大小通常比栈内存大得多,它的分配和释放不受限制,并且可以在程序的任意位置分配和释放内存。
    • 堆内存的管理是由程序员手动进行的,需要通过调用 newmake 或者其他内存分配函数来分配内存,并通过 delete 或者垃圾回收机制来释放内存。
    • 堆内存的使用通常用于存储较大的数据、动态增长的数据结构(如切片、映射、通道等)、对象实例等,具有较长的生命周期。

下面是一个使用栈和堆的示例代码:

package main

import "fmt"

// 在栈上分配内存
func stackAllocation() {
    var x int = 10 // 在栈上分配一个整数变量
    fmt.Println("Value of x (Stack):", x)
}

// 在堆上分配内存
func heapAllocation() *int {
    // 使用new函数在堆上分配一个整数变量
    y := new(int)
    *y = 20 // 修改堆上的整数变量值
    return y // 返回指向堆上变量的指针
}

func main() {
    // 在栈上分配内存
    stackAllocation()

    // 在堆上分配内存
    ptr := heapAllocation()
    fmt.Println("Value of ptr (Heap):", *ptr)

    // 使用完堆上分配的内存后,需要手动释放
    // Go语言的垃圾回收器会自动回收不再使用的内存,但最好还是手动释放
    // 这样可以更及时地释放内存,减少内存占用
    // 在实际开发中,需要根据具体情况选择是否手动释放内存
    //defer fmt.Println("Defered deletion of ptr (Heap)")
    //delete(ptr)
}

在这个示例中,stackAllocation() 函数演示了如何在栈上分配内存,它声明了一个整数变量 x,该变量的值被存储在栈上。而 heapAllocation() 函数演示了如何在堆上分配内存,它使用 new 函数创建了一个整数变量,并返回了一个指向该变量的指针。在 main() 函数中,我们调用了这两个函数,并分别输出了栈上和堆上分配的内存的值。

需要注意的是,堆上分配的内存在使用完毕后应当手动释放。在Go语言中,垃圾回收器会自动回收不再使用的内存,但为了及时释放内存并减少内存占用,最好还是手动释放。在示例代码中,我们使用了 delete 函数来手动释放堆上分配的内存。

M2.1.4 - Garbage Collection

在Go语言中,内存管理是通过自动垃圾回收器(Garbage Collector,简称GC)来处理的。垃圾回收器负责检测和回收不再使用的内存对象,以便释放内存并避免内存泄漏。

以下是关于Go语言垃圾回收的一些重要概念和工作原理:

  1. 标记-清除算法(Mark and Sweep): Go语言的垃圾回收器采用标记-清除算法来回收不再使用的内存对象。该算法分为两个阶段:标记阶段和清除阶段。在标记阶段,垃圾回收器会遍历所有的可达对象,并标记它们为活动对象。在清除阶段,垃圾回收器会回收未被标记的对象,并将它们的内存释放。
  2. 并发标记和并发清除: Go语言的垃圾回收器是并发执行的,意味着垃圾回收过程不会阻塞程序的执行。标记阶段和清除阶段可以与程序的其他部分并发执行,从而减少了垃圾回收对程序性能的影响。
  3. 内存分配: Go语言的内存分配由运行时系统负责管理。当程序需要分配内存时,运行时系统会从堆(heap)中分配一块合适大小的内存,并返回一个指向该内存的指针。当内存不再使用时,垃圾回收器会自动回收该内存。
  4. 对象生命周期: 在Go语言中,垃圾回收器根据对象的引用关系来确定对象是否仍然活跃。如果一个对象不再被任何其他对象引用,则该对象将被认为是不再活跃的,并最终会被垃圾回收器回收。

M2.2.1 - Comments, Printing, Integers

Comments

Printing

在 Go 语言中,fmt 包提供了打印输出的功能。fmt 包中的 Print 系列函数用于将数据输出到控制台或其他输出流,并根据数据的类型进行格式化。以下是 fmt 包中常用的打印函数:

  1. Println: Println 函数用于打印数据并在最后添加一个换行符。它会将数据按照默认格式进行格式化输出。

    fmt.Println("Hello, world!")
    
  2. Printf: Printf 函数用于按照指定格式打印数据。你可以使用格式化占位符 % 来指定数据的输出格式。

    name := "Alice"
    age := 30
    fmt.Printf("Name: %s, Age: %d\n", name, age)
    
  3. Print: Print 函数与 Printf 类似,但是它不支持格式化。它直接将数据原样输出到控制台。

    fmt.Print("Hello, ")
    fmt.Print("world!\n")
    
  4. Sprint: Sprint 函数用于将数据转换为字符串,但不会打印输出。它返回一个字符串而不是将数据输出到控制台。

    str := fmt.Sprint("Hello, ", "world!")
    

这些是 fmt 包中用于打印输出的一些常用函数。根据需求和场景选择适合的打印函数,以方便地输出数据。

Integers

在 Go 中,整数(integers)是一种基本数据类型,用于表示整数值。整数类型在 Go 中是有限精度的,其大小取决于平台和编译器。

Go语言提供了多种整数类型,包括有符号整数和无符号整数。下面是Go语言中常见的整数类型:

  1. 有符号整数:

    • int:有符号整数类型,其大小与机器字长相同,32位或64位。
    • int8int16int32int64:有符号整数类型,分别表示8位、16位、32位和64位的有符号整数。
  2. 无符号整数:

    • uint:无符号整数类型,其大小与机器字长相同,32位或64位。
    • uint8uint16uint32uint64:无符号整数类型,分别表示8位、16位、32位和64位的无符号整数。
  3. 其他整数类型:

    • byte:与 uint8 等价,常用于表示字节值。
    • rune:与 int32 等价,常用于表示Unicode字符的码点值。

在使用整数时,需要注意以下几点:

  • 在算术运算中,如果两个整数类型不匹配,则会进行隐式类型转换。例如,int32int64 进行运算时,结果会自动转换为更大的类型。
  • 整数类型的取值范围取决于平台和编译器,但通常遵循固定规则。例如,int32 的取值范围通常为 -2^312^31-1uint64 的取值范围通常为 02^64-1
  • 在使用整数时,需要注意可能的溢出和精度损失问题,特别是在进行大数字计算时。

整数类型在Go语言中被广泛应用于各种算术运算、位运算和控制流程中,是编程中常用的基本数据类型之一。

M2.2.2 - Ints, Floats, Strings

Type Conversions

在 Go 中,类型转换(Type Conversion)是将一个数据类型的值转换为另一个数据类型的过程。类型转换可以在不同数据类型之间进行,但必须满足一定的规则。

以下是 Go 中类型转换的一些重要特点和技巧:

  1. 基本语法: 使用括号将目标类型放在待转换的值之前来执行类型转换。例如:destinationType(expression)

  2. 显式类型转换: Go 不允许隐式类型转换,因此在进行类型转换时需要显式地指定目标类型。例如:

    var x int = 10
    var y float64 = float64(x)
    

    这里将整数类型 x 转换为浮点数类型 float64

  3. 安全的类型转换: 在进行类型转换时,需要确保目标类型能够完全容纳待转换的值,否则可能会发生精度丢失或溢出。例如:

    var x int32 = 100
    var y int16 = int16(x) // 安全的类型转换,x 可以容纳在 int16 范围内
    
  4. 类型断言: 在 Go 中,接口类型的变量可以使用类型断言来转换为其他具体类型。类型断言的语法为 value.(Type),其中 value 是接口类型的变量,Type 是具体的目标类型。例如:

    var val interface{} = 42
    x := val.(int) // 类型断言将接口类型的值转换为 int 类型
    
  5. 字符串转换: 在 Go 中,字符串类型和字节切片类型之间可以相互转换。可以使用 string()[]byte() 函数来进行转换。例如:

    str := "hello"
    bytes := []byte(str) // 字符串转换为字节切片
    
    bytes := []byte{'h', 'e', 'l', 'l', 'o'}
    str := string(bytes) // 字节切片转换为字符串
    

在进行类型转换时,需要注意避免类型不匹配、溢出和精度丢失等问题,以确保程序的正确性和健壮性。同时,合理地使用类型转换可以提高代码的可读性和可维护性。

Floats

在 Go 中,float 类型用于表示浮点数,即带有小数点的数字。Go 提供了两种浮点数类型:float32float64,分别对应单精度浮点数和双精度浮点数。

以下是关于 Go 中浮点数的一些重要特点和注意事项:

  1. float32 和 float64: Go 提供了两种浮点数类型,分别是 float32float64float32 类型用于表示单精度浮点数,占用 32 位内存空间;float64 类型用于表示双精度浮点数,占用 64 位内存空间。通常推荐使用 float64 类型,因为它具有更高的精度。

  2. 表示范围: 浮点数类型可以表示一定范围内的小数值,但并不是所有的小数值都可以精确表示。在进行浮点数运算时,可能会出现精度损失的情况。因此,在比较浮点数时应该使用误差范围进行比较,而不是直接比较值是否相等。

  3. 默认类型: 当定义浮点数变量时,如果不指定类型,则默认为 float64 类型。例如:

    var x = 3.14 // 默认为 float64 类型
    
  4. 常量: Go 中可以使用科学计数法表示浮点数常量。例如:

    const pi = 3.1415926 // 无类型浮点数常量,默认为 float64 类型
    
  5. 类型转换: 可以在不同类型之间进行浮点数类型的转换,但需要显式地指定目标类型。例如:

    var x float32 = 3.14
    var y float64 = float64(x) // 显式将 float32 转换为 float64
    
  6. 零值: 浮点数类型的零值为 0。

在编写程序时,应根据需要选择合适的浮点数类型,并注意浮点数精度问题,以避免由于精度损失导致的错误。

Strings

ASCII(American Standard Code for Information Interchange)和 Unicode 都是字符编码标准,用于表示文本中的字符和符号。

  1. ASCII: ASCII 是一种最初用于英语的字符编码标准,它定义了128个字符,包括26个大写字母、26个小写字母、数字、标点符号和一些控制字符。ASCII 编码使用7位二进制数来表示一个字符,因此共有128个字符,其范围是从0到127。ASCII 编码标准在计算机领域得到了广泛应用,尤其是在早期的计算机系统中。

  2. Unicode: Unicode 是一种更为全面的字符编码标准,它旨在统一世界上所有文字和符号的表示方式。Unicode 定义了一个巨大的字符集,包含了几乎所有已知的语言和符号,共有数十万个字符。为了支持这么多的字符,Unicode 使用了不同长度的编码方案,包括UTF-8、UTF-16和UTF-32等。其中,UTF-8 是一种可变长度的编码方案,可以表示 Unicode 中的所有字符,同时保持与 ASCII 兼容,因此在 Web 开发和计算机系统中得到了广泛应用。

总的来说,ASCII 主要用于表示英语文本中的字符,而 Unicode 则是一种更为通用和全面的字符编码标准,可以支持世界上所有的语言和符号。在现代计算机系统中,通常使用 Unicode 编码来表示文本数据,以便支持多语言环境和国际化应用。

M2.2.3 - String Packages

Unicode Package

在 Go 中,unicode 包提供了一些用于处理 Unicode 字符的函数和工具。这些函数和工具允许你对 Unicode 字符进行分类、测试属性、转换大小写等操作。

下面是一些常见的 unicode 包中的函数和工具:

  1. Is函数系列: unicode 包提供了一系列的 IsXxx 函数,用于测试字符是否属于特定的 Unicode 分类。例如:

    • IsLetter:测试字符是否为字母。
    • IsDigit:测试字符是否为数字。
    • IsSpace:测试字符是否为空白字符。
    • IsUpper:测试字符是否为大写字母。
    • IsLower:测试字符是否为小写字母。
  2. To函数系列: unicode 包提供了一系列的 ToXxx 函数,用于将字符转换为特定的大小写形式。例如:

    • ToUpper:将字符转换为大写形式。
    • ToLower:将字符转换为小写形式。
  3. 特殊函数:

    • IsControl:测试字符是否为控制字符。
    • IsPrint:测试字符是否为可打印字符。
    • IsPunct:测试字符是否为标点符号。
    • IsSymbol:测试字符是否为符号字符。
    • IsMark:测试字符是否为标记字符(如重音符号、变音符号等)。
  4. Case和Range函数:

    • CaseRange:返回给定字符的 Unicode 大小写形式的范围。
    • Range:返回给定字符的 Unicode 属性范围。

rune 类型是 Go 语言中的一个别名,它实际上是 int32 类型的别名。在 Go 语言中,rune 类型通常用于表示 Unicode 码点(code point),即一个 Unicode 字符的值。因此,rune 类型是处理 Unicode 字符的首选类型。

通过结合使用 unicode 包和 rune 类型,你可以方便地处理 Unicode 字符,执行字符分类、大小写转换等操作,从而满足多样化的文本处理需求。

Strings Package

在 Go 语言中,strings 包提供了许多用于处理字符串的函数。这些函数包括字符串的拼接、切割、查找、替换、大小写转换等操作。下面是 strings 包中一些常用的函数和它们的功能:

  1. 字符串连接:

    • Concat:将多个字符串连接成一个字符串。
    • Join:将字符串切片连接成一个字符串,使用指定的分隔符。
  2. 字符串切割:

    • Split:根据指定的分隔符将字符串切割成多个子串,返回一个字符串切片。
    • Fields:根据空格将字符串切割成多个子串,返回一个字符串切片。
  3. 字符串查找:

    • Contains:判断一个字符串是否包含另一个字符串。
    • Index:返回一个字符串在另一个字符串中首次出现的位置。
  4. 字符串替换:

    • Replace:将字符串中指定的子串替换为另一个子串。
  5. 字符串大小写转换:

    • ToLower:将字符串中的字母转换为小写。
    • ToUpper:将字符串中的字母转换为大写。
  6. 字符串修剪:

    • Trim:删除字符串开头和结尾指定的字符。
    • TrimLeft:删除字符串开头指定的字符。
    • TrimRight:删除字符串结尾指定的字符。
  7. 字符串转换:

    • ParseBool:将字符串转换为布尔值。
    • ParseInt:将字符串转换为整数。
    • ParseFloat:将字符串转换为浮点数。
    • Atoi:将字符串转换为整数,快速失败版本。
    • Itoa:将整数转换为字符串。
  8. 其他功能:

    • Len:返回一个字符串的字节数。
    • Repeat:重复一个字符串多次。
    • Compare:比较两个字符串的大小。

这些函数提供了丰富的功能,能够满足处理字符串的各种需求。在实际编程中,可以根据具体情况选择合适的函数来处理字符串,以简化代码并提高效率。

String Manipulation

Strconv Package

strconv 包是 Go 语言标准库中的一个包,提供了字符串和基本数据类型之间的转换功能。它的名字源自 “string conversions”,用于处理字符串和基本数据类型之间的转换。

以下是 strconv 包中一些常用的函数和功能:

  1. 字符串转换为基本数据类型:

    • Atoi:将字符串转换为整数类型。
    • ParseBool:将字符串转换为布尔类型。
    • ParseFloat:将字符串转换为浮点数类型。
  2. 基本数据类型转换为字符串:

    • Itoa:将整数类型转换为字符串。
    • FormatBool:将布尔类型转换为字符串。
    • FormatFloat:将浮点数类型转换为字符串。
  3. 其他功能:

    • AppendBool:将布尔值转换为字符串并追加到指定的字节切片。
    • AppendIntAppendUint:将整数值转换为字符串并追加到指定的字节切片。
    • QuoteQuoteToASCII:将字符串转换为带有引号的字符串表示形式。

strconv 包中的函数提供了方便的方法来进行字符串和基本数据类型之间的转换,非常适用于处理从外部数据源读取的字符串或将数据转换为字符串的场景。在实际开发中,我们经常会用到这些函数来进行数据类型之间的转换,从而完成数据的解析、序列化和格式化等操作。

M2.3.1 - Constants

iota 是 Go 语言中的一个常量生成器,用于生成一组递增的整数常量值。iotaconst 声明中使用,它会自动从 0 开始递增,每次出现在常量声明中时都会自增 1。如果 iota 在同一行的多个常量声明中使用,它会根据出现的位置递增。

iota 的主要特点包括:

  1. 递增特性: 在常量声明中,每次出现 iota 时都会递增 1。如果 iota 出现在多个 const 声明中,它会根据每个 const 声明的位置递增。

  2. 重置特性:iota 出现在新的 const 声明中时,它会重新从 0 开始递增。

  3. 自动赋值特性:iota 出现在常量声明中时,它会自动被赋予当前的值。如果 iota 出现在常量表达式中,它将不会被自动赋值。

以下是一个示例,展示了 iota 的使用方式:

package main

import "fmt"

const (
    A = iota // 0
    B        // 1
    C        // 2
)

const (
    D = iota // 0
    E        // 1
    F        // 2
)

func main() {
    fmt.Println(A, B, C) // 输出:0 1 2
    fmt.Println(D, E, F) // 输出:0 1 2
}

在这个示例中,ABCDEF 都是使用 iota 定义的一组常量。在第一个 const 声明中,iota 会自动从 0 开始递增,因此 A 的值为 0,B 的值为 1,C 的值为 2。在第二个 const 声明中,iota 会重新从 0 开始递增,因此 D 的值为 0,E 的值为 1,F 的值为 2。

M2.3.2 - Control Flow

在 Go 中,iffor 是两种基本的控制流结构,用于实现条件判断和循环操作。

if 语句

if 语句用于根据条件执行代码块。其基本语法如下:

if condition {
    // 如果条件为真,则执行这里的代码块
} else if anotherCondition {
    // 如果上面的条件不满足,且这个条件为真,则执行这里的代码块
} else {
    // 如果上面的条件都不满足,则执行这里的代码块
}

例如:

x := 10
if x > 5 {
    fmt.Println("x is greater than 5")
} else if x == 5 {
    fmt.Println("x is equal to 5")
} else {
    fmt.Println("x is less than 5")
}

for 循环

for 循环用于重复执行一段代码,直到指定的条件不再满足。Go 语言中的 for 循环有多种形式,包括基本的 for 循环、for 循环的另一种形式 for range,以及用于循环的 breakcontinue 语句。

  1. 基本的 for 循环:

    for initialization; condition; post {
        // 在条件为真时执行这里的代码块
    }
    

    示例:

    for i := 0; i < 5; i++ {
        fmt.Println(i)
    }
    
  2. for range 循环:

    for range 循环用于遍历字符串、数组、切片、映射等集合类型的元素。

    for index, value := range collection {
        // 在每次迭代中,index 和 value 分别是集合中的索引和值
    }
    

    示例:

    for index, char := range "hello" {
        fmt.Printf("Character at index %d is %c\n", index, char)
    }
    
  3. 无限循环:

    可以使用空的 for 循环来创建无限循环。

    for {
        // 无限循环
    }
    
  4. break 和 continue:

    在循环中使用 break 语句可以立即终止循环,而使用 continue 语句可以跳过当前迭代,进入下一次迭代。

以上是 iffor 在 Go 中的基本用法。这些控制流结构提供了灵活的方式来实现条件判断和循环操作,是编写复杂程序的重要工具。

Switch/Case

在 Go 中,switch 是一种条件语句,用于根据表达式的值执行不同的代码块。switch 语句可以替代多个 if-else if-else 结构,使代码更加简洁和可读。switch 语句可以与 case 关键字一起使用,用于匹配特定的条件,并执行对应的代码块。

基本的 switch 语法如下:

switch expression {
case value1:
    // 如果 expression 的值等于 value1,则执行这里的代码块
case value2:
    // 如果 expression 的值等于 value2,则执行这里的代码块
default:
    // 如果 expression 的值不匹配任何 case,则执行这里的代码块
}

switch 语句中,expression 表示待匹配的表达式,value1value2 等表示可能的取值,case 关键字用于指定每个条件的匹配值。如果 expression 的值与某个 case 的值相等,则执行对应的代码块。如果没有匹配的 case,则执行 default 下的代码块。

例如:

day := "Wednesday"
switch day {
case "Monday", "Tuesday", "Wednesday", "Thursday", "Friday":
    fmt.Println("Weekday")
case "Saturday", "Sunday":
    fmt.Println("Weekend")
default:
    fmt.Println("Invalid day")
}

在这个例子中,switch 语句根据 day 的值执行不同的代码块。如果 day 的值为工作日(Monday 到 Friday),则打印 “Weekday”;如果 day 的值为周末(Saturday 或 Sunday),则打印 “Weekend”;如果 day 的值不匹配任何情况,则打印 “Invalid day”。

switch 语句还有一种形式,允许在 case 中使用表达式,而不是具体的值。这种形式称为 “expression switch”。例如:

age := 30
switch {
case age < 18:
    fmt.Println("Underage")
case age >= 18 && age < 65:
    fmt.Println("Adult")
case age >= 65:
    fmt.Println("Senior")
}

在这个例子中,根据 age 的值执行不同的代码块。case 中的表达式会被求值,并与 switch 表达式的值进行比较,以确定要执行的代码块。

switch 语句在 Go 中是一个非常灵活和强大的工具,可以根据不同的条件执行不同的代码块,使程序的逻辑更加清晰和可读。

在 Go 中,switch 语句与 C++ 中的 switch 语句有一些相似之处,但也有一些不同之处。

相似之处:

  1. 语法结构: Go 中的 switch 语句的语法与 C++ 中的 switch 语句相似,都是使用关键字 switch 后跟一个表达式,然后是一系列的 case 分支。

  2. 多分支判断: switch 语句可以用于根据表达式的值选择不同的分支进行执行。

不同之处:

  1. 自动break: 在 Go 中,每个 case 分支执行完毕后不会自动执行下一个分支,需要显式使用 fallthrough 关键字才能让程序执行下一个分支。而在 C++ 中,默认情况下,每个 case 分支执行完毕后会自动执行下一个分支,除非显式使用 break 关键字终止分支的执行。

  2. 表达式类型: Go 中的 switch 语句的表达式可以是任意类型,而 C++ 中的 switch 语句的表达式只能是整数类型或枚举类型。

在 Go 中,如果你需要在一个 case 分支执行完毕后继续执行下一个分支,可以在该分支的末尾使用 fallthrough 关键字。例如:

switch i {
case 1:
    fmt.Println("One")
    fallthrough
case 2:
    fmt.Println("Two")
default:
    fmt.Println("Other")
}

在这个例子中,如果 i 的值为 1,那么程序会输出:

One
Two

因为在第一个 case 分支中使用了 fallthrough 关键字,所以在执行完第一个分支后会继续执行下一个分支。

M2.3.3 - Control Flow, Scan

Tagless Switch

Scan

在 Go 中,scan 函数通常指的是 fmt.Scanfmt.Scanf 函数,用于从标准输入中读取用户输入的数据。

fmt.Scan 函数

fmt.Scan 函数用于从标准输入中按空格分隔读取多个值,并将这些值存储到指定的变量中。它的基本语法如下:

fmt.Scan(&variable1, &variable2, ...)

其中,&variable1, &variable2, ... 是一系列变量的地址,用于存储从标准输入读取的值。

例如:

var x, y int
fmt.Println("Enter two integers:")
fmt.Scan(&x, &y)
fmt.Println("You entered:", x, y)

fmt.Scanf 函数

fmt.Scanf 函数与 fmt.Scan 函数类似,不同之处在于它可以根据指定的格式从标准输入中读取数据。它的基本语法如下:

fmt.Scanf(formatString, &variable1, &variable2, ...)

其中,formatString 是一个格式化字符串,用于指定输入的格式,&variable1, &variable2, ... 是一系列变量的地址,用于存储从标准输入读取的值。

例如:

var name string
var age int
fmt.Println("Enter your name and age:")
fmt.Scanf("%s %d", &name, &age)
fmt.Printf("Hello, %s! You are %d years old.\n", name, age)

以上是 fmt.Scanfmt.Scanf 函数的基本用法,它们提供了一种方便的方式来从标准输入中读取用户输入的数据,并将其存储到变量中供后续处理使用。

Peer-graded Assignment: Module 2 Activity: trunc.go

Write a program which prompts the user to enter a floating point number and prints the integer which is a truncated version of the floating point number that was entered. Truncation is the process of removing the digits to the right of the decimal place.

Submit your source code for the program, “trunc.go”.

package main

import "fmt"

func main() {
	var input_number float64
	fmt.Println("Please enter a floating point number:")
	fmt.Scan(&input_number)

	truncated :=int(input_number)
	fmt.Printf("The truncated number is: %d", truncated)
}

Peer-graded Assignment: Module 2 Activity: findian.go

Write a program which prompts the user to enter a string. The program searches through the entered string for the characters ‘i’, ‘a’, and ‘n’. The program should print “Found!” if the entered string starts with the character ‘i’, ends with the character ‘n’, and contains the character ‘a’. The program should print “Not Found!” otherwise. The program should not be case-sensitive, so it does not matter if the characters are upper-case or lower-case.

Examples: The program should print “Found!” for the following example entered strings, “ian”, “Ian”, “iuiygaygn”, “I d skd a efju N”. The program should print “Not Found!” for the following strings, “ihhhhhn”, “ina”, “xian”.

Submit your source code for the program, “findian.go”.

package main

import (
	"fmt"
	"strings"
)

func main() {
	fmt.Println("Please enter a string:")
	var input_string string
	fmt.Scan(&input_string)

	lower_case_string := strings.ToLower(input_string)

	last_index := len(lower_case_string) - 1

	if lower_case_string[0] == 'i' && lower_case_string[last_index] == 'n' && strings.Contains(lower_case_string, "a") {
		fmt.Println("Found!")
	} else {
		fmt.Println("Not Found!")
	}
}

Module 2 Quiz

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

第七题答案好像是最后一个

Week 03: Composite Data Types

At this point, we’re ready to move into more complex data types, including arrays, slices, maps, and structs. As in the previous module, you’ll have a chance to practice writing code that makes use of these data types.

Learning Objectives


  • Describe arrays, slices, maps, and structs. Explain how they are used in Go programming.
  • Write a Go program employing a loop structure that fills a slice with an arbitrary number of integers.

Module 3 Overview

M3.1.1 - Arrays

在这里插入图片描述

在 Go 中,数组(array)是一种固定大小的数据结构,用于存储具有相同数据类型的元素序列。数组的大小在声明时就确定,并且不能动态改变。

数组的基本语法如下:

var arrayName [size]dataType

其中:

  • arrayName 是数组的名称。
  • size 是数组的大小,表示数组可以容纳的元素数量。
  • dataType 是数组中元素的数据类型。

例如,以下是一个包含 5 个整数的数组的声明:

var numbers [5]int

数组的索引是从 0 开始的,所以这个数组的索引范围是 0 到 4。可以通过索引来访问数组中的元素,例如:

numbers[0] = 10
numbers[1] = 20

数组的初始化也可以在声明时进行,例如:

var numbers = [5]int{10, 20, 30, 40, 50}

或者使用初始化表达式自动推断数组大小:

var numbers = [...]int{10, 20, 30, 40, 50}

数组的长度是数组类型的一部分,因此具有不同长度的数组是不同的类型。这意味着 [5]int[10]int 是不同的类型,不能相互赋值或进行比较。

数组是一种较为基本的数据结构,适用于存储固定大小的元素集合。然而,在实际开发中,切片(slice)通常更加灵活和常用,因为切片可以动态增长和缩小。

在这里插入图片描述

Iterating Through Arrays

在 Go 中遍历数组可以使用 for 循环结合数组的索引或者使用 range 关键字。

使用索引遍历数组

package main

import "fmt"

func main() {
    // 创建一个包含 5 个元素的整数数组
    numbers := [5]int{10, 20, 30, 40, 50}

    // 使用 for 循环和数组的索引遍历数组
    for i := 0; i < len(numbers); i++ {
        fmt.Println("Element", i, ":", numbers[i])
    }
}

使用 range 关键字遍历数组

package main

import "fmt"

func main() {
    // 创建一个包含 5 个元素的整数数组
    numbers := [5]int{10, 20, 30, 40, 50}

    // 使用 range 关键字遍历数组
    for index, value := range numbers {
        fmt.Println("Element", index, ":", value)
    }
}

在上述示例中,range 关键字用于迭代数组 numbers,每次迭代返回当前元素的索引和值。index 表示当前元素的索引,value 表示当前元素的值。

使用 range 关键字遍历数组是 Go 中常见的做法,因为它更简洁且不需要手动管理数组的索引。同时,range 关键字也适用于切片(slice)、映射(map)等数据结构的遍历。

M3.1.2 - Slices

Slices

在 Go 中,切片(slice)是一种动态数组,它是对数组的一个引用,并且可以动态增长和缩小。切片提供了一种方便且灵活的方式来处理序列化的数据。

创建切片

// 使用 make 函数创建一个切片,长度为 3,容量为 5
slice := make([]int, 3, 5)

上述代码创建了一个初始长度为 3、容量为 5 的整数切片。切片的长度是其当前包含的元素数量,容量是切片底层数组的大小。

切片初始化

// 直接初始化切片
slice := []int{1, 2, 3, 4, 5}

// 从数组中截取切片
array := [5]int{1, 2, 3, 4, 5}
slice := array[1:4]

切片操作

// 访问切片中的元素
fmt.Println(slice[0]) // 输出:2

// 追加元素到切片末尾
slice = append(slice, 6)

// 复制切片
copyOfSlice := make([]int, len(slice))
copy(copyOfSlice, slice)

切片遍历

for index, value := range slice {
    fmt.Printf("Index: %d, Value: %d\n", index, value)
}

切片长度和容量

切片的长度表示切片中当前包含的元素数量,可以通过内置函数 len() 获取。切片的容量表示切片底层数组的大小,可以通过内置函数 cap() 获取。

fmt.Println("Length:", len(slice))
fmt.Println("Capacity:", cap(slice))

切片的长度和容量可以随着元素的增加和删除而动态变化。

切片是 Go 中非常重要和常用的数据结构,它提供了对数组的抽象和更加灵活的操作方式,使得在处理序列化的数据时更加方便和高效。

Slice Examples

在 Go 中,切片(slice)有两个重要的属性:长度(length)和容量(capacity)。

  • 长度(Length): 切片中当前包含的元素数量。
  • 容量(Capacity): 切片底层数组中能够容纳的元素数量,即切片的最大长度。

以下是一个简单的示例代码,演示了切片的长度和容量:

package main

import "fmt"

func main() {
    // 创建一个初始长度为 3,容量为 5 的整数切片
    slice := make([]int, 3, 5)

    // 打印切片的长度和容量
    fmt.Println("Length:", len(slice))
    fmt.Println("Capacity:", cap(slice))

    // 向切片中添加元素
    slice = append(slice, 4, 5, 6)

    // 打印切片的长度和容量
    fmt.Println("Length:", len(slice))
    fmt.Println("Capacity:", cap(slice))
}

在上面的示例中,我们首先使用 make() 函数创建了一个初始长度为 3,容量为 5 的整数切片。然后,我们打印了切片的长度和容量,此时切片中包含了 3 个元素。接着,我们向切片中添加了 3 个元素,此时切片的长度变为 6,但是容量仍然保持不变,为 5。

运行以上代码,输出如下:

Length: 3
Capacity: 5
Length: 6
Capacity: 10

可以看到,切片的容量会根据需要自动增长,当切片的长度超过了容量时,Go 会自动重新分配底层数组,并将容量扩大为原来的两倍,以保证足够的容量来存储新增的元素。

Slice Iterals

Slice literals(切片字面量)是一种在 Go 中创建切片的简便方法,类似于数组字面量。Slice literals 的语法是在一对方括号 [] 中指定一组元素,以逗号分隔。在创建切片字面量时,可以选择性地指定切片的长度和容量。

以下是创建切片字面量的一般语法:

slice := []ElementType{element1, element2, ..., elementN}

其中:

  • slice 是切片的名称。
  • ElementType 是切片中元素的类型。
  • {element1, element2, ..., elementN} 是切片中包含的元素。

例如,下面是几个示例:

// 创建一个包含整数的切片
numbers := []int{1, 2, 3, 4, 5}

// 创建一个包含字符串的切片
fruits := []string{"apple", "banana", "orange"}

// 创建一个空的整数切片
emptySlice := []int{}

切片字面量可以在声明时初始化切片,并根据提供的元素自动推断切片的长度和容量。如果不指定切片的长度和容量,切片的长度和容量将根据提供的元素数量动态确定。这种方法使得切片的创建更加简洁和方便。

M3.1.3 - Variable Slices

在这里插入图片描述

M3.2.1 - Hash Tables

在 Go 中,哈希表通常是通过使用内置的 map 类型来实现的。哈希表是一种高效的数据结构,用于存储键值对,其中每个键都与一个值相关联。哈希表的主要特点是快速的查找、插入和删除操作,它通过哈希函数将键映射到存储桶(buckets)中,以便快速定位和访问值。

创建和初始化哈希表

要创建一个哈希表,可以使用 make() 函数或直接声明并初始化一个 map 类型的变量。以下是几种常见的创建和初始化哈希表的方法:

// 使用 make() 函数创建一个空的哈希表
m := make(map[keyType]valueType)

// 直接声明并初始化一个哈希表
m := map[keyType]valueType{
    key1: value1,
    key2: value2,
    // ...
}

其中 keyTypevalueType 分别表示键和值的类型,key1key2 等为键,value1value2 等为对应的值。

插入和访问元素

要向哈希表中插入元素,可以使用赋值语句将键值对分配给哈希表的键。要访问哈希表中的元素,可以使用键作为索引。

// 插入元素
m[key] = value

// 访问元素
v := m[key]

删除元素

要从哈希表中删除元素,可以使用 delete() 函数。

delete(m, key)

遍历哈希表

可以使用 range 关键字遍历哈希表中的所有键值对。

for key, value := range m {
    // 对每个键值对执行操作
}

哈希表在 Go 中是一种非常常用的数据结构,用于存储和检索键值对数据。它提供了快速、灵活和高效的方式来管理数据,并在许多情况下被广泛应用于实际编程中。

M3.2.2 - Maps

在这里插入图片描述

two-value assignment for existence of the key

在 Go 中,map 是一种哈希表(hash table)的实现,用于存储键值对。在使用 map 的过程中,经常需要检查某个键是否存在于 map 中。为了实现这一目的,Go 语言中的 map 提供了一种特殊的两值赋值(two-value assignment)机制。

当从 map 中读取一个值时,可以使用两值赋值方式。第一个值是该键所对应的值,第二个值是一个布尔值,表示该键是否存在于 map 中。如果键存在,则布尔值为 true,否则为 false。

下面是一个示例:

package main

import "fmt"

func main() {
    // 创建一个 map,存储学生的姓名和年龄
    students := map[string]int{
        "Alice": 20,
        "Bob":   21,
        "Charlie": 22,
    }

    // 读取 map 中的值,并判断键是否存在
    age, ok := students["Alice"]
    if ok {
        fmt.Println("Alice's age:", age)
    } else {
        fmt.Println("Alice not found")
    }

    age, ok = students["Eve"]
    if ok {
        fmt.Println("Eve's age:", age)
    } else {
        fmt.Println("Eve not found")
    }
}

在上面的示例中,我们首先创建了一个 map 存储学生的姓名和年龄。然后,我们通过两值赋值方式从 map 中读取键 “Alice” 和 “Eve” 对应的值。如果键存在,布尔值 ok 将为 true,否则为 false。通过检查布尔值,我们可以判断键是否存在,并据此执行相应的逻辑。

这种两值赋值机制为检查 map 中是否存在某个键提供了一种简洁和直观的方法。

M3.3.1 - Structs

在 Go 中,struct(结构体)是一种用户自定义的数据类型,用于组织和存储不同类型的数据字段。它是由一组字段(field)组成的数据结构,每个字段都有一个名称和一个相应的数据类型。结构体允许将多个相关的数据字段组合在一起,形成一个逻辑上的单元。

定义结构体

结构体的定义使用 typestruct 关键字,语法如下:

type StructName struct {
    Field1 DataType1
    Field2 DataType2
    // ...
}

其中:

  • StructName 是结构体的名称。
  • Field1, Field2, … 是结构体的字段名。
  • DataType1, DataType2, … 是字段的数据类型。

以下是一个简单的示例:

type Person struct {
    Name string
    Age  int
    City string
}

在这个示例中,我们定义了一个名为 Person 的结构体,它有三个字段:Name(字符串类型)、Age(整数类型)和City(字符串类型)。

创建结构体实例

可以通过结构体字面量来创建结构体的实例,也可以使用 var 关键字声明一个结构体类型的变量。

// 使用结构体字面量创建结构体实例
person := Person{"Alice", 25, "New York"}

// 使用 var 关键字声明结构体类型的变量
var person2 Person
person2.Name = "Bob"
person2.Age = 30
person2.City = "Los Angeles"

访问结构体字段

Accessing Struct Fields

可以通过点操作符 . 来访问结构体的字段:

fmt.Println("Name:", person.Name)
fmt.Println("Age:", person.Age)
fmt.Println("City:", person.City)

结构体提供了一种组织和管理数据的灵活方式,是 Go 中常用的数据类型之一。结构体可以嵌套在其他结构体中,也可以作为函数的参数或返回值。通过结构体,可以轻松地定义和操作复杂的数据结构。

Peer-graded Assignment: Module 3 Activity: slice.go

Write a program which prompts the user to enter integers and stores the integers in a sorted slice. The program should be written as a loop. Before entering the loop, the program should create an empty integer slice of size (length) 3. During each pass through the loop, the program prompts the user to enter an integer to be added to the slice. The program adds the integer to the slice, sorts the slice, and prints the contents of the slice in sorted order. The slice must grow in size to accommodate any number of integers which the user decides to enter. The program should only quit (exiting the loop) when the user enters the character ‘X’ instead of an integer.

Submit your source code for the program, “slice.go”.

package main

import (
	"fmt"
	"sort"
	"strconv"
)

func main() {
	slice := make([]int, 0, 3)

	for {
		var input string
		fmt.Print("Enter an integer (or 'X' to quit): ")
		fmt.Scanln(&input)

		if input == "X" || input == "x" {
			break
		}

		num, err := strconv.Atoi(input)

		if err != nil {
			fmt.Println("Invalid input. Please enter an integer or 'X' to quit.")
			continue
		}

		slice = append(slice, num)
		sort.Ints(slice)

		fmt.Println("Sorted slice: ", slice)

	}
}

测试结果

在这里插入图片描述

Module 3 Quiz

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

第三题再做一遍,这次理解了slice的capacity。

在这里插入图片描述

The capacities are the difference between the length of the underlying array (len(x) = 5) and the starting index of the slice. So cap(y) = 5 and cap(z) = 4.

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

Week 04: Protocols and Formats

This final module of the course introduces the use of remote function calls (RFCs) and JavaScript Object Notation (JSON) in Go. You’ll learn how to access and manipulate data from external files, and have an opportunity to write several routines using Go that exercise this functionality.

Learning Objectives


  • Describe remote function calls (RFCs).
  • Describe JavaScript Object Notation (JSON).
  • Describe methods for accessing data in files.
  • Write and implement a Go program that employs RFCs and JSON.
  • Write and implement a Go program that reads structured text data from a file and then performs some manipulations with those data.

Module 4 Overview

M4.1.1 - RFCs

Protocols and Formats

Requests for Comments (RFC)

Definitions of Internet protocols and formats

M4.1.2 - JSON

JSON Marshalling

JSON Marshalling 是指将 Go 中的数据结构转换为 JSON 格式的过程。在 Go 中,Marshalling 指的是将数据序列化为特定格式的字符串,而 JSON 是一种常用的数据交换格式,具有简洁、易读、跨语言等特点,因此在网络通信和数据存储中被广泛使用。

要将 Go 中的数据结构转换为 JSON 格式,可以使用 encoding/json 包中的 Marshal 函数或 json.Marshal 方法。这些方法将 Go 中的数据结构转换为 JSON 格式的字节切片。

例如,以下是一个简单的示例:

package main

import (
	"encoding/json"
	"fmt"
)

type Person struct {
	Name string `json:"name"`
	Age  int    `json:"age"`
}

func main() {
	p := Person{"Alice", 30}

	// 将 Person 结构体转换为 JSON 字符串
	jsonData, err := json.Marshal(p)
	if err != nil {
		fmt.Println("JSON Marshalling failed:", err)
		return
	}

	fmt.Println("JSON data:", string(jsonData))
}

在这个示例中,定义了一个 Person 结构体表示一个人的信息。通过调用 json.Marshal 函数将 Person 结构体转换为 JSON 格式的字符串。最后打印出转换后的 JSON 数据。

值得注意的是,结构体字段的标签 json:"name"json:"age" 用于指定 JSON 字段的名称。在 Marshalling 过程中,这些标签会被用来生成对应的 JSON 键。

JSON Unmarshalling

M4.2.1 - File Access, ioutil

ioutil 是 Go 语言标准库中的一个包,用于提供对文件 I/O 操作的简便接口。ioutil 包包含了一系列用于读取和写入文件的函数,以及其他一些文件操作的实用函数。

以下是 ioutil 包中一些常用的函数:

  1. ReadFile(filename string) ([]byte, error):读取文件的内容并返回一个字节切片。该函数会将整个文件的内容读入内存,适用于文件不太大的情况。

  2. WriteFile(filename string, data []byte, perm os.FileMode) error:将字节切片写入文件。如果文件不存在,将创建一个新文件;如果文件已存在,将覆盖原有内容。

  3. ReadDir(dirname string) ([]os.FileInfo, error):读取目录中的文件列表,并返回一个文件信息的切片。

  4. TempDir(dir, prefix string) (string, error):创建一个临时目录,并返回其路径。dir 参数指定了临时目录的父目录,若为空字符串则使用系统默认的临时目录。

  5. TempFile(dir, prefix string) (f *os.File, err error):在指定目录中创建一个临时文件,并返回该文件的句柄。dir 参数指定了临时文件所在的目录,若为空字符串则使用系统默认的临时目录。

这些函数提供了一种简洁的方式来执行文件操作,并且隐藏了许多底层的细节。但需要注意的是,ioutil 包中的一些函数可能会一次性加载整个文件内容到内存中,因此不适用于处理大文件。对于大文件的处理,建议使用 os 包中提供的更灵活的接口。

M4.2.2 - File Access, os

os File Reading

os File Create/Write

在 Go 的 os 包中提供了许多用于文件操作的函数和方法,以下是一些常用的文件操作函数:

  1. 创建文件

    • func Create(name string) (*File, error): 创建一个文件,如果文件已存在则截断为零长度。
  2. 打开文件

    • func Open(name string) (*File, error): 以只读模式打开一个文件。
    • func OpenFile(name string, flag int, perm FileMode) (*File, error): 打开一个文件用于读写操作。flag 参数指定打开文件的方式,perm 参数指定文件的权限。
  3. 读取文件内容

    • func (f *File) Read(b []byte) (n int, err error): 从文件中读取数据到字节切片中。
    • func (f *File) ReadAt(b []byte, off int64) (n int, err error): 从文件指定偏移量处开始读取数据到字节切片中。
  4. 写入文件内容

    • func (f *File) Write(b []byte) (n int, err error): 将字节切片中的数据写入文件。
    • func (f *File) WriteAt(b []byte, off int64) (n int, err error): 将字节切片中的数据从指定偏移量处开始写入文件。
  5. 文件操作

    • func (f *File) Seek(offset int64, whence int) (ret int64, err error): 设置文件的读写偏移量。
    • func (f *File) Close() error: 关闭文件。
  6. 文件信息

    • func Stat(name string) (FileInfo, error): 返回指定文件的文件信息。
    • type FileInfo interface { ... }: 文件信息接口,包含了获取文件信息的方法,如文件名、文件大小、修改时间等。
  7. 其他功能

    • func Remove(name string) error: 删除指定的文件或目录。
    • func Rename(oldpath, newpath string) error: 重命名文件或目录。

通过这些函数和方法,可以方便地进行文件的创建、打开、读写、关闭、删除、重命名等操作。值得注意的是,在进行文件操作时应该注意错误处理,以确保操作的安全性和稳定性。

Peer-graded Assignment: Module 4 Activity: makejson.go

Write a program which prompts the user to first enter a name, and then enter an address. Your program should create a map and add the name and address to the map using the keys “name” and “address”, respectively. Your program should use Marshal() to create a JSON object from the map, and then your program should print the JSON object.

Submit your source code for the program, “makejson.go”.

package main

import (
	"encoding/json"
	"fmt"
)

func main() {
	// Prompt the user to enter a name
	fmt.Print("Enter a name: ")
	var name string
	fmt.Scanln(&name)

	// Prompt the user to enter an address
	fmt.Print("Enter an address: ")
	var address string
	fmt.Scanln(&address)

	// Create a map to store the name and address
	person := map[string]string{
		"name":    name,
		"address": address,
	}

	// Convert the map to JSON
	jsonData, err := json.Marshal(person)
	if err != nil {
		fmt.Println("Error:", err)
		return
	}

	// Print the JSON object
	fmt.Println("JSON Object:")
	fmt.Println(string(jsonData))
}

Peer-graded Assignment: Final Course Activity: read.go

Write a program which reads information from a file and represents it in a slice of structs. Assume that there is a text file which contains a series of names. Each line of the text file has a first name and a last name, in that order, separated by a single space on the line.

Your program will define a name struct which has two fields, fname for the first name, and lname for the last name. Each field will be a string of size 20 (characters).

Your program should prompt the user for the name of the text file. Your program will successively read each line of the text file and create a struct which contains the first and last names found in the file. Each struct created will be added to a slice, and after all lines have been read from the file, your program will have a slice containing one struct for each line in the file. After reading all lines from the file, your program should iterate through your slice of structs and print the first and last names found in each struct.

Submit your source code for the program, “read.go”.

package main

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

// Define a struct for name with fields fname and lname
type name struct {
	fname string
	lname string
}

func main() {
	// Prompt the user for the name of the text file
	fmt.Print("Enter the name of the text file: ")
	var fileName string
	fmt.Scanln(&fileName)

	// Open the text file
	file, err := os.Open(fileName)
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	defer file.Close()

	// Create a slice to store structs
	var names []name

	// Create a scanner to read the file line by line
	scanner := bufio.NewScanner(file)
	for scanner.Scan() {
		line := scanner.Text()

		// Split the line into first name and last name
		parts := strings.Split(line, " ")
		if len(parts) != 2 {
			fmt.Println("Invalid format:", line)
			continue
		}

		// Create a struct and add it to the slice
		names = append(names, name{
			fname: parts[0],
			lname: parts[1],
		})
	}

	// Check for any errors during scanning
	if err := scanner.Err(); err != nil {
		fmt.Println("Error:", err)
		return
	}

	// Print the first and last names found in each struct
	for _, n := range names {
		fmt.Printf("First Name: %s, Last Name: %s\n", n.fname, n.lname)
	}
}

后记

完成这门课

在这里插入图片描述

2024年3月15日完成这门课的学习,从昨天晚上心血来潮学习这门课,到今天晚上,总共花费1天的时间完成。这个Specialzation有三门课,后续两门课不知道何时能够有时间学习。高效能人士的习惯有一条是以终为始,希望自己今年能够完成Golang这个Specialization专项。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值