Go语言学习记录

学习时间段

时长:2个月
目标:掌握基础语法,练习一个小项目

2024-06-02 2024-06-09 2024-06-16 2024-06-23 2024-06-30 2024-07-07 2024-07-14 2024-07-21 2024-07-28 2024-08-04 已学习时长 学习计划时长 归纳整理总结 时长 学习时间段

2024-06-02学习记录

安装&环境配置

安装包下载地址:https://golang.google.cn/dl/
这里我选择下载:1.21.9最新稳定版本用于学习

Go安装包内容

/bin:包含可执行文件,如:编译器,Go 工具
/doc:包含示例程序,代码工具,本地文档等
/lib:包含文档模版
/misc:包含与支持 Go 编辑器有关的配置文件以及 cgo 的示例
/os_arch:包含标准库的包的对象文件(.a)
/src:包含源代码构建脚本和标准库的包的完整源代码(Go 是一门开源语言)
/src/cmd:包含 Go 和 C 的编译器和命令行脚本

统一入门姿势:hello world实现

在这里插入图片描述

Go语法初学

Go 运行时(runtime)

尽管 Go 编译器产生的是本地可执行代码,然而这些代码仍旧运行在 Go 的 runtime(这部分的代码可以在 runtime包中找到)当中。这个 runtime 类似 Java 和 .NET 语言所用到的虚拟机,它负责管理包括内存分配、垃圾回收、栈处理、goroutine、channel、切片(slice)、map 和反射(reflection)等等。

runtime 主要由 C 语言编写(自 Go 1.5 起开始自举),并且是每个 Go 包的最顶级包。你可以在目录 $GOROOT/src/runtime 中找到相关内容。

垃圾回收器 Go 拥有简单却高效的标记 - 清除回收器。它的主要思想来源于 IBM 的可复用垃圾回收器,旨在打造一个高效、低延迟的并发回收器。目前 gccgo 还没有回收器,同时适用于 gc 和 gccgo 的新回收器正在研发中。使用一门具有垃圾回收功能的编程语言不代表你可以避免内存分配所带来的问题,分配和回收内容都是消耗 CPU 资源的一种行为。

Go 的可执行文件都比相对应的源代码文件要大很多,这恰恰说明了 Go 的 runtime 嵌入到了每一个可执行文件当中。当然,在部署到数量巨大的集群时,较大的文件体积也是比较头疼的问题。但总得来说,Go 的部署工作还是要比 Java 和 Python 轻松得多。因为 Go 不需要依赖任何其它文件,它只需要一个单独的静态文件,这样你也不会像使用其它语言一样被各种不同版本的依赖文件混淆。

Go解释器

因为 Go 具有像动态语言那样快速编译的能力,自然而然地就有人会问 Go 语言能否在 REPL(read-eval-print loop)编程环境下实现。

点击了解:Go解释器具体代码实现

2024-06-03学习记录

交叉编译

在日常工作中,线上机器一般是linux。开发机有可能是Windows,MacOs。这时我们build的代码在线上机可能就没办法运行了。Go支持交叉编译, 在一个平台上生成然后再另外一个平台去执行。具体命令如下:

要去linux下执行

CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build main.go

要去Mac下执行

CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build main.go

要去win下执行

CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build main.go

构建参数说明

  • CGO_ENABLED : CGO 表示golang中的工具,CGO_ENABLED 表示CGO禁用,交叉编译中不能使用CGO的
  • GOOS : 目标平台
  • mac 对应 darwin
  • linux 对应 linux
  • windows 对应 windows
  • GOARCH :目标平台的体系架构【386,amd64,arm】, 目前市面上的个人电脑一般都是amd64架构的386也称 x86 对应 32位操作系统,amd64 也称 x64 对应 64位操作系统,arm 这种架构一般用于嵌入式开发。 比如 Android , IOS , Win mobile , TIZEN 等

参数声明

Go语言的变量声明格式为:

var 变量名 变量类型

  • 变量声明以关键字var开头,变量类型放在变量的后面,行尾无需分号。

申明示例:

package main

func main() {
	var name string
	name = "zhangsan"
	println(name)
}

变量输出:
在这里插入图片描述

批量声明:

  • 每声明一个变量就需要写var关键字会比较繁琐,go语言中还支持批量变量声明
    var (
        a string
        b int
        c bool
        d float32
    )

变量初始化:

Go语言在声明变量的时候,会自动对变量对应的内存区域进行初始化操作。每个变量会被初始化成其类型的默认值,例如: 整型和浮点型变量的默认值为0。 字符串变量的默认值为空字符串。 布尔型变量默认为false。 切片、函数、指针变量的默认为nil

变量申明并赋值

var 变量名 类型 = 表达式

package main

func main() {
	var name string = "zhangsan"
	println(name)
}

一次申明多个变量

package main

func main() {
	var name string, age int = "zhangsan", 15
	println("姓名:", name, "年龄:", age)
}

类型推导:

将变量的类型省略,这个时候编译器会根据等号右边的值来推导变量的类型完成初始化

package main

func main() {
	var name = "zhangsan"
	println("姓名:", name)
}

短变量申明:

package main

import "fmt"

// 全局变量m
var m = 100

