Coursera上Golang专项课程2:Functions, Methods, and Interfaces in Go 学习笔记

Functions, Methods, and Interfaces in Go

Course Certificate
在这里插入图片描述

本文是学习 Functions, Methods, and Interfaces in Go 这门课的学习笔记,如有侵权,请联系删除。

在这里插入图片描述

MODULE 1: FUNCTIONS AND ORGANIZATION

Continue your exploration of the Go programming language as you learn about functions, methods, and interfaces. Topics include the implementation of functions, function types, object-orientation in Go, methods, and class instantiation. As with the first course in this series, you’ll have an opportunity to create your own Go applications so you can practice what you’re learning. The first module introduces the concept of functions and shows you how to use them to manipulate data programmatically. Once again, the treatment of the topic begins at a fairly rudimentary level to ensure everyone is starting from the same point in terms of basic concepts.

Learning Objectives


  • Identify the basic features and purposes of functions.
  • Identify the benefits of using pointers when working with functions.
  • Identify the difference between passing a slice and an array as function arguments.
  • Use functions and a slice to implement a sorting routine for integers.

Pass by Pointer vs. Pass by Reference in Go Programming

M1.1.1 - Why Use Functions?

从抽象和复用的角度来看,函数在编程中起着至关重要的作用:

  1. 抽象

    • 函数允许将复杂的操作和逻辑抽象为简单的、易于理解的单元。通过将代码封装在函数中,我们可以隐藏具体的实现细节,只暴露函数的接口和功能。
    • 函数提供了一种语义化的方式来描述代码的作用和意图。通过函数名称和参数列表,我们可以清晰地了解函数的功能和使用方式,从而使代码更易于理解和维护。
  2. 复用

    • 函数可以被多次调用,从而实现了代码的复用。通过将通用的功能封装在函数中,我们可以在不同的地方调用同一个函数,避免了重复编写相同的代码。
    • 函数允许我们将代码逻辑抽象为独立的单元,这些单元可以被其他部分的代码重复利用。这种复用性使得我们可以更快速地开发和扩展程序。

通过抽象和复用,函数使得代码更易于编写、理解和维护。它们提供了一种有效的方式来组织和管理代码,使得我们可以更高效地构建复杂的软件系统。在面向对象编程中,函数也是构建类和对象的基础,类中的方法本质上也是函数,因此函数的抽象和复用也是面向对象编程中的重要概念。

M1.1.2 - Function Parameters and Return Values

在 Go 语言中,函数是一段完成特定任务的代码块,具有一定的输入和输出。函数可以被多次调用,并且可以接受参数,执行一系列操作,然后返回结果。

以下是函数在 Go 中的一些基本特性和用法:

  1. 函数声明:函数的声明使用 func 关键字,后跟函数的名称、参数列表和返回值类型。
func functionName(parameter1 type1, parameter2 type2) returnType {
    // 函数体
}
  1. 函数参数:函数可以接受零个或多个参数,并且每个参数都必须指定类型。参数列表用圆括号括起来,多个参数之间用逗号分隔。
func add(x int, y int) int {
    return x + y
}
  1. 函数返回值:函数可以返回一个或多个值。返回值列表用括号括起来,如果函数有返回值,则必须在函数体内使用 return 语句来返回结果。
func divide(dividend int, divisor int) (int, error) {
    if divisor == 0 {
        return 0, errors.New("division by zero")
    }
    return dividend / divisor, nil
}
  1. 函数调用:通过函数名称和参数列表来调用函数。
sum := add(3, 5)
result, err := divide(10, 2)
  1. 匿名函数:在 Go 中可以创建匿名函数,即没有名称的函数,用于一次性的场景。
func main() {
    // 匿名函数赋值给变量
    add := func(x, y int) int {
        return x + y
    }
    sum := add(3, 5)
    fmt.Println(sum) // 输出 8
}
  1. 多返回值:函数可以返回多个值,这在 Go 中是一个常见的特性。
func divide(dividend int, divisor int) (int, int) {
    quotient := dividend / divisor
    remainder := dividend % divisor
    return quotient, remainder
}
  1. 可变参数:函数可以接受可变数量的参数。
func sum(nums ...int) int {
    total := 0
    for _, num := range nums {
        total += num
    }
    return total
}

这些是 Go 中函数的基本特性和用法。函数是 Go 语言中的重要概念,可以帮助将代码模块化、组织化,提高代码的可读性和复用性。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

M1.1.3 - Call by Value, Reference

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在 Go 中,函数参数传递采用的是值传递(call by value)机制,而不是引用传递(call by reference)。

  1. Call by Value(按值传递)
    • 在调用函数时,参数的值会被复制一份,然后传递给函数。函数内部对参数的修改不会影响到调用者的原始数据。
    • 在函数内部对参数进行修改时,只会修改函数内部的副本,不会影响到函数外部的原始数据。

示例:

package main

import "fmt"

func modify(x int) {
    x = 10 // 修改函数内部的副本
}

func main() {
    a := 5
    modify(a)
    fmt.Println(a) // 输出 5,因为函数内部对参数的修改不会影响到原始数据
}
  1. Call by Reference(按引用传递)
    • 在调用函数时,函数接收的是变量的地址(指针),而不是变量的值。这意味着函数可以直接访问和修改变量的原始数据。
    • 在函数内部对参数进行修改时,会直接影响到函数外部的原始数据。

在 Go 中并不存在严格意义上的引用传递,但可以通过传递指针来模拟引用传递的效果。

示例:

package main

import "fmt"

func modify(x *int) {
    *x = 10 // 修改原始数据
}

func main() {
    a := 5
    modify(&a)
    fmt.Println(a) // 输出 10,因为函数内部修改了原始数据
}

综上所述,Go 中的函数参数传递采用的是值传递机制,但可以通过传递指针来实现类似引用传递的效果,从而实现在函数内部修改原始数据。

M1.1.4 - Passing Arrays and Slices

Passing Array Arguments

Passing Array Pointers

在 Go 中,数组作为参数传递给函数可以采用两种方式:传递数组本身和传递数组指针。下面分别介绍这两种方式:

Passing Array Arguments(传递数组本身)

当你将一个数组作为参数传递给函数时,实际上是将数组的一个副本传递给函数。这意味着在函数内部对数组的修改不会影响到函数外部的原始数组。

示例:

package main

import "fmt"

// 函数接受一个数组作为参数
func modifyArray(arr [3]int) {
    arr[0] = 100 // 修改函数内部的副本
}

func main() {
    // 定义并初始化数组
    nums := [3]int{1, 2, 3}

    // 调用函数传递数组
    modifyArray(nums)

    // 打印原始数组
    fmt.Println(nums) // 输出 [1 2 3],因为函数内部对数组的修改不会影响到原始数组
}

Passing Array Pointers(传递数组指针)

如果你想在函数内部修改原始数组,可以将数组的指针传递给函数。在函数内部,可以通过指针来访问和修改数组的元素。

示例:

package main

import "fmt"

// 函数接受一个数组指针作为参数
func modifyArray(arr *[3]int) {
    (*arr)[0] = 100 // 修改原始数组
}

func main() {
    // 定义并初始化数组
    nums := [3]int{1, 2, 3}

    // 调用函数传递数组指针
    modifyArray(&nums)

    // 打印修改后的原始数组
    fmt.Println(nums) // 输出 [100 2 3]
}

