Golang基础学习笔记(一)

文章目录

1.Golang的数据类型

1.1整型

整型的类型有很多中,包括 int8,int16,int32,int64。我们可以根据具体的情况来进行定义

如果我们直接写 int也是可以的,它在不同的操作系统中,int的大小是不一样的

  • 32位操作系统:int -> int32
  • 64位操作系统:int -> int64

在这里插入图片描述
可以通过unsafe.Sizeof 查看不同长度的整型,在内存里面的存储空间

var num2 = 12
fmt.Println(unsafe.Sizeof(num2))

1.2 浮点型

Go语言支持两种浮点型数:float32和float64。这两种浮点型数据格式遵循IEEE754标准:

float32的浮点数的最大范围约为3.4e38,可以使用常量定义:math.MaxFloat32。float64的浮点数的最大范围约为1.8e308,可以使用一个常量定义:math.MaxFloat64

打印浮点数时,可以使用fmt包配合动词%f,代码如下:

var pi = math.Pi
// 打印浮点类型,默认小数点6位
fmt.Printf("%f\n", pi)
// 打印浮点类型,打印小数点后2位
fmt.Printf("%.2f\n", pi)

1.2.1 Golang中精度丢失问题

几乎所有的编程语言都有精度丢失的问题,这是典型的二进制浮点数精度损失问题,在定长条件下,二进制小数和十进制小数互转可能有精度丢失

d := 1129.6
fmt.Println(d*100) //输出112959.99999999

解决方法,使用第三方包来解决精度损失的问题:

package main
 
import (
	"log"
	"github.com/shopspring/decimal"
)
 
func main() {
	xdecimal, err := decimal.NewFromString("1129.6")
	if err != nil {
		log.Println("转化decimal失败", err)
	}
	ydecimal  := decimal.NewFromFloat(100)
	
	resultdecimal := xdecimal.Mul(ydecimal)
	log.Println(resultdecimal)  //112960
 
}

更详情的用法参考包:github.com/shopspring/decimal

1.3 布尔类型

定义

var fl = false
if f1 {
    fmt.Println("true")
} else {
    fmt.Println("false")
}

1.4 字符串类型

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

s1 := "hello"
s1 := "你好"

如果想要定义多行字符串,可以使用反引号

	var str = `第一行
第二行`
	fmt.Println(str)

字符串常见操作

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

1.5 byte 和 rune类型

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

Go语言中的字符有以下两种类型

  • uint8类型:或者叫byte型,代表了ACII码的一个字符
  • rune类型:代表一个UTF-8字符

当需要处理中文,日文或者其他复合字符时,则需要用到rune类型,rune类型实际上是一个int32

Go使用了特殊的rune类型来处理Unicode,让基于Unicode的文本处理更为方便,也可以使用byte型进行默认字符串处理,性能和扩展性都有照顾。

需要注意的是,在go语言中,一个汉字占用3个字节(utf-8),一个字母占用1个字节

package main
import "fmt"

func main() {
	var a byte = 'a'
	// 输出的是ASCII码值,也就是说当我们直接输出byte(字符)的时候,输出的是这个字符对应的码值
	fmt.Println(a)
	// 输出的是字符
	fmt.Printf("%c", a)

	// for循环打印字符串里面的字符
	// 通过len来循环的,相当于打印的是ASCII码
	s := "你好 golang"
	for i := 0; i < len(s); i++ {
		fmt.Printf("%v(%c)\t", s[i], s[i])
	}

	// 通过rune打印的是 utf-8字符
	for index, v := range s {
		fmt.Println(index, v)
	}
}

1.5.1 修改字符串

要修改字符串,需要先将其转换成[]rune 或 []byte类型,完成后在转换成string,无论哪种转换都会重新分配内存,并复制字节数组

//转换为 []byte 类型
s1 := "big"
byteS1 := []byte(s1)
byteS1[0] = 'p'
fmt.Println(string(byteS1))

//转换为rune类型
s2 := "你好golang"
byteS2 := []rune(s2)
byteS2[0] = '我'
fmt.Println(string(byteS2))

1.6 基本数据类型转换

数值类型转换

// 整型和浮点型之间转换
var aa int8 = 20
var bb int16 = 40
fmt.Println(int16(aa) + bb)

// 建议整型转换成浮点型
var cc int8 = 20
var dd float32 = 40
fmt.Println(float32(cc) + dd)
建议从低位转换成高位,这样可以避免

转换成字符串类型

第一种方式,就是通过 fmt.Sprintf()来转换:

// 字符串类型转换
var i int = 20
var f float64 = 12.456
var t bool = true
var b byte = 'a'
str1 := fmt.Sprintf("%d", i)
fmt.Printf("类型:%v-%T \n", str1, str1)

str2 := fmt.Sprintf("%f", f)
fmt.Printf("类型:%v-%T \n", str2, str2)

str3 := fmt.Sprintf("%t", t)
fmt.Printf("类型:%v-%T \n", str3, str3)

str4 := fmt.Sprintf("%c", b)
fmt.Printf("类型:%v-%T \n", str4, str4)

第二种方法就是通过strconv包里面的集中转换方法进行转换:

// int类型转换str类型
var num1 int64 = 20
s1 := strconv.FormatInt(num1, 10)
fmt.Printf("转换:%v - %T", s1, s1)

// float类型转换成string类型
var num2 float64 = 3.1415926

/*
		参数1:要转换的值
		参数2:格式化类型 'f'表示float,'b'表示二进制,‘e’表示 十进制
		参数3:表示保留的小数点,-1表示不对小数点格式化
		参数4:格式化的类型,传入64位 或者 32位
	 */
s2 := strconv.FormatFloat(num2, 'f', -1, 64)
fmt.Printf("转换:%v-%T", s2, s2)

字符串转换成int 和 float类型

