记录学习Go的亿点问题

本文介绍了Go语言开发环境的配置,包括VSCode的设置和Go环境变量的检查。针对编译错误,如`go.mod`文件缺失,提出了执行`go mod init`的解决方案。此外,讨论了Go语言的变量声明、类型转换、接口、错误处理、并发编程和通道使用等核心概念,以及在编程过程中遇到的问题和解决办法。
摘要由CSDN通过智能技术生成

1.按照下文配置环境时ctrl+shift+p出不来、安装失败

vs code配置go开发环境 - 知乎

答:多试几次多多重启vscode,记得打开vscode的管理员模式、检查go的环境变量。

2.解决方案自go故障排查集锦 - chalon - 博客园  摘抄如下:

问题4:无法编译go程序,提示:

Build Error: go build -o e:\go\src\1go\1day\1_hello\__debug_bin.exe -gcflags all=-N -l .
go: go.mod file not found in current directory or any parent directory; see 'go help modules' (exit status 1)

原因分析:未发现go.mod;

解决方法:切换至项目目录下,执行go mod init命令,初始化项目即可产生go.mod;

最后看到这些还都蛮感人的

 

3.在程序输入时报错”noDebug mode: unable to process 'evaluate' request“

第一个方法是按这个做: 

noDebug mode: unable to process 'evaluate' request · Issue #2015 · golang/vscode-go · GitHub

第二个方法是放弃在这里

 

 进行输入,而是用终端

来运行程序输入输出。

 

4.语法上的特殊性质

具体见菜鸟教程Go 语言基础语法 | 菜鸟教程

 

 

go的百分号格式化 见文章GO语言百分号参数_XUAN528XUAN的博客-CSDN博客_go 百分号

 

 

 

error是接口

变量声明

格式为var 变量名 变量类型。如var num float32 = 1.5 

也可以不指定类型,这时候go会帮你判断是什么类型。var 变量名 = value

也可以使用:= : 变量名 := value

 

对于值类型,可以通过&i来获取变量i的内存地址。

对于引用类型:

局部变量不允许声明了然后不用,但是全局变量允许

go的并行赋值

函数有多个返回值时可以并行赋值 

package main

import "fmt"

func main() {
	_, a := count(10, 3)
	fmt.Println(a)
}

func count(a, b int) (int, int) {
	return a + b, a - b
}

go的常量

定义格式 const 常量名 [数据类型] = value

当然可以省略[数据类型]

常量定义也存在并行赋值的方法

/*输出 1 1 1 1*/
package main

import "fmt"

const (
	a = 1
	b
	c
	d
)

func main() {
	fmt.Println(a, b, c, d)
}

go还有一种特殊常量,iota,可以认为是一个可以被编译器修改的常量,可以理解为const语句块内的行索引

/*输出为 0 1 2 0 1*/
package main

import "fmt"

const (
	UNKNOWN = iota
	FEMALE
	MALE
)

const (
	HUMAN = iota
	HAHA
)

func main() {
	fmt.Println(UNKNOWN, FEMALE, MALE, HUMAN, HAHA)
}

这点很神奇,看来名字不能乱取

 这个也是,看来行不能乱换。

运算符

条件控制 

 

 if-else

package main

import "fmt"

var i int = 0

func main() {
	if i < 0 {//如果写成带括号形式会自动帮你去掉
		fmt.Println(i)
	} else {//这里不能换行
		fmt.Print("big")
	}
}

 switch-case

package main

import "fmt"

var i int = 1

func main() {
	switch i {
	case 0:
		fallthrough
	case 1:
		fmt.Println("对了")
	}
}

 

select语句

这个还看不大懂,之后再说吧

循环语句

格式 for init; condition; post { } ——相当于c的for循环

        for condition { }                 ——相当于c的while循环

        for { }                                ——相当于c的死循环

