通过示例学习-Go-语言-2023-二十四-

龙哥盟 / 2024-10-23 / 原文

通过示例学习 Go 语言 2023(二十四)

OOP:Go 中的多态完全指南

来源:golangbyexample.com/oop-polymorphism-in-go-complete-guide/

在理解多态之前,首先了解什么是多态。

“同名多形态”

在编程上下文中,我们还会遇到许多行为在不同环境中相似的用例。将此类行为用相同的名称表示更为合适。这就是多态在编程中的意义。从编程的角度来看,有两种可能的多态性——

  • 编译时多态——在这种多态中,编译器能够知道特定调用将执行哪些具体函数。编译时多态的示例包括:

    • 函数重载——同一函数名具有不同的参数。

    • 运算符重载

  • 运行时多态——在这种多态中,调用的函数在运行时决定。

让我们看看在 GO 中可能实现的编译时和运行时多态的类型。

  • Go 中的编译时多态

  • Go 中的运行时多态

结论:

如果你阅读了以上两篇文章,你会发现 GoLang 中不支持编译时多态。它只具有运行时多态。运行时多态在 GO 中是通过接口实现的。

Go 程序的执行顺序

来源:golangbyexample.com/order-execution-program-golang/

目录

  • 概述

  • 示例

概述

下面是 Go 程序的执行顺序。

  • 程序从主包开始。

  • 主包源文件中所有导入的包都被初始化。同样的事情也会递归发生在进一步导入的包中。

  • 然后这些包中的全局变量声明被初始化。初始化依赖关系将启动这些变量的初始化。golang.org/ref/spec#Order_of_evaluation

  • 之后,这些包中的 init()函数将被执行。

  • 主包中的全局变量被初始化。

  • 如果存在,主包中的 init 函数将被运行。

  • 主包中的 main 函数被执行。

请注意,即使包被多次导入,包的初始化也只会执行一次。

例如,如果主包导入包a,而包a又导入包b,那么以下是执行顺序。

  • 包 b 中的全局变量将被初始化。包 b 源文件中的 init 函数将被执行。

  • a中的全局变量将被初始化。包a源文件中的 init 函数将被执行。

  • 主包中的全局变量将被初始化。主包源文件中的 init 函数将被执行。

  • main 函数将开始执行。

示例

让我们看看一个相同的程序。

go.mod

module sample

go 1.14

sample/b/b1.go

package b

import (
	"fmt"
)

func init() {
	fmt.Println("Init: b1")
}

func TestB() error {
	return nil
}

sample/b/b2.go

package b

import (
	"fmt"
)

func init() {
	fmt.Println("Init: b2")
}

sample/a/a1.go

package a

import (
	"fmt"
	"sample/b"
)

func init() {
	fmt.Println("Init: a1")
}

func TestA() error {
	return b.TestB()
}

sample/a/a2.go

package a

import (
	"fmt"
)

func init() {
	fmt.Println("Init: a2")
}

sample/main.go

package main

import (
	"fmt"
	"sample/a"
)

func init() {
	fmt.Println("Init: main")
}
func main() {
	fmt.Println("Main Function Executing")
	a.TestA()
}

输出

Init: b1
Init: b2
Init: a1
Init: a1
Init: main
Main Function Executing

注意在上述示例中,包b的源文件中的 init 函数首先被执行。然后包a的源文件中的 init 函数被执行,最后主包的源文件中的 init 函数被执行。之后 main 函数被执行。

Go (Golang) 中的包名称和目录/文件夹名称 - 它们需要相同吗

来源:golangbyexample.com/package-folder-name-golang/

目录

  • 概述

  • 示例

概述

包名称和包含该包的目录名称不一定必须相同。那么包的导入路径指定了什么呢?例如,如果我们有任何包,则导入路径可能如下所示:

import "xyz.com/abc/sample"

上述陈述的本质是导入位于目录“sample”中的包。这并不意味着导入包sample。为了验证这一点,让我们看一个示例。

示例

首先让我们创建一个名为learn的目录,然后创建一个导入路径为“sample.com/learn”的模块。

go mod create sample.com/learn

让我们在learn目录中创建一个名为math的目录。在math目录中创建一个文件math.go,内容如下:

learn/math/math.go

package mathematics

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

注意上面的包声明

package mathematics

包名称是数学,但包含该包的目录名称是math

现在让我们在main.go文件中使用数学包。

learn/main.go

package main

import (
    "fmt"
    "sample.com/learn/math"
)

func main() {
    fmt.Println(mathematics.Add(2, 1))
}

在上面的main.go中,看看我们是如何导入包的。

"sample.com/learn/math"

上述陈述的本质是导入位于目录“math”中的包。这并不意味着导入包math

请查看在main函数中我们是如何使用该包的。

fmt.Println(mathematics.Add(2, 1))

这就是我们所说的导入意味着导入位于该目录位置的包。

如果你运行这个程序,输出将是正确的。

3

这表明包的名称不一定要与包含该包的目录名称相同。另一种方便的方法是在这种情况下使用包别名,如下所示。

import (
    "fmt"
    mathematics "sample.com/learn/math"
)

Go 中的包与模块(Golang)

来源:golangbyexample.com/package-vs-module-golang/

目录

  • 概述

  • 模块时代之前

    • Go 版本 1.11 之前

    • 在 Go 版本 1.11 中

    • 在 Go 版本 1.13 之后

  • 创建模块

概述

根据模块的定义,它是一个包含嵌套和相关的 Go 包集合的目录,根目录下有 go.mod 文件。go.mod 文件定义了

  • 模块导入路径。

  • 模块的依赖要求,以确保成功构建。它定义了项目的依赖要求,并将其锁定到正确的版本。

模块提供了

  • 依赖管理

  • 使用模块后,Go 项目不必再位于 $GOPATH/src 文件夹中。

此外,除了 go.mod 文件,Go 还会保留一个 go.sum 文件,其中包含所有项目依赖模块的加密哈希。这是为了确保项目的依赖模块没有发生变化。

模块内包的行为与之前相同。因此,之前适用于包的规则现在也适用。这一点没有改变。然而,当需要单独版本化时,可以将一组包称为模块。此外,当它是通用代码并且你想在多个项目之间共享这段代码时,也是如此。

模块时代之前

让我们按版本查看变化,以充分理解早期的限制以及自模块以来的变化。

  • Go 版本 1.11 之前 – 模块根本不存在。

  • Go 版本 1.11 – 引入了模块,但尚未最终确定。

  • Go 版本 1.13 – 引入了模块。

Go 版本 1.11 之前

在模块出现之前,Go 只有包。$GOPATH 位置将包含三个目录。

  • src

  • pkg

  • bin

这些是在模块时代之前存在的问题。

  • 所有 Go 项目位于 $GOPATH/src 目录中。

  • 没有原生的依赖管理支持。

  • 所有依赖项将在 $GOPATH/src 目录中下载,而没有版本控制。

让我们逐个看待每个问题。

  • 任何 Go 项目都必须在 $GOPATH/src 目录内。

这是一个重大限制,因为它限制了你可以存放项目的位置。

  • 没有原生的依赖管理支持。

在模块出现之前的一个问题是没有办法在项目中指定依赖。虽然有像 dep、glide 这样的替代解决方案,但缺乏原生解决方案。

  • 所有依赖项将在 $GOPATH/src 目录中下载,而没有版本控制。

当我们执行 go get 时,它将在 $GOPATH/src 目录中下载所需的包。请运行下面的 go get 命令。

go get github.com/pborman/uuid

它将下载位于该位置的包。

$GOPATH/src/github.com/pborman/uuid