通过传递数组指针,函数可以直接修改原始数组的内容,而不是对数组的副本进行操作。这使得可以在函数内部对原始数据进行修改,并且函数的开销较小,因为只需要传递指针而不是整个数组的副本。

在这里插入图片描述

Pass Slices Instead

在 Go 中,更常见的做法是通过传递切片(slice)而不是数组来处理函数参数。这是因为切片是对底层数组的一个动态视图,而且切片的长度和容量等信息都被封装在切片类型中,使得对切片的处理更加灵活和方便。

当你需要在函数中对数组进行操作时,通常会将数组转换为切片,然后将切片传递给函数。函数接受的参数是一个切片,它可以灵活地操作切片中的元素,而且对切片的修改会影响到原始数据。

示例:

package main

import "fmt"

// 函数接受一个切片作为参数
func modifySlice(slice []int) {
    // 在切片末尾添加一个新元素
    slice = append(slice, 4)
}

func main() {
    // 定义并初始化数组
    nums := []int{1, 2, 3}

    // 调用函数传递切片
    modifySlice(nums)

    // 打印原始切片
    fmt.Println(nums) // 输出 [1 2 3 4],因为函数内部对切片的修改会影响到原始切片
}

在这个示例中,函数接受一个切片作为参数,并在切片末尾添加了一个新元素。由于切片是对底层数组的引用,所以对切片的修改会影响到原始数据。

通过传递切片而不是数组,我们可以更灵活地处理数据,使得函数的设计更加简洁和易于理解。这也是 Go 中推荐的做法。

M1.2.1 - Well-Written Functions

Understandability

在这里插入图片描述

M1.2.2 - Guidelines for Functions

Function Naming

Functional Cohesion

Few Parameters

M1.2.3 - Function Guidelines

Function Complexity

Function Call Hierarchy

Control-flow Complexity

Partitioning Conditionals

Module 1 Quiz

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

Peer-graded Assignment: Module 1 Activity: Bubble Sort Program

Write a Bubble Sort program in Go. The program should prompt the user to type in a sequence of up to 10 integers. The program should print the integers out on one line, in sorted order, from least to greatest. Use your favorite search tool to find a description of how the bubble sort algorithm works.

As part of this program, you should write a function called BubbleSort() which takes a slice of integers as an argument and returns nothing. The BubbleSort() function should modify the slice so that the elements are in sorted order.

A recurring operation in the bubble sort algorithm is the Swap operation which swaps the position of two adjacent elements in the slice. You should write a Swap() function which performs this operation. Your Swap() function should take two arguments, a slice of integers and an index value i which indicates a position in the slice. The Swap() function should return nothing, but it should swap the contents of the slice in position i with the contents in position i+1.

Submit your Go program source code.

package main

import (
	"fmt"
	"strconv"
)

// BubbleSort sorts the given slice of integers using the Bubble Sort algorithm
func BubbleSort(nums []int) {
	n := len(nums)
	for i := 0; i < n-1; i++ {
		for j := 0; j < n-i-1; j++ {
			if nums[j] > nums[j+1] {
				Swap(nums, j)
			}
		}
	}
}

// Swap swaps the elements at index i and i+1 in the given slice of integers
func Swap(nums []int, i int) {
	nums[i], nums[i+1] = nums[i+1], nums[i]
}

func main() {
	var nums []int

	// Prompt user to input integers
	fmt.Println("Enter integers (press 'X' to stop):")
	for len(nums) < 10 {
		var input string
		fmt.Scan(&input)
		if input == "X" || input == "x" {
			break
		}
		num, err := strconv.Atoi(input)
		if err != nil {
			fmt.Println("Invalid input. Please enter a valid integer.")
			continue
		}
		nums = append(nums, num)
	}

	// Sort the integers using Bubble Sort
	BubbleSort(nums)

	// Print the sorted integers
	fmt.Println("Sorted integers:")
	for _, num := range nums {
		fmt.Printf("%d ", num)
	}
	fmt.Println()
}

MODULE 2: FUNCTION TYPES

This second module continues to explore the use of functions in Go. You’ll continue learning about the capabilities and features of functions, and write a routine that solves a practical physics problem.

Learning Objectives


  • Identify advanced types, properties, and uses of functions.
  • Identify the output that would result from running a given code block containing functions.
  • Develop a routine containing functions in Go that solves a practical physics problem.

M2.1.1 - First-Class Values

Functions are First-class

在这里插入图片描述

Variables as Functions

Functions as Arguments

Anonymous Functions

在 Go 中,函数是一等公民(First-class citizen),这意味着函数可以像其他类型的值一样被传递、赋值、返回和存储。这种特性使得函数在 Go 中具有很高的灵活性和表达力。

具体来说,函数是一等公民的特性包括:

函数可以被赋值给变量

var add func(int, int) int

add = func(x, y int) int {
    return x + y
}

这段代码首先声明了一个变量 add,其类型为一个接受两个 int 类型参数并返回一个 int 类型结果的函数。

然后,通过匿名函数的方式将一个函数赋值给了变量 add。这个匿名函数接受两个参数 xy,并返回它们的和 x + y

因此,通过这段代码,我们创建了一个名为 add 的函数变量,并将其赋值为一个可以对两个整数进行加法运算的函数。这样,我们可以通过调用 add 变量来执行加法运算,就像调用一个普通的函数一样。例如:

result := add(3, 5)
fmt.Println(result) // 输出 8

函数可以作为参数传递给其他函数

func apply(f func(int, int) int, x, y int) int {
    return f(x, y)
}

这段代码定义了一个名为 apply 的函数,它接受三个参数:

  1. f:一个函数变量,它接受两个整数参数并返回一个整数结果。
  2. xy:两个整数参数。

函数 apply 的作用是将函数 f 应用到参数 xy 上,并返回函数 f 的结果。

具体来说,apply 函数通过调用 f(x, y) 来执行函数 f,并返回函数 f 的结果。

这个函数的设计使得我们可以将任意的函数作为参数传递给 apply 函数,并对任意的整数进行处理。这种灵活性使得 apply 函数可以用于各种情况,例如对两个整数进行加法、减法、乘法等操作。

下面是调用 apply 函数的代码示例:

package main

import "fmt"

// apply 函数接受一个函数 f 和两个整数 x 和 y,并将函数 f 应用到 x 和 y 上
func apply(f func(int, int) int, x, y int) int {
	return f(x, y)
}

// add 函数接受两个整数并返回它们的和
func add(x, y int) int {
	return x + y
}

// subtract 函数接受两个整数并返回它们的差
func subtract(x, y int) int {
	return x - y
}

func main() {
	// 调用 apply 函数,并将 add 函数作为参数传递给 apply
	sum := apply(add, 3, 5)
	fmt.Println("Sum:", sum) // 输出 8

	// 调用 apply 函数,并将 subtract 函数作为参数传递给 apply
	diff := apply(subtract, 10, 7)
	fmt.Println("Difference:", diff) // 输出 3
}

在这个示例中,我们定义了两个函数 addsubtract,分别用于执行加法和减法操作。然后,我们通过调用 apply 函数,将这两个函数作为参数传递给 apply 函数,从而实现对两个整数进行加法和减法操作。最终,我们输出了加法和减法的结果。

函数可以匿名定义

func(x, y int) int {
    return x + y
}

在这里插入图片描述

这些特性使得函数在 Go 中可以更灵活地使用,例如可以将函数作为参数传递给其他函数来实现回调机制,或者将函数作为返回值用于构造闭包等。这种函数式编程的风格使得 Go 语言在处理并发、事件驱动等场景中变得更加强大和易用。

