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

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

在 Go (Golang) 中忽略错误

来源:golangbyexample.com/ignoring-errors-golang/

目录

  • 概述

  • 代码

概述

下划线(‘_’)运算符可用于忽略函数调用返回的错误。在查看程序之前,需要注意的是,错误不应该被忽略。这不是推荐的做法。让我们来看一个程序。

代码

package main
import (
    "fmt"
    "os"
)
func main() {
    file, _ := os.Open("non-existing.txt")
    fmt.Println(file)
}

输出

{nil}

在上述程序中,我们使用下划线运算符忽略了打开不存在文件时返回的错误。这就是为什么函数返回的文件实例为 nil。因此,在使用函数返回的任何其他参数之前,最好先检查错误,因为它可能为 nil,并导致不必要的问题,有时甚至可能导致恐慌。

IIF 或在 Go (Golang) 中的立即调用函数

来源:golangbyexample.com/immediately-invoked-function-go/

目录

  • 概述:

  • 用例

  • 代码:

概述:

IIF 或 立即调用函数是指那些可以同时定义和执行的函数。通过在函数结束括号后添加 (),可以立即调用该函数。

用例

IIF 函数的一个用途是在你不想暴露函数逻辑时,无论是在包内还是包外。例如,假设有一个函数用于设置某个值。你可以将所有设置逻辑封装在一个 IIF 函数中。这个函数在包外或包内都无法被调用。

代码:

让我们看看工作代码。计算 2 的平方的函数通过在末尾加上 () 立即被调用,返回的值被赋给变量 squareOf2

package main

import "fmt"

func main() {
    squareOf2 := func() int {
        return 2 * 2
    }()
    fmt.Println(squareOf2)
}

输出:

2

在 Go 中实现 while 循环(Golang)

来源:golangbyexample.com/go-implement-while-loop/

Go 没有while关键字。相反,它只有for关键字。然而,for关键字可以用来模拟与while相同的功能。

GO 中的for循环基本上有三个部分:

for initialization_part; condition_part; increment_part {
   ...
}

如果initialization_partincrement_part可以被跳过,for循环可以被实现为与while行为相同。以下是一个示例:

package main

import "fmt"

func main() {
    i := 1
    for i <= 5 {
        fmt.Println(i)
        i++
    }
}

输出:

1
2
3
4
5

在 Go (Golang) 中实现你自己的 Atoi 函数

来源:golangbyexample.com/implement-your-own-atoi-function-golang/

目录

  • 概述

  • 程序

概述

Atoi 函数将给定字符串转换为其数字表示。例如

Input: "121"
Output: 121

Input: "-121"
Output: 121

Input: "0"
Output: 0

程序

以下是相应的程序。

package main

import (
	"fmt"
	"strconv"
)

func main() {
	output := myAtoi("121")
	fmt.Println(output)

	output = myAtoi("-121")
	fmt.Println(output)

	output = myAtoi("0")
	fmt.Println(output)
}

func myAtoi(s string) int {

	var output int

	sign := "positive"
	if string(s[0]) == "-" {
		sign = "negtive"
		s = s[1:]
	} else if string(s[0]) == "+" {
		s = s[1:]
	}

	stringLen := len(s)

	for i := 0; i < stringLen; i++ {
		tempNum, _ := strconv.Atoi(string(s[i]))
		output = output*10 + tempNum
	}

	if sign == "negtive" {
		output = output * -1
	}

	return output
}

输出

121
-121
0

在 Go (Golang) 中导入同名包或在导入包时使用别名

来源:golangbyexample.com/import-same-package-name-golang/

目录

  • 概述

  • 示例

概述

导入包时的别名意味着给导入的包一个不同的名称。其语法为

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

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

  • 在当前上下文中给导入的包一个更相关的名称。

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

示例

创建一个模块,导入路径为 sample.com/learn

go mod init sample.com/learn

在下面的示例中,我们创建了两个目录 math 和 math2。

  • math 目录包含文件 math.go,包声明为
package math
  • math2 目录包含文件 math2.go,包声明为

请注意,包名(即 math)在 math 文件夹和 math2. 文件夹中是相同的。因此,两个文件夹 math2math 都包含相同的包,即 math. 由于这两个目录具有相同的包名,别名是将两个包用于同一文件的唯一方法。这就是我们在 main.go 中如何别名和使用两个包。

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

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

让我们看看完整的工作代码

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

在 Go (Golang) 中从不同模块本地导入包

来源:golangbyexample.com/import-local-module-golang/

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

  • sample.com/math模块

  • school模块

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

首先创建sample.com/math模块,该模块将被school模块使用

  • 创建一个math目录

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

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

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

现在让我们创建 school 模块

  • 在与math目录并排的同一路径下创建一个school目录

  • 创建一个模块名为school

go mod init school
  • 现在让我们修改go.mod文件以在 school 模块中导入 math 模块。要导入一个未推送到版本控制系统的本地模块,我们将使用替换目录。替换目录将用你指定的路径替换模块路径。
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。

此外,它还将更新go.mod,并包含sample.com/math模块的版本信息

module school

go 1.14

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

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

在 Go(Golang)中导入同一模块内的包

来源:golangbyexample.com/importing-package-same-module-go/

同一模块内的任何包都可以使用模块的导入路径加上包含该包的目录来导入。为了说明,让我们创建一个模块

  • 创建一个learn目录

  • 创建一个导入路径为“learn”的模块