请注意上面的 go get 命令没有指定版本。因此,它下载最新版本。此外,请注意下载的包。即使它没有列出任何版本信息。这是个问题。如果github.com/pborman/uuid包有更新,而你想获取该更新,由于没有版本控制,更新的包将被下载到同一位置,替换掉旧的包。

在 Go 版本 1.11 中

在 Go 1.11 中,模块被引入但尚未定型。因此,如果你仍在使用它,最好切换到最新版本。

在 Go 版本 1.13 之后

我们已经讨论了在预模块时代存在的所有问题。现在让我们看看这些问题是如何通过引入模块得到解决的。

第一个问题是。

  • 所有 Go 项目在$GOPATH/src 目录下。

有了模块,这不再是一个要求。

  • 没有本地依赖管理支持。

模块在 Go 中引入了本地依赖管理。通过模块,它提供了两个新的文件。

  1. go.mod

  2. go.sum

通过go.modgo.sum文件,我们能够安装特定版本的依赖项而不会破坏任何内容。我们在本教程开始时已经简要介绍了这些文件。稍后在教程中,我们将详细讨论。

  • 所有依赖项将被下载到$GOPATH/pkg/mod目录,并带有版本信息。

因此,如果你下载同一库的不同版本,则这两个版本将下载到\(GOPATH/pkg/mod**中的不同目录,而不会相互覆盖。**\)GOPATH/pkg/mod中将包含两个内容。

  1. 缓存——这是所有依赖项将下载到的文件夹,以及压缩代码。

  2. 所有下载的依赖项的压缩代码将从缓存目录复制过来。

现在让我们创建一个模块。我们讨论的内容将在那时更加清晰。

创建模块

可以使用以下命令创建一个模块。

go mod init {module_import_path}

让我们创建一个模块。

go mod init learn

此命令将在同一目录中创建一个go.mod文件。现在什么是go.mod文件。

让我们检查一下这个文件的内容。执行cat go.mod

module learn

go 1.14

它包含模块的导入路径和创建该模块时使用的 Go 版本。

由于这是一个空模块,因此尚未指定任何直接依赖项。让我们在同一目录下创建一个名为uuid.go的文件,内容如下。

uuid.go

package main

import (
	"fmt"
	"strings"

	"github.com/pborman/uuid"
)

func main() {
	uuidWithHyphen := uuid.NewRandom()
	uuid := strings.Replace(uuidWithHyphen.String(), "-", "", -1)
	fmt.Println(uuid)
}

请注意,我们在 uuid.go 中也导入了依赖项。

"github.com/pborman/uuid"

让我们运行以下命令。

go mod tidy

此命令将下载您源文件中所需的所有依赖项,并用该依赖项更新go.mod文件。在运行此命令后,让我们再次检查go.mod文件的内容。执行cat go.mod

module learn

go 1.14

require github.com/pborman/uuid v1.2.1

它列出了在 uuid 文件中指定的直接依赖项及其确切版本。现在让我们检查一下go.sum文件。

执行cat go.sum

github.com/google/uuid v1.0.0 h1:b4Gk+7WdP/d3HZH8EJsZpvV7EtDOgaZLtnaNGIu1adA=
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/pborman/uuid v1.2.1 h1:+ZZIw58t/ozdjRaXh/3awHfmWRbzYxJoAdNJxe/3pvw=
github.com/pborman/uuid v1.2.1/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=

go.sum 文件列出了模块所需的直接和间接依赖项的校验和。 github.com/google/uuid 在内部被 github.com/pborman/uuid 使用。它是模块的间接依赖,因此记录在 go.sum 文件中。

Go(Golang)中的包和模块 – 第一部分

来源:golangbyexample.com/packages-modules-go-first/

这是 golang 综合教程系列的第四章。有关该系列其他章节,请参考此链接 – Golang 综合教程系列

下一个教程 – 包和模块 – 第二部分

上一个教程 – 设置 GO 工作区和 Hello World 程序

现在让我们查看当前教程。以下是当前教程的目录。

目录

  • 概述

  • 模块

  • 模块世界之前

    • Go 版本 1.11 之前

    • 在 Go 版本 1.11 中

    • Go 版本 1.13 之后

  • 理解包及创建模块

  • 模块之前创建应用程序

  • 导出与未导出名称

  • 嵌套包

  • 导入包中的别名

  • 初始化函数

  • Go 程序的执行顺序

  • 导入中的空标识符

  • 包命名约定

  • 结论 * *# 概述

包是 GO 中代码重用的一种方式。顾名思义,它是将相关代码分组的一种方法。Go 模块是处理 golang 中依赖关系的一种方式。

GO 应用程序文件中的每个 GO 源文件(.go 文件)都属于一个包。这就是每个.go文件以此开头的原因。

package <package_name></package_name>

上述称为包声明。请记住这个术语,因为在本教程中将多次使用。

同一目录下的所有.go 文件都将属于同一包。关于包的一个很大误解是,包是包含.go 文件的目录的名称。这并不正确。目录只是一个目录,而包的名称就是在包声明中出现的名称。那么目录名称的重要性是什么呢?将在教程中逐步解释。

包可以分为两种类型。

  • 可执行包 – 只有main是 GoLang 中的可执行包。一个.go 文件可能属于特定目录中的主程序包。稍后我们将看到目录名称或.go文件名称的重要性。主程序包将包含一个主函数,标志着程序的开始。安装主程序包后,它将在$GOBIN目录中创建一个可执行文件。

  • 实用程序包– 任何非主程序包都是实用程序包。它不是自执行的,仅包含可供可执行包使用的实用函数和其他实用工具。

模块

模块是 Go 对依赖管理的支持。根据定义,模块是一组相关包,根目录下有go.mod文件。go.mod文件定义了

  • 模块导入路径。

  • 模块的依赖要求,以便成功构建。它定义了项目的依赖要求,并将其锁定到正确的版本。

将模块视为包含一组包的目录。这些包也可以是嵌套的。模块提供

  • 依赖管理。

  • 有了模块,Go 项目不一定必须位于$GOPATH/src文件夹中。

除了go.mod文件,Go 还保留了go.sum文件,包含所有项目依赖模块的加密哈希。这是为了验证项目的依赖模块未发生变化。

模块之前的世界

模块在 Go 1.11 版本中引入。让我们逐版本查看变更,以便充分了解之前的限制以及自模块以来的变化。

  • Go 版本 1.11 之前 – 模块根本不存在。

  • Go 版本 1.11 – 引入了模块,但尚未最终确定。

  • Go 版本 1.13 – 引入了模块。

Go 版本 1.11 之前

在模块出现之前,Go 仅有包。$GOPATH 位置将有三个目录。

  • src

  • pkg

  • bin

这些是模块时代之前存在的问题。

  • 所有 Go 项目在$GOPATH/src目录中。

  • 没有本地依赖管理支持。

  • 所有依赖项将下载到$GOPATH/src目录中,不带版本控制。

让我们逐一查看每个问题:

  • 任何 Go 项目都必须位于$GOPATH/src目录中。

这是一个重大限制,因为它限制了你可以放置项目的位置。

  • 没有本地依赖管理支持。

此外,在模块之前的一个问题是,没有办法在项目中指定依赖项。虽然有 dep、glide 等替代解决方案,但缺少本地解决方案。

  • 所有依赖项将下载到$GOPATH/src目录中,不带版本控制。

当我们执行 go get 时,它将在$GOPATH/src 目录中下载所需的包。因此,在 Go 1.11 版本之前,以下 go get 命令

go get github.com/pborman/uuid

它将在该位置下载包。

$GOPATH/src/github.com/pborman/uuid

注意上面的 go get 命令没有指定版本。因此,它下载的是最新版本。此外,请注意下载的包。它甚至没有列出任何版本信息。现在,这是一个问题。如果 github.com/pborman/uuid 包有更新,你想获取该更新。由于没有版本控制,更新的包将下载到相同位置,替换旧版本。