M2.1.2 - Returning Functions

函数可以作为返回值

func getAdder() func(int, int) int {
    return func(x, y int) int {
        return x + y
    }
}

问题:func getAdder() func(int, int) int 中为什么getAdder前面有func关键字,后面也有关键字func?

回答:

在 Go 中,func 关键字用于声明一个函数。在 func getAdder() func(int, int) int 这个语句中,有两个 func 关键字的原因是:

  1. 第一个 func 关键字用于声明 getAdder 函数,它表示 getAdder 是一个函数。
  2. 第二个 func 关键字用于声明 getAdder 函数的返回值类型,它表示 getAdder 函数返回的是一个函数。

这种语法称为函数返回函数(Function Returning Function)。在 Go 中,函数可以作为参数传递给其他函数,也可以作为另一个函数的返回值,这使得函数式编程在 Go 中变得非常方便。

问题:再举一个例子,getAdder函数有输入,返回值仍然是一个函数的例子

回答:

下面是一个例子,getAdder 函数接受一个整数参数,并返回一个函数,这个函数接受一个整数参数,并返回两个整数的和:

package main

import "fmt"

// getAdder 函数接受一个整数参数 x,并返回一个函数,该函数接受一个整数参数 y,并返回 x+y 的结果
func getAdder(x int) func(int) int {
	// 返回一个闭包,捕获了参数 x,并对每个传入的 y 返回 x+y 的结果
	return func(y int) int {
		return x + y
	}
}

func main() {
	// 获取一个将传入的整数与 10 相加的函数
	add10 := getAdder(10)

	// 调用 add10 函数,传入参数 5,并输出结果
	result := add10(5)
	fmt.Println("Result:", result) // 输出 15
}

在这个示例中,getAdder 函数接受一个整数参数 x,并返回一个函数,这个函数接受一个整数参数 y,并返回 x+y 的结果。在 main 函数中,我们使用 getAdder 函数获取了一个将传入的整数与 10 相加的函数 add10。然后,我们调用 add10 函数,并传入参数 5,得到 15 作为结果。

函数可以被存储在数据结构中

type MathFunc func(int, int) int

var mathFuncs map[string]MathFunc
mathFuncs = map[string]MathFunc{
    "add": func(x, y int) int { return x + y },
    "sub": func(x, y int) int { return x - y },
}

这段代码首先定义了一个新的函数类型 MathFunc,这个类型表示一个接受两个整数参数并返回一个整数结果的函数。

然后,定义了一个名为 mathFuncs 的变量,它是一个字符串到 MathFunc 函数类型的映射。在这个 mathFuncs 变量中,我们可以存储不同的数学运算函数,并通过字符串键来访问它们。

接下来,使用字面量初始化了这个 mathFuncs 映射,并存储了两个函数:加法和减法。每个函数都符合 MathFunc 函数类型的定义。

代码示例如下:

package main

import "fmt"

// 定义一个新的函数类型 MathFunc
type MathFunc func(int, int) int

func main() {
	// 声明一个 map,键为字符串,值为 MathFunc 函数类型
	var mathFuncs map[string]MathFunc

	// 初始化 mathFuncs 映射并存储加法和减法函数
	mathFuncs = map[string]MathFunc{
		"add": func(x, y int) int { return x + y },
		"sub": func(x, y int) int { return x - y },
	}

	// 使用字符串键调用加法函数
	addResult := mathFuncs["add"](3, 5)
	fmt.Println("Addition Result:", addResult) // 输出 8

	// 使用字符串键调用减法函数
	subResult := mathFuncs["sub"](10, 7)
	fmt.Println("Subtraction Result:", subResult) // 输出 3
}

在这个示例中,我们首先定义了一个 MathFunc 类型,然后声明了一个名为 mathFuncs 的映射,并初始化它以存储加法和减法函数。最后,我们通过字符串键调用了存储在 mathFuncs 中的函数,并输出了它们的结果。

Closure

在 Go 中,闭包(closure)是指一个函数值,它引用了自己定义之外的变量。换句话说,闭包是一个函数值,它可以捕获和存储其周围的变量的状态,并且可以在之后的调用中访问和修改这些变量。

闭包在 Go 中是一种强大的特性,它可以用于创建灵活且功能强大的函数。通过闭包,我们可以实现函数的延迟执行、函数工厂、函数的记忆和函数的封装等功能。

闭包的基本结构如下:

func closureFunc() func() int {
    var x int = 0
    return func() int {
        x++
        return x
    }
}

在这个示例中,closureFunc 函数返回了一个匿名函数,这个匿名函数内部引用了 closureFunc 函数中的变量 x。因此,每次调用返回的匿名函数时,都会对 x 进行修改并返回新的值。

下面是一个使用闭包的例子,演示了如何使用闭包实现一个计数器:

package main

import "fmt"

// 返回一个闭包函数,用于生成递增的序列号
func counter() func() int {
    var count int = 0
    return func() int {
        count++
        return count
    }
}

func main() {
    // 创建一个计数器
    c := counter()

    // 输出前五个序列号
    for i := 0; i < 5; i++ {
        fmt.Println("Counter:", c())
    }
}

在这个例子中,counter 函数返回了一个闭包函数,用于生成递增的序列号。在 main 函数中,我们创建了一个计数器,并使用循环调用了计数器函数,输出了前五个序列号。由于闭包引用了外部变量 count,因此每次调用闭包函数时,count 都会递增并返回新的值。

Output

Counter: 1
Counter: 2
Counter: 3
Counter: 4
Counter: 5

闭包函数内部的匿名函数捕获了外部的变量 count。在 Go 中,当一个函数返回时,其内部的变量并不会被销毁,而是会继续存在于内存中,直到该变量不再被引用。因此,每次调用闭包函数时,都会共享同一个 count 变量的实例。

因为闭包函数内部的匿名函数捕获了外部的 count 变量,所以在闭包函数中,count 变量不会重新初始化,而是会继续累加。每次调用闭包函数时,都会修改同一个 count 变量,并返回递增后的值。

因此,闭包函数的行为是与外部环境中的变量状态相关联的,而不是在每次调用时都重新初始化。

M2.2.2 - Variadic and Deferred

M2.2.2 -可变和延期

Variable Argument Number

在 Go 中,可变长度参数(Variadic Parameters)允许函数接受可变数量的参数。这意味着你可以定义函数,使其可以接受任意数量的参数。

可变长度参数的语法是在参数类型前面加上省略号 ...,表示这个参数是一个切片,可以接受任意数量的参数。在函数内部,可变长度参数被视为一个切片,可以使用切片相关的操作来访问和处理这些参数。

下面是一个简单的例子,演示了如何使用可变长度参数:

package main

import "fmt"

// 定义一个函数,接受任意数量的整数参数,并返回它们的和
func sum(nums ...int) int {
	total := 0
	for _, num := range nums {
		total += num
	}
	return total
}

func main() {
	// 调用 sum 函数,传入不同数量的参数
	fmt.Println(sum(1, 2))           // 输出 3
	fmt.Println(sum(1, 2, 3))        // 输出 6
	fmt.Println(sum(1, 2, 3, 4, 5))  // 输出 15
}

在这个例子中,sum 函数定义了一个可变长度参数 nums,它可以接受任意数量的整数参数。在函数内部,我们使用一个 for 循环遍历 nums 切片,并将每个参数相加得到总和,然后返回总和。

