一天速转golang!
首先你需要会一门后端语言,以及要有基本的网络编程能力和并发思想
环境
本人在linux环境下下载的go1.23.3版本,使用Vim+go作为IDE
具体做法此处不再赘述,自行查阅其他博客
第一个Golang程序
- package main ——定义声明包名(可以类比java来理解go)
必须在源文件中非注释的第一行指明这个文件属于哪个包,如:package main。package main表示一个可独立执行的程序,每个 Go 应用程序都包含一个名为 main 的包。
- import "fmt" ——引入包
告诉 Go 编译器这个程序需要使用 fmt 包(的函数,或其他元素),fmt 包实现了格式化 IO(输入/输出)的函数。(本例中的println函数)
四种变量的声明方式
省去var的方式只用于局部变量!
//声明全局变量 方法一二三都可以 方法四不可以!
func main(){
//方法一:声明一个变量,用var int的话,不给他初始值,默认为0
var a int
fmt.Println("a = ", a) //a = 0
//方法二:声明一个变量,给他一个初始值
var b int = 100
fmt.Println("b = ", b) //b = 100
//方法三:初始化的时候,可以省去数据类型,通过值自动匹配当前变量的数据类型
var c = 100
fmt.Println("c = ", c) //c = 100
fmt.Println("type of c = %T\n", c) //打印c的类型
//方法四:(最常用)省去var关键字,直接自动匹配
e := 100
fmt.Println("e = ", e) //e = 100
fmt.Println("type of e = %T\n", e) //打印c的类型
}
//声明多个变量
var xx, yy int = 100,200
var kk, ll = 100, "ABC"
var{
vv int = 100
jj bool = true
}
const与iota的注意事项
iota是逐行累加,一行再多变量,他们的iota也是一样的
iota的使用非常局限,只能配合const出现
//const来定义枚举变量
const{
//可以在const()添加关键字iota,每行的iota都会累加1,第一行的iota默认0
BEIJING = iota
SHANGHAI
HAERBING
}
const{
a, b = iota+1, iota+2 // iota = 0, a = iota+1, b = iota+2, a = 1, b = 2
c, d // iota = 1, c = iota+1, d = iota+2, c = 2, d = 3
e, f // iota = 2, e = iota+1, f = iota+2, e = 3, f = 4
g, h = iota*2, iota*3 // iota = 3, e = iota*2, f = iota*3, e = 6, f = 9
i, k // iota = 4, e = iota*2, f = iota*3, e = 8, f = 12
}
func main(){
//const是常量,只读属性,不可修改
const length int = 10
fmt.Println("SHANGHAI = ", SHANGHAI) // 1
fmt.Println("HAERBING = ", HAERBING) // 2
}
多返回值的三种写法
//返回一个返回值
func foo1(a string, b int) int {
...
c := 100
return c
}
//返回多个返回值,匿名
func foo2(a string, b int) (int, int){
...
return 666,777
}
//返回多个返回值,有形参名
func foo3(a string, b int) (r1 int, r2 int){
...
r1 = 1000
r2 = 2000
return
}
//返回多个返回值,有形参名,r1, r2 int也行,逆天语法
func foo4(a string, b int) (r1, r2 int){
...
r1 = 1000
r2 = 2000
return
}
import相关
main,import以及init的流程:
import之后不会继续执行,而是解析包
现在自定义两个包,lib1.go和lib2.go,路径如下
//lib1.go
package lib1
import "fmt"
func Lib1Test(){
fmt.Println("lib1Test()...")
}
func main(){
fmt.Prinln("lib1.init()...")
}
//lib2.go
package lib2
import "fmt"
func Lib21Test(){
fmt.Println("lib2Test()...")
}
func main(){
fmt.Prinln("lib2.init()...")
}
//main.go
package main
import(
"GolangStudy/5-init/lib1"
"GolangStudy/5-init/lib2"//必须写路径,不然找不到
)
func main(){
lib1.Lib1Test() //go声明必须使用,这里每一行都不能少
lib2.Lib2Test()
}
此外,import还有匿名导包和给包取别名的方式:
匿名导包用在,咱只需要这个包的某一个函数,但不想import这个包
_"GolangStudy/5-init/lib1"
加个下划线就行
给包起别名,比如包太长了,不愿意总写
mylib2 "GolangStudy/5-init/lib2"
前面加个mylib2即可!接下来调用这个包的函数就是->mylib2.Lib2Test()
或者:
. "GolangStudy/5-init/lib2"
用.去省略,接下来调用这个包的函数就是->Lib2Test()
.不要轻易使用!
defer的执行顺序
defer是在结束的时候执行,并且defer是栈的形式,第一个defer先入栈,后一个再入栈,那么后一个就是先出栈的
func main(){
//写入defer关键字
defer fmt.Println("main end1")
defer fmt.Println("main end2")
fmt.Println("main::hello go 1")
fmt.Println("main::hello go 2")
}
/*
main::hello go 1
main::hello go 2
main end2
main end1
*/
defer和return是谁先谁后?结果是return快于defer,return会先被调用,defer是程序所有函数生命周期结束才被调用的!
切片slice相关
slice是动态数组的类型
定义遍历和打印:
//变长数组
func printArray(myArray []int){
for _, value := range myArray{
fmt.Println("value = ", value)
}
}
func main(){
myArray := []int{1,2,3,4}
fmt.Print("myArray type is %T\n", myArray)
}
slice的4种声明:
func main(){
//声明slice1是一个切片,并且初始化,默认值是1,2,3,长度是3
slice1 := []int{1,2,3}
//声明slice1是一个切片,但是并没有给slice1分配空间,也就是完全没有容量
var slice1 []int
slice1 = make([]int, 2)//先给slice1开辟空间
//声明slice是一个切片,同时给slice1分配空间,3个空间,初始是0
var slice1 []int = make([]int, 3)
//通过:=推导出slice1是一个切片
slice1 := make([]int, 3)
//判断slice是否为0
if slice == nil{
fmt.Println("slice1是一个空切片")
}
else{
fmt.Println("slice1是有空间的")
}
}
切片容量的追加:
func main(){
var numbers = make([]int, 3, 5)
fmt.Println("len = %d, cap = %d, slice = %v\n", len(numbers), cap(numbers))//cap求容量,打印结果:len = 3,cap = 5,slice = [0 0 0]
通过append追加值
//向numbers切片追加一个元素1,, numbers len = 4, [0 0 0 1], cap = 5
numbers = append(numbers, 1)
cap满了之后再append会怎样?底层会再次开辟一个cap空间
切片截取:
numbers := []int{0,1,2,3,4,5,6,7,8}
//切片截取
fmt.Println("numbers[1:4] ==", numbers[1:4]) // 打印子切片从索引1(包含)到索引4(不包含)
fmt.Println("numbers[:3] ==", numbers[:3]) // 默认下限为0
fmt.Println("numbers[4:] ==", numbers[4:]) //默认上限为len(s)
map相关
map的3种声明方式
//声明方式一
var myMap1 map[string]string
//使用map前,给他开辟空间
make(map[string]string, 10)
myMap2["one"] = "java"
myMap2["two"] = "c++"
myMap2["three"] = "python"
//声明方式二
myMap2 := make(map[int]string)
myMap2[1] = "java"
myMap2[2] = "c++"
myMap2[3] = "python"
//声明方式三
myMap3 := map[string]string{
"one": "php",
"two": "c++",
"three": "python",
}
map的使用:
cityMap := make(map[string]string)
//添加
cityMap["China"] = "Beijing"
cityMap["Japan"] = "Tokyo"
cityMap["USA"] = "newYork"
//遍历
for key, value := range cityMap{
fmt.Println("key = ", key)
fmt.Println("value = ", value)
}
//删除
delete(cityMap, "China")
//修改
cityMap["USA"] = "DC"
golang的面向对象
封装
type Hero struct{
Name string
Ad int
Level int
}
func (this *Hero) show(){
fmt.Println("Name = ", this.Name)
fmt.Println("Ad = ", this.Ad)
fmt.Println("Level = ", this.level)
}
func (this *Hero) GetName() string{
return this.Name
}
func (this *Hero) SetName(newName string){
//this只是调用该方法的对象的一个副本
this.Name = newName
}
继承
//父类
type Human struct{
name string
sex string
}
func (this *Human) Eat(){
fmt.Println("Human.Eat()...")
}
func (this *Human) Walk(){
fmt.Println("Human.Walk()...")
}
//子类
type SuperMan struct{
Human //SuperMan类继承了Human类的方法
level int
}
//重新定义父类的方法Eat
func (this *SuperMan) Eat(){
fmt.Println("SuperMan.Eat()...")
}
//子类新方法
func (this *SuperMan) Fly(){
fmt.Println("SuperMan.Fly()...")
}
func (this *SuperMan) Print(){
fmt.Println("name = ", this.name)
fmt.Println("sex = ", this.sex)
fmt.Println("level = ", this.level)
}
//定义一个子类对象
//s := SperMan{Human{"li4", "female"}, 88}
var s SuperMan
s.name = "li4"
s.sex = "male"
s.level = 88
多态
- 多态(Polymorphism)是面向对象编程(OOP)中的一个重要概念。它指的是同一个操作作用于不同的对象,可以有不同的解释,产生不同的执行结果。简单来说,就是用同一种方式去调用不同类中的同名方法,这些方法在不同类中有不同的实现,从而展现出不同的行为。
- 实现多态的条件
- 要有继承关系。例如,有一个基类(父类)和一个或多个派生类(子类),子类继承父类的属性和方法。
- 要有方法重写。子类重写(override)父类中的方法,即在子类中重新定义了与父类中同名、同参数列表(参数的类型、个数和顺序相同)的方法,并且方法体的内容不同。
package main
import "fmt"
// 定义一个接口,它有一个方法Sound
type Animal interface {
Sound() string
}
// 定义狗结构体
type Dog struct{}
// 狗结构体实现Animal接口的Sound方法
func (d Dog) Sound() string {
return "汪汪汪"
}
// 定义猫结构体
type Cat struct{}
// 猫结构体实现Animal接口的Sound方法
func (c Cat) Sound() string {
return "喵喵喵"
}
// 定义一个函数,它接受实现了Animal接口的对象,并调用其Sound方法
func MakeSound(a Animal) {
fmt.Println(a.Sound())
}
func main() {
dog := Dog{}
cat := Cat{}
MakeSound(dog)
MakeSound(cat)
}
interface{}是空接口,int、string、float32、float64、struct...都实现了interface{}。可以用interface{}类型,引用任意数据类型
func myFunc(arg interface{}){
fmt.Println("myFunc is called...")
fmt.Println(arg)
}
type Book struct{
auth string
}
func main(){
book := Book{"Golang"}
myFunc(book)
myFunc(3.14)
myFunc("abc")
}
/*
打印输出:
Golang
3.14
abc
*/
那么interface{}是如何区分 此时引用的底层数据是什么类型?
->给interface{}提供"类型断言"机制
func myFunc(arg interface{}){
fmt.Println("myFunc is called...")
fmt.Println(arg)
//给 interface{} 提供"类型断言"
value, ok := arg.(string)
if !ok {
fmt.Println("arg is not string type")
}else{
fmt.Println("arg is string type, value = ", value)
}
}
在代码 value, ok := arg.(string)
中,这是一个类型断言表达式,它尝试将接口类型 arg
断言为 string
类型,并且会返回两个值:
-
value
:- 如果接口
arg
确实是string
类型,那么value
就会被赋值为接口所包含的string
值。例如,如果arg
是通过传入一个字符串"hello"
得到的接口值,那么当类型断言成功时,value
就会被赋值为"hello"
。 - 如果类型断言失败,
value
的值是对应类型的零值。对于string
类型来说,零值就是""
(空字符串)。
- 如果接口
-
ok
:ok
是一个布尔值,它用于指示类型断言是否成功。- 如果
arg
确实是string
类型,那么ok
会被赋值为true
,表示类型断言成功,此时你可以放心地使用value
变量,因为它已经正确获取到了接口中所包含的string
值。 - 如果
arg
不是string
类型,那么ok
会被赋值为false
,表示类型断言失败,此时value
虽然也有值(对应类型的零值),但它并不是我们所期望的接口中原本的正确值,所以需要通过检查ok
的值来判断是否可以正确使用value
变量。
反射
变量结构如下:
OpenFile返回了一个指针类型和一个error
//tty: pair<type:*os.File, value:"/dev/tty">
tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0)
reflect包:
func ValueOf(i interface{}) Value {...}
// ValueOf用来获取输入参数接口中的数据的值,如果接口为空则返回0
func TypeOf(i interface{}) Type {...}
//TypeOf用来动态获取输入参数接口中的值的类型,如果接口为空则返回nil
具体来说
package main
import (
"fmt"
"reflect"
)
func reflectNum(arg interface{}){
fmt.Println("type: ", reflect.TypeOf(arg)) //float64
fmt.Println("value: ", reflect.ValueOf(arg)) //1.234
}
func main(){
var num float64 = 1.234
reflectNum(num)
}
结构体标签
package main
type resume struct{
Name string 'info:"name" doc:"mine"'
Sex string 'info:"sex"'
}
func findTag(str interface{}){
t := reflect.TypeOf(str).Elem()
for i := 0; i < t.NumField(); i ++{
taginfo := t.Field(i).Tag.Get("info")
tagdoc := t.Field(i).Tag.Get("doc")
fmt.Println("info:", taginfo, "doc:", tagdoc)
}
}
结构体的标签引用在json编解码或者orm映射关系
goroutine
- Goroutine 是 Go 语言特有的轻量级线程,它可以让一个函数在后台并发地执行。与传统的操作系统线程相比,Goroutine 非常轻量,一个 Go 程序可以轻松地创建成千上万个 Goroutine。
- 例如,在一个 Web 服务器中,可以为每个传入的请求创建一个 Goroutine 来处理,这样就能够高效地处理多个请求,而不会因为创建大量线程导致系统资源耗尽。
- 创建 Goroutine 非常简单,只需要在函数调用前加上
go
关键字。
go func() {
defer fmt.Println("A.defer")
//匿名函数
func() {
defer fmt.Println("B.defer")
//退出当前goroutine
runtime.Goexit()
fmt.Println("8")
}()
for{
time.Sleep(1*time.Second)
}
}
/*输出
B
B.defer
A
A.defer
*/
channel
-
概念
- 在 Go 语言中,
channel
是一种用于在不同的goroutine
之间进行通信和同步的机制。它可以被看作是一个管道,数据可以通过这个管道在goroutine
之间传递。
- 在 Go 语言中,
-
基本操作
- 创建 channel:可以使用
make
函数来创建channel
。channel
有两种类型:无缓冲的channel
和有缓冲的channel
。- 无缓冲的 channel:
- 示例:
ch := make(chan int)
。这创建了一个可以传输int
类型数据的无缓冲channel
。无缓冲channel
在发送和接收数据时必须有对应的接收者和发送者同时准备好,否则会导致goroutine
阻塞。
- 示例:
- 有缓冲的 channel:
- 示例:
ch := make(chan int, 3)
。这创建了一个可以传输int
类型数据的有缓冲channel
,缓冲大小为 3。有缓冲channel
允许在没有接收者的情况下,发送者可以发送一定数量(缓冲大小)的数据,这些数据会存储在channel
的缓冲区中,直到被接收。
- 示例:
- 无缓冲的 channel:
- 发送和接收数据:
- 发送数据:使用
ch <- value
的语法,其中ch
是channel
,value
是要发送的数据。例如,ch := make(chan int); go func() { ch <- 5 }()
,这里在一个goroutine
中向channel
发送了数字 5。 - 接收数据:使用
value := <-ch
的语法来接收channel
中的数据。例如,ch := make(chan int); go func() { value := <-ch; fmt.Println(value) }()
,这个goroutine
会从channel
中接收数据并打印出来。
- 发送数据:使用
- 创建 channel:可以使用
-
同步作用
channel
除了用于数据传输,还具有同步goroutine
的作用。- 例如,当一个
goroutine
向无缓冲channel
发送数据时,如果没有其他goroutine
来接收这个数据,发送数据的goroutine
会阻塞,直到有接收者出现。同样,当一个goroutine
试图从一个空的channel
接收数据时,它也会阻塞,直到有数据被发送到这个channel
。 - 这种阻塞机制可以确保
goroutine
之间的协调和同步。比如在一个并发计算任务中,一个goroutine
完成部分计算后,通过channel
发送结果给另一个goroutine
,第二个goroutine
在收到数据前会一直等待,这样就保证了数据的正确传递和任务的有序进行。
-
关闭 channel
- 可以使用
close(ch)
来关闭channel
。关闭channel
后,再试图向其发送数据会引发panic
,但仍然可以从已经关闭的channel
中接收数据。 - 接收已关闭
channel
的数据时,会正常接收缓冲区中剩余的数据,当缓冲区为空时,会得到对应类型的零值,并且可以通过一个额外的返回值来判断channel
是否已经关闭。例如,value, ok := <-ch
,如果ok
为false
,则表示channel
已经关闭。 - 关闭
channel
通常用于表示数据发送已经完成,让接收者知道不会再有新的数据发送过来了。例如,在一个生产者 - 消费者模型中,当生产者完成所有数据的生产后,可以关闭channel
,消费者在接收完剩余数据后,通过检查channel
是否关闭来结束接收操作。
- 可以使用
func main() {
c := make(chan int)
go func(){
defer fmt.Println("goroutine结束")
fmt.Println("goroutine 正在运行...")
c <- 666 //将666发送给c
}()
num := <-c //从c中接受数据,并赋值给num
fmt.Println("num = ", num)
fmt.Println("main goroutine 结束...")
}
/* 输出
goroutine 正在运行...
goroutine结束
num = 666
main goroutine 结束...
*/