Golang基础详细总结
文章目录
前言
Golang语法上几乎没有增加什么显眼的语法特性,变化主要围绕降低心智负担、细节完善、工程化、Go编译提速、性能GC优化、多平台的移植、标准化、强化社区约定等。
3到5年内,Go主攻方向仍然是服务端开发,偏向于云计算方向
会出现更多移动开发,在iOS和Android的应用会越来越多
网络应用的第三方库会越来越丰富
Go语言会在桌面端编程占据重要地位
为了避免之后开发项目的时候走很多不必要的坑,所以花了长达三天的时间来仔细学习了和总结Golang基础,这篇文章结合了菜鸟教程,李文周博客以及topgoer网站等学习资料;学习Golang之前自己本身有java和C的底子,还是比较容易的,下面开始总结!
一、Go开发环境
这里推荐Intellij公司旗下的产品Goland,一款功能强大的编辑器,学习java用idea,学习C/C++用Clion,全智能,不废话!
这里就涉及到Golang的安装,配置环境变量,下载编辑器等一些准备工作了,这里建议看哔哩哔哩直接搞,或者向学长直接要,因为是速成,所以咱就别那么费事了。
二、Go基础语法
1. 基础语法
代码如下(示例):
package main//package main表示一个可独立执行的程序,每个 Go 应用程序都包含一个名为 main 的包。
import "fmt"//引入fmt包功能为实现IO
/* 运行主函数 */
func main() {
fmt.Println("Hello, World!")
}
运行结果:Hello, World!
Golang语句是可以不用分号“ ; ”结尾的哦,使得代码整洁,方便
注释:和java一样的用法:/ 单行注释 / 和 /* 多行注释 */
标识符规则:一个标识符实际上就是一个或是多个字母(A ~ Z和a ~ z)数字(0~9)、下划线_组成的序列,但是第一个字符必须是字母或下划线而不能是数字。
注意:Go里面左边的花括号不能单独占一行
2. 数据类型
- 布尔型(true,false)
- 数字类型(int8,int16,int32,int64,uint8,unit16,uint32,uint64,float32,float64,complex64,complex128)
备注:
1.后面的数字是二进制位数的长度,比如int8的取值范围就是 -27 ~ 27 - 1(正负符号占了一位,所以数字大小长度为7位,学过二进制就知道啦)
2.uint是无符号类型的整型数字,所以数字大小长度为8位(比如uint8:没有负数,正数范围是差不多int8的2倍,为0 ~ 28 - 1)
3.float32为32位浮点型数,float为64位浮点型数
4.complx为复数类型,后面的数字也表示位数。
5.byte类似为uint8,rune类似于int32,uint(32位或者是64位),int(与uint一样大小),uintptr(用于存放一个指针) - 字符串类型(string)
- 派生类型
- 指针类型(Pointer)
- 数组类型
- 结构化类型(struct)
- Channel类型
- 函数类型
- 切片类型
- 接口类型( interface)
- Map类型
3. 常量和变量
附:常量和变量声明的方式差不多,变量是用var
,常量是用const
,声明多个全局变量的时候也用const,不用像变量一样加s(vars
),变量的值可以被多次修改,常量的值不能被修改。
/*
声明一个变量需要有以下三个元素
var 变量名 数据类型
用 := 来声明一个变量的话只能在函数体内,不能被用来声明全局变量
*/
package main
import "fmt"
//声明单个全局变量
var all int = 1
//声明多个全局变量
vars(
all_var1 int
all-var2 string = "Kangkang"
)
//为了方便举例子,下面统一用一个变量名称!
func main() {
var a string = "Kangkang"//第一种声明方式
a := "Kangkang" //第二种声明方式(去除var,加冒号)
var a = "Kangkang" //第三种声明方式(后面不加数据类型,自动判断)
//先声明,在赋值
var a int
a = 10
//声明其他类型的变量(当然也可以用 := 来取代var,但是必须要赋值)
var a *int //指针类型
var a []int //数组类型
var a map[string] int //map类型
var a chan int //通道类型
var a func(string) int //方法
var a error // error 是接口
var b, c int = 1, 2 //连续声明多个变量,依次给b,c赋值
b, c := 2,"string" //用 := 连续赋值
fmt.Println(b, c) //输出的时候不用 + 号,用逗号分隔
/* 强制转换
举例:将int类型转化为int64
注意:转化的时候要注意两个数字的具体大小,如果大的数据类型转成了小的数据类型,
但是小的数据类型接受不了这么大的范围,就会造成输出负数等情况
*/
var a int8 = 1 //声明int8类型
b := int64(a) //转化为了int64类型
}
注意:
如果指定变量类型,没有初始化赋值,则变量默认为零值。
数值类型(包括complex64/128)为 0
布尔类型为 false
字符串为 “”(空字符串)
以下几种类型为 nil:
var a *int
var a []int
var a map[string] int
var a chan int
var a func(string) int
var a error
iota
在定义多个变量的时候,第一个 iota 等于 0,每当 iota 在新的一行被使用时,它的值都会自动加 1;类似每一行的变量的索引值
package main
import "fmt"
func main() {
const (
a = iota //0
b //1
c //2
d = "ha" //独立值,iota += 1
e //"ha" iota += 1
f = 100 //iota +=1
g //100 iota +=1
h = iota //7,恢复计数
)
const (
i=1<<iota //iota = 0
j=3<<iota //iota = 1
k //iota = 2 默认:k = 3<<2
l //iota = 3 默认:l = 3<<3
)
fmt.Println(a,b,c,d,e,f,g,h)
fmt.Println(i,j,k,l)
}
//结果为:
//0 1 2 ha ha 100 100 7 8
//1,6,12,24
注意:在Golang中,如果声明了一个变量,但是没有用到,这个时候会报错,导包也是一样,如果导入包,但是不用也会报错,强制要求代码不要冗余!当然啦,变量不能重复声明,只能重复赋值。
4. 运算符
- 算术运算符(+,-,*,/,%,++,- -)
- 关系运算符(==,!=,>,<,>=,<=)
- 逻辑运算符(&&,||,!)
- 位运算符 (&,|,^,<<,>>)
- 赋值运算符(=,+=,-=,/=,*=,%=,<<=,>>=,&=,^=,|=)
- 其他运算符(&【获取变量地址】,*【得到地址里的值】)
优先级 | 运算符 |
---|---|
5 | * / % << >> && ^ |
4 | + - |
3 | == != < <= > >= |
2 | && |
1 |
5. 条件语句
1. if, else if else
package main
import "fmt"
func main() {
/* 定义局部变量 */
var a int = 1
var b int = 2
/* 判断条件 嵌套if ,else if, else*/
if a == 1 {
if b == 2 {
fmt.Printf("b = 2\n" );
} else if b < 2 {
fmt.Printf("b < 2\n" );
} else {
fmt.Printf("b > 2\n" );
}
} else {
fmt.Printf("a > 1\n" );
}
}
2. switch case
switch 默认情况下 case 最后自带 break 语句,匹配成功后就不会执行其他 case,如果我们需要执行后面的 case,可以使用 fallthrough 。
package main
import "fmt"
func main() {
/* 定义局部变量 */
var grade string = "B"
var marks int = 90
//第一种写法(switch之后有变量)
switch marks {
case 90: grade = "A"
case 80: grade = "B"
case 50,60,70 : grade = "C"
default: grade = "D"
}
//第二种写法(switch之后没有变量)
switch {
case false:
fmt.Println("1、case 条件语句为 false")
fallthrough
case true:
fmt.Println("2、case 条件语句为 true")
fallthrough
case false:
fmt.Println("3、case 条件语句为 false")
fallthrough
case true:
fmt.Println("4、case 条件语句为 true")
case false:
fmt.Println("5、case 条件语句为 false")
fallthrough
default:
fmt.Println("6、默认 case")
}
/* 结果如下
2、case 条件语句为 true
3、case 条件语句为 false
4、case 条件语句为 true
从以上代码输出的结果可以看出:switch 从第一个判断表达式为 true 的 case 开始执行,
如果 case 带有 fallthrough,程序会继续执行下一条 case,
且它不会去判断下一个 case 的表达式是否为 true。
*/
//switch 语句还可以被用于 type-switch 来判断某个 interface 变量中实际存储的变量类型
var x interface{}
switch i := x.(type) {
case nil:
fmt.Printf(" x 的类型 :%T",i)
case int:
fmt.Printf("x 是 int 型")
case float64:
fmt.Printf("x 是 float64 型")
case func(int) float64:
fmt.Printf("x 是 func(int) 型")
case bool, string:
fmt.Printf("x 是 bool 或 string 型" )
default:
fmt.Printf("未知型")
}
}
3. select
select 是 Go 中的一个控制结构,类似于用于通信的 switch 语句。每个 case 必须是一个通信操作,要么是发送要么是接收。
package main
import "fmt"
func main() {
var c1, c2, c3 chan int //定义三个通道类型的整型变量(通道类型后面会说到)
var i1, i2 int
select {
case i1 = <-c1:
fmt.Println("i1 = <-c1")
case c2 <- i2:
fmt.Println("c2 <- i2")
case i3, ok := (<-c3): // 也可以这样写: i3, ok := <-c3
if ok {
fmt.Println("ok")
} else {
fmt.Println("not ok")
}
default:
fmt.Println("no communication")
}
//结果:no communication
}
select 随机执行一个可运行的 case。如果没有 case 可运行,它将阻塞,直到有 case 可运行。一个默认的子句应该总是可运行的。
6. 循环语句
Go语言里面只有for循环!while也被for取代,请看下面的案例,给出了几种写法:
也可以通过break,continue,goto语句来中断或者跳转。
package main
import "fmt"
func main() {
//这是无限循环
for {
fmt.Printf("这是无限循环。\n");
//break 加上break,最近的一层循环就中断了
}
//求0~10加起来的和
sum := 0
for i := 0; i <= 10; i++ {
sum += i
}
fmt.Println(sum)
//10个1相加, 这样像 java 和 C 里的 While 语句形式
for sum <= 10{
sum += 1
}
fmt.Println(sum)
//For-each range 循环,可以对字符串、数组、切片等进行迭代输出元素
//第一个接收的变量 i 是索引值(index), 第二个接收的变量s是 数组真正的值(value)
//当然啦,这两个变量的名称有我们决定,如果不使用则可以用下划线 “ _ ” 代替,节省内存空间
strings := []string{"google", "runoob"}
for i, s := range strings {
fmt.Println(i, s)
}
numbers := [6]int{1, 2, 3, 5}
for i,_:= range numbers {
fmt.Printf("第 %d 位 x 的值 = %d\n", 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)
}
}
三、Go的小进阶
1. 函数
函数的一般形式如下:
func 函数名(参数)(返回值){
函数体
}
函数也可以没有参数,也可以没有返回值
重点:参数可以为任意类型
其中:
- 函数名:由字母、数字、下划线组成。但函数名的第一个字母不能是数字。在同一个包内,函数名也称不能重名(包的概念详见后文)。
- 参数:参数由参数变量和参数变量的类型组成,多个参数之间使用 “ , ” 分隔。
- 返回值:返回值由返回值变量和其变量类型组成,也可以只写返回值的类型,多个返回值必须用()包裹,并用 “ , ” 分隔。
- 函数体:实现指定功能的代码块。
package main
import "fmt"
func main() {
/* 定义局部变量 */
var a int = 100
var b int = 200
/* 调用函数并返回两数之和*/
ans := add(a, b)
f2(f3,"string类型的参数")
//调用含有可变参数的函数
ret1 := Sum()
ret2 := Sum(10)
ret3 := Sum(10, 20)
ret4 := Sum(10, 20, 30)
fmt.Println(ret1, ret2, ret3, ret4) //结果:0 10 30 60
}
/* 函数返回两个数的最大值 */
func max(num1, num2 int) int {
/* 定义局部变量 */
var result int = num1 + num2
return result
}
//案例:部分其他样式的函数
func f() {} //无参无返回值
//这里使用了多个参数,并且可以返回多个参数,也可以返回一个函数
func f1(s string,n int) (string int){return s, n}
//这个时候f2函数内有一个函数类型的参数,咱在上面调用一下f3函数,将f3作为参数传到f2里
//注意:传入的实参要与形参的格式保持一样: 参数个数和类型一样,返回值一样
func f2(name func()int,s string) int{
return 1
}
func f3() int{
return 1
}
/*
可变参数
可变参数是指函数的参数数量不固定。Go语言中的可变参数通过在参数名后加...来标识。
注意:一个函数内,只能传入一个可变参数,并且只能作为最后一个参数。
本质上,函数的可变参数是通过切片来实现的。
请看下面的案例:并且会在主函数main中多次传不同的参数进行调用
*/
func Sum(s string , x ...int) int {
fmt.Println(x) //x是一个切片(切片的概念后面会讲到,现在理解成模拟数组就行了)
sum := 0
for _, v := range x {
sum = sum + v
}
return sum
}
我们可以使用type关键字来自定义函数类型,具体格式如下:
type calculation func(int, int) int,参数和返回值的数量和类型可以自己控制
calculation 是一种函数类型,这种函数接收两个int类型的参数并且返回一个int类型的返回值。
简单来说,凡是满足这个条件的函数都是calculation 类型的函数
用法如下:可以用来快速定义函数型参数,也可以直接定义变量
package main
import "fmt"
type calculation func(int, int) int //给一个定义好的函数一个别名叫calculation
type long int64 //修改数据类型的名称,但还是可以用int64
func main() {
var c calculation //定义calculation类型的变量用来接受函数型参数
var as int
as, c = addSub(add,sub) //接收addSub函数的两个参数
fmt.Println(as,c(1,2)) //打印输出函数的两个参数,因为第二个是函数,所以需要传入实参
}
//定义一个两个参数都是函数型,返回值一个为int类型,一个为函数类型的add_sub函数
func add_sub(f1 calculation,f2 calculation) (i int,f3 calculation) {
a := f1(1,2)
b := f2(3,4)
i = a+b
return i, f2
}
func add(x, y int) int {
return x + y
}
func sub(x, y int) int {
return x - y
}
结果为:2 -1
插入一个知识点:类型定义和类型别名的区别:
类型别名与类型定义表面上看只有一个等号的差异,我们通过下面的这段代码来理解它们之间的区别。
//类型定义
type NewInt int
//类型别名
type MyInt = int
func main() {
var a NewInt
var b MyInt
fmt.Printf("type of a:%T\n", a) //type of a:main.NewInt
fmt.Printf("type of b:%T\n", b) //type of b:int
}
结果显示:
a的类型是main.NewInt,表示main包下定义的NewInt类型。
b的类型是int。MyInt类型只会在代码中存在,编译完成时并不会有MyInt类型。
匿名函数的使用,代码如下:
package main
import "fmt"
func main() {
// 将匿名函数保存到变量
add := func(x, y int) {
fmt.Println(x + y)
}
add(10, 20) // 通过变量调用匿名函数
//自执行函数:匿名函数定义完加()直接执行
func(x, y int) {
fmt.Println(x + y)
}(10, 20)
}
闭包:
闭包指的是一个函数和与其相关的引用环境组合而成的实体。简单来说,闭包=函数+引用环境。
package main
import "fmt"
func main() {
f := adder(5) //将adder返回的函数赋值给f
fmt.Println(f(10)) //15
fmt.Println(f(20)) //35
fmt.Println(f(30)) //65
f1 := adder(3) //再次赋值给一个新变量
fmt.Println(f1(10)) //13
fmt.Println(f1(20)) //33
}
func adder(x int) func(int) int {
return func(y int) int {
x += y
return x
}
}
第二个闭包案例,代码如下:
package main
import "fmt"
func main() {
f1, f2 := calc(10)
fmt.Println(f1(1), f2(2)) //11 9
fmt.Println(f1(3), f2(4)) //12 8
fmt.Println(f1(5), f2(6)) //13 7
}
func calc(base int) (func(int) int, func(int) int) {
add := func(i int) int {
base += i
return base
}
sub := func(i int) int {
base -= i
return base
}
return add, sub
}
在Go语言的函数中return语句在底层并不是原子操作,它分为给返回值赋值和RET指令两步。而defer语句执行的时机就在返回值赋值操作后,RET(return)指令执行前。
因为情况比较多,这里推荐学习网站 --> defer的详细使用
视频网站:–> defer的视频讲解
2. 数组
数组是同一种数据类型元素的集合。 在Go语言中,数组从声明时就确定,使用时可以修改数组成员,但是数组大小不可变化,下标从0开始。
数组定义如下:
var 数组变量名 [元素数量]数据类型
package main
import "fmt"
func main() {
var testArray [3]int
var numArray = [...]int{1, 2}
var cityArray = [...]string{"北京", "上海", "深圳"}
fmt.Println(testArray) //[0 0 0]
fmt.Println(numArray) //[1 2]
fmt.Printf("type of numArray:%T\n", numArray) //type of numArray:[2]int
fmt.Println(cityArray) //[北京 上海 深圳]
fmt.Printf("type of cityArray:%T\n", cityArray) //type of cityArray:[3]string
//指定下标来定义下标所在的值,
a := [...]int{1: 1, 3: 5}
fmt.Println(a) // [0 1 0 5]
//二维数组(里面每一个元素都是一个一维数组),多维数组以此类推
more := [3][2]string{
{"北京", "上海"},
{"广州", "深圳"},
{"成都", "重庆"},
}
fmt.Println(a) //[[北京 上海] [广州 深圳] [成都 重庆]]
fmt.Println(a[2][1]) //支持索引取值:重庆
//数组的遍历
//一维数组的遍历
// 方法1:for循环遍历
for i := 0; i < len(a); i++ {
fmt.Println(a[i])
}
// 方法2:for range遍历
for index, value := range a {
fmt.Println(index, value)
}
//二维数组的遍历
for i := 0; i < len(a); i++ {
for j := 0; j < len(a[i]); j++ {
fmt.Println(a[i][j])
}
}
for _, v1 := range more {
for _, v2 := range v1 {
fmt.Printf("%s\t", v2)
}
fmt.Println()
}
}
注意: 多维数组只有第一层可以使用…来让编译器推导数组长度。例如:
支持的写法
a := […][2]string{ {“北京”, “上海”}, {“广州”, “深圳”} }
3. 指针
- 区别于C/C++中的指针,Go语言中的指针不能进行偏移和运算,是安全指针。
- 要搞明白Go语言中的指针需要先知道3个概念:指针地址、指针类型和指针取值。
- Go语言中的函数传参都是值拷贝,当我们想要修改某个变量的时候,我们可以创建一个指向该变量地址的指针变量。
- 传递数据使用指针,而无须拷贝数据。类型指针不能进行偏移和运算。Go语言中的指针操作非常简单,只需要记住两个符号:&(取地址)和*(根据地址取值)。
- Go语言中的值类型(int、float、bool、string、array、struct)都有对应的指针类型,如:*int、*int64、*string等。
// ptr := &v v的类型为 T
// v:代表被取地址的变量,类型为 T
// ptr:用于接收地址的变量,ptr的类型就为 *T,称做 T 的指针类型。* 代表指针。
package main
import "fmt"
func main() {
a := 10
b := &a
fmt.Printf("a:%d ptr:%p\n", a, &a) // a:10 ptr:0xc00001a078
fmt.Printf("b:%p type:%T\n", b, b) // b:0xc00001a078 type:*int
fmt.Println(&b) // 0xc00000e018
}
方法内部改变变量的值:
package main
import "fmt"
func modify1(x int) {
x = 100
}
func modify2(x *int) {
*x = 100
}
func main() {
a := 10
modify1(a)
fmt.Println(a) // 10
modify2(&a)
fmt.Println(a) // 100
}
当一个指针被定义后没有分配到任何变量时,它的值为 nil
我们来看一个例子
package main
import "fmt"
func main() {
var a *int
*a = 100
fmt.Println(*a) //报错
var b map[string]int
b["测试"] = 100
fmt.Println(b["测试"]) //报错
}
执行上面的代码会引发panic,为什么呢?
在Go语言中对于引用类型的变量,我们在使用的时候不仅要声明它,还要为它分配内存空间,否则我们的值就没办法存储。
而对于值类型的声明不需要分配内存空间,是因为它们在声明的时候已经默认分配好了内存空间。
要分配内存,就引出来今天的new和make。
Go语言中new和make是内建的两个函数,主要用来分配内存
本节开始的示例代码中var a *int只是声明了一个指针变量a但是没有初始化,指针作为引用类型需要初始化后才会拥有内存空间,才可以给它赋值。
应该按照如下方式使用内置的new函数对a进行初始化之后就可以正常对其赋值了:
package main
import "fmt"
func main() {
var a *int
a = new(int) //对a使用new进行初始化
*a = 10
fmt.Println(*a)
}
make也是用于内存分配的,区别于new,它只用于slice、map以及chan的内存创建,而且它返回的类型就是这三个类型本身,而不是他们的指针类型,因为这三种类型就是引用类型,所以就没有必要返回他们的指针
package main
import "fmt"
func main() {
var b map[string]int
b = make(map[string]int, 10) //对b使用make进行初始化
b["测试"] = 100
fmt.Println(b)
}
new 和 make 的区别有以下几点:
1.二者都是用来做内存分配的。
2.make只用于slice、map以及channel的初始化,返回的还是这三个引用类型本身;
3.而new用于类型的内存分配,并且内存对应的值为类型零值,返回的是指向类型的指针。
4. 结构体
Go语言提供了一种自定义数据类型,可以封装多个基本数据类型,这种数据类型叫 结构体,英文名称 struct 。 也就是我们可以通过struct来定义自己的类型
Go语言中通过struct来实现面向对象。
type 类型名 struct {
字段名 字段类型
字段名 字段类型
…
}
- 定义一个person(人)结构体
type person struct {
name string
city string
age int8
}
- 定义一个score(成绩)结构体
type score struct {
name string
english, math, chinese int8 //一样类型的可以写在一行,同时声明
}
结构体实例化
package main
import "fmt"
type person struct {
name string
age int8
}
func main() {
var p1 person //进行结构体初始化,定义一个person类型的变量
p1.name = "pprof.cn" //依次给相应的name,city,age 赋值
p1.age = 18
fmt.Printf("p1=%v\n", p1) //p1={pprof.cn 18}
fmt.Printf("p1=%#v\n", p1) //p1=main.person{name:"pprof.cn", age:18}
//定义一个匿名结构体,并实例化
var person struct{name string; age int}
user.name = "pprof.cn"
user.age = 18
fmt.Printf("%#v\n", person)
//使用new关键字对结构体进行实例化,得到的是结构体的地址
var p2 = new(person)
p2.name = "测试"
p2.age = 18
fmt.Printf("p2=%#v\n", p2) //p2=&main.person{name:"测试", age:18}
//使用&对结构体进行取地址操作相当于对该结构体类型进行了一次new实例化操作。
p3 := &person{}
fmt.Printf("%T\n", p3) //*main.person
fmt.Printf("p3=%#v\n", p3) //p3=&main.person{name:"", age:0}
p3.name = "博客"
p3.age = 30
fmt.Printf("p3=%#v\n", p3) //p3=&main.person{name:"博客", age:30}
//使用键值对初始化结构体
p4 := person{
name: "pprof.cn",
age: 18,
}
fmt.Printf("p5=%#v\n", p4) //p5=main.person{name:"pprof.cn", age:18}
//也可以对结构体指针进行键值对初始化
p5 := &person{
name: "pprof.cn",
age: 18,
}
fmt.Printf("p5=%#v\n", p5) //p5 = &main.person{name:"pprof.cn", age:18}
//当某些字段没有初始值的时候,该字段可以不写。此时,没有指定初始值的字段的值就是该字段类型的零值。
p7 := &person{
name: "pprof.cn",
}
fmt.Printf("p7=%#v\n", p7) //p7=&main.person{name:"pprof.cn", age:0}
//初始化结构体的时候可以简写,也就是初始化的时候不写键,直接写值:
//注意: 1. 必须全部初始化,必须按照顺序来初始化,不能和键值初始化方式混用。
p8 := &person{
"pprof.cn",
18,
}
fmt.Printf("p8=%#v\n", p8) //p8=&main.person{name:"pprof.cn", city:"北京", age:18}
//结构体内存布局
type test struct {
a int8
b int8
c int8
}
n := test{
1, 2, 3
}
fmt.Printf("n.a %p\n", &n.a) // 0xc0000a0060
fmt.Printf("n.b %p\n", &n.b) // 0xc0000a0061
fmt.Printf("n.c %p\n", &n.c) // 0xc0000a0062
}
构造函数
Go语言的结构体没有构造函数,我们可以自己实现。 例如,下方的代码就实现了一个person的构造函数。 因为struct是值类型,如果结构体比较复杂的话,值拷贝性能开销会比较大,所以该构造函数返回的是结构体指针类型。
package main
import "fmt"
type person struct {
name string
age int8
}
func newPerson(name string, age int8) *person {
return &person{
name: name,
age: age,
}
}
func main(){
p := newPerson("pprof.cn", 90)
fmt.Printf("%#v\n", p)
}
5. Map
Go语言中提供的映射关系容器为map,其内部使用散列表(hash)实现。
map是一种无序的基于key-value
的数据结构,Go语言中的map是引用类型
,必须初始化才能使用。
map的声明与初始化
//map类型的变量默认初始值为nil,需要使用make()函数来分配内存。
make(map[KeyType]ValueType, [cap])
//其中cap表示map的容量,该参数虽然不是必须的,
//但是我们应该在初始化map的时候就为其指定一个合适的容量。
func main() {
scoreMap := make(map[string]int, 8)
scoreMap["张三"] = 90
scoreMap["小明"] = 100
fmt.Println(scoreMap)
fmt.Println(scoreMap["小明"])
fmt.Printf("type of a:%T\n", scoreMap)
}
/* 结果如下:
map[小明:100 张三:90]
100
type of a:map[string]int
*/
//map也支持在声明的时候填充元素,这个时候不用make()
func main(){
userinfo := map[string]string{
"name":"Kangkang",
"sex" : "男",
}
fmt.Println(userinfo)
}
/* 结果如下:
map[name:Kangkang sex:男]
*/
判断某一个键是否存在:
value, ok := map[key]
举个例子
func main() {
scoreMap := make(map[string]int)
scoreMap["张三"] = 90
scoreMap["小明"] = 100
// 如果key存在ok为true,v为对应的值;不存在ok为false,v为值类型的零值
v, ok := scoreMap["张三"]
if ok {
fmt.Println(v)
} else {
fmt.Println("查无此人")
}
}
map的遍历
使用for range
遍历map
func main() {
scoreMap := make(map[string]int)
scoreMap["张三"] = 90
scoreMap["小明"] = 100
scoreMap["娜扎"] = 60
for k, v := range scoreMap {
fmt.Println(k, v)
}
}
但我们只想遍历key或者value的时候,可以按下面的写法:
func main() {
scoreMap := make(map[string]int)
scoreMap["张三"] = 90
scoreMap["小明"] = 100
scoreMap["娜扎"] = 60
//只遍历key
for k:= range scoreMap {
fmt.Println(k)
}
//只遍历值,用下划线取代key
for _, v:= range scoreMap {
fmt.Println(v)
}
}
使用 delete()
函数删除键值对
delete(要删除的键值对所在的map,要删除的键值对的键(key))
看以下案例:
func main(){
scoreMap := make(map[string]int)
scoreMap["张三"] = 90
scoreMap["小明"] = 100
scoreMap["娜扎"] = 60
delete(scoreMap, "小明")//将 小明:100 从map中删除
for k,v := range scoreMap{
fmt.Println(k, v)
}
/* 结果如下(小明被删除了,只剩了两个):
张三 90
娜扎 60
*/
}
按照指定顺序遍历map
package main
import (
"fmt"
"sort"
)
func main() {
var scoreMap = make(map[string]int, 10)
for i := 0; i < 5; i++ {
key := fmt.Sprintf("stu%02d", i) //生成stu开头的字符串
value := i
scoreMap[key] = value
}
fmt.Println("这是刚刚存入的结果:")
for key, value := range scoreMap {
fmt.Println(key, value)
}
fmt.Println("这是排序好的结果:")
//取出map中的所有key存入切片keys
var keys = make([]string, 0, 200)
for key := range scoreMap {
keys = append(keys, key)
}
//对切片进行排序
sort.Strings(keys)
//按照排序后的key遍历map
for _, key := range keys {
fmt.Println(key, scoreMap[key])
}
}
结果如下:
这是刚刚存入的结果:
stu04 4
stu00 0
stu01 1
stu02 2
stu03 3
这是排序好的结果:
stu00 0
stu01 1
stu02 2
stu03 3
stu04 4
value值为切片类型的map
无非就是切片里面不是int,string这些了,里面都是map,只不过里面的map没有名字,都是通过下标来调取,代码如下:
func main() {
//定义一个切片类型的数组
var mapSlice = make([]map[string]string, 3)
// 对切片中下标为0的map进行初始化
mapSlice[0] = make(map[string]string, 10)
mapSlice[0]["name"] = "小王子"
mapSlice[0]["password"] = "123456"
mapSlice[0]["address"] = "沙河"
for index, value := range mapSlice {
fmt.Println(index, value)
}
}
元素为map类型的切片
这种类型就是key该是啥还是啥,就是每一个key对应的value不是一个值,而是一个切片了,这是一对多的关系
func main() {
//定义一个值为切片类型的map,map容量为3
var sliceMap = make(map[string][]string, 3)
key := "中国"
value, ok := sliceMap[key]
if !ok {
value = make([]string, 0, 2)
}
value = append(value, "北京", "上海") //向切片里面添加元素
sliceMap[key] = value
fmt.Println(sliceMap)
}
结果:map[中国:[北京 上海]]
6. 接口
接口(interface)定义了一个对象的行为规范,只定义规范不实现,由具体的对象来实现规范的细节。
在Go语言中接口(interface)是一种类型,一种抽象的类型。
interface
是一组method
的集合,是duck-type programming的一种体现。
接口做的事情就像是定义一个协议(规则),只要一台机器有洗衣服和甩干的功能,我就称它为洗衣机。不关心属性(数据),只关心行为(方法)。
每个接口由数个方法组成,接口的定义格式如下:
type 接口名 interface{
方法名1( 参数列表1 ) 返回值列表1
方法名2( 参数列表2 ) 返回值列表2
…
}
1.接口名:使用type将接口定义为自定义的类型名。Go语言的接口在命名时,一般会在单词后面添加er,
如有写操作的接口叫Writer,有字符串功能的接口叫Stringer等。接口名最好要能突出该接口的类型含义。
2.方法名:当方法名首字母是大写且这个接口类型名首字母也是大写时,
这个方法可以被接口所在的包(package)之外的代码访问。
3.参数列表、返回值列表:参数列表和返回值列表中的参数变量名可以省略。
举个例子:
type writer interface{
Write([]byte) error
}
当你看到这个接口类型的值时,你不知道它是什么,唯一知道的就是可以通过它的Write方法来做一些事情。
怎么去使用接口?
package main
import "fmt"
//定义一个功能为说话的接口
type Sayer interface {
say(name string)
}
//定义dog结构体
type dog struct {
name string
}
//定义cat结构体
type cat struct {}
//cat和dog实现Sayer接口(say()里面虽然定义接口的时候有参数,但是实现的时候可以没有)
func (c cat) say() {
fmt.Println("喵喵喵")
}
func (d dog) say(name string) {
fmt.Println(name)
}
func main() {
c := cat{} // 实例化一个cat
d := dog{} // 实例化一个dog
c.say() // 调用say()方法
d.name = "汪汪汪"
d.say(d.name) //调用say()方法
}
值接收者 || 指针接收者 --> 实现接口
1. 值接收者
package main
import "fmt"
type Mover interface {
move()
}
type dog struct {}
func (d dog) move() {
fmt.Println("狗会动")
}
func main() {
var x Mover
var wangcai = dog{} // 旺财是dog类型
x = wangcai // x可以接收dog类型
var fugui = &dog{} // 富贵是*dog类型
x = fugui // x可以接收*dog类型
x.move()
}
从上面的代码中我们可以发现,使用值接收者实现接口之后,不管是dog结构体还是结构体指针*dog类型的变量都可以赋值给该接口变量。
因为Go语言中有对指针类型变量求值的语法糖,dog指针fugui内部会自动求值*fugui
。
2. 指针接收者
//这里的代码和上方一样,唯一的区别就是这个方法接受的是*dog,不是dog
func (d *dog) move() {
fmt.Println("狗会动")
}
func main() {
var x Mover
var wangcai = dog{} // 旺财是dog类型
x = wangcai // 注意: 这个时候 x 不可以接收dog类型
var fugui = &dog{} // 富贵是*dog类型
x = fugui // x可以接收*dog类型
}
此时实现Mover接口的是*dog类型,所以不能给x传入dog类型的 wangcai,此时x只能存储 *dog类型的值。
一个类型实现多个接口
package main
import "fmt"
// Sayer 接口
type Sayer interface {
say()
}
// Mover 接口
type Mover interface {
move()
}
//定义dog结构体
type dog struct{
name string
}
// 实现Sayer接口
func (d dog) say() {
fmt.Printf("%s会叫汪汪汪\n", d.name)
}
// 实现Mover接口
func (d dog) move() {
fmt.Printf("%s会动\n", d.name)
}
func main() {
var a = dog{name: "旺财"}
a.say()
a.move()
}
结果如下:
旺财会叫
旺财会动
接口嵌套
接口与接口间可以通过嵌套创造出新的接口。
嵌套得到的接口的使用与普通接口一样,这里我们让cat实现animal接口,代码如下:
package main
import "fmt"
// Sayer 接口
type Sayer interface {
say()
}
// Mover 接口
type Mover interface {
move()
}
// 接口嵌套
type animal interface {
Sayer
Mover
}
type cat struct {
name string
}
func (c cat) say() {
fmt.Println("喵喵喵")
}
func (c cat) move() {
fmt.Println("猫会动")
}
func main() {
var x animal
x = cat{name: "花花"}
x.move()
x.say()
}
空接口的作用
空接口是指没有定义任何方法的接口。因此任何类型都实现了空接口。
空接口类型的变量可以存储任意类型的变量。
- 空接口作为函数的参数
// 空接口作为函数参数
func show(a interface{}) {
fmt.Printf("type:%T value:%v\n", a, a)
}
- 空接口作为map的值
var studentInfo = make(map[string]interface{})
studentInfo["name"] = "李白" //字符串类型
studentInfo["age"] = 18 //整型类型
studentInfo["married"] = false //布尔类型
fmt.Println(studentInfo)
- 怎么取出存在空接口里的数据呢
想要判断空接口中的值这个时候就可以使用类型断言,其语法格式:
x.(T)
其中:
x:表示类型为interface{}的变量
T:表示断言x可能是的类型。
该语法返回两个参数,第一个参数是x转化为T类型后的变量,第二个值是一个布尔值,
若为true则表示断言成功,为false则表示断言失败。
举个例子:
package main
import "fmt"
func main() {
var x interface{}
x = "pprof.cn"
v, ok := x.(string)
if ok {
fmt.Println(v)
} else {
fmt.Println("类型断言失败")
}
}
上面的示例中如果要断言多次就需要写多个if判断,这个时候我们可以使用switch语句来实现:
package main
import "fmt"
func main() {
var x interface{}
x = "pprof.cn"
justifyType(x)
}
func justifyType(x interface{}) {
switch v := x.(type) {
case string:
fmt.Printf("x is a string,value is %v\n", v)
case int:
fmt.Printf("x is a int is %v\n", v)
case bool:
fmt.Printf("x is a bool is %v\n", v)
default:
fmt.Println("unsupport type!")
}
}
注意:只有当有两个或两个以上的具体类型必须以相同的方式进行处理时才需要定义接口。不要为了接口而写接口,那样只会增加不必要的抽象,导致不必要的运行时损耗。
7. 错误处理
panic
1、内置函数
2、假如函数F中书写了panic语句,会终止其后要执行的代码,在panic所在函数F内如果存在要执行的defer函数列表,按照defer的逆序执行
3、返回函数F的调用者G,在G中,调用函数F语句之后的代码不会执行,假如函数G中存在要执行的defer函数列表,按照defer的逆序执行
4、直到goroutine整个退出,并报告错误
recover
1、内置函数
2、用来控制一个goroutine的panicking行为,捕获panic,从而影响应用的行为
3、一般的调用建议
a). 在defer函数中,通过recover来终止一个goroutine的panicking过程,从而恢复正常代码的执行
b). 可以获取通过panic传递的error
package main
import "fmt"
func funcA() {
fmt.Println("func A")
}
func funcB() {
defer func() {
err := recover()
//如果程序出出现了panic错误,可以通过recover恢复过来
if err != nil {
fmt.Println("recover in B")
}
}()
panic("panic in B")
fmt.Println("func C") //这条语句在panic之后,所以不会执行
}
func funcC() {
fmt.Println("func B")
}
func main() {
funcA()
funcB()
funcC()
}
结果:
func A
recover in B
func C
注意:
1.利用recover处理panic指令,defer 必须放在 panic 之前定义,另外 recover 只有在 defer 调用的函数中才有效。否则当panic时,recover无法捕获到panic,无法防止panic扩散。
2.recover 处理异常后,逻辑并不会恢复到 panic 那个点去,函数跑到 defer 之后的那个点。
3.多个 defer 会形成 defer 栈,后定义的 defer 语句会被最先调用。
总结
以上就是我这周学习Golang基础的总结,第一次写知识总结性质的博客,有些地方不太好,时间较为紧张,总结的还不是很全面,还需多加努力,知识还需进一步掌握,根基稳了,走路的时候才能走的实!如果还有下一篇关于Golang的博客,那主题应该就是Golang网络编程啦,期待会更好❥(^_-)在这里推荐两个Golang的学习网站:
topgoer.com
liwenzhou.com
附:如果有哪些地方表达不是很准确,欢迎大牛前来指正!!!