使用可变长度参数可以使函数更加灵活,可以接受不同数量的参数,而不需要每次都定义不同数量的形参。

Deferred Function Calls

在 Go 中,延迟函数调用(Deferred Function Calls)是一种机制,允许在函数执行结束时才执行某个函数,即使函数执行过程中出现了错误或者函数提前返回也不影响延迟函数的执行。

通过 defer 关键字可以延迟函数的执行。延迟的函数调用会被推迟到包含 defer 语句的函数即将返回时执行,无论函数是正常返回还是由于 panic 导致的异常返回。多个延迟函数调用将按照后进先出(LIFO)的顺序执行。

下面是一个示例代码,演示了如何使用延迟函数调用:

package main

import "fmt"

func main() {
    defer fmt.Println("World")

    fmt.Print("Hello ")
}

在这个例子中,World 的打印被延迟到 main 函数即将返回时执行。因此,程序的输出顺序是 Hello World

延迟函数调用的典型用途包括释放资源、解锁互斥量、关闭文件等操作,这样可以确保资源在函数执行结束时被正确处理,避免资源泄漏和错误的发生。

Deferred Call Arguments

在 Go 中,延迟函数调用(Deferred Function Calls)中的参数是在延迟函数调用语句被执行时就会被求值的表达式。这意味着参数的值在延迟函数被推迟执行之前就会被确定,并且在实际执行时使用该值。

下面是一个示例代码,演示了延迟函数调用中的参数:

package main

import "fmt"

func main() {
    // 定义一个函数,接受一个参数并打印它
    printValue := func(value int) {
        fmt.Println("Deferred Value:", value)
    }

    // 定义一个变量并赋值
    x := 10

    // 延迟调用 printValue 函数,并传入变量 x 作为参数
    defer printValue(x)

    // 修改变量 x 的值
    x = 20

    // 输出变量 x 的值
    fmt.Println("Original Value:", x)
}

在这个例子中,我们定义了一个匿名函数 printValue,它接受一个整数参数并打印它。然后,我们定义了一个变量 x 并赋值为 10。接着,我们通过 defer 关键字延迟调用了 printValue 函数,并将变量 x 作为参数传递给了该函数。在后续的代码中,我们修改了变量 x 的值为 20,然后打印了变量 x 的值。

由于延迟函数调用中的参数是在 defer 语句执行时就会被求值,因此在延迟函数被执行时,使用的是该参数在 defer 语句执行时的值。因此,即使在延迟函数被执行时,变量 x 的值已经被修改为 20,但延迟函数调用仍然打印的是 10,这是因为延迟函数调用中的参数值在 defer 语句执行时就已经确定了。

Module 2 Quiz

在这里插入图片描述

在这里插入图片描述

第四题做错了

在这里插入图片描述

第四题第二遍又做错了,我感觉第一个选项是对的

正确答案给的是:我觉得答案是错的

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

Peer-graded Assignment: Module 2 Activity

Let us assume the following formula for displacement s as a function of time t, acceleration a, initial velocity v**o, and initial displacement s**o.

s = 1 2 a t 2 + v 0 t + s 0 s = \frac{1}{2} a t^2+ v_0 t+ s_0 s=21at2+v0t+s0

Write a program which first prompts the user to enter values for acceleration, initial velocity, and initial displacement. Then the program should prompt the user to enter a value for time and the program should compute the displacement after the entered time.

You will need to define and use a function called GenDisplaceFn() which takes three float64 arguments, acceleration a, initial velocity vo, and initial displacement so. GenDisplaceFn() should return a function which computes displacement as a function of time, assuming the given values acceleration, initial velocity, and initial displacement. The function returned by GenDisplaceFn() should take one float64 argument t, representing time, and return one float64 argument which is the displacement travelled after time t.

For example, let’s say that I want to assume the following values for acceleration, initial velocity, and initial displacement: a = 10, v**o = 2, s**o = 1. I can use the following statement to call GenDisplaceFn() to generate a function fn which will compute displacement as a function of time.

fn := GenDisplaceFn(10, 2, 1)

Then I can use the following statement to print the displacement after 3 seconds.

fmt.Println(fn(3))

And I can use the following statement to print the displacement after 5 seconds.

fmt.Println(fn(5))

Submit your Go program source code.

package main

import (
	"fmt"
)

// GenDisplaceFn generates a function to compute displacement based on given parameters
func GenDisplaceFn(a, vo, so float64) func(float64) float64 {
	return func(t float64) float64 {
		return 0.5*a*t*t + vo*t + so
	}
}

func main() {
	// Prompt the user to enter values for acceleration, initial velocity, and initial displacement
	var a, vo, so float64
	fmt.Println("Enter acceleration:")
	fmt.Scanln(&a)
	fmt.Println("Enter initial velocity:")
	fmt.Scanln(&vo)
	fmt.Println("Enter initial displacement:")
	fmt.Scanln(&so)

	// Generate displacement function based on user input
	fn := GenDisplaceFn(a, vo, so)

	// Prompt the user to enter a value for time
	var t float64
	fmt.Println("Enter a value for time:")
	fmt.Scanln(&t)

	// Compute and print displacement after entered time
	fmt.Printf("Displacement after %.2f seconds: %.2f\n", t, fn(t))
}

MODULE 3: OBJECT ORIENTATION IN GO

Functions in Go have many useful and powerful capabilities, which you’ll explore in this module. At the end, you’ll develop a Go routine that takes full advantage of Go’s object orientation.

Learning Objectives


  • Identify the basic properties and uses of objects and classes.
  • Identify the differences between structs (in Go) and classes (in an object-oriented language).
  • Identify the uses of methods and different data types.
  • Develop an interactive Go routine that uses classes, objects, instances, structs, and methods to query the properties of existing instances.

M3.1.1 - Classes and Encapsulation

在 Go 中,没有类和对象的概念,而是通过结构体(struct)和方法(method)来实现面向对象的编程。

结构体(struct)是一种复合数据类型,它可以包含不同类型的数据字段。通过定义结构体,可以创建自定义的数据类型,用来表示复杂的数据结构。

方法(method)是一种与结构体关联的函数,它可以在特定类型的值上执行操作。方法可以在结构体上定义,用于操作该结构体的数据。在方法的定义中,接收者(receiver)指定了方法所作用的对象类型。

下面是一个示例代码,演示了如何在 Go 中使用结构体和方法来实现面向对象的编程:

package main

import "fmt"

// 定义一个结构体
type Circle struct {
    radius float64
}

// 定义 Circle 结构体的方法,用于计算圆的面积
func (c Circle) Area() float64 {
    return 3.14 * c.radius * c.radius
}

func main() {
    // 创建一个 Circle 结构体对象
    c := Circle{radius: 5}

    // 调用 Circle 结构体的方法来计算面积
    fmt.Println("Circle Area:", c.Area())
}

在这个例子中,我们定义了一个名为 Circle 的结构体,它有一个 radius 字段表示圆的半径。然后,我们为 Circle 结构体定义了一个方法 Area(),用于计算圆的面积。在 main 函数中,我们创建了一个 Circle 结构体对象 c,并调用了它的 Area() 方法来计算圆的面积并打印出来。

虽然 Go 中没有类和对象的概念,但是通过结构体和方法的组合,可以实现面向对象的编程范式。

在 Go 中,方法(method)是与特定类型关联的函数。方法的语法如下:

func (receiver ReceiverType) methodName(parameters) returnType {
    // 方法体
}

其中:

  • func 是定义函数的关键字。
  • (receiver ReceiverType) 是方法的接收者(receiver),它指定了方法与哪种类型相关联。接收者可以是任何类型,通常是结构体类型。如果方法没有使用接收者,则称该函数为普通函数而不是方法。
  • methodName 是方法的名称。
  • parameters 是方法的参数列表。
  • returnType 是方法的返回值类型,如果没有返回值,则可以省略。
  • 方法体是方法的具体实现,包括要执行的操作。

接收者(receiver)部分定义了方法与类型之间的关系。它可以是指针类型或非指针类型。如果方法需要修改接收者的值,则通常使用指针类型作为接收者。如果方法不需要修改接收者的值,则可以使用非指针类型。

下面是一个示例,演示了方法的语法:

package main

import "fmt"

// 定义一个结构体
type Rectangle struct {
    width, height float64
}

// 定义 Rectangle 结构体的方法,用于计算面积
func (r Rectangle) Area() float64 {
    return r.width * r.height
}

func main() {
    // 创建一个 Rectangle 结构体对象
    r := Rectangle{width: 5, height: 3}

    // 调用 Rectangle 结构体的方法来计算面积
    fmt.Println("Rectangle Area:", r.Area())
}

在这个例子中,我们定义了一个名为 Rectangle 的结构体,它有 widthheight 两个字段表示矩形的宽和高。然后,我们为 Rectangle 结构体定义了一个方法 Area(),用于计算矩形的面积。在 main 函数中,我们创建了一个 Rectangle 结构体对象 r,并调用了它的 Area() 方法来计算矩形的面积并打印出来。

在 Go 中,方法(method)和函数(function)是两个不同的概念,它们之间有以下区别:

  1. 关联类型

    • 方法是与特定类型关联的函数,它在类型上定义了一些行为。方法可以被定义在任何自定义类型上,包括结构体、接口、基本类型等。
    • 函数则是独立的,不与特定类型相关联。函数可以在任何地方被定义和调用,与类型无关。
  2. 语法

    • 方法在声明时需要指定一个接收者(receiver),它告诉编译器该方法与哪种类型相关联。接收者可以是值类型或指针类型。
    • 函数没有接收者,只是一组独立的执行代码。
  3. 调用方式

    • 方法通过类型的实例(或指针)来调用,可以使用.来访问方法,以调用与该类型相关联的方法。
    • 函数可以直接通过函数名来调用,不需要通过特定的类型。
  4. 修改接收者

    • 方法可以修改接收者所属类型的值,包括其字段的值。这通常通过使用指针类型的接收者来实现。
    • 函数不能直接修改类型的字段,因为它们没有直接关联的类型。

下面是一个简单的示例,演示了方法和函数之间的区别:

package main

import "fmt"

// 定义一个结构体
type Rectangle struct {
    width, height float64
}

// 定义 Rectangle 结构体的方法,用于计算面积
func (r Rectangle) Area() float64 {
    return r.width * r.height
}

// 定义一个函数,用于计算两个数的和
func Add(a, b int) int {
    return a + b
}

func main() {
    // 创建一个 Rectangle 结构体对象
    r := Rectangle{width: 5, height: 3}

    // 调用 Rectangle 结构体的方法来计算面积
    fmt.Println("Rectangle Area:", r.Area())

    // 调用函数来计算两个数的和
    sum := Add(3, 5)
    fmt.Println("Sum:", sum)
}

在这个例子中,Area()Rectangle 结构体的方法,它与特定类型 Rectangle 关联。而 Add() 是一个普通函数,不与特定类型关联。

M3.1.2 - Support for Classes (1)

在这里插入图片描述

Implicit Method Argument

在 Go 中,方法(method)的第一个参数称为接收者(receiver),它是一个隐式的方法参数。接收者指定了方法与特定类型的关联,使得该方法可以在该类型的实例上被调用。

隐式方法参数的存在使得在方法调用时可以更加简洁和直观。在调用方法时,不需要显式地传递接收者作为方法参数,而是直接通过.来调用方法,并将方法所属类型的实例作为接收者。

M3.1.3 - Support for Classes (2)

Structs with Methods

M3.2.1 - Encapsulation

Controlling Access

Controlling Access to Structs

在 Go 中,可以通过大小写来控制对结构体字段和方法的访问权限。具体来说,以大写字母开头的字段或方法可以被外部包访问,而以小写字母开头的字段或方法只能在定义它们的包内部访问。

这种访问控制方式有助于封装数据和行为,使得包的设计更加模块化和安全。通过隐藏内部实现细节,可以减少外部包的依赖和耦合,提高代码的可维护性和可重用性。

下面是一个示例,演示了如何在 Go 中控制对结构体字段和方法的访问权限:

package main

import (
    "fmt"
    "strings"
)

// 定义一个结构体
type Person struct {
    firstName string // 小写字母开头的字段只能在定义它们的包内部访问
    lastName  string // 同样,这里的字段只能在包内部访问
}

// 创建一个新的 Person 对象
func NewPerson(firstName, lastName string) Person {
    return Person{
        firstName: firstName,
        lastName:  lastName,
    }
}

// 定义一个公开的方法来获取 Person 的全名
func (p Person) FullName() string {
    return strings.Join([]string{p.firstName, p.lastName}, " ")
}

func main() {
    // 创建一个 Person 对象
    p := NewPerson("John", "Doe")

    // 调用公开的方法来获取 Person 的全名
    fullName := p.FullName()

    // 打印全名
    fmt.Println("Full Name:", fullName)

    // 尝试直接访问结构体字段(这将导致编译错误)
    // fmt.Println("First Name:", p.firstName)
    // fmt.Println("Last Name:", p.lastName)
}

在这个示例中,Person 结构体中的 firstNamelastName 字段都是以小写字母开头的,因此只能在定义它们的包内部访问。而 FullName() 方法是一个公开的方法,可以被外部包访问。在 main 函数中,我们创建了一个 Person 对象 p,并通过 FullName() 方法获取了其全名,而尝试直接访问 firstNamelastName 字段将导致编译错误。

M3.2.2 - Point Receivers

Limitations of Methods

Large Receivers

在 Go 中,方法可以定义在任何自定义类型上,包括结构体、基本类型、接口等。方法的定义需要一个接收者(receiver),它决定了方法与特定类型的关联。接收者可以是值类型或指针类型。当方法的接收者是指针类型时,它被称为指针接收者(pointer receiver),或者称为指针方法(pointer method)。

指针接收者允许方法修改调用者的值,而不仅仅是对副本进行操作。因为方法的接收者是指针,所以方法在执行时会直接操作原始对象,而不是对象的副本。

下面是一个示例,演示了指针接收者的用法:

package main

import "fmt"

// 定义一个结构体
type Point struct {
    x, y int
}

// 定义一个指针接收者方法
func (p *Point) Move(dx, dy int) {
    p.x += dx
    p.y += dy
}

func main() {
    // 创建一个 Point 结构体对象的指针
    p := &Point{x: 1, y: 2}

    // 调用指针接收者方法
    p.Move(2, 3)

    // 打印移动后的坐标
    fmt.Println("New Coordinates:", p)
}

