Go 菜鸟学习笔记-快速入门(下)

本文是Go语言初学者的学习笔记,涵盖了切片、范围、Map、递归函数、数据类型转换、接口、错误处理、并发、通道和缓冲区等内容。详细解释了Go语言中切片的声明、初始化、容量与长度的关系,以及如何通过append函数增加容量。还讨论了Go的并发特性,包括goroutine和通道,并介绍了通道的缓冲区。最后提到了Go的错误处理和并发中的通道控制读写权限。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Go语言切片

Go语言中切片和数组的区别?

  1. 数组长度不可改变
  2. 切片是对数组的抽象,相对于数组来说,是一种“动态数组”,切片长度不是固定的,可以追加元素,
    感觉和Java 中的 array 与 list 的关系 很像

常见操作
1.声明方式

//1.声明一个未指定大小的数组来定义切片
var identifier []type 
//2.使用make()函数来创建切片
var slice1 []type = make([]type , len)
slice1 := make([]type ,len)
slice2 := make([]T, len, capacity) //也可以指定容量,len 是数组的长度并且也是切片初始长度

2.初始化

//直接初始化切片,[] 表示是切片类型,{1,2,3} 初始化值依次是 1,2,3,其 cap=len=3
s :=[] int {1,2,3 } 
//使用数组初始化切片
s := arr[:] 
s := arr[startIndex:endIndex] 
s := arr[startIndex:] 
s := arr[:endIndex] 
//使用切片s 初始化切片 s1
s1 := s[startIndex:endIndex] 
//使用内置函数make() 初始化切片s
s :=make([]int,len,cap) 

len() 和 cap() 函数

func main() {
   var numbers = make([]int,3,5)

   printSlice(numbers)
}

func printSlice(x []int){
   fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}

//结果:len=3 cap=5 slice=[0 0 0]

空切片(nil)

func main() {
   var numbers []int

   printSlice(numbers)

   if(numbers == nil){
      fmt.Printf("切片是空的")
   }
}

func printSlice(x []int){
   fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}

//结果:
//len=0 cap=0 slice=[]
//切片是空的

切片的截取
可以通过设置下限及上限来设置截取切片 [lower-bound:upper-bound]

func main() {
   /* 创建切片 */
   numbers := []int{0,1,2,3,4,5,6,7,8}  
   printSlice(numbers)

   /* 打印原始切片 */
   fmt.Println("numbers ==", numbers)

   /* 打印子切片从索引1(包含) 到索引4(不包含)*/
   fmt.Println("numbers[1:4] ==", numbers[1:4])

   /* 默认下限为 0*/
   fmt.Println("numbers[:3] ==", numbers[:3])

   /* 默认上限为 len(s)*/
   fmt.Println("numbers[4:] ==", numbers[4:])

   numbers1 := make([]int,0,5)
   printSlice(numbers1)

   /* 打印子切片从索引  0(包含) 到索引 2(不包含) */
   number2 := numbers[:2]
   printSlice(number2)

   /* 打印子切片从索引 2(包含) 到索引 5(不包含) */
   number3 := numbers[2:5]
   printSlice(number3)

}

func printSlice(x []int){
   fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}

结果

len=9 cap=9 slice=[0 1 2 3 4 5 6 7 8]
numbers == [0 1 2 3 4 5 6 7 8]
numbers[1:4] == [1 2 3]
numbers[:3] == [0 1 2]
numbers[4:] == [4 5 6 7 8]
len=0 cap=5 slice=[]
len=2 cap=9 slice=[0 1]
//capacity 是7 ,是因为number3的ptr指向第三个元素,后面还剩 2,3,4,5,6,7,8, 所以 cap=7
len=3 cap=7 slice=[2 3 4]

分析
通过结果发现 容量capacity 与 下限 lower-bound有关
我们基于原数组或者切片创建一个新的切片后,那么新的切片的大小和容量是多少呢?
这里有个公式,对于底层数组容量是 k 的切片 slice[i:j] 来说:

长度: j-i
容量: k-i