在 Go 版本 1.11

在 Go 1.11 中,引入了模块,但尚未最终确定。因此,如果你仍在使用它,最好切换到最新版本。

在 Go 版本 1.13 之后

我们已经讨论了在预模块时代存在的所有问题。现在让我们看看这些问题是如何通过引入模块得到解决的。

第一个问题是

  • 所有 Go 项目位于 $GOPATH/src 目录。

有了模块,这不再是一个要求。

  • 没有本地依赖管理支持。

模块在 Go 中引入了本地依赖管理。通过模块,它提供了两个新文件:

  1. go.mod

  2. go.sum

使用 go.modgo.sum 文件,我们能够安装精确版本的依赖项而不破坏任何东西。我们已经在本教程开头简要介绍了这些文件。稍后在教程中,我们将详细查看它。

  • 所有依赖项将在 $GOPATH/pkg/mod 目录中下载并进行版本控制。

所以如果你下载同一库的不同版本,那么两个版本将下载到 \(GOPATH/pkg/mod** 中的不同目录,而不会互相覆盖。**\)GOPATH/pkg/mod 将包含两样东西。

  1. cache – 这是所有依赖项将被下载的文件夹,连同压缩代码。

  2. 所有下载的依赖项的压缩代码将从缓存目录复制过来。

此外,还引入了一个名为 GO111MODULE 的新环境变量。

GO111MODULE=off 时,go get 将以旧方式运行,会在 $GOPATH/src 文件夹中下载依赖项。

GO111MODULE=on 时,go get 将以新方式运行,所有模块将下载到 $GOPATH/pkg/mod/cache 文件夹,并进行版本控制。

GO111MODULE=auto 时,

  • 在 $GOPATH/src 文件夹外运行 go get 时,它将表现得好像 GO111MODULE=on。

  • 在 $GOPATH/src 文件夹中运行 go get 时,它将表现得好像 GO111MODULE=off。

现在让我们创建一个模块。我们讨论的内容将更加清晰。

理解包并创建模块

可以使用以下命令来创建模块。

go mod init {module_import_path}

让我们再次看看之前讨论过的 go.modgo.sum 文件。

go.mod

这是模块依赖文件。它将包含三样东西

  • 模块的导入路径位于顶部。

  • 创建模块时所使用的 Go 版本。

  • 模块的直接依赖项。

go.sum

此文件列出了所需的直接和间接依赖项的校验和及其版本。需要提到的是,go.mod文件对于成功构建来说是足够的。go.sum文件中的校验和用于验证每个直接和间接依赖项的校验和。

现在问题是import_path是什么。import_path是其他任何模块用来导入你的模块的前缀路径。我们将在教程的第二部分深入了解导入路径。

转到$GOPATH/src 文件夹外的任何目录。假设目录名称为learn

mkdir learn
cd learn

假设模块的导入路径是sample.com/learn

go mod init sample.com/learn

此命令将在同一目录中创建一个go.mod文件。

让我们检查一下该文件的内容。执行命令cat go.mod

module sameple.com/learn

go 1.14

当模块首次使用 init 命令创建时,go.mod 文件将仅包含两项内容。

  • 模块的导入路径位于顶部。
module sameple.com/learn
  • 创建模块时使用的 Go 版本。
go 1.14

由于它是一个空模块,因此尚未指定任何直接依赖项。让我们在同一目录中创建一个名为main.go的文件,内容如下。

main.go

package main

import (
	"fmt"
	"strings"

	"github.com/pborman/uuid"
)

func main() {
	uuidWithHyphen := uuid.NewRandom()
	uuid := strings.Replace(uuidWithHyphen.String(), "-", "", -1)
	fmt.Println(uuid)
}

注意上面文件中的包声明。

package main

这意味着上面的源文件属于main包。注意我们在main.go中也导入了该依赖项。

"github.com/pborman/uuid"

由于main.go文件属于 main 包,因此我们可以创建一个可执行文件。可执行文件始终与包含 main 包的模块的导入路径中的最后一个名称一起构建。在我们的案例中,模块的导入路径是 sample.com/learn,导入路径中的最后一个名称是 learn。因此,运行 go install 时将创建名为 learn 的可执行文件。

可以使用这三个命令中的任何一个来创建可执行文件。

  • ‘go install learn’

  • ‘go install’

  • ‘go install .’

这里目录的名称无关紧要。可执行文件的名称将始终与模块名称相同。上述所有命令将在\(GOBIN**目录中创建名为**learn**的可执行文件。如果**\)GOBIN目录在你的路径中,你可以直接运行该可执行文件。

learn

同时,go install 命令将下载源文件中所需的所有依赖项,并用该依赖项更新go.mod文件。

在运行该命令后,让我们再次检查go.mod文件的内容。

执行命令cat go.mod

module learn

go 1.14

require github.com/pborman/uuid v1.2.1

该文件列出了在main.go文件中指定的直接依赖项及其确切版本。现在让我们检查一下go.sum文件。

执行命令cat go.sum

github.com/google/uuid v1.0.0 h1:b4Gk+7WdP/d3HZH8EJsZpvV7EtDOgaZLtnaNGIu1adA=
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/pborman/uuid v1.2.1 h1:+ZZIw58t/ozdjRaXh/3awHfmWRbzYxJoAdNJxe/3pvw=
github.com/pborman/uuid v1.2.1/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=

go.sum 文件列出了模块所需的直接和间接依赖项的校验和。 github.com/google/uuid 在 github.com/pborman/uuid 中被内部使用。它是模块的间接依赖,因此记录在 go.sum 文件中。直接依赖和间接依赖都会下载到 $GOAPTH/pkg/mod/cache 文件夹中,并带有版本信息。

我们还可以直接运行可执行文件。

learn

注意:$GOBIN 需要直接在你的路径中,以便上述命令可以运行。

输出

e594dc4d9a754bcb83b56e89b18b4b46

以上方式我们在源文件中添加了一个依赖项,当我们运行 go install 时,它下载了该依赖项并将其添加到 go.mod 文件中。可执行文件也创建在 $GOBIN 目录中。

现在有几个要注意的点。

  • 目录名称是 learn

包含 go.mod 文件的目录名称无关紧要。你可以尝试将名称更改为任何内容。它始终会创建一个与模块导入路径最后部分相同名称的可执行文件。

  • 模块的导入路径是 sample.com/learn

可执行文件的名称始终是模块导入路径最后部分的名称,这里是 learn。导入路径的重要性将在教程的第二部分学习。现在只需理解模块导入路径用于将该模块导入到另一个模块中。如果模块导入路径仅是一个名称,那么可执行文件将只会以该名称创建。例如,模块导入路径可能仅为 learn。在这种情况下,可执行文件的名称也是 learn。因此,对于以下模块导入路径,可执行文件的名称将是 learn

sample.com/manage/learn
sample.com/learn
learn
  • 文件名称是 main.go

在运行 ‘go install’ 时,文件名称在这里并不重要。你可以尝试将文件从 main.go 更改为 test.go。之后再次运行上述命令。它将创建一个与模块名称相同的可执行文件,即 learn,且没有任何变化。

  • learn 模块中的包名称是 main

包的名称是重要的。如果 go install 在目录中看到一个主包,它将会在 $GOBIN 目录中创建一个可执行文件。可执行文件的名称将与模块导入路径中的最后名称相同。

到目前为止我们学到的:

  • 只有 main 包是可执行的。

  • 包含模块的目录名称在可执行文件的名称中并不重要。

  • 运行 ‘go install’ 时,它会创建一个二进制文件,名称与包含主包的 .go 文件的模块导入的最后部分相同。

  • 在运行 go install 时,文件名称并不重要。你可以有任何名称。

