go语言入门基础详谈(语法篇)

文章目录

  • GO语言学习笔记详谈
  • GO基础相关学习:
    • 1.Go语言基础之变量常量
      • 1.Go语言的变量声明格式为:
      • 2.Go语言的常量声明格式为:
    • 2.Go语言基础之基本数据类型
        • 1.1整型
        • 1.2.浮点型
        • 1.3复数
        • 1.4布尔值
        • 1.5字符串
        • 1.6字符串的常用操作
        • 1.7byte和rune类型
        • 1.8修改字符串
        • 1.9类型转换
    • 3.Go语言基础之流程控制
        • 一.if else(分支语句)
        • 二.for(循环结构)
          • 1.for循环
          • 2.忽略for循环的初始语句
          • 3.省略初始语句和结束语句
          • 4无限循环
          • 5.for range(键值循环)
        • 三.switch case
          • 2.Type Switch
          • 3.select 语句
        • 四.goto,break,continue
          • 1.goto(跳转到指定标签)
          • 2.break(跳出循环)
          • 3.continue(继续下次循环)
    • 4.Go语言基础之数组(重点)
          • 1.数组的定义
          • 2.数组的初始化
          • 3.数组的遍历
          • 4.多维数组
          • 5.数组是值类型(重要‼️)
    • 5.Go语言基础之切片(重要‼️)
          • 1切片的定义
          • 2.切片的长度和容量
          • 3.切片表达式
            • 3.1简单切片表达式
            • 3.2完整切片表达式
            • 3.3使用make()函数构造切片
          • 4.切片的本质
      • 判断切片是否为空
          • 5.切片不能直接比较
          • 6.切片的赋值拷贝
          • 7.切片遍历
          • 8.append()方法为切片添加元素
          • 9.切片的扩容策略
          • 10.使用copy()函数复制切片
          • 11.从切片中删除元素
          • 12.字符串和切片(string and slice)
    • 6.Go语言基础之指针
        • 1.Go语言中的指针
        • 2.指针地址和指针类型
          • 2.1指针取值
          • 2.2空指针
        • 3.new和make
          • 3.1.new
          • 3.2.make
          • 3.3new与make的区别
    • 7.Go语言基础之map
        • 1.map定义:
        • 2.map基本使用
        • 3.判断某一个键是否存在
        • 4.map的遍历
        • 5.使用delete()函数删除键值对
        • 6.按照指定的顺序遍历map
        • 7.元素为map类型的切片
        • 8.值为切片类型的map
    • 8.Go语言基础之函数
      • 一.函数
        • 1.函数的定义
        • 2.函数的调用
        • 3.参数
          • 3.1类型简写
          • 3.2可变参数
        • 4.返回值
          • 4.1多返回值
          • 4.2返回值命名
          • 4.3返回值补充
      • 二.函数进阶
        • 1.变量作用域
        • 2.局部变量
        • 3.函数类型与变量
          • 3.1定义函数类型
          • 3.2函数类型变量
        • 4.高阶函数
          • 1.函数作为参数
          • 2.函数作为返回值
      • 三.匿名函数和闭包
        • 1.匿名函数
        • 2.闭包
      • 四.defer语句
          • 1.defer执行时机
          • 2.defer经典案例
      • 五.内置函数介绍
    • 9.Go语言基础之结构体
      • 一.类型别名和自定义类型
        • 1.类型别名
        • 2.自定义类型
        • 3.类型定义和类型别名的区别
      • 二.结构体
        • 1.结构体的定义
        • 2.结构体实例化
          • 2.1基本实例化
          • 2.2匿名结构体
          • 2.3创建指针类型结构体
        • 3.结构体初始化
          • 3.1使用键值对初始化
          • 3.2使用值的列表初始化
        • 4.结构体内存布局
        • 5.构造函数
        • 6.方法和接收者
          • 6.1指针类型的接收者
          • 6.2值类型的接受者
          • 6.3什么时候使用指针类型接受者
        • 7.结构体的匿名字段
        • 8.嵌套结构体
          • 8.1嵌套匿名字段
          • 8.2嵌套结构体的字段名冲突
          • 8.3结构体的"继承"
        • 9.结构体字段的可见性
        • 10.结构体标签(Tag)
        • 11.结构体与JSON序列化
        • 12.结构体使用map和slice时注意事项

引言:本文综合了几个比较主流的go语言博客,取长补短,较为详细的总结了变量常量,基本数据类型,流程控制,数组,切片,指针,map,函数方法,结构体等语言基础内容,在总结的过程中,我也更加熟练的使用go语言语法并粗浅的了解其原理。后面的章节内容需要更加深入的了解方能做好归纳总结并加以自己的观点,后续会做好总结,就先发前面的基础

GO语言学习笔记详谈

GO基础相关学习:

Go语言基础–Printf格式化输出、Scanf格式化输入详解:https://blog.csdn.net/qq_34777600/article/details/81266453

1.Go语言基础之变量常量

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

var 变量名 变量类型
var 变量名 类型 = 表达式
  • 整型和浮点型变量的默认值为0。 字符串变量的默认值为空字符串。 布尔型变量默认为false。 切片、函数、指针变量的默认为nil

  • 匿名变量用一个下划线_表示,_多用于占位,表示忽略值。

  • :=不能使用在函数外。

2.Go语言的常量声明格式为:

const pi = 3.1415
const e = 2.7182
//多个常量也可以一起声明。
const (
    pi = 3.1415
    e = 2.7182
)
//const同时声明多个常量时,如果省略了值则表示和上面一行的值相同。
const (
    n1 = 100
    n2
    n3
)
  • iota是go语言的常量计数器,只能在常量的表达式中使用。
  • iota在const关键字出现时将被重置为0。const中每新增一行常量声明将使iota计数一次(iota可理解为const语句块中的行索引)。 使用iota能简化定义,在定义枚举时很有用。

2.Go语言基础之基本数据类型

Go语言中有丰富的数据类型,除了基本的整型、浮点型、布尔型、字符串外,还有数组、切片、结构体、函数、map、通道(channel)等。

  • 值类型
		bool
    int(32 or 64), int8, int16, int32, int64
    uint(32 or 64), uint8(byte), uint16, uint32, uint64
    float32, float64
    string
    complex64, complex128
    array    -- 固定长度的数组
  • 引用类型(指针类型)
		slice   -- 序列数组(最常用)
	  map     -- 映射
    chan    -- 管道

1.1整型

  • 整型分为以下两个大类: 按长度分为:int8、int16、int32、int64 对应的无符号整型:uint8、uint16、uint32、uint64

  • uint8就是我们熟知的byte型,int16对应C语言中的short型,int64对应C语言中的long型。

1.2.浮点型

  • Go语言支持两种浮点型数:float32float64。这两种浮点型数据格式遵循IEEE 754标准: float32 的浮点数的最大范围约为3.4e38,可以使用常量定义:math.MaxFloat32float64 的浮点数的最大范围约为 1.8e308,可以使用一个常量定义:math.MaxFloat64

1.3复数

  • complex64complex128
  • 复数有实部和虚部,complex64的实部和虚部为32位,complex128的实部和虚部为64位

1.4布尔值

  • Go语言中以bool类型进行声明布尔型数据,布尔型数据只有true(真)false(假)两个值。
    • 注意:
      • 布尔类型变量的默认值为false。
      • Go 语言中不允许将整型强制转换为布尔型.
      • 布尔型无法参与数值运算,也无法与其他类型进行转换。

