Go语言入门

2024/1/25 基础语法

# 语言结构

# 1、基础组成

  • 包声明
  • 引入包
  • 函数
  • 变量
  • 语句 & 表达式
  • 注释
package main    // Go程序的包声明。Go程序由多个包组成

import "fmt"    // fmt包,它提供了格式化输入和输出的功能

func main() {   // 程序的主函数,也是程序的入口点
 fmt.Println("hello world")
}
1
2
3
4
5
6
7

让我们来看下以上程序的各个部分:

  1. 第一行代码 package main 定义了包名。你必须在源文件中非注释的第一行指明这个文件属于哪个包,如:package main。package main表示一个可独立执行的程序,每个 Go 应用程序都包含一个名为 main 的包。
  2. 下一行 import "fmt" 告诉 Go 编译器这个程序需要使用 fmt 包(的函数,或其他元素),fmt 包实现了格式化 IO(输入/输出)的函数。
  3. 下一行 func main() 是程序开始执行的函数。main 函数是每一个可执行程序所必须包含的,一般来说都是在启动后第一个执行的函数(如果有 init() 函数则会先执行该函数)。
  4. 下一行 /.../ 是注释,在程序执行时将被忽略。单行注释是最常见的注释形式,你可以在任何地方使用以 // 开头的单行注释。多行注释也叫块注释,均已以 /* 开头,并以 */ 结尾,且不可以嵌套使用,多行注释一般用于包的文档描述或注释成块的代码片段。
  5. 下一行 fmt.Println(...) 可以将字符串输出到控制台,并在最后自动增加换行字符 \n。 使用 fmt.Print("hello, world\n") 可以得到相同的结果。 Print 和 Println 这两个函数也支持使用变量,如:fmt.Println(arr)。如果没有特别指定,它们会以默认的打印格式将变量 arr 输出到控制台。
  6. 当标识符(包括常量、变量、类型、函数名、结构字段等等)以一个大写字母开头,如:Group1,那么使用这种形式的标识符的对象就可以被外部包的代码所使用(客户端程序需要先导入这个包),这被称为导出(像面向对象语言中的 public);标识符如果以小写字母开头,则对包外是不可见的,但是他们在整个包的内部是可见并且可用的(像面向对象语言中的 protected )。

# 2、执行 Go 程序

让我们来看下如何编写 Go 代码并执行它。步骤如下:

  1. 打开编辑器如Sublime2,将以上代码添加到编辑器中。

  2. 将以上代码保存为 hello.go

  3. 打开命令行,并进入程序文件保存的目录中。

  4. 输入命令 go run hello.go 并按回车执行代码。

  5. 如果操作正确你将在屏幕上看到 "Hello World!" 字样的输出。

    $ go run hello.go
    Hello, World!
    
    1
    2
  6. 我们还可以使用 go build 命令来生成二进制文件:

    $ go build hello.go 
    $ ls
    hello    hello.go
    $ ./hello 
    Hello, World!
    
    1
    2
    3
    4
    5