让我们在模块之前也看看同样的例子,以了解引入模块所提供的好处。

在模块之前创建应用程序

在模块出现之前,你的所有代码都应该位于\(GOPATH/src**文件夹中。让我们在**\)GOPATH/src目录中创建一个名为learn的目录。

$GOPATH/src/learn/main.go

package main

import (
	"fmt"
	"strings"

	"github.com/pborman/uuid"
)

func main() {
	uuidWithHyphen := uuid.NewRandom()
	uuid := strings.Replace(uuidWithHyphen.String(), "-", "", -1)
	fmt.Println(uuid)
}

运行

export GO111MODULE=off
go install

在模块出现之前,可执行文件始终使用包含main包的目录名称进行构建,而运行go install命令时,在我们的例子中目录的名称是learn

上述所有命令将在\(GOBIN 目录中创建一个名为**learn**的可执行文件。如果\)GOBIN 目录在你的路径中,那么你可以直接运行这个可执行文件。

learn

让我们看看模块之前的限制。

  • 所有代码都需要位于$GOPATH/src/文件夹中。

  • 当你执行go install时,它将下载\(GOPATH/src**文件夹中的直接和间接依赖项,而没有任何版本信息。你可以检查你的**\)GOPATH/src文件夹。它会下载“github.com/pborman/uuid”依赖项,而没有任何版本信息。

  • 在代码中没有办法定义依赖版本。简单的go get将下载依赖的最新版本,并将其保存在$GOPATH/src位置,覆盖已存在的旧版本(如果有)。

模块解决了我们在第一个例子中看到的所有问题。现在我们已经理解了包和模块的基础知识,以及模块提供的优势。接下来,让我们在本教程中更详细地了解包的内容。我们将在本教程的第二部分——模块中更加关注模块。第二部分的链接是 – golangbyexample.com/packages-modules-go-second/。

现在让我们添加另一个属于主包的文件。

learn/subtract.go

package main
func subtract(a, b int) int {
    return a - b
}

learn/main.go

package main
import "fmt"
func main() {
    fmt.Println(add(2, 1))
    fmt.Println(subtract(2, 1))
}
func add(a, b int) int {
    return a + b
}

请再次尝试运行go install命令。它将在$GOBIN目录中创建一个名为learn的可执行文件。尝试运行这个可执行文件,它将给出以下输出。此外,请注意main.go中的main函数能够调用subtract.go中的subtract函数。这是因为mainsubtract都属于同一个包 main。

learn $ learn
3
-1

所以基本上

  • 在同一个包内,所有变量、函数、常量都可以在属于该包的不同.go 文件之间访问。

同一目录中的所有.go文件将属于同一个包。这对于所有包含包的目录都是正确的。无论该目录是否包含go.mod文件,这一点都不重要。让我们验证一下。将包声明更改为

package subtract

go.mod

module sameple.com/learn

go 1.14

learn/subtract.go

package subtract

func subtract(a, b int) int {
    return a-b
}

learn/main.go

package main

import "fmt"

func main() {
    fmt.Println(add(2, 1))
    fmt.Println(subtract(2, 1))
}

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

注意包声明。 它是

package subtract

让我们尝试运行go install命令。它将产生错误。

can't load package: package learn: found packages subtract (subtract.go) and main (main.go) in

上述错误意味着目录learn包含两个文件subtract.gomain.go,它们分别属于不同的包,名为subtractmain。因此,在同一目录中,属于不同包的 GO 源文件是不允许的,因此出现了这个错误。将 subtract.go 中的Package Declaration更改为属于main包,错误将消失。

如你所见,我们使用了两个函数,addsubtract。这两个方法是算术函数,如果能将其作为一些公共代码的一部分,供其他部分使用,那将是很好的。这就是包的用处。我们的想法是创建一个新的包math,其中包含这两个函数。通过这种方式,包提供了代码重用的方法。让我们引入一个新包math

go.mod

module sameple.com/learn

go 1.14

learn/math/math.go

package math

func Add(a, b int) int {
    return a + b
}
func Subtract(a, b int) int {
    return a - b
}

learn/main.go

package main

import (
    "fmt"
    "sample.com/learn/math"
)

func main() {
    fmt.Println(math.Add(2, 1))
    fmt.Println(math.Subtract(2, 1))
}

如果你运行这个程序,输出将是相同的:

3
1

关于上述程序需要注意的几点

  • 导入一个包

main.go 文件通过“sample.com/learn/math”导入包,并能够使用 math.Add(..)和 math.Subtract(..)调用 Add 和 Subtract。看看我们是如何在 main.go 文件中导入 math 包的。

"sample.com/learn/math"

这里的导入路径是模块的导入路径,即learn + 包所在的目录math。因此为“sample.com/learn/math”。嵌套目录中的包也可以以相同方式导入。

  • 创建一个包

我们创建了一个新目录 math,并在其中创建了新文件 math.go。math.go 的包声明为“package math”。它还包含文件math.go,其中包含两个函数AddSubtract

首先让我们讨论导入语句。我们可以通过两种方式导入一个包。

  • 导入的简写方式
import (
    "fmt"
    "sample.com/learn/math"
)

还有另一种形式

import "fmt"
import "sample.com/learn/math"

在程序中,你可以导入任意数量的包。GO 尝试以下方式解析。

  • 检查目录\(GOROOT/src – “fmt”包位于位置**\)GOROOT/src/fmt**。

  • 使用模块时,从模块的根开始解析路径,即包含go.mod文件的目录。

import "sample.com/learn/math"

上述语句基本上意味着导入位于位置“sample.com/learn/math”的包。它并不意味着导入包math。为验证这一点,让我们将目录名称更改为math2。包声明仍然相同,即“package math”

go.mod

module sample.com/learn

go 1.14

learn/math2/math.go – 请注意,这里目录名称是math2

package math

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

func Subtract(a, b int) int {
    return a - b
}

learn/main.go

package main

import (
    "fmt"
    "sample.com/learn/math2"
)

func main() {
    fmt.Println(math.Add(2, 1))
    fmt.Println(math.Subtract(2, 1))
}

如果你运行这个程序,输出仍然是相同的:

main.go中,导入文件夹更改为“sample.com/learn/math2”,但main函数在main.go中仍然引用AddSubtract作为math.Add()math.Subtract()。所以基本上 GO 从“math2”目录导入math包。这就是我们所说的导入意味着导入位于该目录位置的包。

所以我们现在知道包目录名称的用途。

  • 目录的使用是为了 GO 程序中的导入语句。在导入时,我们提供目录路径而不是包名称。GO 然后获取所有具有相同包名称的文件。

文件名怎么样?在上述情况下,文件名是math.go。文件名并不重要。你可以尝试将文件名从math.go更改为其他名称,输出仍然是相同的。

让我们继续。你可能注意到AddSubtract都是大写的。这是为什么呢?如果它们不是大写会发生什么?如果 Add 和 Subtract 不是大写的,main.go将无法引用它们。这引出了我们下一个讨论话题。

导出名称与未导出名称

Go 没有任何publicprivateprotected关键字。控制可见性的唯一机制是使用大写和小写格式。

  • 大写标识符是导出的。大写字母表示这是一个导出标识符,它将在其他包中可见。

  • 未大写的标识符不会被导出。小写表示该标识符不会被导出,只能在同一包内访问。

有五种标识符可以是导出的或未导出的。

  1. 结构

  2. 结构的方法

  3. 结构的字段

  4. 函数

  5. 变量

让我们添加一个名为multiply的函数,它将是小写的。我们将它添加到 math 包中,看看它是否可以从main.go访问。

go.mod