在这个例子中,Move() 方法有一个指针接收者 *Point,表示它可以修改 Point 结构体对象的值。在 main() 函数中,我们创建了一个 Point 结构体对象的指针 p,然后调用了 Move() 方法来移动点的坐标。由于 Move() 方法的接收者是指针类型,因此方法会直接修改原始对象的值,而不是副本。

M3.2.3 - Point Receivers, Referencing, Dereferencing

在这里插入图片描述

在 Go 中,方法可以使用值类型或指针类型作为接收者(receiver)。当使用指针类型作为接收者时,称之为指针接收者(pointer receiver)。指针接收者允许方法直接修改接收者所指向的对象的值,而不是对接收者的副本进行操作。

当方法的接收者是指针类型时,在方法内部可以直接访问接收者指向的对象,并且可以对其进行修改。在方法调用时,如果接收者是值类型,则会隐式地使用取址操作(referencing)将其转换为指针类型,从而将接收者的地址传递给方法。

另一方面,如果需要在方法内部访问接收者指向的对象的值,可以使用解引用操作(dereferencing)。解引用操作使用 * 符号将指针转换为其指向的值。这样就可以通过指针来访问对象的字段或调用对象的方法。

下面是一个示例,演示了指针接收者、引用和解引用的用法:

package main

import "fmt"

// 定义一个结构体
type Point struct {
    x, y int
}

// 定义一个指针接收者方法,用于修改接收者指向的对象的值
func (p *Point) Move(dx, dy int) {
    p.x += dx
    p.y += dy
}

// 定义一个值类型接收者方法,用于访问接收者指向的对象的值
func (p Point) Distance() float64 {
    return float64(p.x*p.x + p.y*p.y)
}

func main() {
    // 创建一个 Point 结构体对象的指针
    p := &Point{x: 3, y: 4}

    // 调用指针接收者方法,修改接收者指向的对象的值
    p.Move(2, 3)

    // 调用值类型接收者方法,访问接收者指向的对象的值
    distance := p.Distance()

    // 打印移动后的坐标和距离
    fmt.Println("New Coordinates:", *p) // 解引用操作,访问指针指向的对象的值
    fmt.Println("Distance from Origin:", distance)
}

在这个例子中,Move() 方法和 Distance() 方法分别是指针接收者方法和值类型接收者方法。在 main() 函数中,我们创建了一个 Point 结构体对象的指针 p,然后通过指针调用了 Move() 方法来移动点的坐标,并通过值类型调用了 Distance() 方法来计算点到原点的距离。在打印移动后的坐标时,使用了解引用操作来访问指针指向的对象的值。

Module 3 Quiz

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

Peer-graded Assignment: Module 3 Activity

Write a program which allows the user to get information about a predefined set of animals. Three animals are predefined, cow, bird, and snake. Each animal can eat, move, and speak. The user can issue a request to find out one of three things about an animal: 1) the food that it eats, 2) its method of locomotion, and 3) the sound it makes when it speaks. The following table contains the three animals and their associated data which should be hard-coded into your program.

AnimalFood eatenLocomotion methodSpoken sound
cowgrasswalkmoo
birdwormsflypeep
snakemiceslitherhsss

Your program should present the user with a prompt, “>”, to indicate that the user can type a request. Your program accepts one request at a time from the user, prints out the answer to the request, and prints out a new prompt. Your program should continue in this loop forever. Every request from the user must be a single line containing 2 strings. The first string is the name of an animal, either “cow”, “bird”, or “snake”. The second string is the name of the information requested about the animal, either “eat”, “move”, or “speak”. Your program should process each request by printing out the requested data.

You will need a data structure to hold the information about each animal. Make a type called Animal which is a struct containing three fields:food, locomotion, and noise, all of which are strings. Make three methods called Eat(), Move(), and Speak(). The receiver type of all of your methods should be your Animal type. The Eat() method should print the animal’s food, the Move() method should print the animal’s locomotion, and the Speak() method should print the animal’s spoken sound. Your program should call the appropriate method when the user makes a request.

Submit your Go program source code.

package main

import (
	"fmt"
)

// Animal struct definition
type Animal struct {
	food       string // Field for the food the animal eats
	locomotion string // Field for the locomotion method of the animal
	noise      string // Field for the sound the animal makes
}

// Eat method prints the animal's food
func (a Animal) Eat() {
	fmt.Println(a.food)
}

// Move method prints the animal's locomotion method
func (a Animal) Move() {
	fmt.Println(a.locomotion)
}

// Speak method prints the animal's spoken sound
func (a Animal) Speak() {
	fmt.Println(a.noise)
}

func main() {
	// Create a map to hold information about animals
	animals := map[string]Animal{
		"cow":   Animal{"grass", "walk", "moo"},
		"bird":  Animal{"worms", "fly", "peep"},
		"snake": Animal{"mice", "slither", "hsss"},
	}

	// Infinite loop to process user requests
	for {
		var animalName, actionName string

		// Prompt the user for input
		fmt.Print(">")
		// Read user input
		fmt.Scan(&animalName, &actionName)

		// Look up the animal based on its name
		animal, found := animals[animalName]
		if !found {
			fmt.Println("Animal not found")
			continue
		}

		// Call the appropriate method based on the action requested
		switch actionName {
		case "eat":
			animal.Eat()
		case "move":
			animal.Move()
		case "speak":
			animal.Speak()
		default:
			fmt.Println("Invalid action")
		}
	}
}

MODULE 4: INTERFACES FOR ABSTRACTION

This last module rounds out the course by introducing several new concepts including interfaces and polymorphism. You’ll finish the module – and course – by creating a Go routine that creates instances of a class and allows you to query its properties.

Learning Objectives


  • Identify the basic characteristics and features of interfaces in Go.
  • Identify the characteristics of polymorphism.
  • Identify the features associated with overriding and inheritance.
  • Develop an interactive Go routine that uses classes, objects, instances, structs, and methods to create new class instances and later query their properties.

M4.1.1 - Polymorphism

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