func main() {
	for i := 0; i < 10; i++ {
		fmt.Println(i)
	}
	sum := 1
	for sum <= 10 {
		sum += sum
	}
	fmt.Println(sum)
}
func main() {
	arr := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}
	for i := range arr {
		fmt.Println(arr[i])
	}
	fmt.Println("------")
	for i, ele := range arr {
		fmt.Println(i, ele)
	}
}
/*
1
2
3
4
5
6
7
8
9
10
11
12
------
0 1
1 2
2 3
3 4
4 5
5 6
6 7
7 8
8 9
9 10
10 11
11 12
*/

go的break新增了一个标记,这点不错,可以用来退出多重循环

 continue也有标记

 会从continue语句那里跳到标签层的下一轮循环继续。相当于在continue那句变成break,然后最外层循环内的内层循环下面啥都没有

函数

定义格式:

func 函数名 (参数列表) (返回类型列表【可以返回多个值】){   } 

 跟JavaScript一样灵活,go也可以用一个变量储存函数的地址

package main

import "fmt"

func main() {
	myFunction := func(a string) {
		fmt.Printf("Please call me %s", a)
	}
	myFunction("大佬")
}

 

 函数闭包

 create返回的函数变量用到了函数体的外部变量c,但在create执行结束后,仍能得到这个c的值,这就满足函数闭包,并称变量c为捕获变量

【Golang】是闭包啊..._哔哩哔哩_bilibili

这个讲得很清楚,通常称闭包函数为有状态的函数存了捕获列表和函数地址

 每次都打印2

方法

 

/*这是一般的函数*/
/* 定义结构体 */
type Circle struct {
	radius float64
}

func main() {
	var c1 Circle
	c1.radius = 10.00
	fmt.Println("圆的面积 = ", getArea(c1))
}

//该 method 属于 Circle 类型对象中的方法
func getArea(c Circle) float64 {
	//c.radius 即为 Circle 类型对象中的属性
	return 3.14 * c.radius * c.radius
}
/* 这是方法 */
type Circle struct {
	radius float64
}

func main() {
	var c1 Circle
	c1.radius = 10.00
	fmt.Println("圆的面积 = ", c1.getArea())
}

//该 method 属于 Circle 类型对象中的方法
func (c Circle) getArea() float64 {
	//c.radius 即为 Circle 类型对象中的属性
	return 3.14 * c.radius * c.radius
}

 如果想要改变结构体中radius的值,需要传指针

 

package main

import (
	"fmt"
)

/* 定义结构体 */
type Circle struct {
	radius float64
}

func main() {
	var c Circle
	fmt.Println(c.radius)
	c.radius = 10.00
	fmt.Println(c.getArea())
	c.changeRadius(20)
	fmt.Println(c.radius)
	change(&c, 30)
	fmt.Println(c.radius)
}
func (c Circle) getArea() float64 {
	return c.radius * c.radius
}

// 注意如果想要更改成功c的值,这里需要传指针
func (c *Circle) changeRadius(radius float64) {
	c.radius = radius
}

// 以下操作将不生效
//func (c Circle) changeRadius(radius float64)  {
//   c.radius = radius
//}
// 引用类型要想改变值需要传指针
func change(c *Circle, radius float64) {
	c.radius = radius
}

变量的作用域

 

 

数组

定义格式         var variable_name [SIZE] variable_type  eg. var arr [10]int

初始化方法

                 var arr = [ ]type { }    eg.var arr = [9]int {1,2,3,4,5,6,7,8,9}

                arr := []type{}             eg.arr := [...]int{1,2,3,4,5,6,7,8,9}

                

func main() {
	arr := [][]int{}
	row1 := []int{1, 2, 3, 4}
	row2 := []int{5, 6, 7, 8}
    //使用 append() 函数向空的二维数组添加两行一维数组
	arr = append(arr, row1)
	arr = append(arr, row2)
    for i := 0; i < len(arr); i++ {
		for j := 0; j < len(arr[i]); j++ {
			fmt.Print(arr[i][j])
		}
	}
	/*
	打印数组也可以这样
	fmt.Println(arr[0], arr[1])
	*/
	
}

 

 

向函数传递数组