module sameple.com/learn

go 1.14

learn/math/math.go

package math
func Add(a, b int) int {
    return a + b
}
func Subtract(a, b int) int {
    return a - b
}
func multiply(a, b int) int {
    return a * b
}

learn/main.go

package main
import (
    "fmt"
    "sample.com/learn/math"
)
func main() {
    fmt.Println(math.Add(2, 1))
    fmt.Println(math.Subtract(2, 1))
    fmt.Println(math.multiply(2, 1))
}

让我们运行这个程序。它会产生错误。

learn $ go install
learn $ learn
./main.go:12:14: cannot refer to unexported name math.multiply
./main.go:12:14: undefined: math.multiply

错误是因为main.go无法引用未导出的名称math.multiply。将 multiply 改为大写,它就能工作了。之后应该会输出如下结果。

3
1
2

如在教程开始时提到的,multiply函数将在math包中可用。为了说明这一点,让我们在 arithmetic.go 中创建一个Mul函数,它将内部调用multiply函数。

go.mod

module sameple.com/learn

go 1.14

learn/math/math.go

package math
func Add(a, b int) int {
    return a + b
}
func Subtract(a, b int) int {
    return a - b
}
func Mul(a, b int) int {
    return multiply(a, b)
}
func multiply(a, b int) int {
    return a * b
}

learn/main.go

package main
import (
    "fmt"
    "sample.com/learn/math"
)
func main() {
    fmt.Println(math.Add(2, 1))
    fmt.Println(math.Subtract(2, 1))
    fmt.Println(math.Mul(2, 1))
}

让我们运行这个程序。

learn $ go install
learn $ learn
3
1
2

上述程序能够工作,因为在同一包内,你可以引用未导出或未大写的名称。

目前math.go包含AddSubtract函数。我们可以将这两个函数分配到不同的文件中,同时仍然属于同一包math吗?可以。让我们在math目录下创建add.gosubtract.go文件。add.go 将包含Add函数,subtract.go 将包含Subtract函数。注意add.gosubtract.go的包声明都是package math

go.mod

module sameple.com/learn

go 1.14

learn/math/add.go

package math
func Add(a, b int) int {
    return a + b
}

learn/math/subtract.go

package math
func Subtract(a, b int) int {
    return a - b
}

learn/main.go

package main
import (
    "fmt"
    "sample.com/learn/math"
)
func main() {
    fmt.Println(math.Add(2, 1))
    fmt.Println(math.Subtract(2, 1))
}

让我们运行这个程序。

learn $ go install
learn $ learn
3
1

它能够工作,因为main.go中的导入语句从导入路径“sample.com/learn/math”中获取所有属于math包的标识符。

嵌套包

在 GO 中,可以创建嵌套包。我们将在math目录中创建一个名为advanced的新目录。“.” 该目录将包含square**.go 文件,包声明为“package advanced”。

go.mod

module sameple.com/learn

go 1.14

learn/math/math.go

package math
func Add(a, b int) int {
    return a + b
}
func Subtract(a, b int) int {
    return a - b
}

learn/math/advanced/advanced.go

package advanced
func Square(a int) int {
    return a * a
}

learn/main.go

package main
import (
    "fmt"
    "sample.com/learn/math"
    "sample.com/learn/math/advanced"
)
func main() {
    fmt.Println(math.Add(2, 1))
    fmt.Println(math.Subtract(2, 1))
    fmt.Println(advanced.Square(2))
}

让我们运行这个程序。

learn $ go install
learn $ learn
3
1
4

关于上述程序需要注意的几点。

  • 我们在 main.go 中使用完整的路径导入了advanced包,即import “sample.com/learn/math/advanced”

  • Square函数通过advanced包进行引用,即advanced.Square(2)

  • 如前所述,目录名称可以是其他advanced,只是必须相应导入。

  • 此外,文件名可以是其他任何名称,而不必是advanced.go

导入包时的别名

在导入包时使用别名意味着为导入的包指定不同的名称。其语法为:

import <new_name> <directory_path></directory_path></new_name>

上述语句的意思是,无论目录<directory_path>中存在什么包,都以<new_name>导入该包。别名对于命名很有用。

  • 在当前上下文中为导入的包提供更相关的名称。

  • 当两个不同的导入路径包含相同的包名时,则将其中一个作为不同的名称导入,以防止冲突。

让我们来看一个例子。

  • 创建一个文件夹math2. 创建一个文件math2.go,内容包含Subtract函数。math2.go中的包声明为“package math”,这意味着包名称仍为math

  • math文件夹仍包含仅有Add函数的math.go文件。

  • 请注意,包名(即math)在math文件夹和math2.文件夹中是相同的。因此,文件夹math2math都包含相同的包,即math

go.mod

module sameple.com/learn

go 1.14

learn/math2/math2.go

package math
func Subtract(a, b int) int {
    return a - b
}

learn/math/math.go

package math
func Add(a, b int) int {
    return a + b
}

learn/main.go

package main
import (
    "fmt"
    "sample.com/learn/math"
    math2 "sample.com/learn/math2"
)
func main() {
    fmt.Println(math.Add(2, 1))
    fmt.Println(math2.Subtract(2, 1))
}

让我们运行这个程序。

go install
learn $ learn
3
1

请注意这一行。我们将“sample.com/learn/math2”中存在的math包别名为math2. 如果我们没有这样做,那么 GO 将引发编译问题,因为它无法从两个不同的文件夹导入同名的包。这是使用别名的一个优势。

math2 "sample.com/learn/math2"

Init 函数

init()函数是一个特殊函数,用于初始化包的全局变量。这些函数在包初始化时执行。包中的每个 GO 源文件都可以有自己的 init()函数。每当你在程序中导入任何包时,在该程序执行时,属于该导入包的 GO 源文件中的 init 函数(如果存在)会首先被调用。关于 init 函数需要注意的几点:

  • Init 函数是可选的。

  • Init 函数不接受任何参数。

  • Init 函数没有返回值。

  • Init 函数是隐式调用的。由于它是隐式调用的,init 函数无法从任何地方引用。

  • 在同一个源文件中可以有多个 init()函数。

init 函数主要用于初始化无法通过初始化表达式初始化的全局变量。例如,它可能需要网络调用来初始化任何数据库客户端。另一个例子可能是在启动时获取密钥。init 函数还用于运行只需要执行一次的任何内容。让我们看看使用 init 函数的一个简单用例。

让我们看看一个使用 init 函数的示例。

go.mod

module sameple.com/learn

go 1.14

learn/math/add.go

package math

import "fmt"

func init(){
	fmt.Println("In add init")
}

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

learn/math/subtract.go

package math

import "fmt"

func init(){
	fmt.Println("In subtract init")
}

func Subtract(a, b int) int {
	return a - b
}

learn/main.go

package main

import (
	"fmt"

	"sample.com/learn/math"
)

func init(){
    fmt.Println("In main init")
}

func main() {
	fmt.Println(math.Add(2, 1))
	fmt.Println(math.Subtract(2, 1))
}

输出

In add init
In subtract init
In main init
3
1

从输出中可以注意到

  • math/add.go 文件中的 init() 函数被执行,并打印 – “In add init”

  • math/subtract.go 文件中的 init() 函数被执行,并打印 – “In subtract init”

  • 接下来,main.go 文件中的 init() 函数被执行,并打印 – “In main init”

尽管这两个源文件属于同一个包 math,但两个源文件中的 init 函数都被执行。

Go 程序的执行顺序