func main() {
	n := 10
	//m := 200 // 此处声明局部变量m, 注意:局部变量会覆盖全局变量
	fmt.Println(m, n)
}

匿名变量:

在使用多重赋值时,如果想要忽略某个值,可以使用匿名变量(anonymous variable)。 匿名变量用一个下划线_表示

package main

import "fmt"

func foo() (int, string) {
	return 10, "Q1mi"
}

func main() {
	x, _ := foo()
	_, y := foo()
	fmt.Println("x=", x)
	fmt.Println("y=", y)
}

变量申明注意事项:

  • 函数外的每个语句都必须以关键字开始(var、const、func等)
  • :=不能使用在函数外。
  • _多用于占位,表示忽略值。

常量

相对于变量,常量是恒定不变的值,多用于定义程序运行期间不会改变的那些值。 常量的声明和变量声明非常类似,只是把var换成了const,常量在定义的时候必须赋值。

const version = 1.0.0

多个常量也可以一起声明:

const (
    version = 1.0.0
    image = test_image_20240508.tar.gz
)

const同时声明多个常量时,如果省略了值则表示和上面一行的值相同

const (
    version = 1.0.0
    image
)

iota

iota是go语言的常量计数器,只能在常量的表达式中使用。iota在const关键字出现时将被重置为0。const中每新增一行常量声明将使iota计数一次(iota可理解为const语句块中的行索引)。 使用iota能简化定义,在定义枚举时很有用。

计数

const (
        n1 = iota //0
        n2        //1
        n3        //2
        n4        //3
)

使用_跳过某些值


const (
        n1 = iota //0
        n2        //1
        _
        n4        //3
    )

iota声明中间插队

    const (
            n1 = iota //0
            n2 = 100  //100
            n3 = iota //2
            n4        //3
        )
    const n5 = iota //0

定义数量级

这里的<<表示左移操作,1<<10表示将1的二进制表示向左移10位,也就是由1变成了10000000000,也就是十进制的1024。同理2<<2表示将2的二进制表示向左移2位,也就是由10变成了1000,也就是十进制的8。

 const (
         _  = iota
         KB = 1 << (10 * iota)
         MB = 1 << (10 * iota)
         GB = 1 << (10 * iota)
         TB = 1 << (10 * iota)
         PB = 1 << (10 * iota)
     )

多个iota定义在一行, 从第一行开始计数为0,第二行为1… 以此类推

 const (
         a, b = iota + 1, iota + 2 //1,2
         c, d                      //2,3
         e, f                      //3,4
     )

2024-06-04学习记录

Go基础数据类型

以下是 Go 中可用的基本类型:

  • bool (布尔)
  • 数值类型
    • int8、int16、int32、int64、int
    • uint8、uint16、uint32、uint64、uint
    • float32, float64
    • complex64, complex128
    • byte
    • rune
  • string

布尔类型

一般我们用于判断条件, 它的取值范围为 true, false

// 短类型声明 (推荐使用) 只能用于函数内部
var isExit := true
// 或完整申明
var isExit bool = false

字符串类型

字符串是 Go 中字节的集合。

var say = "hello" //单行字符串
var tag = "\"" // 转义符

var say = `hello` //原样输出
var mLine = `line1  //多行输出
line2
line3
`
var str = "hello, 世界"

数字类型

数字类型主要分为有符号数和无符号数,有符号数可以用来表示负数,除此之外它们还有位数的区别,不同的位数代表它们实际存储占用空间,以及取值的范围。

具体范围

uint8 the set of all unsigned 8-bit integers (0 to 255)
uint16 the set of all unsigned 16-bit integers (0 to 65535)
uint32 the set of all unsigned 32-bit integers (0 to 4294967295)
uint64 the set of all unsigned 64-bit integers (0 to 18446744073709551615)

int8 the set of all signed 8-bit integers (-128 to 127)
int16 the set of all signed 16-bit integers (-32768 to 32767)
int32 the set of all signed 32-bit integers (-2147483648 to 2147483647)
int64 the set of all signed 64-bit integers (-9223372036854775808 to 9223372036854775807)

float32 the set of all IEEE-754 32-bit floating-point numbers
float64 the set of all IEEE-754 64-bit floating-point numbers

complex64 the set of all complex numbers with float32 real and imaginary parts
complex128 the set of all complex numbers with float64 real and imaginary parts

类型转换

Go 对显式类型非常严格。没有自动类型提升或转换。

package main
 
import "fmt"
 
func main() {  
    i := 55      //int
    j := 67.8    //float64
    sum := i + j //int + float64 
    fmt.Println(sum)
}

上面的代码在 java 语言中是完全合法的。但是在 go 的情况下,这是行不通的。i 是 int 类型,j 是 float64 类型。我们正在尝试添加 2 个不同类型的数字,这是不允许的。当你运行程序时,你会得到: invalid operation: i + j (mismatched types int and float64)

转换类型:float 转换为 int类型将损失精度

package main

import "fmt"

func main() {
	i := 55           //int
	j := 67.8         //float64
	sum := i + int(j) // 将float转换为int, 这里转换会损失精度, 小数点后的数值会被省略
	fmt.Println(sum)
}

赋值操作

package main

import "fmt"

