GoLang学习笔记
根据尚硅谷视频教学以及博客笔记,再结合自己对go的理解做的笔记,包括了golang语言的绝大部分知识点,附有代码样例便于理解,希望对你有帮助~~
文章主路线:
- go基础
- go面向对象
- io操作
- goroutine
- channel
Go基础
简介:Go是一种静态强类型、编译型语言。Go语法与C相近,但功能上有:内存安全、GC,结构形态及CSP-style并发计算。
特点:
- 从c语言中继承了很多理念,包括表达式语法,控制结构,基础数据类型,调用参数传值,指针等。
- Go语言的一个文件都要归属于一个包,而不能单独存在
- 支持垃圾自动回收机制(GC)
- 天然并发
- goroutine,轻量级线程,可实现大并发处理,高效利用多核
- 基于CPS并发模型实现
- 吸收了管道通信机制,形成Go语言特有的管道channel,通过管道channel,可以实现不同的goroute之间的通信
- 函数返回多个值
- 切片slice,延时执行defer
数据类型
基本数据类型
- 数值型:
- int(根据计算机操作位数定,64位操作系统为8个字节), int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, byte(0-255)
- rune(有符号,等价int32)
- float32,float64(float64的精度更高)
- 字符型:(没有专门的字符型,使用byte来保存单个字母字符)
- 布尔型:(bool)
- 字符串:string
派生/复杂数据类型
- 指针(Pointer)
- 数组
- 结构体(struct)
- 管道(Channel)
- 函数
- 切片(slice)
- 接口(interface)
- map集合
定义变量
package main
import (
"fmt"
"unsafe"
)
// 定义全局变量
var hobby = "唱 跳 rap"
// 多个变量时 可以这样写
var (
a = "a"
b = "b"
c = "c"
)
func main() {
// 先定义后赋值
var i int
i = 10
fmt.Println("i=", i)
// 定义并赋值
var name string = "xdj"
fmt.Println("name=", name)
// 不声明变量类型 自动推导
var sex = "男"
fmt.Println("sex=", sex)
// := 方式
age := 21
fmt.Println("age=", age)
//多变量声名
var n1, n2, n3 int = 1, 2, 3
fmt.Println("n1=", n1)
fmt.Println("n2=", n2)
fmt.Println("n3=", n3)
fmt.Println("hobby=", hobby)
fmt.Println(a, b, c)
// 查看数据类型
var n4 = 100
fmt.Printf("n1的数据类型是%T", n4)
fmt.Println()
//查看占用内存大小
fmt.Printf("n4占用字节数为%d", unsafe.Sizeof(n4))
}
浮点数据类型
package main
import "fmt"
// 浮点类型
func main() {
// 默认为float64 精度更高
var salary float32 = 99.99
fmt.Println(salary)
// 十进制形式
n1 := 5.20
n2 := .520
fmt.Println(n1, n2)
//科学计数法
n3 := 5.1234e2
fmt.Println(n3)
n4 := 5.1234e-2
fmt.Println(n4)
}
byte类型
package main
import "fmt"
// 一般用byte来保存单个字符(不是单个汉字)
func main() {
var c1 byte = 'a'
var c2 byte = '0'
// 输出的是对应的码值
fmt.Println(c1, c2)
//格式化输出
fmt.Printf("%c\n", c1)
fmt.Printf("%c\n", c2)
// 汉字可以使用int 类型存储
var c3 int = '东'
fmt.Println(c3)
fmt.Printf("%c\n", c3)
}
bool类型
package main
import "fmt"
// bool类型 true false
func main() {
// 一个字节
var b = true
fmt.Println(b)
}
string类型
package main
import "fmt"
// string 类型
func main() {
var address string = "大学城广东外语外贸大学"
fmt.Println(address)
//字符串一旦赋值了就不可以被修改
var s = "hello"
// s[0] = 'a' 这样是错误的
s = "dj"
fmt.Println(s)
// 使用反引号能够将字符串按照原来的格式输出
s1 := `
package main
import "fmt"
// string 类型
func main() {
var address string = "大学城广东外语外贸大学"
fmt.Println(address)
//字符串一旦赋值了就不可以被修改
var s = "hello"
// s[0] = 'a' 这样是错误的
s = "dj"
fmt.Println(s)
}
`
fmt.Println(s1)
// 字符串拼接
var str = "hello" + "world"
str += "dj"
fmt.Println(str)
//拼接操作很长时 注意加号要留在行尾
str4 := "hello" + "world" + "hello" + "world" +
"hello" + "world" + "hello" + "world"
fmt.Println(str4)
}
基本数据类型的转换
不同类型之间的赋值需要显示转换,不会自动转换
package main
import "fmt"
// 基本数据类型转换
func main() {
// 变量i本身的数据类型并没有变化
var i int32 = 100
// 强制转化
var n1 float32 = float32(i)
var n2 int8 = int8(i)
var n3 int64 = int64(i)
fmt.Printf("n1 = %v; n2 = %v; n3 = %v\n", n1, n2, n3)
// 大的数据类型转化为小的数据类型不会报错 但会溢出
}
基本数据类型和string类型之间的转换
- 基本数据类型转string类型
方式1:fmt.Sprintf(“%参数”, 表达式) 会返回转换后的字符
方式2:使用strconv包的函数
package main
import (
"fmt"
"strconv"
)
// 基本数据类型转化为string类型
func main() {
var num1 int = 99
var num2 float64 = 99.9999
var b bool = true
var str string
//方式1
str = fmt.Sprintf("%d", num1)
fmt.Println(str)
str = fmt.Sprintf("%f", num2)
fmt.Println(str)
str = fmt.Sprintf("%t", b)
fmt.Println(str)
// 方式2
var num3 int = 99
var num4 float64 = 99.9999
var b1 bool = true
// 10 表示十进制
str = strconv.FormatInt(int64(num3), 10)
str = strconv.Itoa(num3)
fmt.Println(str)
// 'f'字符串格式, 10表示小数点保留10位,64表示是float64
str = strconv.FormatFloat(num4, 'f', 10, 64)
fmt.Println(str)
str = strconv.FormatBool(b1)
fmt.Println(str)
}
- string类型转基本数据类型
使用strconv函数
package main
import (
"fmt"
"strconv"
)
// string类型转基本数据类型
func main() {
var str string = "true"
var b bool
// 这个函数返回两个值 第二个值是我们不需要的可以用 _ 忽略
b, _ = strconv.ParseBool(str)
fmt.Println(b)
var str2 string = "12345"
var n1 int64
// 10 表示 十进制, 64表示int64
n1, _ = strconv.ParseInt(str2, 10, 64)
fmt.Println(n1)
var str3 string = "12.3456"
var f float64
f, _ = strconv.ParseFloat(str3, 64)
fmt.Println(f)
}
注意:
-
string转基本数据类型时,若不能够转换,不会报错,但会转化为默认值
-
var str4 string = "hello" var n2 int64 n2, _ = strconv.ParseInt(str4, 10, 64) fmt.Printf("n2=%v\n", n2) // 0 即使n2有初值转化后也变为0
-
指针
获取变量的地址
var i int = 10
fmt.Printf("i的地址为%v", &i)
指针变量存放的是一个地址,这个地址指向的空间才是值
package main
import "fmt"
func main() {
//获取变量的地址
var i int = 10
fmt.Printf("i的地址为%v\n", &i)
//指针
var p *int
p = &i
// *p表示引用
fmt.Printf("变量地址为%v; 值大小为%v", p, *p)
// 注意p本身也有一个地址 &p
}
值类型和引用类型
值类型:int系列,float系列,bool,string,数组和结构体,变量直接存储值,内存通常在栈中分配
引用类型:指针,slice切片,map,管道chan,interface,变量存储地址,内存通常在堆中分配
值类型的变量在函数内修改时不会影响原来值得大小
如果变量名、函数名、常量名首字母大写,则可以被其他的包访问;如果首字母小写,则只能在本包中使用
运算符
i++只能单独使用,而且没有++i
a := i++ // 错误的
if i++ > 0 {// 这也是错误的
}
++i // 这也是错误的
问题:有两个变量,要求进行交换,但不使用中间变量,如何实现?
var a int = 10
var b int = 20
a = a + b
b = a - b
a = a - b
go不支持三元运算符,需要使用if else
键盘输入
fmt.Scanln() 或者 fmt.Scanf()
package main
import "fmt"
func main() {
// 键盘获取输入
// 方式1
var name string
var age byte
var sal float64
var isPass bool
fmt.Println("请输入姓名 ")
_, err := fmt.Scanln(&name)
if err != nil {
return
}
fmt.Println("请输入年龄 ")
_, err = fmt.Scanln(&age)
if err != nil {
return
}
fmt.Println("请输入薪水 ")
_, err = fmt.Scanln(&sal)
if err != nil {
return
}
fmt.Println("请输入是否通过考试 ")
_, err = fmt.Scanln(&isPass)
if err != nil {
return
}
fmt.Printf("name=%v age=%v sal=%v isPass=%v\n", name, age, sal, isPass)
//方式2 空格隔开
fmt.Println("请输入学生信息 ")
_, err = fmt.Scanf("%s %d %f %t", &name, &age, &sal, &isPass)
if err != nil {
return
}
fmt.Printf("name=%v age=%v sal=%v isPass=%v\n", name, age, sal, isPass)
}
流程控制
- 顺序
- 分支
- 注意if条件没有括号
- 循环
注意:switch分支不需要加break,case后面的表达式可以有多个,用逗号隔开
switch 表达式 {
case 表达式1, 表达式2:
代码块
case 表达式3, 表达式4:
代码块
...
default:
代码块
}
switch后也可以不带表达式, 类似if - else 分支来使用
switch {
case age == 10:
代码块
}
switch穿透-fallthrough, 如果在case语句后面加fallthrough,则会继续执行下一个case,也叫switch穿透
for循环
var n = 10
for i := 0; i < n; i++ {
fmt.Println(i)
}
// 另一种写法
j := 0
for j < n {
fmt.Println(j)
j++
}
//死循环
for {
fmt.Println("dj")
// break
}
//等价
/*
for ; ; {
}
*/
使用for-range方式遍历字符串或数组
//遍历字符串 传统方式
var str string = "hello world"
for i := 0; i < len(str); i++ { // 注意使用这种方式中文会出现问题
fmt.Println(string(str[i]))
}
// for-range
for index, value := range str {
fmt.Println(index, string(value))
}
go没有while 和 do…while
// for循环实现while效果
循环变量初始化
for {
if 循环条件表达式 {
break // 跳出循环
}
循环操作语句
变量迭代
}
// for循环实现do...while效果
循环变量初始化
for {
循环操作语句
变量迭代
if 循环条件表达式 {
break // 跳出循环
}
}
函数
定义
//函数的基本语法
func 函数名(形参列表)(返回值列表){ // 形参名在前 形参类型在后 返回值可以有多个
执行语句..
return 返回值列表
}
package main
import (
"fmt"
)
func cal(n1 float64, n2 float64, operator byte) float64 {
var res float64
switch operator {
case '+':
res = n1 + n2
case '-':
res = n1 - n2
case '*':
res = n1 * n2
case '/':
res = n1 / n2
default:
fmt.Println("操作符号错误...")
}
return res
}
func main() {
//请大家完成这样一个需求:
//输入两个数,再输入一个运算符(+,-,*,/),得到结果.。
//分析思路....
var n1 float64 = 1.2
var n2 float64 = 2.3
var operator byte = '+'
result := cal(n1, n2 , operator)
fmt.Println("result~=", result)
}
函数调用机制
- 在调用一个函数时,会给该函数分配一个新的空间,编译器会通过自身的处理让这个新的空间和其它的栈的空间区分开来
- 在每个函数对应的栈中,数据空间是独立的,不会混淆
- 当一个函数调用完毕(执行完毕)后,程序会销毁这个函数对应的栈空间。
注意
-
函数的形参列表可以是多个,返回值列表也可以是多个。
-
形参列表和返回值列表的数据类型可以是值类型和引用类型。
-
函数的命名遵循标识符命名规范,首字母不能是数字,首字母大写该函数可以被本包文件和其它包文件使用,类似public, 首字母小写,只能被本包文件使用,其它包文件不能使用,类似private
-
基本数据类型和数组默认都是值传递的,即进行值拷贝。在函数内修改,不会影响到原来的值。
-
如果希望函数内的变量能修改函数外的变量(指的是默认以值传递的方式的数据类型),可以传入变量的地址&,函数内以指针的方式操作变量。从效果上看类似引用 。
-
package main import ( "fmt" ) // n1 就是 *int 类型 func test03(n1 *int) { fmt.Printf("n1的地址 %v\n",&n1) *n1 = *n1 + 10 fmt.Println("test03() n1= ", *n1) // 30 } func main() { num := 20 fmt.Printf("num的地址=%v\n", &num) test03(&num) fmt.Println("main() num= ", num) // 30 }
-
-
Go函数不支持函数重载
-
package main import ( "fmt" ) //有两个test02不支持重载 func test02(n1 int) { n1 = n1 + 10 fmt.Println("test02() n1= ", n1) } //有两个test02不支持重载 func test02(n1 int , n2 int) { } func main() { }
-
-
在Go中,函数也是一种数据类型,可以赋值给一个变量,则该变量就是一个函数类型的变量了。通过该变量可以对函数调用
-
package main import ( "fmt" ) //在Go中,函数也是一种数据类型, //可以赋值给一个变量,则该变量就是一个函数类型的变量了。通过该变量可以对函数调用 func getSum(n1 int, n2 int) int { return n1 + n2 } func main() { a := getSum fmt.Printf("a的类型%T, getSum类型是%T\n", a, getSum) res := a(10, 40) // 等价 res := getSum(10, 40) fmt.Println("res=", res) }
-
-
函数既然是一种数据类型,因此在Go中,函数可以作为形参,并且调用
-
package main import ( "fmt" ) //在Go中,函数也是一种数据类型, //可以赋值给一个变量,则该变量就是一个函数类型的变量了。通过该变量可以对函数调用 func getSum(n1 int, n2 int) int { return n1 + n2 } //函数既然是一种数据类型,因此在Go中,函数可以作为形参,并且调用 func myFun(funvar func(int, int) int, num1 int, num2 int ) int { return funvar(num1, num2) } func main() { //看案例 res2 := myFun(getSum, 50, 60) fmt.Println("res2=", res2) }
-
-
为了简化数据类型定义,Go支持自定义数据类型
-
基本语法:type 自定义数据类型名 数据类型 // 理解: 相当于一个别名
-
package main import ( "fmt" ) //在Go中,函数也是一种数据类型, //可以赋值给一个变量,则该变量就是一个函数类型的变量了。通过该变量可以对函数调用 func getSum(n1 int, n2 int) int { return n1 + n2 } //函数既然是一种数据类型,因此在Go中,函数可以作为形参,并且调用 func myFun(funvar func(int, int) int, num1 int, num2 int ) int { return funvar(num1, num2) } //再加一个案例 //这时 myFun 就是 func(int, int) int类型 type myFunType func(int, int) int //函数既然是一种数据类型,因此在Go中,函数可以作为形参,并且调用 func myFun2(funvar myFunType, num1 int, num2 int ) int { return funvar(num1, num2) } func main() { // 给int取了别名 , 在go中 myInt 和 int 虽然都是int类型,但是go认为myInt和int两个类型 type myInt int var num1 myInt // var num2 int num1 = 40 num2 = int(num1) //各位,注意这里依然需要显示转换,go认为myInt和int两个类型 fmt.Println("num1=", num1, "num2=",num2) //看案例 res3 := myFun2(getSum, 500, 600) fmt.Println("res3=", res3) }
-
-
支持对函数返回值命名
-
package main import ( "fmt" ) //支持对函数返回值命名 func getSumAndSub(n1 int, n2 int) (sum int, sub int){ sub = n1 - n2 sum = n1 + n2 return } func main() { //看案例 a1, b1 := getSumAndSub(1, 2) fmt.Printf("a=%v b=%v\n", a1, b1) }
-
-
go支持可变参数
-
//支持0到多个参数 func sum(args...int){ } //支持1到多个参数 func sum(n1 int,args... int) sum int{ }
-
args是slice切片,通过args[index]可以访问到各个值
-
如果一个函数的形参列表中有可变的参数,则可变参数需要放到形参列表的最后
-
init()函数
每一个源文件都可以包含一个 init 函数,该函数会在main函数执行前,被Go运行框架调用,也 就是说init会在main函数前被调用。
package main
import (
"fmt"
)
func init() {
fmt.Println("init()")
}
func main() {
fmt.Println("main()")
}
如果一个文件同时包含全局变量定义, init 函数和 main 函数,则执行的流程全局变量定义 - >init函数 - >main 函数
-
package main import ( "fmt" ) var age = test() //为了看到全局变量是先被初始化的,我们这里先写函数 func test() int { fmt.Println("test()")//1 return 90 } // init函数,通常可以在init函数中完成初始化工作 func init() { fmt.Println("init()")//2 } func main() { fmt.Println("main()...age=",age)//3 }
匿名函数
Go支持匿名函数,匿名函数就是没有名字的函数,如果我们某个函数只是希望使用一次,可以考 虑使用匿名函数,匿名函数也可以实现多次调用。
-
使用方式1
-
package main import ( "fmt" ) func main() { //在定义匿名函数时就直接调用,这种方式匿名函数只能调用一次 //案例演示,求两个数的和, 使用匿名函数的方式完成 res1 := func (n1 int, n2 int) int { return n1 + n2 }(10, 20) fmt.Println("res1=", res1) }
-
-
使用方式2
-
package main import ( "fmt" ) func main() { //将匿名函数func (n1 int, n2 int) int赋给 a变量 //则a 的数据类型就是函数类型 ,此时,我们可以通过a完成调用 a := func (n1 int, n2 int) int { return n1 - n2 } res2 := a(10, 30) fmt.Println("res2=", res2) res3 := a(90, 30) fmt.Println("res3=", res3) }
-
-
全局匿名函数
-
如果将匿名函数赋给一个全局变量,那么这个匿名函数,就成为一个全局匿名函数,可以在程序有效。
-
package main import ( "fmt" ) var ( //fun1就是一个全局匿名函数 Fun1 = func (n1 int, n2 int) int { return n1 * n2 } ) func main() { //全局匿名函数的使用 res4 := Fun1(4, 9) fmt.Println("res4=", res4) }
-
包
go的每一个文件都是属于一个包的,也就是说go是以包的形式来管理文件和项目目录结构的
引入包:
import "包名"
import (
"包名"
"包名"
)
- package 指令在 文件第一行,然后是 import 指令。
- 在import 包时,路径从 $GOPATH的 src 下开始,不用带src, 编译器会自动从src下开始引入
- 为了让其它包的文件,可以访问到本包的函数,则该函数名的首字母需要大写,类似其它语言的public,这样才能跨包访问.
- 如果包名较长,Go支持给包取别名, 注意细节:取别名后,原来的包名就不能使用了
- 说明: 如果给包取了别名,则需要使用别名来访问该包的函数和变量。
- 在同一包下,不能有相同的函数名(也不能有相同的全局变量名),否则报重复定义
- 如果你要编译成一个可执行程序文件,就需要将这个包声明为 main, 即 package main.这个就 是一个语法规范,如果你是写一个库 ,包名可以自定义
闭包
基本介绍:闭包就是一个函数和与其相关的引用环境组合的一个整体(实体)
闭包让你可以在一个内层函数中访问到其外层函数的作用域。
可简单理解为:有权访问另一个函数作用域内变量的函数都是闭包。
例如:
package main
import (
"fmt"
)
//累加器
func AddUpper() func (int) int {
var n int = 10
return func (x int) int {
n = n + x
return n
}
}
func main() {
//使用前面的代码
f := AddUpper()
fmt.Println(f(1))// 11
fmt.Println(f(2))// 13
fmt.Println(f(3))// 16
}
-
AddUpper 是一个函数,返回的数据类型是 fun(int)int
-
闭包
-
var n int = 10 return func (x int) int { n = n + x return n }
-
返回的是一个匿名函数, 但是这个匿名函数引用到函数外的n,因此这个匿名函数就和n形成一个整体,构成闭包。
-
案例:
1 ) 编写一个函数 makeSuffix(suffixstring) 可以接收一个文件后缀名(比如.jpg),并返回一个闭包
2 ) 调用闭包,可以传入一个文件名,如果该文件名没有指定的后缀(比如.jpg),则返回 文件名.jpg, 如果已经有.jpg后缀,则返回原文件名。
3 ) 要求使用闭包的方式完成
4 ) strings.HasSuffix, 该函数可以判断某个字符串是否有指定的后缀。
package main
import (
"fmt"
"strings"
)
func makeSuffix(suffix string) func(string) string {
return func(name string) string {
if !strings.HasSuffix(name, suffix) {
return name + suffix
}
return name
}
}
func main() {
// 1)编写一个函数 makeSuffix(suffix string) 可以接收一个文件后缀名(比如.jpg),并返回一个闭包
// 2)调用闭包,可以传入一个文件名,如果该文件名没有指定的后缀(比如.jpg) ,则返回 文件名.jpg , 如果已经有.jpg后缀,则返回原文件名。
// 3)要求使用闭包的方式完成
// 4)strings.HasSuffix , 该函数可以判断某个字符串是否有指定的后缀。
f := makeSuffix(".jpg")
name := f("xdj")
fmt.Println(name)
}
说明:上面的匿名函数和suffix变量组成一个闭包,因为返回的函数用到了suffix
==好处:==我们体会一下闭包的好处,如果使用传统的方法,也可以轻松实现这个功能,但是传统方法需要每次都传入 后缀名,比如 .jpg,而闭包因为可以保留上次引用的某个值,所以我们传入一次就可以反复使用。(以面向对象思想理解闭包----------外部整体 像一个类,先传入的.jpg 像设置类里的一个public属性,再向返回函数传参 像调用类的成员函数,此时成员函数可以调用类里已设置的属性。)
使用闭包注意点:
因为使用闭包会包含其他函数的作用域,会比其他函数占据更多的内存空间,不会在调用结束之后被垃圾回收机制(简称GC机制)回收,多度使用闭包会过度占用内存,造成内存泄漏。
defer
在函数中,程序员经常需要创建资源(比如:数据库连接、文件句柄、锁等) ,为了在函数执行完毕后,及时的释放资源,Go的设计者提供defer(延时机制)。defer 最主要的价值是在,当函数执行完毕后,可以及时的释放函数创建的资源。
package main
import "fmt"
func sum(a, b int) int {
// defer后面的语句暂时不会执行,defer后面的语句会按顺序压栈 先进后出 当函数执行完毕再执行
defer fmt.Println("a=", a) // 第三输出
defer fmt.Println("b=", b) // 第二输出
// 在defer 将语句放入到栈时,也会将相关的值拷贝同时入栈
res := a + b
fmt.Println("res=", res) // 第一输出
return res
}
// defer 延迟机制
func main() {
res := sum(10, 20)
fmt.Println("res=", res) // 第四输出
//
}
/*
res= 30
b= 20
a= 10
res= 30
*/
defer的使用:
1 ) 在golang编程中的通常做法是,创建资源后,比如(打开了文件,获取了数据库的链接,或者是锁资源), 可以执行 defer file.Close() defer connect.Close()
2 ) 在defer后,可以继续使用创建资源.
3 ) 当函数完毕后,系统会依次从defer栈中,取出语句,关闭资源.
4 ) 这种机制,非常简洁,程序员不用再为在什么时机关闭资源而烦心。
字符串常用系统函数
1 ) 统计字符串的长度,按字节 len(str)
2 ) 字符串遍历,同时处理有中文的问题 r:=[]rune(str)
3 ) 字符串转整数: n,err:=strconv.Atoi(" 12 ")
4 ) 整数转字符串 str=strconv.Itoa( 12345 )
5 ) 字符串 转 []byte: varbytes=[]byte(“hello go”)
6.) []byte 转 字符串:str=string([]byte{ 97 , 98 , 99 })
7 ) 10 进制转 2 , 8 , 16.进制: str=strconv.FormatInt( 123 , 2 )// 2 - > 8 , 16
8 ) 查找子串是否在指定的字符串中:strings.Contains(“seafood”,“foo”)//true
9 ) 统计一个字符串有几个指定的子串 : strings.Count(“ceheese”,“e”)// 4
10 ) 不区分大小写的字符串比较(==是区分字母大小写的):fmt.Println(strings.EqualFold(“abc”,“Abc”))//true
11 )返回子串在字符串第一次出现的index值,如果没有返回- 1 :strings.Index(“NLT_abc”,“abc”)// 4
12 ) 返回子串在字符串最后一次出现的index,如没有返回- 1 :strings.LastIndex(“gogolang”,“go”)
13 ) 将指定的子串替换成 另外一个子串:strings.Replace(“gogohello”,“go”,“go语言”,n)n可以指定你希望替换几个,如果n=- 1 表示全部替换
14 ) 按照指定的某个字符,为分割标识,将一个字符串拆分成字符串数组:strings.Split(“hello,wrold,ok”,“,”)
15 ) 将字符串的字母进行大小写的转换:strings.ToLower(“Go”)//gostrings.ToUpper(“Go”)//GO
16.) 将字符串左右两边的空格去掉: strings.TrimSpace("tnalonegopherntrn ")
17 ) 将字符串左右两边指定的字符去掉 : strings.Trim(“!hello!”,“!”) //[“hello”]//将左右两边! 和 ""去掉
18 ) 将字符串左边指定的字符去掉 : strings.TrimLeft(“!hello!”,“!”) //[“hello”]//将左边! 和 " "去掉
19 ) 将字符串右边指定的字符去掉 :strings.TrimRight(“!hello!”,“!”) //[“hello”]//将右边! 和 " "去掉
20 ) 判断字符串是否以指定的字符串开头:strings.HasPrefix("ftp:// 192. 168. 10. 1 ",“ftp”)//true
21 ) 判断字符串是否以指定的字符串结束:strings.HasSuffix(“NLT_abc.jpg”,“abc”)//false
时间和日期函数
package main
import (
"fmt"
"time"
)
// 时间和日期
func main() {
// 获取当前时间
now := time.Now()
fmt.Printf("now=%v type=%T\n", now, now)
// 获取部分日期信息
// 通过now获取年份月份
fmt.Printf("年=%v\n", now.Year())
fmt.Printf("月=%v\n", now.Month())
fmt.Printf("月=%v\n", int(now.Month()))
fmt.Printf("日=%v\n", now.Day())
fmt.Printf("时=%v\n", now.Hour())
fmt.Printf("分=%v\n", now.Minute())
fmt.Printf("秒=%v\n", now.Second())
// 格式化日期
// 方式一
fmt.Printf("当前年月 %d %d %d %d:%d:%d", now.Year(), now.Month(), now.Day(), now.Hour(), now.Minute(), now.Second())
str := fmt.Sprintf("当前年月 %d %d %d %d:%d:%d", now.Year(), now.Month(), now.Day(), now.Hour(), now.Minute(), now.Second())
fmt.Println(str)
// 方式二 使用Time.Format
fmt.Println(now.Format("2006-01-02 15:04:05"))
}
时间常量
const(
Nanosecond Duration= 1 //纳秒
Microsecond = 1000 *Nanosecond //微秒
Millisecond = 1000 *Microsecond//毫秒
Second = 1000 *Millisecond//秒
Minute = 60 *Second//分钟
Hour = 60 *Minute//小时
)
// time.Minute
Unix
package main
import (
"fmt"
"strconv"
"time"
)
func test() {
str := ""
for i := 0; i < 100000; i++ {
str += "hello" + strconv.Itoa(i)
}
}
func main() {
// 记录开始时间
start := time.Now().Unix()
test()
end := time.Now().Unix()
fmt.Printf("执行test方法耗时:%v秒\n", end-start)
}
系统函数
-
len:用来求长度,比如string、array、slice、map、channel
-
new:用来分配内存,主要用来分配值类型,比如int、float 32 ,struct返回的是指针
-
package main import ( "fmt" ) func main() { num1 := 100 fmt.Printf("num1的类型%T , num1的值=%v , num1的地址%v\n", num1, num1, &num1) num2 := new(int) // *int //num2的类型%T => *int //num2的值 = 地址 0xc04204c098 (这个地址是系统分配) //num2的地址%v = 地址 0xc04206a020 (这个地址是系统分配) //num2指向的值 = 100 *num2 = 100 fmt.Printf("num2的类型%T , num2的值=%v , num2的地址%v\n num2这个指针,指向的值=%v", num2, num2, &num2, *num2) }
-
-
make:用来分配内存,主要用来分配引用类型,比如channel、map、slice。
错误处理
1 ) 在默认情况下,当发生错误后(panic),程序就会退出(崩溃.)
2 ) 如果我们希望:当发生错误后,可以捕获到错误,并进行处理,保证程序可以继续执行。还可以在捕获到错误后,给管理员一个提示(邮件,短信。。。)
package main
import (
"fmt"
"time"
)
func test() {
//使用defer + recover 来捕获和处理异常
defer func() {
err := recover() // recover()内置函数,可以捕获到异常
if err != nil { // 说明捕获到错误
fmt.Println("err=", err)
}
}()
num1 := 10
num2 := 0
res := num1 / num2
fmt.Println("res=", res)
}
func main() {
//测试
test()
for {
fmt.Println("main()下面的代码...")
time.Sleep(time.Second)
}
}
基本说明:
- Go语言追求简洁优雅,所以,Go语言不支持传统的 trycatchfinally 这种处理。
- Go中引入的处理方式为: defer , panic , recover
- 这几个异常的使用场景可以这么简单描述:Go中可以抛出一个panic的异常,然后在defer中通过recover捕获这个异常,然后正常处理
自定义错误
Go程序中,也支持自定义错误, 使用errors.New 和 panic 内置函数。
- errors.New(“错误说明”), 会返回一个error类型的值,表示一个错误
- panic内置函数 ,接收一个interface{}类型的值(也就是任何值了)作为参数。可以接收error类型的变量,输出错误信息,并退出程序.
package main
import (
"fmt"
_ "time"
"errors"
)
//函数去读取以配置文件init.conf的信息
//如果文件名传入不正确,我们就返回一个自定义的错误
func readConf(name string) (err error) {
if name == "config.ini" {
//读取...
return nil
} else {
//返回一个自定义错误
return errors.New("读取文件错误..")
}
}
func test02() {
err := readConf("config2.ini")
if err != nil {
//如果读取文件发送错误,就输出这个错误,并终止程序
panic(err)
}
fmt.Println("test02()继续执行....")
}
func main() {
//测试自定义错误的使用
test02()
fmt.Println("main()下面的代码...")
}
数组
数组
数组可以存放多个同一类型数据。数组也是一种数据类型,在Go中,数组是值类型。
语法:
var 数组名 [数组大小]数据类型
var a [5]int// 数组名 [长度]数据类型
赋初值 a[0]= 1 a[1]= 30
数组的地址分布
1 ) 数组的地址可以通过数组名来获取 &intArr
2 ) 数组的第一个元素的地址,就是数组的首地址
3 ) 数组的各个元素的地址间隔是依据数组的类型决定,比如int 64 - > 8 int 32 - > 4
数组初始化
package main
import "fmt"
// 数组初始化
func main() {
// 方式一
var arr1 [3]int = [3]int{1, 2, 3}
fmt.Println("arr1", arr1)
// 方式2 [...]会自动推导数组的大小
var arr2 = [...]int{1, 2, 3, 4, 5}
fmt.Println("arr2", arr2)
// 方式3
var arr3 = [3]int{1, 2, 3}
fmt.Println("arr3", arr3)
// 方式4 指定下标初始化 左边的为下标 以最大的下标+1作为数组大小 没有赋值的下标赋默认值
var arr4 = [...]int{1: 100, 0: 99, 2: 8888}
fmt.Println("arr4", arr4)
f := [...]int{0: 1, 4: 1, 9: 1} // [1 0 0 0 1 0 0 0 0 1]
fmt.Println(f)
e := [5]int{4: 100} // [0 0 0 0 100]
fmt.Println(e)
//类型推导
strArr05 := [...]string{1: "tom", 0: "jack", 2: "mary"}
fmt.Println("strArr05", strArr05)
}
数组遍历
package main
import (
"fmt"
)
func main() {
//演示for-range遍历数组
heroes := [...]string{"宋江", "吴用", "卢俊义"}
// 常规方式
for i := 0; i < len(heroes); i++ {
fmt.Println(heroes[i])
}
for i, v := range heroes {
fmt.Printf("i=%v v=%v\n", i, v)
fmt.Printf("heroes[%d]=%v\n", i, heroes[i])
}
for _, v := range heroes {
fmt.Printf("元素的值=%v\n", v)
}
}
说明:
1.index数组的下标
2.value该下标对应的值
3.他们都是for循环内可见的局部变量
4.如果不想使用下标index,可以替换为"_"
5.index和value的名称不是固定的。可以自己改变
注意事项
1 ) 数组是多个相同类型数据的组合,一个数组一旦声明/定义了,其长度是固定的, 不能动态变化。否则报越界
2 ) 数组中的元素可以是任何数据类型,包括值类型和引用类型,但是不能混用。
3 ) 数组创建后,如果没有赋值,有默认值(零值)
数值类型数组:默认值为 0
字符串数组: 默认值为 “”
bool数组: 默认值为 false
5 ) 使用数组的步骤 1. 声明数组并开辟空间 2 给数组各个元素赋值(默认零值) 3 使用数组
6 ) Go的数组属值类型, 在默认情况下是值传递, 因此会进行值拷贝。数组间不会相互影响
7 ) 如想在其它函数中,去修改原来的数组,可以使用引用传递(指针方式)
package main
import (
"fmt"
)
//函数
func test02(arr *[3]int) {
fmt.Printf("arr指针的地址=%p", &arr)
(*arr)[0] = 88 //!!
}
func main() {
arr := [3]int{11, 22, 33}
fmt.Printf("arr 的地址=%p", &arr)
test02(&arr)
fmt.Println("main arr=", arr)
}
8) 长度是数组类型的一部分,在传递函数参数时 需要考虑数组的长度,看下面案例
//题1
package main
import (
"fmt"
)
//默认值拷贝
func modify(arr []int) {
arr[0] = 100
fmt.Println("modify的arr",arr)
}
func main() {
var arr = [...]int{1,2,3}
modify(arr)
}
//编译错误,因为不能把[3]int 传递给[]int
//题2
package main
import (
"fmt"
)
//默认值拷贝
func modify(arr [4]int) {
arr[0] = 100
fmt.Println("modify的arr",arr)
}
func main() {
var arr = [...]int{1,2,3}
modify(arr)
}
//编译错误,因为不能把[3]int 传递给[4]int
二维数组
语法:
var 数组名 [大小][大小]类型 =[大小][大小]类型 { { 初值 },{ 初值 } }
var 数组名 [大小][大小]类型 =[...][大小]类型{{初值},{初值}}
var 数组名 = [大小][大小]类型{{初值},{初值}}
var 数组名 = [...][大小]类型{{初值},{初值}}
例如:
package main
import (
"fmt"
)
func main() {
arr3 := [2][3]int{{1,2,3}, {4,5,6}}
fmt.Println("arr3=", arr3)
}
二位数组遍历
- 双层for循环
- for-range
package main
import (
"fmt"
)
func main() {
//演示二维数组的遍历
var arr3 = [2][3]int{{1,2,3}, {4,5,6}}
//for循环来遍历
for i := 0; i < len(arr3); i++ {
for j := 0; j < len(arr3[i]); j++ {
fmt.Printf("%v\t", arr3[i][j])
}
fmt.Println()
}
//for-range来遍历二维数组
for i, v := range arr3 {
for j, v2 := range v {
fmt.Printf("arr3[%v][%v]=%v \t",i, j, v2)
}
fmt.Println()
}
}
切片
定义
1 ) 切片的英文是slice
2 ) 切片是数组的一个引用,因此切片是引用类型,在进行传递时,遵守引用传递的机制。
3 ) 切片的使用和数组类似,遍历切片、访问切片的元素和求切片长度len(slice)都一样。
4 ) 切片的长度是可以变化的,因此切片是一个可以动态变化数组。
语法
var 切片名 []类型
//比如:var a []int // 注意这里没有大小
package main
import "fmt"
// slice 切片
func main() {
// 声名一个数组 通过切片引用数组
var intArr = [...]int{1, 2, 3, 4, 5, 6, 7, 8}
// 引用
sliceArr := intArr[1:7] // 取值左闭右开
fmt.Println("sliceArr:", sliceArr) //[2 3 4 5]
fmt.Println("len:", len(sliceArr)) // 元素个数
fmt.Println("cap:", cap(sliceArr)) // 切片容量 len(intArr) - start
var slice = make([]float64, 5, 10)
slice[0] = 1.0
fmt.Println("slice:", slice) // slice: [1 0 0 0 0]
fmt.Println("len:", len(slice)) // 5
fmt.Println("cap:", cap(slice)) // 10
}
切片的内存形式
-
slice的确是一个引用类型
-
slice 从底层来说,其实就是一个数据结构(struct结构体)
-
type slice struct{ ptr *[ 2 ]int len int cap int }
切片的使用
-
方式一:定义一个切片,然后让切片去引用一个已经创建好的数组,比如前面的案例就是这样的。
-
方式二:通过 make 来创建切片
-
基本语法:
var 切片名 []type = make([]type,len,[cap])
-
type: 就是数据类型 len: 大小 cap :指定切片容量,可选,如果你分配了 cap, 则要求 cap>=len.
-
package main import ( "fmt" ) func main() { var slice []float64 = make([]float64, 5, 10) slice[1] = 10 slice[3] = 20 fmt.Println(slice) fmt.Println("slice的size=", len(slice)) fmt.Println("slice的cap=", cap(slice)) }
-
小结:
1 ) 通过make方式创建切片可以指定切片的大小和容量
2 ) 如果没有给切片的各个元素赋值,那么就会使用默认值[int,float=> 0 string=>”” bool=> false]
3 ) 通过make方式创建的切片对应的数组是由make底层维护,对外不可见,即只能通过slice去访问各个元素
-
方式三:定义一个切片,直接就指定具体数组,使用原理类似make的方式
-
package main import ( "fmt" ) func main() { var strSlice []string = []string{"tom", "jack", "mary"} fmt.Println("strSlice=", strSlice) fmt.Println("strSlice的size=", len(strSlice)) fmt.Println("strSlice的cap=", cap(strSlice)) }
-
方式 1 和方式 2 的区别**(面试)**
方式1是直接引用数组,这个数组是事先存在的,程序员是可见的
方式2是通过make来创建切片,make也会创建一个数组,是由切片在底层进行维护,程序员是看不见的
切片使用注意事项
1)从数组引用切片规则左闭合右开,即
切片初始化时 varslice=arr[startIndex:endIndex]
从arr数组下标为startIndex,取到 下标为endIndex的元素(不含arr[endIndex])。
2 ) 切片定义完后,还不能使用,因为本身是一个空的,需要让其引用到一个数组,或者make一 个空间供切片来使用
3 ) 切片可以继续切片
4 ) 用append内置函数,可以对切片进行动态追加
package main
import (
"fmt"
)
func main() {
//使用常规的for循环遍历切片
var arr [5]int = [...]int{10, 20, 30, 40, 50}
//slice := arr[1:4] // 20, 30, 40
slice := arr[1:4]
for i := 0; i < len(slice); i++ {
fmt.Printf("slice[%v]=%v ", i, slice[i])
}
fmt.Println()
//使用for--range 方式遍历切片
for i, v := range slice {
fmt.Printf("i=%v v=%v \n", i, v)
}
slice2 := slice[1:2] // slice [ 20, 30, 40] [30]
slice2[0] = 100 // 因为arr , slice 和slice2 指向的数据空间是同一个,因此slice2[0]=100,其它的都变化
fmt.Println("slice2=", slice2)
fmt.Println("slice=", slice)
fmt.Println("arr=", arr)
fmt.Println()
//用append内置函数,可以对切片进行动态追加
var slice3 []int = []int{100, 200, 300}
//通过append直接给slice3追加具体的元素
slice3 = append(slice3, 400, 500, 600)
fmt.Println("slice3", slice3) //100, 200, 300,400, 500, 600
//通过append将切片slice3追加给slice3
slice3 = append(slice3, slice3...) // 100, 200, 300,400, 500, 600 100, 200, 300,400, 500, 600
fmt.Println("slice3", slice3)
}
切片 append 操作的底层原理分析:
- 切片append操作的本质就是对数组扩容
- go底层会创建一下新的数组newArr(安装扩容后大小)
- 将slice原来包含的元素拷贝到新的数组newArr
- slice 重新引用到newArr
- 注意newArr是在底层来维护的,程序员不可见.
5)切片的拷贝操作
切片使用内置函数copy完成拷贝
package main
import (
"fmt"
)
func main() {
//切片的拷贝操作
//切片使用copy内置函数完成拷贝,举例说明
fmt.Println()
var slice4 []int = []int{1, 2, 3, 4, 5}
var slice5 = make([]int, 10)
copy(slice5, slice4)
fmt.Println("slice4=", slice4) // 1, 2, 3, 4, 5
fmt.Println("slice5=", slice5) // 1, 2, 3, 4, 5, 0 , 0 ,0,0,0
}
- ( 1 ) copy(para 1 ,para 2 ) 参数的数据类型是切片
- ( 2 ) 按照上面的代码来看,slice 4 和slice 5 的数据空间是独立,相互不影响,也就是说 slice 4 [ 0 ]= 999 ,slice 5 [ 0 ] 仍然是 1,所以是值复制
string和slice
1 ) string底层是一个byte数组,因此string也可以进行切片处理
2 ) string是不可变的,也就说不能通过 str[ 0 ]=‘z’ 方式来修改字符串
//string是不可变的,也就说不能通过 str[0] = 'z' 方式来修改字符串
str[0] = 'z' [编译不会通过,报错,原因是string是不可变]
3 ) 如果需要修改字符串,可以先将string->[]byte/ 或者 []rune-> 修改 -> 重写转成string
package main
import (
"fmt"
)
func main() {
//如果需要修改字符串,可以先将string -> []byte / 或者 []rune -> 修改 -> 重写转成string
str := "helloDJ"
arr1 := []byte(str)
arr1[5] = 'z'
arr1[6] = 'y'
str = string(arr1)
fmt.Println("str=", str)
}
package main
import (
"fmt"
)
func main() {
//如果需要修改字符串,可以先将string -> []byte / 或者 []rune -> 修改 -> 重写转成string
str := "董军"
// 细节,我们转成[]byte后,可以处理英文和数字,但是不能处理中文
// 原因是 []byte 字节来处理 ,而一个汉字,是3个字节,因此就会出现乱码
// 解决方法是 将 string 转成 []rune 即可, 因为 []rune是按字符处理,兼容汉字
arr1 := []rune(str)
arr1[0] = '栈'
arr1[1] = '也'
str = string(arr1)
fmt.Println("str=", str)
}
map
概念
map是key-value数据结构,又称为字段或者关联数组
基本语法:
var 变量名 map[keyType]valueType
map可以是什么类型?
bool, 数字,string, 指针,channel, 还可以是只包含前面几个类型的 接口, 结构体, 数组
注意:slice, map 还有 function 不可以,因为这几个没法用 ==来判断
声明:
var a map[string]string
var a map[string]int
var a map[int]string
var a map[string]map[string]string
注意:声明是不会分配内存的,初始化需要make ,分配内存后才能赋值和使用。
package main
import "fmt"
// map的使用
func main() {
// 第一种方式
// 声明一个map
var a map[string]string
// 分配一个空间 只有分配了一个空间才能使用
a = make(map[string]string, 10)
a["name"] = "XDJ"
a["age"] = "21"
a["name"] = "ZY" // key相同时覆盖前面的值
fmt.Println(a)
// map中的key是无序的
// 第二种方式
var b = make(map[string]string, 10)
b["name"] = "DJ"
b["age"] = "21"
fmt.Println(b)
// 第三种方式
c := map[string]string{
"name": "DJ",
"age": "21",
"hobby": "rap 打篮球",
}
fmt.Println(c)
}
map操作
增改删
package main
import "fmt"
// map操作
func main() {
// 增改
a := make(map[string]string, 10)
a["name"] = "DJ" // 增
a["name"] = "ZY" // 修改
// 删除 使用内置函数
a["age"] = "21"
delete(a, "age") // 删除, 如果key不存在 不进行操作 但也不会报错
fmt.Println(a)
// 假如我们想删除一个map中所有的数据,go没有提供一次性删除所有的方法,但我们可以遍历全部来进行删除
// 或者 通过make 将原来的map指向一个空的map,原来的map就会被gc
a = make(map[string]string)
fmt.Println(a)
}
查找
package main
import "fmt"
// 查找
func main() {
cities := make(map[string]string)
cities["no1"] = "茂名"
cities["no2"] = "广州"
cities["no3"] = "深圳"
// 根据key查找
value, ok := cities["no1"]
if ok {
fmt.Println(value)
} else {
fmt.Println("不存在key=no1")
}
}
map遍历(for-range)
package main
import (
"fmt"
)
// 查找
func main() {
// for-range 遍历
for key, value := range cities {
fmt.Printf("key=%s, value=%s\n", key, value)
}
//使用for-range遍历一个较复杂的map
studentMap := make(map[string]map[string]string)
studentMap["no1"] = make(map[string]string)
studentMap["no1"]["name"] = "XDJ"
studentMap["no1"]["age"] = "21"
studentMap["no2"] = make(map[string]string)
studentMap["no2"]["name"] = "ZY"
studentMap["no2"]["age"] = "21"
//遍历
for key, value := range studentMap {
fmt.Println("学生的序号为:", key)
for studentKey, studentValue := range value {
fmt.Printf("%v=%v\t", studentKey, studentValue)
}
fmt.Println()
}
fmt.Println("studentMap的长度为: ", len(studentMap))
}
map切片(即切片的数据类型是map)
package main
import "fmt"
// map 切片
func main() {
/*
要求:使用一个map来记录student的信息 name 和 age, 也就是说一个
student对应一个map,并且学生的个数可以动态的增加=>map切片
*/
student := make([]map[string]string, 2)
student[0] = make(map[string]string, 2)
student[0]["name"] = "XDJ"
student[0]["age"] = "21"
student[1] = make(map[string]string, 2)
student[1]["name"] = "ZY"
student[1]["age"] = "21"
// 新增
newStudent := make(map[string]string, 2)
newStudent["name"] = "ZY"
newStudent["age"] = "21"
student = append(student, newStudent)
fmt.Println(student)
}
map排序
1 ) golang中没有一个专门的方法针对map的key进行排序
2 ) golang中的map默认是无序的,注意也不是按照添加的顺序存放的,你每次遍历,得到的输出可能不一样.
3 ) golang中map的排序,是先将key进行排序,然后根据key值遍历输出即可
package main
import (
"fmt"
"sort"
)
func main() {
//map的排序
map1 := make(map[int]int, 10)
map1[10] = 100
map1[1] = 13
map1[4] = 56
map1[8] = 90
fmt.Println(map1)
//如果按照map的key的顺序进行排序输出
//1. 先将map的key 放入到 切片中
//2. 对切片排序
//3. 遍历切片,然后按照key来输出map的值
var keys []int
for k, _ := range map1 {
keys = append(keys, k)
}
//排序
sort.Ints(keys)
fmt.Println(keys)
for _, k := range keys{
fmt.Printf("map1[%v]=%v \n", k, map1[k])
}
}
map使用细节
1 ) map是引用类型,遵守引用类型传递的机制,在一个函数接收map,修改后,会直接修改原来的map
package main
import (
"fmt"
)
func modify(map1 map[int]int) {
map1[10] = 900
}
func main() {
//map是引用类型,遵守引用类型传递的机制,在一个函数接收map,
//修改后,会直接修改原来的map
map1 := make(map[int]int, 2)
map1[1] = 90
map1[2] = 88
map1[10] = 1
map1[20] = 2
modify(map1)
// 看看结果, map1[10] = 900 ,说明map是引用类型
fmt.Println(map1)
}
2 ) map的容量达到后,再想map增加元素,会自动扩容,并不会发生panic,也就是说map 能动态的增长 键值对(key-value)
3 ) map的 value 也经常使用 struct 类型,更适合管理复杂的数据(比前面value是一个map更好),比如value为 Student结构体
package main
import (
"fmt"
)
func modify(map1 map[int]int) {
map1[10] = 900
}
//定义一个学生结构体
type Stu struct {
Name string
Age int
Address string
}
func main() {
//map的value 也经常使用struct 类型,
//更适合管理复杂的数据(比前面value是一个map更好),
//比如value为 Student结构体
//1.map 的 key 为 学生的学号,是唯一的
//2.map 的 value为结构体,包含学生的 名字,年龄, 地址
students := make(map[string]Stu, 10)
//创建2个学生
stu1 := Stu{"tom", 18, "北京"}
stu2 := Stu{"mary", 28, "上海"}
students["no1"] = stu1
students["no2"] = stu2
fmt.Println(students)
//遍历各个学生信息
for k, v := range students {
fmt.Printf("学生的编号是%v \n", k)
fmt.Printf("学生的名字是%v \n", v.Name)
fmt.Printf("学生的年龄是%v \n", v.Age)
fmt.Printf("学生的地址是%v \n", v.Address)
fmt.Println()
}
}
面向对象
说明
- Golang没有类(class),Go语言的结构体(struct)和其它编程语言的类(class)有同等的地位,可以理解Golang是基于struct来实现OOP特性的。
- Golang面向对象编程非常简洁,去掉了传统OOP语言的继承、方法重载、构造函数和析构函数、隐藏的this指针等等
- Golang仍然有面向对象编程的继承,封装和多态的特性,只是实现的方式和其它OOP语言不一样,比如继承 :Golang没有extends 关键字,继承是通过匿名字段来实现。
- Golang面向对象(OOP)很优雅,OOP本身就是语言类型系统(typesystem)的一部分,通过接口(interface)关联,耦合性低,也非常灵活。也就是说在Golang中面向接口编程是非常重要的特性。
基本语法
type 结构体名称 struct {
field1 type
field2 type
}
package main
import "fmt"
// 结构体
type Cat struct {
Name string
Age int
Color string
Hobby string
}
func main() {
var cat Cat = Cat{"ZY", 18, "粉红色", "爱睡觉"}
fmt.Println(cat)
var cat1 Cat
cat1.Name = "DJ"
cat1.Age = 21
cat1.Color = "黑色"
cat1.Hobby = "吃"
fmt.Println(cat1)
}
注意:不同结构体变量的字段是独立,互不影响,一个结构体变量字段的更改,不影响另外一个, 结构体是值类型。
package main
import "fmt"
type Monster struct {
Name string
Age int
}
// 结构体是值类型
func main() {
var monster1 Monster
monster1.Name = "DJ"
monster1.Age = 21
monster2 := monster1
monster2.Name = "Zy"
fmt.Println(monster1)
fmt.Println(monster2)
//{DJ 21}
//{Zy 21}
}
创建结构体变量
方式1
var monster1 Monster // 例如上面的例子
monster1.Name = "DJ"
monster1.Age = 21
方式2
var cat Cat = Cat{"value1", "value2"}
例如:
var cat Cat = Cat{"ZY", 18, "粉红色", "爱睡觉"}
方式3
指针的方式
package main
import "fmt"
type Person struct {
Name string
Age int
}
// 结构体定义方式
func main() {
// 方式3
var person *Person = new(Person)
// 通过解引用的方式给属性赋值
(*person).Name = "DJ"
// 也可以这样
person.Name = "ZY" // 编译器会优化成 (*person).Name
(*person).Age = 21
fmt.Println(*person)
// {ZY 21}
}
方式4
也是指针的方式,其实跟方式3差不多
package main
import "fmt"
type Person struct {
Name string
Age int
}
// 结构体定义方式
func main() {
// 方式3
//var person *Person = new(Person)
var person *Person = &Person{}
// 通过解引用的方式给属性赋值
(*person).Name = "DJ"
// 也可以这样
person.Name = "ZY" // 编译器会优化成 (*person).Name
(*person).Age = 21
fmt.Println(*person)
// {ZY 21}
}
细节
-
结构体中的所有变量在内存中是连续的
-
结构体是用户单独定义的类型,和其它类型进行转换时需要有完全相同的字段(名字、个数和类型)
-
package main import "fmt" type A struct { Num int } type B struct { Num int } func main() { var a A var b B a.Num = 21 b = B(a) // 结构体 A 和 B 中的变量类型和数目必须一样 fmt.Println(a, b) }
-
-
struct的每个字段上,可以写上一个 tag , 该tag可以通过反射机制获取,常见的使用场景就是序列化和反序列化。
-
package main import ( "encoding/json" "fmt" ) type Person struct { Name string `json:"Name"` // `json:"Name"` 就是一个struct tag Age int `json:"age"` Skill string `json:"skill"` // 好像不写也可以? } func main() { person := Person{"DJ", 21, "coding"} // 使用json.Marshal进行序列化成json格式对象 jsonStr, err := json.Marshal(person) if err != nil { fmt.Println("json", err) } else { fmt.Println(string(jsonStr)) // {"Name":"DJ","age":21,"skill":"coding"} } }
-
方法
结构体出了含有属性外,还可以包含方法
Golang中的方法是作用在指定的数据类型上的(即:和指定的数据类型绑定),因此自定义类型,都可以有方法,而不仅仅是 struct
看例子:
package main
import "fmt"
type Person struct {
Name string
Age int
}
// 方法和结构体绑定,因为结构体是值类型传递,如果需要改变原结构体属性,可以传一个结构体指针
func (p *Person) walk() {
fmt.Printf("%s is walking\n", p.Name)
}
func main() {
person := Person{"DJ", 21}
person.walk()// DJ is walking
}
注意:
-
walk方法只能被Person类型的实例调用
-
如果绑定的类型不是指针的话,那么是一个副本
-
结构体调用方法时,结构体实例本身也会作为一个参数传递到方法
-
结构体类型是值类型,在方法调用中,遵守值类型的传递机制,是值拷贝传递方式
-
如程序员希望在方法中,修改结构体变量的值,可以通过结构体指针的方式来处理
-
Golang中的方法作用在指定的数据类型上的(即:和指定的数据类型绑定),因此自定义类型,都可以有方法,而不仅仅是struct, 比如int,float 32 等都可以有方法
-
package main import ( "fmt" ) /* Golang中的方法作用在指定的数据类型上的(即:和指定的数据类型绑定),因此自定义类型, 都可以有方法,而不仅仅是struct, 比如int , float32等都可以有方法 */ type integer int func (i integer) print() { fmt.Println("i=", i) } //编写一个方法,可以改变i的值 func (i *integer) change() { *i = *i + 1 } func main() { var i integer = 10 i.print() i.change() fmt.Println("i=", i) }
-
-
方法的访问范围控制的规则,和函数一样。方法名首字母小写,只能在本包访问,方法首字母大写,可以在本包和其它包访问
-
如果一个结构体实现了String()这个方法,那么这个结构体实例在调用fmt.println时,会去默认调用String()这个方法
-
package main import "fmt" type Person struct { Name string Age int } // fmt.Println回去默认调用这个方法,可以理解为java的toString方法 func (p *Person) String() string { return fmt.Sprintf("Name is %s, Age is %d\n", p.Name, p.Age) } func main() { person := Person{"DJzz", 21} fmt.Println(&person) }
-
方法和函数的区别
1)调用方式不一样
- 函数的调用方式: 函数名(实参列表)
- 方法的调用方式: 变量.方法名(实参列表)
2)对于普通函数,接收者为值类型时,不能将指针类型的数据直接传递,反之亦然
package main
import (
"fmt"
)
type Person struct {
Name string
}
//函数
//对于普通函数,接收者为值类型时,不能将指针类型的数据直接传递,反之亦然
func test01(p Person) {
fmt.Println(p.Name)
}
func test02(p *Person) {
fmt.Println(p.Name)
}
func main() {
p := Person{"tom"}
test01(p)
test02(&p)
}
3)对于方法(如struct的方法),接收者为值类型时,可以直接用指针类型的变量调用方法,反过来同样也可以
package main
import "fmt"
type Person struct {
Name string
}
func (p Person) test01() {
p.Name = "DJ"
fmt.Println("test01's name=", p.Name) // DJ
}
func (p *Person) test02() {
(*p).Name = "ZY"
fmt.Println("test02's name=", (*p).Name) // ZY
}
func main() {
person := Person{"tom"}
person.test01()
fmt.Println(person.Name) // tom
(&person).test01() // 形式上传入的是地址,但本质还是值拷贝,不会影响原来的值
fmt.Println(person.Name) // tom
(&person).test02()
fmt.Println(person.Name) // ZY
person.test02() // 形式上是传入的是值类型,但本质是传入的是地址,会修改原来的值
fmt.Println(person.Name) // ZY
}
说明:
- 不管调用形式如何,真正决定是值拷贝还是地址拷贝,看这个方法是和哪个类型绑定.
- 如果是和值类型,比如 ( pPerson ), 则是值拷贝, 如果和指针类型,比如是 ( p*Person ) 则是地址拷贝。
工厂模式
Golang的结构体没有构造函数,通常可以使用工厂模式来解决这个问题。
当我们的结构体名称是小写的时候,需要通过公共的方法来获取实例
package model
// 小写 所以只能在本包中使用
type person struct {
Name string
Age int
}
// 定义一个获取实例的函数
func GetInstance(name string, age int) *person {
return &person{name, age}
}
package main
import (
"fmt"
"study/oop/structdemo08/model"
)
func main() {
p := model.GetInstance("DJ", 21)
fmt.Println(*p)
fmt.Println("name=", p.Name, "age=", p.Age)
}
如果结构体的字段也是小写的,则可以提供一个公共的get方法来获取,提供一个公共的set方法来修改
封装
封装(encapsulation)就是把抽象出的字段和对字段的操作封装在一起,数据被保护在内部,程序的其它包只有通过被授权的操作(方法),才能对字段进行操作
func(var 结构体类型名)SetXxx(参数列表){
//加入数据验证的业务逻辑
var.字段 =参数
}
func(var 结构体类型名)GetXxx()(返回值){
return var.age;
}
继承
介绍:
-
当多个结构体存在相同的属性(字段)和方法时,可以从这些结构体中抽象出结构体, 在该结构体中定义这些相同的属性和方法。
-
其它的结构体不需要重新定义这些属性(字段)和方法,只需嵌套一个匿名结构体即可。
-
在Golang中,如果一个struct嵌套了另一个匿名结构体,那么这个结构体可以直接访问匿名结构体的字段和方法,从而实现了继承特性。
package main
import "fmt"
type Goods struct {
name string
price int
}
type Books struct {
Goods // 嵌套匿名结构体
writer string
}
func main() {
var book Books
//结构体可以使用匿名结构体的所有字段和方法 包括大写和小写的
book.name = "天龙八部"
book.price = 10
book.writer = "DJ"
fmt.Println(book)
}
- 结构体可以使用匿名结构体的所有字段和方法 包括大写和小写的
package main
import "fmt"
type A struct {
name string
age int
}
func (a *A) SayOk() {
fmt.Println("A say ok", a.name)
}
func (a *A) hello() {
fmt.Println("A hello", a.name)
}
type B struct {
A
name string
}
func (b *B) SayOk() {
fmt.Println("B say ok", b.name)
}
func main() {
var b B
b.name = "DJ" // B 本身的name属性
b.age = 21 // 使用继承的A的属性
b.SayOk() // 使用B本身的方法
b.hello() // 使用继承的A的方法
fmt.Println(b)
}
-
当我们直接通过 b 访问字段或方法时,其执行流程如下比如 b.name
-
编译器会先看b对应的类型有没有name, 如果有,则直接调用B类型的name字段
-
如果没有就去看B中嵌入的匿名结构体A有没有声明name字段,如果有就调用,如果没有继续查找…如果都找不到就报错.
-
当结构体和匿名结构体有相同的字段或者方法时,编译器采用就近访问原则访问,如希望访问匿名结构体的字段和方法,可以通过匿名结构体名来区分
func main() {
var b B
b.Name = "jack" // ok
b.A.Name = "scott"
b.age = 100 //ok
b.SayOk() // B SayOk jack
b.A.SayOk() // A SayOk scott
b.hello() // A hello ? "jack" 还是 "scott"
}
补充:
- 结构体嵌入两个(或多个)匿名结构体(多重继承),如两个匿名结构体有相同的字段和方法(同时结构体本身没有同名的字段和方法),在访问时,就必须明确指定匿名结构体名字,否则编译报错。
接口
概念:interface类型可以定义一组方法,但是这些不需要实现。并且interface不能包含任何变量。到某个 自定义类型(比如结构体Phone)要使用的时候,在根据具体情况把这些方法写出来(实现)。
特点:
- 接口里的所有方法都没有方法体,即接口的方法都是没有实现的方法。接口体现了程序设计的多态和高内聚低偶合的思想。
- Golang中的接口,不需要显式的实现。只要一个变量,含有接口类型中的所有方法,那么这个变量就实现这个接口。因此,Golang中没有 implement这样的关键字
注意细节:
- 接口本身不能创建实例,但是可以指向一个实现了该接口的自定义类型的变量(实例)
- 接口中所有的方法都没有方法体,即都是没有实现的方法。
- 在Golang中,一个自定义类型需要将某个接口的所有方法都实现,我们说这个自定义类型实现 了该接口。
- 一个自定义类型只有实现了某个接口,才能将该自定义类型的实例(变量)赋给接口类型
- 只要是自定义数据类型,就可以实现接口,不仅仅是结构体类型。
- 一个自定义类型可以实现多个接口
package main
import "fmt"
type AInterface interface {
Say()
}
type BInterface interface {
Hello()
}
// Monster 实现了两个接口的全部方法 也就实现了接口
type Monster struct {
}
func (m *Monster) Say() {
fmt.Println("Say...")
}
func (m *Monster) Hello() {
fmt.Println("Hello...")
}
func main() {
// Monster 实现了AInterface , BInterface
var monster Monster
var a AInterface = &monster
var b BInterface = &monster
a.Say()
b.Hello()
}
- 注意接口中不能含有属性
- 一个接口(比如A接口)可以继承多个别的接口(比如B,C接口),这时如果要实现A接口,也必须将B,C接口的方法也全部实现。
package main
import (
"fmt"
)
type BInterface interface {
test01()
}
type CInterface interface {
test02()
}
type AInterface interface {
BInterface
CInterface
test03()
}
//如果需要实现AInterface,就需要将BInterface CInterface的方法都实现
type Stu struct {
}
func (stu Stu) test01() {
}
func (stu Stu) test02() {
}
func (stu Stu) test03() {
}
func main() {
var stu Stu
var a AInterface = stu
a.test01()
}
- 空接口interface{}没有任何方法,所以所有类型都实现了空接口, 即我们可以把任何一个变量 赋给空接口。
package main
import (
"fmt"
)
type T interface{
}
func main() {
var stu Stu
var a AInterface = stu
a.test01()
var t T = stu //ok
fmt.Println(t)
var t2 interface{} = stu
var num1 float64 = 8.8
t2 = num1
t = num1
fmt.Println(t2, t)
}
多态
- 多态参数
- 多态数组
package main
import "fmt"
type Usb interface {
start()
stop()
}
type Phone struct {
name string
}
func (p Phone) start() {
fmt.Println("手机开始工作")
}
func (p Phone) stop() {
fmt.Println("手机停止工作")
}
type Camera struct {
name string
}
func (c Camera) start() {
fmt.Println("相机开始工作")
}
func (c Camera) stop() {
fmt.Println("相机停止工作")
}
func main() {
// 定义一个接口数组 可以存放Phone 和 Camera结构体变量
var usb [3]Usb
usb[0] = Phone{"小米"}
usb[1] = Phone{"vivo"}
usb[2] = Camera{"索尼"}
usb[0].start()
usb[0].stop()
fmt.Println(usb)
}
类型断言
类型断言,由于接口是一般类型,不知道具体类型,如果要转成具体类型,就需要使用类型断言,
在进行类型断言时,如果类型不匹配,就会报 panic, 因此进行类型断言时,要确保原来的空接口指向的就是断言的类型
package main
import "fmt"
type Point struct {
x int
y int
}
func main() {
var p Point = Point{1, 2}
var a interface{} // 空接口可以接收任意类型
a = p // 向下转型
var b Point
// b = a // 这是不可以的
b = a.(Point) // 类型断言
fmt.Println(b)
}
这个断言就跟java里面判断实例的类型一样,instanceof
监测机制
package main
import (
"fmt"
)
type Point struct {
x int
y int
}
func main() {
//类型断言(带检测的)
var x interface{}
var b2 float32 = 2.1
x = b2 //空接口,可以接收任意类型
// x=>float32 [使用类型断言]
//类型断言(带检测的)
if y, ok := x.(float32); ok {
fmt.Println("convert success")
fmt.Printf("y 的类型是 %T 值是=%v", y, y)
} else {
fmt.Println("convert fail")
}
fmt.Println("继续执行...")
}
文件操作
文件在程序中是以流的形式来操作的
输入流:数据从数据源(文件)到程序(内存)的路径
输出流:数据从程序(内存)到数据源(文件)的路径
文件打开
func Open(name string) (file *File, err error)
文件关闭
func (f *File) Close() error
案例演示
package main
import (
"fmt"
"os"
)
func main() {
// 打开一个文件 file对象 file指针 file 文件句柄
file, err := os.Open("d:/test.txt")
if err != nil {
fmt.Println("err=", err)
return
}
// file 是一个指针
fmt.Printf("file=%#v\n", file)
defer func(file *os.File) {
err := file.Close()
if err != nil {
fmt.Println("关闭文件失败", err)
}
}(file)
}
文件读取
1 ) 读取文件的内容并显示在终端(带缓冲区的方式),使用os.Open,file.Close,bufio.NewReader(), reader.ReadString 函数和方法
package main
import (
"bufio"
"fmt"
"io"
"os"
)
func main() {
// 打开一个文件 file对象 file指针 file 文件句柄
file, err := os.Open("d:/test.txt")
if err != nil {
fmt.Println("err=", err)
return
}
// file 是一个指针
fmt.Printf("file=%#v\n", file)
// 当函数退出时及时关闭
defer func(file *os.File) {
err := file.Close()
if err != nil {
fmt.Println("关闭文件失败", err)
}
}(file)
// 创建一个*Reader 是带缓冲的
reader := bufio.NewReader(file)
// 循环读取文件内容
for {
str, err := reader.ReadString('\n') // 读到一个换行就结束
if err == io.EOF { // io.EOF表示文件的末尾
fmt.Println(str)
break
}
// 输出内容
fmt.Printf(str)
}
fmt.Println("文件读取结束~~~")
}
2 ) 一次性读取文件的内容并显示在终端(使用os一次将整个文件读入到内存中),这种方式适用于文件 不大的情况。相关方法和函数(os.ReadFile)
package main
import (
"fmt"
"os"
)
func main() {
// 使用os.Readfile一次性将文件读取到位
file := "d:/test.txt"
content, err := os.ReadFile(file)
if err != nil {
fmt.Println("read file error:", err)
}
fmt.Println(string(content))
// 我们没有显式Open文件 也不需要显式Close文件
// open and close 被封装到readFile里面了
}
文件写入
1)创建一个不存在的文件 写入内容
package main
import (
"bufio"
"fmt"
"os"
)
// 文件写入
func main() {
// 创建一个文件 写入内容
filePath := "d:/abc.txt"
// 仅写 或不存在时创建
file, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE, 0666)
if err != nil {
fmt.Println("open file err:", err)
return
}
// 关闭句柄
defer func(file *os.File) {
err := file.Close()
if err != nil {
fmt.Println("close file err:", err)
return
}
}(file)
str := "hello world\r\n"
// 使用*Writer
writer := bufio.NewWriter(file)
for i := 0; i < 5; i++ {
_, err := writer.WriteString(str)
if err != nil {
return
}
}
//因为writer是带缓存,因此在调用WriterString方法时,其实
//内容是先写入到缓存的,所以需要调用Flush方法,将缓冲的数据
//真正写入到文件中, 否则文件中会没有数据!!!
err = writer.Flush()
if err != nil {
fmt.Println("flush err:", err)
return
}
}
2)覆盖内容
package main
import (
"fmt"
"bufio"
"os"
)
func main() {
//打开一个存在的文件中,将原来的内容覆盖成新的内容10句 "你好,Golang Roadmap!"
//创建一个新文件,写入内容 5句 "hello, Gardon"
//1 .打开文件已经存在文件, d:/abc.txt
filePath := "d:/abc.txt"
file, err := os.OpenFile(filePath, os.O_WRONLY | os.O_TRUNC, 0666)
if err != nil {
fmt.Printf("open file err=%v\n", err)
return
}
//及时关闭file句柄
defer file.Close()
//准备写入5句 "你好,Golang Roadmap!"
str := "你好,Golang Roadmap!\r\n" // \r\n 表示换行
//写入时,使用带缓存的 *Writer
writer := bufio.NewWriter(file)
for i := 0; i < 10; i++ {
writer.WriteString(str)
}
//因为writer是带缓存,因此在调用WriterString方法时,其实
//内容是先写入到缓存的,所以需要调用Flush方法,将缓冲的数据
//真正写入到文件中, 否则文件中会没有数据!!!
writer.Flush()
}
3)追加内容
package main
import (
"fmt"
"bufio"
"os"
)
func main() {
//打开一个存在的文件,在原来的内容追加内容 'ABC! ENGLISH!'
//1 .打开文件已经存在文件, d:/abc.txt
filePath := "d:/abc.txt"
file, err := os.OpenFile(filePath, os.O_WRONLY | os.O_APPEND, 0666)
if err != nil {
fmt.Printf("open file err=%v\n", err)
return
}
//及时关闭file句柄
defer file.Close()
//准备写入5句 "你好,Golang Roadmap!"
str := "ABC,ENGLISH!\r\n" // \r\n 表示换行
//写入时,使用带缓存的 *Writer
writer := bufio.NewWriter(file)
for i := 0; i < 10; i++ {
writer.WriteString(str)
}
//因为writer是带缓存,因此在调用WriterString方法时,其实
//内容是先写入到缓存的,所以需要调用Flush方法,将缓冲的数据
//真正写入到文件中, 否则文件中会没有数据!!!
writer.Flush()
}
4)读写权限,并追加内容
package main
import (
"fmt"
"bufio"
"os"
"io"
)
func main() {
//打开一个存在的文件,将原来的内容读出显示在终端,并且追加5句"hello,北京!"
//1 .打开文件已经存在文件, d:/abc.txt
filePath := "d:/abc.txt"
file, err := os.OpenFile(filePath, os.O_RDWR | os.O_APPEND, 0666)
if err != nil {
fmt.Printf("open file err=%v\n", err)
return
}
//及时关闭file句柄
defer file.Close()
//先读取原来文件的内容,并显示在终端.
reader := bufio.NewReader(file)
for {
str, err := reader.ReadString('\n')
if err == io.EOF { //如果读取到文件的末尾
break
}
//显示到终端
fmt.Print(str)
}
//准备写入5句 "你好,Golang Roadmap!"
str := "hello,北京!\r\n" // \r\n 表示换行
//写入时,使用带缓存的 *Writer
writer := bufio.NewWriter(file)
for i := 0; i < 5; i++ {
writer.WriteString(str)
}
//因为writer是带缓存,因此在调用WriterString方法时,其实
//内容是先写入到缓存的,所以需要调用Flush方法,将缓冲的数据
//真正写入到文件中, 否则文件中会没有数据!!!
writer.Flush()
}
5)读取一个文件写入到另一个文件
package main
import "os"
func main() {
source := "d:/abc.txt"
target := "d:/kkk.txt"
// 一次性全读取一个文件
data, err := os.ReadFile(source)
if err != nil {
panic(err)
}
// 一次性写入
err = os.WriteFile(target, data, 0666)
if err != nil {
panic(err)
}
}
图片拷贝
package main
import (
"bufio"
"fmt"
"io"
"os"
)
// 自定义一个文件复制函数
func CopyFile(source string, dest string) (written int64, err error) {
// 打开源文件
sourceFile, err := os.Open(source)
if err != nil {
return 0, err
}
defer sourceFile.Close()
// 创建reader
reader := bufio.NewReader(sourceFile)
// 创建目的文件
destFile, err := os.OpenFile(dest, os.O_WRONLY|os.O_CREATE, 0666)
if err != nil {
return 0, err
}
defer destFile.Close()
// 创建Writer
writer := bufio.NewWriter(destFile)
return io.Copy(writer, reader)
}
func main() {
source := "d:/a.jpg"
dest := "d:/b.jpg"
_, err := CopyFile(source, dest)
if err != nil {
fmt.Println("拷贝失败")
return
}
fmt.Println("拷贝完成")
}
判断文件是否存在
golang判断文件或文件夹是否存在的方法为使用os.Stat()函数返回的错误值进行判断:
- 如果返回的错误为nil,说明文件或文件夹存在
- 如果返回的错误类型使用os.IsNotExist()判断为true,说明文件或文件夹不存在
- 如果返回的错误为其他类型,则不确定是否存在
func PathExists(path string)(bool,error){
_, err := os.Stat(path)
if err == nil{//文件或目录存在
return true,nil
}
if os.IsNotExit(err){
return false,nil
}
return false,err
}
JSON序列化
json序列化是指,将有 key-valu e结构的数据类型(比如结构体、 map 、切片)序列化成json字符串的操作。
package main
import (
"encoding/json"
"fmt"
)
// 定义一个结构体
type Monster struct {
Name string `json:"monster_name"` //反射机制
Age int `json:"monster_age"`
Birthday string //....
Sal float64
Skill string
}
func testStruct() {
//演示
monster := Monster{
Name: "牛魔王",
Age: 500,
Birthday: "2011-11-11",
Sal: 8000.0,
Skill: "牛魔拳",
}
//将monster 序列化
data, err := json.Marshal(&monster) //..
if err != nil {
fmt.Printf("序列号错误 err=%v\n", err)
}
//输出序列化后的结果
fmt.Printf("monster序列化后=%v\n", string(data))
}
// 将map进行序列化
func testMap() {
//定义一个map
var a map[string]interface{}
//使用map,需要make
a = make(map[string]interface{})
a["name"] = "红孩儿"
a["age"] = 30
a["address"] = "洪崖洞"
//将a这个map进行序列化
//将monster 序列化
data, err := json.Marshal(a)
if err != nil {
fmt.Printf("序列化错误 err=%v\n", err)
}
//输出序列化后的结果
fmt.Printf("a map 序列化后=%v\n", string(data))
}
// 演示对切片进行序列化, 我们这个切片 []map[string]interface{}
func testSlice() {
var slice []map[string]interface{}
var m1 map[string]interface{}
//使用map前,需要先make
m1 = make(map[string]interface{})
m1["name"] = "jack"
m1["age"] = "7"
m1["address"] = "北京"
slice = append(slice, m1)
var m2 map[string]interface{}
//使用map前,需要先make
m2 = make(map[string]interface{})
m2["name"] = "tom"
m2["age"] = "20"
m2["address"] = [2]string{"墨西哥", "夏威夷"}
slice = append(slice, m2)
//将切片进行序列化操作
data, err := json.Marshal(slice)
if err != nil {
fmt.Printf("序列化错误 err=%v\n", err)
}
//输出序列化后的结果
fmt.Printf("slice 序列化后=%v\n", string(data))
}
// 对基本数据类型序列化,对基本数据类型进行序列化意义不大
func testFloat64() {
var num1 float64 = 2345.67
//对num1进行序列化
data, err := json.Marshal(num1)
if err != nil {
fmt.Printf("序列化错误 err=%v\n", err)
}
//输出序列化后的结果
fmt.Printf("num1 序列化后=%v\n", string(data))
}
func main() {
//演示将结构体, map , 切片进行序列号
testStruct()
testMap()
testSlice() //演示对切片的序列化
testFloat64() //演示对基本数据类型的序列化
}
JSON反序列化
package main
import (
"fmt"
"encoding/json"
)
//定义一个结构体
type Monster struct {
Name string
Age int
Birthday string //....
Sal float64
Skill string
}
//演示将json字符串,反序列化成struct
func unmarshalStruct() {
//说明str 在项目开发中,是通过网络传输获取到.. 或者是读取文件获取到
str := "{\"Name\":\"牛魔王~~~\",\"Age\":500,\"Birthday\":\"2011-11-11\",\"Sal\":8000,\"Skill\":\"牛魔拳\"}"
//定义一个Monster实例
var monster Monster
err := json.Unmarshal([]byte(str), &monster)
if err != nil {
fmt.Printf("unmarshal err=%v\n", err)
}
fmt.Printf("反序列化后 monster=%v monster.Name=%v \n", monster, monster.Name)
}
//将map进行序列化
func testMap() string {
//定义一个map
var a map[string]interface{}
//使用map,需要make
a = make(map[string]interface{})
a["name"] = "红孩儿~~~~~~"
a["age"] = 30
a["address"] = "洪崖洞"
//将a这个map进行序列化
//将monster 序列化
data, err := json.Marshal(a)
if err != nil {
fmt.Printf("序列化错误 err=%v\n", err)
}
//输出序列化后的结果
//fmt.Printf("a map 序列化后=%v\n", string(data))
return string(data)
}
//演示将json字符串,反序列化成map
func unmarshalMap() {
//str := "{\"address\":\"洪崖洞\",\"age\":30,\"name\":\"红孩儿\"}"
str := testMap()
//定义一个map
var a map[string]interface{}
//反序列化
//注意:反序列化map,不需要make,因为make操作被封装到 Unmarshal函数
err := json.Unmarshal([]byte(str), &a)
if err != nil {
fmt.Printf("unmarshal err=%v\n", err)
}
fmt.Printf("反序列化后 a=%v\n", a)
}
//演示将json字符串,反序列化成切片
func unmarshalSlice() {
str := "[{\"address\":\"北京\",\"age\":\"7\",\"name\":\"jack\"}," +
"{\"address\":[\"墨西哥\",\"夏威夷\"],\"age\":\"20\",\"name\":\"tom\"}]"
//定义一个slice
var slice []map[string]interface{}
//反序列化,不需要make,因为make操作被封装到 Unmarshal函数
err := json.Unmarshal([]byte(str), &slice)
if err != nil {
fmt.Printf("unmarshal err=%v\n", err)
}
fmt.Printf("反序列化后 slice=%v\n", slice)
}
func main() {
unmarshalStruct()
unmarshalMap()
unmarshalSlice()
}
goroutine
进程和线程
进程:进程是一个具有一定独立功能的程序在一个数据集合上依次动态执行的过程,是一个正在执行的程序的实例,是操作系统资源分配的最小单元。
线程:线程是操作系统能够进行运算调度的最小单元,被设计成进程的一个路径,同一个进程中的线程共享进程中的资源。
进程和线程的区别:
- 本质区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位
- 包含关系:一个进程至少有一个线程,线程是进程的一部分
- 资源开销:每个进程都有独立的地址空间,进程之间切换开销较大,线程可以看作是轻量级的进程,同一个进程内的线程共享进程的地址空间,每个线程都有自己独立的运行栈和程序计数器,线程之间切换的开销小
- 影响关系:一个进程崩溃后,在保护模式下其他的进程不会受影响,但是一个线程崩溃后可能导致整个进程被操作系统杀掉,所以多进程要比多线程强壮
并发:多线程程序在单核上运行,就是并发,在某一个时间节点上只有一个线程在执行
并行:多线程程序在多核上运行,就是并行,在某一个时间节点上有多个线程在执行
Go协程和Go主线程
- Go主线程(线程/也可以理解为进程):一个Go线程上,可以起多个协程,可以理解为协程是轻量级的线程
- Go协程(goroutine)的特点:
- 独立的栈空间
- 共享程序堆的空间
- 调度由用户控制
- 协程是轻量级的线程
案例:
package main
import "fmt"
func test() {
for i := 0; i < 100; i++ {
fmt.Println("test() 协程执行")
}
}
// 主线程 和 协程
func main() {
// 开启一个协程
go test()
// 主线程也执行循环100次
for i := 0; i < 100; i++ {
fmt.Println("main() 主线程执行")
}
// 我们可以看到协程和主线程交替进行
}
1)主线程是一个物理线程,直接作用在CPU上面的,是重量级的,非常耗费cpu的资源
2)协程是从主线程开启的,是轻量级的线程,是逻辑态。对资源消耗相对小。
3)Golang的协程机制是重要的特点,可以轻松开启上万个协程。其他编程语言的并发机制一般是基于线程的,开启多的线程,资源耗费大,这里就凸显Golang在并发上的优势了
MPG模式
M:操作系统主线程(是物理线程)
P:协程执行需要的上下文
G:协程
- 为了充分利用计算机的多cpu(核),可以在Golang程序中设置运行的cpu数目
package main
import (
"fmt"
"runtime"
)
func main() {
// 查看cpu数量
cpuNum := runtime.NumCPU()
fmt.Println("cpuNum:", cpuNum)
// 自己设置使用多个cpu
runtime.GOMAXPROCS(cpuNum - 1)
fmt.Println("ok")
}
- go1.8后,默认让程序运行在多个cpu上,可以不用设置
解决协程出现panic
goroutine中使用recover,解决协程中出现panic,导致程序崩溃问题
说明:如果我们起了一个协程,但是这个协程出现了panic,如果我们没有捕获这个panic,就会造成整个程序崩溃,这时候我们可以在goruntine中使用recover来捕获panic,来进行处理,这样即使这个协程发生问题,但是主线程不受影响,可以继续执行。
package main
import (
"fmt"
"time"
)
func SayHello() {
for i := 0; i < 10; i++ {
fmt.Println("SayHello", "hello world")
time.Sleep(time.Second)
}
}
func test() {
// defer + recover
defer func() {
if err := recover(); err != nil {
// assignment to entry in nil map
fmt.Println(err)
return
}
}()
// 定义一个map不分配空间,这里会出现错误
var m map[string]int
m["age"] = 21
}
// defer + recover()捕获异常
func main() {
// 开启两个协程
go SayHello()
// 出现异常 主程序不退出
go test()
for i := 0; i < 10; i++ {
fmt.Println("main", "Hello world")
time.Sleep(time.Second)
}
}
channel管道
goroutine存在的问题
先前使用goroutine来完成任务,效率高,但可能会出现并行/并发安全问题
来看一个例子:
求多个数的阶乘
方案:
- 定义一个全局变量map,存储数字对应的阶乘值
- 开启多个协程,并发执行求阶乘,写入map
- 主线程遍历map
package main
import (
"fmt"
"time"
)
// 定义一个全局变量map,各个写协程共享
var (
myMap = make(map[int]uint64)
)
// 定义一个函数来求n的阶乘
func test(n int) {
sum := uint64(0)
for i := 1; i <= n; i++ {
sum *= uint64(i)
}
myMap[n] = sum
}
// 求各个数的阶乘 并放到一个map中
func main() {
// 开启多个协程来并发求阶乘
for i := 1; i <= 200; i++ {
go test(i)
}
// 休眠一小会 防止主线程退出导致协程销毁
time.Sleep(20 * time.Second)
// 主线程遍历map
for k, v := range myMap {
fmt.Printf("%d的阶乘为:%d\n", k, v)
}
// 当我们运行时 会出现异常 : concurrent map writes
// 原因是 协程和主线程并发执行 协程在对全局变量map进行写 而主线程在对map进行读
// 产生了资源利用问题(同一个资源不能同时被多个线程使用)
// 解决方法:可以对共享资源进行加锁 使得某一时刻只能一个线程访问
}
问题:
当我们运行时,会出现问题,主线程和协程访问资源冲突了
解决:
加锁
package main
import (
"fmt"
"sync"
"time"
)
// 定义一个全局变量map,各个写协程共享
var (
myMap = make(map[int]uint64)
// 声明一个互斥锁 : Mutex 互斥
lock sync.Mutex
)
// 定义一个函数来求n的阶乘
func test(n int) {
sum := uint64(1)
for i := 1; i <= n; i++ {
sum *= uint64(i)
}
// 加锁
lock.Lock()
myMap[n] = sum
// 用完释放
lock.Unlock()
}
// 求各个数的阶乘 并放到一个map中
func main() {
// 开启多个协程来并发求阶乘
for i := 1; i <= 10; i++ {
go test(i)
}
// 休眠一小会 防止主线程退出导致协程销毁
time.Sleep(10 * time.Second)
// 主线程遍历map
lock.Lock()
for k, v := range myMap {
fmt.Printf("%d的阶乘为:%d\n", k, v)
}
lock.Unlock()
}
问题:
- 虽然使用了全局变量加锁来解决goroutine的通讯,但是不完美
- 主线程等待所有的goroutine全部完成的时间很难确定
- 如果主线程休眠的时间长了,会加长等待时间,如果短了,则主线程退出,协程也会退出
- 通过全局变量加锁同步来实现通讯,也并不利于多个线程对全局变量的读写操作
所以引出新的通信机制-channel
概念
- channel本质就是一个数据结构-队列
- 队列的特点是先进先出
- 线程安全,多个goroutine访问时,不需要加锁,channel本身是线程安全的
- channel是有类型的,例如一个string的channel只能存放string类型的数据
定义声明
var 变量名 chan 数据类型
such as:
var intChan chan int(intChan用于存放int数据)
var mapChan chan map[int]string(mapChan用于存放map[int]string类型)
var perChan chan Person
var perChan2 chan *Person
- channel是引用类型
- channel必须初始化才能写入数据,也就是先make
- channel也是有数据类型的,只能写入特定数据类型
写入读出
package main
import "fmt"
// 演示 channel 的读写
func main() {
// 创建一个可以存放3个int的管道
intChan := make(chan int, 3)
// 看看intChan是什么
fmt.Printf("intChan = %v, *intChain = %v\n", intChan, &intChan)
// 向管道写入数据
intChan <- 10
intChan <- 20
intChan <- 30
// 写入管道的数据不能超过其容量 取出后可以继续写入
<-intChan
intChan <- 40
// 查看管道长度和容量
fmt.Printf("intChan的长度为%v, intChain的容量为%v\n", len(intChan), cap(intChan))
// 从管道中读取数据
n1 := <-intChan
fmt.Println("n=", n1)
fmt.Printf("intChan的长度为%v, intChain的容量为%v\n", len(intChan), cap(intChan))
n2 := <-intChan
fmt.Println("n=", n2)
n3 := <-intChan
fmt.Println("n=", n3)
// 如果管道的数据全部取出 继续取的话会出现错误 deadlock
//n4 := <-intChan
//fmt.Println("n=", n4)
}
管道阻塞
什么时候管道阻塞?
- 向一个值为nil的管道写或读数据
- 无缓冲区时单独的写或读数据
- 缓冲区为空时进行读数据
- 缓冲区满时进行写数据
常见错误:
- 关闭值为nil的管道
- 关闭已经关闭的管道
- 往已经close的管道发送数据会panic
注意:
从已关闭的通道接收数据时将不会发生阻塞
由此可见通道如果不主动close掉,读出通道全部数据后该协程就会阻塞
从已经关闭的通道接收数据或者正在接收数据时,将会接收到通道类型的零值,然后停止阻塞并返回。
channel可以声明为只读,或者只写性质
package main
import (
"fmt"
)
func main() {
//管道可以声明为只读或者只写
//1. 在默认情况下下,管道是双向
//var chan1 chan int //可读可写
//2 声明为只写
var chan2 chan<- int
chan2 = make(chan int, 3)
chan2<- 20
//num := <-chan2 //error
fmt.Println("chan2=", chan2)
//3. 声明为只读
var chan3 <-chan int
num2 := <-chan3
//chan3<- 30 //err
fmt.Println("num2", num2)
}
select
select是go语言当中提供的一个选择语句。select的语法类似switch语句,也属于控制语句。
select可以用于解决从管道取数据的阻塞问题
select特性:
-
select只能用于channel操作,每个case都必须是一个channel;
-
如果有多个case可以允许(channel没有阻塞),则随机选择一条case语句执行;
-
如果没有case语句可以执行(channel发生阻塞),且没有default语句,则select语句会阻塞;
-
如果没有case语句可以执行(channel发生阻塞),有default语句,则执行default语句;
-
一般使用超时语句代替default语句
package main import ( "fmt" "time" ) // select 使用 func main() { ch1 := make(chan int) ch2 := make(chan int) select { case num1 := <-ch1: fmt.Println("ch1中的数据是:", num1) case num2 := <-ch2: fmt.Println("ch2中的数据是:", num2) case <-time.After(3 * time.Second): fmt.Println("timeout...") } }
-
如果case语句中的channel为nil,则忽略该分支,相当于从select中删除了该分支;
-
如果select语句在for循环中,一般不使用default语句,因为会引起CPU占用过高问题。
select语法
select {
case communication clause :
statement(s);
case communication clause :
statement(s);
/* 你可以定义任意数量的 case */
default : /* 可选 */
statement(s);
}
such as :
package main
import (
"fmt"
"time"
)
func main() {
//使用select可以解决从管道取数据的阻塞问题
//1.定义一个管道 10个数据int
intChan := make(chan int, 10)
for i := 0; i < 10; i++ {
intChan<- i
}
//2.定义一个管道 5个数据string
stringChan := make(chan string, 5)
for i := 0; i < 5; i++ {
stringChan <- "hello" + fmt.Sprintf("%d", i)
}
//传统的方法在遍历管道时,如果不关闭会阻塞而导致 deadlock
//问题,在实际开发中,可能我们不好确定什么关闭该管道.
//可以使用select 方式可以解决
//label:
for {
select {
//注意: 这里,如果intChan一直没有关闭,不会一直阻塞而deadlock
//,会自动到下一个case匹配
case v := <-intChan :
fmt.Printf("从intChan读取的数据%d\n", v)
time.Sleep(time.Second)
case v := <-stringChan :
fmt.Printf("从stringChan读取的数据%s\n", v)
time.Sleep(time.Second)
default :
fmt.Printf("都取不到了,不玩了, 程序员可以加入逻辑\n")
time.Sleep(time.Second)
return
//break label
}
}
}
channel关闭
使用内置函数close可以关闭channel, 当channel关闭后,就不能再向channel写数据了,但是仍然 可以从该channel读取数据
such as :
package main
import (
"fmt"
)
func main() {
intChan := make(chan int, 3)
intChan<- 100
intChan<- 200
close(intChan) // close
//这是不能够再写入数到channel
//intChan<- 300
fmt.Println("okook~")
//当管道关闭后,读取数据是可以的
n1 := <-intChan
fmt.Println("n1=", n1)
}
channel支持for–range的方式进行遍历,被关闭的信道会禁止数据流入, 是只读的。我们仍然可以从关闭的信道中取出数据,但是不能再写入数据了。
channel遍历
1 ) 在遍历时,如果channel没有关闭,则会出现deadlock的错误
2 ) 在遍历时,如果channel已经关闭,则会正常遍历数据,遍历完后,就会退出遍历
package main
import (
"fmt"
)
func main() {
intChan := make(chan int, 3)
intChan<- 100
intChan<- 200
close(intChan) // close
//这是不能够再写入数到channel
//intChan<- 300
fmt.Println("okook~")
//当管道关闭后,读取数据是可以的
n1 := <-intChan
fmt.Println("n1=", n1)
//遍历管道
intChan2 := make(chan int, 100)
for i := 0; i < 100; i++ {
intChan2<- i * 2 //放入100个数据到管道
}
//遍历管道不能使用普通的 for 循环
// for i := 0; i < len(intChan2); i++ {
// }
//在遍历时,如果channel没有关闭,则会出现deadlock的错误
//在遍历时,如果channel已经关闭,则会正常遍历数据,遍历完后,就会退出遍历
close(intChan2)
for v := range intChan2 {
fmt.Println("v=", v)
}
}
waitgroup
WaitGroup在go语言中,用于线程同步,单从字面意思理解,wait等待的意思,group组、团队的意思,WaitGroup就是指等待一组,等待一个系列执行完成后才会继续向下执行。
-
WaitGroup能够一直等到所有的goroutine执行完成,并且阻塞主线程的执行,直到所有的goroutine执行完成。
-
WaitGroup总共有三个方法:Add(delta int),Done(),Wait()。简单的说一下这三个方法的作用。
-
Add:添加或者减少等待goroutine的数量;
-
Done:相当于Add(-1);
-
Wait:执行阻塞,直到所有的WaitGroup数量变成 0;
google官方示例:
package main
import (
"fmt"
"sync"
"net/http"
)
func main() {
var wg sync.WaitGroup
var urls = []string{
"http://www.golang.org/",
"http://www.google.com/",
"http://www.baiyuxiong.com/",
}
for _, url := range urls {
// Increment the WaitGroup counter.
wg.Add(1)
// Launch a goroutine to fetch the URL.
go func(url string) {
// Decrement the counter when the goroutine completes.
defer wg.Done()
// Fetch the URL.
http.Get(url)
fmt.Println(url);
}(url)
}
// Wait for all HTTP fetches to complete.
wg.Wait()
fmt.Println("over");
}
执行结果:
http://www.baiyuxiong.com/
http://www.google.com/
http://www.golang.org/
over
从执行结果可看出:
1、取三个网址信息的时候,结果显示顺序与for循环的顺序没有必然关系。
2、三个goroutine全部执行完成后,wg.Wait()才停止等待,继续执行并打印出over字符。
锁
互斥锁
互斥锁是传统并发程序进行共享资源访问控制的主要方法。Go中由结构体sync.Mutex表示互斥锁,保证同时只有一个 goroutine 可以访问共享资源。
such as :
package main
import (
"fmt"
"sync"
//"sync"
"time"
)
func main() {
var mutex sync.Mutex
num := 0
// 开启10个协程,每个协程都让共享数据 num + 1
for i := 0; i < 1000; i++ {
go func() {
mutex.Lock() // 加锁,阻塞其他协程获取锁
num += 1
mutex.Unlock() // 解锁
}()
}
// 大致模拟协程结束 等待5秒
time.Sleep(time.Second * 5)
// 输出1000,如果没有加锁,则输出的数据很大可能不是1000
fmt.Println("num = ", num)
}
注意的问题:
- 对同一个互斥量的锁定和解锁应该成对出现,对一个已经锁定的互斥量进行重复锁定,会造成goroutine阻塞,直到解锁
- 对未加锁的互斥锁解锁,会引发运行时崩溃
读写锁
在开发场景中,经常遇到多处并发读取,一次并发写入的情况,Go为了方便这些操作,在互斥锁基础上,提供了读写锁操作。
互斥锁与读写锁的性能比较:
RWMutex显然更适用于读多写少的场景。仅针对读的性能来说,RWMutex要高于Mutex,因为rwmutex的多个读可以并存。
读写锁特点:
- 写操作与读操作之间也是互斥的
- 读写锁控制下的多个写操作之间是互斥的,即一路写
- 多个读操作之间不存在互斥关系,即多路读
在Go中,读写锁由结构体sync.RWMutex表示,包含两对方法:
// 设定为写模式:与互斥锁使用方式一致,一路只写
func (*RWMutex) Lock() // 锁定写
func (*RWMutex) Unlock() // 解锁写
// 设定为读模式:对读执行加锁解锁,即多路只读
func (*RWMutex) RLock()
func (*RWMutex) RUnlock()
注意:
- 所有被读锁定 的goroutine会在写 解锁 时唤醒
- 读 解锁 只会在没有任何 读 锁定 时,唤醒一个要进行 写锁定 而被阻塞的goroutine
- 对未被锁定的读写锁进行写解锁或读解锁,都会引发运行时崩溃(要成对出现)
- 对同一个读写锁来说,读锁定可以有多个,所以需要进行等量的读解锁,才能让某一个写锁获得机会,否则该goroutine一直处于阻塞
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var rwm sync.RWMutex
for i := 0; i < 3; i++ {
go func(i int) {
rwm.RLock()
// 各个读锁之间不必等待另一个锁释放
fmt.Println("Ready Lock reading i:", i)
time.Sleep(time.Second * 2)
rwm.RUnlock()
}(i)
}
time.Sleep(time.Millisecond * 100)
// 读锁没有完全释放 写 这个进程就会一直等待
// 需要等全部的读锁释放
rwm.Lock()
fmt.Println("Ready Locked writing ")
rwm.Unlock()
}
补充:
死锁概念:
两个协程之间都持有对方锁需要的资源,并且在等待对方资源的释放,导致一直等待,从而产生死锁
感谢大家的观看,如有不正确,欢迎批评指正~~