Golang与C#技术栈的核心对比

学习新知识,我的策略是模仿-->归纳--->举一反三,

本次记录入局Golang,6月龄必知必会,形式是同我的主力语言C#做对比。

1. 宏观预览

1.1 常见结构对比

go语言 --- C#语言 ---
module assemble
package go get github.com/thoas/go-funk namespace Install-Package Masuit.Tools.Core
struct class、struct
pointer reference
net/http web脚手架、 httpclient kestrel、httpclient
net/http/DefaultServeMux ASP.NETCore脚手架路由
goroutine go语法糖 基于线程池的异步任务 async/await语法糖
channel CSP TPL data flow CSP模型在C#并非主流
context timeout、 cancellation-token

1.2 访问级别

go语言使用首字母大小写来体现公开/私有, 应用到package/struct/function;

C#显式使用关键字来体现, public、protected、private

1.3 类型初始化

go语言有两个初始化的内置关键字

  • new : 用于分配内存(带内存零值),返回指针 new(int), new(Cat)
  • make : 只用于slice、map、 channel 引用类型的初始化
    也可以使用字面量初始化:
person := Person{ 
    Name: "Alice",
    Age: 30, 
    City: "New York", 
}

C#基础类型使用字面量, 引用类型使用new关键字

2. 面向对象

同样是面向对象编程语言,go用结构体来体现,倾向于组合;
C#常用类来体现,现代化的显式面向对象。

封装

go基于结构体struct和接收者函数来封装事物和行为。

接收者函数分为: 值接收者函数、指针接收者函数。

  • 指针接收者函数内的操作会体现到入参

  • 不管是值,还是指针,都能调用指针接收者函数/值对象接受者函数,效果还是如上一点一致。

C# 显式使用Class struct等结构来封装数据和行为。

抽象/继承

go没有显式抽象函数、抽象类的做法,鸭子模式体现接口、 组合体现父子类继承关系。

接口将具有共性的方法放在一起,其他任何类型只要实现了这些方法就是实现了接口,俗称鸭子模式。

C#具备语义化的继承/抽象/多态, 显式继承。

3. 类型和内存分配

go是C语言家族

指针跟其他基础类型一样都是一等公民;
其他我们认为有引用类型效果的常见对象:string、slice、map、channel要么是struct+pointer的复合类型、要么本身就是指针的别名。

基础类型 : int long double char rune bool struct array pointer

golang 日常唱双簧的是指针和其他基础类型,
只看

var p = new(Point) 或者 

p2 := Person{ 
    Name: "Alice",
    Age: 30, 
    City: "New York", 
}

并不决定p所指向的对象是分配在堆还是栈。

go不像java .NET明确地根据类型划分堆栈存储, 取决于编译器的逃逸分析

如果编译器能推断变量的生命周期没有超过函数的作用域,就定义在栈上; 如果对象的大小超过阈值,或者变量的指针被返回或者被存储到其他地方,导致其生命周期超出了当前作用域,编译器会将其分配到堆上。

C#的类型划分更为现代化

严格划分值类型、引用类型,

  • 值类型: int long double char struct
  • 引用类型: string array List<> Dictionary

这个“严格” 体现在值类型分配在栈上, 引用类型分配在堆上。


go的指针弱化了C语言的指针操作,go指针的作用仅操作其指向的对象, 不能基于地址这个概念做指针移位, 也不能基于地址这个概念做类型转化。

从这个意义上看,C#的引用等价于go的指针, 都是类型安全的指针

另一方面, 两种语言都提供了对内存进行任意读写的姿势(非代码安全)。

go的unsafe.Pointer本质是一个int指针,

C# unsafe关键字可用在函数、属性、构造函数、代码块。

4. 异步编程的实现

异步: 在核心执行流上具备开启新任务的能力。

两者给到开发者的编程实践都比较一目了然: “以同步的思绪流开启异步任务”。

异步编程 go C#
编程实践 go语法糖(函数可以有形参,返回值会被忽略) async/await语法糖(有传染性)
底层原理 用户态轻量级线程,基于GPM调度模型 行为状态机,有默认的任务调度器
目标倾向 利用用户态线程尽量避免线程切换 常规线程池调度
适合 互联网高并发、负载的、短小任务 机制稳定、生态繁荣

核心调度流程图

核心认知

线程是cpu调度的基本单位,不管是goroutine还是async-wait机制都是在尝试提高cpu的利用率

对比传统的利用线程池做辗转腾挪,goroutine提供了轻量级用户态线程和便利的并发能力,调度开销低,适合高并发的小任务场景, 天然适配互联网高并发的、复杂的、小任务请求响应模型。

但是如果是纯CPU密集的请求响应模型, golang在传统线程之外新增用户态调度未必就比java .net 等传统异步模型要优。

5. GC

GC 只负责回收堆对象;
栈对象在函数返回时被清空,但是栈上对象可能引用了堆上对象。

GC从root根对象出发,root对象:全局变量和栈区变量。

5.1 golang GC: 三色并发标记、清除

go 在1.3 之前是标记/清除算法,需要stw;
在1.4引入了三色标记/清除算法, 需要stw;
在1.5 引入了混合写屏障,现在是三色并发标记/清除, stw只存在开关写屏障阶段,耗时很短。

什么叫混合写屏障

  1. 栈区出发的对象全为黑色, 栈区新增的也为黑色
  2. gc过程中 新增的对象、删除的对象均为灰色。

1.5之后的gc

  1. 开启混合写屏障
  2. 三色并发标记: 存在白、灰、黑三集合,初始全为白色,下游节点被触达,下游标记为灰色,自身标记为黑色。
  3. 清除
  4. 关闭写屏障

触发时机

  • 定时触发: 默认2min
  • 内存主动触发: 自上次GC之后累计申请的堆内存达到阈值触发GC,阈值是上次gc后堆内存分配量的两倍, 由GOGC 这个参数控制。
  • 手动触发: runtime.GC()函数

5.1 C# GC是基于分代策略的标记压缩

标记压缩
标记 :一开始假设所有的对象都是垃圾,然后对栈上变量、全局变量、静态变量这些根进行扫描, 标记活跃对象;

清理: 不可达对象即为可回收;

压缩: 并不是我们理解的占用内存减少,更像一种磁盘清理,将非垃圾对象移动到占有较大空间的连续垃圾对象区域。

分代策略, 触发时机

C# GC中包含“代”的机制,目的是为了提升性能,垃圾收集就是在第0代满的时候发生的(还不足,就会去清理第1代)。

  • 第0代: 新申请的对象、往往短命, 这一代清理会很频繁。
  • 第1代: 从第0代晋升的对象, 清理频率稍低一点。
  • 第2代: 从第1代晋升的对象,清理频率最低。

触发时机:第0 代满的时候,一般是512k;

posted @ 2022-10-25 16:40  码甲哥不卷  阅读(629)  评论(1)    收藏  举报