func main() {
	i := 10           //int
	var j float64 = float64(i) //float64  这里显示申明也同样需要转换类型
	fmt.Println(j)
}

循环与条件判断

for

循环语句用于重复执行一段代码。
for是 Go 中唯一可用的循环。Go 没有其他语言(如 Java)中存在的 while 或 do while 循环。

for initialisation; condition; post {  
}

初始化语句将只执行一次。循环初始化后,将检查条件。如果条件评估为真,则将执行内部循环的主体{ },然后执行 post 语句。post 语句将在循环的每次成功迭代后执行。post语句执行后,条件会被重新检查。如果为真,循环将继续执行,否则 for 循环终止。

示例:

package main
 
import "fmt"
 
func main() {  
    for i := 1; i <= 10; i++ {
        fmt.Printf(" %d",i)
    }
}

1、在上面的程序中,i被初始化为 1。条件语句将检查 if i <= 10。如果条件为真,则打印 i 的值,否则终止循环。post 语句在每次迭代结束时将i 递增 1。一旦i大于 10,循环终止。
2、在 for 循环中声明的变量仅在循环范围内可用。因此i不能在 for 循环体之外访问。
3、上面的程序将打印1 2 3 4 5 6 7 8 9 10

在这里插入图片描述
break

该break语句用于在 for 循环完成正常执行之前突然终止 for 循环,并将控件移动到 for 循环之后的代码行。

package main
 
import "fmt"
 
func main() {  
    for i := 1; i <= 10; i++ {
    	if i > 5 {
			break // 当i等于5时, 退出循环
		}
        fmt.Printf(" %d",i)
    }
}

在上面的程序中,每次迭代都会检查 i 的值。如果 i 大于 5 则break执行并终止循环。然后执行 for 循环之后的 print 语句。

continue

该continue语句用于跳过 for 循环的当前迭代。在 continue 语句之后出现在 for 循环中的所有代码都不会在当前迭代中执行。循环将继续进行下一次迭代。

package main

import "fmt"

func main() {
	for i := 1; i <= 10; i++ {
		if i == 5 {
			continue
		}
		fmt.Printf(" %d", i)
	}
}

在这里插入图片描述

2024-06-05学习记录

判断语句

if else 语句

if 主要用于条件判断,语法为:
if 条件 { 业务代码 }

package main

import "fmt"

func main() {
    age := 7  //赋值为7,则满足大于6的条件,控制台会打印good
    if age > 6 {
        fmt.Println("good")
    }
}

与 && 、或 || 来进行组合判断

package main

import "fmt"

func main() {
    age := 7  //赋值为7
    
    //需要满足age大于6  并且 小于12 则成立
    if age > 6 && age < 12 {
        fmt.Println("good")
    }
    //需要满足age大于6 或者 小于12 都成立
    if age > 6 || age < 12 {
        fmt.Println("good")
    }
}

if…else if…else

package main

import "fmt"

func main() {
    age := 7  //赋值为7
    
    //需要满足age大于6  并且 小于12 则成立
    if age > 6 && age < 12 {
        fmt.Println("good")
    } else if age > 6 || age < 12 { //需要满足age大于6 或者 小于12 都成立
        fmt.Println("good")
    } else{ // 若以上条件都不成立,则输出else中的语句
		fmt.Println("未匹配到满足项")
	}
}

Go的使用习惯

我们已经看到了各种if-else结构,实际上我们也看到了编写相同程序的多种方法。在Go的哲学中,最好避免不必要的代码分支和缩进。人们也认为,越早返回越好。

package main

import "fmt"

func main() {
    age := 7  //赋值为7
    
    //需要满足age大于6  并且 小于12 则成立
    if age > 6 && age < 12 {
        fmt.Println("good")
        return
    } else if age > 6 || age < 12 { //需要满足age大于6 或者 小于12 都成立
        fmt.Println("good")
        return                     //结束语句
    } 
    // 若以上条件都不成立,则输出else中的语句
	fmt.Println("未匹配到满足项")
}

switch

switch 是一个条件语句,它计算表达式并将其与可能匹配的列表进行比较并执行相应的代码块。它可以被认为是替换复杂if else从句的惯用方式。当然这里的语法与java一致。

package main

import "fmt"

func main() {
    age := 10
    switch age {
    case 5:
        fmt.Println("The age is 5")
    case 7:
        fmt.Println("The age is 7")
    case 10:
        fmt.Println("The age is 10")  // 满足该条件,则会执行
    default:
        fmt.Println("The age is unkown")  //以上都不满足则执行默认语句
    }
}

注意:在 Go 中 switch 只要匹配中了就会中止剩余的匹配项,这和 Java 很大不一样,它需要使用 break 来主动跳出。

switch 的 case 条件可以是多个值:
注意: 同一个 case 中的多值不能重复。

package main

import "fmt"

func main() {
    age := 7

    switch age {
    case 7, 8, 9, 10, 11, 12:
        fmt.Println("It's primary school")
    case 13, 14, 15:
        fmt.Println("It's middle school")
    case 16, 17, 18:
        fmt.Println("It's high school")
    default:
        fmt.Println("The age is unkown")
    }
}

2024-06-06学习记录

数组

