GO基础学习

开始学习go之旅,带你快速了解go语言[超级详细讲解,持续更新]

在这里插入图片描述
go中文文档,可以快速学习查看

package main

import "fmt"

func main() {
    fmt.Println("hello world")
}
要运行这个程序,先将将代码放到名为 hello-world.go 的文件中,然后执行 go run
$ go run hello-world.go
hello world

如果我们想将程序编译成二进制文件(Windows 平台是 .exe 可执行文件), 可以通过 go build 来达到目的。
$ go build hello-world.go
$ ls
hello-world    hello-world.go

然后我们可以直接运行这个二进制文件。
$ ./hello-world
hello world

一.变量

package main

import "fmt"

func main() {
	// var 声明 1 个或者多个变量。
    var a = "initial"
    fmt.Println(a)

    var b, c int = 1, 2
    fmt.Println(b, c)
	// Go 会自动推断已经有初始值的变量的类型。
    var d = true
    fmt.Println(d)
	// 声明后却没有给出对应的初始值时,变量将会初始化为 零值 
    var e int
    fmt.Println(e)
	// := 语法是声明并初始化变量的简写, 例如 var f string = "short" 可以简写为右边这样
    f := "short"
    fmt.Println(f)
}
$ go run variables.go
initial
1 2
true
0
short

二.常量

package main

import (
    "fmt"
    "math"
)

const s string = "constant"

func main() {
    fmt.Println(s)
	// const 语句可以出现在任何 var 语句可以出现的地方
    const n = 500000000

    const d = 3e20 / n
    fmt.Println(d)
	// 数值型常量没有确定的类型,直到被给定某个类型,比如显式类型转化。
    fmt.Println(int64(d))
	// 一个数字可以根据上下文的需要(比如变量赋值、函数调用)自动确定类型。
    fmt.Println(math.Sin(n))
}
$ go run constant.go 
constant
6e+11
600000000000
-0.28470407323754404

三.循环

for 是 Go 中唯一的循环结构。这里会展示 for 循环的一些基本使用方式。

package main

import "fmt"

func main() {

    i := 1
    for i <= 3 {
        fmt.Println(i)
        i = i + 1
    }

    for j := 7; j <= 9; j++ {
        fmt.Println(j)
    }

    for {
        fmt.Println("loop")
        break
    }

    for n := 0; n <= 5; n++ {
        if n%2 == 0 {
            continue
        }
        fmt.Println(n)
    }
}
$ go run for.go
1
2
3
7
8
9
loop
1
3
5

四.if/else分支

注意,在 Go 中,条件语句的圆括号不是必需的,但是花括号是必需的。

Go 没有三目运算符, 即使是基本的条件判断,依然需要使用完整的 if 语句。

package main

import "fmt"

func main() {

    if 7%2 == 0 {
        fmt.Println("7 is even")
    } else {
        fmt.Println("7 is odd")
    }

    if 8%4 == 0 {
        fmt.Println("8 is divisible by 4")
    }

    if num := 9; num < 0 {
        fmt.Println(num, "is negative")
    } else if num < 10 {
        fmt.Println(num, "has 1 digit")
    } else {
        fmt.Println(num, "has multiple digits")
    }
}
$ go run if-else.go 
7 is odd
8 is divisible by 4
9 has 1 digit

五.switch分支

switch 是多分支情况时快捷的条件语句。

package main

import (
    "fmt"
    "time"
)

func main() {

    i := 2
    fmt.Print("write ", i, " as ")
    switch i {
    case 1:
        fmt.Println("one")
    case 2:
        fmt.Println("two")
    case 3:
        fmt.Println("three")
    }

    switch time.Now().Weekday() {
    case time.Saturday, time.Sunday:
        fmt.Println("It's the weekend")
    default:
        fmt.Println("It's a weekday")
    }

    t := time.Now()
    switch {
    case t.Hour() < 12:
        fmt.Println("It's before noon")
    default:
        fmt.Println("It's after noon")
    }

    whatAmI := func(i interface{}) {
        switch t := i.(type) {
        case bool:
            fmt.Println("I'm a bool")
        case int:
            fmt.Println("I'm an int")
        default:
            fmt.Printf("Don't know type %T\n", t)
        }
    }
    whatAmI(true)
    whatAmI(1)
    whatAmI("hey")
}
$ go run switch.go
Write 2 as two
It's a weekday
It's after noon
I'm a bool
I'm an int
Don't know type string

六.数组

在 Go 语言中,数组(Array)是一种固定长度的集合类型,它存储一组同类型的元素。数组的长度是固定的,一旦定义了数组的大小,就不能改变。

1. 数组的定义

数组的定义包括指定元素的类型和数组的长度。例如,一个存储 5 个整数的数组可以这样定义:

var arr [5]int  // 定义一个长度为 5 的整数数组

也可以在定义时直接初始化数组的元素:

var arr = [3]int{1, 2, 3}  // 定义并初始化数组

或者 Go 支持根据初始化值自动推导数组的长度:

var arr = [...]int{1, 2, 3}  // 自动推导长度为 3

2. 数组的访问和修改

数组的元素通过索引访问,索引从 0 开始。可以通过下标访问和修改数组的元素:

arr := [3]int{1, 2, 3}
fmt.Println(arr[0])  // 输出 1
arr[1] = 10         // 修改第二个元素
fmt.Println(arr[1])  // 输出 10

3. 数组的长度

数组的长度是固定的,可以使用 len() 函数获取数组的长度:

arr := [3]int{1, 2, 3}
fmt.Println(len(arr))  // 输出 3

4. 数组的传递

在 Go 中,数组是值类型,当你将一个数组作为参数传递给函数时,实际上是传递了该数组的副本。如果你修改了副本中的元素,原始数组不会受到影响。

func modifyArray(arr [3]int) {
    arr[0] = 100
}

arr := [3]int{1, 2, 3}
modifyArray(arr)
fmt.Println(arr)  // 输出 [1, 2, 3],原始数组没有被修改

如果你希望修改原数组的内容,可以传递指向数组的指针:

func modifyArray(arr *[3]int) {
    arr[0] = 100
}

arr := [3]int{1, 2, 3}
modifyArray(&arr)
fmt.Println(arr)  // 输出 [100, 2, 3],原数组已被修改

5. 多维数组

Go 支持多维数组,可以通过嵌套的方式定义多维数组。例如,定义一个 2 行 3 列的数组:

var arr [2][3]int
arr[0][0] = 1
arr[0][1] = 2
arr[0][2] = 3
arr[1][0] = 4
arr[1][1] = 5
arr[1][2] = 6
fmt.Println(arr)

输出:

[[1 2 3] [4 5 6]]

6. 数组的初始化

数组可以在声明时进行初始化,也可以在定义之后进行逐一赋值。以下是几种初始化数组的方式:

  • 声明并初始化:

    arr := [3]int{1, 2, 3}  // 定义并初始化数组
    
  • 使用 ... 自动推导数组长度:

    arr := [...]int{1, 2, 3}  // 自动推导数组长度为 3
    
  • 使用 new 函数创建一个指向数组的指针:

    arr := new([3]int)  // 创建一个指向长度为 3 的数组的指针
    arr[0] = 10
    fmt.Println(arr)  // 输出 &[10 0 0]
    

7. 数组与切片的区别

数组和切片的区别是一个重要的概念,尤其是 Go
语言的切片(Slice)非常常用。切片是对数组的一个抽象,允许灵活的动态扩展和缩减,而数组的大小是固定的。

  • 数组:长度固定,类型和值传递。
  • 切片:动态大小,引用传递,可以灵活扩展。

示例,使用切片:

arr := [3]int{1, 2, 3}    // 数组
slice := arr[:]           // 切片
fmt.Println(slice)        // 输出 [1 2 3]

8. 数组的应用场景

数组在 Go 中通常用于以下几种情况:

  • 当需要固定大小的数据集合时。
  • 需要用到固定大小的内存时(例如嵌入式系统或性能优化的场景)。
  • 数组的值传递方式可以避免不必要的内存共享,确保数据不会被意外修改。

总结

Go 语言中的数组是固定长度的,定义时需要指定数组的长度。

  • 数组通过下标访问和修改元素,且下标从 0 开始。
  • Go 中的数组是值类型,传递数组时会复制副本。如果希望修改原始数组,需要传递指针。
  • Go 支持多维数组,可以使用嵌套方式定义。
  • 数组与切片不同,切片更灵活且常用于实际编程中。
package main

import "fmt"

func main() {

    var a [5]int
    fmt.Println("emp:", a)

    a[4] = 100
    fmt.Println("set:", a)
    fmt.Println("get:", a[4])

    fmt.Println("len:", len(a))

    b := [5]int{1, 2, 3, 4, 5}
    fmt.Println("dcl:", b)

    var twoD [2][3]int
    for i := 0; i < 2; i++ {
        for j := 0; j < 3; j++ {
            twoD[i][j] = i + j
        }
    }
    fmt.Println("2d: ", twoD)
}
$ go run arrays.go
emp: [0 0 0 0 0]
set: [0 0 0 0 100]
get: 100
len: 5
dcl: [1 2 3 4 5]
2d:  [[0 1 2] [1 2 3]]

七.切片

在 Go
语言中,slice(切片)是一种灵活、动态的数组类型,它是对数组的一个引用,具有更高效的内存管理和更灵活的功能。切片不像数组那样具有固定的长度,它可以动态增长和缩小。

1. 创建切片

  • 切片的创建有几种方式:

    • 使用 make 函数:

      slice := make([]int, 5) // 创建一个长度为5的切片
      

      你还可以指定切片的容量:

      slice := make([]int, 5, 10) // 创建一个长度为5,容量为10的切片
      
    • 直接使用数组字面量:

      slice := []int{1, 2, 3, 4, 5} // 创建一个包含5个元素的切片
      

2. 切片的属性

每个切片都包含三个基本属性:

  • 指针(Pointer): 指向底层数组的指针。
  • 长度(Length): 切片中元素的数量。
  • 容量(Capacity): 切片底层数组的大小,从切片的起始位置到底层数组的末尾。

你可以通过 len()cap() 函数获取切片的长度和容量:

fmt.Println(len(slice)) // 输出切片的长度
fmt.Println(cap(slice)) // 输出切片的容量

3. 切片的切割操作

  • 切片可以从另一个切片或数组中通过索引和范围来切割:

    slice := []int{1, 2, 3, 4, 5}
    subSlice := slice[1:4] // 获取从索引1到索引3的元素
    fmt.Println(subSlice)  // 输出: [2 3 4]
    

    你还可以指定切片的容量:

    subSlice := slice[1:4:5] // 切片从索引1到索引3,容量为5
    fmt.Println(subSlice)    // 输出: [2 3 4]
    

4. 动态扩展切片

  • 使用 append 扩展切片:

    slice := []int{1, 2, 3}
    slice = append(slice, 4, 5) // 扩展切片
    fmt.Println(slice)          // 输出: [1 2 3 4 5]
    