可以看出切片,实际是获取的数组的某一个部分,len切片 <= cap切片 <= len数组
切片由三部分组成: 指向底层数组的指针、len、cap

//切片的内部结构
struct Slice
{   
    byte*    array;       // actual data
    uintgo    len;        // number of elements
    uintgo    cap;        // allocated number of elements
};

append() 和 copy() 函数
想增加切片容量,可以创建一个新的更大的切片并把原分片的内容都拷贝过来
也可以直接向切片追加新元素的append方法

func main() {
   var numbers []int
   printSlice(numbers)

   /* 允许追加空切片 */
   numbers = append(numbers, 0)
   printSlice(numbers)

   /* 向切片添加一个元素 */
   numbers = append(numbers, 1)
   printSlice(numbers)

   /* 同时添加多个元素 */
   numbers = append(numbers, 2,3,4)
   printSlice(numbers)

   /* 创建切片 numbers1 是之前切片的两倍容量*/
   numbers1 := make([]int, len(numbers), (cap(numbers))*2)

   /* 拷贝 numbers 的内容到 numbers1 */
   copy(numbers1,numbers)
   printSlice(numbers1)  
}

func printSlice(x []int){
   fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}

结果

len=0 cap=0 slice=[]
len=1 cap=1 slice=[0]
len=2 cap=2 slice=[0 1]
//cap为何变为了6,而不是5?^_^
len=5 cap=6 slice=[0 1 2 3 4]
len=5 cap=12 slice=[0 1 2 3 4]

分析
1.当append(list, [params]),先判断 list 的 cap 长度是否大于等于len(list) + len([params]),如果大于那么 cap 不变,否则cap = 2 * max{cap(list), cap[params]},所以当 append(numbers, 2, 3, 4) cap 从 2 变成 6。
2.通过 append() 函数向数组中添加元素,首先 cap 会以二倍的速度增长,如果发现增长 2 倍以上的容量可以满足扩容后的需求,那么 cap*2,否则就会看扩容后数组的 length 是多少 cap=length+1。
以上是两种分析,作为菜鸟不知道 具体是怎样的过程,留待后续查看底层源码再来分析。。。


范围 (Range)

range 用于for循环种迭代数组(array),切片(slice),通道(channel)或集合(map)

func main() {
    //这是我们使用range去求一个slice的和。使用数组跟这个很类似
    nums := []int{2, 3, 4}
    sum := 0
    for _, num := range nums {
        sum += num
    }
    fmt.Println("sum:", sum)
    //在数组上使用range将传入index和值两个变量。上面那个例子我们不需要使用该元素的序号,所以我们使用空白符"_"省略了。有时侯我们确实需要知道它的索引。
    for i, num := range nums {
        if num == 3 {
            fmt.Println("index:", i)
        }
    }
    //range也可以用在map的键值对上。
    kvs := map[string]string{"a": "apple", "b": "banana"}
    for k, v := range kvs {
        fmt.Printf("%s -> %s\n", k, v)
    }
    //range也可以用来枚举Unicode字符串。第一个参数是字符的索引,第二个是字符(Unicode的值)本身。
    for i, c := range "go" {
        fmt.Println(i, c)
    }
}

结果

sum: 9
index: 1
a -> apple
b -> banana
0 103
1 111

注意:如果不使用索引,要写_
如下写法其实遍历的是nums的索引

for num:=range nums {
	fmt.Printf("num:%d\n",num)
}

正确遍历写法是

for _, num := range nums {
    fmt.Printf("num:%d\n",num)
}
//或者
for num:=range nums {
	fmt.Printf("num:%d\n",nums[num])
}

Map集合

Map 是一种无序的键值对的集合,是通过 key 来快速检索数据,key 类似于索引,指向数据的值

示例