1.5字符串

  • Go语言中的字符串以原生数据类型出现,使用字符串就像使用其他原生数据类型(int、bool、float32、float64 等)一样。 Go 语言里的字符串的内部实现使用UTF-8编码。 字符串的值为双引号(")中的内容,可以在Go语言的源码中直接添加非ASCII码字符

1.6字符串的常用操作

  • len(str) 求长度
  • +或fmt.Sprintf 拼接字符串
  • strings.Split 分割
  • strings.contains 判断是否包含
  • strings.HasPrefix,strings.HasSuffix 前缀/后缀判断
  • strings.Index(),strings.LastIndex() 子串出现的位置
  • strings.Join(a[]string, sep string) join操作

1.7byte和rune类型

  • 组成每个字符串的元素叫做“字符”,可以通过遍历或者单个获取字符串元素获得字符。 字符用单引号(’)包裹起来。

  • var b = 'x'
    
  • Go 语言的字符有以下两种:

    • uint8类型,或者叫 byte 型,代表了ASCII码的一个字符。
    • rune类型,代表一个 UTF-8字符
  • 注意:当需要处理中文、日文或者其他复合字符时,则需要用到rune类型。rune类型实际是一个int32。Go 使用了特殊的 rune 类型来处理 Unicode,让基于 Unicode 的文本处理更为方便,也可以使用 byte 型进行默认字符串处理,性能和扩展性都有照顾。

// 遍历字符串
func traversalString() {
	s := "hello沙河"
	for i := 0; i < len(s); i++ { //byte
		fmt.Printf("%v(%c) ", s[i], s[i])
	}
	fmt.Println()
	for _, r := range s { //rune
		fmt.Printf("%v(%c) ", r, r)
	}
	fmt.Println()
}

1.8修改字符串

  • 要修改字符串,需要先将其转换成[]rune[]byte,完成后再转换为string。无论哪种转换,都会重新分配内存,并复制字节数组。
func changeString() {
	s1 := "big"
	// 强制类型转换
	byteS1 := []byte(s1)
	byteS1[0] = 'p'
	fmt.Println(string(byteS1))

	s2 := "白萝卜"
	runeS2 := []rune(s2)
	runeS2[0] = '红'
	fmt.Println(string(runeS2))
}

1.9类型转换

  • Go语言中只有强制类型转换,没有隐式类型转换。该语法只能在两个类型之间支持相互转换的时候使用。
//举例:
func sqrtDemo() {
	var a, b = 3, 4
	var c int
	// math.Sqrt()接收的参数是float64类型,需要强制转换
	c = int(math.Sqrt(float64(a*a + b*b)))
	fmt.Println(c)
}

3.Go语言基础之流程控制

一.if else(分支语句)

1.if条件判断基本写法

if 表达式1 {           //判断语句没有括号
    分支1
} else if 表达式2 {
    分支2
} else{
    分支3
}
例子:
score := 65
	if score >= 90 {
		fmt.Println("A")
	} else if score > 75 {
		fmt.Println("B")
	} else {
		fmt.Println("C")
	}

2.if条件判断还有一种特殊的写法,可以在 if 表达式之前添加一个执行语句,再根据变量值进行判断。

func ifDemo2() {
	if score := 65; score >= 90 {
		fmt.Println("A")
	} else if score > 75 {
		fmt.Println("B")
	} else {
		fmt.Println("C")
	}
}

二.for(循环结构)

  • Go 语言中的所有循环类型均可以使用for关键字来完成。
//for循环的基本格式如下:
for 初始语句;条件表达式;结束语句{
    循环体语句
}
1.for循环

条件表达式返回true时循环体不停地进行循环,直到条件表达式返回false时自动退出循环。

func forDemo() {
	for i := 0; i < 10; i++ {
		fmt.Println(i)
	}
}
2.忽略for循环的初始语句

for循环的初始语句可以被忽略,但是初始语句后的分号必须要写

func forDemo2() {
	i := 0
	for ; i < 10; i++ {
		fmt.Println(i)
	}
}
3.省略初始语句和结束语句
s := "abc"
n := len(s)
for n > 0 {                // 替代 while (n > 0) {}
    println(s[n])        // 替代 for (; n > 0;) {}
    n-- 
}
//这种写法类似于其他编程语言中的while,在while后添加一个条件表达式,满足条件表达式时持续循环,否则结束循环。
4无限循环
for {
    循环体语句
}
  • 注意:for循环可以通过breakgotoreturnpanic语句强制退出循环。
5.for range(键值循环)
  • Go语言中可以使用for range遍历数组、切片、字符串、map 及通道(channel)。
    • 数组、切片、字符串返回索引和值。
    • map返回键和值。
    • 通道(channel)只返回通道内的值。
    • *注意,range 会复制对象。建议改用引用类型(切片),其底层数据不会被复制。另外两种引用类型 map、channel 是指针包装,而不像 slice 是 struct。

三.switch case

1.使用switch语句可方便地对大量的值进行条件判断。

func switchDemo1() {
	finger := 3
	switch finger {
	case 1:
		fmt.Println("大拇指")
	case 2:
		fmt.Println("食指")
	default:
		fmt.Println("无效的输入!")
	}
}

//Go语言规定每个switch只能有一个default分支。

1.1一个分支可以有多个值,多个case值中间使用英文逗号分隔。

switch n := 7; n {
	case 1, 3, 5, 7, 9:
		fmt.Println("奇数")
	case 2, 4, 6, 8:
		fmt.Println("偶数")
	default:
		fmt.Println(n)
	}

1.2分支还可以使用表达式,这时候switch语句后面不需要再跟判断变量。

case age < 25:
		fmt.Println("好好学习吧")

1.3fallthrough语法可以执行满足条件的case的下一个case。

2.Type Switch
  • switch 语句还可以被用于 type-switch 来判断某个 interface 变量中实际存储的变量类型。
switch x.(type){
    case type:
       statement(s)      
    case type:
       statement(s)
    /* 你可以定义任意个数的case */
    default: /* 可选 */
       statement(s)
}
//实例:
 var x interface{}
    //写法一:
    switch i := x.(type) { // 带初始化语句
    case nil:
        fmt.Printf(" x 的类型 :%T\r\n", 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 语句类似于 switch 语句,但是select会随机执行一个可运行的case。如果没有case可运行,它将阻塞,直到有case可运行。select 是Go中的一个控制结构,类似于用于通信的switch语句。每个case必须是一个通信操作,要么是发送要么是接收。 select 随机执行一个可运行的case。如果没有case可运行,它将阻塞,直到有case可运行。一个默认的子句应该总是可运行的。

1.Go 编程语言中 select 语句的语法如下:

select {
    case communication clause  :
       statement(s);      
    case communication clause  :
       statement(s);
    /* 你可以定义任意数量的 case */
    default : /* 可选 */
       statement(s);
}
		每个case都必须是一个通信
    所有channel表达式都会被求值
    所有被发送的表达式都会被求值
    如果任意某个通信可以进行,它就执行;其他被忽略。
    如果有多个case都可以运行,Select会随机公平地选出一个执行。其他不会执行。
  否则:
    如果有default子句,则执行该语句。
    如果没有default字句,select将阻塞,直到某个通信可以运行;Go不会重新对channel或值进行求值。

实例:
	 var c1, c2, c3 chan int
   var i1, i2 int
   select {
      case i1 = <-c1:
         fmt.Printf("received ", i1, " from c1\n")
      case c2 <- i2:
         fmt.Printf("sent ", i2, " to c2\n")
      case i3, ok := (<-c3):  // same as: i3, ok := <-c3
         if ok {
            fmt.Printf("received ", i3, " from c3\n")
         } else {
            fmt.Printf("c3 is closed\n")
         }
      default:
         fmt.Printf("no communication\n")
   }    

结果: no communication

具体使用:https://www.topgoer.com/%E6%B5%81%E7%A8%8B%E6%8E%A7%E5%88%B6/%E6%9D%A1%E4%BB%B6%E8%AF%AD%E5%8F%A5select.html

四.goto,break,continue

1.goto(跳转到指定标签)
  • goto语句通过标签进行代码间的无条件跳转。goto语句可以在快速跳出循环、避免重复退出上有一定的帮助。Go语言中使用goto语句能简化一些代码的实现过程。 例如双层嵌套的for循环要退出时:
func gotoDemo2() {
	for i := 0; i < 10; i++ {
		for j := 0; j < 10; j++ {
			if j == 2 {
				// 设置退出标签
				goto breakTag
			}
			fmt.Printf("%v-%v\n", i, j)
		}
	}
	return
	// 标签
breakTag:
	fmt.Println("结束for循环")
}
2.break(跳出循环)
  • break语句可以结束forswitchselect的代码块。
  • break语句还可以在语句后面添加标签,表示退出某个标签对应的代码块,标签要求必须定义在对应的forswitchselect的代码块上
func breakDemo1() {
BREAKDEMO1:
	for i := 0; i < 10; i++ {
		for j := 0; j < 10; j++ {
			if j == 2 {
				break BREAKDEMO1
			}
			fmt.Printf("%v-%v\n", i, j)
		}
	}
	fmt.Println("...")
}
3.continue(继续下次循环)
  • continue语句可以结束当前循环,开始下一次的循环迭代过程,仅限在for循环内使用。在 continue语句后添加标签时,表示开始标签对应的循环。

4.Go语言基础之数组(重点)

引言:

  • 数组是同一种数据类型元素的集合。 在Go语言中,数组从声明时就确定,使用时可以修改数组成员,但是数组大小不可变化。
// 定义一个长度为3元素类型为int的数组a
var a [3]int
1.数组的定义
var 数组变量名 [元素数量]T

注:比如:var a [5]int, 数组的长度必须是常量,并且长度是数组类型的一部分。一旦定义,长度不能变。 [5]int[10]int是不同的类型。数组可以通过下标进行访问,下标是从0开始,最后一个元素下标是:len-1,访问越界(下标在合法范围之外),则触发访问越界,会panic。

2.数组的初始化

2.方法一:

  • 初始化数组时可以使用初始化列表来设置数组元素的值。
	var testArray [3]int                               //数组会初始化为int类型的零值
	var numArray = [3]int{1, 2}                        //使用指定的初始值完成初始化
	var cityArray = [3]string{"北京", "上海", "深圳"}    //使用指定的初始值完成初始化

2.2方法二:

  • 按照上面的方法每次都要确保提供的初始值和数组长度一致,一般情况下我们可以让编译器根据初始值的个数自行推断数组的长度,
	var testArray [3]int
	var numArray = [...]int{1, 2}
	var cityArray = [...]string{"北京", "上海", "深圳"}

2.3方法三:

  • 我们还可以使用指定索引值的方式来初始化数组,
3.数组的遍历
  • 遍历数组有以下两种方法:
func main() {
	var a = [...]string{"北京", "上海", "深圳"}
	// 方法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)
	}
}
4.多维数组
  • Go语言是支持多维数组的,我们这里以二维数组为例(数组中又嵌套数组)。

4.1二维数组的定义

func main() {
	a := [3][2]string{
		{"北京", "上海"},
		{"广州", "深圳"},
		{"成都", "重庆"},
	}
  //遍历这个二维数组
  for _, v1 := range a {
		for _, v2 := range v1 {
			fmt.Printf("%s\t", v2)
		}
		fmt.Println()
	}
}

4.2多维数组只有第一层可以使用...来让编译器推导数组长度。

5.数组是值类型(重要‼️)
  • 数组是值类型,赋值和传参会复制整个数组。因此改变副本的值,不会改变本身的值
  • 注意:
    • 数组支持 “==“、”!=” 操作符,因为内存总是被初始化过的。
    • [n]*T表示指针数组,*[n]T表示数组指针 。

5.Go语言基础之切片(重要‼️)

  • 切片:切片(Slice)是一个拥有相同类型元素的可变长度的序列。它是基于数组类型做的一层封装。它非常灵活,支持自动扩容。切片是一个引用类型,它的内部结构包含地址长度容量。切片一般用于快速地操作一块数据集合。
1切片的定义
var name []T
//name:表示变量名
//T:表示切片中的元素类型

例子:
// 声明切片类型
	var a []string              //声明一个字符串切片
	var b = []int{}             //声明一个整型切片并初始化
	var c = []bool{false, true} //声明一个布尔切片并初始化

注意:切片是引用类型,不支持直接比较,只能和nil比较

2.切片的长度和容量
  • 切片拥有自己的长度和容量,我们可以通过使用内置的len()函数求长度,使用内置的cap()函数求切片的容量。
3.切片表达式
  • 切片表达式从字符串、数组、指向数组或切片的指针构造子字符串或切片。它有两种变体:一种指定low和high两个索引界限值的简单的形式,另一种是除了low和high索引界限值外还指定容量的完整的形式。
3.1简单切片表达式
  • 切片的底层就是一个数组,所以我们可以基于数组通过切片表达式得到切片。 切片表达式中的lowhigh表示一个索引范围(左包含,右不包含),也就是下面代码中从数组a中选出1<=索引值<3的元素组成切片s,得到的切片长度=high-low,容量等于得到的切片的底层数组的容量。
func main() {
	a := [5]int{1, 2, 3, 4, 5}
	s := a[1:3]  // s := a[low:high]
	fmt.Printf("s:%v len(s):%v cap(s):%v\n", s, len(s), cap(s))
}
//结果:s:[2 3] len(s):2 cap(s):4
  • 为了方便起见,可以省略切片表达式中的任何索引。省略了low则默认为0;省略了high则默认为切片操作数的长度:
a[2:]  // 等同于 a[2:len(a)]
a[:3]  // 等同于 a[0:3]
a[:]   // 等同于 a[0:len(a)]

**注意:**对于数组或字符串,如果0 <= low <= high <= len(a),则索引合法,否则就会索引越界(out of range)。

  • 对切片再执行切片表达式时(切片再切片),high的上限边界是切片的容量cap(a),而不是长度。常量索引必须是非负的,并且可以用int类型的值表示;对于数组或常量字符串,常量索引也必须在有效范围内。如果lowhigh两个指标都是常数,它们必须满足low <= high。如果索引在运行时超出范围,就会发生运行时panic
func main() {
	a := [5]int{1, 2, 3, 4, 5}
	s := a[1:3]  // s := a[low:high]
	fmt.Printf("s:%v len(s):%v cap(s):%v\n", s, len(s), cap(s))
	s2 := s[3:4]  // 索引的上限是cap(s)而不是len(s)
	fmt.Printf("s2:%v len(s2):%v cap(s2):%v\n", s2, len(s2), cap(s2))
}
//结果
s:[2 3] len(s):2 cap(s):4
s2:[5] len(s2):1 cap(s2):1
3.2完整切片表达式
  • 对于数组,指向数组的指针,或切片a(注意不能是字符串)支持完整切片表达式:
a[low : high : max]

上面的代码会构造与简单切片表达式a[low: high]相同类型、相同长度和元素的切片。另外,它会将得到的结果切片的容量设置为max-low。在完整切片表达式中只有第一个索引值(low)可以省略;它默认为0。

func main() {
	a := [5]int{1, 2, 3, 4, 5}
	t := a[1:3:5]
	fmt.Printf("t:%v len(t):%v cap(t):%v\n", t, len(t), cap(t))
}
//结果
t:[2 3] len(t):2 cap(t):4

完整切片表达式需要满足的条件是0 <= low <= high <= max <= cap(a),其他条件和简单切片表达式相同。

3.3使用make()函数构造切片
  • 我们上面都是基于数组来创建的切片,如果需要动态的创建一个切片,我们就需要使用内置的make()函数
make([]T, size, cap)

T:切片的元素类型
size:切片中元素的数量
cap:切片的容量

例子:
func main() {
	a := make([]int, 2, 10)
	fmt.Println(a)      //[0 0]
	fmt.Println(len(a)) //2
	fmt.Println(cap(a)) //10
}

上面代码中a的内部存储空间已经分配了10个,但实际上只用了2个。 容量并不会影响当前元素的个数,所以len(a)返回2,cap(a)则返回该切片的容量。

4.切片的本质
  • 切片的本质就是对底层数组的封装,它包含了三个信息:底层数组的指针、切片的长度(len)和切片的容量(cap)

​ 举个例子,现在有一个数组a := [8]int{0, 1, 2, 3, 4, 5, 6, 7},切片s1 := a[:5],相应示意图如下。

go语言入门基础详谈(语法篇)_第1张图片

切片s2 := a[3:6],相应示意图如下:

go语言入门基础详谈(语法篇)_第2张图片

  • 判断切片是否为空

  • 要检查切片是否为空,请始终使用len(s) == 0来判断,而不应该使用s == nil来判断。

5.切片不能直接比较
  • 切片之间是不能比较的,我们不能使用==操作符来判断两个切片是否含有全部相等元素。 切片唯一合法的比较操作是和nil比较。 一个nil值的切片并没有底层数组,一个nil值的切片的长度和容量都是0。但是我们不能说一个长度和容量都是0的切片一定是nil,例如下面的示例:
var s1 []int         //len(s1)=0;cap(s1)=0;s1==nil
s2 := []int{}        //len(s2)=0;cap(s2)=0;s2!=nil
s3 := make([]int, 0) //len(s3)=0;cap(s3)=0;s3!=nil

注意:所以要判断一个切片是否是空的,要是用len(s) == 0来判断,不应该使用s == nil来判断。

6.切片的赋值拷贝
  • 下面的代码中演示了拷贝前后两个变量**共享底层数组**,对一个切片的修改会影响另一个切片的内容,这点需要特别注意。
func main() {
	s1 := make([]int, 3) //[0 0 0]
	s2 := s1             //将s1直接赋值给s2,s1和s2共用一个底层数组
	s2[0] = 100
	fmt.Println(s1) //[100 0 0]
	fmt.Println(s2) //[100 0 0]
}
7.切片遍历
  • 切片的遍历方式和数组是一致的,支持索引遍历和for range遍历。
func main() {
	s := []int{1, 3, 5}
	//索引遍历
	for i := 0; i < len(s); i++ {
		fmt.Println(i, s[i])
	}
	//for range遍历
	for index, value := range s {
		fmt.Println(index, value)
	}
}
8.append()方法为切片添加元素
  • Go语言的内建函数append()可以为切片动态添加元素。 可以一次添加一个元素,可以添加多个元素,也可以添加另一个切片中的元素(后面加…)。
func main(){
	var s []int
	s = append(s, 1)        // [1]
	s = append(s, 2, 3, 4)  // [1 2 3 4]
	s2 := []int{5, 6, 7}  
	s = append(s, s2...)    // [1 2 3 4 5 6 7]
}
注意:通过var声明的零值切片可以在append()函数直接使用,无需初始化。
var s []int
s = append(s, 1, 2, 3)
  • 每个切片会指向一个底层数组,这个数组的容量够用就添加新增元素。当底层数组不能容纳新增的元素时,切片就会自动按照一定的策略进行“扩容”,此时该切片指向的底层数组就会更换。“扩容”操作往往发生在append()函数调用时,所以我们通常都需要用原变量接收append函数的返回值。
func main() {
	//append()添加元素和切片扩容
	var numSlice []int
	for i := 0; i < 10; i++ {
		numSlice = append(numSlice, i)
		fmt.Printf("%v  len:%d  cap:%d  ptr:%p\n", numSlice, len(numSlice), 		cap(numSlice), numSlice)
	}
}
//结果
[0]  len:1  cap:1  ptr:0xc0000a8000
[0 1]  len:2  cap:2  ptr:0xc0000a8040
[0 1 2]  len:3  cap:4  ptr:0xc0000b2020
[0 1 2 3]  len:4  cap:4  ptr:0xc0000b2020
[0 1 2 3 4]  len:5  cap:8  ptr:0xc0000b6000
[0 1 2 3 4 5]  len:6  cap:8  ptr:0xc0000b6000
[0 1 2 3 4 5 6]  len:7  cap:8  ptr:0xc0000b6000
[0 1 2 3 4 5 6 7]  len:8  cap:8  ptr:0xc0000b6000
[0 1 2 3 4 5 6 7 8]  len:9  cap:16  ptr:0xc0000b8000
[0 1 2 3 4 5 6 7 8 9]  len:10  cap:16  ptr:0xc0000b8000

从上面的结果可以看出:

  1. append()函数将元素追加到切片的最后并返回该切片。

  2. 切片numSlice的容量按照1,2,4,8,16这样的规则自动进行扩容,每次扩容后都是扩容前的2倍。

  3. append()函数还支持一次性追加多个元素。

9.切片的扩容策略
  • 可以通过查看$GOROOT/src/runtime/slice.go源码,其中扩容相关代码如下:
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
	newcap = cap
} else {
	if old.len < 1024 {
		newcap = doublecap
	} else {
		// Check 0 < newcap to detect overflow
		// and prevent an infinite loop.
		for 0 < newcap && newcap < cap {
			newcap += newcap / 4
		}
		// Set newcap to the requested cap when
		// the newcap calculation overflowed.
		if newcap <= 0 {
			newcap = cap
		}
	}
}

1.首先判断,如果新申请容量(cap)大于2倍的旧容量(old.cap),最终容量(newcap)就是新申请的容量(cap)。

2.否则判断,如果旧切片的长度小于1024,则最终容量(newcap)就是旧容量(old.cap)的两倍,即(newcap=doublecap),

3.否则判断,如果旧切片长度大于等于1024,则最终容量(newcap)从旧容量(old.cap)开始循环增加原来的1/4,即(newcap=old.cap,for {newcap += newcap/4})直到最终容量(newcap)大于等于新申请的容量(cap),即(newcap >= cap4.如果最终容量(cap)计算值溢出,则最终容量(cap)就是新申请容量(cap)。

需要注意的是,切片扩容还会根据切片中元素的类型不同而做不同的处理,比如intstring类型的处理方式就不一样。
10.使用copy()函数复制切片
  • 首先我们来看一个问题:
func main() {
	a := []int{1, 2, 3, 4, 5}
	b := a
	fmt.Println(a) //[1 2 3 4 5]
	fmt.Println(b) //[1 2 3 4 5]
	b[0] = 1000
	fmt.Println(a) //[1000 2 3 4 5]
	fmt.Println(b) //[1000 2 3 4 5]
}

由于切片是引用类型,所以a和b其实都指向了同一块内存地址。修改b的同时a的值也会发生变化。

Go语言内建的copy()函数可以迅速地将一个切片的数据复制到另外一个切片空间中,copy()函数的使用格式如下:

		copy(destSlice, srcSlice []T
         
    1.srcSlice: 数据来源切片
		2.destSlice: 目标切片
11.从切片中删除元素
  • Go语言中并没有删除切片元素的专用方法,我们可以使用切片本身的特性来删除元素。
func main() {
	// 从切片中删除元素
	a := []int{30, 31, 32, 33, 34, 35, 36, 37}
	// 要删除索引为2的元素
	a = append(a[:2], a[3:]...)
	fmt.Println(a) //[30 31 33 34 35 36 37]
}

总结一下就是:要从切片a中删除索引为index的元素,操作方法是a = append(a[:index], a[index+1:]...)

12.字符串和切片(string and slice)
  • string底层就是一个byte的数组,因此,也可以进行切片操作。
func main() {
    str := "hello world"
    s1 := str[0:5]
    fmt.Println(s1)

    s2 := str[6:]
    fmt.Println(s2)
}
//结果
		hello
    world
  • string本身是不可变的,因此要改变string中字符。需要如下操作: 英文字符串:
func main() {
    str := "Hello world"
    s := []byte(str) //中文字符需要用[]rune(str)   强转
    s[6] = 'G'
    s = s[:8]
    s = append(s, '!')
    str = string(s)
    fmt.Println(str)
}
  • 含有中文字符串
func main() {
    str := "你好,世界!hello world!"
    s := []rune(str) //强转
    s[3] = '够'
    s[4] = '浪'
    s[12] = 'g'
    s = s[:14]
    str = string(s)
    fmt.Println(str)
}

//结果:你好,够浪!hello go

Slice底层实现:https://www.topgoer.com/go%E5%9F%BA%E7%A1%80/Slice%E5%BA%95%E5%B1%82%E5%AE%9E%E7%8E%B0.html#nil-%E5%92%8C%E7%A9%BA%E5%88%87%E7%89%87

6.Go语言基础之指针

1.Go语言中的指针

  • Go语言中的函数传参都是值拷贝,当我们想要修改某个变量的时候,我们可以创建一个指向该变量地址的指针变量。传递数据使用指针,而无须拷贝数据。类型指针不能进行偏移和运算。Go语言中的指针操作非常简单,只需要记住两个符号:&(取地址)和*(根据地址取值)。

2.指针地址和指针类型

  • 每个变量在运行时都拥有一个地址,这个地址代表变量在内存中的位置。Go语言中使用&字符放在变量前面对变量进行“取地址”操作。 Go语言中的值类型(int、float、bool、string、array、struct)都有对应的指针类型,如:*int、*int64、*string等。
  • 变量指针的语法如下:
    ptr := &v    // v的类型为T

    v:代表被取地址的变量,类型为T
    ptr:用于接收地址的变量,ptr的类型就为*T,称做T的指针类型。*代表指针。

    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
    }

​ 我们来看一下b := &a的图示:

go语言入门基础详谈(语法篇)_第3张图片

2.1指针取值
  • 在对普通变量使用&操作符取地址后会获得这个变量的指针,然后可以对指针使用*操作,也就是指针取值,代码如下。
func main() {
    //指针取值
    a := 10
    b := &a // 取变量a的地址,将指针保存到b中
    fmt.Printf("type of b:%T\n", b)
    c := *b // 指针取值(根据指针去内存取值)
    fmt.Printf("type of c:%T\n", c)
    fmt.Printf("value of c:%v\n", c)
}
结果:
 		type of b:*int
    type of c:int
    value of c:10
  • 总结: 取地址操作符&和取值操作符*是一对互补操作符,&取出地址,*根据地址取出地址指向的值。
变量、指针地址、指针变量、取地址、取值的相互关系和特性如下:

	  1.对变量进行取地址(&)操作,可以获得这个变量的指针变量。
    2.指针变量的值是指针地址。
    3.对指针变量进行取值(*)操作,可以获得指针变量指向的原变量的值。

实例:
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
}
2.2空指针
  • 当一个指针被定义后没有分配到任何变量时,它的值为 nil
  • 空指针的判断
func main() {
    var p *string
    fmt.Println(p)
    fmt.Printf("p的值是%v\n", p)
    if p != nil {
        fmt.Println("非空")
    } else {
        fmt.Println("空值")
    }
}

3.new和make

我们先来看一个例子:

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是内建的两个函数,主要用来分配内存

3.1.new
  • new是一个内置的函数,它的函数签名如下
func new(Type) *Type

    1.Type表示类型,new函数只接受一个参数,这个参数是一个类型
    2.*Type表示类型指针,new函数返回一个指向该类型内存地址的指针。
  • new函数不太常用,使用new函数得到的是一个类型的指针,并且该指针对应的值为该类型的零值。举个例子:
func main() {
    a := new(int)
    b := new(bool)
    fmt.Printf("%T\n", a) // *int
    fmt.Printf("%T\n", b) // *bool
    fmt.Println(*a)       // 0
    fmt.Println(*b)       // false
}
  • 节开始的示例代码中var a *int只是声明了一个指针变量a但是没有初始化,指针作为引用类型需要初始化后才会拥有内存空间,才可以给它赋值。应该按照如下方式使用内置的new函数对a进行初始化之后就可以正常对其赋值了:
func main() {
    var a *int
    a = new(int) //初始化
    *a = 10
    fmt.Println(*a)
3.2.make
  • make也是用于内存分配的,区别于new,它只用于slice、map以及chan的内存创建,而且它返回的类型就是这三个类型本身,而不是他们的指针类型,因为这三种类型就是引用类型,所以就没有必要返回他们的指针了。make函数的函数签名如下:
func make(t Type, size ...IntegerType) Type
  • make函数是无可替代的,我们在使用slice、map以及channel的时候,都需要使用make进行初始化,然后才可以对它们进行操作。这个我们在上一章中都有说明,关于channel我们会在后续的章节详细说明。

    本节开始的示例中var b map[string]int只是声明变量b是一个map类型的变量,需要像下面的示例代码一样使用make函数进行初始化操作之后,才能对其进行键值对赋值:

func main() {
    var b map[string]int
    b = make(map[string]int, 10) //初始化
    b["测试"] = 100
    fmt.Println(b)
}
3.3new与make的区别
		1.二者都是用来做内存分配的。
    2.make只用于slice、map以及channel的初始化,返回的还是这三个引用类型本身;
    3.new用于类型的内存分配,并且内存对应的值为类型零值,返回的是指向类型的指针。

7.Go语言基础之map

  • map:map是一种无序的基于key-value的数据结构,Go语言中的map是引用类型,必须初始化才能使用。

1.map定义:

  • Go语言中 map的定义语法如下:
		map[KeyType]ValueType

    KeyType:表示键的类型。
    ValueType:表示键对应的值的类型。
  • map类型的变量默认初始值为nil,需要使用make()函数来分配内存。语法为:
make(map[KeyType]ValueType, [cap])

其中cap表示map的容量,该参数虽然不是必须的,但是我们应该在初始化map的时候就为其指定一个合适的容量。

2.map基本使用

  • 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也支持在声明的时候填充元素
func main() {
	userInfo := map[string]string{
		"username": "李政",
		"password": "123456",
	}
	fmt.Println(userInfo) 
}

3.判断某一个键是否存在

  • Go语言中有个判断map中键是否存在的特殊写法
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("查无此人")
	}
}