注意:需要注意的是 { 不能单独放在一行,所以以下代码在运行时会产生错误。

# 语言基础语法

Go 程序可以由多个标记组成,可以是关键字,标识符,常量,字符串,符号。

# 1、关键字

下面列举了 Go 代码中会使用到的 25 个关键字或保留字:

break default func interface select
case defer go map struct
chan else goto package switch
const fallthrough if range type
continue for import return var

除了以上介绍的这些关键字,Go 语言还有 36 个预定义标识符:

append bool byte cap close complex complex64 complex128 uint16
copy false float32 float64 imag int int8 int16 uint32
int32 int64 iota len make new nil panic uint64
print println real recover string true uint uint8 uintptr

程序一般由关键字、常量、变量、运算符、类型和函数组成。

程序中可能会使用到这些分隔符:括号 (),中括号 [] 和大括号 {}。

程序中可能会使用到这些标点符号:.,;:

# 2、标识符

标识符用来命名变量、类型等程序实体。一个标识符实际上就是一个或是多个字母(A~Z和a~z)数字(0~9)、下划线_组成的序列,但是第一个字符必须是字母或下划线而不能是数字。

以下是有效的标识符:

mahesh   kumar   abc   move_name   a_123
myname50   _temp   j   a23b9   retVal
1
2

以下是无效的标识符:

1ab(以数字开头)
case(Go 语言的关键字)
a+b(运算符是不允许的)
1
2
3

# 3、格式化字符串

Go 语言中使用 fmt.Sprintffmt.Printf 格式化字符串并赋值给新串:

  • Sprintf 根据格式化参数生成格式化的字符串并返回该字符串。

    package main
    
    import (
        "fmt"
    )
    
    func main() {
       // %d 表示整型数字,%s 表示字符串
        var stockcode=123
        var enddate="2020-12-31"
        var url="Code=%d&endDate=%s"
        var target_url=fmt.Sprintf(url,stockcode,enddate)
        fmt.Println(target_url)
    }
    
    //Code=123&endDate=2020-12-31
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
  • Printf 根据格式化参数生成格式化的字符串并写入标准输出。

    package main
    
    import (
        "fmt"
    )
    
    func main() {
       // %d 表示整型数字,%s 表示字符串
        var stockcode=123
        var enddate="2020-12-31"
        var url="Code=%d&endDate=%s"
        fmt.Printf(url,stockcode,enddate)
    }
    
    //Code=123&endDate=2020-12-31
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15

# 数据类型

定义变量之前我们需要先了解在Go语言中提供了几种基本数据类型:

# 1)布尔类型

布尔型的值只可以是常量 true 或者 false。一个简单的例子:var b bool = true。

# 2)数字类型

整型 int 和浮点型 float32、float64,Go 语言支持整型和浮点型数字,并且支持复数,其中位的运算采用补码

# 3)字符串类型

string,字符串就是一串固定长度的字符连接起来的字符序列。Go 的字符串是由单个字节连接起来的。Go 语言的字符串的字节使用 UTF-8 编码标识 Unicode 文本。

# 4)派生类型

  • 指针类型(Pointer Types):用于存储变量的内存地址。
  • 数组类型(Array Types):表示固定长度的同一类型元素的集合。
  • 结构体类型(Struct Types):表示具有不同类型字段的自定义数据结构。
  • 通道类型(Channel Types):用于在Go协程之间进行通信。
  • 切片类型(Slice Types):表示可变长度的元素序列。
  • 映射类型(Map Types):表示键-值对的集合。
  • 错误类型(Error Type)error,表示错误的接口类型。

# 1、数字类型

Go 也有基于架构的类型,例如:int、uint 和 uintptr。

序号 类型和描述
1 uint8 无符号 8 位整型 (0 到 255)
2 uint16 无符号 16 位整型 (0 到 65535)
3 uint32 无符号 32 位整型 (0 到 4294967295)
4 uint64 无符号 64 位整型 (0 到 18446744073709551615)
5 int8 有符号 8 位整型 (-128 到 127)
6 int16 有符号 16 位整型 (-32768 到 32767)
7 int32 有符号 32 位整型 (-2147483648 到 2147483647)
8 int64 有符号 64 位整型 (-9223372036854775808 到 9223372036854775807)

# 2、浮点型

序号 类型和描述
1 float32 IEEE-754 32位浮点型数
2 float64 IEEE-754 64位浮点型数
3 complex64 32 位实数和虚数
4 complex128 64 位实数和虚数

# 3、其他数字类型

序号 类型和描述
1 byte 类似 uint8
2 rune 类似 int32
3 uint 32 或 64 位
4 int 与 uint 一样大小
5 uintptr 无符号整型,用于存放一个指针

# 变量与常量

# 1、单变量声明

在 Go 中,有两种方式可以声明变量

  • 使用 var 关键字

    var variablename [type] = value
    
    1

    看到这个有点意思,通过 var 可以想到 JavaScript,通过 variablename type 的类型声明又可以联想到 TypeScript,不同的是少了 :

  • 使用 := 标识符

    variablename := value
    
    1

    在这种情况下,变量的类型是从值推断出来的(意味着编译器根据值决定变量的类型)

  • 示例

    package main
    
    import ("fmt")
    
    func main(){
        //类型是sting
        var name string = "咸鱼"
        //类型是inferred(编译器自己推断)
        var name2 ="咸鱼"
        //类型是inferred(编译器自己推断)
        name3:=2
        
        fmt.Println(name)
        fmt.Println(name2)
        fmt.Println(name3)
    }
    
    //打印出来为
    咸鱼
    咸鱼
    2
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21

# 2、多变量声明

在 Go 中,可以在同一行中声明多个变量。

package main

import ("fmt")

func main(){
    var a,b,c,d int = 1,3,5,7
    
    fmt.Println(a)
    fmt.Println(b)
    fmt.Println(c)
    fmt.Println(d)
}

//打印出来为
1
3
5
7
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 3、常量声明

const variablename [type] = value
1

关键字是const,可以省略类型说明符[type],因为编译器可以根据变量的值来推断其类型。

  • 显式类型定义:const b sting = "abc"

  • 隐式类型定义:const b = "abc"

# 4、常量枚举

常量表达式中,函数必须是内置函数

const (
	a = "abc"
    //string的底层是通过byte数组实现的,test=“abc”为3个字节
    b = len(a)
    //unsafe.Sizeof返回的是数据类型的大小,而string在Go中并不是直存类型,它是一个结构体类型
    //type StringHeader struct {
    //   Data uintptr
    //   Len  int
    //}
    //在64位系统上 uintptr与int都是8字节 8+8=16,因此结果为16
    c = unsafe.Sizeof(a)
)
1
2
3
4
5
6
7
8
9
10
11
12

# 控制流程语句

# 1、if 语句

需要注意的是 条件 不需要使用 () 包裹

if condition {
}else if condition{
}else{}
1
2
3

示例

package main

import ("fmt")

func main(){
    time := 22
    if time < 10 {
        fmt.Println("Good morning")
    }else if time < 20 {
        fmt.Println("Good afternoon.")
    }else {
        fmt.Println("Good evening")
    }
}

//控制台打印输出 Good evening
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 2、switch 语句

使用 switch 语句来选择要执行的多个代码块之一。Go 中的语句类似于 C、C++、Java、JavaScript 和 PHP 中的 switch 语句。不同之处在于它只运行对应匹配的代码块,因此不需要 break 语句。

package main

import("fmt")

func main(){
    time := 12
    switch time{
        case 7:
        	fmt.Println("Good Moning")
        case 12 :
        	fmt.Println("Good afternoon")
        case 20 :
       		fmt.Println("Good evening")
    }
}

//控制台打印输出 Good afternoon
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

使用须知:

  • 表达式只计算一次
  • switch 将表达式的值与每个 case 表达式的值进行比较
  • 如果存在匹配项,则执行关联的代码块
  • 关键字 default 是可选的。它指定在没有 case 匹配项时要运行的一些代码

多用例 switch 语句

switch 语句 case 中可以为单个值有多个值:

package main

import("fmt")

func main(){
    time := 16
    switch tiem{
        case 7:
        	fmt.Println("Good moning")
        case 12,16:
        	fmt.Println("Good afternoon")
        case 20,22:
        	fmt.Println("Good evening")
    }
}

//控制台打印输出 Good afternoon
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 3、for 语句

for 循环是 Go 中唯一可用的循环。

 for statement1; statement2; statement3 {  
   ...do something  
}
1
2
3

使用须知:

  • statement1 初始化循环计数器值。
  • statement2 针对每个循环迭代计算。
  • statement3 增加循环计数器值。
package main

import("fmt")

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

//控制台打印输出
0
1
2
3
4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  • continue

    continue 语句用于跳过循环中的一次或多次迭代。然后,它继续循环中的下一次迭代。

  • break

    break 语句用于中断/终止循环执行。

  • range 关键字

    关键字 range 用于更轻松地迭代数组、切片或映射。它同时返回索引和值。

    for index, value := range array|slice|map {  
     ...do something  
    }
    
    1
    2
    3
    package main
    
    import("fmt")
    
    func main(){
        //声明数组
        fruits :=[3]string{"apple","orange","banana"}
        //关键字遍历数组
        for idx,val := range fruits {
            fmt.Println("%v\t%v\n",idx,val)
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12

# 函数

# 1、基本声明

什么是函数?函数是可以在程序中重复使用的语句块。要声明函数,需要执行以下操作:

  • 使用 func 关键字。
  • 指定函数的名称,后跟括号 (),() 内可添加参数列表
  • 最后,在大括号 {} 内添加定义函数应执行的操作的代码。
func FunctionName(param1 type, param2 type, param3 type) {  
 ...do something  
}
1
2
3

示例:

package main

import "fmt"

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

func main(){
    fmt.Println(add(42, 13))
}

//控制台打印输出 55
1
2
3
4
5
6
7
8
9
10
11
12
13

# 2、多值返回

函数中可以返回多个值

func swap(x, y string) (string, string) {  
   return y, x  
}
1
2
3

# 3、连续参数

当两个或多个连续的命名函数参数共享一个类型时,可以省略除最后一个之外的所有类型。

package main

import "fmt"

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

func main() {
    fmt.Println(add(42, 13))
}

//控制台打印输出 55
1
2
3
4
5
6
7
8
9
10
11
12
13

# 数据结构

# 1、数组

数组用于在单个变量中存储相同类型的多个值,而不是为每个值声明单独的变量。

# 1)创建方式

  1. 使用数组字面量初始化:

    array := [size]Type{element1, element2, ...}
    
    1

    这种方式直接使用数组字面量来初始化数组,并提供初始元素。例如:

  2. 声明数组后逐个赋值:

    var array [size]Type
    array[index] = value
    
    1
    2

    这种方式先声明一个数组,然后逐个为数组元素赋值。例如:

    var numbers [2]int
    numbers[0] = 1
    numbers[1] = 2
    
    1
    2
    3
  3. 使用数组初始化表达式:

    array := [...]Type{element1, element2, ...}
    
    1

    这种方式使用 ... 来让编译器根据提供的初始元素数量自动推断数组的大小。例如:

    numbers := [...]int{1, 2, 3, 4, 5}
    
    1

    上述代码将根据提供的 5 个初始元素推断出数组的大小为 5。

需要注意的是,数组的长度在声明后是固定的,无法更改。如果需要动态长度的序列,可以使用切片(slice)类型。

# 2)常见操作

  • len():返回数组的长度,即数组中元素的个数。
  • for循环:可以使用for循环和数组的长度来遍历数组中的元素。
  • 索引访问:通过索引操作符[]来访问和修改数组中的元素。
  • 多维数组:Go语言支持多维数组,可以使用嵌套的方式创建和访问多维数组。

# 2、切片

切片(slice)是一种动态长度的数据结构,它是对底层数组的一种引用。切片提供了一种方便且灵活的方式来操作和管理数据集合。切片由以下三个部分组成:

  1. 指针(Pointer):指向底层数组的第一个元素。
  2. 长度(Length):切片当前包含的元素个数。
  3. 容量(Capacity):底层数组从切片的起始位置到数组末尾的元素个数。

# 1)创建方式

  1. 使用切片字面量初始化:

    slice := []Type{element1, element2, ...}
    
    1

    这种方式直接使用切片字面量来初始化切片,并提供初始元素。例如:

    numbers := []int{1, 2, 3, 4, 5}
    
    1
  2. 使用 make() 函数创建指定长度的切片:

    slice := make([]Type, length)
    
    1

    这种方式使用 make() 函数创建指定长度的切片,初始值为该类型的零值。例如:

    numbers := make([]int, 5)
    
    1

    上述代码将创建一个包含 5 个整数的切片

  3. 使用 make() 函数创建指定长度和容量的切片:

    slice := make([]Type, length, capacity)
    
    1

    这种方式使用 make() 函数创建指定长度和容量的切片。length 表示切片的长度,capacity 表示底层数组的容量,初始值为该类型的零值。例如:

    numbers := make([]int, 5, 10)
    
    1

    上述代码将创建一个长度为 5,容量为 10 的切片。

  4. 通过切片表达式(Slice Expression)从现有切片或数组创建切片:

    slice := existingSliceOrArray[start:end]
    
    1

    这种方式使用切片表达式从现有的切片或数组中创建一个新的切片。start 表示起始索引(包含),end 表示结束索引(不包含)。例如:

    numbers := []int{1, 2, 3, 4, 5}
    subSlice := numbers[1:3]
    
    1
    2

    上述代码将从 numbers 切片中创建一个新的切片 subSlice,包含索引 1 到索引 2 的元素。

# 2)常见操作

切片还有一些内置函数和操作符,可以方便地进行切片的创建、修改和操作,例如:

  • append():用于向切片末尾添加元素,可以动态扩展切片的长度。
  • copy():用于复制切片的内容到另一个切片或数组。
  • len():返回切片的长度。
  • cap():返回切片的容量。

切片是引用类型,多个切片可以共享同一个底层数组。当对一个切片进行修改时,其他共享同一底层数组的切片也会受到影响。

# 3)与数组的差异

使用切片相比数组的优势在于其动态性和灵活性,能够根据需要动态地调整长度,并且可以方便地进行切片的操作和扩展。

  1. 长度的灵活性:数组具有固定的长度,切片具有动态长度
  2. 内存管理:数组在声明时会分配一段连续的内存空间,而切片则是一个引用类型,它包含了指向底层数组的指针、长度和容量。
  3. 传递和赋值行为:数组在传递给函数或赋值给新变量时会进行值拷贝,这意味着会复制整个数组的内容。而切片在传递和赋值时是传递的引用,不会复制底层数组的内容,多个切片可以共享同一个底层数组。
  4. 动态操作:由于切片具有动态长度,可以使用内置函数 appendcopy 来动态扩展切片的长度和复制切片的内容。而数组的长度固定,无法直接进行动态操作,需要创建一个新的数组并进行元素的复制。

在实际的开发中,切片比数组更常用,切片提供了灵活性和便利性,同时可以避免不必要的内存复制。数组通常用于特定的场景,比如需要确切长度和固定内存布局的情况。

# 3、Map

# 1)基本概念

  • Map 是一种无序的键值对的集合。它同时是一种集合,所以我们可以像迭代数组和切片那样迭代它。不过,Map 是无序的,遍历 Map 时返回的键值对的顺序是不确定的。
  • 在获取 Map 的值时,如果键不存在,返回该类型的零值,例如 int 类型的零值是 0,string 类型的零值是 ""。
  • Map 是引用类型,如果将一个 Map 传递给一个函数或赋值给另一个变量,它们都指向同一个底层数据结构,因此对 Map 的修改会影响到所有引用它的变量。

# 2)创建方式

  1. 利用内建函数 make 创建

    //创建一个空的map
    m :=make(map[string]int)
    
    //创建一个初始容量为 10 的map
    m :=make(map[string] int, 10)
    
    1
    2
    3
    4
    5
  2. 使用字面量创建

    //使用字面量创建map
    m :=map[string] int{
        "apple": 1,
        "banana": 2,
        "orange": 3,
    }
    
    1
    2
    3
    4
    5
    6
  3. 获取元素

    // 获取键值对
    v1 := m["apple"]
    v2, ok := m["pear"]  // 如果键不存在,ok 的值为 false,v2 的值为该类型的零值
    
    1
    2
    3
  4. 修改元素

    // 修改键值对
    m["apple"] = 5
    
    1
    2
  5. 获取 Map 的长度

    // 获取 Map 的长度
    len := len(m)
    
    1
    2
  6. 遍历 Map

    // 遍历 Map
    for k, v := range m {
        fmt.Printf("key=%s, value=%d\n", k, v)
    }
    
    1
    2
    3
    4
  7. 删除元素

    // 删除键值对
    delete(m, "banana")
    
    1
    2

    实例

    package main
    
    import "fmt"
    
    func main() {
        var siteMap map[string]string /*创建集合 */
        siteMap = make(map[string]string)
    
        /* map 插入 key - value 对,各个国家对应的首都 */
        siteMap [ "Google" ] = "谷歌"
        siteMap [ "Runoob" ] = "菜鸟教程"
        siteMap [ "Baidu" ] = "百度"
        siteMap [ "Wiki" ] = "维基百科"
    
        /*使用键输出地图值 */ 
        for site := range siteMap {
            fmt.Println(site, "首都是", siteMap [site])
        }
    
        /*查看元素在集合中是否存在 */
        name, ok := siteMap [ "Facebook" ] /*如果确定是真实的,则存在,否则不存在 */
        /*fmt.Println(capital) */
        /*fmt.Println(ok) */
        if (ok) {
            fmt.Println("Facebook 的 站点是", name)
        } else {
            fmt.Println("Facebook 站点不存在")
        }
    }
    
    //以上实例运行结果为:
    //Wiki 首都是 维基百科
    //Google 首都是 谷歌
    //Runoob 首都是 菜鸟教程
    //Baidu 首都是 百度
    //Facebook 站点不存在
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36

# 4、结构体

# 1)基本概念

在 Go 编程语言中,是没有类似于其他面向对象编程语言中的传统对象的概念。

Go 是一种结构化编程语言,它鼓励使用结构体(struct)和方法(method)来组织和抽象代码。

结构体可以包含字段(fields),这些字段可以是不同的数据类型,如整数、字符串、数组等。结构体还可以拥有方法,方法是与结构体关联的函数。

尽管 Go 中没有传统意义上的对象,但可以通过结构体和方法来实现面向对象的编程风格。你可以将结构体看作是数据的容器,方法则可以看作是操作这些数据的函数。通过定义方法,可以在结构体上执行各种操作,从而实现面向对象的封装、继承和多态等概念。

# 2)继承

Go 编程语言中,没有传统意义上的类继承的概念,但是可以通过其他方式实现类似的功能。可以通过 结构体嵌入 来实现一种类似于继承的行为。

# 3)声明方式

结构(结构的缩写)用于将不同数据类型的成员集合创建到单个变量中。数组用于将相同数据类型的多个值存储到单个变量中,而结构用于将不同数据类型的多个值存储到单个变量中。

# 定义
type structName struct {  
  member1 datatype;  
  member2 datatype;  
  member3 datatype;
  ...  
}
1
2
3
4
5
6
package main

import "fmt"

type Vertex struct{
    X int
    Y int
}

func main(){
    v :=Vertex{1,2}
    v.X = 4
    fmt.Println(v.X)
    fmt.Println(v.Y)
    
    var v1 Vertex
    v1.X = 1
    fmt.Println(v1.X)
}

//控制台打印输出 
4
2
1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 使用
  1. 使用 var 关键字var v1 Vertex这里我们并不需要为结构初始化
  2. 使用 := 标识符v1 := Vertex{}
  • 嵌套结构 写法1:

    package main
    
    import "fmt"
    
    type Person struct {
        name string
        address Address
    }
    
    type Address struct {
        street string
    }
    
    func main(){
        var person Person
        person.name = "咸鱼"
        
        var address Address
        address.street = "街道"
        person.address = address
        
        fmt.Println("Person", person)
    }
    
    //控制台打印输出 
    Person: {咸鱼 {街道}}
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
  • 写法2:

    package main
    
    import "fmt"
    
    type Person struct {
        name string
        address Address
    }
    
    type Address struct {
        street string
    }
    
    func main(){
        Person := Person{
            name: "咸鱼",
            address: Address{
                street: "街道",
            },
        }
        fmt.Println("Person", person)
    }
    
    //控制台打印输出 
    Person: {咸鱼 {街道}}
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25

# 5、接口

Go 语言中的接口是隐式实现的,也就是说,如果一个类型实现了一个接口定义的所有方法,那么它就自动地实现了该接口。因此,我们可以通过将接口作为参数来实现对不同类型的调用,从而实现多态。

// 定义接口
type interfaceName interface {
    method_name1 [return_type]
    method_name2 [return_type]
}

//定义结构体
type structName struct {
    /* variables */
}

//实现接口方法
func (struct_name structName) methodName() [returnType]{
    /* 方法实现 */
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

实例

package main

import (
    "fmt"
)

type Phone interface {
    call()
}

type NokiaPhone struct {
}

func (nokiaPhone NokiaPhone) call() {
    fmt.Println("I am Nokia, I can call you!")
}

type IPhone struct {
}

func (iPhone IPhone) call() {
    fmt.Println("I am iPhone, I can call you!")
}

func main() {
    var phone Phone

    phone = new(NokiaPhone)
    phone.call()

    phone = new(IPhone)
    phone.call()

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

# 6、指针

在 Go 中,指针(pointer)是一种变量,它存储了一个值的内存地址。换句话说,指针指向内存中存储的数据的位置。

当用于操作变量时,&* 是 Go 语言中的两个操作符,具有不同的含义。

指针声明格式:

var var_name *var-type

1
2

var-type 为指针类型,var_name 为指针变量名,* 号用于指定变量是作为一个指针。以下是有效的指针声明:

var ip *int        /* 指向整型*/
var fp *float32    /* 指向浮点型 */
1
2
  1. & 操作符(取地址操作符):
    • 当用于变量之前,& 操作符用于获取变量的内存地址。
    • 例如:&x 表达式表示获取变量 x 的内存地址。
  2. * 操作符(解引用操作符):
    • 当用于指针变量之前,* 操作符用于获取指针指向位置的值。
    • 例如:*ptr 表达式表示获取指针 ptr 指向位置的值。

这两个操作符在使用指针时经常一起使用,配合使用可以实现指针的创建、操作和访问。例子:

package main

import "fmt"

func main(){
    //声明一个整数变量 i 并赋值为 42
    i := 42
    //p 是一个指向 i 的指针,存储了 i 的内存地址
    p := &i
    //输出指针 p 的值,即 i 的内存地址:0xc000012028
    fmt.Println(p)
    //输出指针 p 指向的值,即 i 的值:42
    fmt.Println(*p)
    
    //使用指针 p 修改指针指向位置的值,即修改 i 的值为 21
    *p = 21 
    //输出指针 p 指向的值,即 i 的新值:21
    fmt.Println(*p) 
    //输出变量 i 的值,也是:21
    fmt.Println(p)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 类型转换

类型转换用于将一种数据类型的变量转换为另外一种类型的变量。

Go 语言类型转换基本格式如下:

//type_name 为类型,expression 为表达式。
type_name(expression)
1
2

# 1、数值类型转换

//将整型转换为浮点型
var a int = 10
var b float64 = float64(a)
1
2
3

实例

package main

import "fmt"

func main() {
   var sum int = 17
   var count int = 5
   var mean float32
   
   mean = float32(sum)/float32(count)
   fmt.Printf("mean 的值为: %f\n",mean)
}

//mean 的值为: 3.400000
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 2、字符串类型转换

//将一个字符串转换成另一个类型
var str string = "10"
var num int
num, _ = strconv.Atoi(str)
1
2
3
4

注意,strconv.Atoi 函数返回两个值,第一个是转换后的整型值,第二个是可能发生的错误,我们可以使用空白标识符 _ 来忽略这个错误

实例

将整数转换为字符串

package main

import (
    "fmt"
    "strconv"
)

func main() {
    str := "123"
    num, err := strconv.Atoi(str)
    if err != nil {
        fmt.Println("转换错误:", err)
    } else {
        fmt.Printf("字符串 '%s' 转换为整数为:%d\n", str, num)
    }
}

//字符串 '123' 转换为整数为:123
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

将字符串转换为浮点数

package main

import (
    "fmt"
    "strconv"
)

func main() {
    str := "3.14"
    num, err := strconv.ParseFloat(str, 64)
    if err != nil {
        fmt.Println("转换错误:", err)
    } else {
        fmt.Printf("字符串 '%s' 转为浮点型为:%f\n", str, num)
    }
}

//字符串 '3.14' 转为浮点型为:3.140000
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

将浮点数转换为字符串

package main

import (
    "fmt"
    "strconv"
)

func main() {
    num := 3.14
    str := strconv.FormatFloat(num, 'f', 2, 64)
    fmt.Printf("浮点数 %f 转为字符串为:'%s'\n", num, str)
}

//浮点数 3.140000 转为字符串为:'3.14'
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 3、接口类型转换

接口类型转换有两种情况:类型断言类型转换

类型断言用于将接口类型转换为指定类型,其语法为:

value.(type) 
或者 
value.(T)
1
2
3

其中 value 是接口类型的变量,type 或 T 是要转换成的类型。

如果类型断言成功,它将返回转换后的值和一个布尔值,表示转换是否成功。

实例

package main

import "fmt"

func main() {
    var i interface{} = "Hello, World"
    str, ok := i.(string)
    if ok {
        fmt.Printf("'%s' is a string\n", str)
    } else {
        fmt.Println("conversion failed")
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

以上实例中,我们定义了一个接口类型变量 i,并将它赋值为字符串 "Hello, World"。然后,我们使用类型断言将 i 转换为字符串类型,并将转换后的值赋值给变量 str。最后,我们使用 ok 变量检查类型转换是否成功,如果成功,我们打印转换后的字符串;否则,我们打印转换失败的消息。

类型转换用于将一个接口类型的值转换为另一个接口类型,其语法为:

T(value)
1

T 是目标接口类型,value 是要转换的值。

在类型转换中,我们必须保证要转换的值和目标接口类型之间是兼容的,否则编译器会报错。

实例

package main

import "fmt"

type Writer interface {
    Write([]byte) (int, error)
}

type StringWriter struct {
    str string
}

func (sw *StringWriter) Write(data []byte) (int, error) {
    sw.str += string(data)
    return len(data), nil
}

func main() {
    var w Writer = &StringWriter{}
    sw := w.(*StringWriter)
    sw.str = "Hello, World"
    fmt.Println(sw.str)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

以上实例中,我们定义了一个 Writer 接口和一个实现了该接口的结构体 StringWriter。然后,我们将 StringWriter 类型的指针赋值给 Writer 接口类型的变量 w。接着,我们使用类型转换将 w 转换为 StringWriter 类型,并将转换后的值赋值给变量 sw。最后,我们使用 sw 访问 StringWriter 结构体中的字段 str,并打印出它的值。

# 异常

Go 中,通过显式的、单独的返回值来传达错误是惯用的。Go语言鼓励使用返回错误值的方式来处理异常情况,而不是通过抛出和捕获异常来处理错误。

Go 语言通过内置的错误接口提供了非常简单的错误处理机制。error 类型是一个接口类型,定义如下:

type error interface{
    Error() string
}
1
2
3

我们可以在编码中通过实现 error 接口类型来生成错误信息。

函数通常在最后的返回值中返回错误信息。使用 errors.New 可返回一个错误信息:

func Sqrt(f float64) (float64, error) {
    if f < 0 {
        return 0, errors.New("math: square root of negative number")
    }
    // 实现
}
1
2
3
4
5
6

在下面的例子中,我们在调用 Sqrt 的时候传递的一个负数,然后就得到了 non-nil 的 error 对象,将此对象与 nil 比较,结果为 true,所以 fmt.Println(fmt 包在处理 error 时会调用 Error 方法)被调用,以输出错误,请看下面调用的示例代码:

result, err:= Sqrt(-1)

if err != nil {
   fmt.Println(err)
}
1
2
3
4
5

实例

package main

import (
    "fmt"
)

// 定义一个 DivideError 结构
type DivideError struct {
    dividee int
    divider int
}

// 实现 `error` 接口
func (de *DivideError) Error() string {
    strFormat := `
    Cannot proceed, the divider is zero.
    dividee: %d
    divider: 0
`
    return fmt.Sprintf(strFormat, de.dividee)
}

// 定义 `int` 类型除法运算的函数
func Divide(varDividee int, varDivider int) (result int, errorMsg string) {
    if varDivider == 0 {
            dData := DivideError{
                    dividee: varDividee,
                    divider: varDivider,
            }
            errorMsg = dData.Error()
            return
    } else {
            return varDividee / varDivider, ""
    }

}

func main() {

    // 正常情况
    if result, errorMsg := Divide(100, 10); errorMsg == "" {
            fmt.Println("100/10 = ", result)
    }
    // 当除数为零的时候会返回错误信息
    if _, errorMsg := Divide(100, 0); errorMsg != "" {
            fmt.Println("errorMsg is: ", errorMsg)
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

# 并发

在Go语言中,使用并发可以通过goroutine和通道(channel)来实现。Goroutine是一种轻量级的线程,可以与其他goroutine并发运行,而通道是用于goroutine之间进行通信和数据同步的机制。

# 1、Goroutine

语法格式

go 函数名( 参数列表 )

go f(x, y, z)
1
2
3

Go 允许使用 go 语句开启一个新的运行期线程, 即 goroutine,以一个不同的、新创建的 goroutine 来执行一个函数。同一个程序中的所有 goroutine 共享同一个地址空间。

package main

import (
        "fmt"
        "time"
)

func say(s string) {
        for i := 0; i < 5; i++ {
                time.Sleep(100 * time.Millisecond)
                fmt.Println(s)
        }
}

func main() {
        go say("world")
        say("hello")
}

//控制台输出的 hello 和 world 是没有固定先后顺序
world
hello
hello
world
world
hello
hello
world
world
hello
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

# 2、Channel

通道(channel)是用来传递数据的一个数据结构。可用于两个 goroutine 之间通过传递一个指定类型的值来同步运行和通讯。操作符 <- 用于指定通道的方向,发送或接收。如果未指定方向,则为双向通道。

//把 v 发送到通道 ch
ch <- v
// 从 ch 接收数据;并把值付给 v
v := <-ch 
1
2
3
4

声明一个通道很简单,我们使用chan关键字即可,通道在使用前必须先创建

ch := make(chan int)
1

注意:默认情况下,通道是不带缓冲区的。发送端发送数据,同时必须有接收端相应的接收数据。

以下实例通过两个 goroutine 来计算数字之和,在 goroutine 完成计算后,它会计算两个结果的和:

package main

import "fmt"

func sum(s []int,c chan int) {
    sum := 0
    for _, v := range s{
        sum += v 
    }
    // 把 sum 发送到通道 c
    c <- sum
}

func main() {
    s := []int{7,2,8,-9,4,0}
    
    c := make(chan int)
    go sum(s[:len(s)/2],c)
    go sum(s[len(s)/2:],c)
    //从通道 c 中接收
    x,y := <-c, <-c
    
    fmt.Println(x,y,x+y)
}

//控制台打印输出  :-5 17 12
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

# 通道缓冲区

通道可以设置缓冲区,通过 make 的第二个参数指定缓冲区大小:

ch := make(chan int ,100)
1
  • 带缓冲区的通道允许发送端的数据发送和接收端的数据获取处于异步状态,就是说发送端发送的数据可以放在缓冲区里面,可以等待接收端去获取数据,而不是立刻需要接收端去获取数据。
  • 由于缓冲区的大小是有限的,所以还是必须有接收端来接收数据的,否则缓冲区一满,数据发送端就无法再发送数据了。

注意:如果通道不带缓冲,发送方会阻塞直到接收方从通道中接收了值。如果通道带缓冲,发送方则会阻塞直到发送的值被拷贝到缓冲区内;如果缓冲区已满,则意味着需要等待直到某个接收方获取到一个值。接收方在有值可以接收之前会一直阻塞。

# 通道遍历

Go 通过 range 关键字来实现遍历读取到的数据,类似于与数组或切片。格式如下:

v, ok := <-ch
1
  1. 如果通道中有可接收的值,那么接收操作会成功,将该值赋给变量 v,同时 ok 的值为 true
  2. 通道可以使用 close() 函数来关闭。
  3. 如果通道已经关闭且没有可接收的值,那么接收操作会立即返回,并将变量 v 的零值赋给它,同时 ok 的值为 false
func main(){
    ch := make(chan int)
    
    //启动一个goroutine 发送数据到通道
    go func(){
        //在发送完数据后关闭通道
        defer close(ch)
        for i :=1; i <=5; i++ {
            //发送数据到通道
            ch <- i
        }
    }()
    
    //循环接收通道中的元素,直到通道关闭并且没有可以接收的值
    for{
        num, ok := <-ch
        if ok {
            //输出通道中的元素
            fmt.Println(num)
        } else {
            //通道已关闭,退出循环
            break
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

defer 是 Go 语言中的一个关键字,用于延迟(defer)函数或方法的执行。当在函数或方法中使用 defer 关键字来延迟函数的执行时,被延迟执行的函数会在包含它的函数或方法即将返回之前被调用。

package main

import (
        "fmt"
)

func fibonacci(n int, c chan int) {
        x, y := 0, 1
        for i := 0; i < n; i++ {
                c <- x
                x, y = y, x+y
        }
        close(c)
}

func main() {
        c := make(chan int, 10)
        go fibonacci(cap(c), c)
        // range 函数遍历每个从通道接收到的数据,因为 c 在发送完 10 个
        // 数据之后就关闭了通道,所以这里我们 range 函数在接收到 10 个数据
        // 之后就结束了。如果上面的 c 通道不关闭,那么 range 函数就不
        // 会结束,从而在接收第 11 个数据的时候就阻塞了。
        for i := range c {
                fmt.Println(i)
        }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26