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"
)<