Go语言基础
推荐教程 李文周的博客 Golang文档查询
开发环境搭建 环境安装 GO环境下载地址
安装开发工具 VSCode下载
windows配置代理,然后重启VsCode
1 2 go env -w GO111MODULE =on go env -w GOPROXY =https://goproxy.cn,direct
ctrl + shift + p
:输入Go Install/Update Tools
Hello World 新建一个文件 xx.go
1 2 3 func main () { fmt.Println("Hello world" ) }
在当前目录输入 go build xx.go
,会生成一个可执行的文件 在当前目录输入 go run xx.go
,会执行当前文件
基础 变量 声明变量:var 变量名 变量类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 var name string var age int var isOk bool var ( name2 string age2 int isOk2 bool )func main () { name2 = "小祥" age2 = 10 isOk2 = true fmt.Printf("name2:%s" , name2) fmt.Println(age2) fmt.Println(isOk2) var a string = "a" fmt.Println(a) }
初始化多个变量 1 var name, age = "小祥" , 10
类型推导 1 2 var name = "小祥" var age = 22
短变量声明
匿名变量 匿名变量不占用命名空间,不会分配内存,所以匿名变量之间不存在重复声明
1 2 3 4 5 6 7 8 9 func foo () (int , string ) { return 20 , "小祥" }func main () { x, _ := foo() _, y := foo() fmt.Println("x=" , x) fmt.Println("y=" , y) }
注意事项
函数外 的每个语句都必须以关键字开始(var、const、func等)
:=不能 使用在函数外。
_多用于占位 ,表示忽略值。
常量 相对于变量,常量是恒定不变的值 ,多用于定义程序运行期间不会改变的那些值。 常量的声明和变量声明非常类似,只是把var换成了const,常量在定义的时候必须赋值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const a = "a" const ( NUM1 = 1 NUM2 = 2 )const ( B1 = 0 B2 B3 )
iota iota是go语言的常量计数器,只能在常量的表达式中使用
iota在const关键字出现时将被重置为0 。const中每新增一行常量声明将使iota计数一次(iota可理解为const语句块中的行索引)。 使用iota能简化定义,在定义枚举时很有用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 const ( C1 = iota C2 = iota C3 C4 )const ( D1 = iota D2 _ D3 )const ( E1 = iota E2 = 10 E3 = iota E4 )const ( F1, F2 = iota + 1 , iota + 2 F3, F4 = iota + 2 , iota + 3 )
基本数据类型 整型 整型分为以下两个大类: 按长度分为:int8、int16、int32、int64
对应的无符号整型:uint8、uint16、uint32、uint64
其中,uint8就是我们熟知的byte型,int16对应C语言中的short型,int64对应C语言中的long型
类型
描述
uint8
无符号 8位整型 (0 到 255)
uint16
无符号 16位整型 (0 到 65535)
uint32
无符号 32位整型 (0 到 4294967295)
uint64
无符号 64位整型 (0 到 18446744073709551615)
int8
有符号 8位整型 (-128 到 127)
int16
有符号 16位整型 (-32768 到 32767)
int32
有符号 32位整型 (-2147483648 到 2147483647)
int64
有符号 64位整型 (-9223372036854775808 到 9223372036854775807)
特殊整型
类型
描述
uint
32位操作系统上就是uint32,64位操作系统上就是uint64
int
32位操作系统上就是int32,64位操作系统上就是int64
uintptr
无符号整型,用于存放一个指针
进制
1 2 3 4 5 6 7 8 9 10 11 12 func main () { i := 10 fmt.Printf("i: %d\n" , i) fmt.Printf("i: %b\n" , i) fmt.Printf("i: %o\n" , i) fmt.Printf("i: %x\n" , i) fmt.Printf("i是 %T 类型\n" , i) i2 := int8 (2 ) fmt.Printf("i2是 %T 类型\n" , i2) }
浮点数 1 2 3 4 5 6 7 func main () { f1 := 1.203922 fmt.Printf("f1 是 %T 类型\n" , f1) f2 := float32 (1.223 ) fmt.Printf("f2 是 %T 类型\n" , f2) }
布尔值 默认是false
1 2 3 4 5 func main () { b1 := true fmt.Printf("b1 是 %T 类型\n" , b1) }
字符串 Go语言中的字符串以原生数据类型出现,使用字符串就像使用其他原生数据类型(int、bool、float32、float64 等)一样 Go 语言里的字符串的内部实现使用UTF-8编码
字符串的值为双引号(“)中的内容,单引号(‘)包裹的是字符 ,可以在Go语言的源码中直接添加非ASCII码字符
多行字符串使用 `` 包裹
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 func main () { b2 := "b2" fmt.Printf("b2 是 %T 类型\n" , b2) b3 := ` 第一行 第二行 第三行 ` fmt.Println(b3) b4 := "b4" fmt.Printf("字符串 %v\n" , b4) fmt.Printf("字符串 %#v\n" , b4) }
字符串转义符
转义符
描述
\r
回车符(返回行首)
\n
换行符(直接跳到下一行的同列位置)
\t
制表符
\‘
单引号
\“
双引号
\\
反斜杠
字符串常用操作
方法
描述
len(str)
求长度
+或fmt.Sprintf
拼接字符串
strings.Split(str,par)
分割
strings.contains(str,par)
判断是否包含
strings.HasPrefix(str,par),strings.HasSuffix(str,par)
前缀\后缀判断
strings.Index(str,par),strings.LastIndex(str,par)
子串出现的位置
strings.Join(a[]string, sep string)
join操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 package mainimport ( "fmt" "strings" )func main () { b4 := "b4" fmt.Println(len (b4)) b5 := "你好" b6 := "字符串" b7 := fmt.Sprintf("%s %s" , b5, b6) b8 := b5 + b6 fmt.Println("b7 = " , b7) fmt.Println("b8 = " , b8) b9 := "a-b-c-d-c" fmt.Println(strings.Split(b9, "-" )) fmt.Println(strings.Contains(b9, "c" )) fmt.Println(strings.HasPrefix(b9, "a" )) fmt.Println(strings.HasSuffix(b9, "a" )) fmt.Println(strings.Index(b9, "c" )) fmt.Println(strings.LastIndex(b9, "c" )) fmt.Println(strings.Join(strings.Split(b9, "-" ), ":" )) }
修改字符串 要修改字符串,需要先将其转换成[]rune或[]byte
,完成后再转换为string 无论哪种转换,都会重新分配内存,并复制字节数组
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package mainimport ( "fmt" "strings" )func main () { b10 := "string" byteS1 := []byte (b10) byteS1[0 ] = 'S' fmt.Println(string (byteS1)) b11 := "hot" runeS2 := []rune (b11) runeS2[0 ] = 'H' fmt.Println(string (runeS2)) }
流程控制 Go语言中最常用的流程控制有if和for 而switch和goto主要是为了简化代码、降低重复代码而生的结构,属于扩展类的流程控制
if else 1 2 3 4 5 6 7 if 表达式1 { 分支1 } else if 表达式2 { 分支2 } else { 分支3 }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 func main () { age := 20 if age > 20 { fmt.Println("年龄大于20" ) } else { fmt.Println("年龄不够20" ) } if age2 := 10 ; age2 > 10 { fmt.Println("年龄大于等于10" ) } if age > 10 && age != 19 { fmt.Println("年龄大于10 不等于19" ) } }
for for循环可以通过break、continue(结束本次循环)、goto、return、panic
语句强制退出循环。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 func main () { for i := 0 ; i < 10 ; i++ { fmt.Printf("%d" , i) } fmt.Println() i := 5 for ; i < 10 ; i++ { fmt.Printf("%d" , i) } fmt.Println() var i2 int = 3 for i2 < 10 { fmt.Printf("%d" , i2) i2++ } for { fmt.Println("死循环" ) } }
for range Go语言中可以使用for range遍历数组、切片、字符串、map 及通道(channel) 通过for range遍历的返回值有以下规律:
数组、切片、字符串返回索引和值。
map返回键和值。
通道(channel)只返回通道内的值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 func main () { s := "Hello小祥" for i, v := range s { fmt.Printf("索引是 %d,值是%c\n" , i, v) } }
1 2 3 4 5 6 7 for i := 1 ; i < 10 ; i++ { for j := 1 ; j <= i; j++ { fmt.Printf("%d * %d = %d " , i, j, i*j) } fmt.Println() }
switch case 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 func main () { f := 2 switch f { case 1 : fmt.Println(1 ) case 2 , 3 : fmt.Println(2 , 3 ) case 4 : fmt.Println(4 ) default : fmt.Println("无" ) } switch n := 7 ; n { case 1 , 3 , 5 , 7 , 9 : fmt.Println("基数" ) case 2 , 4 , 6 , 8 , 10 : fmt.Println("偶数" ) default : fmt.Println(n) } }
数组 数组是同一种数据类型元素的集合。 在Go语言中,数组从声明时就确定,使用时可以修改数组成员,但是数组大小不可变化
1 2 3 4 5 6 func main () { var a1 [3 ]int var a2 [4 ]int fmt.Printf("a1 是 %T, a2 是 %T\n" , a1, a2) }
数组初始化 如果不初始化,默认元素都是零值,布尔就是false、整型和浮点型是0、字符串是””
第一种方式 初始化数组时可以使用初始化列表来设置数组元素的值
1 2 3 4 5 6 7 8 func main () { var testArray [3 ]int var numArray = [3 ]int {1 , 2 } var cityArray = [3 ]string {"北京" , "上海" , "深圳" } fmt.Println(testArray) fmt.Println(numArray) fmt.Println(cityArray) }
第二种方式 让编译器根据初始值的个数自行推断数组的长度
1 2 3 4 5 6 7 8 func main () { var numArray = [...]int {1 , 2 } var cityArray = [...]string {"北京" , "上海" , "深圳" } fmt.Println(numArray) fmt.Printf("numArray是:%T\n" , numArray) fmt.Println(cityArray) fmt.Printf("cityArray是:%T\n" , cityArray) }
第三种方式 使用指定索引值的方式来初始化数组
1 2 3 4 5 func main () { a := [...]int {1 : 1 , 3 : 5 } fmt.Println(a) fmt.Printf("类型a是:%T\n" , a) }
多维数组 1 2 3 4 5 6 7 8 9 func main () { a := [3 ][2 ]string { {"北京" , "上海" }, {"广州" , "深圳" }, {"成都" , "重庆" }, } fmt.Println(a) fmt.Println(a[2 ][1 ]) }
注意: 多维数组只有**第一层可以使用…**来让编译器推导数组长度
1 2 3 4 5 6 7 8 9 10 11 12 a := [...][2 ]string { {"北京" , "上海" }, {"广州" , "深圳" }, {"成都" , "重庆" }, } b := [3 ][...]string { {"北京" , "上海" }, {"广州" , "深圳" }, {"成都" , "重庆" }, }
数组是值传递 数组是值类型,赋值和传参会复制整个数组。因此改变副本的值,不会改变本身的值。
1 2 3 4 a2 := [2 ]int {1 , 2 } a3 := a2 a3[0 ] = 100 fmt.Println(a2, a3)
切片
切片指向了一个底层的数组 切片的长度 就是他元素的个数 切片的容量 是底层数组从切片的第一个元素到最后一个元素的数量 切片是引用类型 ,真正的数据都存储在底层数组里的 切片拥有自己的长度和容量,我们可以通过使用内置的len()
函数求长度,使用内置的cap()
函数求切片的容量 切片是一个框,框住的是一块连续的内存
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 func main () { var s1 []int fmt.Println(s1) fmt.Println(s1 == nil ) s1 = []int {1 , 2 , 3 } fmt.Println(s1) fmt.Println(len (s1)) fmt.Println(cap (s1)) s2 := s1[:2 ] s3 := s1[1 :] s4 := s1[:] fmt.Println(s2, s3, s4) s1[0 ] = 100 fmt.Println(s2, s3, s4) fmt.Printf("s2的长度为%d, 容量为%d\n" , len (s2), cap (s2)) fmt.Printf("s3的长度为%d, 容量为%d\n" , len (s3), cap (s3)) fmt.Printf("s4的长度为%d, 容量为%d\n" , len (s4), cap (s4)) }
切片的遍历 1 2 3 4 5 6 7 8 9 10 11 func main () { d1 := make ([]int , 3 ) for i := 0 ; i < len (d1); i++ { fmt.Println(d1[i]) } for _, v := range d1 { fmt.Println(v) } }
make函数 T
: 切片的元素类型size
: 切片中元素的数量cap
: 切片的容量
1 2 3 4 5 6 func main () { a := make ([]int , 2 , 10 ) fmt.Println(a) fmt.Println(len (a)) fmt.Println(cap (a)) }
append方法 Go语言的内建函数append()可以为切片动态添加元素。可以一次添加一个元素,可以添加多个元素 ,也可以添加另一个切片中的元素(后面加… ) append()函数将元素追加到切片的最后并返回该切片 切片numSlice的容量按照1,2,4,8,16
这样的规则自动进行扩容,每次扩容后都是扩容前的2倍
1 2 3 4 5 6 7 8 9 10 11 func main () { var f1 []int f1 = append (f1, 1 ) fmt.Println(f1) f1 = append (f1, 2 , 3 ) fmt.Println(f1) f2 := []int {5 , 6 } f1 = append (f1, f2...) fmt.Println(f1) }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 func main () { var h1 []int for i := 0 ; i < 10 ; i++ { h1 = append (h1, i) fmt.Printf("%v len:%d cap:%d ptr:%p\n" , h1, len (h1), cap (h1), h1) } }
copy函数 Go语言内建的copy()
函数可以迅速地将一个切片的数据复制到另外一个切片空间中
1 copy (destSlice, srcSlice []T)
1 2 3 4 5 6 7 8 9 10 func main () { a := []int {1 , 2 , 3 } var b = make ([]int , 3 , 3 ) copy (b, a) fmt.Println(a) fmt.Println(b) b[0 ] = 10 fmt.Println(a) fmt.Println(b) }
从切片中删除元素 Go语言中并没有删除切片元素的专用方法,我们可以使用切片本身的特性来删除元素
切片不保存具体的值
切片对应一个底层数组
底层数组都是占用一块连续的内存
1 2 3 4 5 6 7 8 9 func main () { a := [...]int {1 , 3 , 5 } a2 := a[:] fmt.Println(a, a2) a2 = append (a2[:1 ], a2[2 :]...) fmt.Println(a, a2) }
注意 要检查切片是否为空,使用len(s) == 0
来判断,而不应该使用s == nil
来判断。
1 2 3 4 5 6 7 8 func main () { var s1 []int s2 := []int {} s3 := make ([]int , 0 ) fmt.Println(len (s1), cap (s1), s1 == nil ) fmt.Println(len (s2), cap (s2), s2 == nil ) fmt.Println(len (s3), cap (s3), s3 == nil ) }
拷贝前后两个变量共享底层数组,对一个切片的修改会影响另一个切片的内容
1 2 3 4 5 6 7 8 func main () { d1 := make ([]int , 3 ) d2 := d1 fmt.Println(d1) d2[2 ] = 10 fmt.Println(d1) fmt.Println(d2) }
切片的扩容策略 可以通过查看$GOROOT/src/runtime/slice.go
源码,其中扩容相关代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 newcap := old.cap doublecap := newcap + newcapif cap > doublecap { newcap = cap } else { if old.len < 1024 { newcap = doublecap } else { for 0 < newcap && newcap < cap { newcap += newcap / 4 } if newcap <= 0 { newcap = cap } } }
首先判断,如果新申请容量(cap)大于2倍的旧容量(old.cap),最终容量(newcap)就是新申请的容量(cap)。
否则判断,如果旧切片的长度小于1024 ,则最终容量(newcap)就是旧容量(old.cap)的两倍 ,即(newcap=doublecap),
否则判断,如果旧切片长度大于等于1024 ,则最终容量(newcap)从旧容量(old.cap)开始循环增加原来的1/4 ,即(newcap=old.cap,for {newcap += newcap/4})直到最终容量(newcap)大于等于新申请的容量(cap),即(newcap >= cap)
如果最终容量(cap)计算值溢出,则最终容量(cap)就是新申请容量(cap)。
需要注意的是,切片扩容还会根据切片中元素的类型不同而做不同的处理 ,比如int和string类型的处理方式就不一样。
map 基本结构
KeyType:表示键的类型。
ValueType:表示键对应的值的类型。
map类型的变量默认初始值为nil
,需要使用make()函数
来分配内存
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 func main () { map1 := make (map [int ]string , 8 ) fmt.Println(map1) map1[1 ] = "1" map1[2 ] = "2" fmt.Println(map1) fmt.Println(map1[1 ]) map2 := map [string ]bool { "小米" : true , "大米" : false , } fmt.Println(map2) _, ok := map2["1" ] fmt.Println("值为 " , ok) }
使用delete()
内建函数从map中删除一组键值对
1 2 3 4 5 6 7 8 9 10 func main () { map3 := map [int ]string { 1 : "小明" , 2 : "小红" , 3 : "小白" , } fmt.Println(map3) delete (map3, 2 ) fmt.Println(map3) }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 func main () { mS := make ([]map [string ]string , 2 , 10 ) for i, m := range mS { fmt.Printf("索引:%d,值:%v\n" , i, m) } map4 := map [string ]string { "1" : "值1" , "2" : "值2" , "3" : "值3" , } mS = append (mS, map4) fmt.Println(mS) sM := make (map [string ][]string , 2 ) s := make ([]string , 2 , 4 ) s[0 ] = "1" s[1 ] = "2" sM["1" ] = s fmt.Println(sM) }
指针
1 2 3 4 5 6 7 8 9 func main () { n := 10 p := &n fmt.Println("指针地址为 " , p) fmt.Printf("类型为 %T\n" , p) m := *p fmt.Println(m) fmt.Printf("类型为 %T\n" , m) }
new和make new new
是一个内置的函数
Type表示类型,new函数只接受一个参数 ,这个参数是一个类型
*Type表示类型指针,new函数返回一个指向该类型内存地址的指针 。
使用new函数得到的是一个类型的指针,并且该指针对应的值为该类型的零值
1 2 3 4 5 6 7 8 9 10 func main () { a := new (int ) b := new (bool ) fmt.Printf("%T\n" , a) fmt.Printf("%T\n" , b) fmt.Println(*a) fmt.Println(*b) *a = 100 fmt.Println(*a) }
make make
也是用于内存分配的,区别于new,它只用于slice、map以及chan的内存创建 ,而且它返回的类型就是这三个类型本身,而不是他们的指针类型 ,因为这三种类型就是引用类型
1 func make (t Type, size ...IntegerType) Type
new与make的区别
二者都是用来做内存分配 的。
make
只用于slice、map以及channel的初始化,返回的还是这三个引用类型本身 ;
而new
用于类型的内存分配,并且内存对应的值为类型零值,返回的是指向类型的指针 。
函数 Go语言中定义函数使用func 关键字
1 2 3 func 函数名(参数) (返回值) { 函数体 }
函数的调用
定义了函数之后,我们可以通过函数名()
的方式调用函数
1 2 3 4 5 func main () { sayHello() ret := intSum(10 , 20 ) fmt.Println(ret) }
函数的参数中如果相邻变量的类型相同,则可以省略类型
可变参数是指函数的参数数量不固定。Go语言中的可变参数通过在参数名后加...
来标识
1 2 3 4 5 6 7 8 9 10 11 12 func intSum (x, y int ) int { return x + y }func intSum2 (x int , y ...int ) int { fmt.Println(x) sum := 0 for _, v := range x { sum = sum + v } return sum }
返回值 Go语言中函数支持多返回值,函数如果有多个返回值时必须用()将所有返回值包裹起来 函数定义时可以给返回值命名,并在函数体中直接使用这些变量,最后通过return关键字返回 当我们的一个函数返回值类型为slice时,nil可以看做是一个有效的slice,没必要显示返回一个长度为0的切片
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 func calc (x, y int ) (int , int ) { sum := x + y sub := x - y return sum, sub }func calc2 (x, y int ) (sum, sub int ) { sum = x + y sub = x - y return }func someFunc (x string ) []int { if x == "" { return nil } ... }
函数类型 我们可以使用type关键字来定义一个函数类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 type c func (int , int ) int func add (x, y int ) int { return x + y }func sub (x, y int ) int { return x - y }func main () { var cc c cc = add }
高阶函数 函数可以作为参数 1 2 3 4 5 6 7 8 9 10 func add (x, y int ) int { return x + y }func calc (x, y int , op func (int , int ) int ) int { return op(x, y) }func main () { ret2 := calc(10 , 20 , add) fmt.Println(ret2) }
函数作为返回值 1 2 3 4 5 6 7 8 9 10 11 func do (s string ) (func (int , int ) int , error ) { switch s { case "+" : return add, nil case "-" : return sub, nil default : err := errors.New("无法识别的操作符" ) return nil , err } }
匿名函数 匿名函数就是没有函数名的函数
1 2 3 4 5 6 7 8 9 10 11 12 func main () { add := func (x, y int ) { fmt.Println(x + y) } add(10 , 20 ) func (x, y int ) { fmt.Println(x + y) }(10 , 20 ) }
闭包 闭包是一个函数,这个函数包含了他外部作用域的一个变量 闭包指的是一个函数和与其相关的引用环境组合而成的实体。简单来说,闭包=函数+引用环境
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 func adder () func (int ) int { var x int return func (y int ) int { x += y return x } }func main () { var f = adder() fmt.Println(f(10 )) fmt.Println(f(20 )) fmt.Println(f(30 )) f1 := adder() fmt.Println(f1(40 )) fmt.Println(f1(50 )) }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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 }func main () { f1, f2 := calc(10 ) fmt.Println(f1(1 ), f2(2 )) fmt.Println(f1(3 ), f2(4 )) fmt.Println(f1(5 ), f2(6 )) }
defer语句 Go语言中的defer语句会将其后面跟随的语句进行延迟处理 。 在defer归属的函数即将返回时,将延迟处理的语句按defer定义的逆序进行执行 也就是说,先进后出,后进先出 ,先被defer的语句最后被执行,最后被defer的语句,最先被执行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 func main () { fmt.Println("start" ) defer fmt.Println(1 ) defer fmt.Println(2 ) defer fmt.Println(3 ) fmt.Println("end" ) }
执行时机 在Go语言的函数中return语句在底层并不是原子操作,它分为给返回值赋值和RET指令两步。而defer语句执行的时机就在返回值赋值操作后,RET指令执行前
面试题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 func calc (index string , a, b int ) int { ret := a + b fmt.Println(index, a, b, ret) return ret }func main () { x := 1 y := 2 defer calc("AA" , x, calc("A" , x, y)) x = 10 defer calc("BB" , x, calc("B" , x, y)) y = 20 }
内置函数
内置函数
介绍
close
主要用来关闭channel
len
用来求长度,比如string、array、slice、map、channel
new
用来分配内存,主要用来分配值类型,比如int、struct。返回的是指针
make
用来分配内存,主要用来分配引用类型,比如chan、map、slice
append
用来追加元素到数组、slice中
panic和recover
用来做错误处理
类型别名和自定义类型 自定义类型 通过type关键字 的定义,MyInt就是一种新的类型,它具有int的特性。
类型别名 1 2 3 4 type TypeAlias = Typetype byte = uint8 type rune = int32
两者区别 1 2 3 4 5 6 7 8 9 10 11 12 13 type NewInt int type MyInt = int func main () { var a NewInt var b MyInt fmt.Printf("type of a:%T\n" , a) fmt.Printf("type of b:%T\n" , b) }
结果显示a的类型是main.NewInt
,表示main包下定义的NewInt
类型。 b的类型是int
。MyInt类型只会在代码中存在,编译完成时并不会有MyInt类型。
结构体 结构体中字段大写开头表示可公开访问,小写表示私有 (仅在定义当前结构体的包中可访问)
基本类型 Go语言中通过struct
来实现面向对象 使用type
和struct
关键字来定义结构体
类型名:标识自定义结构体的名称,在同一个包内不能重复 。
字段名:表示结构体字段名。结构体中的字段名必须唯一 。
字段类型:表示结构体字段的具体类型。
1 2 3 4 5 type 类型名 struct { 字段名 字段类型 字段名 字段类型 … }
1 2 3 4 5 6 7 8 9 10 type person struct { name string city string age int8 }type person1 struct { name, city string age int8 }
实例化 只有当结构体实例化时,才会真正地分配内存。也就是必须实例化后才能使用结构体的字段。
基本实例化 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 var 结构体实例 结构体类型type person struct { name string city string age int8 }func main () { var p1 person p1.name = "极客小祥" p1.city = "北京" p1.age = 18 fmt.Printf("p1=%v\n" , p1) fmt.Printf("p1=%#v\n" , p1) }
匿名结构体 1 2 3 4 5 6 func main () { var user struct {Name string ; Age int } user.Name = "小王子" user.Age = 18 fmt.Printf("%#v\n" , user) }
结构体的匿名字段 结构体允许其成员字段在声明时没有字段名而只有类型,这种没有名字的字段就称为匿名字段 注意:这里匿名字段的说法并不代表没有字段名,而是默认会采用类型名作为字段名,结构体要求字段名称必须唯一 ,因此一个结构体中同种类型的匿名字段只能有一个
1 2 3 4 5 6 7 8 9 10 11 12 13 14 type Person struct { string int }func main () { p1 := Person{ "小王子" , 18 , } fmt.Printf("%#v\n" , p1) fmt.Println(p1.string , p1.int ) }
创建指针类型结构体 通过使用new
关键字对结构体进行实例化,得到的是结构体的地址 Go语言中支持对结构体指针直接使用.
来访问结构体的成员
1 2 3 4 5 6 7 8 func main () { var p2 = new (person) p2.name = "小王子" p2.age = 28 p2.city = "上海" fmt.Printf("%T\n" , p2) fmt.Printf("p2=%#v\n" , p2) }
取结构体的地址实例化 使用&
对结构体进行取地址操作相当于对该结构体类型进行了一次new
实例化操作
1 2 3 4 5 6 7 8 9 func mian () { p3 := &person{} fmt.Printf("%T\n" , p3) fmt.Printf("p3=%#v\n" , p3) p3.name = "七米" p3.age = 30 p3.city = "成都" fmt.Printf("p3=%#v\n" , p3) }
初始化 使用键值对初始化 使用键值对对结构体进行初始化时,键对应结构体的字段,值对应该字段的初始值
1 2 3 4 5 6 7 8 func main () { p5 := person{ name: "小王子" , city: "北京" , age: 18 , } fmt.Printf("p5=%#v\n" , p5) }
也可以对结构体指针进行键值对初始化
1 2 3 4 5 6 7 8 func mian () { p6 := &person{ name: "小王子" , city: "北京" , age: 18 , } fmt.Printf("p6=%#v\n" , p6) }
当某些字段没有初始值的时候,该字段可以不写。此时,没有指定初始值的字段的值就是该字段类型的零值
1 2 3 4 5 6 func main () { p7 := &person{ city: "北京" , } fmt.Printf("p7=%#v\n" , p7) }
使用值的列表初始化 初始化结构体的时候可以简写,也就是初始化的时候不写键,直接写值 使用这种格式初始化时,需要注意:
必须初始化结构体的所有字段。
初始值的填充顺序必须与字段在结构体中的声明顺序一致。
该方式不能和键值初始化方式混用。
1 2 3 4 5 6 7 8 func main () { p8 := &person{ "沙河娜扎" , "北京" , 28 , } fmt.Printf("p8=%#v\n" , p8) }
结构体内存布局 结构体占用一块连续的内存
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 func main () { type test struct { a int8 b int8 c int8 d int8 } n := test{ 1 , 2 , 3 , 4 , } fmt.Printf("n.a %p\n" , &n.a) fmt.Printf("n.b %p\n" , &n.b) fmt.Printf("n.c %p\n" , &n.c) fmt.Printf("n.d %p\n" , &n.d) }
构造函数 Go语言的结构体没有构造函数,我们可以自己实现 因为struct是值类型,如果结构体比较复杂的话,值拷贝性能开销会比较大,所以该构造函数返回的是结构体指针类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 type person struct { name string city string age int8 }func newPerson (name, city string , age int8 ) *person { return &person{ name: name, city: city, age: age, } }func main () { p9 := newPerson("张三" , "沙河" , 90 ) fmt.Printf("%#v\n" , p9) }
方法和接收者 Go语言中的方法(Method)是一种作用于特定类型变量的函数 。这种特定类型变量叫做接收者(Receiver)
接收者变量:接收者中的参数变量名在命名时,官方建议使用接收者类型名称首字母的小写,而不是self、this之类的命名。例如,Person
类型的接收者变量应该命名为 p
,Connector
类型的接收者变量应该命名为c
等。
接收者类型:接收者类型和参数类似,可以是指针类型和非指针类型。
方法名、参数列表、返回参数:具体格式与函数定义相同。
方法与函数的区别是:函数不属于任何类型,方法属于特定的类型 。
1 2 3 func (接收者变量 接收者类型) 方法名(参数列表) (返回参数) { 函数体 }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 type Person struct { name string age int8 }func NewPerson (name string , age int8 ) *Person { return &Person{ name: name, age: age, } }func (p Person) Dream() { fmt.Printf("%s的梦想是学好Go语言!\n" , p.name) }func main () { p1 := NewPerson("小王子" , 25 ) p1.Dream() }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 func (p *Person) SetAge(newAge int8 ) { p.age = newAge }func main () { p1 := NewPerson("小王子" , 25 ) fmt.Println(p1.age) p1.SetAge(30 ) fmt.Println(p1.age) }
嵌套结构体 一个结构体中可以嵌套包含另一个结构体或结构体指针
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 type Address struct { Province string City string CreateTime string }type User struct { Name string Gender string Address Address }type Email struct { Account string CreateTime string }type User2 struct { Name string Gender string Address Email }func main () { user1 := User{ Name: "小王子" , Gender: "男" , Address: Address{ Province: "山东" , City: "威海" , }, } fmt.Printf("user1=%#v\n" , user1) var user2 User2 user2.Name = "小王子" user2.Gender = "男" user2.Address.Province = "山东" user2.City = "威海" fmt.Printf("user2=%#v\n" , user2) var user3 User2 user3.Name = "沙河娜扎" user3.Gender = "男" user3.Address.CreateTime = "2000" user3.Email.CreateTime = "2000" }
结构体中的继承 Go语言中使用结构体也可以实现其他编程语言中面向对象的继承
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 type Animal struct { name string }func (a *Animal) move() { fmt.Printf("%s会动!\n" , a.name) }type Dog struct { Feet int8 *Animal }func (d *Dog) wang() { fmt.Printf("%s会汪汪汪~\n" , d.name) }func main () { d1 := &Dog{ Feet: 4 , Animal: &Animal{ name: "乐乐" , }, } d1.wang() d1.move() }
结构体与JSON序列化 Tag是结构体的元信息
,可以在运行的时候通过反射的机制读取出来。 Tag在结构体字段的后方定义,由一对反引号包裹起来
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 import ( "encoding/json" "fmt" )type person struct { Name string `json:"name" db:"name"` Age int `json:"age"` }func main () { p := person{ Name: "小祥" , Age: 20 , } b, err := json.Marshal(p) if err != nil { fmt.Println("转换错误!" ) return } fmt.Println(string (b)) str := `{"name":"小祥","age":20,"a":1}` var p2 person _ = json.Unmarshal([]byte (str), &p2) fmt.Println(p2) }
接口 接口是一种由程序员来定义的类型,一个接口类型就是一组方法的集合,它规定了需要实现的所有方法
1 2 3 4 5 type 接口类型名 interface { 方法名1 ( 参数列表1 ) 返回值列表1 方法名2 ( 参数列表2 ) 返回值列表2 … }
接口类型名:Go语言的接口在命名时,一般会在单词后面添加er
,如有写操作的接口叫Writer,有关闭操作的接口叫closer等。接口名最好要能突出该接口的类型含义。
方法名:当方法名首字母是大写且这个接口类型名首字母也是大写
时,这个方法可以被接口所在的包(package)之外的代码访问。
参数列表、返回值列表:参数列表和返回值列表中的参数变量名可以省略
实现接口的条件 接口就是规定了一个需要实现的方法列表,在 Go 语言中一个类型只要实现了接口中规定的所有方法,那么我们就称它实现了这个接口。
1 2 3 4 5 6 7 8 9 10 11 type Singer interface { Sing() }type Bird struct {}func (b Bird) Sing() { fmt.Println("汪汪汪" ) }
接口类型变量 1 2 3 4 5 6 7 var x Sayer a := Cat{} b := Dog{} x = a x.Say() x = b x.Say()
值接收者和指针接收者 使用值接收者实现接口之后,不管是结构体类型还是对应的结构体指针类型的变量都可以赋值给该接口变量 。
1 2 3 4 type Mover interface { Move() }
值接收者实现接口 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 type Dog struct {}func (d Dog) Move() { fmt.Println("狗会动" ) }func main () { var x Mover var d1 = Dog{} x = d1 x.Move() var d2 = &Dog{} x = d2 x.Move() }
指针接收者实现接口 由于Go语言中有对指针求值的语法糖,对于值接收者实现的接口,无论使用值类型还是指针类型都没有问题。 但是我们并不总是能对一个值求址,所以对于指针接收者实现的接口要额外注意
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 type Cat struct {}func (c *Cat) Move() { fmt.Println("猫会动" ) }func main () { var c1 = &Cat{} x = c1 x.Move() var c2 = Cat{} x = c2 }
类型与接口的关系 一个类型实现多个接口 一个类型可以同时实现多个接口,而接口间彼此独立
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 type Sayer interface { Say() }type Mover interface { Move() }type Dog struct { Name string }func (d Dog) Say() { fmt.Printf("%s会叫汪汪汪\n" , d.Name) }func (d Dog) Move() { fmt.Printf("%s会动\n" , d.Name) }func main () { var d = Dog{Name: "旺财" } var s Sayer = d var m Mover = d s.Say() m.Move() }
多种类型实现同一接口 Go语言中不同的类型还可以实现同一接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 type Mover interface { Move() }type Dog struct { Name string }func (d Dog) Move() { fmt.Printf("%s会动\n" , d.Name) }type Car struct { Brand string }func (c Car) Move() { fmt.Printf("%s速度70迈\n" , c.Brand) }func main () { var obj Mover obj = Dog{Name: "旺财" } obj.Move() obj = Car{Brand: "宝马" } obj.Move() }
一个接口的所有方法,不一定需要由一个类型完全实现,接口的方法可以通过在类型中嵌入其他类型或者结构体来实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 type WashingMachine interface { wash() dry() }type dryer struct {}func (d dryer) dry() { fmt.Println("甩一甩" ) }type haier struct { dryer }func (h haier) wash() { fmt.Println("洗刷刷" ) }
接口组合 接口与接口之间可以通过互相嵌套形成新的接口类型,例如Go标准库io源码中就有很多接口之间互相组合的示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 type Reader interface { Read(p []byte ) (n int , err error ) }type Writer interface { Write(p []byte ) (n int , err error ) }type Closer interface { Close() error }type ReadWriter interface { Reader Writer }type ReadCloser interface { Reader Closer }type WriteCloser interface { Writer Closer }
空接口 空接口是指没有定义任何方法的接口类型。因此任何类型都可以视为实现了空接口。也正是因为空接口类型的这个特性,空接口类型的变量可以存储任意类型的值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package mainimport "fmt" type Any interface {}type Dog struct {}func main () { var x Any x = "你好" fmt.Printf("type:%T value:%v\n" , x, x) x = 100 fmt.Printf("type:%T value:%v\n" , x, x) x = true fmt.Printf("type:%T value:%v\n" , x, x) x = Dog{} fmt.Printf("type:%T value:%v\n" , x, x) }
通常我们在使用空接口类型时不必使用type
关键字声明,可以像下面的代码一样直接使用interface{}
应用 使用空接口实现可以接收任意类型的函数参数
1 2 3 4 func show (a interface {}) { fmt.Printf("type:%T value:%v\n" , a, a) }
使用空接口实现可以保存任意值的字典
1 2 3 4 5 6 7 8 9 func main () { var studentInfo = make (map [string ]interface {}) studentInfo["name" ] = "沙河娜扎" studentInfo["age" ] = 18 studentInfo["married" ] = false fmt.Println(studentInfo) }
类型断言 想要从接口值中获取到对应的实际值需要使用类型断言,其语法格式如下
x:表示接口类型的变量
T:表示断言x可能是的类型。
1 2 3 4 5 6 7 8 var n Mover = &Dog{Name: "旺财" } v, ok := n.(*Dog)if ok { fmt.Println("类型断言成功" ) v.Name = "富贵" } else { fmt.Println("类型断言失败" ) }
如果对一个接口值有多个实际类型需要判断,推荐使用switch语句来实现
1 2 3 4 5 6 7 8 9 10 11 12 13 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!" ) } }
包 Go语言中支持模块化的开发理念,在Go语言中使用包(package)来支持代码模块化和代码复用。一个包是由一个或多个Go源码文件(.go结尾的文件)组成 ,是一种高级的代码复用方案,Go语言为我们提供了很多内置包,如fmt、os、io等。
标识符可见性 如果想让一个包中的标识符(如变量、常量、类型、函数等)能被外部的包使用,那么标识符必须是对外可见的(public)。 在Go语言中是通过标识符的首字母大/小写来控制标识符的对外可见 (public)/不可见(private)的。在一个包内部只有首字母大写的标识符才是对外可见 的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 package demoimport "fmt" type Student struct { Name string class string }var num = 100 const Mode = 1 type person struct { name string Age int }func Add (x, y int ) int { return x + y }func sayHi () { var myName = "七米" fmt.Println(myName) }
包的引入 要在当前包中使用另外一个包的内容就需要使用import关键字引入这个包,并且import语句通常放在文件的开头,package声明语句的下方
1 import importname "path/to/package"
importname:引入的包名,通常都省略
。默认值为引入包的包名。
path/to/package:引入包的路径名称,必须使用双引号包裹起来
。
Go语言中禁止循环导入包。
1 2 3 4 5 6 7 8 9 10 import "fmt" import "net/http" import "os" import ( "fmt" "net/http" "os" )
如果引入一个包的时候为其设置了一个特殊_作为包名,那么这个包的引入方式就称为匿名引入
。 一个包被匿名引入的目的主要是为了加载这个包,从而使得这个包中的资源得以初始化。 被匿名引入的包中的init函数 将被执行并且仅执行一遍。
1 import _ "github.com/go-sql-driver/mysql"
init初始化函数 在每一个Go源文件中,都可以定义任意个如下格式的特殊函数 这种特殊的函数不接收任何参数也没有任何返回值
,我们也不能在代码中主动调用
它。 当程序启动的时候,init函数会按照它们声明的顺序自动执行