4.map的遍历

Go语言中使用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的时候,可以按下面的写法:
for k := range scoreMap {
		fmt.Println(k)
	}

注意: 遍历map时的元素顺序与添加键值对的顺序无关。

5.使用delete()函数删除键值对

  • 使用delete()内建函数从map中删除一组键值对
delete(map, key)

map:表示要删除键值对的map
key:表示要删除的键值对的键

6.按照指定的顺序遍历map

func main() {
	rand.Seed(time.Now().UnixNano()) //初始化随机数种子

	var scoreMap = make(map[string]int, 200)

	for i := 0; i < 100; i++ {
		key := fmt.Sprintf("stu%02d", i) //生成stu开头的字符串
		value := rand.Intn(100)          //生成0~99的随机整数
		scoreMap[key] = value
	}
	//取出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])
	}
}

7.元素为map类型的切片

func main() {
	var mapSlice = make([]map[string]string, 3)
	for index, value := range mapSlice {
		fmt.Printf("index:%d value:%v\n", index, value)
	}
	fmt.Println("after init")
	// 对切片中的map元素进行初始化
	mapSlice[0] = make(map[string]string, 10)
	mapSlice[0]["name"] = "李政"
	mapSlice[0]["password"] = "123456"
	mapSlice[0]["address"] = "信阳"
	for index, value := range mapSlice {
		fmt.Printf("index:%d value:%v\n", index, value)
	}
}