append 函数用于向切片中添加元素,并且如果需要,它会自动调整切片的容量。

5. 删除切片元素

  • 删除指定索引的元素:

    slice = append(slice[:index], slice[index+1:]...)
    slice := []int{1, 2, 3, 4, 5}
    slice = append(slice[:2], slice[3:]...) // 删除索引2的元素
    fmt.Println(slice)  // 输出: [1 2 4 5]
    

6. 切片的反转

  • 反转切片元素:

    for i := 0; i < len(slice)/2; i++ {
        slice[i], slice[len(slice)-1-i] = slice[len(slice)-1-i], slice[i]
    }
    

7. 切片的查找

  • 查找指定元素:

    found := false
    for _, v := range slice {
        if v == target {
            found = true
            break
        }
    }
    

8. 切片的分块

  • 将切片分割成多个小块:

    func chunk(slice []int, size int) [][]int {
        var chunks [][]int
        for i := 0; i < len(slice); i += size {
            end := i + size
            if end > len(slice) {
                end = len(slice)
            }
            chunks = append(chunks, slice[i:end])
        }
        return chunks
    }
    

9. 切片排序

  • 对切片进行排序:

    import "sort"
    sort.Ints(slice)  // 对整数切片进行升序排序
    

10. 删除重复元素

  • 去除切片中的重复元素:

    func removeDuplicates(slice []int) []int {
        seen := make(map[int]struct{})
        result := []int{}
        for _, v := range slice {
            if _, ok := seen[v]; !ok {
                seen[v] = struct{}{}
                result = append(result, v)
            }
        }
        return result
    }
    

11. copy 函数

  • 复制切片内容:

    copy(dst, src) // 将 src 的内容复制到 dst
    

总结

Go 切片提供了高效且灵活的操作方式,以下是常见的切片操作总结:

  • 创建:通过 make 或字面量创建。
  • 切割:使用切片操作符可以截取切片的部分。
  • 扩展:通过 append 函数动态扩展切片。
  • 删除:通过 append 删除切片中的元素。
  • 反转:通过交换元素实现反转。
  • 查找:通过遍历切片查找元素。
  • 分块:将切片分割成多个小块。
  • 排序:使用标准库中的 sort 对切片进行排序。
  • 去重:通过 map 去除切片中的重复元素。
  • 复制:使用 copy 将一个切片的内容复制到另一个切片中。
package main

import "fmt"

func main() {

    s := make([]string, 3)
    fmt.Println("emp:", s)

    s[0] = "a"
    s[1] = "b"
    s[2] = "c"
    fmt.Println("set:", s)
    fmt.Println("get:", s[2])

    fmt.Println("len:", len(s))

    s = append(s, "d")
    s = append(s, "e", "f")
    fmt.Println("apd:", s)

    c := make([]string, len(s))
    copy(c, s)
    fmt.Println("cpy:", c)

    l := s[2:5]
    fmt.Println("sl1:", l)

    l = s[:5]
    fmt.Println("sl2:", l)

    l = s[2:]
    fmt.Println("sl3:", l)

    t := []string{"g", "h", "i"}
    fmt.Println("dcl:", t)

    twoD := make([][]int, 3)
    for i := 0; i < 3; i++ {
        innerLen := i + 1
        twoD[i] = make([]int, innerLen)
        for j := 0; j < innerLen; j++ {
            twoD[i][j] = i + j
        }
    }
    fmt.Println("2d: ", twoD)
}
$ go run slices.go
emp: [  ]
set: [a b c]
get: c
len: 3
apd: [a b c d e f]
cpy: [a b c d e f]
sl1: [c d e]
sl2: [a b c d e]
sl3: [c d e f]
dcl: [g h i]
2d:  [[0] [1 2] [2 3 4]]

八.map

在Go语言中,map 是一种内建的数据类型,用于存储键值对(key-value
pairs)。它类似于其他语言中的哈希表或字典,能够快速地根据键查找对应的值。

1. 创建 map

Go语言中的 map 可以通过内建的 make 函数或者字面量(literal)来创建。

使用 make 函数
m := make(map[string]int)

上面的代码创建了一个空的 map,键为 string 类型,值为 int 类型。

使用字面量(literal)创建
m := map[string]int{
    "apple":  5,
    "banana": 3,
}

上面的代码创建了一个初始包含两个键值对的 map

2. 向 map 中添加和更新元素

你可以直接通过键来添加或更新值:

m["apple"] = 10  // 更新值
m["orange"] = 7  // 添加新元素

3. 获取 map 中的值

通过键来访问对应的值:

value := m["apple"]
fmt.Println(value)  // 输出 10

4. 删除 map 中的元素

使用 delete 函数来删除指定键的元素:

delete(m, "banana")  // 删除键为"banana"的元素

5. 检查键是否存在

通过多返回值的方式来判断键是否存在。如果键存在,第二个返回值为 true,否则为 false

value, ok := m["apple"]
if ok {
    fmt.Println("Found:", value)
} else {
    fmt.Println("Not found")
}

6. 遍历 map

使用 for 循环来遍历 map 的所有键值对:

for key, value := range m {
    fmt.Println(key, value)
}

7. map 的特性

  • 无序性map 中的元素是无序的,遍历时顺序是随机的。
  • 线程不安全:在多线程(goroutines)中并发修改同一个 map 时,Go 运行时会引发恐慌(panic)。如果需要并发访问 map,应使用 sync.Mutex 或者 sync.RWMutex 来加锁。