以下是 Go 程序的执行顺序。

  • 程序从主包开始。

  • 主包源文件中所有导入的包都被初始化。后续导入的包也以递归方式执行相同的操作。

  • 然后这些包中的全局变量声明被初始化。初始化依赖关系会启动以初始化这些变量。 golang.org/ref/spec#Order_of_evaluation

  • 此后,这些包中的 init() 函数被运行。

  • 主包中的全局变量被初始化。

  • 如果存在,主包中的 init 函数将被运行。

  • 主包中的 main 函数将被运行。

注意,这里包的初始化只进行一次,即使被多次导入。

例如,如果主包导入包 a,而包 a 又导入包 b,那么顺序将如下:

  • b 中的全局变量将被初始化。包 b 的源文件中的 init 函数将被运行。

  • a 中的全局变量将被初始化。包 b 的源文件中的 init 函数将被运行。

  • main 包中的全局变量将被初始化。main 包源文件中的 init 函数将被运行。

  • main 函数将开始执行。

让我们看看一个相同的程序。

go.mod

module sameple.com/learn

go 1.14

learn/b/b1.go

package b

import (
	"fmt"
)

func init() {
	fmt.Println("Init: b1")
}

func TestB() error {
	return nil
}

learn/b/b2.go

package b

import (
	"fmt"
)

func init() {
	fmt.Println("Init: b2")
}

learn/a/a1.go

package a

import (
	"fmt"
	"sample/b"
)

func init() {
	fmt.Println("Init: a1")
}

func TestA() error {
	return b.TestB()
}

learn/a/a2.go

package a

import (
	"fmt"
)

func init() {
	fmt.Println("Init: a2")
}

learn/main.go

package main

import (
	"fmt"
	"sample/a"
)

func init() {
	fmt.Println("Init: main")
}
func main() {
	fmt.Println("Main Function Executing")
	a.TestA()
}

输出

Init: b1
Init: b2
Init: a1
Init: a2
Init: main
Main Function Executing

请注意,在上面的示例中,包 b 的源文件中的 init 函数最先运行。然后运行包 a 源文件中的 init 函数,最后运行主包的源文件中的 init 函数。之后,main 函数开始执行。

导入中的空标识符

导入包中的空标识符意味着为导入的包指定一个空导入。其语法为

import _ <directory_path></directory_path>

这个空导入是什么,为什么会使用它。为此,你需要了解两件事。

  1. 关于 init 函数

  2. 关于由下划线(‘_’)表示的空标识符

init() 函数我们已经在上面学习过。

现在让我们来谈谈空标识符。

你已经知道 Go 不允许任何未使用的变量。任何未使用的变量可以用空标识符(‘_’)替代。

因此,现在当

  • 在当前程序中,导入的包没有被使用。

  • 但我们打算导入该包,以便可以调用属于该包的 GO 源文件中的 init 函数,并能正确初始化该包中的变量。

所以基本上,空导入是在仅为其副作用导入包时使用的。例如,MySQL 包作为空导入使用,目的是在 MySQL 包的 init() 函数中注册 MySQL 驱动程序作为数据库驱动程序,而不导入任何其他函数:

_ "github.com/go-sql-driver/mysql"

包命名约定

包的名称非常重要,因为访问包的类型、函数、常量或变量时都需要以包名为前缀。因此,包名应该简短且清晰。建议避免

  • 包名中的下划线

  • 驼峰命名法或任何混合大小写

结论

在本教程中,我们学习了包并简要介绍了模块。在我们完全转向模块之前,理解包是非常重要的。在本教程的第二部分,我们将完全专注于模块。希望你喜欢这篇文章。请在评论中分享反馈/错误/改进意见。

下一个教程 包和模块 – 第二部分 上一个教程 设置 GO 工作区和 Hello World 程序

Go (Golang) 中的包与模块 – 第二部分

来源:golangbyexample.com/packages-modules-go-second/

这是 Go 语言综合教程系列的第五章。有关该系列其他章节的信息,请参考此链接 – Golang 综合教程系列

下一个教程 – 变量

上一个教程 – 包和模块 – 第一部分

现在让我们查看当前的教程。下面是当前教程的目录。

目录

  • 概述

  • 模块的类型

  • 包与模块

  • 向你的项目添加依赖

    • 直接添加到 go.mod 文件中

    • 执行 go get

    • 将依赖添加到你的源代码并执行 go mod tidy

  • 添加供应商目录

  • 模块导入路径

  • 在同一模块中导入包

  • 从不同模块本地导入包

  • 选择库的版本

    • 在次要或补丁版本中不同

    • 在主版本中不同

  • go mod 命令

  • go.mod 文件中的直接与间接依赖

  • 结论

概述

在上一个教程中,我们详细了解了包以及模块的概述。

在本教程中,我们将重点关注模块。

模块的类型

我们了解到模块是一个包含嵌套 Go 包的目录。因此,模块本质上可以视为一个包,只是它包含嵌套的包。我们在包的教程中看到,包可以是可执行包或实用包(非可执行)。类似于包,模块也可以分为两种类型。

  • 可执行模块——我们已经知道main是 GoLang 中的可执行包。因此,包含 main 包的模块是可执行模块。main包将包含一个main函数,表示程序的起始。在安装包含main包的模块时,它将在$GOBIN 目录中创建一个可执行文件。

  • 非可执行模块或实用模块——除了main包以外的任何包都是非可执行包。它不是自执行的。它仅包含实用函数和其他可被可执行包使用的实用内容。因此,如果模块不包含main包,那么它就是一个非可执行或实用模块。此模块旨在作为实用工具使用,将被其他模块导入。

创建模块的可执行文件(仅适用于具有主包的模块)。

  • 执行go build,它将在当前目录中创建可执行文件。

  • 运行go install,它将在$GOBIN 目录中创建可执行文件。

包与模块

根据模块定义,它是一个包含嵌套和相关 Go 包的目录,根目录下有go.mod文件。go.mod文件定义了。

  • 模块导入路径。

  • 模块成功构建的依赖要求。它定义了项目的依赖需求,并将其锁定到正确的版本。

模块提供了。

  • 依赖管理。

  • 使用模块时,Go 项目不一定要位于$GOPATH/src文件夹。

除了go.mod文件,Go 还保持一个go.sum文件,其中包含所有项目依赖模块的加密哈希。这是为了验证项目的依赖模块没有被更改。

模块内包的行为与之前相同。因此,任何适用于包的内容现在也适用。在这一点上没有变化。然而,当需要单独版本化时,多个包的集合可以称为模块。此外,当它是共享代码的一部分时,您希望在多个项目中共享该代码。

向项目添加依赖

让我们探索一些将依赖添加到项目的方法。

  • 直接添加到go.mod文件。

  • 执行go get

  • 将依赖添加到源代码并执行go mod tidy

在查看每种方式之前,先创建一个模块。

go mod init sample.com/learn

直接添加到 go.mod 文件

我们也可以直接将依赖添加到 go.mod 文件。让我们来做这个。

将以下依赖添加到go.mod文件中。

require github.com/pborman/uuid v1.2.1

添加此依赖后,go.mod 文件将如下所示。

module sample.com/learn

go 1.14

require github.com/pborman/uuid v1.2.1

现在我们也需要下载新添加的依赖。为此,我们可以使用下面的命令。

go mod download

这个命令将下载github.com/pborman/uuid模块及其所有依赖项。此外,它还会更新go.sum文件,包含所有直接和间接依赖项的校验和和版本。go buildgo install也会下载依赖项并构建二进制文件。go run也会下载并运行二进制文件。go mod download命令用于在不构建或运行的情况下预下载依赖项。

执行go get

只需执行go get也会在go.mod文件中添加依赖项。将我们之前在go.mod中添加的 uuid 依赖项删除,并清理go.sum文件。现在运行下面的命令。

export GO111MODULE=on
go get github.com/pborman/uuid

现在检查go.mod文件的内容。执行cat **go.mod**