数组是具有相同 唯一类型 的一组以编号且长度固定的数据项序列。 例如,整数 5、8、9、79、76 的集合形成一个数组

数据的长度是固定的。我们在声明一个数组时需要指定它的长度,一旦指定了长度,那么它的长度值是不可以改变的。

数组的声明

一个数组的表示形式为 T[n]。n 表示数组中元素的数量,T 代表每个元素的类型。元素的数量n也是该类型的一部分。

package main
 
import "fmt"
 
func main() {  
    var a [3]int // 定义长度为 3 的 int 类型数组
    a[0] = 1 // 第一个索引位置设置数值
    a[1] = 2 // 第二个索引位置设置数值
    a[2] = 3 // 第三个索引位置设置数值
    fmt.Println(a)
}

 
func main2() {  
    a := [3]int{1, 2, 3} // 简写模式,在定义的同时给出了赋值
    fmt.Println(a)
}

不定长度数组申明,由编译器完成长度计算

package main 
 
import "fmt"
 
func main() {  
    a := [...]int{1, 2, 3} // 也可以不显式定义数组长度,由编译器完成长度计算
    fmt.Println(a)
}

数组是值类型

Go 中的数组是值类型而不是引用类型。这意味着当数组赋值给一个新的变量时,该变量会得到一个原始数组的一个副本。如果对新变量进行更改,则不会影响原始数组。

package main
 
import "fmt"
 
func main() {  
    a := [...]string{"中国", "美国", "日本", "法国"}
    b := a // 将 a 数组赋值给数组 b, 这里是b拷贝了a的备份, b的改变不会影响a
    b[1] = "俄罗斯"
    fmt.Println("a is ", a)
    fmt.Println("b is ", b) 
}

使用 range 遍历数组

for 循环可用于遍历数组中的元素。

package main
 
import "fmt"
 
func main() {  
    a := [...]float64{67.7, 89.8, 21, 78}
    for i := 0; i < len(a); i++ {  // 使用len()获取数组长度。
        fmt.Printf("%d th element of a is %.2f\n", i, a[i])
    }
}

Go 提供了一种更好、更简洁的方法,通过使用 for 循环的 range 方法来遍历数组。range 返回索引和该索引处的值。还可以获取数组中所有元素的总和。

package main
 
import "fmt"
 
func main() {  
    a := [...]float64{67.7, 89.8, 21, 78}
    sum := float64(0)
    for i, v := range a {  // 这里的i等于索引  v等于值
        fmt.Printf("%d the element of a is %.2f\n", i, v)
        sum += v  //计算数组总和
    }
    fmt.Println("\nsum of all elements of a",sum)
}

多维数组

常用于算法中的平面计算。

package main

import "fmt"

func printarray(a [3][2]string) {  
    for _, v1 := range a { // 这里取出平面中第一行数组
        for _, v2 := range v1 { // 这里取出第一行数组中的第一个索引的数值
            fmt.Printf("%s ", v2)
        }
        fmt.Printf("\n")
    }
}
 
func main() {  
    a := [3][2]string{
        {"lion", "tiger"},
        {"cat", "dog"},
        {"pigeon", "peacock"},  //这里的逗号是必须的
    }
    printarray(a)
}

2024-06-10学习记录

切片

切片(slice)是对数组一个连续片段的引用(该数组我们称之为相关数组,通常是匿名的),所以切片是一个引用类型。
实际开发中我们很少使用数组,取而代之的是切片。切片是一个 长度可变的数组。

创建切片

package main

import "fmt"

func main() {  
    a := [5]int{76, 77, 78, 79, 80}
    var b []int = a[1:4] //创建一个切片 a[1] to a[3]
    fmt.Println(b)
}

使用语法 a[start:end] 创建一个从 a 数组索引 start 开始到 end - 1 结束的切片。因此,在上述程序的第 9 行中, a[1:4] 为从索引 1 到 3 创建了 a 数组的一个切片表示。因此, 切片 b的值为 [77 78 79]。

修改切片

切片自己不拥有任何数据。它只是底层数组的一种表示。对切片所做的任何修改都会反映在底层数组中。

package main
 
import  "fmt"

func main() {  
    darr := [...]int{57, 89, 90, 82, 100, 78, 67, 69, 59}
    dslice := darr[2:5]
    fmt.Println("array before",darr)
    for i := range dslice {
        dslice[i]++  //这里我们将切片数组内的元素进行+1操作
    }
    fmt.Println("array after",darr) 
}

在上面程序的第 9 行,我们根据数组索引 2,3,4 创建一个切片 dslice。for 循环将这些索引中的值逐个递增。当重新使用 for 循环打印数组时,可以看到对切片的更改反映到了数组中。该程序的输出为:
array before [57 89 90 82 100 78 67 69 59]
array after [57 89 91 83 101 78 67 69 59]

切片的长度和容量

package main
 
import "fmt"
 
func main() {  
    fruitarray := [...]string{"apple", "orange", "grape", "mango", "water melon", "pine apple", "chikoo"}
    fruitslice := fruitarray[1:3]
    fmt.Printf("length of slice %d capacity %d", len(fruitslice), cap(fruitslice)) 
}

在上面的程序中,从 fruitarray 的索引 1 和 2 创建fruitslice 。 因此,fruitlice 的长度为 2。