func main() {
	var countryCapitalMap map[string]string /*创建集合 */
	countryCapitalMap = make(map[string]string)

	/* map插入key - value对,各个国家对应的首都 */
	countryCapitalMap [ "France" ] = "巴黎"
	countryCapitalMap [ "Italy" ] = "罗马"
	countryCapitalMap [ "Japan" ] = "东京"
	countryCapitalMap [ "India " ] = "新德里"

	/*使用键输出地图值 */
	for country := range countryCapitalMap {
		fmt.Println(country, "首都是", countryCapitalMap [country])
	}

	/*查看元素在集合中是否存在 */
	capital, ok := countryCapitalMap [ "American" ] /*如果确定是真实的,则存在,否则不存在 */
	fmt.Println("capital:",capital)
	fmt.Println("ok:",ok)
	if (ok) {
		fmt.Println("American 的首都是", capital)
	} else {
		fmt.Println("American 的首都不存在")
	}

	delete(countryCapitalMap,"Japan")
	//打印删除后的map集合
	for country:= range countryCapitalMap{
		fmt.Println(country,"首都是", countryCapitalMap [country])
	}
}

结果

France 首都是 巴黎
Italy 首都是 罗马
Japan 首都是 东京
India  首都是 新德里
capital: 
ok: false
American 的首都不存在
Italy 首都是 罗马
India  首都是 新德里
France 首都是 巴黎

递归函数

递归,就是在运行的过程中调用自己。

实例:阶乘

func Factorial(n uint64)(result uint64) {
    if (n > 0) {
        result = n * Factorial(n-1)
        return result
    }
    return 1
}

func main() {  
    var i int = 15
    fmt.Printf("%d 的阶乘是 %d\n", i, Factorial(uint64(i)))
}

数据类型转换

go 不支持隐式转换类型
类型转换用于将一种数据类型的变量转换为另外一种类型的变量。

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

接口

接口作为一种数据类型,且属于引用类型。它把所有的具有共性的方法定义在一起,任何其他类型只要实现了这些方法就是实现了这个接口
和Java的写法完全不同

public interface interface_name{
	void method1();
	void method2();
}

public class class_name implment interface_name{
	@Override
	public void method1(){
		//方法体
	}
	....
}

go 实现方法的写法顺序简单归纳为:func (struct类型),方法名(),返回值类型
Java 实现方法的写法顺序为:public 返回值类型,方法名,(方法参数)
估摸着可以近似看成是 写法相反!!!

/* 定义接口 */
type interface_name interface {
   method_name1 [return_type]
   method_name2 [return_type]
   method_name3 [return_type]
   ...
   method_namen [return_type]
}

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

/* 实现接口方法 */
func (struct_name_variable struct_name) method_name1() [return_type] {
   /* 方法实现 */
}
...
func (struct_name_variable struct_name) method_namen() [return_type] {
   /* 方法实现*/
}

示例

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()

}

结果

I am Nokia, I can call you!
I am iPhone, I can call you!

实现方法的写法与正常方法的写法的区别

func (name string) imp() string{
	 print("这是实现方法的写法")
}

func sum(x int,y int) int{
	 print("这是正常写法")
}

将接口作为参数

type Phone interface {
    call() string 
}

type Android struct {
    brand string
}

type IPhone struct {
    version string
}

func (android Android) call() string {
    return "I am Android " + android.brand
}

func (iPhone IPhone) call() string {
    return "I am iPhone " + iPhone.version
}
//将接口作为参数
func printCall(p Phone) {
    fmt.Println(p.call() + ", I can call you!")
}

func main() {
    var vivo = Android{brand:"Vivo"}
    var hw = Android{"HuaWei"}

    i7 := IPhone{"7 Plus"}
    ix := IPhone{"X"}

    printCall(vivo)
    printCall(hw)
    printCall(i7)
    printCall(ix)
}

错误处理 error接口类型

type error interface {
    Error() string
}

使用示例

package main

import (
    "fmt"
)