module sample.com/learn

go 1.14

require github.com/pborman/uuid v1.2.1 //indirect

该依赖项将标记为//indirect,因为它在任何源文件中都未被使用。一旦你在源文件中使用后执行go build//indirect将会被 go 自动移除。同时,它还会更新go.sum文件,包含所有直接和间接依赖项的校验和和版本。

将依赖项添加到你的源代码中,并执行go mod tidy

基本上,go mod tidy命令确保你的go.mod文件反映了你在项目中实际使用的依赖项。当我们运行go mod tidy命令时,它会做两件事情。

  • 添加在源文件中导入的任何依赖项。

  • 移除go.mod文件中提到但未在任何源文件中导入的依赖项。

让我们看一个例子。创建一个导入路径为“sample.com/learn”的模块。

go mod init sample.com/learn

让我们在同一目录中创建一个名为uuid.go的文件,内容如下。

uuid.go

package main

import (
	"fmt"
	"strings"

	"github.com/pborman/uuid"
)

func main() {
	uuidWithHyphen := uuid.NewRandom()
	uuid := strings.Replace(uuidWithHyphen.String(), "-", "", -1)
	fmt.Println(uuid)
}

注意我们在uuid.go中也导入了依赖项。

"github.com/pborman/uuid"

让我们运行下面的命令。

go mod tidy

这个命令将下载在你的源文件中所需的所有依赖项,并使用该依赖项更新go.mod文件。运行此命令后,让我们再次检查go.mod文件的内容。执行cat **go.mod**

module sample.com/learn

go 1.14

require github.com/pborman/uuid v1.2.1

添加 vendor 目录

如果你想将依赖项放入 vendor 中,可以使用下面的命令来实现。

go mod vendor

它将在你的项目目录中创建一个 vendor 目录。你也可以将 vendor 目录中的内容检查到你的版本控制系统(VCS)中。这在某种程度上变得有用,因为运行时不需要下载任何依赖项,因为它们已经存在于检查到 VCS 中的 vendor 文件夹中。

模块导入路径

我们已经看到模块导入路径是用于导入该模块内所有包的前缀路径。

有三种情况决定可以使用什么导入路径名与模块。

  • 该模块是一个实用模块,并且你计划发布你的模块。

  • 该模块是一个实用模块,并且你不打算发布你的模块。

  • 该模块是一个可执行模块。

该模块是一个实用模块,并且你计划发布你的模块

如果你打算发布你的模块,则模块名称应与托管该模块的代码库的 URL 匹配。Go 会尝试使用相同的导入路径从版本控制系统下载依赖项。

该模块是一个工具模块,你不打算发布你的模块

当你只打算在本地使用工具模块时,就是这种情况。在这种情况下,导入路径可以是任何东西。

该模块是一个可执行模块

在这种情况下,模块导入路径也可以是任何东西。即使你打算将模块提交到版本控制系统,模块导入路径也可以是非 URL,因为它不会被其他模块使用。

然而,在创建模块时使用有意义的导入路径是一个好习惯。

在同一模块中导入包

同一模块中的任何包都可以使用模块 + 包所在目录的导入路径进行导入。为了说明这一点,让我们创建一个模块。

  • 创建一个learn目录。

  • 创建一个导入路径为“sample.com/learn”的模块。

go mod init sample.com/learn
  • 现在创建 main.go(包含 main 包和 main 函数)。

  • 以及 math/math.go – 数学包。

main.go

package main

import (
	"fmt"
	"sample.com/learn/math"
)

func main() {
	fmt.Println(math.Add(1, 2))
}

math/math.go

package math

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

查看我们如何在 main.go 文件中导入数学包。

"sample.comlearn/math"

这里的导入路径是模块的导入路径,即sample.com/learn + 包所在的目录,即math。因此是“sample.com/learn/math”。嵌套目录中的包也可以以相同的方式导入。它的工作原理是,由于前缀是模块导入路径,因此 Go 会知道你正在尝试从同一模块导入。因此,它将直接引用,而不是下载。

从不同模块本地导入包

有时我们希望导入一个本地存在的模块。让我们了解如何导入这样的模块。但首先,我们必须创建一个可以被其他模块使用的模块,然后将其导入到其他模块中。为此,让我们创建两个模块。

  • sample.com/math模块。

  • school模块。

school模块将调用sample.com/math模块的代码。

让我们首先创建将被school模块使用的sample.com/math模块。

  • 创建一个math目录。

  • 创建一个导入路径为sample.com/math的模块。

go mod init sample.com/math
  • math目录中创建一个名为math.go的文件,内容如下。
package math

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

现在让我们创建学校模块。

  • 现在在与math目录并排的相同路径中创建一个school目录。

  • 创建一个模块名称为school

go mod init school
  • 现在,让我们修改go.mod文件,以便在学校模块中导入数学模块。要导入一个未推送到版本控制系统的本地模块,我们将使用替换目录。替换目录将用你指定的路径替换模块路径。
module school

go 1.14

replace sample.com/math => ../math
  • 创建一个名为 school.go 的文件,它将使用sample.com/math模块中的 Add 函数。
package main

import (
	"fmt"
	"sample.com/math"
)

func main() {
	fmt.Println(math.Add(2, 4))
}

现在运行go run

go run school.go

它能够调用sample.com/math模块的 Add 函数,并正确输出为 6。

另外,它将更新sample.com/math模块的 go.mod 版本信息。

module school

go 1.14

replace sample.com/math => ../math

require sample.com/math v0.0.0-00010101000000-000000000000

选择库的版本

为了理解 GO 在选择在go.mod文件中指定的两个版本的库版本时的做法,我们必须先理解语义版本控制。

语义版本控制由三个部分组成,由点分隔。以下是版本控制的格式。

v{major_version}.{minor_version}.{patch_version}

其中

  • v – 它只是一个指示版本的标志。

  • major_version – 它表示库中不兼容的 API 变化。因此,当库中有不向后兼容的变化时,major_version 会递增。

  • minor_version – 它表示库功能的向后兼容变化。因此,当库中有某些功能变化,但这些变化是向后兼容的情况下,minor 版本会递增。

  • patch_version – 它表示库中以向后兼容的方式修复的错误。因此,当库的现有功能修复了错误时,在这种情况下,patch_version 会递增。

现在可以有两种情况。

  • 使用了两个相同库的版本,它们只在小版本和补丁版本上有所不同。它们的主要版本是相同的。

  • 使用了两个相同库的版本,它们在主要版本上有所不同。

让我们看看 go 在上述两种情况下遵循什么方法。

在小版本或补丁版本上有所不同

Go 在选择两个版本在go.mod文件中指定的库版本时遵循最小版本策略方法,这两个版本只在小版本或补丁版本上有所不同。

例如,如果你使用相同库的两个版本,它们是。

1.2.0

并且

1.3.0

然后 go 将选择 1.3.0,因为它是最新版本。

在主要版本上有所不同

Go 将主要版本视为不同的模块。那这意味着什么呢?这本质上意味着导入路径将以主要版本作为后缀。让我们以任何 go 库的 VCS 为github.com/sample为例。让我们最新的语义版本是。

v8.2.3

然后 go.mod 文件将如下所示。

module github.com/sample/v8

go 1.13

..

它在导入路径中有主要版本。因此,任何使用此示例库的库必须像这样导入。

import "github.com/sample/v8"

如果未来发布v9版本,则必须像这样导入到应用程序中。

import "github.com/sample/v9"

此外,该库将更改其 go.mod 文件以反映 v9 主要版本。

module github.com/sample/v9

它本质上允许在同一 go 应用程序中使用相同库的不同主要版本。当在同一应用程序中导入相同库的不同主要版本时,我们还可以给出有意义的名称。例如。