fruitarray 的长度是 7。fruiteslice 是从 fruitarray 的索引 1 开始创建的。因此, fruitslice的容量是从 fruitarray 索引为 1开始,也就是说从 orange 开始,该值为 6。因此, fruitslice的容量为 6。该程序输出length of slice 2 capacity 6 。

切片容量重置

package main
 
import "fmt"
 
func main() {  
    fruitarray := [...]string{"apple", "orange", "grape", "mango", "water melon", "pine apple", "chikoo"}
    fruitslice := fruitarray[1:3]
    fmt.Printf("length of slice %d capacity %d\n", len(fruitslice), cap(fruitslice)) //length of is 2 and capacity is 6
    fruitslice = fruitslice[:cap(fruitslice)] 
    fmt.Println("After re-slicing length is",len(fruitslice), "and capacity is",cap(fruitslice))
}

切片的切片操作可以通过索引来完成,它的一般形式是 slice[low:high],其中 low 是切片的新起始索引,high 是切片的新结束索引(但不包含 high 位置的元素)。这里还有一种简写形式,可以直接省略 low 或 high,使用默认值,比如 slice[:] 表示从当前切片的起始位置到结束位置的一个新切片,保持原切片的长度和容量;而 slice[low:] 则表示从 low 索引处开始到原切片结束的新切片。

当我们使用 slice[:cap(slice)] 这样的表达式时,它是切片操作的一种特殊形式,具体意义如下:

  • cap(slice) 是一个内置函数,用于获取切片的容量,即切片所能达到的最大长度,而不必分配新的底层数组。
  • slice[:cap(slice)] 表示创建一个新的切片,它的起始位置仍然是原切片的起始位置,但结束位置扩展到了原切片的容量。换句话说,这会使得新切片的长度增加到与当前容量相同,前提是原切片后面有足够的元素可以包含。

这种用法常用于想要利用已分配的内存空间,尤其是在需要在不重新分配内存的情况下向切片追加更多元素的场景。通过这种方式扩展切片的长度,直到达到其最大容量,之后如果还需要继续增长,则会分配一个新的更大的底层数组。

2024-06-11学习记录

make

使用 make 创建一个切片

func make([]T,len,cap)[]T 通过传递类型,长度和容量来创建切片。容量是可选参数, 默认值为切片长度。make 函数创建一个数组,并返回引用该数组的切片。

package main
 
import "fmt"
 
func main() {  
    i := make([]int, 5, 5)  //func make([]T,len,cap)[]T 通过传递类型,长度和容量来创建切片
    fmt.Println(i)
}

使用 make 创建切片时默认情况下这些值为零。上面程序的输出为 [0 0 0 0 0]。

追加切片元素

数组的长度是固定的,它的长度不能增加。切片是动态的,使用 append 可以将新元素追加到切片上。append 函数的定义是 func append(s[]T,x ... T)[]T
x ... T 在函数定义中表示该函数接受参数x的个数是可变的。这些类型的函数被称为可变参函数。

切片扩容机制(旧数组的2倍)

有一个问题可能会困扰你。如果切片由数组支持,并且数组本身的长度是固定的,那么切片如何具有动态长度。以及内部发生了什么,当新的元素被添加到切片时,会创建一个新的数组。现有数组的元素被复制到这个新数组中,并返回这个新数组的新切片引用。现在新切片的容量是旧切片的两倍。

package main
 
import "fmt"
 
func main() {  
    cars := []string{"Ferrari", "Honda", "Ford"}
    fmt.Println("cars:", cars, "has old length", len(cars), "and capacity", cap(cars)) 
    cars = append(cars, "Toyota")
    fmt.Println("cars:", cars, "has new length", len(cars), "and capacity", cap(cars)) 
}

在上面程序中,cars的容量最初是 3。在第 10 行,我们给 cars 添加了一个新的元素,并把 append(cars, "Toyota") 返回的切片赋值给 cars。现在 cars 的容量翻了一番,变成了 6。上面程序的输出为:

cars: [Ferrari Honda Ford] has old length 3 and capacity 3
cars: [Ferrari Honda Ford Toyota] has new length 4 and capacity 6

切片的函数传递

可以认为切片在内部由结构类型表示。看起来是这样的:

type slice struct {  
    Length        int
    Capacity      int
    ZerothElement *byte
}

切片包含长度、容量和指向数组第零元素的指针。当切片传递给函数时,即使它是按值传递的,指针变量也会引用同一个底层数组。因此,当切片作为参数传递给函数时,函数内部所做的更改在函数外部也可见。让我们编写一个程序来检查一下。

package main
 
import  "fmt"

func subtactOne(numbers []int) {  
    for i := range numbers {
        numbers[i] -= 2
    }
}
func main() {  
    nos := []int{8, 7, 6}
    fmt.Println("slice before function call", nos)
    subtactOne(nos) // 值传递                            
    fmt.Println("slice after function call", nos) 
}

上面程序,调用函数将切片中的每个元素递减 2。在函数调用后打印切片时,这些更改是可见的。
slice before function call [8 7 6]
slice after function call [6 5 4]

多维切片

与数组类似,切片可以有多个维度。

package main
 
import "fmt"