8.值为切片类型的map

func main() {
	var sliceMap = make(map[string][]string, 3)
	fmt.Println(sliceMap)
	fmt.Println("after init")
	key := "中国"
	value, ok := sliceMap[key]
	if !ok {
		value = make([]string, 0, 2)
	}
	value = append(value, "北京", "上海")
	sliceMap[key] = value
	fmt.Println(sliceMap)
}

8.Go语言基础之函数

  • 函数是组织好的、可重复使用的、用于执行指定任务的代码块

一.函数

  • Go语言中支持函数、匿名函数和闭包,并且函数在Go语言中属于“一等公民”。

1.函数的定义

  • Go语言中定义函数使用func关键字。
func 函数名(参数)(返回值){
    函数体
}

函数名:由字母、数字、下划线组成。但函数名的第一个字母不能是数字。在同一个包内,函数名也称不能重名(包的概念详见后文)。
参数:参数由参数变量和参数变量的类型组成,多个参数之间使用,分隔。
返回值:返回值由返回值变量和其变量类型组成,也可以只写返回值的类型,多个返回值必须用()包裹,并用,分隔。
函数体:实现指定功能的代码块。

2.函数的调用

  • 定义了函数之后,我们可以通过函数名()的方式调用函数。
func intSum(x int, y int) int {
	return x + y
}