// 自定义错误信息结构
type DIV_ERR struct {   
    etype int  // 错误类型   
    v1 int     // 记录下出错时的除数、被除数   
    v2 int
}
// 实现接口方法 error.Error()
func (div_err DIV_ERR) Error() string {   
    if 0==div_err.etype {      
        return "除零错误"   
    }else{   
        return "其他未知错误"  
    }
}
// 除法
func div(a int, b int) (int,*DIV_ERR) {  
    if b == 0 {     
        // 返回错误信息    
        return 0,&DIV_ERR{0,a,b}  
    } else {   
        // 返回正确的商  
        return a / b, nil   
    }
}
func main() { 
    // 正确调用  
    v,r :=div(100,2)  
    if nil!=r{   
        fmt.Println("(1)fail:",r)  
    }else{   
        fmt.Println("(1)succeed:",v) 
    }   
    // 错误调用
    v,r =div(100,0) 
    if nil!=r{   
        fmt.Println("(2)fail:",r)  
    }else{  
        fmt.Println("(2)succeed:",v) 
    }
}

panic 与 recover

panic 与 recover 是 Go 的两个内置函数,这两个内置函数用于处理 Go 运行时的错误,
panic 用于主动抛出错误,
recover 用来捕获 panic 抛出的错误
与Java中的throws new Exception 和 try…catch Exception 相似

异常处理机制留到后面分析


Go 并发

Go 允许使用 go 语句开启一个新的运行期线程, 即 goroutine,以一个不同的、新创建的 goroutine 来执行一个函数。 同一个程序中的所有 goroutine 共享同一个地址空间。
goroutine 是轻量级线程,goroutine 的调度是由 Golang 运行时进行管理的
语法格式:

go 函数名( 参数列表 )

代码示例

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")
}

结果:
输出结果没有固定先后顺序,因为是两个goroutine在执行

hello
world
world
hello
hello
world
world
hello
hello

通道(channel)

通道(channel)是用来传递数据的一个数据结构

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

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

通道的声明: ch := make(chan int)

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

代码示例: 通过两个 goroutine 来计算数字之和

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

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)
        x, y := <-c, <-c // 从通道 c 中接收

        fmt.Println(x, y, x+y)
}

//结果为: -5  17  12

通道缓冲区

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

ch := make(chan int, 100)

带缓冲区的通道允许发送端的数据发送和接收端的数据获取处于异步状态,就是说发送端发送的数据可以放在缓冲区里面,可以等待接收端去获取数据,而不是立刻需要接收端去获取数据。

不过由于缓冲区的大小是有限的,所以还是必须有接收端来接收数据的,否则缓冲区一满,数据发送端就无法再发送数据了。

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

有缓冲和无缓冲的区别
无缓冲是同步的,例如 make(chan int),就是一个送信人去你家门口送信,你不在家他不走,你一定要接下信,他才会走,无缓冲保证信能到你手上。

有缓冲是异步的,例如 make(chan int, 1),就是一个送信人去你家仍到你家的信箱,转身就走,除非你的信箱满了,他必须等信箱空下来,有缓冲的保证信能进你家的邮箱。

代码示例

func main() {
    // 这里我们定义了一个可以存储整数类型的带缓冲通道
        // 缓冲区大小为2
        ch := make(chan int, 2)

        // 因为 ch 是带缓冲的通道,我们可以同时发送两个数据
        // 而不用立刻需要去同步读取数据
        ch <- 1
        ch <- 2

        // 获取这两个数据
        fmt.Println(<-ch)
        fmt.Println(<-ch)
}

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

v, ok := <-ch

代码示例

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)
        }
}

补充:Channel 控制读写权限

go func(c chan int) { //读写均可的channel c } (a)
go func(c <- chan int) { //只读的Channel } (a)
go func(c chan <- int) {  //只写的Channel } (a)

后记

1.基础部分基本入门,连蒙带猜的大概能看懂部分代码了。时间余额已不足,后续要开始正式研究ckman项目了(工作所迫xiaoku),
带着目标学习的动力很足呀^_^
2.在研究项目的过程中准备对菜鸟教程中没有涉及的内容做特定研究,如 slice和map的源码及使用细节,go中面向对象的编程思想,异常处理接口error,go并发和channel的使用 等做特定研究

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值