func main() {  
     pls := [][]string {
	            {"C", "C++"},
	            {"Java"},
	            {"Go", "Rust"},
            }
    for _, v1 := range pls {
        for _, v2 := range v1 {
            fmt.Printf("%s ", v2)
        }
        fmt.Printf("\n")
    }
}

切片总结

在Go语言中,切片(slice)相比数组(array)更常被推荐和使用,原因主要包括以下几点:

  • 动态大小:切片的长度可以在运行时改变,这让它们非常适合处理大小不确定的数据集,相比之下数组的长度在声明时就必须确定且之后不可更改。
  • 内存效率:切片本身是一个轻量级结构,只包含一个指向底层数组的指针、长度和容量,这使得切片在传递和赋值时更加高效,尤其是在处理大量数据时。
  • 自动扩容:当需要向切片追加元素而当前容量不足时,Go会自动管理底层数组的扩容,这对于开发者来说是透明的,减少了手动管理内存的需求。
  • 共享数据:多个切片可以共享同一个底层数组,这在某些场景下可以提高内存使用效率并简化代码逻辑。

尽管如此,数组在某些特定场景下仍有其用武之地,比如:
当确切知道所需数据大小且不希望数据在运行时改变时。
在性能敏感的循环中,尤其是当避免切片扩容带来的额外开销很重要时。
作为固定大小的数据缓冲区或其他底层数据结构的一部分。
总的来说,Go语言中提倡多使用切片,以利用其灵活性和高效性,而数组则更多地用于那些对数据大小有严格要求或特定性能考量的场景。

参考:
Go 为什么建议使用切片,少使用数组?
Go 语言为什么建议多使用切片,少使用数组?
深入分析Go语言切片的工作原理

2024-06-12学习记录

map

概念 : map 一种无序的键值对, 它是数据结构 hash 表的一种实现方式。map工作方式就是:定义键和值,并且可以获取,设置和删除其中的值。

声明:可以通过将键和值的类型make传递给函数来创建映射。make(map[type of key]type of value)

package main
 
import "fmt"
    
func main() {  
	// 使用关键字 map 来声明
	lookup := map[string]int{ "goku": 9001, "gohan": 2044, }
	// 使用make来声明
	cMap := make(map[string]int)
	cMap["北京"] = 1
    fmt.Println("lookup:",lookup)
    fmt.Println("cMap:",cMap)
}

将元素添加到 Map

将元素添加到 Map 的语法与数组相同。上面的程序就是两种添加 map 的方式。
键不一定只能是 string 类型。所有可比较的类型,如 boolean,interger,float,complex,string 等,都可以作为键。即使是用户定义的类型(例如结构)也可以是键。

Map的零值

Map的零值是nil。如果您尝试向 Map添加元素,则会发生nil运行时报错。因此,必须在添加元素之前初始化 Map。

package main
 
func main() {  
    var employeeSalary map[string]int
    employeeSalary["steve"] = 12000
}

在上面的程序中,employeeSalarynil。而我们正在尝试向 Map添加一个新键。程序因错误而报错:panic: assignment to entry in nil map

检索键的值
现在我们已经向 Map添加了一些元素,让我们学习如何检索它们。检索 Map元素的语法为map[key]

package main

import "fmt"

func main() {
	cities := map[string]int{
		"北京": 100000,
		"湖南": 430000,
	}
	city := "北京"
	postCode := cities[city]
	fmt.Println("城市:", city, "邮编:", postCode)
}

检查键值是否存在

当键不存在时,将返回类型的零值。当我们想要找出键是否真的存在于 Map 中时,可以通过value, ok := map[key] , 判断ok即可。

package main
 
import "fmt"

func main() {  
    cities := map[string]int{
        "北京": 100000,
        "湖南": 430000,
    }
    newEmp := "上海"
    value, ok := cities[newEmp]
    if ok == true {
        fmt.Println("邮编:", value)
        return
    }
    fmt.Println(newEmp, "邮编不存在")
}

遍历 Map中的所有元素

可以用for循环的range形式用于迭代 Map的所有元素。

package main

import "fmt"

func main() {
	cities := map[string]int{
		"北京": 100000,
		"湖南": 430000,
	}
	for key, value := range cities {
		fmt.Printf("cities[%s] = %d\n", key, value)
	}
}

值得注意的是,因为 map 是无序的,因此对于程序的每次执行,不能保证使用 for range 遍历 map 的顺序总是一致的,而且遍历的顺序也不完全与元素添加的顺序一致。

从 Map中删除元素

delete(map, key) 用于删除 map 中的键。delete 函数没有返回值。

package main

import "fmt"

func main() {
	cities := map[string]int{
		"北京": 100000,
		"湖南": 430000,
	}
	fmt.Println("map before deletion", cities)
	delete(cities, "北京")
	fmt.Println("map after deletion", cities)
}

上面的程序删除以 北京 为键的元素。程序输出为:
map before deletion map[北京:100000 湖南:430000]
map after deletion map[湖南:430000]

如果我们尝试删除 Map中不存在的键,则不会出现运行时错误。

