Go 语言入门指南 | 青训营

181 阅读21分钟

变量类型

GO语言是一门强类型语言,常见的变量类型有:字符串,整形,浮点型,布尔型

字符串

字符串的定义:

  1. 使用var:
csharp
复制代码
// 使用 var 关键字定义字符串变量 
var str1 string fmt.Println("str1:", str1) // 输出:str1: 
// 可以在声明时给变量赋初值 
var str2 string = "Hello, World!" fmt.Println("str2:", str2) // 输出:str2: Hello, World!
  1. 使用":=":
go
复制代码
str1 := "Hello, World!"
  1. 使用反引号(用于多行字符串):
csharp
复制代码
str2 := `This is a 
multi-line 
string.`
  1. 使用内建的string函数:
go
复制代码
str3 := string([]byte{'H', 'e', 'l', 'l', 'o'})

无论使用哪种定义方式,变量str1str2str3都将被声明为字符串类型,并且可以在程序中进行操作和使用。

字符串的长度:

在Go中,字符串的长度可以通过len()函数获取,例如:

go
复制代码
length := len(str1)
fmt.Println("Length of str1:", length) // Output: Length of str1: 13

字符串操作

字符串是go的内置变量类型,可以直接用+操作进行连接,字符串也支持索引访问和切片操作:

go
复制代码
// 字符串连接
str3 := str1 + str2

// 索引访问
firstChar := str1[0] // 获取字符串的第一个字符 'H'

// 切片操作
substring := str1[0:5] // 获取从索引0开始的前5个字符 "Hello"

请注意,由于Go的字符串是不可变的,对字符串的修改实际上会创建一个新的字符串。因此,如果需要频繁地进行字符串拼接或修改,建议使用strings.Builder类型或者将字符串转换为字节切片进行处理。

整形

整形类型:

有符号整型:

  • int8: 8位有符号整数
  • int16: 16位有符号整数
  • int32 (或 rune): 32位有符号整数
  • int64: 64位有符号整数
  • int: 根据计算机平台,可以是32位或64位有符号整数

无符号整型:

  • uint8 (或 byte): 8位无符号整数
  • uint16: 16位无符号整数
  • uint32 (或 uintptr): 32位无符号整数
  • uint64: 64位无符号整数
  • uint: 根据计算机平台,可以是32位或64位无符号整数

定义整型变量

go
复制代码
var age int = 30
height := 180

注意事项

  • 使用无符号整型:当需要处理正整数或非负整数时,可以考虑使用无符号整型,避免出现负数溢出等问题。
  • 避免溢出:在进行整型运算时,务必考虑数据范围,避免溢出问题。可以使用Go标准库中的math包来处理大整数运算。

浮点数

浮点型(Floating-Point Type)是计算机编程中用于表示实数(包括小数)的数据类型。在Go语言中,浮点型分为两种形式:float32float64,分别表示单精度浮点数和双精度浮点数。浮点数用于表示带有小数点的数值,适用于需要更高精度的计算。

浮点型的定义方法:

go
复制代码
var number1 float32 = 3.14
number2 := 1.618

在定义浮点型变量时,可以使用var关键字或简短声明符号:=。Go会根据初始化值自动推断变量的类型。

浮点型的注意事项:

  • 浮点数在计算机中并不是精确的,而是近似表示。因此,在进行浮点数计算时,可能会出现精度损失的问题。
  • 避免直接比较浮点数:由于浮点数的近似性,直接使用==比较两个浮点数可能会出现不确定的结果。应该使用范围或误差值来判断浮点数是否相等。
  • 注意溢出:浮点数的取值范围是有限的,当超出范围时,会导致溢出问题。
  • 使用math包:Go标准库中的math包提供了许多有用的浮点数函数,如取整、四舍五入、开方、幂等等,可以在实际开发中使用。

总之,在处理浮点数时需要注意其近似性和范围限制,以及避免使用直接比较操作。如果需要高精度的小数运算,可以考虑使用高精度计算库。

布尔型