func sayHello() {
	fmt.Println("Hello 沙河")
}

func main() {
	sayHello()
	ret := intSum(10, 20)
	fmt.Println(ret)
}
  • 注意,调用有返回值的函数时,可以不接收其返回值。

3.参数

3.1类型简写
  • 函数的参数中如果相邻变量的类型相同,则可以省略类型
func intSum(x, y int) int {
	return x + y
}

上面的代码中,intSum函数有两个参数,这两个参数的类型均为int,因此可以省略x的类型,因为y后面有类型说明,x参数也是该类型。
3.2可变参数
  • 可变参数是指函数的参数数量不固定。Go语言中的可变参数通过在参数名后加...来标识。
  • 可变参数通常要作为函数的最后一个参数。
func intSum2(x ...int) int {
	fmt.Println(x) //x是一个切片
	sum := 0
	for _, v := range x {
		sum = sum + v
	}
	return sum
}
//调用上面的函数:
ret1 := intSum2()
ret2 := intSum2(10)
ret3 := intSum2(10, 20)
ret4 := intSum2(10, 20, 30)
fmt.Println(ret1, ret2, ret3, ret4) //0 10 30 60
  • 固定参数搭配可变参数使用时,可变参数要放在固定参数的后面
func intSum3(x int, y ...int) int {
	fmt.Println(x, y)
	sum := x
	for _, v := range y {
		sum = sum + v
	}
	return sum
}
//调用上面的函数
ret5 := intSum3(100)
ret6 := intSum3(100, 10)
ret7 := intSum3(100, 10, 20)
ret8 := intSum3(100, 10, 20, 30)
fmt.Println(ret5, ret6, ret7, ret8) //100 110 130 160
//本质上,函数的可变参数是通过切片来实现的。