方法一,形参设定数组大小

               eg.func myFunction(param [10]int){  }

方法二,形参未设定数组大小

               eg.func myFunction(param []int){  }

如下段代码给出求和函数。

func main() {
	arr := []int{1, 2, 3, 4, 5, 6}
	fmt.Println(getSum(arr))
}

func getSum(arr []int) int {
	sum := 0
	for i := 0; i < len(arr); i++ {
		sum += arr[i]
	}
	return sum
}

指针

指针的定义格式 var 变量 *type,或者比如说变量 := &a

Go可以定义指针数组,即数组元素都是指针。

func main() {
	num := []int{1, 2, 3}
	var arr [3]*int
	for i := range num {
		arr[i] = &num[i]
	}
	for i := range arr {
		fmt.Println(arr[i], *arr[i])
	}
}

  

指针作为函数参数

package main

import (
	"fmt"
)

func main() {
	a := 10
	b := 20
	fmt.Printf("Swap之前 %d %d\n", a, b)
	swap(&a, &b)
	fmt.Printf("Swap之后 %d %d\n", a, b)
	a, b = swap2(a, b)
	fmt.Printf("Swap2之后 %d %d\n", a, b)
}

//C语言传统swap写法
func swap(a, b *int) {
	var temp = *a
	*a = *b
	*b = temp
}

//Go
func swap2(a, b int) (int, int) {
	return b, a
}

结构体

定义结构

type struct_variable_type struct {
   member definition
   member definition
   ...
   member definition
}

type Book struct {

    name string

    id int16

    author string

    subject string

}

结构体的定义方式

type Book struct {
	name    string
	id      int
	author  string
	subject string
}

func main() {
	//两种定义
	a := Book{"Hello", 123455, "me", "English"}
	var b = Book{"Hello", 123455, "me", "English"}
	fmt.Println(a == b)
}
//输出:True

还可以像其他oop语言一样用new 

作为函数参数

func printBook(b Book) {
	fmt.Print(b.name, b.author, b.subject, b.id)
}

这点跟C语言不大一样

注意,

 从上可注意到if里边也是可以定义变量。

切片Slice

切片Slice与数组Array的区别:

 定义格式

 var slice []int

切片不需要说明长度,但也可以用make来定义切片的初始长度

切片的初始化

也可以通过另一个数组初始化切片

还可以通过切片初始化切片。注意这个可以用来截取切片,左闭右开

最后可以通过内置函数初始化切片。其中capacity是可选参数

 

 

package main

import "fmt"

func main() {
	a := make([]int, 0, 10)
	for i := 0; i < 19; i++ {
		a = append(a, i)
	}
	for i := 0; i < len(a); i++ {
		fmt.Println(a[i])
	}
	fmt.Println(len(a), cap(a))

}

/*
输出:
0123456789101112131415161718
19 20
*/

Range

package main

import "fmt"

func main() {
	var arr1 = []int{1, 2, 3}
	sum := 0
	for _, i := range arr1 {
		sum += i
	}
	fmt.Println(sum)
}

 此处可以用格式化输出printf,就可以输出字符了

Go的集合Map

Map 是一种无序的键值对的集合,key-value。

定义格式

1.声明变量,默认为nil var myMap[key的类型]value的类型

2.使用make函数           myMap := make(map[key的类型]value的类型)

注意不能往nil的map里面塞东西。所以你就算用了第一个定义格式,也得再make。

package main

import "fmt"

func main() {
	// var myMap map[string]int
	// myMap = make(map[string]int)
	myMap := make(map[string]int)
	myMap["张三"] = 33
	myMap["李四"] = 99
	myMap["王五"] = 88
	myMap["老刘"] = 90
	for key, value := range myMap {
		fmt.Println(key, value)
	}
}

对一个map取key值,如myMap["张三"],其实得到的返回值不止一个,第一个返回值就是key存在时对应的value,第二个返回值则是bool类型,key存在返回true,不存在返回false

类型转换

格式:

不支持隐形类型转换