8. map 的容量

可以使用 len() 函数获取 map 中元素的数量:

fmt.Println(len(m))  // 输出 map 中的元素个数

9. 空 mapnil map

  • 如果 map 没有被初始化(nil),则它的行为类似于空 map,但是尝试向 nil map 中添加或删除元素会引发运行时错误。
  • 可以使用 make 函数初始化一个空 map
示例代码:
package main

import "fmt"

func main() {
    // 创建一个 map
    m := make(map[string]int)

    // 向 map 中添加元素
    m["apple"] = 5
    m["banana"] = 2

    // 更新元素
    m["apple"] = 10

    // 获取元素
    if value, ok := m["apple"]; ok {
        fmt.Println("Apple:", value)
    }

    // 删除元素
    delete(m, "banana")

    // 遍历 map
    for key, value := range m {
        fmt.Println(key, value)
    }

    // 获取 map 的长度
    fmt.Println("Length of map:", len(m))
}
package main

import "fmt"

func main() {

    m := make(map[string]int)

    m["k1"] = 7
    m["k2"] = 13

    fmt.Println("map:", m)

    v1 := m["k1"]
    fmt.Println("v1: ", v1)

    fmt.Println("len:", len(m))

    delete(m, "k2")
    fmt.Println("map:", m)

    _, prs := m["k2"]
    fmt.Println("prs:", prs)

    n := map[string]int{"foo": 1, "bar": 2}
    fmt.Println("map:", n)
}
$ go run maps.go 
map: map[k1:7 k2:13]
v1:  7
len: 2
map: map[k1:7]
prs: false
map: map[foo:1 bar:2]

九.range

在Go语言中,range 是一个非常强大的关键字,用于遍历数组、切片、map 和通道等数据结构。通过
range,可以轻松遍历数据结构的元素,并同时获得索引(或键)和值。

1. range 遍历数组和切片

数组
arr := [3]int{1, 2, 3}
for index, value := range arr {
    fmt.Println(index, value)
}

在遍历数组时,range 返回两个值:

  • index:当前元素的索引。
  • value:当前元素的值。
切片
slice := []string{"apple", "banana", "cherry"}
for index, value := range slice {
    fmt.Println(index, value)
}

对于切片,range 也返回两个值:

  • index:当前元素的索引。
  • value:当前元素的值。

2. range 遍历 map

map 中,range 返回的是键值对:

m := map[string]int{"apple": 5, "banana": 3}
for key, value := range m {
    fmt.Println(key, value)
}

对于 maprange 返回两个值:

  • key:当前元素的键。
  • value:当前元素的值。
注意事项:
  • map 的遍历顺序是无序的,每次遍历的顺序可能不同。

3. range 遍历字符串

range 也可以用于遍历字符串。它会逐个获取字符串中的 Unicode 字符,而不是按字节遍历:

str := "hello"
for index, runeValue := range str {
    fmt.Println(index, runeValue)
}

在遍历字符串时,range 返回两个值:

  • index:当前字符的索引。
  • runeValue:当前字符的 Unicode 码点值(rune 类型)。

4. 只获取值或索引

你可以通过 _ 忽略 range 返回的某些值。

  • 只获取索引:
for index := range arr {
    fmt.Println(index)
}
  • 只获取值:
for _, value := range arr {
    fmt.Println(value)
}

5. range 遍历通道(channel)

range 也可以用来遍历通道(channel)中的元素。当通道关闭时,range 循环会停止。

ch := make(chan int, 3)
ch <- 1
ch <- 2
ch <- 3
close(ch)

for value := range ch {
    fmt.Println(value)
}

当通道关闭后,range 会自动遍历通道中的所有元素。

6. 示例:综合使用 range

package main

import "fmt"

func main() {
    // 遍历数组
    arr := [3]int{1, 2, 3}
    for index, value := range arr {
        fmt.Printf("arr[%d] = %d\n", index, value)
    }

    // 遍历切片
    slice := []string{"apple", "banana", "cherry"}
    for _, value := range slice {
        fmt.Println(value)
    }

    // 遍历map
    m := map[string]int{"apple": 5, "banana": 3}
    for key, value := range m {
        fmt.Printf("%s has %d fruits\n", key, value)
    }

    // 遍历字符串
    str := "hello"
    for index, runeValue := range str {
        fmt.Printf("Index: %d, Rune: %c\n", index, runeValue)
    }
}

总结

  • range 是 Go 语言中遍历数据结构的关键字,可以遍历数组、切片、map、通道等。
  • 在遍历 map 时,range 返回的是键和值;遍历数组或切片时,返回的是索引和值。
  • 可以使用 _ 来忽略不需要的返回值(如只关注索引或只关注值)。
  • range 在处理通道时会在通道关闭时停止。

range 是一个高效且灵活的工具,能够简化代码并提高可读性。

package main

import "fmt"

func main() {

    nums := []int{2, 3, 4}
    sum := 0
    for _, num := range nums {
        sum += num
    }
    fmt.Println("sum:", sum)

    for i, num := range nums {
        if num == 3 {
            fmt.Println("index:", i)
        }
    }

    kvs := map[string]string{"a": "apple", "b": "banana"}
    for k, v := range kvs {
        fmt.Printf("%s -> %s\n", k, v)
    }

    for k := range kvs {
        fmt.Println("key:", k)
    }

    for i, c := range "go" {
        fmt.Println(i, c)
    }
}
$ go run range.go
sum: 9
index: 1
a -> apple
b -> banana
key: a
key: b
0 103
1 111

