闻道Go语言,6月龄必知必会
大家好,我是马甲哥,
学习新知识, 我的策略是模仿-->归纳--->举一反三,
在同程倒腾Go语言一年有余,本次记录《闻道Go语言,6月龄必知必会》,形式是同我的主力语言C#做姿势对比。
1. 宏观预览
1.1 常见结构对比
某些不一定完全对标,实现方式,侧重点略点差异。
go语言 | --- | C#语言 | --- |
---|---|---|---|
module | assembly | ||
pkg | go get github.com/thoas/go-funk | package | Install-Package Masuit.Tools.Core |
struct | class | ||
pointer | reference | ||
net/http | web脚手架、 httpclient | ASP.NETCore、httpclient | |
net/http/DefaultServeMux | ASP.NETCore脚手架路由 | ||
goroutine | 异步任务、 async/await | ||
channel | CSP | TPL data flow | CSP模型在C#并非主流 |
context | timeout、 cancellation-token |
golang init函数
是特殊的函数
- 基本特征: 在main函数之前由golang自动调用, 没有参数和返回值;
- 用途:用于初始化变量, 一般用于初始化在编译期无法确定的 全局变量, 如可以在init函数通过环境变量初始化全局变量;
- 结构化特征: init函数是package级别, 一个包内多个文件的init函数,调用顺序与文件名顺序一致。
- 关联特征: A包中引入的其他B包,会先调用该B包的init函数,
1.2 访问级别
go语言使用[首字母大小]写来体现公开/私有, 应用到package struct function;
C#显式使用关键字来体现。
1.3 类型初始化
go语言有两初始化的内置关键字
- new : 返回已经清零内存的指针,new(T)返回*T, new(int), new(Cat)
- make :返回一个复杂结构, make(T,args)只用于slice、map、 channel引用类型的初始化, 返回普通的T, 通常情况下T内部有隐式的指针。
C#基础类型使用字面量, 引用类型使用new关键字
golang 严格定义了代码格式,所有的变量必须使用才会编译通过, 一个很搞笑的实例如下
pp := &Person{Name: "foo", Age: 42}
// 没有这句,编译不通过: 上面一行会爆: pp declared and not used compiler
fmt.Println(pp)
// 将 p 置为空
pp = nil
1.4 golang C# 对于字符串编码的区别
golang 字符串默认的utf-8编码, 一个汉字占用3个字节。since Go source text is Unicode characters encoded in UTF-8,
为了便于统计实际肉眼可见的字符数量, golang提供 rune类型。
var str = "hello,世界" //计算占用的字节数, golang默认的字符串编码是uft-8, 每个汉字占用3个字节
fmt.Println("字节数:", len(str)) // output: 12
fmt.Println("字符数:", utf8.RuneCountInString(str)) //output:8
fmt.Println("字符数:", len([]rune(str))) //output:8
C#字符串默认的编码方式是unicode
.NET uses UTF-16 encoding (represented by the UnicodeEncoding class) for string instances.
var str = "hello,世界";
Console.WriteLine("字节数:"+ str.Length); //output: 8
1.4 包文件组织方式
golang 现在使用 go module 方式, 已经解决了之前被限定在 GOROOT 和GOPATH 路径组织文件和依赖包的问题。
使用 go mod init xxx.com/demo 初始化module 之后, 引入同一路径其他文件夹的包, 要带上完整module名。
example.com/myproject/
├── go.mod
├── main.go
└── utils/
└── helper.go
在 main.go 中,如果你想要引用 utils 文件夹下的包,你应该使用模块名称加上子目录路径
C# 之间就是命名空间 namespce
2. 编码逻辑结构
2.1 顺序
这没什么好说的,都是至上而下, 遇到函数进函数堆栈。
go语言每行代码后不需要加分号;C#语言每行代码后需要加分号。
go对于括号的使用有要求: 斜对称, C#无要求。
2.2 分支
if --- elseif --- else
go和C#语言基本是一样的
- go语言else if、 else 不允许换行,C#对此无要求。
- C#要求[使用括号包围]条件判断语句。
switch -- case [break]
- go语言case语句默认都加上了break,加不加都一样,满足当前case,执行完就会跳出当前switch, 不会一直case下去;
- C#语言执行分支需要主动break, 若没有break,表示共用可用的执行体。
2.3 循环
![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0264fb5ade6247c39e7f735a33a13e08~tplv-k3u1fbpfcp-zoom-1.image)
- go语言只有for循环,C#还有while, do while , 可以认为go的for行为类似于C#foreach, 内核是C# for关键字。
使用for来体现while/do while
2.4 堆栈控制逻辑
golang: 内置defer函数 、 recover函数
C# : try catch finally
golang的defer函数用于函数压栈 (先进后出),一般用于资源的释放。
请注意,defer函数在压栈时会保存函数的入参, 并非在执行时取值。
package main
import "fmt"
func main() {
a()
b()
}
func a() {
i := 0
defer fmt.Println(i) // 压栈时保存了defer函数的参数值i=0
i++
return
}
func b() {
i := 0
defer func() { // 压栈时尝试保存defer函数参数,但是defer函数无参数,故仅仅将函数压栈, 但是又发现i是自由变量,故产生闭包,执行输出1
fmt.Println(i)
}()
i++
return
}
-- output:
0
1
defer函数可以读取和重新赋值主函数的命名返回参数。
package main
import "fmt"
func main() {
ii := c()
fmt.Println(ii)
}
func c() (i int) {
defer func() {
i++
}()
return 1
}
-- output
2
3. 面向对象
封装\抽象\继承\多态
同样是面向对象编程语言,go用结构体来体现,C#常用类来体现。
封装
通常go语言基于结构体struct、接收者函数来[封装/提炼]事物和行为。
-
接收者函数分为: 值接收者函数、指针接收者函数。
-
两种都能体现封装, 但[指针接收者函数]内的操作会体现到入参。
-
不管是值,还是指针,都能调用指针接收者函数/值对象接受者函数,效果还是如上一点一致。
C# 显式使用Class
struct
等结构来封装数据和行为。
抽象 + 继承
go语言没有抽象函数、抽象类的说法,有接口抽象 和父子类继承关系。
接口将具有共性的方法放在一起,其他任何类型只要实现了这些方法就是实现了接口,俗称鸭子模式。
C#具备语义化的继承/抽象/多态, 显式继承。
golang里面更常见的是[组合,利用的继承]和[重写],
golang里面获取 response statuscode就可以用这一方式,默认的response在pipeline完成之前,会设定statuscode, 但是没有提供外部的api或者属性拿到 statusocde。
可以通过组合,重写开放response的行为:
type MResponse struct {
http.ResponseWriter
statuscode int
}
func (m *MResponse) WriteHeader(code int) {
m.ResponseWriter.WriteHeader(statusCode) // 这是原本response要执行的行为
m.ststuscode = code // 给个机会,外部MResponse可以自行使用statuscode
}
go 里面有空 struct 结构:
var s struct {}
不占用内存,类型占用空间大小是0 (unsafe.szie()=0), 在具体分配内存时, 根据这个size=0, 固定返回一个 全局地址,所有空struct 地址值是一样且固定的。
这个类型因为不包含字段,所以无状态,可以做标记位 ; 因为占用空间是0 ,常做不重复集合set的实现。
4. 指针 vs 引用
指针指向一个内存地址; 引用指向内存中某个对象。
一般认为go是C语言的家族,但是go的指针弱化了C语言的指针操作,go指针的作用仅操作其指向的对象, 不能基于地址这个概念做指针移位, 也不能基于地址这个概念做类型转化。
A value of a pointer type whose base type is T can only store the addresses of values of type T.
go的指针简化了指针的使用,减少了指针出错的概率。
引用可看做是指针的抽象,也基于code safe的理由,不能在引用上做算术运算和低级别的取巧。
从这个意义上看,C#的引用等价于go的指针, 都是类型安全的指针。
另一方面, 两种语言都提供了对内存进行任意读写的姿势(非代码安全)。
go的unsafe.Pointer本质是一个int指针。
type Pointer *ArbitraryType
、
type ArbitraryType int
C# unsafe
关键字可用在函数、属性、构造函数、代码块。
5. goroutine vs async-await
表象
- goroutine由go的原生函数生成,只要前面加上go的语法关键字
go
(可以有形参,返回值会被忽略)。 - await/async语法糖,简化了异步编程的姿势;实际会被编译器编译成一个状态机。
goroutine是在runtime级别深度内置, async-await是在CLR之上基于C#语言构建。
核心对比
首先要知道: 线程是cpu调度的最小单位,不管是goroutine还是async-wait机制都是在尝试提高[cpu调度线程的效率]。
-
go在os内核线程之上,原生支持了轻量级的用户态线程goroutine,堆栈很小,开销很小 (存在一个用户态逻辑处理器给线程投喂goroutine)。
-
C#编译器生成的状态机,转化并管控基于线程池线程的主调任务、异步任务、后继任务。
两者支持并发的思路有明显差异:
go: 内核态线程切换开销大,原生提供用户态线程,开销极小,天然支持高并发,且不轻易坠落到内核态, 是一个革命派的思路。
C#:async-await针对线程调度做辗转腾挪,高效利用, 是一个改良派的思路。
异步
都具备异步的能力,go语言没有await的概念,goroutine在等待通道读操作时[挂起自身,并将OS线程释放给另一个goroutine], 跟C#执行时遇到await关键字的行为效果是一样的。
推荐附加阅读
-
https://grantjam.es/concurrency-comparing-golangs-channels-to-c-sharps-asyncawait/
-
https://techstacks.io/posts/6628/go-vs-csharp-part-1-goroutines-vs-async-await
本文限于篇幅,只记录了go语言和C#语言的入门6月龄的核心差异点和重难点,高手绕道, 后续会不断完善, 请有心人持续关注左下角原文, 如果能点赞更是莫大的鼓励。
本文来自博客园,作者:{有态度的马甲},转载请注明原文链接:https://www.cnblogs.com/JulianHuang/p/16825391.html
欢迎关注我的原创技术、职场公众号, 加好友谈天说地,一起进化