> 文章列表 > golang基础知识和用法

golang基础知识和用法

golang基础知识和用法

阅读本文需要一些编程基础

环境配置就不说了

golang

  • 概述
  • 语言结构
  • 数据类型
  • 语法
    • 变量和常量
    • 运算符
    • 语句
    • 结构体
    • 切片
    • Map集合
  • 接口
  • 异常
  • 并发

概述

可以先看看golang的FAQS,有很多问题都有设计者的解答,最为客观
https://go.dev/doc/faq#no_pointer_arithmetic

语言结构

  • 先看一个简单的程序
package mainimport "fmt"func f() int {fmt.Println("function f")return 123456
}var T = f()func init() {fmt.Println("init")
}
func main() {var a intsf, err := fmt.Scanf("%d", &a)if err != nil {return}println(123, 456)fmt.Println(sf, 1)fmt.Printf("%c\\n", 48)s := fmt.Sprintf("%s%d%T", "123", 123, 1234)fmt.Println(s)
}
  • 首先观察这个程序,package指明当前的包名,import表示引入了哪些包,这里的fmt包是一个golang的标准输入和输出包,这和Java类似,但是区别是main函数package需要是main
  • golang提供了两个基本的输入和输出,printprintln,它们在builtin这个库里面,这个库包含许多其他的预定义标识符,包括int等,这些标识符的实现个人理解是在编译器中进行的,包括保留字,编译器识别到这些预定义标识符之后,会把它们转化为特定的汇编代码,从而执行相关任务,所以你在golang的函数库里面是找不到print函数的实现的
  • 那么为什么要引入一个fmt库呢?因为golang内置的输出函数没什么功能,而fmt库里面提供了包括格式化输入输出,格式化字符串(fmt.Sprintf)等等,这些函数的使用是和C++的输入和输出是类似的
  • 上面展示了简单的输入和输出
  • 这里面还有一些特殊的地方,就是这个main的开始不是真开始,结束也不是真结束,这是我自己个人理解,关于这句话的详细解释需要深入研究。那么放到这个文件中就是首先进行变量T的初始化,先执行f函数,然后执行init函数,之后才轮到main的执行

数据类型

  1. 整型包括int,int8,int16,uint,uint8,uintptr等等
  2. 浮点数包括float32和float64
  3. 复数包括complex64(实部和虚部各32位)和complex128
  4. 还有一些其他类型包括typerune等等,使用它们的原因是便于区分字节值和字符类型的值,(相当于springboot中的@Service@Component注解,作用基本没区别,只是用于分层),type等价于uint8rune等价于int32

简单说一点,每个字符占用的字节长度是不一定一样的,普通字符(ACSII字符)占用1个字节,但是汉字占用3个字节,所以如果使用字符串下标去修改字符串值可能是错误的

  • 可以使用len函数来进行简单测试
func main() {s := "你好"fmt.Println(len(s))// 6s2 := "s2"fmt.Println(len(s2))// 2
}
  • 正确的做法应该是先把字符串转化为rune,也就是每个字符都用4个字节来表示,然后再去修改,这样才是正确的
func main() {s := "1s你好"s2 := []rune(s)fmt.Println(len(s))
}
  • 此外,string类型和Java中是类似的,都是final类,也就是不可修改,线程安全,所以不能够直接去修改,需要拷贝一份,再拷贝出来的数组上进行修改

语法

变量和常量

  • 定义一个变量可以使用var identifier type这种方式,也可以使用var indentifier = value这种形式,还可以使用短变量声明,也就是indentifier := value,第一种方式适合延迟赋值,后两种方式都会推断变量类型
  • 注意短变量声明相当于定义之后赋值,所以不能在已经定义了的变量之后再次使用,当然还有一个需要注意的是变量作用域,如果在一个代码段内进行短变量声明不会影响到代码段外面的相同名字的变量,也就是幽灵变量的问题
  • 对于常量来说,适当使用iota可以起到枚举类型的作用,下面观察一下truefalse的实现
const (true  = 0 == 0 // Untyped bool.false = 0 != 0 // Untyped bool.
)
// 下面的例子就是iota
const (MONDAY = iota // 0TUESDAY// 1WEDNESDAY// 2
)
  • 同时golang支持多变量赋值,比如C++中的swap函数我们可以一行实现