布尔型(Boolean Type)是计算机编程中用于表示真(true)或假(false)两种状态的数据类型。在Go语言中,布尔型的定义方法很简单,它只有两个取值:truefalse

布尔型的定义方法:

go
复制代码
var isTrue bool = true
isFalse := false

在定义布尔型变量时,可以使用var关键字或简短声明符号:=。Go会根据初始化值自动推断变量的类型。

布尔型的注意事项:

  • 布尔型只有两个取值:truefalse,不可以使用其他值来表示真或假。
  • 布尔型变量用于条件判断,例如在if语句或循环中。

使用技巧:

  • 简洁明了:布尔型变量用于表示简单的真假状态,它可以使代码更加简洁和易读。
  • 合理使用:在进行条件判断时,使用布尔型变量能够更直观地表达代码意图。
  • 避免多余的判断:避免使用冗余的判断表达式,尽可能使用直接的布尔型值。

例如,下面是一个简单的示例,使用布尔型变量来表示是否满足某个条件:

go
复制代码
package main

import "fmt"

func main() {
    age := 25
    isAdult := age >= 18 // 根据年龄判断是否成年

    if isAdult {
        fmt.Println("You are an adult.")
    } else {
        fmt.Println("You are not an adult.")
    }
}

以上代码根据年龄判断一个人是否成年,并输出相应的信息。使用布尔型变量isAdult可以让代码更加清晰易懂。

数组

数组(Array)是一种固定长度的数据结构,它由一组相同类型的元素组成。在Go语言中,数组是一种值类型,表示一个固定大小的数据容器,一旦创建后,其大小不能改变。

数组的定义方法:

go
复制代码
// 使用 var 关键字定义数组
var arr1 [5]int

// 使用数组字面值初始化数组
arr2 := [3]string{"apple", "banana", "orange"}

// 根据初始化值自动推断数组大小
arr3 := [...]string("10", "20", "30", "40", "50")

在上述代码中,我们展示了三种定义数组的方式:

  • 使用var关键字定义数组,需要显式指定数组大小。
  • 使用数组字面值初始化数组,并指定元素的初始值。
  • 使用省略号(...)根据初始化值自动推断数组大小。

数组的元素可以通过索引访问,索引从0开始,例如:

scss
复制代码
fmt.Println(arr2[0]) // 输出:apple

需要注意的是,数组的大小是数组类型的一部分。换句话说,不同大小的数组是不同的类型,因此不能将一个大小为5的数组赋值给一个大小为3的数组变量。

数组的注意事项:

  • 数组是固定大小的,在定义时必须指定数组的大小。
  • 数组在内存中是连续存储的,因此访问速度较快。
  • 数组的索引是从0开始的,范围是0到数组长度减1。
  • 数组的长度是数组类型的一部分,因此不同长度的数组是不同的类型。
  • 数组是值类型,当将一个数组赋值给另一个数组时,会复制整个数组。

使用技巧:

  • 数组适用于固定数量的元素集合,例如表示一周的七天、表示某种特定规格的图像等。
  • 当数据量较小且大小固定时,可以选择使用数组,但如果数据量较大或大小不固定,可以使用切片(Slice)来代替数组,切片具有更灵活的动态大小特性。
  • 使用循环来遍历数组元素,可以使用for循环和range关键字。
  • 在函数参数中传递数组时,会复制整个数组,如果数组较大,可以考虑使用指针或切片来避免复制。

切片

切片(Slice)是Go语言中非常重要且灵活的数据结构,它可以看作是数组的一种封装,用于表示一段连续的元素序列。相比于数组,切片具有动态大小和更强大的操作功能,是在实际开发中更常用的数据结构之一。

1. 定义切片:

切片的定义方式是在数组或其它切片的基础上创建一个新的切片。切片由两个索引组成,即左闭右开区间,用于标识切片的起始位置和结束位置。

go
复制代码
// 使用数组创建切片
arr := [5]int{1, 2, 3, 4, 5}
slice1 := arr[1:4] // 切片包含arr[1], arr[2], arr[3]

// 使用切片创建切片(切片重组)
slice2 := slice1[1:3] // 切片包含slice1[1], slice1[2]