注意

  • 与切片一样,maps 是引用类型。当一个 map 赋值给一个新的变量,它们都指向同一个内部数据结构。因此改变其中一个也会反映到另一个。

  • 须指定 key, value 的类型,插入的纪录类型必须匹配

  • key 具有唯一性,插入纪录的 key 不能重复

  • KeyType 可以为基础数据类型(例如 bool, 数字类型字符串), 不能为数组,切片,map,它的取值必须是能够使用 == 进行比较。

  • ValueType 可以为任意类型

  • 无序性

  • 线程不安全, 一个 goroutine 在对 map 进行写的时候,另外的 goroutine 不能进行读和写操作,Go 1.6 版本以后会抛出 runtime 错误信息

2024-06-17学习记录

函数

函数声明
在 go 中声明函数的语法是:

func name(parameter) (result-list){
    //body
}

函数声明以func关键字开头,后跟name(函数名). 在括号中指定参数,后面为函数返回值result-list
指定参数的语法是,参数名称后跟类型。可以指定任意数量的参数,例如(parameter1 type, parameter2 type)。而{,}内的代码为函数的主体内容。

函数组成

  • 函数名
  • 参数列表(parameter)
  • 返回值(result-list)
  • 函数体(body)

参数和返回类型在函数中是可选的。因此,以下语法也是有效的函数声明。

func name() {  
}

单返回值函数

package main
 
import "fmt"

func plus(a, b int) (res int){
    return a + b
}

func main() {  
    a, b := 90, 6
    sumAll := plus(a, b)
    fmt.Println("sum", sumAll)
}

上面程序,函数plus 接受两个 int 类型的值,并返回最终和。输出结果如下:

sum 96

多返回值函数
一个函数可以返回多个值。

package main

import "fmt"

func multi() (string, int) {
	return "小李", 18
}

func main() {
	name, age := multi()
	fmt.Println("name:", name, "age:", age)
}

上述程序降会输出:

name: 小李 age: 18

命名返回值
可以从函数返回命名值。如果返回值被命名,则可以认为它在函数的第一行被声明为变量。

// 被命名的返回参数的值为该类型的默认零值
// 该例子中 name 默认初始化为空字符串,height 默认初始化为 0
func namedReturnValue()(name string, height int){
    name = "xiaoming"
    height = 180
    return
}

参数可变函数

package main

import "fmt"

func sum(nums ...int)int{
    fmt.Println("len of nums is : ", len(nums))
    res := 0
    for _, v := range nums{
        res += v
    }
    return res
}

func main(){
    fmt.Println(sum(1))
    fmt.Println(sum(1,2))
    fmt.Println(sum(1,2,3))
}

匿名函数

func main(){
    func(name string){
       fmt.Println(name)
    }("https://www.gotribe.cn")
}

指针

指针是存储另一个变量的内存地址的变量。
变量 b 的值是 156,存储在地址为 0x1040a124的内存中。变量 a 存储了变量 b 的地址。现在可以说 a 指向 b

指针的声明

指向类型 T 的指针用 *T 表示。

package main
 
import "fmt"

func main() {  
    b := 255
    var a *int = &b
    fmt.Printf("Type of a is %T\n", a)
    fmt.Println("address of b is", a)
}

& 操作符用来获取一个变量的地址。在上面的程序中,我们将 b 的地址赋给 aa 的类型为 *int)。现在我们说 a指向了 b。当我们打印 a 的值时,b 的地址将会被打印出来。程序的输出为:

Type of a is *int
address of b is 0x1040a124

你可能得到的是一个不同的 b 的地址,因为 b 可以在内存中的任何地方。

指针的空值

指针的零值为nil。

package main
 
import "fmt"
 
func main() {  
    a := 25
    var b *int
    if b == nil {
        fmt.Println("b is", b)
        b = &a
        fmt.Println("b after initialization is", b)
    }
}

b 在上述程序中最初为 nil,然后分配给 a 的地址。该程序输出

b is nil
b after initialisation is 0x1040a124

使用新函数创建指针

Go 还提供了一个方便的函数new来创建指针。该new函数将一个类型作为参数并返回一个指针,该指针指向作为参数传递的类型的新分配的空值。

package main
 
import "fmt"
 
func main() {  
    size := new(int)
    fmt.Printf("Size value is %d, type is %T, address is %v\n", *size, size, size)
    *size = 85
    fmt.Println("New size value is", *size)
}

Size value is 0, type is *int, address is 0x414020
New size value is 85

指针解引用

解引用指针的意思是通过指针访问被指向的值。指针 a 的解引用表示为:*a

package main  
import "fmt"
 
func main() {  
    b := 255
    a := &b
    fmt.Println("address of b is", a)
    fmt.Println("value of b is", *a)
}
package main
 
import "fmt"
 
func main() {  
    b := 255
    a := &b
    fmt.Println("address of b is", a)
    fmt.Println("value of b is", *a)
    *a++
    fmt.Println("new value of b is", b)
}

我们把 a指向的值加 1,由于 a 指向了 b,因此 b 的值也发生了同样的改变。于是 b 的值变为 256

向函数传递指针参数

函数返回局部变量的指针是完全合法的。Go 编译器足够智能,它会在堆上分配这个变量。

package main
 
import "fmt"
 
func hello() *int {  
    i := 5
    return &i
}
func main() {  
    d := hello()
    fmt.Println("Value of d", *d)
}