str := "10"
// 第一个参数:需要转换的数,第二个参数:进制, 参数三:32位或64位
num,_ = strconv.ParseInt(str, 10, 64)

// 转换成float类型
str2 := "3.141592654"
num,_ = strconv.ParseFloat(str2, 10)

2.Golang的流程控制

流程控制是每种编程语言控制逻辑走向和执行次序的重要部分,流程控制可以说是一门语言的“经脉"。

Go 语言中最常用的流程控制有iffor,而switchgoto主要是为了简化代码、降低重复代码而生的结构,属于扩展类的流程控制。

2.1 for range(键值循环)

Go 语言中可以使用for range遍历数组、切片、字符串、map及通道(channel)。通过for range遍历的返回值有以下规律:

  • 数组、切片、字符串返回索引和值。
  • map返回键和值。
  • 通道(channel)只返回通道内的值。

实例:遍历字符串(默认遍历的是rune类型Unicode编码,for遍历的是byte类型ASCII编码

var str = "你好golang"
for key, value := range str {
    fmt.Printf("%v - %c ", key, value) //一个汉字占3个字节,一个字符1个字节
}

遍历切片(数组)

var array = []string{"php", "java", "node", "golang"}
for index, value := range array {
    fmt.Printf("%v %s ", index, value)
}

注意,在Go语言中,没有while语句,我们可以通过for来代替

for {
    循环体
}

for循环可以通过breakgotoreturnpanic语句退出循环

2.2 switch case

使用switch语句可方便的对大量的值进行条件判断,并且同时一个分支可以有多个值

extname := ".txt"
switch extname {
	case ".html": {
		fmt.Println(".html")
		fallthrought
	}
	case ".txt",".doc": {
		fmt.Println("传递来的是文档")
		fallthrought
	}
	case ".js": {
		fmt.Println(".js")
		fallthrought
	}
	default: {
		fmt.Println("其它后缀")
	}
}

tip:在golang中,break可以不写,也能够跳出case,而不会执行其它的。

如果我们需要使用switch的穿透 fallthrought,fallthrough语法可以执行满足条件的 case 的下一个case,为了兼容c语言中的case设计

extname := ".txt"
switch extname {
	case ".html": {
		fmt.Println(".html")
		fallthrought
	}
	case ".txt",".doc": {
		fmt.Println("传递来的是文档")
		fallthrought
	}
	case ".js": {
		fmt.Println(".js")
		fallthrought
	}
	default: {
		fmt.Println("其它后缀")
	}
}

fallthrought 只能穿透紧挨着的一层,不会一直穿透,但是如果每一层都写的话,就会导致每一层都进行穿透。

2.3 goto:跳转到指定标签

goto 语句通过标签进行代码间的无条件跳转。goto 语句可以在快速跳出循环、避免重复退出上有一定的帮助。Go语言中使用goto语句能简化一些代码的实现过程。

	var n = 20
	if n > 24 {
		fmt.Println("成年人")
	} else {
		goto lable3
	}

	fmt.Println("aaa")
	fmt.Println("bbb")
lable3:
	fmt.Println("ccc")
	fmt.Println("ddd")

3.Golang的数组

和数组对应的类型是Slice(切片),Slice是可以增长和收缩的动态序列,功能也更灵活,但是想要理解slice工作原理的话需要先理解数组。

3.1 数组定义和初始化

// 数组的长度是类型的一部分
var arr1 [3]int
var arr2 [4]string
fmt.Printf("%T, %T \n", arr1, arr2)

// 数组的初始化 第一种方法
var arr3 [3]int
arr3[0] = 1
arr3[1] = 2
arr3[2] = 3
fmt.Println(arr3)

// 第二种初始化数组的方法
var arr4 = [4]int {10, 20, 30, 40}
fmt.Println(arr4)

// 第三种数组初始化方法,自动推断数组长度
var arr5 = [...]int{1, 2}
fmt.Println(arr5)

// 第四种初始化数组的方法,指定下标
a := [...]int{1:1, 3:5}
fmt.Println(a)

3.2 数组的值类型

数组是值类型,赋值和传参会赋值整个数组,因此改变副本的值,不会改变本身的值

// 数组
var array1 = [...]int {1, 2, 3}
array2 := array1
array2[0] = 3
fmt.Println(array1, array2)

例如上述的代码,我们将数组进行赋值后,该改变数组中的值时,发现结果如下

[1 2 3] [3 2 3]

这就说明了,golang中的数组是值类型,而不是和java一样属于引用数据类型。

3.3 切片定义(引用类型)

在golang中,切片的定义和数组定义是相似的,但是需要注意的是,切片是引用数据类型,如下

// 切片定义
var array3 = []int{1,2,3}
array4 := array3
array4[0] = 3
fmt.Println(array3, array4)

我们通过改变第一个切片元素,然后查看最后的效果

[3 2 3] [3 2 3]

4.Golang的切片

4.1 为什么要使用切片

切片(Slice)是一个拥有相同类型元素的可变长度的序列。它是基于数组类型做的一层封装。 它非常灵活,支持自动扩容。

切片是一个引用类型,它的内部结构包含地址、长度和容量。

声明切片类型的基本语法如下:

var name [] T

其中:

name:表示变量名
T:表示切片中的元素类型

举例

// 声明切片,把长度去除就是切片
var slice = []int{1,2,3}
fmt.Println(slice)

4.2 基于数组定义切片

由于切片的底层就是一个数组,所以我们可以基于数组来定义切片

// 基于数组定义切片
a := [5]int {55,56,57,58,59}
// 获取数组所有值,返回的是一个切片
b := a[:]
// 从数组获取指定的切片
c := a[1:4]
// 获取 下标3之前的数据(不包括3)
d := a[:3]
// 获取下标3以后的数据(包括3)
e := a[3:]

运行结果

[55 56 57 58 59]
[55 56 57 58 59]
[56 57 58]
[55 56 57]
[58 59]

同理,我们不仅可以对数组进行切片,还可以切片在切片

4.3 切片的长度和容量

切片拥有自己的长度和容量,我们可以通过使用内置的len()函数求长度,使用内置的cap() 函数求切片的容量。

切片的长度就是它所包含的元素个数。

切片的容量是从它的第一个元素开始数,到其底层数组元素末尾的个数。切片s的长度和容量可通过表达式len(s)cap(s)来获取。

举例

// 长度和容量
s := []int {2,3,5,7,11,13}
fmt.Printf("长度%d 容量%d\n", len(s), cap(s))

ss := s[2:]
fmt.Printf("长度%d 容量%d\n", len(ss), cap(ss))

sss := s[2:4]
fmt.Printf("长度%d 容量%d\n", len(sss), cap(sss))

运行结果

长度6 容量6
长度4 容量4
长度2 容量4

为什么最后一个容量不一样呢,因为我们知道,经过切片后sss = [5, 7] 所以切片的长度为2,但是一因为容量是从2的位置一直到末尾,所以为4

4.4 切片的本质

切片的本质就是对底层数组的封装,它包含了三个信息:

  • 底层数组的指针
  • 切片的长度(len)
  • 切片的容量(cap)

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

4.5 使用make函数构造切片

我们上面都是基于数组来创建切片的,如果需要动态的创建一个切片,我们就需要使用内置的make函数,格式如下:

make ([]T, size, cap)

其中:

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

举例:

// make()函数创建切片
fmt.Println()
var slices = make([]int, 4, 8)
//[0 0 0 0]
fmt.Println(slices)
// 长度:4, 容量8
fmt.Printf("长度:%d, 容量%d", len(slices), cap(slices))

需要注意的是,golang中没办法通过下标来给切片扩容,如果需要扩容,需要用到append

slices2 := []int{1,2,3,4}
slices2 = append(slices2, 5)
fmt.Println(slices2)
// 输出结果 [1 2 3 4 5]

同时切片还可以将两个切片进行合并

// 合并切片
slices3 := []int{6,7,8}
slices2 = append(slices2, slices3...)
fmt.Println(slices2)
// 输出结果  [1 2 3 4 5 6 7 8]

需要注意的是,切片会有一个扩容操作,当元素存放不下的时候,会将原来的容量扩大两倍。

4.6 使用copy()函数复制切片

前面我们知道,切片就是引用数据类型

  • 值类型:改变变量副本的时候,不会改变变量本身
  • 引用类型:改变变量副本值的时候,会改变变量本身的值

如果我们需要改变切片的值,同时又不想影响到原来的切片,那么就需要用到copy函数

// 需要复制的切片
var slices4 = []int{1,2,3,4}
// 使用make函数创建一个切片
var slices5 = make([]int, len(slices4), len(slices4))
// 拷贝切片的值
copy(slices5, slices4)
// 修改切片
slices5[0] = 4
fmt.Println(slices4)
fmt.Println(slices5)

运行结果为

[1 2 3 4]
[4 2 3 4]

4.7 删除切片中的值

Go语言中并没有删除切片元素的专用方法,我们可以利用切片本身的特性来删除元素。代码如下

// 删除切片中的值
var slices6 = []int {0,1,2,3,4,5,6,7,8,9}
// 删除下标为1的值
slices6 = append(slices6[:1], slices6[2:]...)
fmt.Println(slices6)

运行结果

[0 2 3 4 5 6 7 8 9]

4.8 切片的排序算法以及sort包

编写一个简单的冒泡排序算法

func main() {
	var numSlice = []int{9,8,7,6,5,4}
	for i := 0; i < len(numSlice); i++ {
		flag := false
		for j := 0; j < len(numSlice) - i - 1; j++ {
			if numSlice[j] > numSlice[j+1] {
				var temp = numSlice[j+1]
				numSlice[j+1] = numSlice[j]
				numSlice[j] = temp
				flag = true
			}
		}
		if !flag {
			break
		}
	}
	fmt.Println(numSlice)
}

在来一个选择排序

// 编写选择排序
var numSlice2 = []int{9,8,7,6,5,4}
for i := 0; i < len(numSlice2); i++ {
    for j := i + 1; j < len(numSlice2); j++ {
        if numSlice2[i] > numSlice2[j] {
            var temp = numSlice2[i]
            numSlice2[i] = numSlice2[j]
            numSlice2[j] = temp
        }
    }
}
fmt.Println(numSlice2)

对于int、float64 和 string数组或是切片的排序,go分别提供了sort.Ints()、sort.Float64s() 和 sort.Strings()函数,默认都是从小到大进行排序

var numSlice2 = []int{9,8,7,6,5,4}
sort.Ints(numSlice2)
fmt.Println(numSlice2)

对于降序排列,Golang的sort包可以使用 sort.Reverse(slic e) 来调换slice.Interface.Less,也就是比较函数,所以int、float64 和 string的逆序排序函数可以这样写:

// 逆序排列
var numSlice4 = []int{9,8,4,5,1,7}
sort.Sort(sort.Reverse(sort.IntSlice(numSlice4)))
fmt.Println(numSlice4)

5.关于nil的认识

当你声明了一个变量,但却还并没有赋值时,golang中会自动给你的变量赋值一个默认的零值。这是每种类型对应的零值。

  • bool:false
  • numbers:0
  • string:""
  • pointers:nil
  • slices:nil
  • maps:nil
  • channels:nil
  • functions:nil

nil表示空,也就是数组初始化的默认值就是nil

var slice2 [] int
fmt.Println(slice2 == nil)

运行结果

true

6.Golang map详解

6.1 map的介绍

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

Go语言中map的定义语法如下:

map[KeyType]ValueType

其中:

  • KeyType:表示键的类型
  • ValueType:表示键对应的值的类型

map类型的变量默认初始值为nil,需要使用make()函数来分配内存。语法为:

make:用于slice、map和channel的初始化

示例如下所示:

// 方式1初始化
var userInfo = make(map[string]string)
userInfo["userName"] = "zhangsan"
userInfo["age"] = "20"
userInfo["sex"] = "男"
fmt.Println(userInfo)
fmt.Println(userInfo["userName"])
// 创建方式2,map也支持声明的时候填充元素
var userInfo2 = map[string]string {
    "username":"张三",
    "age":"21",
    "sex":"女",
}
fmt.Println(userInfo2)

6.2 遍历map

使用for range遍历

// 遍历map
for key, value := range userInfo2 {
    fmt.Println("key:", key, " value:", value)
}

6.3 判断map中某个键值是否存在

我们在获取map的时候,会返回两个值,也可以是返回的结果,一个是是否有该元素

// 判断是否存在,如果存在  ok = true,否则 ok = false
value, ok := userInfo2["username2"]
fmt.Println(value, ok)

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

使用delete()内建函数从map中删除一组键值对,delete函数的格式如下所示

delete(map 对象, key)

其中:

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

示例代码如下

// 删除map数据里面的key,以及对应的值
delete(userInfo2, "sex")
fmt.Println(userInfo2)

6.5 元素为map类型的切片

我们想要在切片里面存放一系列用户的信息,这时候我们就可以定义一个元素为map类型的切片

// 切片在中存放map
var userInfoList = make([]map[string]string, 3, 3)
var user = map[string]string{
    "userName": "张安",
    "age": "15",
}
var user2 = map[string]string{
    "userName": "张2",
    "age": "15",
}
var user3 = map[string]string{
    "userName": "张3",
    "age": "15",
}
userInfoList[0] = user
userInfoList[1] = user2
userInfoList[2] = user3
fmt.Println(userInfoList)

for _, item := range userInfoList {
    fmt.Println(item)
}

6.6 值为切片类型的map

我们可以在map中存储切片

// 将map类型的值
var userinfo = make(map[string][]string)
userinfo["hobby"] = []string {"吃饭", "睡觉", "敲代码"}
fmt.Println(userinfo)

示例,统计字符串中单词出现的次数

// 写一个程序,统计一个字符串中每个单词出现的次数。比如 "how do you do"
var str = "how do you do"
array := strings.Split(str, " ")
fmt.Println(array)
countMap := make(map[string]int)
for _, item := range array {
    countMap[item]++
}
fmt.Println(countMap)

7.Golang的函数

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

Go语言支持:函数、匿名函数和闭包。

7.1 函数定义

Go语言中定义函数使用func关键字,具体格式如下:

func 函数名(参数)(返回值) {
    函数体
}

示例

// 求两个数的和
func sumFn(x int, y int) int{
	return x + y
}
// 调用方式
sunFn(1, 2)

获取可变的参数,可变参数是指函数的参数数量不固定。Go语言中的可变参数通过在参数名后面加... 来标识。

注意:可变参数通常要作为函数的最后一个参数

func sunFn2(x ...int) int {
	sum := 0
	for _, num := range x {
		sum = sum + num
	}
	return sum
}
// 调用方法
sunFn2(1, 2, 3, 4, 5, 7)

方法多返回值,Go语言中函数支持多返回值,同时还支持返回值命名,函数定义时可以给返回值命名,并在函数体中直接使用这些变量,最后通过return关键字返回

// 方法多返回值
func sunFn4(x int, y int)(sum int, sub int) {
	sum = x + y
	sub = x -y
	return
}

7.2 函数类型和变量

定义函数类型
我们可以使用type关键字来定义一个函数类型,具体格式如下

type calculation func(int, int) int

上面语句定义了一个calculation类型,它是一种函数类型,这种函数接收两个int类型的参数并且返回一个int类型的返回值。

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

type calc func(int, int) int
// 求两个数的和
func sumFn(x int, y int) int{
	return x + y
}
func main() {
    var c calc
    c = add
}

方法作为参数

/**
	传递两个参数和一个方法
 */
func sunFn (a int, b int, sum func(int, int)int) int {
	return sum(a, b)
}

或者使用switch定义方法,这里用到了匿名函数

// 返回一个方法
type calcType func(int, int)int
func do(o string) calcType {
	switch o {
		case "+":
			return func(i int, i2 int) int {
				return i + i2
			}
		case "-":
			return func(i int, i2 int) int {
				return i - i2
			}
		case "*":
			return func(i int, i2 int) int {
				return i * i2
			}
		case "/":
			return func(i int, i2 int) int {
				return i / i2
			}
		default:
			return nil

	}
}

func main() {
	add := do("+")
	fmt.Println(add(1,5))
}

7.3 匿名函数

函数当然还可以作为返回值,但是在Go语言中,函数内部不能再像之前那样定义函数了,只能定义匿名函数。匿名函数就是没有函数名的函数,匿名函数的定义格式如下

func (参数)(返回值) {
    函数体
}

匿名函数因为没有函数名,所以没有办法像普通函数那样调用,所以匿名函数需要保存到某个变量或者作为立即执行函数:

func main() {
	func () {
		fmt.Println("匿名自执行函数")
	}()
}

Golang中的闭包

7.4 全局变量和局部变量

全局变量的特点:

  • 常驻内存
  • 污染全局

局部变量的特点

  • 不常驻内存
  • 不污染全局

闭包

  • 可以让一个变量常驻内存
  • 可以让一个变量不污染全局

闭包可以理解成 “定义在一个函数内部的函数”。在本质上,闭包就是将函数内部 和 函数外部连接起来的桥梁。或者说是函数和其引用环境的组合体。

  • 闭包是指有权访问另一个函数作用域中的变量的函数
  • 创建闭包的常见的方式就是在一个函数内部创建另一个函数,通过另一个函数访问这个函数的局部变量

注意:由于闭包里作用域返回的局部变量资源不会被立刻销毁,所以可能会占用更多的内存,过度使用闭包会导致性能下降,建议在非常有必要的时候才使用闭包。

// 闭包的写法:函数里面嵌套一个函数,最后返回里面的函数就形成了闭包
func adder() func() int {
	var i = 10
	return func() int {
		return i + 1
	}
}

func main() {
	var fn = adder()
	fmt.Println(fn())
	fmt.Println(fn())
	fmt.Println(fn())
}

最后输出的结果

11
11
11

另一个闭包的写法,让一个变量常驻内存,不污染全局

func adder2() func(y int) int {
	var i = 10
	return func(y int) int {
		i = i + y
		return i
	}
}

func main() {
	var fn2 = adder2()
	fmt.Println(fn2(10))
	fmt.Println(fn2(10))
	fmt.Println(fn2(10))
}

7.5 defer语句

Go 语言中的defer 语句会将其后面跟随的语句进行延迟处理。在defer归属的函数即将返回时,将延迟处理的语句按defer定义的逆序进行执行,也就是说,先被defer的语句最后被执行,最后被defer的语句,最先被执行。

// defer函数
fmt.Println("1")
defer fmt.Println("2")
fmt.Println("3")
fmt.Println("4")

defer将会延迟执行

1
3
4
2

如果有多个defer修饰的语句,将会逆序进行执行

// defer函数
fmt.Println("1")
defer fmt.Println("2")
defer fmt.Println("3")
fmt.Println("4")

运行结果

1
4
3
2

如果需要用defer运行一系列的语句,那么就可以使用匿名函数

func main() {
	fmt.Println("开始")
	defer func() {
		fmt.Println("1")
		fmt.Println("2")
	}()
	fmt.Println("结束")
}

运行结果

开始
结束
1
2

defer执行时机

在Go语言的函数中return语句在底层并不是原子操作,它分为返回值赋值和RET指令两步。而defer语句执行的时机就在返回值赋值操作后,RET指令执行前,具体如下图所示

在这里插入图片描述

7.6 panic/revocer处理异常

Go语言中是没有异常机制,但是使用panic / recover模式来处理错误

panic:可以在任何地方引发
recover:只有在defer调用函数内有效
func fn1() {
	fmt.Println("fn1")
}

func fn2() {
	panic("抛出一个异常")
}
func main() {
    fn1()
	fn2()
    fmt.Println("结束")
}

上述程序会直接抛出异常,无法正常运行

fn1
panic: 抛出一个异常

解决方法就是使用 recover进行异常的监听

func fn1() {
	fmt.Println("fn1")
}

func fn2() {
	// 使用recover监听异常
	defer func() {
		err := recover()
		if err != nil {
			fmt.Println(err)
		}
	}()
	panic("抛出一个异常")
}
func main() {
    fn1()
	fn2()
    fmt.Println("结束")
}

异常运用场景

模拟一个读取文件的方法,这里可以主动发送使用panic 和 recover

func readFile(fileName string) error {
	if fileName == "main.go" {
		return nil
	} else {
		return errors.New("读取文件失败")
	}
}

func myFn () {
	defer func() {
		e := recover()
		if e != nil {
			fmt.Println("给管理员发送邮件")
		}
	}()
	err := readFile("XXX.go")
	if err != nil {
		panic(err)
	}
}

func main() {
	myFn()
}

7.7 内置函数

内置函数介绍
close主要用来关闭channel
len用来求长度,比如string、array、slice、map、channel
new用来分配内存、主要用来分配值类型,比如 int、struct ,返回的是指针
make用来分配内存,主要用来分配引用类型,比如chan、map、slice
append用来追加元素到数组、slice中
panic\recover用来处理错误

7.8 new和make函数

需要注意的是,指针必须在创建内存后才可以使用,这个和 slice 和 map是一样的

// 引用数据类型map、slice等,必须使用make分配空间,才能够使用
var userInfo = make(map[string]string)
userInfo["userName"] = "zhangsan"
fmt.Println(userInfo)

var array = make([]int, 4, 4)
array[0] = 1
fmt.Println(array)

对于指针变量来说

// 指针变量初始化
var a *int
*a = 100
fmt.Println(a)

执行上面的代码会引发panic,为什么呢?在Go语言中对于引用类型的变量,我们在使用的时候不仅要声明它,还要为它分配内存空间,否则我们的值就没办法存储。而对于值类型的声明不需要分配内存空间,是因为它们在声明的时候已经默认分配好了内存空间。要分配内存,就引出来今天的new和make。Go 语言中new和make是内建的两个函数,主要用来分配内存。

这个时候,我们就需要使用new关键字来分配内存,new是一个内置的函数,它的函数签名如下:

func new(Type) *Type

其中

  • Type表示类型,new函数只接受一个参数,这个参数是一个类型
  • *Type表示类型指针,new函数返回一个指向该类型内存地址的指针
    实际开发中new函数不太常用,使用new函数得到的是一个类型的指针,并且该指针对应的值为该类型的零值。举个例子:
// 使用new关键字创建指针
aPoint := new(int)
bPoint := new(bool)
fmt.Printf("%T \n", aPoint)
fmt.Printf("%T \n", bPoint)
fmt.Println(*aPoint)
fmt.Println(*bPoint)

本节开始的示例代码中 var a *int 只是声明了一个指针变量a但是没有初始化,指针作为引用类型需要初始化后才会拥有内存空间,才可以给它赋值。应该按照如下方式使用内置的

make和new的区别

  • 两者都是用来做内存分配的
  • make只能用于slice、map以及channel的初始化,返回的还是这三个引用类型的本身
  • 而new用于类型的内存分配,并且内存赌赢的值为类型的零值,返回的是指向类型的指针

8.Golang中的指针

Golang中的指针和C语言差不多,不做赘述。

9.Golang中的日期函数

9.1 time包

时间和日期是我们编程中经常会用到的,在golang中time包提供了时间的显示和测量用的函数。

time.Now获取当前时间
timeObj := time.Now()
year := timeObj.Year()
month := timeObj.Month()
day := timeObj.Day()
fmt.Printf("%d-%02d-%02d \n", year, month, day)

9.2 格式化日期

时间类型有一个自带的方法 Format进行格式化

需要注意的是Go语言中格式化时间模板不是长久的 Y-m-d H:M:S

而是使用Go的诞生时间 2006年1月2日 15点04分 (记忆口诀:2006 1 2 3 4 5)

/**
		时间类型有一个自带的方法 Format进行格式化
		需要注意的是Go语言中格式化时间模板不是长久的 Y-m-d H:M:S
		而是使用Go的诞生时间 2006年1月2日 15点04分 (记忆口诀:2006 1 2 3 4 5)
	 */
timeObj2 := time.Now()
// 24小时值  (15表示二十四小时)
fmt.Println(timeObj2.Format("2006-01-02 15:04:05"))
// 12小时制
fmt.Println(timeObj2.Format("2006-01-02 03:04:05"))

9.3 获取当前时间戳

时间戳是自1070年1月1日(08:00:00GMT)至当前时间的总毫秒数。它也被称为Unix时间戳

/**
	获取当前时间戳
	 */
timeObj3 := time.Now()
// 获取毫秒时间戳
unixTime := timeObj3.Unix()
// 获取纳秒时间戳
unixNaTime := timeObj3.UnixNano()

9.4 时间戳转日期字符串

通过将时间戳我们可以转换成日期字符串

// 时间戳转换年月日时分秒(一个参数是秒,另一个参数是毫秒)
var timeObj4 = time.Unix(1595289901, 0)
var timeStr = timeObj4.Format("2006-01-02 15:04:05")
fmt.Println(timeStr)

9.5 日期字符串转换成时间戳

// 日期字符串转换成时间戳
var timeStr2 = "2020-07-21 08:10:05";
var tmp = "2006-01-02 15:04:05"
timeObj5, _ := time.ParseInLocation(tmp, timeStr2, time.Local)
fmt.Println(timeObj5.Unix())

9.6 时间间隔

time.Durationtime包定义的一个类型,它代表两个时间点之间经过的时间,以纳秒为单位。time.Duration表示一段时间间隔,可表示的最大长度段大约290年。

time包中定义的时间间隔类型的常量如下:

image-20200721081402315

9.7 时间操作函数

我们在日常的编码过程中可能会遇到要求时间+时间间隔的需求,Go语言的时间对象有提供Add方法如下

func (t Time) Add(d Duration)Time

例如

// 时间相加
now := time.Now()
// 当前时间加1个小时后
later := now.Add(time.Hour)
fmt.Println(later)

同理的方法还有:时间差、判断相等

9.8 定时器

方式1:使用time.NewTicker(时间间隔)来设置定时器

// 定时器, 定义一个1秒间隔的定时器
ticker := time.NewTicker(time.Second)
n := 0
for i := range ticker.C {
    fmt.Println(i)
    n++
    if n>5 {
        // 终止定时器
        ticker.Stop()
        return
    }
}

方式2:time.Sleep(time.Second)来实现定时器

for  {
    time.Sleep(time.Second)
    fmt.Println("一秒后")
}

10.Type关键字

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

type myInt int

上面代码表示:将mylnt定义为int类型,通过type关键字的定义,mylnt就是一种新的类型,它具有int的特性。

示例:如下所示,我们定义了一个myInt类型

type myInt int
func main() {
	var a myInt = 10
	fmt.Printf("%v %T", a, a)
}

输出查看它的值以及类型,能够发现该类型就是myInt类型

10 main.myInt

除此之外,我们还可以定义一个方法类型

type myFn func(x,y int) int

func fun(x int, y int)int {
	return x + y
}
func main() {
	var fn myFn = fun
	fmt.Println(fn(1, 2))
}

然后调用并输出

3

类型别名

type关键字是Golang1.9版本以后添加的新功能,又叫"类型别名"

类型别名规定:TypeAlias只是Type的别名,本质上TypeAlias与Type是同一个类型。就像一个孩子小时候有大名、小名、英文名,但这些名字都指的是他本人
 
type TypeAlias = Type

我们之前见过的rune 和 byte 就是类型别名,他们的底层代码如下

type byte = uint8
type rune = int32

11.Golang中的结构体

Golang中没有“类”的概念,Golang中的结构体和其他语言中的类有点相似。和其他面向对象语言中的类相比,Golang中的结构体具有更高的扩展性和灵活性。(整体也和C语言中的结构体类似,注意下述差别即可)

11.1 结构体的定义

使用type 和 struct关键字来定义结构体,具体代码格式如下所示:

/**
	定义一个人结构体
 */
type Person struct {
	name string
	age int
	sex string
}
func main() {
	// 实例化结构体
	var person Person
	person.name = "张三"
	person.age = 20
	person.sex = "男"
	fmt.Printf("%#v", person)
}

注意:结构体首字母可以大写也可以小写,大写表示这个结构体是公有的,在其它的包里面也可以使用,小写表示结构体属于私有的,在其它地方不能使用,例如:

type Person struct {
	Name string
	Age int
	Sex string
}

11.2 实例化结构体

实例化结构体1
刚刚实例化结构体用到了:var person Person

// 实例化结构体
var person Person
person.name = "张三"
person.age = 20
person.sex = "男"

实例化结构体2
我们下面使用另外一个方式来实例化结构体,通过new关键字来实例化结构体,得到的是结构体的地址,格式如下

var person2 = new(Person)
person2.name = "李四"
person2.age = 30
person2.sex = "女"
fmt.Printf("%#v", person2)

输出如下所示,从打印结果可以看出person2是一个结构体指针

&main.Person{name:"李四", age:30, sex:"女"}

需要注意:在Golang中支持对结构体指针直接使用,来访问结构体的成员

person2.name = "李四"
// 等价于
(*person2).name = "李四"

实例化结构体3
使用&对结构体进行取地址操作,相当于对该结构体类型进行了一次new实例化操作

// 第三种方式实例化
var person3 = &Person{}
person3.name = "赵四"
person3.age = 28
person3.sex = "男"
fmt.Printf("%#v", person3)

实例化结构体4
使用键值对的方式来实例化结构体,实例化的时候,可以直接指定对应的值

// 第四种方式初始化
var person4 = Person{
    name: "张三",
    age: 10,
    sex: "女",
}
fmt.Printf("%#v", person4)

实例化结构体5
第五种和第四种差不多,不过是用了取地址,然后返回的也是一个地址

// 第五种方式初始化
var person5 = &Person{
    name: "孙五",
    age: 10,
    sex: "女",
}
fmt.Printf("%#v", person5)

实例化结构体6
第六种方式是可以简写结构体里面的key

var person6 = Person{
    "张三",
    5,
    "女",
}
fmt.Println(person6)

11.3 结构体方法和接收者

在go语言中,没有类的概念但是可以给类型(结构体,自定义类型)定义方法。所谓方法就是定义了接收者的函数。接收者的概念就类似于其他语言中的this 或者self

方法的定义格式如下:

func (接收者变量 接收者类型) 方法名(参数列表)(返回参数) {
    函数体
}

其中

  • 接收者变量:接收者中的参数变量名在命名时,官方建议使用接收者类型名的第一个小写字母,而不是self、this之类的命名。例如,Person类型的接收者变量应该命名为p,Connector类型的接收者变量应该命名为c等。、
  • 接收者类型:接收者类型和参数类似,可以是指针类型和非指针类型。
    • 非指针类型:表示不修改结构体的内容
    • 指针类型:表示修改结构体中的内容
  • 方法名、参数列表、返回参数:具体格式与函数定义相同

如果示例所示:

/**
	定义一个人结构体
 */
type Person struct {
	name string
	age int
	sex string
}

// 定义一个结构体方法
func (p Person) PrintInfo() {
	fmt.Print(" 姓名: ", p.name)
	fmt.Print(" 年龄: ", p.age)
	fmt.Print(" 性别: ", p.sex)
	fmt.Println()
}
func (p *Person) SetInfo(name string, age int, sex string)  {
	p.name = name
	p.age = age
	p.sex = sex
}

func main() {
	var person = Person{
		"张三",
		18,
		"女",
	}
	person.PrintInfo()
	person.SetInfo("李四", 18, "男")
	person.PrintInfo()
}

运行结果为:

 姓名: 张三 年龄: 18 性别: 女
 姓名: 李四 年龄: 18 性别: 男

注意,因为结构体是值类型,所以我们修改的时候,必须是传入的指针

func (p *Person) SetInfo(name string, age int, sex string)  {
	p.name = name
	p.age = age
	p.sex = sex
}

11.4 给任意类型添加方法

在Go语言中,接收者的类型可以是任何类型,不仅仅是结构体,任何类型都可以拥有方法。

举个例子,我们基于内置的int类型使用type关键字可以定义新的自定义类型,然后为我们的自定义类型添加方法。

type myInt int
func fun(x int, y int)int {
	return x + y
}
func (m myInt) PrintInfo()  {
	fmt.Println("我是自定义类型里面的自定义方法")
}
func main() {
	var a myInt = 10
	fmt.Printf("%v %T \n", a, a)
	a.PrintInfo()
}

11.5 结构体的匿名字段

结构体允许其成员字段在声明时没有字段名而只有类型,这种没有名字的字段就被称为匿名字段

匿名字段默认采用类型名作为字段名,结构体要求字段名称必须唯一,因此一个结构体中同种类型的匿名字段只能一个

/**
	定义一个人结构体
 */
type Person struct {
	string
	int
}

func main() {
	// 结构体的匿名字段
	var person = Person{
		"张三",
		18
	}
}

结构体的字段类型可以是:基本数据类型,也可以是切片、Map 以及结构体

如果结构体的字段类似是:指针、slice、和 map 的零值都是nil,即还没有分配空间

如果需要使用这样的字段,需要先make,才能使用

/**
	定义一个人结构体
 */
type Person struct {
	name string
	age int
	hobby []string
	mapValue map[string]string
}

func main() {
	// 结构体的匿名字段
	var person = Person{}
	person.name = "张三"
	person.age = 10

	// 给切片申请内存空间
	person.hobby = make([]string, 4, 4)
	person.hobby[0] = "睡觉"
	person.hobby[1] = "吃饭"
	person.hobby[2] = "打豆豆"

	// 给map申请存储空间
	person.mapValue = make(map[string]string)
	person.mapValue["address"] = "北京"
	person.mapValue["phone"] = "123456789"

	// 加入#打印完整信息
	fmt.Printf("%#v", person)
}

同时还支持结构体的嵌套,如下所示

// 用户结构体
type User struct {
	userName string
	password string
	sex string
	age int
	address Address // User结构体嵌套Address结构体
}

// 收货地址结构体
type Address struct {
	name string
	phone string
	city string
}

func main() {
	var u User
	u.userName = "moguBlog"
	u.password = "123456"
	u.sex = "男"
	u.age = 18
	
	var address Address
	address.name = "张三"
	address.phone = "110"
	address.city = "北京"
	u.address = address
	fmt.Printf("%#v", u)
}

嵌套结构体的字段名冲突

嵌套结构体内部可能存在相同的字段名,这个时候为了避免歧义,需要指定具体的内嵌结构体的字段。(例如,父结构体中的字段 和 子结构体中的字段相似)

默认会从父结构体中寻找,如果找不到的话,再去子结构体中在找

如果子类的结构体中,同时存在着两个相同的字段,那么这个时候就会报错了,因为程序不知道修改那个字段的为准。

11.6 结构体的继承

结构体的继承,其实就类似于结构体的嵌套,区别在于嵌套是另一结构体作为该结构体的一属性,需要指明属性名称和类型,而继承没有属性名,只有结构体类型名;继承后子结构体将拥有父结构体的特性。如下所示,我们定义了两个结构体,分别是Animal 和 Dog,其中每个结构体都有各自的方法,然后通过Dog结构体 继承于 Animal结构体

// 用户结构体
type Animal struct {
	name string
}
func (a Animal) run() {
	fmt.Printf("%v 在运动 \n", a.name)
}
// 子结构体
type Dog struct {
	age int
	// 通过结构体嵌套,完成继承
	Animal
}
func (dog Dog) wang()  {
	fmt.Printf("%v 在汪汪汪 \n", dog.name)
}

func main() {
	var dog = Dog{
		age: 10,
		Animal: Animal{
			name: "阿帕奇",
		},
	}
	dog.run();
	dog.wang();
}

运行后,发现Dog拥有了父类的方法

阿帕奇 在运动 
阿帕奇 在汪汪汪

11.7 Go中的结构体和Json相互转换

Golang JSON序列化是指把结构体数据转化成JSON格式的字符串,Golang JSON的反序列化是指把JSON数据转化成Golang中的结构体对象

Golang中的序列化和反序列化主要通过“encoding/json”包中的 json.Marshal()son.Unmarshal()

// 定义一个学生结构体,注意结构体的首字母必须大写,代表公有,否则将无法转换
type Student struct {
	ID string
	Gender string
	Name string
	Sno string
}
func main() {
	var s1 = Student{
		ID: "12",
		Gender: "男",
		Name: "李四",
		Sno: "s001",
	}
	// 结构体转换成Json(返回的是byte类型的切片)
	jsonByte, _ := json.Marshal(s1)
	jsonStr := string(jsonByte)
	fmt.Printf(jsonStr)
}

将字符串转换成结构体类型

// 定义一个学生结构体,注意结构体的首字母必须大写,代表公有,否则将无法转换
type Student struct {
	ID string
	Gender string
	Name string
	Sno string
}
func main() {
	// Json字符串转换成结构体
	var str = `{"ID":"12","Gender":"男","Name":"李四","Sno":"s001"}`
	var s2 = Student{}
	// 第一个是需要传入byte类型的数据,第二参数需要传入转换的地址
	err := json.Unmarshal([]byte(str), &s2)
	if err != nil {
		fmt.Printf("转换失败 \n")
	} else {
		fmt.Printf("%#v \n", s2)
	}
}

注意

我们想要实现结构体转换成字符串,必须保证结构体中的字段是公有的,也就是首字母必须是大写的,这样才能够实现结构体 到 Json字符串的转换。

11.8 结构体标签Tag

Tag是结构体的元信息,可以在运行的时候通过反射的机制读取出来。Tag在结构体字段的后方定义,由一对反引号包裹起来,具体的格式如下:

key1:"value1" key2:"value2"

结构体tag由一个或多个键值对组成。键与值使用冒号分隔,值用双引号括起来。同一个结构体字段可以设置多个键值对tag,不同的键值对之间使用空格分隔。

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

如下所示,我们通过tag标签,来转换字符串的key

// 定义一个Student体,使用结构体标签
type Student2 struct {
	Id string `json:"id"` // 通过指定tag实现json序列化该字段的key
	Gender string `json:"gender"`
	Name string `json:"name"`
	Sno string `json:"sno"`
}
func main() {
	var s1 = Student2{
		Id: "12",
		Gender: "男",
		Name: "李四",
		Sno: "s001",
	}
	// 结构体转换成Json
	jsonByte, _ := json.Marshal(s1)
	jsonStr := string(jsonByte)
	fmt.Println(jsonStr)

	// Json字符串转换成结构体
	var str = `{"Id":"12","Gender":"男","Name":"李四","Sno":"s001"}`
	var s2 = Student2{}
	// 第一个是需要传入byte类型的数据,第二参数需要传入转换的地址
	err := json.Unmarshal([]byte(str), &s2)
	if err != nil {
		fmt.Printf("转换失败 \n")
	} else {
		fmt.Printf("%#v \n", s2)
	}
}

11.9 嵌套结构体和Json序列化反序列化

和刚刚类似,我们同样也是使用的是json.Marshal()

// 嵌套结构体 到 Json的互相转换

// 定义一个Student结构体
type Student3 struct {
	Id int
	Gender string
	Name string
}

// 定义一个班级结构体
type Class struct {
	Title string
	Students []Student3
}

func main() {
	var class = Class{
		Title: "1班",
		Students: make([]Student3, 0),
	}
	for i := 0; i < 10; i++ {
		s := Student3{
			Id: i + 1,
			Gender: "男",
			Name: fmt.Sprintf("stu_%v", i + 1),
		}
		class.Students = append(class.Students, s)
	}
	fmt.Printf("%#v \n", class)

	// 转换成Json字符串
	strByte, err := json.Marshal(class)
	if err != nil {
		fmt.Println("打印失败")
	} else {
		fmt.Println(string(strByte))
	}
}



后续笔记 Golang基础学习笔记(二) ~

参与评论 您还未登录,请先 登录 后发表或查看评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:终极编程指南 设计师:CSDN官方博客 返回首页

打赏作者

MrKorbin

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值