import sample_v8 "github.com/sample/v8"
import sample_v9 "github.com/sample/v9"

这也被称为语义导入版本控制

还要注意

  • 对于第一个版本,不在 go.mod 文件中指定版本是可以的。

  • 导入相同库的不同主要版本时要小心。注意可能与新版本一起提供的新功能。

同样出于同样的原因,当你使用更新特定模块时。

go get -u

然后它只会升级到最新的小版本或补丁版本(如果适用)。例如,假设应用程序使用的当前版本是

v1.1.3

同样,假设我们有以下可用版本。

v1.2.0
v2.1.0

然后当我们运行时

go get

然后它将更新为

v1.2.0

原因是因为 go get 只会更新小版本或补丁版本,但从不更新主要版本,因为 go 将模块的主要版本视为完全不同的模块。

要升级主要版本,请在 go.mod 文件中显式指定升级的依赖项或执行该版本的 go get。

还有几点关于升级模块的注意事项。

  • 要仅将依赖项升级到其最新补丁版本,请使用以下命令。
go get -u=patch <dependency_name></dependency_name>
  • 要将依赖项升级到特定版本,请使用以下命令。
go get dependency@version
  • 要将依赖项升级到特定提交,请使用以下命令。
go get <dependency_name>@commit_number</dependency_name>
  • 要将所有依赖项升级到它们最新的小版本和补丁版本,请使用以下命令。
go get ./...

go mod 命令

以下是 go mod 命令的一些选项。

  • 下载 – 它将把所需的依赖项下载到 $GOPATH/pkg/mod/cache 文件夹中。同时,它将用所有直接和间接依赖项的校验和和版本更新 go.sum 文件。

  • 编辑 – 这是用于编辑 go.mod 文件。它提供了一组编辑标志。运行以下命令查看所有可用的编辑标志。

go help mod edit

go help mod edit 例如,以下是一些可用的编辑标志。

  1. -fmt 标志将格式化 go.mod 文件。它不会进行其他更改。

  2. -module 标志可用于设置模块的导入路径。

  • 图形 – 这可以用来打印模块需求依赖图。

  • 初始化 – 我们已经在上面看到这个命令的用法。它用于初始化一个新模块。

  • 整理 – 此命令将下载源文件中所需的所有依赖项。

  • 供应商 – 如果你想为你的依赖项创建供应商,则可以使用以下命令实现相同的功能。它将在你的项目目录中创建一个 vendor 目录。你也可以将 vendor 目录检查到你的版本控制系统(VCS)中。

  • 验证 – 此命令检查当前下载的依赖项是否已被修改。如果任何下载的依赖项经过验证,它将以非零代码退出。

  • 为什么 – 此命令分析来自主模块的包图。它打印从主模块到给定包的最短路径。例如,在“从不同模块本地导入包”部分中创建的 school 模块,如果我们打印为什么命令如下。

go mod why sample.com/math

然后以下将是输出。

# sample.com/math
school
sample.com/math

输出说明 sample.com/math 包在图中距离主模块(在这里是 school)有一段距离。

go.mod 文件中的直接与间接依赖

直接依赖是模块直接导入的依赖。间接依赖是由模块的直接依赖导入的依赖。此外,任何在 go.mod 文件中提到但未在模块的任何源文件中导入的依赖也被视为间接依赖。

go.mod 文件仅记录直接依赖。然而,在以下情况下,它可能会记录间接依赖。

  • 任何在你直接依赖的 go.mod 文件中未列出的间接依赖,或者如果直接依赖没有 go.mod 文件,则该直接依赖将以 //direct 作为后缀添加到 go.mod 文件中。

  • 任何未在模块的任何源文件中导入的依赖(我们之前在教程中已经看到过这个示例)。

go.sum 将记录直接和间接依赖的校验和。

go.mod 文件中的间接依赖示例

让我们通过一个示例来理解它。为此,让我们先创建一个模块。

git mod init sample.com/learn

让我们在 go.mod 文件中添加 colly 库版本 v1.2.0 作为依赖。colly 版本 v1.2.0 没有 go.mod 文件。

module sample.com/learn

go 1.14

require github.com/gocolly/colly v1.2.0

现在创建一个文件 learn.go。

package main

import (
	"github.com/gocolly/colly"
)

func main() {
	_ = colly.NewCollector()
}

现在执行 go build。由于 colly 版本 v1.2.0 没有 go.mod 文件,colly 所需的所有依赖将以 //indirect 作为后缀添加到 go.mod 文件中。执行 go build。现在检查 go.mod 文件。你会看到以下文件内容。

module learn

go 1.14

require (
	github.com/PuerkitoBio/goquery v1.6.0 // indirect
	github.com/antchfx/htmlquery v1.2.3 // indirect
	github.com/antchfx/xmlquery v1.3.3 // indirect
	github.com/gobwas/glob v0.2.3 // indirect
	github.com/gocolly/colly v1.2.0
	github.com/kennygrant/sanitize v1.2.4 // indirect
	github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca // indirect
	github.com/temoto/robotstxt v1.1.1 // indirect
	golang.org/x/net v0.0.0-20201027133719-8eef5233e2a1 // indirect
	google.golang.org/appengine v1.6.7 // indirect
)

所有其他依赖都以 //indirect 作为后缀。还要检查所有直接和间接依赖将在 go.sum 文件中记录。

结论

这就是 golang 中的包和模块。希望你喜欢这篇文章。请在评论中分享反馈/错误/改进建议。

下一个教程 – **变量**上一个教程** – **包和模块 – 第一部分

在 Go (Golang) 中交换链表中的节点

来源:golangbyexample.com/pair-swap-nodes-linked-list-go/

目录

  • 概述

  • 程序

概述

给定一个链表和一个数字 k,将链表中的节点按 k 的分组进行反转。

例如

Input: 1->2->3->4->5->6->7
k: 3
Output: 3->2->1->6->5->4->7

如果链表最后一组的长度小于 k,则保持最后一组不变

程序

package main

import "fmt"

func main() {
	first := initList()
	first.AddFront(4)
	first.AddFront(3)
	first.AddFront(2)
	first.AddFront(1)

	first.Head.Traverse()
	temp := ReverseKGroup(first.Head, 3)
        fmt.Println()
	temp.Traverse()

}

func initList() *SingleList {
	return &SingleList{}
}

type ListNode struct {
	Val  int
	Next *ListNode
}

func (l *ListNode) Traverse() {
	for l != nil {
		fmt.Println(l.Val)
		l = l.Next
	}
}

type SingleList struct {
	Len  int
	Head *ListNode
}

func ReverseKGroup(head *ListNode, k int) *ListNode {

	curr := head
	var prev *ListNode
	var next *ListNode

	i := 0

	for curr != nil && i < k {
		i++
		curr = curr.Next
	}
	if i == k {
		curr = head
	} else {
		return head
	}

	i = 0
	for curr != nil && i < k {
		next = curr.Next
		curr.Next = prev
		prev = curr
		curr = next
		i++
	}

	head.Next = ReverseKGroup(curr, k)
	return prev
}

func (s *SingleList) AddFront(num int) {
	ele := &ListNode{
		Val: num,
	}
	if s.Head == nil {
		s.Head = ele
	} else {
		ele.Next = s.Head
		s.Head = ele
	}
	s.Len++
}

输出

1
2
3
4

3
2
1
4

注意: 请查看我们的 Golang 高级教程。本系列教程内容详尽,我们努力涵盖所有概念和示例。这个教程适合那些希望获得 Golang 专业知识和扎实理解的人 - Golang 高级教程

如果你有兴趣了解如何在 Golang 中实现所有设计模式。如果是的话,那么这篇文章适合你 - 所有设计模式 Golang