Go 语言是一种静态类型语言,不像一些其他语言(如 Java、C#)那样直接支持传统意义上的多态性。传统上,多态性指的是在运行时根据对象的实际类型来调用相应的方法。然而,Go 语言中的接口(interfaces)机制可以实现类似多态的效果。

在 Go 中,多态性通过接口实现。接口是一种抽象类型,它定义了对象的行为。任何类型只要实现了接口中定义的方法,就被视为该接口的实现。这使得可以在不关心对象具体类型的情况下,通过接口来调用对象的方法。

以下是一个示例,演示了 Go 中接口的多态性:

package main

import "fmt"

// Animal 接口定义了动物的行为
type Animal interface {
	Speak() string
}

// Dog 结构体实现了 Animal 接口
type Dog struct{}

// Speak 方法返回狗的叫声
func (d Dog) Speak() string {
	return "Woof!"
}

// Cat 结构体实现了 Animal 接口
type Cat struct{}

// Speak 方法返回猫的叫声
func (c Cat) Speak() string {
	return "Meow!"
}

// PrintAnimalSound 函数接受 Animal 接口类型的参数,并调用其 Speak 方法
func PrintAnimalSound(a Animal) {
	fmt.Println(a.Speak())
}

func main() {
	dog := Dog{}
	cat := Cat{}

	PrintAnimalSound(dog) // 输出:Woof!
	PrintAnimalSound(cat) // 输出:Meow!
}

在这个例子中,定义了一个 Animal 接口,其中包含一个 Speak 方法。然后,定义了两个结构体 Dog 和 Cat,并分别实现了 Animal 接口中的 Speak 方法。最后,定义了一个函数 PrintAnimalSound,该函数接受一个 Animal 接口类型的参数,并调用其 Speak 方法。在 main 函数中,分别创建了 Dog 和 Cat 的实例,并将它们作为参数传递给 PrintAnimalSound 函数。由于 Dog 和 Cat 都实现了 Animal 接口,因此它们可以被传递给接口类型的参数,这展示了多态性的概念。

M4.1.2 - Interfaces

在Go语言中,接口(interface)是一种抽象类型,它定义了对象的行为,但不包含对象的数据。接口中定义了一组方法的签名,任何类型只要实现了接口中定义的所有方法,就被视为该接口的实现。这种设计使得在不同类型之间实现多态性变得非常简洁。

接口的语法如下:

type InterfaceName interface {
    Method1() ReturnType1
    Method2() ReturnType2
    // 更多方法...
}

其中,InterfaceName 是接口的名称,Method1、Method2 等是接口中定义的方法,ReturnType1、ReturnType2 等是方法的返回类型。

接口的优点包括:

  1. 解耦合:接口将类型的实现细节与使用接口的代码分离开来,使得代码更容易理解和维护。

  2. 多态性:通过接口,可以在不关心具体类型的情况下,以统一的方式对不同类型的对象进行操作。

  3. 代码复用:接口可以被多个不相关的类型实现,从而使得代码具有更高的复用性。

下面是一个简单的示例,演示了如何定义和使用接口:

package main

import "fmt"

// 定义一个接口
type Shape interface {
    Area() float64
}

// 定义矩形类型
type Rectangle struct {
    Width  float64
    Height float64
}

// 矩形类型实现接口的方法
func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

// 定义圆形类型
type Circle struct {
    Radius float64
}

// 圆形类型实现接口的方法
func (c Circle) Area() float64 {
    return 3.14 * c.Radius * c.Radius
}

func main() {
    // 创建一个矩形实例
    rectangle := Rectangle{Width: 5, Height: 3}
    // 创建一个圆形实例
    circle := Circle{Radius: 2}

    // 使用接口来计算不同形状的面积
    shapes := []Shape{rectangle, circle}
    for _, shape := range shapes {
        fmt.Println("Area:", shape.Area())
    }
}

在这个例子中,定义了一个 Shape 接口,其中包含一个 Area 方法。然后,定义了 Rectangle 和 Circle 两种类型,并分别实现了 Shape 接口中的 Area 方法。在 main 函数中,创建了一个包含不同形状的切片,并通过循环遍历计算每种形状的面积。通过接口的方式,可以在统一的代码框架下处理不同类型的对象,展现了接口的多态性。

问题:解释这个语法: shapes := []Shape{rectangle, circle}

回答:这段代码创建了一个切片(slice),其中包含了不同类型的对象,并且这些对象都实现了 Shape 接口。让我们来解释一下这个语法:

  1. []Shape:这表示创建一个空的接口类型切片。[] 表示切片类型,Shape 表示切片中的元素类型是 Shape 接口。

  2. {rectangle, circle}:这表示在切片中初始化两个元素,分别是 rectangle 和 circle。由于 rectangle 和 circle 都实现了 Shape 接口中的方法,因此它们可以被添加到 Shape 类型的切片中。

这种初始化方式称为切片字面量(slice literal),它允许我们直接在代码中指定切片的初始值。在这个例子中,我们将 Rectangle 和 Circle 实例添加到了一个包含 Shape 接口类型的切片中,这使得我们可以通过循环遍历这个切片,统一地处理不同类型的对象。

M4.1.3 - Interface vs. Concrete Types

Concrete vs Interface Types

Concrete Types

· Specify the exact representation of the data and methods

· Complete method implenetation is included

Interface Types

· Specifies some method signatures

· Implementations are abstracted

在这里插入图片描述

在这里插入图片描述

Interface with Nil Dynamic Value

在Go语言中,接口由两个组件组成:动态值和动态类型。动态值是存储在接口中的具体值,而动态类型是该值的类型。

当接口值为nil时,它的动态值和动态类型都是nil。这意味着如果你有一个接口变量设置为nil,它不持有任何具体类型的具体值。但是,它仍然有一个类型,即接口类型本身。

让我们通过一个例子来说明:

package main

import "fmt"

type MyInterface interface {
    Method()
}

type MyStruct struct{}

func (s *MyStruct) Method() {
    fmt.Println("Method called")
}

func main() {
    var i MyInterface
    var s *MyStruct

    i = s // 将nil指针分配给接口
    fmt.Println(i) // 输出:<nil>

    // 对接口变量调用Method()将导致运行时恐慌,因为动态值为nil
    // 并且没有具体值来调用方法。
    // i.Method() // 这会引发恐慌
}

在上面的例子中,i是一个类型为MyInterface的接口变量,s是一个指向MyStructnil指针。当我们将s赋给i时,i变成了一个nil接口,因为它不持有任何具体值。因此,在i上进行任何方法调用都将导致运行时恐慌,因为没有具体值来调用方法。

在代码中,要优雅地处理nil接口值,以避免运行时恐慌。你可以在进行任何方法调用之前检查接口值是否为nil

Nil Dynamic Value

Nil Interface Value

M4.2.1 - Using Interfaces

使用接口这部分同M4.1.2 - Interfaces章节一样。

在 Go 语言中,空接口(Empty Interface)是一种特殊的接口类型,它不包含任何方法声明。因此,空接口可以表示任何类型的值。空接口的类型声明形式为 interface{},其中 {} 表示空的方法集合。由于空接口不包含任何方法,因此它可以接收任何类型的值。

空接口的特点如下:

  1. 接收任意类型

    • 空接口可以用来接收任意类型的值,因为它不限制具体的类型。
  2. 灵活性

    • 空接口提供了很高的灵活性,可以用于需要处理多种类型的场景,比如在函数参数或者返回值中使用。
  3. 泛型编程

    • 空接口在 Go 中实现了一种类似泛型编程的机制,使得代码可以更加通用和灵活。

下面是一个简单的示例,演示了空接口的用法:

package main

import "fmt"

// 定义一个接收空接口参数的函数
func printValue(v interface{}) {
    fmt.Printf("Value: %v, Type: %T\n", v, v)
}

func main() {
    // 使用空接口接收不同类型的值
    printValue(42)         // 整数
    printValue("hello")    // 字符串
    printValue(3.14)       // 浮点数
    printValue(true)       // 布尔值
    printValue([]int{1, 2, 3}) // 切片
}

在上面的示例中,printValue 函数接收一个空接口参数 v,然后使用 %v%T 格式化字符串打印参数的值和类型。通过调用 printValue 函数并传递不同类型的值作为参数,我们可以看到空接口可以接收任意类型的值。

M4.2.2 - Type Assertions

Type Assertions for Disambiguation

M4.2.3 - Error Handling

在Go语言中,Error接口是一个内置的接口类型,用于表示错误的通用接口。它定义了一个单一的方法:

type error interface {
    Error() string
}

Error方法返回一个描述错误的字符串。任何实现了Error方法的类型都可以被视为一个错误,并且可以用于处理和报告错误信息。

通常情况下,当函数或方法遇到错误时,会返回一个实现了Error接口的值来表示错误的信息。这样做的好处是,可以很容易地进行错误处理和错误传播。

下面是一个简单的示例,演示了如何使用Error接口:

package main

import (
    "fmt"
    "errors"
)

// 自定义一个错误类型
type MyError struct {
    Msg string
}

// 实现Error方法
func (e MyError) Error() string {
    return e.Msg
}

// 模拟一个可能会返回错误的函数
func doSomething() error {
    return MyError{"Something went wrong"}
}

func main() {
    // 调用可能会返回错误的函数
    if err := doSomething(); err != nil {
        // 判断错误类型并打印错误信息
        if e, ok := err.(MyError); ok {
            fmt.Println("MyError:", e.Msg)
        } else {
            fmt.Println("Unknown error:", err.Error())
        }
    } else {
        fmt.Println("No error occurred")
    }
}

在上面的示例中,我们定义了一个自定义的错误类型MyError,并实现了Error方法。然后,在doSomething函数中模拟了一个可能会返回错误的情况。在主函数中,我们调用doSomething函数,并检查其返回的错误。如果返回的错误是自定义的MyError类型,则将错误信息打印出来,否则将打印未知错误信息。

通过Error接口,我们可以实现自定义的错误类型,并对错误进行更加灵活和具体的处理。

补充if 语句的知识:

在Go语言中,if语句用于控制程序的流程,根据条件执行不同的代码块。if语句的一般形式如下:

if condition {
    // 执行条件为真时的代码块
} else {
    // 执行条件为假时的代码块(可选)
}

在这个基本形式中,condition 是一个布尔表达式,表示要进行判断的条件。如果 condition 为真,则执行 if 代码块中的语句;如果 condition 为假,则执行 else 代码块中的语句(如果有的话)。

在Go语言中,if语句还支持在条件判断之前执行一条简单的语句,并根据这个语句的执行结果来判断条件。这种形式的if语句通常用于执行某个函数,并根据函数的返回值来判断条件。这种形式的if语句的语法结构如下:

if statement; condition {
    // 执行 statement 后再根据 condition 判断
}

在这种形式中,statement 是一个简单的语句,通常是一个赋值语句或函数调用语句。如果 statement 执行成功并且 condition 为真,则执行 if 代码块中的语句;否则,不执行。

例如,你提到的这个语句:

if err := doSomething(); err != nil {
    // 执行条件为真时的代码块
}

这个语句首先执行 doSomething() 函数,并将返回值赋给变量 err。然后,它判断 err 是否为 nil。如果 err 不为 nil,则执行 if 代码块中的语句;否则,不执行。这种形式的if语句常用于处理函数返回的错误信息。

Module 4 Quiz

在这里插入图片描述

在这里插入图片描述

第五题的答案是:3个都对。

在这里插入图片描述

Peer-graded Assignment: Module 4 Activity

Write a program which allows the user to create a set of animals and to get information about those animals. Each animal has a name and can be either a cow, bird, or snake. With each command, the user can either create a new animal of one of the three types, or the user can request information about an animal that he/she has already created. Each animal has a unique name, defined by the user. Note that the user can define animals of a chosen type, but the types of animals are restricted to either cow, bird, or snake. The following table contains the three types of animals and their associated data.

AnimalFood eatenLocomtion methodSpoken sound
cowgrasswalkmoo
birdwormsflypeep
snakemiceslitherhsss

Your program should present the user with a prompt, “>”, to indicate that the user can type a request. Your program should accept one command at a time from the user, print out a response, and print out a new prompt on a new line. Your program should continue in this loop forever. Every command from the user must be either a “newanimal” command or a “query” command.

Each “newanimal” command must be a single line containing three strings. The first string is “newanimal”. The second string is an arbitrary string which will be the name of the new animal. The third string is the type of the new animal, either “cow”, “bird”, or “snake”. Your program should process each newanimal command by creating the new animal and printing “Created it!” on the screen.

Each “query” command must be a single line containing 3 strings. The first string is “query”. The second string is the name of the animal. The third string is the name of the information requested about the animal, either “eat”, “move”, or “speak”. Your program should process each query command by printing out the requested data.

Define an interface type called Animal which describes the methods of an animal. Specifically, the Animal interface should contain the methods Eat(), Move(), and Speak(), which take no arguments and return no values. The Eat() method should print the animal’s food, the Move() method should print the animal’s locomotion, and the Speak() method should print the animal’s spoken sound. Define three types Cow, Bird, and Snake. For each of these three types, define methods Eat(), Move(), and Speak() so that the types Cow, Bird, and Snake all satisfy the Animal interface. When the user creates an animal, create an object of the appropriate type. Your program should call the appropriate method when the user issues a query command.

Submit your Go program source code.

package main

import (
	"fmt"
)

// Animal interface defines methods Eat(), Move(), and Speak()
type Animal interface {
	Eat()
	Move()
	Speak()
}

// Cow struct
type Cow struct {
	name string
}

// Eat method for Cow
func (c Cow) Eat() {
	fmt.Println("grass")
}

// Move method for Cow
func (c Cow) Move() {
	fmt.Println("walk")
}

// Speak method for Cow
func (c Cow) Speak() {
	fmt.Println("moo")
}

// Bird struct
type Bird struct {
	name string
}

// Eat method for Bird
func (b Bird) Eat() {
	fmt.Println("worms")
}

// Move method for Bird
func (b Bird) Move() {
	fmt.Println("fly")
}

// Speak method for Bird
func (b Bird) Speak() {
	fmt.Println("peep")
}

// Snake struct
type Snake struct {
	name string
}

// Eat method for Snake
func (s Snake) Eat() {
	fmt.Println("mice")
}

// Move method for Snake
func (s Snake) Move() {
	fmt.Println("slither")
}

// Speak method for Snake
func (s Snake) Speak() {
	fmt.Println("hsss")
}

func main() {
	// Animal map to store animals created by the user
	animals := make(map[string]Animal)

	// Prompt the user for input
	fmt.Println("Welcome to Animal program. Type 'newanimal' to create a new animal or 'query' to get information about an existing animal.")

	// Loop forever to accept user commands
	for {
		// Prompt for user input
		var command, name, action string
		fmt.Print("> ")

		// Read user input
		fmt.Scan(&command, &name, &action)

		// Process user command
		switch command {
		case "newanimal":
			createAnimal(&animals, name, action)
		case "query":
			queryAnimal(&animals, name, action)
		default:
			fmt.Println("Invalid command. Please try again.")
		}
	}
}

// Function to create a new animal
func createAnimal(animals *map[string]Animal, name, animalType string) {
	switch animalType {
	case "cow":
		(*animals)[name] = Cow{name}
	case "bird":
		(*animals)[name] = Bird{name}
	case "snake":
		(*animals)[name] = Snake{name}
	default:
		fmt.Println("Invalid animal type. Please choose either 'cow', 'bird', or 'snake'.")
	}
	fmt.Println("Created it!")
}

// Function to query information about an animal
func queryAnimal(animals *map[string]Animal, name, action string) {
	animal, ok := (*animals)[name]
	if !ok {
		fmt.Println("Animal with name", name, "not found.")
		return
	}

	switch action {
	case "eat":
		animal.Eat()
	case "move":
		animal.Move()
	case "speak":
		animal.Speak()
	default:
		fmt.Println("Invalid action. Please choose either 'eat', 'move', or 'speak'.")
	}
}

后记

2024年3月17日完成《Functions, Methods, and Interfaces in Go》这门课程的学习,对Golang的function,method,interface有了一定的认识。准备去学习第三门课:Go的并发。

  • 8
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值