go mod init learn
  • 现在创建 main.go(具有主包和主函数)

  • 以及 math/math.go – math 包

main.go

package main

import (
	"fmt"
	"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 文件中导入 math 包

"learn/math"

这里的导入路径是模块的导入路径learn加上包含该包的目录math。因此是“learn/math”。嵌套目录中的包也可以以同样的方式导入。其工作原理是由于前缀是模块导入路径,因此 Go 会知道你试图从同一模块中导入。这样它会直接引用,而不是下载。

在 Go(Golang)中索引字符串中的字符。

来源:golangbyexample.com/go-index-character-string/

在 Golang 中,字符串是字节的序列。字符串字面量实际上表示 UTF-8 字节序列。在 UTF-8 中,ASCII 字符是单字节,对应于前 128 个 Unicode 字符。所有其他字符占用 1 到 4 个字节。因此,无法在字符串中索引字符。

例如,请看下面的程序及其输出。

package main

import "fmt"

func main() {
    sample := "ab£c"
    for i := 0; i < 4; i++ {
        fmt.Printf("%c\n", sample[i])
    }
    fmt.Printf("Length is %d\n", len(sample))
}

输出:

a
b
Â
£
Length is 5

正如你可能注意到的,它打印了与预期不同的字符,长度也是 5 而不是 4。为什么会这样?要回答这个问题,请记住我们说过字符串本质上是字节的切片。让我们使用以下方式打印该字节切片。

sample := "ab£c"
fmt.Println([]byte(sample))

输出将是

[97 98 194 163 99]

这是每个字符到其字节序列的映射。如你所见,abc各占 1 个字节,但£占用两个字节。这就是为什么字符串的长度是 5 而不是 4。

a 97
b 98
£ 194, 163
c 99

那么我们如何在字符串中进行索引呢?这就是rune数据类型的作用。在 Go 中,rune数据类型表示一个 Unicode 点。你可以在这里了解更多关于 rune 的知识 - golangbyexample.com/understanding-rune-in-golang

一旦字符串被转换为rune数组,就可以在该数组中索引字符。请看下面的代码。

package main

import "fmt"

func main() {
    sample := "ab£c"
    sampleRune := []rune(sample)

    fmt.Printf("%c\n", sampleRune[0])
    fmt.Printf("%c\n", sampleRune[1])
    fmt.Printf("%c\n", sampleRune[2])
    fmt.Printf("%c\n", sampleRune[3])
}

输出:

a
b
£
c

还需提到的是,你可以使用范围操作符迭代字符串中的所有 Unicode 字符,但要在字符串中索引字符,可以将其转换为rune数组。

Go 语言中的中缀到后缀转换

来源:golangbyexample.com/infix-to-postfix-conversion-go/

目录

** 中缀到后缀转换

  • 算法:

  • 实现:

中缀到后缀转换

在本教程中,我们将了解中缀和后缀表达式的表示法,后缀表示法相比中缀的优点,以及如何将中缀表达式转换为后缀表达式。我们将在另一个教程中涵盖后缀表达式的计算。

Infix Expression: In infix expression, the operator is in between pair of operands like (a op b).
example: a+b, 2/2 .
Postfix Expression: In postfix expression, the operator is placed post to both operands like (a b op).
example: ab+, 22/ . 

这些是后缀表达式相比中缀表达式的一些优点:

  • 后缀表达式的计算比中缀表达式的计算更简单。

  • 后缀表达式中不需要括号。

  • 后缀表达式的计算只需单次扫描。

  • 比中缀表示法更快的计算。

算法:

  • 从左到右扫描中缀表达式。

  • 如果当前扫描的字符是操作数,输出它。

  • 否则,如果当前扫描的运算符的优先级高于栈中运算符的优先级(或栈为空或栈中包含一个‘(’),则压入栈。否则,从栈中弹出所有优先级大于或等于扫描运算符的运算符。完成后将扫描到的运算符推入栈中。(如果在弹出时遇到括号,则停止,并将扫描到的运算符压入栈中。)

  • 如果扫描到的字符是‘(’,则将其压入栈中。

  • 如果扫描到的字符是‘)’,则弹出栈并输出,直到遇到‘(’,并丢弃这两个括号。

  • 重复步骤 2-6,直到中缀表达式扫描完成。

  • 打印输出

  • 弹出并输出栈中的内容,直到栈为空。

以下是上述算法的示例:

中缀表达式为 a+b,则后缀表达式为 ab+:

  • a 是操作数(输出它) //output=a

    • 是运算符(推入栈中)
  • b 是操作数(输出它) //output=ab

  • 中缀表达式已完全扫描,现在从栈中弹出并添加到输出,因此后缀变为 ab+

类似地

中缀表达式为

a+b*c+d

然后后缀表达式为

abc*+d+:
  • a 是操作数(输出它) //output=a

    • 是运算符(压入栈中) //stack=+
  • b 是操作数(输出它) //ab

    • 是运算符,优先级高于 +,(压入栈中) //stack=+*
  • c 是操作数(输出它) //output=abc

    • 是运算符,但其优先级低于栈中的运算符。弹出栈中的所有运算符并压入 + //output=abc*+, stack=+
  • d 是操作数(输出它) //output=abc*+d

  • 从栈中弹出并添加到输出 //output=abc*+d+

实现:

以下是 Go 语言中中缀到后缀转换的实现:

package main

import "fmt"

type Stack []string

//IsEmpty: check if stack is empty
func (st *Stack) IsEmpty() bool {
    return len(*st) == 0
}

//Push a new value onto the stack
func (st *Stack) Push(str string) {
    *st = append(*st, str) //Simply append the new value to the end of the stack
}

//Remove top element of stack. Return false if stack is empty.
func (st *Stack) Pop() bool {
    if st.IsEmpty() {
        return false
    } else {
        index := len(*st) - 1 // Get the index of top most element.
        *st = (*st)[:index]   // Remove it from the stack by slicing it off.
        return true
    }
}

//Return top element of stack. Return false if stack is empty.
func (st *Stack) Top() string {
    if st.IsEmpty() {
        return ""
    } else {
        index := len(*st) - 1   // Get the index of top most element.
        element := (*st)[index] // Index onto the slice and obtain the element.
        return element
    }
}

//Function to return precedence of operators
func prec(s string) int {
    if s == "^" {
        return 3
    } else if (s == "/") || (s == "*") {
        return 2
    } else if (s == "+") || (s == "-") {
        return 1
    } else {
        return -1
    }
}

func infixToPostfix(infix string) string {
    var sta Stack
    var postfix string
    for _, char := range infix {
        opchar := string(char)
        // if scanned character is operand, add it to output string
        if (char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') || (char >= '0' && char <= '9') {
            postfix = postfix + opchar
        } else if char == '(' {
            sta.Push(opchar)
        } else if char == ')' {
            for sta.Top() != "(" {
                postfix = postfix + sta.Top()
                sta.Pop()
            }
            sta.Pop()
        } else {
            for !sta.IsEmpty() && prec(opchar) <= prec(sta.Top()) {
                postfix = postfix + sta.Top()
                sta.Pop()
            }
            sta.Push(opchar)
        }
    }
    // Pop all the remaining elements from the stack
    for !sta.IsEmpty() {
        postfix = postfix + sta.Top()
        sta.Pop()
    }
    return postfix
}
func main() {
    //infix := "a+b"
    //infix := "a+b*c+d"
    //infix := "a+b*(c^d-e)^(f+g*h)-i" // abcd^e-fgh*+^*+i-
    //infix := "1+2+3*4+5/5-2"
    infix := "2+3*(2³-5)^(2+1*2)-4" //abcd^e-fgh*+^*+i-
    postfix := infixToPostfix(infix)
    fmt.Printf("%s infix has %s postfix ", infix, postfix)

}

输出:

2+3*(2³-5)^(2+1*2)-4 infix has 2323⁵-212*+^*+4- postfix

我们可以通过取消注释fmt.Println行来检查每次推送和弹出操作后栈的状态。

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

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

使用接口 + 结构体的 GO 继承

来源:golangbyexample.com/inheritance-go-interface-struct/

本文描述了使用接口和结构体的继承。请查看我们关于 Go 继承的完整指南以获取全面参考。

OOP: GOLANG 继承完整指南

golangbyexample.com/oop-inheritance-golang-complete/embed/#?secret=EpVuUFYEMN#?secret=OtvYyDmPmR

Go 通过嵌入结构体或使用接口来支持继承。实现方式不同,每种方式都有一些限制。这些不同的方法包括:

  1. 通过使用嵌入结构体 – 父结构体嵌入在子结构体中。这个方法的限制是无法进行子类型化。你不能将子结构体传递给期望基础结构体的函数。有关更多详细信息,请参考这个链接 – 使用结构体的继承

  2. 通过使用接口 – 可以进行子类型化,但限制是无法引用公共属性。有关更多详细信息,请参考这个链接 – 使用接口的继承

  3. 通过使用接口 + 结构体 – 这解决了上述两种方法的限制,但一个限制是无法重写方法。不过有变通办法。当前文章描述了这种方法。

详细信息:

在这种方法中,基础结构体嵌入在子结构体中,基础结构体实现了所有公共接口的方法。因此,子结构体可以:

  1. 访问基础结构体的方法和属性

  2. 由于基础结构体实现了公共接口的所有函数,公共接口本身可以用于子类型化。

package main

import "fmt"

type iBase interface {
	say()
}

type base struct {
	value string
}

func (b *base) say() {
	fmt.Println(b.value)
}

type child struct {
	base  //embedding
	style string
}

func check(b iBase) {
	b.say()
}

func main() {
	base := base{value: "somevalue"}
	child := &child{
		base:  base,
		style: "somestyle",
	}
	child.say()
	check(child)
} 

输出:

somevalue
somevalue 

使用接口的 GO 中的继承

来源:golangbyexample.com/inheritance-go-interface/

本文描述了使用接口的继承。请访问我们的 Go 中的继承完整指南文章以获取完整参考

OOP:GOLANG 中的继承完整指南

golangbyexample.com/oop-inheritance-golang-complete/embed/#?secret=lwzs553n8n#?secret=ZXUMNIsae2

Go 通过嵌入结构体或使用接口支持继承。有不同的实现方式,每种方式都有一些限制。这些不同的方式是:

  1. 通过使用嵌入式结构体——父结构体嵌入到子结构体中。这个方法的限制是无法进行子类型化。你不能将子结构体传递给期望基类的函数。更多详情请参见此链接——使用结构体的继承

  2. 通过使用接口——子类型化是可能的,但限制是无法引用公共属性。当前的文章描述了这种方法

  3. 通过使用接口 + 结构体——这解决了上述两种方法的限制,但一个限制是无法重写方法。不过有一个变通方法。更多详情请参见此链接——使用接口 + 结构体的继承

详情:

子结构体实现公共接口的方法。这个方法也解决了子类型化的问题。见以下代码

package main

import "fmt"

type iBase interface {
	say()
}

type child struct {
	style string
}

func (b *child) say() {
	fmt.Println(b.style)
}

func check(b iBase) {
	b.say()
}

func main() {
	child := &child{
		style: "somestyle",
	}
	child.say()
	check(child)
} 

输出:

somestyle
somestyle

限制:

这种方法的限制在于,无法引用公共属性,因为接口不能有任何属性。这个问题通过使用结构体 + 接口的混合方法得到解决。

使用结构体(嵌入)的 GO 中的继承

来源:golangbyexample.com/inheritance-go-struct/

本文仅描述使用结构体的继承。请查看我们的《Go 语言中的继承完整指南》以获取完整参考。

面向对象:GOLANG 中的继承完整指南

golangbyexample.com/oop-inheritance-golang-complete/embed/#?secret=z8WWaJ1OiI#?secret=zH9oZkEbS3

Go 支持通过嵌入结构体或使用接口进行继承。实现方式不同,每种方式都有一些限制。不同的方式有:

  1. 通过使用嵌入结构体 – 父结构体嵌入到子结构体中。这个方法的限制是子类型不可能。你不能将子结构体传递给期望基结构体的函数。当前帖子描述了这种方法。

  2. 通过使用接口 – 子类型是可能的,但限制是无法引用公共属性。有关更多详情,请参考此链接 – 使用接口的继承

  3. 通过使用接口 + 结构体 – 这解决了上述两种方法的限制,但一个限制是无法重写方法。不过有一个解决方法。有关更多详情,请参考此链接 – 使用接口 + 结构体的继承

详情:

在使用结构体的继承中,基结构体被嵌入到子结构体中,基属性和方法可以直接在子结构体上调用。请参见下面的代码:

package main

import "fmt"

type base struct {
	value string
}

func (b *base) say() {
	fmt.Println(b.value)
}

type child struct {
	base  //embedding
	style string
}

func check(b base) {
	b.say()
}

func main() {
	base := base{value: "somevalue"}
	child := &child{
		base:  base,
		style: "somestyle",
	}
	child.say()
	//check(child)
} 

输出:

somevalue

限制:

不支持子类型。你不能将子结构体传递给期望基结构体的函数。

例如,在上述代码中,如果你取消注释//check(child),将会出现编译错误:“无法将 child(类型*child)作为参数传递给 check,期望类型为 base”。要解决此问题,我们可以通过接口进行继承。

Go 中的 Init 函数 (Golang)

来源:golangbyexample.com/init-function-golang/

目录

  • 概述

  • 主包中的 Init 函数

  • 同一包中不同源文件的多个 init() 函数 function in different source file of same package")

    • 同一源文件中的多个 init() 函数 function in same source file")
  • 程序执行顺序

概述

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

  • Init 函数是可选的。

  • Init 函数不接受任何参数

  • Init 函数没有返回值。

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

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

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

假设您在应用程序中使用 redis 缓存。您希望在应用程序首次启动时创建 redis 客户端。初始化 redis 客户端的一个好地方是在 init() 函数中。让我们看一个例子。

创建一个名为 redisexample 的模块,并将以下文件放入该模块中。

go.mod

module redisexample

go 1.14

redis/redis.go

package redis

import (
	"encoding/json"
	"time"

	"github.com/go-redis/redis"
)

type redisClient struct {
	c *redis.Client
}

var (
	client = &redisClient{}
)

func init() {
	c := redis.NewClient(&redis.Options{
		Addr: "127.0.0.1:6379",
	})

	if err := c.Ping().Err(); err != nil {
		panic("Unable to connect to redis " + err.Error())
	}
	client.c = c
}

func GetKey(key string, src interface{}) error {
	val, err := client.c.Get(key).Result()
	if err == redis.Nil || err != nil {
		return err
	}
	err = json.Unmarshal([]byte(val), &src)
	if err != nil {
		return err
	}
	return nil
}

//SetKey set key
func SetKey(key string, value interface{}, expiration time.Duration) error {
	cacheEntry, err := json.Marshal(value)
	if err != nil {
		return err
	}
	err = client.c.Set(key, cacheEntry, expiration).Err()
	if err != nil {
		return err
	}
	return nil
}

main.go

package main

import (
	"fmt"
	"log"
	"time"

	"redisexample/redis"
)

func main() {
	err := redis.SetKey("a", "b", time.Minute*1)
	if err != nil {
		log.Fatalf("Error: %v", err.Error())
	}

	var value string
	err = redis.GetKey("a", &value)
	if err != nil {
		log.Fatalf("Error: %v", err.Error())
	}

	fmt.Println(value)

}

输出

b

在上述示例中,我们在 redis.go 文件中有一个 init 函数。在 init 函数中,我们初始化了 redis 全局客户端,该客户端在 main.go 的 main 函数中使用。

主包中的 Init 函数

main 包也可以包含 init 函数。init 函数将在 main 函数之前触发。

package main

import "fmt"

var sample int

func init() {
	fmt.Println("In Init function")
	sample = 4
}

func main() {
	fmt.Println("Main started")
	fmt.Println(sample)
	fmt.Println("Main Ended")
}

输出

In Init function
Main started
4
Main Ended

在上述程序中,我们有一个 init 函数和一个 main 函数。在 init 函数中,我们初始化了全局变量“sample”。注意执行顺序,即 init 函数首先执行,然后执行 main 函数。

同一包中不同源文件的多个 init() 函数

init()函数在属于某个包的所有源文件中被调用(如果存在)。让我们看一个例子。

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"

	"github.com/learn/math"
)

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

输出

In add init
In subtract init
3
1

同一源文件中的多个 init()函数

同一源文件中可以有多个 init 函数。

package main

import "fmt"

var sample int

func init() {
	fmt.Println("Init1")
}

func init() {
	fmt.Println("Init2")
}

func main() {
	fmt.Println("Main started")
	fmt.Println("Main Ended")
}

输出

Init1
Init2
Main started
Main Ended

在上述程序中,我们有一个 init 函数和一个 main 函数。在 init 函数中,我们初始化全局变量“sample”。注意执行顺序,即 init 函数先执行,然后 main 函数执行。

程序的执行顺序

现在如上所述,让我们看看 go 程序的执行顺序。

  • 程序从主包开始。

  • 主包源文件中导入的所有包都会被初始化。进一步导入的包同样会递归地执行。

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

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

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

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

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

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

例如,如果主包导入包a,而包a又导入包b,则执行顺序如下:

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

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

  • main包中的全局变量将被初始化。主包源文件中的 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)中的 defer 中的内联函数

来源:golangbyexample.com/inline-function-defer-go/

目录

  • 概述

  • 示例

概述

也可以有一个带有 defer 的内联函数。

示例

让我们看看一个示例。

package main

import "fmt"

func main() {
    defer func() { fmt.Println("In inline defer") }()
    fmt.Println("Executed")
}

输出

Executed
In inline defer

在上面的代码中,我们使用了一个内联函数的 defer。

defer func() { fmt.Println("In inline defer") }()

这在 Go 中是允许的。还要注意,函数后面必须添加“()”,否则编译器会报错。

expression in defer must be function call

从输出中可以看到,内联函数在主函数的所有内容执行完毕后被调用,并在主函数返回之前调用。这就是原因。

Executed in main

在主函数执行之前打印了。

In inline Defer

Golang 中通道的内部工作原理

来源:golangbyexample.com/inner-working-of-channels-in-golang/

介绍

本文的目的是让人们了解通道的内部工作原理。Golang 有两种并发原语:

  1. 协程 – 轻量级独立执行以实现并发/并行。

  2. 通道 – 提供协程之间的同步和通信。

通道是协程安全的,并以 FIFO 方式管理协程之间的通信。协程可以在通道上阻塞,进行数据的发送或接收,唤醒被阻塞的协程是通道的责任。

通道类型

缓冲通道

  • 仅在缓冲区已满时,发送到缓冲通道才会阻塞。

  • 如果通道为空,接收将被阻塞。

未缓冲通道

  • 在通道上发送将被阻塞,除非有另一个协程来接收。

  • 接收将被阻塞,直到另一协程在另一侧发送数据。

HCHAN 结构

让我们了解创建通道时内部发生的事情。通道在内部由hchan结构表示,其主要元素为:

type hchan struct {
    qcount   uint           // total data in the queue
    dataqsiz uint           // size of the circular queue
    buf      unsafe.Pointer // points to an array of dataqsiz elements
    elemsize uint16
    closed   uint32         // denotes weather channel is closed or not
    elemtype *_type         // element type
    sendx    uint           // send index
    recvx    uint           // receive index
    recvq    waitq          // list of recv waiters
    sendq    waitq          // list of send waiters
    lock     mutex
}

type waitq struct {
   first *sudog
   last  *sudog
}

结构体sudog的主要元素如下所示:

type sudog struct {
   g     *g             //goroutine
   elem  unsafe.Pointer // data element 
   ...
}

让我们深入了解通道发送和接收时发生的事情

在通道上发送

  1. 没有接收者/接收者等待:未缓冲通道或在缓冲通道的情况下缓冲区已满。

  2. 接收器/接收者等待:未缓冲通道或在缓冲通道的情况下缓冲区为空

  3. 缓冲区为空:在缓冲通道的情况下

  4. 通道已关闭

1. 没有接收者/接收者等待:

在以下两种情况下,当没有接收者等待时,行为将是相同的。

  • 缓冲通道:缓冲区已满

  • 未缓冲通道

尝试向通道发送的协程G1,其执行被暂停,仅在接收后恢复。让我们看看这如何发生。

  • 它创建一个sudog对象,g即协程本身,elem指向它想放入缓冲区的数据。

  • 然后将该 sudog 结构添加到发送队列 sendq 中。

  • 协程调用“GOPARK”到 Go 运行时。作为响应,Go 运行时将该 G1 的状态更改为等待。

2. 接收者等待

在以下两种情况下,当有接收者等待时,行为将是相同的

  • 缓冲通道:缓冲区为空

  • 未缓冲通道

让我们看看这如何发生。

  • 协程 G1 从receq中出队,然后将数据直接传递给接收协程。

  • 将接收协程的状态设置为可运行

3. 缓冲区未满:

仅适用于缓冲通道:缓冲区至少有一个空位

  • 将数据写入缓冲区

4. 通道已关闭:

  • 恐慌

从通道接收

  1. 没有发送者/发送者等待:未缓冲通道或在缓冲通道的情况下缓冲区为空

  2. 发送者等待: 未缓冲通道或缓冲通道的缓冲区为空。

  3. 非空缓冲区: 在缓冲通道的情况下,通道至少有 1 个项目。

  4. 通道关闭

1. 没有发送者等待:

对于下面两种情况,当没有接收者等待时,行为将是相同的。

  • 缓冲通道:缓冲区为空

  • 无缓冲通道

尝试接收的 goroutine G1,其执行被暂停,仅在发送后恢复。让我们看看这是如何发生的。

  • 该 goroutine 创建一个sudog对象,自己作为 goroutine,元素为空

  • 然后将该sudog结构添加到等待发送队列 recvq 中

  • 该 goroutine 调用“GOPARK”到 Go 运行时。作为回应,Go 运行时将该 goroutine 的状态更改为等待

2. 发送者/发送者等待:

  • 从缓冲区出队列元素并复制到自身

  • sendq出队列,然后将数据直接复制到缓冲区。

  • 将发送 goroutine 的状态设置为可运行

3. 非空缓冲区:

仅适用于缓冲通道:

  • 该 goroutine 从缓冲区读取数据

4. 通道关闭:

  • 从通道接收数据类型的默认值

  • sidetoc

Go(Golang)中接口的内部工作或内部机制

来源:golangbyexample.com/inner-working-interface-golang/

目录

** 概览

  • 代码

概览

和其他变量一样,接口变量由类型和值表示。接口值在内部由两个元组组成

  • 底层类型

  • 底层值

请参见下图,说明我们上述提到的内容

![](https://gitee.com/OpenDocCN/geekdoc-golang-zh/raw/master/docs/go-exam-2023/img/e09464771d53171817cfeecf9da6a93e.png)

让我们看看一个例子,然后为该例子创建一个类似于上述的图表。

假设我们有一个接口动物如下

type animal interface {
    breathe()
    walk()
}

我们还有一个实现了这个动物接口的狮子结构

type lion struct {
    age int
}

代码

package main

import "fmt"

type animal interface {
    breathe()
    walk()
}

type lion struct {
    age int
}

func (l lion) breathe() {
    fmt.Println("Lion breathes")
}

func (l lion) walk() {
    fmt.Println("Lion walk")
}

func main() {
    var a animal
    a = lion{age: 10}
    a.breathe()
    a.walk()
}

输出

Lion breathes
Lion walk

对于上述案例,狮子结构实现动物接口如下所示

![](https://gitee.com/OpenDocCN/geekdoc-golang-zh/raw/master/docs/go-exam-2023/img/c9cc4eb8c2e7148fd7877e127d773ca6.png)

Golang 提供格式标识符来打印由接口值表示的底层类型和底层值。

  • %T 可以用于打印接口值的具体类型

  • %v 可以用于打印接口值的具体值。

package main

import "fmt"

type animal interface {
    breathe()
    walk()
}

type lion struct {
    age int
}

func (l lion) breathe() {
    fmt.Println("Lion breathes")
}

func (l lion) walk() {
    fmt.Println("Lion walk")
}

func main() {
    var a animal
    a = lion{age: 10}
    fmt.Printf("Underlying Type: %T\n", a)
    fmt.Printf("Underlying Value: %v\n", a)
}

输出

Concrete Type: main.lion
Concrete Value: {10}

Go 语言中的二叉树中序遍历

来源:golangbyexample.com/inorder-binary-tree-golang/

目录

  • 概述

  • 程序

概述

在二叉树的中序遍历中,我们遵循以下顺序

  • 访问左子树

  • 访问根节点

  • 访问右子树

例如,假设我们有如下的二叉树

![](https://gitee.com/OpenDocCN/geekdoc-golang-zh/raw/master/docs/go-exam-2023/img/9a9347838908483552b24df3dc54cd38.png)

那么中序遍历将是

[4 2 1 5 3 6]

程序

下面是相应的程序

package main

import (
	"fmt"
)

type TreeNode struct {
	Val   int
	Left  *TreeNode
	Right *TreeNode
}

func inorderTraversal(root *TreeNode) []int {
	if root == nil {
		return nil
	}

	left := inorderTraversal(root.Left)
	right := inorderTraversal(root.Right)

	output := make([]int, 0)

	output = append(output, left...)
	output = append(output, root.Val)
	output = append(output, right...)
	return output

}

func main() {
	root := TreeNode{Val: 1}
	root.Left = &TreeNode{Val: 2}
	root.Left.Left = &TreeNode{Val: 4}
	root.Right = &TreeNode{Val: 3}
	root.Right.Left = &TreeNode{Val: 5}
	root.Right.Right = &TreeNode{Val: 6}

	output := inorderTraversal(&root)
	fmt.Println(output)

}

输出

[4 2 1 5 3 6]

注意: 请查看我们的 Golang 高级教程。本系列教程内容丰富,我们尝试用例子涵盖所有概念。本教程适合那些希望获得 Golang 专业知识和扎实理解的人 – Golang 高级教程

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

Go 语言中的插入排序

来源:golangbyexample.com/insertion-sort-in-go/

目录

** 介绍

  • 时间复杂度

  • 空间复杂度

  • 实现:

介绍

插入排序是最简单的排序算法之一。在插入排序中,输入数组被分为两部分。

  1. 已排序

  2. 未排序

  • 最初的已排序部分仅包含数组的第一个元素。

  • 在每一步中,我们从未排序部分选择下一个元素,并将其插入到已排序部分的正确位置。

时间复杂度

  • 最坏情况 - O(n*n)

  • 最好情况 - O(n) – 当数组已经排序时

空间复杂度

插入排序的空间复杂度是 O(1)

实现:

package main

import "fmt"

func main() {
    sample := []int{3, 4, 5, 2, 1}
    insertionSort(sample)

    sample = []int{3, 4, 5, 2, 1, 7, 8, -1, -3}
    insertionSort(sample)
}

func insertionSort(arr []int) {
    len := len(arr)
    for i := 1; i < len; i++ {
        for j := 0; j < i; j++ {
            if arr[j] > arr[i] {
                arr[j], arr[i] = arr[i], arr[j]
            }
        }
    }

    fmt.Println("After Sorting")
    for _, val := range arr {
        fmt.Println(val)
    }
}

输出:

After Sorting
1
2
3
4
5

After Sorting
-3
-1
1
2
3
4
5
7
8

在 Linux 上安装 GO(也称为 Golang)

来源:golangbyexample.com/golang-linux-installation/

GO 可在 Windows、Mac 和 Linux 平台上安装。让我们看看 Linux 的安装设置。

安装

  • 从这里下载最新版本的 GO 的压缩包 – golang.org/dl/。下载后在/usr/local.位置解压。你也可以运行以下命令来解压。
tar -C /usr/local -xzf go$VERSION.$OS-$ARCH.tar.gz
  • 解压后,下面的路径包含 GO 二进制文件‘/usr/local/go/bin’。你必须将此位置添加到你的.bashrc 中。打开你的.bashrc 并进行以下条目。如果文件尚不存在,请创建它。
export PATH=$PATH:/usr/local/go/bin

测试安装:

  • 重新启动终端,并在终端中输入命令which go。它将输出/usr/local/go/bin/go. 这是 GO 二进制文件的位置。

  • 尝试运行‘go version’命令。它将输出当前的 GO 版本。

  • 尝试运行‘go’命令。它将输出。

Go is a tool for managing Go source code.

Usage:

go <command></command> [arguments]

The commands are:

bug         start a bug report
build       compile packages and dependencies
clean       remove object files and cached files
doc         show documentation for package or symbol
env         print Go environment information
fix         update packages to use new APIs
fmt         gofmt (reformat) package sources
generate    generate Go files by processing source
get         add dependencies to current module and install them
install     compile and install packages and dependencies
list        list packages or modules
mod         module maintenance
run         compile and run Go program
test        test packages
tool        run specified go tool
version     print Go version
vet         report likely mistakes in packages

Use "go help <command></command>" for more information about a command.

Additional help topics:

buildmode   build modes
c           calling between Go and C
cache       build and test caching
environment environment variables
filetype    file types
go.mod      the go.mod file
gopath      GOPATH environment variable
gopath-get  legacy GOPATH go get
goproxy     module proxy protocol
importpath  import path syntax
modules     modules, module versions, and more
module-get  module-aware go get
module-auth module authentication using go.sum
module-private module configuration for non-public modules
packages    package lists and patterns
testflag    testing flags
testfunc    testing functions

Use "go help <topic>" for more information about that topic.</topic>

卸载

要卸载,请执行以下两个步骤。

  • 运行以下命令以删除文件。这需要 sudo 权限。
rm -rf /usr/local/go
  • 从.bashrc 文件中删除以下条目。
export PATH=$PATH:/usr/local/go/bin

在 MAC 上安装 GO(也称为 Golang)

来源:golangbyexample.com/golang-mac-installation/

目录

  • 概述

  • 使用 .pkg 安装程序

    • 安装

    • 测试安装:

    • 卸载

  • 使用压缩包

    • 安装

    • 测试安装

    • 卸载

  • 使用 brew

    • 安装

    • 测试安装

    • 卸载

概述

GO 可以在 Win、Mac 和 Linux 平台上安装。可以通过三种方式在 MAC 上安装 GO。

  • 使用压缩包

  • 使用 brew

  • 使用 .pkg 安装程序

让我们看看这三种方式。

使用 .pkg 安装程序

安装

  • 从这里下载 MAC pkg 安装程序 – golang.org/dl/。双击 .pkg 文件并按照屏幕上的说明进行操作。完成后,GO 将安装在以下目录中。
/usr/local/go
  • 安装程序还会将 ‘/usr/local/go/bin’ 添加到你的环境 PATH 变量中。这是 GO 二进制文件所在的目录。重新启动终端以使更改生效。

测试安装:

  • 重新启动终端并在终端中输入命令 ‘which go’。它将输出 /usr/local/go/bin/go. 这是 GO 二进制文件的位置。

  • 尝试运行 ‘go version’ 命令。它将输出当前的 GO 版本。

卸载

要卸载,请执行以下两个步骤:

rm -rf /usr/local/go      //Will require sudo permission
rm -rf /etc/paths.do/go   //Will require sudo permission. This action deletes will remove /usr/local/go/bin from PATH env

使用压缩包

安装

  • 从这里下载最新版本的 GO 压缩包 – golang.org/dl/。下载后在 /usr/local 位置解压。你可以运行以下命令进行解压。
tar -C /usr/local -xzf go$VERSION.$OS-$ARCH.tar.gz
  • 解压后,以下路径将包含 GO 二进制文件 ‘/usr/local/go/bin’。你必须将此位置添加到你的 .bashrc。打开你的 .bashrc 并进行以下条目
export PATH=$PATH:/usr/local/go/bin

测试安装

  • 重新启动终端并在终端中输入命令 ‘which go’。它将输出 /usr/local/go/bin/go. 这是 GO 二进制文件的位置。

  • 尝试运行 ‘go version’ 命令。它将输出当前的 GO 版本。

  • 尝试运行 ‘go’ 命令。它将输出。

Go is a tool for managing Go source code.

Usage:

go <command></command> [arguments]
.....

卸载

要卸载,请执行以下两个步骤。

  • 运行以下命令以删除文件。这将需要 sudo 权限。
rm -rf /usr/local/go 
  • .bashrc 文件中删除以下条目。
export PATH=$PATH:/usr/local/go/bin

使用 brew

安装

在 MAC 上安装 GO 的最简单方法是使用 brew。

brew install go

测试安装

  • 重新启动终端并在终端中输入命令 ‘which go’。它将输出 /usr/local/go/bin/go. 这是 GO 二进制文件的位置。

  • 尝试运行 ‘go version’ 命令。它将输出当前的 GO 版本。

卸载

要卸载,只需运行命令。

brew uninstall go

在 Windows 上安装 GO(又名 Golang)

来源:golangbyexample.com/golang-windows-installation/

GO 可以在 Win、Mac 和 Linux 平台上安装。让我们来看一下 Windows 的安装设置。

安装

  • 从这里下载.msi 安装程序 – golang.org/dl/。双击.msi 文件并按照屏幕上的指示进行操作。一旦完成,GO 将安装在以下目录中。
c:\Go
  • 安装程序还会将‘c:\Go\bin’目录添加到你的 PATH 环境变量中。这是 GO 二进制文件所在的目录。你需要重新启动命令提示符以使更改生效。

测试安装

  • 尝试运行‘go version’命令。它会输出当前的 GO 版本。

  • 也尝试运行‘go’命令。它将输出

Go is a tool for managing Go source code.

Usage:

go <command></command> [arguments]

The commands are:

bug         start a bug report
build       compile packages and dependencies
clean       remove object files and cached files
doc         show documentation for package or symbol
env         print Go environment information
fix         update packages to use new APIs
fmt         gofmt (reformat) package sources
generate    generate Go files by processing source
get         add dependencies to current module and install them
install     compile and install packages and dependencies
list        list packages or modules
mod         module maintenance
run         compile and run Go program
test        test packages
tool        run specified go tool
version     print Go version
vet         report likely mistakes in packages

Use "go help <command></command>" for more information about a command.

Additional help topics:

buildmode   build modes
c           calling between Go and C
cache       build and test caching
environment environment variables
filetype    file types
go.mod      the go.mod file
gopath      GOPATH environment variable
gopath-get  legacy GOPATH go get
goproxy     module proxy protocol
importpath  import path syntax
modules     modules, module versions, and more
module-get  module-aware go get
module-auth module authentication using go.sum
module-private module configuration for non-public modules
packages    package lists and patterns
testflag    testing flags
testfunc    testing functions

Use "go help <topic>" for more information about that topic.</topic>

卸载

要卸载,请执行以下两个步骤。

Go (Golang) 中的接口比较

来源:golangbyexample.com/interface-comparison-golang/

为了理解两个接口变量是否相等,我们首先需要了解接口的内部表示。像其他变量一样,接口变量由类型和值表示。接口变量的值,实际上包含两个元组

  • 基本类型

  • 基本值

请参见下面的图示,说明我们上面提到的内容

![](https://gitee.com/OpenDocCN/geekdoc-golang-zh/raw/master/docs/go-exam-2023/img/e09464771d53171817cfeecf9da6a93e.png)

两个接口可比较,如果任一

  • 接口值为 nil 或

  • 基本类型相同且可比较。基本值也是相同的

根据 Go 规范,一些可比较的类型包括

  • 布尔型

  • 数值型

  • 字符串,

  • 指针

  • 通道

  • 接口类型

  • 结构体 – 如果它的所有字段类型都是可比较的

  • 数组 – 如果数组元素的值类型是可比较的

根据 Go 规范,一些不可比较的类型

  • 切片

  • 映射

  • 函数

两个接口变量可以使用==!=运算符进行比较

让我们看看一个程序

package main

import "fmt"

type animal interface {
	breathe()
	walk()
}

type lion struct {
	age int
}

func (l lion) breathe() {
	fmt.Println("Lion breathes")
}

func (l lion) walk() {
	fmt.Println("Lion walk")
}

func main() {
	var a animal
	var b animal
	var c animal
	var d animal
	var e animal

	a = lion{age: 10}
	b = lion{age: 10}
	c = lion{age: 5}

	if a == b {
		fmt.Println("a and b are equal")
	} else {
		fmt.Println("a and b are not equal")
	}

	if a == c {
		fmt.Println("a and c are equal")
	} else {
		fmt.Println("a and c are not equal")
	}

	if d == e {
		fmt.Println("d and e are equal")
	} else {
		fmt.Println("d and e are not equal")
	}
}

输出

a and b are equal
a and c are not equal
d and e are equal

在上述程序中,我们有动物接口,lion结构体通过定义其两个方法来实现动物接口。

接口变量ab相等,因为

  • 基本类型相同,即lion结构体

  • 基本类型是可比较的,即lion结构体只有一个int类型的字段,而int类型是可比较的

  • 基本值相同,即lion.age对于两者都是 10

前两点同样适用于比较 a 和 c,但它们不相等,因为

  • 基本值不相同,即lion.age对于a是 10,而对于c是 5

接口变量 d 和 e 相等,因为

*** 接口值对两者都是 nil

posted @ 2024-10-19 08:37  绝不原创的飞龙  阅读(1)  评论(0编辑  收藏  举报