4.返回值

  • Go语言中通过return关键字向外输出返回值。
4.1多返回值
  • Go语言中函数支持多返回值,函数如果有多个返回值时必须用()将所有返回值包裹起来。
func calc(x, y int) (int, int) {
	sum := x + y
	sub := x - y
	return sum, sub
}
4.2返回值命名
  • 函数定义时可以给返回值命名,并在函数体中直接使用这些变量,最后通过return关键字返回。
func calc(x, y int) (sum, sub int) {
	sum = x + y
	sub = x - y
	return
}
4.3返回值补充
  • 当我们的一个函数返回值类型为slice时,nil可以看做是一个有效的slice,没必要显示返回一个长度为0的切片。
func someFunc(x string) []int {
	if x == "" {
		return nil // 没必要返回[]int{}
	}
	...
}

二.函数进阶

1.变量作用域

1.全局变量

  • 全局变量是定义在函数外部的变量,它在程序整个运行周期内都有效。 在函数中可以访问到全局变量。
package main

import "fmt"

//定义全局变量num
var num int64 = 10

func testGlobalVar() {
	fmt.Printf("num=%d\n", num) //函数中可以访问全局变量num
}
func main() {
	testGlobalVar() //num=10
}

2.局部变量

  • 局部变量又分为两种: 函数内定义的变量无法在该函数外使用,例如下面的示例代码main函数中无法使用testLocalVar函数中定义的变量x:
func testLocalVar() {
	//定义一个函数局部变量x,仅在该函数内生效
	var x int64 = 100
	fmt.Printf("x=%d\n", x)
}

func main() {
	testLocalVar()
	fmt.Println(x) // 此时无法使用变量x
}

注意:如果局部变量和全局变量重名,优先访问局部变量。

  • 语句块定义的变量,通常我们会在if条件判断、for循环、switch语句上使用这种定义变量的方式。
func testLocalVar2(x, y int) {
	fmt.Println(x, y) //函数的参数也是只在本函数中生效
	if x > 0 {
		z := 100 //变量z只在if语句块生效
		fmt.Println(z)
	}
	//fmt.Println(z)//此处无法使用变量z
}
  • for循环语句中定义的变量,也是只在for语句块中生效
func testLocalVar3() {
	for i := 0; i < 10; i++ {
		fmt.Println(i) //变量i只在当前for语句块中生效
	}
	//fmt.Println(i) //此处无法使用变量i
}

3.函数类型与变量

3.1定义函数类型
  • 我们可以使用type关键字来定义一个函数类型,
type calculation func(int, int) int      //定义一个函数了类型
  • 上面语句定义了一个calculation类型,它是一种函数类型,这种函数接收两个int类型的参数并且返回一个int类型的返回值。

  • 简单来说,凡是满足这个条件的函数都是calculation类型的函数,例如下面的add和sub是calculation类型。

func add(x, y int) int {
	return x + y
}

func sub(x, y int) int {
	return x - y
}

//add和sub都能赋值给calculation类型的变量
var c calculation
c = add
3.2函数类型变量

我们可以声明函数类型的变量并且为该变量赋值

func main() {
	var c calculation               // 声明一个calculation类型的变量c
	c = add                         // 把add赋值给c
	fmt.Printf("type of c:%T\n", c) // type of c:main.calculation
	fmt.Println(c(1, 2))            // 像调用add一样调用c

	f := add                        // 将函数add赋值给变量f1
	fmt.Printf("type of f:%T\n", f) // type of f:func(int, int) int
	fmt.Println(f(10, 20))          // 像调用add一样调用f
}

4.高阶函数

  • 高阶函数分为函数作为参数和函数作为返回值两部分。
1.函数作为参数
  • 函数可以作为参数
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) //30
}
2.函数作为返回值
  • 函数也阔以作为返回值
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.匿名函数

  • 函数当然还可以作为返回值,但是在Go语言中函数内部不能再像之前那样定义函数了,只能定义匿名函数。匿名函数就是没有函数名的函数,匿名函数的定义格式如下:
func(参数)(返回值){
    函数体
}
  • 匿名函数因为没有函数名,所以没办法像普通函数那样调用,所以匿名函数需要保存到某个变量或者作为立即执行函数:
func main() {
	// 将匿名函数保存到变量
	add := func(x, y int) {
		fmt.Println(x + y)
	}
	add(10, 20) // 通过变量调用匿名函数

	//自执行函数:匿名函数定义完加()直接执行
	func(x, y int) {
		fmt.Println(x + y)
	}(10, 20)

注:匿名函数多用于实现回调函数和闭包。

2.闭包

  • 闭包指的是一个函数和与其相关的引用环境组合而成的实体。简单来说,闭包=函数+引用环境
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)) //10
	fmt.Println(f(20)) //30
	fmt.Println(f(30)) //60

	f1 := adder()
	fmt.Println(f1(40)) //40
	fmt.Println(f1(50)) //90
}
  • 变量f是一个函数并且它引用了其外部作用域中的x变量,此时f就是一个闭包。 在f的生命周期内,变量x也一直有效。
func adder2(x int) func(int) int {
	return func(y int) int {
		x += y
		return x
	}
}
func main() {
	var f = adder2(10)
	fmt.Println(f(10)) //20
	fmt.Println(f(20)) //40
	fmt.Println(f(30)) //70

	f1 := adder2(20)
	fmt.Println(f1(40)) //60
	fmt.Println(f1(50)) //110
}
  • 闭包其实并不复杂,只要牢记闭包=函数+引用环境

⚠️自己需要在找相关的资料看看。

四.defer语句

  • Go语言中的defer语句会将其后面跟随的语句进行延迟处理。在defer归属的函数即将返回时,将延迟处理的语句按defer定义的逆序进行执行,也就是说,先被defer的语句最后被执行,最后被defer的语句,最先被执行。
func main() {
	fmt.Println("start")
	defer fmt.Println(1)
	defer fmt.Println(2)
	defer fmt.Println(3)
	fmt.Println("end")
}
输出:
start
end
3
2
1
  • 由于defer语句延迟调用的特性,所以defer语句能非常方便的处理资源释放问题。比如:资源清理、文件关闭、解锁及记录时间等。
1.defer执行时机
  • 在Go语言的函数中return语句在底层并不是原子操作,它分为给返回值赋值和RET指令两步。而defer语句执行的时机就在返回值赋值操作后,RET指令执行前。

go语言入门基础详谈(语法篇)_第4张图片

2.defer经典案例
  • 阅读下面的代码,写出最后的打印结果。
func f1() int {
	x := 5
	defer func() {
		x++
	}()
	return x
}

func f2() (x int) {
	defer func() {
		x++
	}()
	return 5
}

func f3() (y int) {
	x := 5
	defer func() {
		x++
	}()
	return x
}
func f4() (x int) {
	defer func(x int) {
		x++
	}(x)
	return 5
}
func main() {
	fmt.Println(f1())
	fmt.Println(f2())
	fmt.Println(f3())
	fmt.Println(f4())
}
//结果
5
6
5
5

五.内置函数介绍

  1. close:主要用来关闭channel
  2. len:用来求长度,比如string、array、slice、map、channel
  3. new:用来分配内存,主要用来分配值类型,比如int、struct。返回的是指针
  4. make:用来分配内存,主要用来分配引用类型,比如chan、map、slice
  5. append:用来追加元素到数组、slice中
  6. panic和recover:用来做错误处理

9.Go语言基础之结构体

  • Go语言中没有“类”的概念,也不支持“类”的继承等面向对象的概念。Go语言中通过结构体的内嵌再配合接口比面向对象具有更高的扩展性和灵活性。

一.类型别名和自定义类型

1.类型别名

  • 类型别名规定:TypeAlias只是Type的别名,本质上TypeAlias与Type是同一个类型。就像一个孩子小时候有小名、乳名,上学后用学名,英语老师又会给他起英文名,但这些名字都指的是他本人。