我们从hello函数中返回局部变量的地址i。Go编译器会进行转义分析,并在地址转义本地范围时在堆上进行分配。因此,该程序将起作用并打印:Value of d 5

Go 不支持指针运算

Go 不支持其他语言(如 C 和 C++)中存在的指针算法。

package main
 
func main() {  
    b := [...]int{109, 110, 111}
    p := &b
    p++
}

上述程序会抛出编译错误main.go:6: invalid operation: p++ (non-numeric type *[3]int)

注意:不要向函数传递数组的指针,而应该使用切片。

2024-06-18学习记录

结构体

概念

数组、切片和 Map 可以用来表示同一种数据类型的集合,但是当我们要表示不同数据类型的集合时就需要用到结构体。

结构体是由零个或多个任意类型的值聚合成的实体,它可以用于将数据分组为一个单元而不是将它们中的每一个作为单独的值。

声明一个结构体

Go 里面用关键字 type 和 struct 用来定义结构体,语法如下:

type StructName struct{
    FieldName type
}

定义一个学生结构体:

package main

import "fmt"

type Student struct {
    Age     int
    Name    string
}

func main() {
    stu := Student{
        Age:     18,
        Name:    "name",
    }
    fmt.Println(stu)

	// 在赋值的时候,字段名可以忽略
	stu2 := Student{20, "new name"} 
    fmt.Println(stu2)

    return
}

以上申明类似于java对象申明,只不过比java对象要简洁很多。

创建匿名结构体

可以在不创建新数据类型的情况下声明结构。这些类型的结构称为匿名结构。

package main
 
import (  
    "fmt"
)
 
func main() {  
    emp3 := struct {
    	//申明属性
        firstName string
        lastName  string
        age       int
        salary    int
    }{
    	//赋予具体值
        firstName: "Andreah",
        lastName:  "Nikola",
        age:       31,
        salary:    5000,
    }
 
    fmt.Println("Employee 3", emp3)
}

获取结构的各个字段

.运算符用于访问结构的各个字段。

package main
 
import (  
    "fmt"
)
 
type Employee struct {  
    firstName string
    lastName  string
    age       int
    salary    int
}
 
func main() {  
    emp6 := Employee{
        firstName: "Sam",
        lastName:  "Anderson",
        age:       55,
        salary:    6000,
    }
    fmt.Println("First Name:", emp6.firstName)
    fmt.Println("Last Name:", emp6.lastName)
    fmt.Println("Age:", emp6.age)
    fmt.Printf("Salary: $%d\n", emp6.salary)
    emp6.salary = 6500
    fmt.Printf("New Salary: $%d", emp6.salary)
}

结构体的指针

也可以创建指向结构的指针。

package main
 
import (  
    "fmt"
)
 
type Employee struct {  
    firstName string
    lastName  string
    age       int
    salary    int
}
 
func main() {  
    emp8 := &Employee{
        firstName: "Sam",
        lastName:  "Anderson",
        age:       55,
        salary:    6000,
    }
    fmt.Println("First Name:", (*emp8).firstName)
    fmt.Println("Age:", (*emp8).age)
}

Go 语言允许我们在访问 firstName 字段时,可以使用 emp8.firstName 来代替显式的解引用 (*emp8).firstName

package main
 
import (  
    "fmt"
)
 
type Employee struct {  
    firstName string
    lastName  string
    age       int
    salary    int
}
 
func main() {  
    emp8 := &Employee{
        firstName: "Sam",
        lastName:  "Anderson",
        age:       55,
        salary:    6000,
    }
    fmt.Println("First Name:", emp8.firstName)
    fmt.Println("Age:", emp8.age)
}

匿名字段

可以创建具有仅包含类型而没有字段名称的字段的结构。这些类型的字段称为匿名字段。

创建了一个结构Person,它有两个匿名字段string和int。

type Person struct {  
    string
    int
}

即使匿名字段没有明确的名称,默认情况下匿名字段的名称就是其类型的名称。例如,在上面的 Person结构中,虽然字段是匿名的,但默认情况下它们采用字段类型的名称。所以Person结构体有 2 个字段,分别为stringint

package main
 
import (  
    "fmt"
)
 
type Person struct {  
    string
    int
}
 
func main() {  
    p1 := Person{
        string: "naveen",
        int:    50,
    }
    fmt.Println(p1.string)
    fmt.Println(p1.int)
}

嵌套结构体

一个结构可能包含一个字段,而该字段又是一个结构。这些类型的结构称为嵌套结构。

package main
 
import (  
    "fmt"
)
 
type Address struct {  
    city  string
    state string
}
 
type Person struct {  
    name    string
    age     int
    address Address
}
 
func main() {  
    p := Person{
        name: "Naveen",
        age:  50,
        address: Address{
            city:  "Chicago",
            state: "Illinois",
        },
    }
 
    fmt.Println("Name:", p.name)
    fmt.Println("Age:", p.age)
    fmt.Println("City:", p.address.city)
    fmt.Println("State:", p.address.state)
}

上述程序中Person的结构有一个字段address,该字段address又是一个结构。

注意:当定义一个结构体变量,但是没有给它提供初始值,则对应的字段被赋予它们各自类型的零值。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值