2. 切片长度和容量:

切片的长度是切片中元素的个数,而容量是从切片的起始位置到底层数组末尾的元素个数。可以通过len()cap()函数获取切片的长度和容量。

go
复制代码
slice := make([]int, 5, 10) // 创建一个长度为5,容量为10的切片
length := len(slice)       // 切片长度为5
capacity := cap(slice)     // 切片容量为10

3. 动态增长切片:

切片的长度可以动态增长,当切片的长度超过其容量时,Go语言会自动扩展底层数组的大小,使切片可以容纳更多的元素。

go
复制代码
slice := make([]int, 3, 5) // 创建一个长度为3,容量为5的切片
slice = append(slice, 4, 5) // 在切片末尾追加两个元素,切片长度变为5

4. 切片的零值和空切片:

切片的零值为nil,表示切片没有引用任何底层数组。一个零值的切片长度和容量都为0,且没有底层数组,称为空切片。

csharp
复制代码
var emptySlice []int // 声明一个空切片

5. 切片的共享底层数组:

多个切片可以共享同一个底层数组,修改其中一个切片的元素会影响其他共享该数组的切片。

go
复制代码
arr := [5]int{1, 2, 3, 4, 5}
slice1 := arr[1:4]
slice2 := arr[2:5]

slice1[0] = 10
fmt.Println(slice2) // 输出:[10 4 5]

6. 使用copy函数复制切片:

可以使用copy()函数将一个切片的内容复制到另一个切片,避免共享底层数组带来的影响。

go
复制代码
source := []int{1, 2, 3, 4, 5}
destination := make([]int, len(source))
copy(destination, source) // 复制source切片到destination切片

7. 使用切片作为函数参数:

在函数中使用切片作为参数时,传递的是切片的拷贝,因此对切片的修改会影响原切片。

go
复制代码
func modifySlice(slice []int) {
    slice[0] = 100
}

arr := [5]int{1, 2, 3, 4, 5}
slice := arr[1:4]
modifySlice(slice)
fmt.Println(arr) // 输出:[1 100 3 4 5]

总结:

切片在Go语言中是非常重要和灵活的数据结构。它提供了动态大小和方便的操作功能,避免了固定大小数组的局限性。在实际开发中,切片常常用于代替数组,成为处理集合数据的主要选择。

Go语言if else(分支结构)

在Go语言中,关键字 if 是用于测试某个条件(布尔型或逻辑型)的语句,如果该条件成立,则会执行 if 后由大括号{}括起来的代码块,否则就忽略该代码块继续执行后续的代码。

如果存在第二个分支,则可以在上面代码的基础上添加 else 关键字以及另一代码块,这个代码块中的代码只有在条件不满足时才会执行,if 和 else 后的两个代码块是相互独立的分支,只能执行其中一个。

if condition {
// do something
} else {
// do something
}

else if 分支的数量是没有限制的,但是为了代码的可读性,还是不要在 if 后面加入太多的 else if 结构,如果必须使用这种形式,则尽可能把先满足的条件放在前面。

关键字 if 和 else 之后的左大括号{必须和关键字在同一行,如果你使用了 else if 结构,则前段代码块的右大括号}必须和 else if 关键字在同一行,这两条规则都是被编译器强制规定的。

Go语言for(循环结构)

与多数语言不同的是,Go语言中的循环语句只支持 for 关键字,而不支持 while 和 do-while 结构,关键字 for 的基本使用方法与C语言和C++中非常接近:

sum := 0
for i := 0; i < 10; i++ {
    sum += i
}