type TypeAlias = Type    

//我们之前见的rune和byte都是类型别名
type byte = unit8
type rune = int32

2.自定义类型

  • 在Go语言中有一些基本的数据类型,如string整型浮点型布尔等数据类型, Go语言中可以使用type关键字来定义自定义类型。

  • 自定义类型是定义了一个全新的类型。我们可以基于内置的基本类型定义,也可以通过struct定义。

//将MyInt定义为int类型
type MyInt int

通过type关键字的定义,MyInt就是一种新的类型,它具有int的特性。

3.类型定义和类型别名的区别

  • 类型别名与类型定义表面上看只有一个等号的差异,我们通过下面的这段代码来理解它们之间的区别。
//类型定义
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的类型是intMyInt类型只会在代码中存在,编译完成时并不会有MyInt类型。

二.结构体

  • Go语言中的基础数据类型可以表示一些事物的基本属性,但是当我们想表达一个事物的全部或部分属性时,这时候再用单一的基本数据类型明显就无法满足需求了,Go语言提供了一种自定义数据类型,可以封装多个基本数据类型,这种数据类型叫结构体,英文名称struct。 也就是我们可以通过struct来定义自己的类型了。

1.结构体的定义

  • 使用typestruct关键字来定义结构体
type 类型名 struct{
  	字段名 字段类型
  	字段名 字段类型
  	...
}
类型名:标识自定义结构体的名称,在同一个包内不能重复。
字段名:表示结构体字段名。结构体中的字段名必须唯一。
字段类型:表示结构体字段的具体类型。
  • 定义一个Person的结构体,
type person struct {
	name string
	city string
	age  int8
}
//同样类型的字段也阔以写在一行
type person1 struct {
	name, city string
	age        int8
}
  • 这样我们就拥有了一个person的自定义类型,它有namecityage三个字段,分别表示姓名、城市和年龄。这样我们使用这个person结构体就能够很方便的在程序中表示和存储人信息了。语言内置的基础数据类型是用来描述一个值的,而结构体是用来描述一组值的。比如一个人有名字、年龄和居住城市等,本质上是一种聚合型的数据类型

2.结构体实例化

  • 只有当结构体实例化时,才会真正地分配内存。也就是必须实例化后才能使用结构体的字段。

  • 结构体本身也是一种类型,我们可以像声明内置类型一样使用var关键字声明结构体类型。

var 结构体实例 结构体类型
2.1基本实例化
  • 我们通过.来访问结构体的字段(成员变量),例如p1.namep1.age等。
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)  //p1={李政 北京 18}
	fmt.Printf("p1=%#v\n", p1) //p1=main.person{name:"李政", city:"北京", age:18}
}
2.2匿名结构体
  • 在定义一些临时数据结构等场景下还可以使用匿名结构体。
func main() {
    var user struct{Name string; Age int}
    user.Name = "李政"
    user.Age = 18
    fmt.Printf("%#v\n", user)
}
2.3创建指针类型结构体
  • 我们还可以通过使用new关键字对结构体进行实例化,得到的是结构体的地址。
var p2 = new(person)
fmt.Printf("%T\n", p2)     //*main.person
fmt.Printf("p2=%#v\n", p2) //p2=&main.person{name:"", city:"", age:0}

从打印的结果中我们可以看出p2是一个结构体指针
  • 需要注意的是在Go语言中支持对结构体指针直接使用.来访问结构体的成员。
var p2 = new(person)
p2.name = "李政"
p2.age = 28
p2.city = "上海"
fmt.Printf("p2=%#v\n", p2) //p2=&main.person{name:"李政", city:"上海", age:28}

2.4取结构体的地址实例化

  • 使用&对结构体进行取地址操作相当于对该结构体类型进行了一次new实例化操作。
p3 := &person{}
fmt.Printf("%T\n", p3)     //*main.person
fmt.Printf("p3=%#v\n", p3) //p3=&main.person{name:"", city:"", age:0}
p3.name = "李政"
p3.age = 30
p3.city = "成都"
fmt.Printf("p3=%#v\n", p3) //p3=&main.person{name:"李政", city:"成都", age:30}

p3.name = "李政"其实在底层是(*p3).name = "李政",这是Go语言帮我们实现的语法糖。

3.结构体初始化

  • 没有初始化的结构体,其成员变量都是对应其类型的零值。
3.1使用键值对初始化
  • 使用键值对对结构体进行初始化时,键对应结构体的字段,值对应该字段的初始值。
p5 := person{
	name: "李政",
	city: "北京",
	age:  18,
}
fmt.Printf("p5=%#v\n", p5) //p5=main.person{name:"李政", city:"北京", age:18}
  • 也可以对结构体指针进行键值对初始化.
p6 := &person{
	name: "李政",
	city: "北京",
	age:  18,
}
fmt.Printf("p6=%#v\n", p6) //p6=&main.person{name:"李政", city:"北京", age:18}
  • 当某些字段没有初始值的时候,该字段可以不写。此时,没有指定初始值的字段的值就是该字段类型的零值。
p7 := &person{
	city: "北京",
}
fmt.Printf("p7=%#v\n", p7) //p7=&main.person{name:"", city:"北京", age:0}
3.2使用值的列表初始化

初始化的时候可以简写,也就是初始化的时候不写键,直接写值。

p8 := &person{
	"李政",
	"北京",
	28,
}
fmt.Printf("p8=%#v\n", p8) //p8=&main.person{name:"李政", city:"北京", age:28}
必须初始化结构体的所有字段。
初始值的填充顺序必须与字段在结构体中的声明顺序一致。
该方式不能和键值初始化方式混用。

4.结构体内存布局

  • 结构体占用一块连续的内存。
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)
//输出
n.a 0xc0000a0060
n.b 0xc0000a0061
n.c 0xc0000a0062
n.d 0xc0000a0063
  • 空结构体:空结构体是不占用空间的。

5.构造函数

  • Go语言的结构体没有构造函数,我们可以自己实现。 例如,下方的代码就实现了一个person的构造函数。 因为struct是值类型,如果结构体比较复杂的话,值拷贝性能开销会比较大,所以该构造函数返回的是结构体指针类型。
func newPerson(name, city string, age int8) *person {
	return &person{
		name: name,
		city: city,
		age:  age,
	 }
}
//调用构造函数
p9 := newPerson("李政", "固始", 90)
fmt.Printf("%#v\n", p9) //&main.person{name:"张三", city:"沙河", age:90}

6.方法和接收者

  • Go语言中的方法(Method)是一种作用于特定类型变量的函数。这种特定类型变量叫做接收者(Receiver)。接收者的概念就类似于其他语言中的this或者 self
func (接收者变量 接收者类型) 方法名(参数列表) (返回参数) {
    函数体
}

接收者变量:接收者中的参数变量名在命名时,官方建议使用接收者类型名称首字母的小写,而不是self、this之类的命名。例如,Person类型的接收者变量应该命名为 p,Connector类型的接收者变量应该命名为c等。
接收者类型:接收者类型和参数类似,可以是指针类型和非指针类型。
方法名、参数列表、返回参数:具体格式与函数定义相同。
  • 方法与函数的区别是,函数不属于任何类型,方法属于特定的类型。
//Person 结构体
type Person struct {
	name string
	age  int8
}

//NewPerson 构造函数
func NewPerson(name string, age int8) *Person {
	return &Person{
		name: name,
		age:  age,
	}
}

//Dream Person做梦的方法
func (p Person) Dream() {
	fmt.Printf("%s的梦想是学好Go语言!\n", p.name)
}

func main() {
	p1 := NewPerson("小王子", 25)
	p1.Dream()
}
6.1指针类型的接收者
  • 指针类型的接收者由一个结构体的指针组成,由于指针的特性,调用方法时修改接收者指针的任意成员变量,在方法结束后,修改都是有效的。这种方式就十分接近于其他语言中面向对象中的this或者self。 例如我们为Person添加一个SetAge方法,来修改实例变量的年龄。
// SetAge 设置p的年龄
// 使用指针接收者
func (p *Person) SetAge(newAge int8) {
	p.age = newAge
}