十.函数

在 Go
语言中,函数(function)是基本的代码结构之一,用于封装一组语句,并通过函数调用来执行这些语句。函数可以有输入参数、返回值,也可以没有参数或返回值。

1. 定义函数

Go 语言使用 func 关键字来定义函数。

基本函数定义
func add(a int, b int) int {
    return a + b
}

这个函数的定义包含了:

  • func 关键字,表示定义一个函数。
  • add,函数的名称。
  • (a int, b int),是函数的参数列表,表示这个函数接收两个 int 类型的参数。
  • int,是返回值的类型,表示该函数返回一个 int 类型的结果。

2. 调用函数

函数定义后,可以通过函数名调用它,并传递参数:

result := add(3, 4)
fmt.Println(result)  // 输出 7

3. 函数参数

  • 单一参数类型:可以在函数参数中使用相同类型的多个参数,简化代码。
func add(a, b int) int {
    return a + b
}
  • 可变参数:Go 支持传递可变数量的参数,使用 ... 来表示可变参数。
func sum(numbers ...int) int {
    total := 0
    for _, number := range numbers {
        total += number
    }
    return total
}

调用:

fmt.Println(sum(1, 2, 3, 4))  // 输出 10

4. 返回值

Go 函数可以返回多个值:

func swap(a, b int) (int, int) {
    return b, a
}

调用:

x, y := swap(1, 2)
fmt.Println(x, y)  // 输出 2 1

5. 命名返回值

Go 允许给返回值命名,返回值就像局部变量一样,可以在函数体内直接使用。这也简化了代码,使得不需要显式地使用 return 语句。

func add(a, b int) (sum int) {
    sum = a + b
    return  // 使用命名返回值
}

6. 函数作为值

Go 允许函数作为值传递。这意味着可以将函数赋值给变量或作为参数传递给其他函数。

将函数赋值给变量
func multiply(a, b int) int {
    return a * b
}

var f func(int, int) int = multiply
fmt.Println(f(2, 3))  // 输出 6
函数作为参数
func operate(a, b int, op func(int, int) int) int {
    return op(a, b)
}

fmt.Println(operate(2, 3, multiply))  // 输出 6

7. 匿名函数

Go 支持匿名函数,即没有函数名的函数。匿名函数常常作为回调函数或临时使用。

func() {
    fmt.Println("Hello from anonymous function!")
}()

8. 函数闭包

Go 支持闭包,闭包是一个函数,它可以“记住”并访问其外部作用域中的变量。即使外部函数返回,闭包仍然可以访问这些变量。

func outer() func() int {
    x := 10
    return func() int {
        x++
        return x
    }
}

increment := outer()
fmt.Println(increment())  // 输出 11
fmt.Println(increment())  // 输出 12

在上面的例子中,increment 是一个闭包,它记住了 x 的值,并且每次调用时都会更新并返回新的值。

9. 函数的递归调用

Go 支持函数的递归调用,即一个函数在其定义中调用自己。

func factorial(n int) int {
    if n == 0 {
        return 1
    }
    return n * factorial(n-1)
}
fmt.Println(factorial(5))  // 输出 120

在这个例子中,factorial 函数调用自己来计算阶乘。

10. 多返回值函数

Go 允许函数返回多个值,常用于处理错误处理等场景。

func divide(a, b int) (int, error) {
    if b == 0 {
        return 0, fmt.Errorf("division by zero")
    }
    return a / b, nil
}

result, err := divide(10, 2)
if err != nil {
    fmt.Println(err)
} else {
    fmt.Println(result)  // 输出 5
}

总结

  • Go 语言中的函数是非常灵活的,可以定义单一返回值、多个返回值、可变参数和命名返回值等。
  • Go 支持将函数作为值传递,允许匿名函数和闭包等特性。
  • 函数调用时,可以直接使用返回值,也可以通过传递函数作为参数来进行更高阶的编程。

基础用法:

package main

import "fmt"

func plus(a int, b int) int {

    return a + b
}

func plusPlus(a, b, c int) int {
    return a + b + c
}

func main() {

    res := plus(1, 2)
    fmt.Println("1+2 =", res)

    res = plusPlus(1, 2, 3)
    fmt.Println("1+2+3 =", res)
}
	
$ go run functions.go
1+2 = 3
1+2+3 = 6

多返回值:

package main

import "fmt"

func vals() (int, int) {
    return 3, 7
}

func main() {

    a, b := vals()
    fmt.Println(a)
    fmt.Println(b)

    _, c := vals()
    fmt.Println(c)
}
	
$ go run multiple-return-values.go
3
7
7

变参函数:

package main

import "fmt"

func sum(nums ...int) {
    fmt.Print(nums, " ")
    total := 0
    for _, num := range nums {
        total += num
    }
    fmt.Println(total)
}

func main() {

    sum(1, 2)
    sum(1, 2, 3)

    nums := []int{1, 2, 3, 4}
    sum(nums...)
}
$ go run variadic-functions.go 
[1 2] 3
[1 2 3] 6
[1 2 3 4] 10

闭包:

package main

import "fmt"

func intSeq() func() int {
    i := 0
    return func() int {
        i++
        return i
    }
}

func main() {

    nextInt := intSeq()

    fmt.Println(nextInt())
    fmt.Println(nextInt())
    fmt.Println(nextInt())

    newInts := intSeq()
    fmt.Println(newInts())
}
$ go run closures.go
1
2
3
1

递归:

package main

import "fmt"