Go的接口

package main

import "fmt"

func main() {
	casio := new(Casio)
	fmt.Println(casio.add(1, 2))
	fmt.Println(casio.sub(1, 2))
}

type Calculator interface {
	add(int, int) int
	sub(int, int) int
	call()
}

type Casio struct {
	name string
}

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

func (c Casio) sub(a, b int) int {
	return a - b
}

 可以发现一个结构体可以选择只实现接口的部分类而非全部类

如果想要在实现方法里修改结构体的属性,只需在方法那边传入结构体指针即可。

同样的也有多态 

Go的类型转换

Go没有自动类型转换,只能强制类型转换。

注意类型转换括号括在变量 

Go的错误处理

有时候错误需要反应的东西不只限于一个string,我们可以用一个对象来表示这个错误类型,即在go中的具体实现是一个表示错误的结构体。 具体的错误类的实现,错误信息方法的实现,错误在函数的用法,以及有错误的函数被调用时的用法,详见下列代码,模拟除0错误。

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

此处有错误处理技巧,参见视频Go语言技巧 - 2.【错误处理】谈谈Go Error的前世今生_哔哩哔哩_bilibili 

其中第一点违背了面向对象的继承法则。

 

正如java中可以把错误throw掉,以及try-catch机制,Go中也有相应的机制,即panic 与 recover,一个用于主动抛出错误,一个用于捕获panic抛出的错误。

在说上面那两个是啥之前,我们可以先了解一下啥是函数中的的defer。

【尚硅谷】Golang入门到实战教程丨一套精通GO语言_哔哩哔哩_bilibili

package main

import "fmt"

func main() {
	fmt.Println(sum(10, 20))
}

func sum(n1, n2 int) int {
	defer fmt.Println("ok1,n1 = ", n1)
	defer fmt.Println("ok2,n2 = ", n2)
	res := n1 + n2
	fmt.Println("ok3,res = ", res)
	return res
}
/*
输出结果:
ok3,res =  30
ok2,n2 =  20
ok1,n1 =  10
30
*/

注意:

func main() {
	fmt.Println(sum(10, 20))
}

func sum(n1, n2 int) int {
	res := n1 + n2
	return 0
	fmt.Println("ok3,res = ", res)
	defer fmt.Println("ok1,n1 = ", n1)
	defer fmt.Println("ok2,n2 = ", n2)
	return res
}
/*
输出结果:
0
*/

因此我们的一般用法是

刚打开资源就加一句defer(如果实际不考虑释放顺序的话)

 此处用的是关闭句柄的功能。

 

 此时要记得defer后面是一个可执行语句,所以不能只定义一个函数,还得去执行。而执行匿名函数很简单,只需在定义的{}后面加个()就可以调用执行了

defer处理完异常后,main下面的代码就可以继续执行了。否则会panic,中断程序

这里也可以这么写。 

自定义错误的话就是这个写法。

有效捕获:

func main() {
	test()
	fmt.Println("hello")
}

func except() {
	recover()
}
func test() {
	defer except()
	panic("runtime error")
}
func test2() {
	defer func() { recover() }()
}

 无效:

输出three 

Go的并发

go关键字

 语法格式:go 函数名(参数列表) 

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

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

go的Channel

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

通道可用于两个 goroutine 之间通过传递一个指定类型的值来同步运行和通讯

操作符 <- 用于指定通道的方向,发送或接收。如果未指定方向,则为双向通道

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

 

package main

import "fmt"

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)
}
/*
    result:-5 17 12
*/

可以看出channel是先进后出的栈结构

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

记录试出来的死锁情况

package main

import "fmt"

func test(ch chan int) {
	for i := 0; i < 8; i++ {
		fmt.Println(<-ch)
	}
	fmt.Println("Finished")
}

func main() {
	ch := make(chan int, 10)
	//缓冲区已满,在等待接收区,死锁了
	for i := 0; i < 13; i++ {
		ch <- i
	}
	go test(ch)
}

 

Go可以使用range关键字遍历得到的channel,类似于数组与切片。