//调用该方法
func main() {
	p1 := NewPerson("李政", 25)
	fmt.Println(p1.age) // 25
	p1.SetAge(30)
	fmt.Println(p1.age) // 30
}
6.2值类型的接受者
  • 当方法作用于值类型接收者时,Go语言会在代码运行时将接收者的值复制一份。在值类型接收者的方法中可以获取接收者的成员值,但修改操作只是针对副本,无法修改接收者变量本身。
// SetAge2 设置p的年龄
// 使用值接收者
func (p Person) SetAge2(newAge int8) {
	p.age = newAge
}

func main() {
	p1 := NewPerson("李政", 25)
	p1.Dream()
	fmt.Println(p1.age) // 25
	p1.SetAge2(30) // (*p1).SetAge2(30)
	fmt.Println(p1.age) // 25
}
6.3什么时候使用指针类型接受者
  1. 需要修改接收者中的值
  2. 接收者是拷贝代价比较大的大对象
  3. 保证一致性,如果有某个方法使用了指针接收者,那么其他的方法也应该使用指针接收者。

7.结构体的匿名字段

  • 结构体允许其成员字段在声明时没有字段名而只有类型,这种没有名字的字段就称为匿名字段。
//Person 结构体Person类型
type Person struct {
	string
	int
}

func main() {
	p1 := Person{
		"李政",
		18,
	}
	fmt.Printf("%#v\n", p1)        //main.Person{string:"北京", int:18}
	fmt.Println(p1.string, p1.int) //北京 18
}

注意:这里匿名字段的说法并不代表没有字段名,而是默认会采用类型名作为字段名,结构体要求字段名称必须唯一,因此一个结构体中同种类型的匿名字段只能有一个。

8.嵌套结构体

  • 一个结构体中可以嵌套包含另一个结构体或结构体指针
//Address 地址结构体
type Address struct {
	Province string
	City     string
}

//User 用户结构体
type User struct {
	Name    string
	Gender  string
	Address Address
}

func main() {
	user1 := User{
		Name:   "李政",
		Gender: "男",
		Address: Address{
			Province: "信阳",
			City:     "固始",
		},
	}
	fmt.Printf("user1=%#v\n", user1)//user1=main.User{Name:"李政", Gender:"男", Address:main.Address{Province:"信阳", City:"固始"}}
}
8.1嵌套匿名字段
  • 上面user结构体中嵌套的Address结构体也可以采用匿名字段的方式。
  • 当访问结构体成员时会先在结构体中查找该字段,找不到再去嵌套的匿名字段中查找。
//Address 地址结构体
type Address struct {
	Province string
	City     string
}

//User 用户结构体
type User struct {
	Name    string
	Gender  string
	Address //匿名字段
}

func main() {
	var user2 User
	user2.Name = "李政"
	user2.Gender = "男"
	user2.Address.Province = "信阳"    // 匿名字段默认使用类型名作为字段名
	user2.City = "固始"                // 匿名字段可以省略
	fmt.Printf("user2=%#v\n", user2) //user2=main.User{Name:"李政", Gender:"男", Address:main.Address{Province:"信阳", City:"固始"}}
}
8.2嵌套结构体的字段名冲突
  • 嵌套结构体内部可能存在相同的字段名。在这种情况下为了避免歧义需要通过指定具体的内嵌结构体字段名。
/Address 地址结构体
type Address struct {
	Province   string
	City       string
	CreateTime string
}

//Email 邮箱结构体
type Email struct {
	Account    string
	CreateTime string
}

//User 用户结构体
type User struct {
	Name   string
	Gender string
	Address
	Email
}

func main() {
	var user3 User
	user3.Name = "李政"
	user3.Gender = "男"
	// user3.CreateTime = "2019" //ambiguous selector user3.CreateTime
	user3.Address.CreateTime = "2001" //指定Address结构体中的CreateTime
	user3.Email.CreateTime = "2001"   //指定Email结构体中的CreateTime
}
8.3结构体的"继承"
  • Go语言中使用结构体也可以实现其他编程语言中面向对象的继承。
//Animal 动物
type Animal struct {
	name string
}

func (a *Animal) move() {
	fmt.Printf("%s会动!\n", a.name)
}

//Dog 狗
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() //李政会动!
}

9.结构体字段的可见性

:结构体中字段大写开头表示可公开访问,小写表示私有(仅在定义当前结构体的包中可访问)。

10.结构体标签(Tag)

  • Tag是结构体的元信息,可以在运行的时候通过反射的机制读取出来。 Tag在结构体字段的后方定义,由一对反引号包裹起来,具体的格式如下:
  • 结构体tag由一个或多个键值对组成。键与值使用冒号分隔,值用双引号括起来。同一个结构体字段可以设置多个键值对tag,不同的键值对之间使用空格分隔。
`key1:"value1" key2:"value2"`

注意事项: 为结构体编写Tag时,必须严格遵守键值对的规则。结构体标签的解析代码的容错能力很差,一旦格式写错,编译和运行时都不会提示任何错误,通过反射也无法正确取值。例如不要在key和value之间添加空格。

  • 例如我们为Student结构体的每个字段定义json序列化时使用的Tag:
//Student 学生
type Student struct {
	ID     int    `json:"id"` //通过指定tag实现json序列化该字段时的key
	Gender string //json序列化是默认使用字段名作为key
	name   string //私有不能被json包访问
}

func main() {
	s1 := Student{
		ID:     1,
		Gender: "男",
		name:   "李政",
	}
	data, err := json.Marshal(s1)
	if err != nil {
		fmt.Println("json marshal failed!")
		return
	}
	fmt.Printf("json str:%s\n", data) //json str:{"id":1,"Gender":"男"}
}

11.结构体与JSON序列化

  • JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。JSON键值对是用来保存JS对象的一种方式,键/值对组合中的键名写在前面并用双引号""包裹,使用冒号:分隔,然后紧接着值;多个键值之间使用英文,分隔。
//Student 学生
type Student struct {
	ID     int
	Gender string
	Name   string
}

//Class 班级
type Class struct {
	Title    string
	Students []*Student
}

func main() {
	c := &Class{
		Title:    "101",
		Students: make([]*Student, 0, 200),
	}
	for i := 0; i < 10; i++ {
		stu := &Student{
			Name:   fmt.Sprintf("stu%02d", i),
			Gender: "男",
			ID:     i,
		}
		c.Students = append(c.Students, stu)
	}
	//JSON序列化:结构体-->JSON格式的字符串
	data, err := json.Marshal(c)
	if err != nil {
		fmt.Println("json marshal failed")
		return
	}
	fmt.Printf("json:%s\n", data)
	//JSON反序列化:JSON格式的字符串-->结构体
	str := `{"Title":"101","Students":[{"ID":0,"Gender":"男","Name":"stu00"},{"ID":1,"Gender":"男","Name":"stu01"},{"ID":2,"Gender":"男","Name":"stu02"},{"ID":3,"Gender":"男","Name":"stu03"},{"ID":4,"Gender":"男","Name":"stu04"},{"ID":5,"Gender":"男","Name":"stu05"},{"ID":6,"Gender":"男","Name":"stu06"},{"ID":7,"Gender":"男","Name":"stu07"},{"ID":8,"Gender":"男","Name":"stu08"},{"ID":9,"Gender":"男","Name":"stu09"}]}`
	c1 := &Class{}
	err = json.Unmarshal([]byte(str), c1)
	if err != nil {
		fmt.Println("json unmarshal failed!")
		return
	}
	fmt.Printf("%#v\n", c1)
}

12.结构体使用map和slice时注意事项

  • 因为slice和map这两种数据类型都包含了指向底层数据的指针,因此我们在需要复制它们时要特别注意。
type Person struct {
	name   string
	age    int8
	dreams []string
}

//错误做法
func (p *Person) SetDreams(dreams []string) {
	p.dreams = dreams
}

//正确做法
func (p *Person) SetDreams(dreams []string) {
	p.dreams = make([]string, len(dreams))
	copy(p.dreams, dreams)
}

func main() {
	p1 := Person{name: "李政", age: 18}
	data := []string{"吃饭", "睡觉", "打豆豆"}
	p1.SetDreams(data)

	// 你真的想要修改 p1.dreams 吗?
	data[1] = "不睡觉"
	fmt.Println(p1.dreams)  // ?
}

⚠️同样的问题也存在于返回值slice和map的情况,在实际编码过程中一定要注意这个问题。

你可能感兴趣的:(go语言专栏,golang,开发语言,入门系列)