使用循环语句时,需要注意的有以下几点:

  • 左花括号{必须与 for 处于同一行。
  • Go语言中的 for 循环与C语言一样,都允许在循环条件中定义和初始化变量,唯一的区别是,Go语言不支持以逗号为间隔的多个赋值语句,必须使用平行赋值的方式来初始化多个变量。
  • Go语言的 for 循环同样支持 continue 和 break 来控制循环,但是它提供了一个更高级的 break,可以选择中断哪一个循环,如下例:
for j := 0; j < 5; j++ {
    for i := 0; i < 10; i++ {
        if i > 5 {
            break JLoop
        }
        fmt.Println(i)
    }
}
JLoop:
// ...

函数

函数是一组执行特定任务的代码块,它可以在程序中被多次调用。在Go语言中,函数是一等公民,支持定义、调用和传递函数,函数可以作为参数和返回值。

Go语言中函数的基本语法

go
复制代码
func functionName(parameter1 type1, parameter2 type2) returnType {
    // 函数体,包含执行特定任务的代码
    return result // 返回值,如果有的话
}
  • func关键字用于定义函数。
  • functionName是函数的名称,遵循标识符规则。
  • (parameter1 type1, parameter2 type2)是函数的参数列表,用于接收外部传递给函数的值。参数可以有多个,如果没有参数,可以省略。
  • returnType是函数返回值的类型。如果函数不返回值,可以省略。
  • return result用于返回函数执行的结果。如果函数没有返回值,可以省略。

函数的定义通常在包级别进行,也可以定义在其他函数内部,称为嵌套函数。

示例代码:

go
复制代码
// 定义一个加法函数
func add(a int, b int) int {
    return a + b
}

// 定义一个没有参数和返回值的函数
func greet() {
    fmt.Println("Hello, World!")
}

函数的调用方式为:functionName(arg1, arg2, ...),其中arg1, arg2, ...是传递给函数的参数。

示例代码:

go
复制代码
result := add(5, 3) // 调用add函数,传递参数5和3,得到结果8
greet()             // 调用greet函数,没有参数,输出"Hello, World!"

函数的注意事项:

  • Go语言中的函数是值传递的,即在函数调用时,参数的副本会被传递给函数。如果需要修改原始数据,可以传递指针作为参数。
  • 函数可以返回多个值,例如:func test() (int, string),可以同时返回一个整数和一个字符串。
  • 函数可以作为参数传递给其他函数,也可以作为函数的返回值。
  • 可以使用匿名函数和闭包(closure)来实现更灵活的函数功能。
  • 可以使用defer关键字延迟函数的执行,通常用于资源释放等操作。

使用技巧:

  • 函数的设计要遵循单一职责原则,即一个函数应该只完成一个具体的任务,提高函数的可读性和可维护性。
  • 函数的命名要有意义,描述函数的作用或功能。
  • 尽量避免过度使用全局变量,应该通过函数参数来传递数据,减少函数之间的依赖。
  • 合理使用函数返回值来传递结果,尤其是在处理错误时,使用多返回值可以返回错误信息和执行结果。

一等公民

在计算机编程中,一等公民(First-class citizen)指的是某种特性或实体在编程语言中被视为普通的对象,可以像其他对象一样进行操作。换句话说,一等公民是在编程语言中具有完全平等地位的元素。

在Go语言中,函数是一等公民,这意味着函数可以像其他类型的值一样被使用和操作。具体来说,Go语言中的函数具有以下特性,使其成为一等公民:

  1. 可以赋值给变量:可以将函数赋值给变量,从而可以通过变量名调用函数。

    go
    复制代码
    func add(a, b int) int {
        return a + b
    }
    
    var sumFunc func(int, int) int
    sumFunc = add
    
  2. 可以作为参数传递:可以将函数作为参数传递给其他函数,实现函数的回调和扩展功能。

    go
    复制代码
    func calculate(a, b int, operation func(int, int) int) int {
        return operation(a, b)
    }
    
    result := calculate(5, 3, add) // 调用calculate函数,并将add函数作为参数传递
    
  3. 可以作为返回值:函数可以作为另一个函数的返回值,实现更复杂的逻辑和高级编程技巧。

    go
    复制代码
    func getAddFunction() func(int, int) int {
        return add
    }
    
    addFunc := getAddFunction()
    
  4. 可以直接使用字面值创建匿名函数:在需要时,可以直接使用匿名函数(没有函数名)来定义函数,非常方便。

    go
    复制代码
    func(x, y int) int {
        return x + y
    }
    

这些特性使得函数在Go语言中具有很高的灵活性和功能性,可以用于实现各种编程范式,例如函数式编程和回调机制。同时,Go语言的函数也可以看作一种类型,从而可以作为值来进行传递和操作,这使得Go语言成为一门非常强大的编程语言。

总结: 函数是Go语言中的重要组成部分,它可以封装代码块,实现代码的复用和模块化。合理设计和使用函数可以使程序更加简洁、清晰和易于维护。

方法

方法与函数的区别

在 Go语言中,方法(Method)和函数(Function)是两种不同的概念,它们在使用和定义上有一些区别。

  1. 方法(Method):

    • 方法是与特定类型(struct类型或者非struct类型)相关联的函数。

    • 方法是在某个类型上定义的,因此它必须有一个接收者(Receiver),这个接收者是某个类型的变量。

    • 方法可以访问接收者的属性和方法。

    • 方法的定义格式:func (r ReceiverType) methodName(parameters) (results)

    • 举例:假设有一个结构体类型Person,可以为其定义一个方法ShowName(),代码如下:

      go
      复制代码
      goCopy code
      type Person struct {
          Name string
          Age  int
      }
      
      func (p Person) ShowName() {
          fmt.Println("Name:", p.Name)
      }
      
  2. 函数(Function):

    • 函数是独立于类型的代码块,可以在任何地方调用,不依赖于某个类型。

    • 函数没有接收者,因为它是独立的。

    • 函数无法访问任何类型的属性,因为它没有关联的类型。

    • 函数的定义格式:func functionName(parameters) (results)

    • 举例:下面是一个简单的函数Add,用于将两个整数相加:

      css
      复制代码
      goCopy code
      func Add(a, b int) int {
          return a + b
      }
      

所以,方法是与类型相关联的函数,而函数是独立于类型的一般性代码块。选择使用方法还是函数取决于具体的需求和程序设计的结构。当我们需要在特定类型上执行某些操作时,通常会选择使用方法,以便可以直接访问该类型的属性和方法。而当我们需要在多个地方重复使用相同的功能代码时,可以将其封装为一个函数。

方法的用法:

方法的用法是为某个类型(结构体类型或非结构体类型)添加特定的行为,以便于对该类型的实例进行操作。方法的目的是将操作与数据关联起来,使得代码更加清晰、可读性更好,并且具有更好的组织性。

让我们看看方法的具体用法:

  1. 操作与类型关联:方法使得操作与类型直接相关联。比如,你可以为自定义的结构体类型添加方法来实现特定的行为,比如计算、打印等操作。
  2. 访问类型的字段:方法可以访问类型的字段,因为它们与类型相关联。这样,在方法内部可以直接操作接收者的属性。
  3. 实现接口:方法是实现接口的一种方式。通过在类型上定义满足接口方法签名的方法,我们可以让该类型实现该接口,并且可以通过接口进行类型转换和多态操作。
  4. 封装复杂逻辑:方法可以封装复杂的逻辑,使得代码更加模块化和易于维护。
  5. 使用指针接收者实现修改:如果方法的接收者是指针类型,那么在方法内部可以修改接收者的状态,这对于需要对类型进行修改的操作非常有用。
  6. 简化调用:方法的调用可以通过实例.方法名()的方式进行,使得调用更加简洁明了。

总结起来,方法的目的是将对类型的操作集中在一起,增加代码的可读性和可维护性,同时提供了一种面向对象的编程方式。通过方法,我们可以更加优雅地处理与类型紧密相关的行为,使得代码更加灵活和易于扩展。

案例证明方法的重要性

假设我们正在开发一个简单的银行账户管理系统,需要实现以下功能:

  1. 创建账户:创建新的银行账户,包括账户持有者的姓名和初始余额。
  2. 存款:对账户进行存款,增加账户余额。
  3. 取款:对账户进行取款,减少账户余额。
  4. 查询余额:查询账户的当前余额。
  5. 显示账户信息:显示账户持有者的姓名和余额。

现在,我们可以通过两种方式来实现这个功能,一种是使用方法,另一种是不使用方法,而是直接使用函数。

使用方法的实现:

go
复制代码
package main

import "fmt"

type BankAccount struct {
    holderName string
    balance    float64
}

func (b *BankAccount) Deposit(amount float64) {
    b.balance += amount
}

func (b *BankAccount) Withdraw(amount float64) {
    if b.balance >= amount {
        b.balance -= amount
    } else {
        fmt.Println("Insufficient balance.")
    }
}

func (b *BankAccount) GetBalance() float64 {
    return b.balance
}

func (b *BankAccount) ShowAccountInfo() {
    fmt.Printf("Account Holder: %s\nBalance: %.2f\n", b.holderName, b.balance)
}

func main() {
    // 创建账户
    account := BankAccount{holderName: "John Doe", balance: 1000.00}

    // 存款
    account.Deposit(500.00)

    // 取款
    account.Withdraw(200.00)

    // 查询余额
    balance := account.GetBalance()
    fmt.Println("Current Balance:", balance)

    // 显示账户信息
    account.ShowAccountInfo()
}

不使用方法的实现:

go
复制代码
package main

import "fmt"

type BankAccount struct {
    holderName string
    balance    float64
}

func Deposit(account *BankAccount, amount float64) {
    account.balance += amount
}

func Withdraw(account *BankAccount, amount float64) {
    if account.balance >= amount {
        account.balance -= amount
    } else {
        fmt.Println("Insufficient balance.")
    }
}

func GetBalance(account *BankAccount) float64 {
    return account.balance
}

func ShowAccountInfo(account *BankAccount) {
    fmt.Printf("Account Holder: %s\nBalance: %.2f\n", account.holderName, account.balance)
}

func main() {
    // 创建账户
    account := BankAccount{holderName: "John Doe", balance: 1000.00}

    // 存款
    Deposit(&account, 500.00)

    // 取款
    Withdraw(&account, 200.00)

    // 查询余额
    balance := GetBalance(&account)
    fmt.Println("Current Balance:", balance)

    // 显示账户信息
    ShowAccountInfo(&account)
}

当我们比较使用方法和不使用方法的实现时,可以从以下几个方面进行详细讲解:

  1. 代码结构和组织性:

    • 使用方法:方法将操作与类型关联,使得代码结构更加清晰,功能与类型紧密相关。所有针对该类型的操作都在类型的定义处,易于查找和维护。
    • 不使用方法:函数是独立的,没有直接关联到类型,需要在其他地方定义函数,并且需要显式地传递类型的指针作为参数,导致代码在多处散落。
  2. 可读性和易用性:

    • 使用方法:方法的调用比较简洁,通过实例调用方法,例如account.Deposit(500.00),更加直观和自然,易于理解。
    • 不使用方法:函数的调用需要显式传递类型的指针,例如Deposit(&account, 500.00),代码看起来较为繁琐,可读性相对较差。
  3. 代码复用:

    • 使用方法:方法可以在类型的多个实例上复用,因为它们关联到类型而不是特定实例。
    • 不使用方法:函数在不同的地方需要重复定义,因为它们没有与类型关联,不方便代码的复用。
  4. 封装性:

    • 使用方法:方法可以访问类型的私有字段,但是可以通过方法的逻辑进行封装,从而实现对类型的字段访问控制。
    • 不使用方法:函数无法直接访问类型的私有字段,需要暴露字段或者提供额外的公开方法,可能导致类型的封装性下降。
  5. 接口实现:

    • 使用方法:方法可以轻松地实现接口,只需要在类型上定义满足接口方法签名的方法即可。
    • 不使用方法:实现接口需要通过额外的函数,在实现接口时要确保函数的参数和返回值与接口方法一致。

综上所述,使用方法在实际开发中更有优势。它使代码结构更加清晰,易读易用,提高了代码的可维护性和可复用性。使用方法能够将操作与类型紧密关联,使得代码更符合面向对象的编程思想。因此,对于具有关联行为的类型,尤其是自定义的结构体类型,通常建议使用方法来实现相关功能,而不是将操作分散在多个独立的函数中。这样,代码将更加优雅、健壮,并且更容易扩展和维护。