如果通道接收不到数据后

ok就变为false。 

可以用close()函数来关闭通道 

package main

import "fmt"

func test(ch chan int) {
	for i := 0; i < 10; i++ {
		ch <- i
	}
	fmt.Println("Finished")
	close(ch)
}

func main() {
	ch := make(chan int, 10)
	go test(ch)
	for i := range ch {
		fmt.Println(i)
	}
}

 如果删去此处的close(),会发生死锁

        // range 函数遍历从通道接收到的数据,因为在发送完 10 个
        // 数据之后就关闭了通道,所以这里我们 range 函数在接收到 10 个数据
        // 之后就结束了。如果上面的 c 通道不关闭,那么 range 函数就不
        // 会结束,而会一直等待接收第 11 个数据,发生阻塞,死锁。

我们单独写一个 say2 函数来跑 goroutine,并且 Sleep 时间设置长一点,150 毫秒,看看会发生什么:

package main
import (
    "fmt"
    "time"
)
func say(s string) {
    for i := 0; i < 5; i++ {
        time.Sleep(100 * time.Millisecond)
        fmt.Println(s, (i+1)*100)
    }
}
func say2(s string) {
    for i := 0; i < 5; i++ {
        time.Sleep(150 * time.Millisecond)
        fmt.Println(s, (i+1)*150)
    }
}
func main() {
    go say2("world")
    say("hello")
}

输出结果:

hello 100
world 150
hello 200
hello 300
world 300
hello 400
world 450
hello 500

[Done] exited with code=0 in 2.066 seconds

问题来了,say2 只执行了 3 次,而不是设想的 5 次,为什么呢?

原来,在 goroutine 还没来得及跑完 5 次的时候,主函数已经退出了

我们要想办法阻止主函数的结束,要等待 goroutine 执行完成之后,再退出主函数。那么我们就可以用个channel,让主函数在那边等接数据

package main

import (
    "fmt"
    "time"
)

func say(s string) {
    for i := 0; i < 5; i++ {
        time.Sleep(100 * time.Millisecond)
        fmt.Println(s, (i+1)*100)
    }
}
func say2(s string, ch chan int) {
    for i := 0; i < 5; i++ {
        time.Sleep(150 * time.Millisecond)
        fmt.Println(s, (i+1)*150)
    }
    ch <- 0
    close(ch)
}

func main() {
    ch := make(chan int)
    go say2("world", ch)
    say("hello")
    fmt.Println(<-ch)
}

我们引入一个信道,默认的,信道的存消息和取消息都是阻塞的,在 goroutine 中执行完成后给信道一个值 0,则主函数会一直等待信道中的值,一旦信道有值,主函数才会结束。

package main

import (
	"fmt"
)

func test(c chan int) {
	for i := 0; i < 100; i++ {
		c <- i
	}
	fmt.Println("Finished test")
}

func main() {
	c := make(chan int, 100)
	go test(c)
	for true {
		val := <-c
		if val > 5 {
			close(c)
			break
		}
		fmt.Println(val)
	}
	fmt.Println("------------------")
	for val := range c {
		fmt.Println(val)
	}
}

channel关闭后不能访问往里面塞东西,但是数值仍存着,还是可以全部拿出来。但是close只能close一遍

package main

import (
	"fmt"
	"time"
)

var flag bool = false

func test(c chan int) {
	for i := 0; i < 300; i++ {
		if flag {
			break
		}
		c <- i
	}
	fmt.Println("Finished test")
}

func main() {
	c := make(chan int, 100)
	go test(c)
	i := 0
	for true {
		i++
		val := <-c
		if val == 5 {
			close(c)
			flag = true
			break
		}
		if i >= 450 {
			break
		}
		fmt.Println(val)
		time.Sleep(1000)
	}
	fmt.Println("------------------")
	for val := range c {
		fmt.Println(val)
	}
}

 输出结果:

0

1

2

3

4

------------------

6

7

......

30

Finished test

31

32

......

105

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值