func fact(n int) int {
    if n == 0 {
        return 1
    }
    return n * fact(n-1)
}

func main() {
    fmt.Println(fact(7))

    var fib func(n int) int

    fib = func(n int) int {
        if n < 2 {
            return n
        }
        return fib(n-1) + fib(n-2)

    }

    fmt.Println(fib(7))
}
$ go run recursion.go 
5040
13

十一.指针

在 Go
语言中,指针是存储变量内存地址的类型,它允许程序间接访问变量的值。指针可以非常方便地用于修改函数外部的变量值,以及在内存中操作数据。Go
的指针与 C 语言类似,但它不允许进行指针算术运算,这让它更加安全。

1. 定义和声明指针

在 Go 中,指针的声明方式是使用 * 表示指向某个类型的指针,类型前加 * 表示该变量是该类型的指针。

var ptr *int

这意味着 ptr 是一个指向 int 类型的指针,但它当前没有指向任何具体的内存地址。

2. 获取变量的地址(取地址操作符 &

要获取一个变量的内存地址,可以使用 & 操作符,它会返回变量的地址。

x := 10
ptr := &x  // ptr 是 x 的指针
fmt.Println(ptr)  // 输出 x 的地址

3. 解引用指针(取值操作符 *

通过指针访问它所指向的变量的值,使用 * 操作符。这个过程叫做“解引用”。

x := 10
ptr := &x  // 获取 x 的地址
fmt.Println(*ptr)  // 输出 10,解引用指针,得到 x 的值

4. 指针与变量的关系

  • & 获取变量的地址。
  • * 解引用指针,获取指针所指向的值。

示例:

x := 10
ptr := &x       // 获取 x 的地址
fmt.Println(ptr) // 输出 x 的内存地址
fmt.Println(*ptr) // 输出 10,通过指针访问 x 的值

5. 修改变量的值通过指针

指针允许通过间接访问修改原变量的值。通过解引用指针可以修改它指向的变量。

x := 10
ptr := &x
*ptr = 20  // 修改 x 的值,通过 ptr 指针
fmt.Println(x)  // 输出 20

6. 指针作为函数参数

在 Go 中,函数参数是值传递的。如果希望函数修改传入的变量,可以传递变量的指针。这通常用于避免复制大量数据的成本,或允许函数修改传入的参数。

func modifyValue(a *int) {
    *a = 20  // 通过指针修改原始值
}

x := 10
modifyValue(&x)  // 传递 x 的指针
fmt.Println(x)  // 输出 20

7. 指针类型与结构体

结构体也可以使用指针,结构体指针常用于修改结构体的成员,避免复制结构体的副本。

type Person struct {
    Name string
    Age  int
}

func updateAge(p *Person) {
    p.Age = 30  // 修改 Person 的 Age
}

p := &Person{Name: "John", Age: 25}
fmt.Println(p.Age)  // 输出 25
updateAge(p)
fmt.Println(p.Age)  // 输出 30

8. nil 指针

指针可以是 nil,即它不指向任何有效的内存地址。你可以检查指针是否为 nil 来避免解引用空指针。

var ptr *int
if ptr == nil {
    fmt.Println("ptr is nil")
}

9. 零值和指针

Go 中,指针的零值是 nil,表示它没有指向任何有效的内存地址。当一个指针没有显式初始化时,它的默认值就是 nil

var ptr *int  // ptr 默认为 nil
fmt.Println(ptr)  // 输出 nil

10. 指针的应用

  • 修改函数外部的变量值:通过指针参数来修改传入的变量。
  • 避免大数据的复制:对于大的数据结构(如数组或结构体),可以通过指针传递,避免复制整个数据结构。
  • 链表、树等数据结构:指针广泛应用于实现链表、二叉树等动态数据结构。

总结

指针是 Go 语言中的一个重要特性,它让你能够间接操作内存,并通过引用传递修改变量的值。Go 语言中的指针有如下特点:

  • 不允许指针算术运算,避免了 C 语言中的很多潜在错误。
  • 使用 & 获取变量的地址,使用 * 解引用指针。
  • 可以通过指针修改变量的值,传递指针给函数可以改变函数外的变量。

指针在 Go 中的使用简单而强大,可以提高程序的效率并且节省内存空间。

package main

import "fmt"

func zeroval(ival int) {
    ival = 0
}

func zeroptr(iptr *int) {
    *iptr = 0
}

func main() {
    i := 1
    fmt.Println("initial:", i)

    zeroval(i)
    fmt.Println("zeroval:", i)

    zeroptr(&i)
    fmt.Println("zeroptr:", i)

    fmt.Println("pointer:", &i)
}
$ go run pointers.go
initial: 1
zeroval: 1
zeroptr: 0
pointer: 0x42131100

十二.字符串和rune类型

字符串(string)

1. 字符串的定义
  • 不可变性:字符串是不可变的,即一旦创建后,无法直接修改其中的内容。任何修改操作都会生成一个新的字符串。
  • 底层结构:字符串底层是一个只读的字节序列([]byte
  • 编码:通常用来存储 UTF-8 编码的数据,可以包含 ASCII 字符、汉字、特殊符号等。

2. 字符串的常见操作
声明与初始化
  • 使用双引号声明普通字符串。
  • 使用反引号声明原始字符串,保留原格式,包括换行和特殊字符。
s1 := "hello, world"         // 普通字符串
s2 := `hello, 
world with "quotes"`        // 原始字符串
获取字符串长度
  • 使用 len() 获取字符串长度,返回的是字节数而非字符数。
s := "你好"
fmt.Println(len(s))  // 输出:6,因为每个汉字占3个字节
索引访问字符串
  • 可以通过索引访问字符串中的字节:
s := "hello"
fmt.Println(s[0])       // 输出:104,'h' 的 ASCII 值
fmt.Printf("%c\n", s[0]) // 输出:h
  • 注意:通过索引访问的是单个字节,而不是字符。
字符串拼接
  • 使用 +fmt.Sprintf() 拼接字符串。
s1 := "hello"
s2 := "world"
s3 := s1 + ", " + s2 + "!"
fmt.Println(s3)  // 输出:hello, world!
切片操作
  • 支持通过切片截取子字符串,但结果是字节切片,可能导致截断多字节字符。
s := "你好世界"
sub := s[:3]  // 截取前3个字节
fmt.Println(sub)  // 输出:乱码(截断了“你”)

3. Go标准库中的字符串方法

Go 提供了 strings 包来处理字符串,包括查找、替换、切分等操作。

常用函数
import "strings"

// 查找子串
fmt.Println(strings.Contains("hello, world", "world")) // true
fmt.Println(strings.Index("hello, world", "o"))        // 4
fmt.Println(strings.LastIndex("hello, world", "o"))    // 8

// 切分与连接
parts := strings.Split("a,b,c", ",")    // [a b c]
joined := strings.Join(parts, "-")     // a-b-c

// 替换
replaced := strings.ReplaceAll("hello, hello", "hello", "hi") // hi, hi

// 大小写转换
fmt.Println(strings.ToUpper("hello")) // HELLO
fmt.Println(strings.ToLower("WORLD")) // world

Rune

1. Rune的定义
  • rune 是 Go 的一个别名类型,等价于 int32,用于表示单个 Unicode 字符
  • 每个 rune 占用4个字节,能够表示所有 Unicode 代码点。

2. Rune的用途
  • 用于处理多字节字符(如汉字、emoji)或逐字符操作。
  • 可以将字符串转换为 []rune,以逐字符处理,而不是逐字节。

3. Rune的常见操作
字符串与Rune的相互转换
  • 将字符串转为 []rune

    runes := []rune("你好世界")
    fmt.Println(runes)  // [20320 22909 19990 30028]
    
  • []rune 转为字符串:

    s := string([]rune{20320, 22909, 19990, 30028})
    fmt.Println(s)  // 你好世界
    
逐字符处理
  • 使用 for遍历 []rune 可以逐字符操作:

    s := "hello, 世界"
    for i, r := range []rune(s) {
        fmt.Printf("第%d个字符:%c (Unicode: %U)\n", i, r, r)
    }
    
修改字符串
  • 字符串不可变,但可以通过 []rune修改后重新构造:

    s := "hello"
    runes := []rune(s)
    runes[0] = 'H'
    s = string(runes)
    fmt.Println(s)  // Hello
    
统计字符数量
  • 使用 len() 获取的是字节数。若要获取字符数,需要将字符串转换为 []rune

    s := "你好世界"
    fmt.Println(len(s))           // 12(字节数)
    fmt.Println(len([]rune(s)))   // 4(字符数)
    

字符串与Rune的关系

特性字符串(string)rune
类型不可变的字节序列表示单个 Unicode 代码点
内存占用每个字符占用 1~4 字节(UTF-8 编码)每个字符固定占用 4 字节(int32 类型)
表示范围UTF-8 编码的字节序列Unicode 字符
适用场景用于存储和操作整段文本用于逐字符处理,支持多字节字符
索引操作索引访问的是字节可以通过 []rune 访问字符

使用场景与注意事项

  1. 字符串处理
    • 如果主要处理整段文本数据,使用 string
    • 避免直接索引多字节字符,否则可能导致错误。
  2. 多字节字符处理
    • 如果需要逐字符操作,使用 []rune
    • 转换为 []rune 后,才能安全地进行字符级别的索引和修改。
  3. 性能权衡
    • 操作 string 通常更高效,因为底层是只读的 []byte
    • 使用 []rune 需要额外的内存,适合复杂字符处理场景。

总结
  • 字符串(string):主要用于存储和处理文本,适合完整的文本操作。支持丰富的库函数,如查找、替换、切分等。
  • Rune:用来处理单个 Unicode 字符,尤其适合多字节字符和逐字符遍历操作。
  • 转换技巧[]runestring 的相互转换,是处理字符和文本的桥梁。
package main

import (
    "fmt"
    "unicode/utf8"
)

func main() {

    const s = "สวัสดี"

    fmt.Println("Len:", len(s))

    for i := 0; i < len(s); i++ {
        fmt.Printf("%x ", s[i])
    }
    fmt.Println()

    fmt.Println("Rune count:", utf8.RuneCountInString(s))

    for idx, runeValue := range s {
        fmt.Printf("%#U starts at %d\n", runeValue, idx)
    }

    fmt.Println("\nUsing DecodeRuneInString")
    for i, w := 0, 0; i < len(s); i += w {
        runeValue, width := utf8.DecodeRuneInString(s[i:])
        fmt.Printf("%#U starts at %d\n", runeValue, i)
        w = width

        examineRune(runeValue)
    }
}

func examineRune(r rune) {

    if r == 't' {
        fmt.Println("found tee")
    } else if r == 'ส' {
        fmt.Println("found so sua")
    }
}
$ go run strings-and-runes.go
Len: 18
e0 b8 aa e0 b8 a7 e0 b8 b1 e0 b8 aa e0 b8 94 e0 b8 b5
Rune count: 6
U+0E2A 'ส' starts at 0
U+0E27 'ว' starts at 3
U+0E31 'ั' starts at 6
U+0E2A 'ส' starts at 9
U+0E14 'ด' starts at 12
U+0E35 'ี' starts at 15
Using DecodeRuneInString
U+0E2A 'ส' starts at 0
found so sua
U+0E27 'ว' starts at 3
U+0E31 'ั' starts at 6
U+0E2A 'ส' starts at 9
found so sua
U+0E14 'ด' starts at 12
U+0E35 'ี' starts at 15

十三.结构体

1. 定义结构体

结构体使用 type 关键字定义,格式如下:

type StructName struct {
    Field1 Type1
    Field2 Type2
    ...
}
示例:
type Person struct {
    Name string
    Age  int
}

2. 创建结构体实例

创建结构体实例的方式有以下几种:

2.1 使用结构体字面量
p := Person{"Alice", 30}
fmt.Println(p)
2.2 使用字段名初始化(推荐)
p := Person{Name: "Bob", Age: 25}
fmt.Println(p)
2.3 创建空结构体并赋值
var p Person
p.Name = "Charlie"
p.Age = 35
fmt.Println(p)
2.4 使用 new 创建结构体指针
p := new(Person)
p.Name = "Diana"
p.Age = 40
fmt.Println(*p)

3. 结构体的零值

结构体的零值是其所有字段的零值,例如:

type Example struct {
    IntField    int
    StringField string
}
var e Example
fmt.Println(e) // 输出:{0 ""}

4. 结构体字段的访问与修改

结构体字段可以通过点操作符(.)访问和修改:

p := Person{Name: "Eve", Age: 28}
fmt.Println(p.Name) // 访问字段
p.Age = 29          // 修改字段
fmt.Println(p.Age)

5. 结构体的比较

两个结构体实例可以直接比较,但前提是它们的所有字段都支持比较操作。

p1 := Person{Name: "Alice", Age: 30}
p2 := Person{Name: "Alice", Age: 30}
fmt.Println(p1 == p2) // 输出:true

6. 匿名字段

结构体支持匿名字段(嵌套结构体),可以通过类型名直接使用:

type Address struct {
    City, State string
}

type User struct {
    Name    string
    Age     int
    Address // 匿名字段
}

u := User{Name: "Frank", Age: 32, Address: Address{City: "New York", State: "NY"}}
fmt.Println(u.City) // 直接访问匿名字段的成员

7. 结构体的方法

Go 中的方法是绑定到特定类型(包括结构体)的函数。

7.1 定义方法

方法定义时,必须有一个接收者(receiver)。接收者可以是值类型或指针类型。

func (p Person) SayHello() {
    fmt.Printf("Hello, my name is %s\n", p.Name)
}
7.2 调用方法
p := Person{Name: "Grace", Age: 24}
p.SayHello() // 调用方法
7.3 值接收者 vs 指针接收者
  • 值接收者:方法操作的是结构体的副本,不会修改原始数据。
  • 指针接收者:方法操作的是结构体的指针,可以修改原始数据。
示例:
func (p Person) ChangeName(newName string) {
    p.Name = newName // 只修改副本,不影响原始数据
}

func (p *Person) UpdateName(newName string) {
    p.Name = newName // 修改原始数据
}

p := Person{Name: "Hank", Age: 26}
p.ChangeName("Ivan")
fmt.Println(p.Name) // 输出:Hank

p.UpdateName("Jack")
fmt.Println(p.Name) // 输出:Jack

8. 嵌套结构体

结构体可以嵌套另一个结构体以实现更复杂的数据结构:

type Company struct {
    Name    string
    Address Address
}

c := Company{Name: "Tech Corp", Address: Address{City: "San Francisco", State: "CA"}}
fmt.Println(c.Address.City)

9. JSON 与结构体

结构体可以与 JSON 数据相互转换,需使用 encoding/json 包。

9.1 转换为 JSON
import "encoding/json"

p := Person{Name: "Lily", Age: 22}
jsonData, _ := json.Marshal(p)
fmt.Println(string(jsonData))
9.2 从 JSON 转换为结构体
jsonStr := `{"Name": "Mike", "Age": 29}`
var p Person
_ = json.Unmarshal([]byte(jsonStr), &p)
fmt.Println(p)
9.3 使用标签自定义 JSON 字段
type Person struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
}

10. 结构体的内存布局

结构体的内存分配是连续的,字段的顺序会影响内存对齐和大小。

优化内存对齐
type Optimized struct {
    A int64
    B int32
    C int16
}

type NonOptimized struct {
    B int32
    C int16
    A int64
}

Optimized 的字段顺序可以减少内存填充,提高内存使用效率。

package main

import "fmt"

type person struct {
    name string
    age  int
}

func newPerson(name string) *person {

    p := person{name: name}
    p.age = 42
    return &p
}

func main() {

    fmt.Println(person{"Bob", 20})

    fmt.Println(person{name: "Alice", age: 30})

    fmt.Println(person{name: "Fred"})

    fmt.Println(&person{name: "Ann", age: 40})

    fmt.Println(newPerson("Jon"))

    s := person{name: "Sean", age: 50}
    fmt.Println(s.name)

    sp := &s
    fmt.Println(sp.age)

    sp.age = 51
    fmt.Println(sp.age)
}
$ go run structs.go
{Bob 20}
{Alice 30}
{Fred 0}
&{Ann 40}
&{Jon 42}
Sean
50
51
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值