闻道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)

使用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关键字的行为效果是一样的。

推荐附加阅读

本文限于篇幅,只记录了go语言和C#语言的入门6月龄的核心差异点和重难点,高手绕道, 后续会不断完善, 请有心人持续关注左下角原文, 如果能点赞更是莫大的鼓励。

posted @ 2022-10-25 16:40  博客猿马甲哥  阅读(570)  评论(1编辑  收藏  举报