头图

前言

没有最完美的语言,只有最完美的使用场景。

不同语言在不同业务场景下发挥着不同的作用。

初期掌握一门编程语言,也能够在IT 行业中游刃有余,然而,随着在软件开发中的经验积累,你可能会发现一门语言的局限性。不同的编程语言在不同的业务场景下发挥着不同的作用。有些语言更适合构建大规模分布式系统,有些语言更适合处理高并发的网络请求,而有些语言则更适合开发移动应用程序或数据科学领域。

然而编程的基本概念,如变量、数据类型、条件语句、循环和函数等。在大多数编程语言中都是通用的,并且可以通过类比和迁移的方式应用于其他语言。

编程语言之间往往存在某种程度的相似性。那么本文就以这些内容为出发点作为Go语言的敲门砖。

学习范围

正文

调试步骤:有条件的可以在本机上安装 go 环境,懒得折腾的可以直接利用网页版开发环境:Go.Dev

一)Hello World

作为学习的必经之路,我们很有必要了解一下Go语言中如何实现 hello word 程序

package main    // Go程序的包声明。Go程序由多个包组成

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

func main() {   // 程序的主函数,也是程序的入口点
    fmt.Println("hello world")
}

二)变量和数据类型

1、数据类型

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

1. 布尔类型

bool,表示逻辑值,只有两个取值:truefalse

2. 数字类型

支持整型(int)和浮点型(float)数字,并且支持复数(complex)。

3. 字符串类型

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

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

这么多类型记不住?没关系,用到的时候自然就记住了

2、变量与常量

1)单变量声明

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

  • 使用 var 关键字

    var variablename [type] = value

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

  • 使用 := 标识符

    variablename := value

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

示例

2)多变量声明

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

3)常量声明
const variablename [type] = value

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

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

    4)常量枚举
    const (  
        a = "abc"  
        b = len(a)  
        c = unsafe.Sizeof(a)  
    )

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

    三)控制流程语句

    1、if 语句

    基本使用如下,这边需要注意的是 条件 不需要使用 () 包裹

    if condition { 
    } else if condition {
    } else {}

2、switch 语句

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

使用须知:

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

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

switch _expression_ {  
case x, y:  
    ...do something
case v, w:
    ...do something
case z:  
    ...do something  
default:
    ...do something  
}

3、for 语句

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

 for statement1; statement2; statement3 {  
   ...do something  
}

使用须知:

  • 语句 1 初始化循环计数器值。
  • 语句 2 针对每个循环迭代计算。
  • 语句3 增加循环计数器值。

    1)continue

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

    2)break

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

    3)range 关键字

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

    for index, value := range array|slice|map {  
      ...do something  
    }

四)函数

1、基本声明

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

  • 使用 func 关键字。
  • 指定函数的名称,后跟括号 ()() 内可添加参数列表
  • 最后,在大括号 {} 内添加定义函数应执行的操作的代码。

    func FunctionName(param1 type, param2 type, param3 type) {  
      ...do something  
    }

    2、多值返回

    函数中可以返回多个值

    func swap(x, y string) (string, string) {  
       return y, x  
    }

    3、连续参数

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

五)数据结构

1、数组

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

1)创建方式
  1. 使用数组字面量初始化:

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

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

    numbers := [5]int{1, 2, 3, 4, 5}
  2. 声明数组后逐个赋值:

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

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

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

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

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

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

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

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

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

2、切片

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

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

    1)创建方式

    以下是几种常见的切片创建方式:

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

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

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

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

    slice := make([]Type, length)

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

    numbers := make([]int, 5)

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

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

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

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

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

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

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

    slice := existingSliceOrArray[start:end]

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

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

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

    2)常见操作

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

  8. append():用于向切片末尾添加元素,可以动态扩展切片的长度。
  9. copy():用于复制切片的内容到另一个切片或数组。
  10. len():返回切片的长度。
  11. cap():返回切片的容量。
切片是引用类型,多个切片可以共享同一个底层数组。当对一个切片进行修改时,其他共享同一底层数组的切片也会受到影响。
3)与数组的差异

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

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

3、Map

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

    2)创建方式
  • 利用内建函数 make 创建

    // 创建一个空的 Map
    m := make(map[string]int)
    
    // 创建一个初始容量为 10 的 Map
    m := make(map[string]int, 10)
  • 使用字面量创建

    // 使用字面量创建 Map
    m := map[string]int{
      "apple": 1,
      "banana": 2,
      "orange": 3,
    }

4、结构体

1)基本概念

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

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

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

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

2)继承

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

3)声明方式

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

定义
type structName struct {  
  member1 datatype;  
  member2 datatype;  
  member3 datatype;
  ...  
}

使用
  1. 使用 var 关键字
    var v1 Vertex
    这里我们并不需要为结构初始化
  2. 使用 := 标识符
    v1 := Vertex{}

嵌套结构
写法1:

写法2:

5、接口

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

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

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

// 实现接口方法
func (struct_name structName) methodName() [returnType] {
   /* 方法实现 */
}

例子:

6、指针

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

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

  1. & 操作符(取地址操作符):

    • 当用于变量之前,& 操作符用于获取变量的内存地址。
    • 例如:&x 表达式表示获取变量 x 的内存地址。
  2. * 操作符(解引用操作符):

    • 当用于指针变量之前,* 操作符用于获取指针指向位置的值。
    • 例如:*ptr 表达式表示获取指针 ptr 指向位置的值。

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

六)异常

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

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

type error interface {
    Error() string
}

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

七)并发

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

1)Goroutine


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

2)Channel

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

ch <- v    // 把 v 发送到通道 ch
v := <-ch  // 从 ch 接收数据; 并把值赋给 v

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

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

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

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

通道遍历

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

v, ok := <-ch
  1. 如果通道中有可接收的值,那么接收操作会成功,将该值赋给变量 v,同时 ok 的值为 true
  2. 如果通道已经关闭且没有可接收的值,那么接收操作会立即返回,并将变量 v 的零值赋给它,同时 ok 的值为 false

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


通过本文的介绍,你应该对Go语言的入门有了更好的了解。在继续深入学习Go语言的过程中,你将会发现它的强大之处。你可以进一步学习高级的并发模型、标准库中丰富的功能和第三方库的使用,以及构建实际应用程序的最佳实践。

希望本文能够为你提供一个良好的起点,激发你对Go语言的兴趣,并为你进一步探索和学习提供指导。


写做
624 声望1.7k 粉丝