func main() {x := 1y := 2x, y = y, xprintln(x, y)
}
  • 同样的,由于有指针的概念,golang中也有值和引用的区别,对于基本类型,关于函数参数,如果不取地址传进去的就是值,对于map等类型,传进去的是引用,更改之后会影响到原来的map
  • 所以对于比较大的类型,我们应该传的是变量的引用,这样能加快速度
  • 但是事情不是这么简单的,因为函数中定义的指针一般是存储在栈中的,但是如果指针作为函数返回值,会发生逃逸现象,也就是变量从栈逃逸到堆,进而引发后续的垃圾回收,导致性能降低;在golang语言中,由编译器进行逃逸分析之后选择到底把变量分配到堆区还是栈区而不是是否进行了new操作,所以说到底传值还是引用需要综合考虑,如果涉及到大量的修改操作传引用比较好,否则传值可以减少垃圾回收的次数

运算符

  • golang的运算符和C++类似,就不细说了

语句

  • 只讲一些比较特殊的,golang没有while,只有for。然后有一个特殊的select语句,
package mainfunc main() {channel1 := make(chan int)channel2 := make(chan int)go func() {for i := 0; i <= 100; i++ {channel1 <- -10}}()go func() {for i := 0; i <= 10; i++ {channel2 <- 2}}()for i := 0; i <= 120; i++ {select {case <-channel1:println(1)breakcase <-channel2:println(2)breakdefault:println(3)}}close(channel1)close(channel2)
}
  • select语句的每一个case都必须是一个通道
  • 所有的channel表达式都会被求值
  • 所有被发送的表达式都会被求值
  • 如果有某个通道可以执行,它就执行,否则被忽略
  • 如果有多个case都可以运行,select会随机公平的选出一个执行,其他不会执行,否则如果有default则执行,要么就阻塞直到某个通道可以运行

结构体

  • 下面的程序定义了一个结构体
package mainimport ("fmt""unsafe"
)type st struct {ck boolid int32c  string
}func main() {p := &st{ck: false,id: 2147483647,c:  "123",}fmt.Println(unsafe.Sizeof(p))
}
  • 有两种方式定义结构体变量,指针或者结构体本身,一般用前者,因为通常比较省空间,在定义结构体的时候,需要注意结构体内存对齐的问题,不同的定义顺序会导致结构体占用的空间不同,具体可以查阅相关资料

切片

  • 切片是对数组的抽象,数组长度是不可变的,但是切片长度可变,有长度和容量的两个概念,当添加元素超过容量会引发扩容,扩容方式见源码文件runtime\\slice.go下的growslice函数,比较复杂

Map集合

  • 这个map是无序的,如果想删除其中元素,需要调用delete方法

接口

  • 我们声明一个接口并实现如下
package mainimport "fmt"type Animal interface {Speak()
}
type Dog struct {
}type Cat struct {
}func (dog Dog) Speak() {fmt.Println("Dog is speaking.")
}
func (cat Cat) Speak() {fmt.Println("Cat is speaking.")
}
func main() {animals := []Animal{Dog{}, Cat{}}for _, value := range animals {value.Speak()}
}
  • golang中没有类似Java中的implements关键字,实现一个接口只需要指明是哪个类实现的即可

异常

  • 抛出一个异常通常使用panic,但是Go提供了一种从异常中恢复的方法,就是recover,看下面的例子
package mainimport "fmt"func main() {defer func() {if err := recover(); err != nil {fmt.Println("an error recovered")}}()defer func() {defer func() {if err := recover(); err != nil {fmt.Println("here.")}}()defer func() {println(1)panic("error 1.")}()println(2)panic("error 2.")}()println(3)panic("error 3.")}
/*
3
2
1
here.
an error recovered
*/
  • 如果想让程序从异常中恢复,需要在panic之后调用recover,但是panic程序就已经退出了,怎么调用recover呢?所以需要利用Go的特性defer,也就是在panic之后执行recover函数,这样程序就会不会直接崩溃而是恢复正常

并发

  • golang主要是通过goruntine来实现的并发,它的并发是协程并发,在语言层面支持协程,这可能是通过它的context来实现的,因为需要记录上下文等信息
package mainimport ("fmt"
)func main() {ch := make(chan int)go func() {for i := 0; i <= 3; i++ {ch <- i}close(ch)}()for i := range ch {fmt.Println(i)}
}
  • 上面就实现了一个简单的并发,我设置通道缓冲区大小是0,程序在main goroutinego func两个协程并发执行
  • 此外golang还提供了sync包来提供一些锁操作