golang基础知识和用法细节

本文介绍了Golang的基础知识,包括语言结构如package和import,数据类型如整型、浮点型和字符串,以及语法特性如变量、常量和运算符。文章还讨论了Golang的并发模型,通过goroutine和通道实现,并提到了交叉编译和依赖管理。此外,接口和异常处理也是文中涵盖的内容。
摘要由CSDN通过智能技术生成

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

环境配置就不说了

概述

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

语言结构

  • 先看一个简单的程序
package main

import "fmt"

func f() int {
	fmt.Println("function f")
	return 123456
}

var T = f()

func init() {
	fmt.Println("init")
}
func main() {
	var a int
	sf, 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))// 6

	s2 := "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 // 0
	TUESDAY// 1
	WEDNESDAY// 2
)
  • 同时golang支持多变量赋值,比如C++中的swap函数我们可以一行实现
func main() {
	x := 1
	y := 2
	x, y = y, x
	println(x, y)
}
  • 同样的,由于有指针的概念,golang中也有值和引用的区别,对于基本类型,关于函数参数,如果不取地址传进去的就是值,对于map等类型,传进去的是引用,更改之后会影响到原来的map
  • 所以对于比较大的类型,我们应该传的是变量的引用,这样能加快速度
  • 但是事情不是这么简单的,因为函数中定义的指针一般是存储在栈中的,但是如果指针作为函数返回值,会发生逃逸现象,也就是变量从栈逃逸到堆,进而引发后续的垃圾回收,导致性能降低;在golang语言中,由编译器进行逃逸分析之后选择到底把变量分配到堆区还是栈区而不是是否进行了new操作,所以说到底传值还是引用需要综合考虑,如果涉及到大量的修改操作传引用比较好,否则传值可以减少垃圾回收的次数

运算符

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

语句

  • 只讲一些比较特殊的,golang没有while,只有for。然后有一个特殊的select语句,
package main

func 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)
			break
		case <-channel2:
			println(2)
			break
		default:
			println(3)
		}
	}
	close(channel1)
	close(channel2)
}
  • select语句的每一个case都必须是一个通道
  • 所有的channel表达式都会被求值
  • 所有被发送的表达式都会被求值
  • 如果有某个通道可以执行,它就执行,否则被忽略
  • 如果有多个case都可以运行,select会随机公平的选出一个执行,其他不会执行,否则如果有default则执行,要么就阻塞直到某个通道可以运行

结构体

  • 下面的程序定义了一个结构体
package main

import (
	"fmt"
	"unsafe"
)

type st struct {
	ck bool
	id int32
	c  string
}

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

切片

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

Map集合

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

接口

  • 我们声明一个接口并实现如下
package main

import "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 main

import "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 main

import (
	"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包来提供一些锁操作

交叉编译

  • go可以在Windows下编译出能够在其他操作系统中运行的程序,主要有GOOS,GOARCH,CGO_ENABLED这三个环境变量
  • GOOS表示目标平台的操作系统(linux,darwin,windows等)
  • GOARCH表示目标平台的体系架构,例如amd64等
  • CGO_ENABLED设为0表示禁用CGO

例如我们在Windows下编译生成Linux环境下的程序,可以用

go env -w CGO_ENABLED=0
go env -w GOOS=linux
go env -w GOARCH=amd64
go build -o hello main.go
  • 注意需要看自己的Linux系统是什么内核架构,如果是x84-64,需要用
go env -w CGO_ENABLED=0
go env -w GOOS=linux 
go env -w GOARCH=x86
go build -o hello main.go

这里的-o指的是将生成的文件命名为hello,如果文件不能执行,需要给执行权限,也就是chmod u+x

依赖引入

  • 在项目中引入的依赖如果想要更新,可以直接修改go.mod文件,也可以使用go get命令,在这之后需要使用go clean -modcache清除本地缓存的依赖项,否则还是原来的依赖

原因

如果你想把一个间接引入的包从v1升级到v2,可能会遇到这样的问题,你不知道哪个包依赖了这个包从而无法升级,可以使用下面的命令查看哪些包依赖了这个包,从而把源包升级之后才能将间接引入的包升级

go mod why -m 包名
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Clarence Liu

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

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

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

打赏作者

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

抵扣说明:

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

余额充值