当前位置: 首页 > article >正文

Go语言设计的一些优点及缺陷

Go 语言的设计哲学强调简洁和效率。部分特性从代码工程最终的结果来看(静态),确实简洁舒服,然而,从开发过程(动态)来看,难道不会加大开发成本吗?!

Go把项目当成艺术品进行雕磨,然而,它似乎不太关心程序员的生产力。

以下观点仅代表个人意见,不喜勿喷😃

1.优秀设计

1.1.统一格式化

在 Go 语言中,gofmt 是官方提供的代码格式化工具,它能够自动格式化 Go 源代码,使得代码风格保持一致。

这是非常有趣和创新的,因为格式和问题,如制表符与空格或“我应该把花括号放在循环定义的同一行还是下一行”,都是浪费时间。

语言创造者定义了规则,每个人都使用这些规则。

这对于拥有大型团队的项目非常有用。

1.2.唯一的循环关键字

在Java编程语言,循环至少有for,do while ,while,foreach等不同的方式。

而在Go,它只提供唯一的选择,那就是for

// 基本循环
for i := 0; i < 10; i++ {
    // 循环体
}

// 死循环
for {
    // 循环体
}

// 条件循环
i := 0
for i < 10 {
    // 循环体
    i++
}

// 标签循环
outerLoop:
for i := 0; i < 3; i++ {
    for j := 0; j < 3; j++ {
        if i == 1 && j == 1 {
            continue outerLoop // 跳过外层循环的剩余部分,开始下一次迭代
        }
        // 循环体
    }
}

1.3.接口使用“鸭子类型”

Go 语言并不支持传统的面向对象编程(OOP)范式,如类(class)和继承(inheritance)。然而,Go 语言提供了一种类似于“鸭子类型”(duck typing)的方式,这种方式侧重于对象的行为不是它们的类型。在 Go 中,你可以通过接口(interface)来实现类似面向对象的特性。

鸭子类型(Duck Typing)

鸭子类型是一种编程范式,其中对象的有效性不是由对象的类型决定的,而是由对象的方法集合决定的。换句话说,如果它像鸭子一样走路,像鸭子一样叫,那么它就可以被认为是鸭子。

接口(Interface)在 Go 中的作用

在 Go 中,接口是一种定义了一组方法签名的类型。任何具有这些方法的类型都实现了接口,而不需要显式声明。这允许你创建具有不同类型但共享相同方法集的对象。

当然,该机制也有一些缺陷,例如:

当接口新增方法时,代码编辑器并不一定会报错,实现类可能不再是实现类。Java的话,接口新增方法,子类会编译报错。幸运的是,goland之类的IDE提供了自动在接口及其实现类添加方法的工具。

再比如,在java里,接口除了表示具有某些行为的抽象,还可以作为标记,只有名称没有方法,例如常见的序列化接口。这在Go里就不存在了!

public interface Serializable {
}

1.4.函数支持多重返回值

Go函数允许返回多个不同类型的值,类似

func operations(a int, b int) (int, int) {
	return a + b, a - b
}

虽然这不是Go的初创(Lua也支持,不考虑java,python这种以对象,元组模拟的多重返回值),但确定好用。但如果告诉你,Go设计这种机制,主要是为了它的异常处理机制,那我还是选择,不要这种特性!

1.5.函数返回值可命名

在 Go 语言中,返回值可以被命名,这通常用于提供更清晰的错误处理和更有意义的变量名。当你给返回值命名时,你可以在函数内部直接通过这些名字来引用它们,而不是使用临时变量。

以下是一个简单的例子,展示了如何在 Go 中命名返回值:

func divide(a, b float64) (result float64, err error) {
    if b == 0 {
        // 直接使用命名的返回值
        err = fmt.Errorf("cannot divide by zero")
        return // 没有提供 result 的值,编译器会使用 result 的类型零值
    }
    result = a / b
    return // 提供了 result 的值
}

1.6.元组赋值

元组赋值是一种特殊的赋值语句,它允许同时更新多个变量的值(Python也支持该模式)。在赋值之前,赋值语句右边的所有表达式将会先进行求值,然后再统一更新左边对应变量的值。这对于处理有些同时出现在元组赋值语句左右两边的变量很有帮助,例如我们可以这样交换两个变量的值:

x, y = y, x

计算计算斐波纳契数列

func fib(n int) int {
    x, y := 0, 1
    for i := 0; i < n; i++ {
        x, y = y, x+y
    }
    return x
}

1.7.集成单元测试与基准测试

Go集成单元测试,并采用“约定胜于配置”的更理念。在包目录内,所有以_test.go为后缀名的源文件表示测试文件。

*_test.go文件中,有三种类型的函数:测试函数基准测试(benchmark)函数示例函数。测试函数是以Test为函数名前缀的函数,用于测试程序的一些逻辑行为是否正确;go test命令会调用这些测试函数并报告测试结果是PASS或FAIL。

基准测试函数是以Benchmark为函数名前缀的函数,它们用于衡量一些函数的性能;go test命令会多次运行基准测试函数以计算一个平均的执行时间。

示例函数是以Example为函数名前缀的函数,提供一个由编译器保证正确性的示例文档。

有一点不好的地方在于,go将测试文件与业务文件放在同一个包里,虽然在执行go build时不会被构建成包的一部分,但仍显得比较混乱。而java将main与test放在不同的目录,并规定了独立的运行环境。

另外,java使用单元测试需要引入junit,使用基准测试需要引入jmh。

2.缺陷设计(仁人见仁,智者见智)

2.1.禁止申明未使用的变量

  • 限制灵活性:开发者可能希望保留某些变量以备将来使用,而不必立即删除它们

  • 增加工作量:开发中需要频繁地添加和删除变量,增加不必要的工作量

  • 调试困难:在调试过程中,需要添加一些临时变量,调试后需要删除

2.2.包范围命名空间

在同一个包内,即使处于不同的文件,我们也无法申明两个名字相同的变量。

为了避免这个问题,我们不得不把变量的名称弄长点,增加一些前缀来表示名称的上下文。这无疑增加了我们起名的难度,毕竟,起个好名不是一件简单的事!

2.3.通过返回值表示异常

这绝对是社区争吵最激烈,最多人诟病的一项特性了。难以想象,要在外层捕捉一个底层的错误返回值,你不得不写出下面的代码(这种机制真的提高开发人员的工作效率了,这里需要打个问号)

package main

import (
	"fmt"
)

func divide(dividend, divisor float64) (float64, error) {
	if divisor == 0 {
		return 0, fmt.Errorf("除数不能为0")
	}
	return dividend / divisor, nil
}

func middleFun(dividend, divisor float64) (float64, error) {
	result, err := divide(dividend, divisor)
	if err != nil {
		return result, err
	}
	// do sth
	return result, nil
}

func outerFun(dividend, divisor float64) (float64, error) {
	result, err := middleFun(dividend, divisor)
	if err != nil {
		return result, err
	}
	// do sth
	return result, nil
}

func main() {
	result, err := outerFun(10, 3)
	if err != nil {
		fmt.Printf("发生错误: %v\n", err)
		return
	}
	fmt.Printf("结果: %v\n", result)
}

2.4.打包体积较大

使用java的打包插件,是可以将业务代码与依赖分离的。因为生产环境一般依赖包的变动很少,只传输业务代码可以提高传输速度。另一方面,如果同一台机器部署多个节点,隔离的依赖包还可以被重用,减少硬盘使用。

而go build命令打出的二进制包,会把全部依赖都包含进去,打成一个”胖包“。这也是一个弊端吧!

2.5.不支持重入锁

 在java里,锁是支持重入的,只要是同一个线程,允许多次获得同一个锁对象,只要保证解锁也是一样次数即可。下面的代码没有任何问题

 public static void main(String[] args) throws Exception {

        Lock l = new ReentrantLock();
        for (int i = 0; i < 100; i++) {
            l.lock();
        }
        for (int i = 0; i < 100; i++) {
            l.unlock();
        }
}

而在Go,下面的代码却是无法执行! 

package main

import "sync"

var mu sync.Mutex

func main() {
	mu.Lock()
	mu.Lock()
}

直接红牌出局

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [sync.Mutex.Lock]:
sync.runtime_SemacquireMutex(0xc000051f40?, 0x9d?, 0x0?)
	D:/Program Files/Go/src/runtime/sema.go:95 +0x25
sync.(*Mutex).lockSlow(0xc08ca8)
	D:/Program Files/Go/src/sync/mutex.go:173 +0x15d
sync.(*Mutex).Lock(...)
	D:/Program Files/Go/src/sync/mutex.go:92
main.main()

2.6.通过大小写表示访问权限

在java里,private表示只能由当前类内部访问,而public表示类外部可以访问(不考虑jdk9的模块化)

而在 Go 语言中,变量的可见性(或者说是访问级别)是由变量名的首字母的大小写决定的:

  • 小写字母开头的变量:这样的变量是包私有的(package-private),它们只能在定义它们的包内部访问。
  • 大写字母开头的变量:这样的变量是公开的(public),它们可以在其他包中访问。

这个规则同样适用于函数、结构体、接口、类型等其他标识符。

乍看起来这促进了简洁性,但随着时间的推移,这种模式的缺点比优点更明显,看下面的例子,类型被变量遮蔽了, 这~~~

// 申明一个包内可见的结构体,小写字母开关
type user struct {
	name string
}

func main() {
    // 申明一个局部变量,变量与类型同名,这没毛病
	user := &user{name: "gforgame"}
    // 申明另一个局部变量,这里的类型被上一个局部变量遮蔽了,导致编译失败
	user2 := &user{name: "jforgame"}
}

2.7.循环变量作用域的陷阱

在下面的程序中,for循环语句引入了新的词法块,循环变量i在这个词法块中被声明。在该循环中生成的所有函数值都共享相同的循环变量。然而,函数中记录的是循环变量的内存地址,而不是循环变量某一时刻的值。以i为例,后续的迭代会不断更新i的值,可能导致不同的goroutine获取到同样的变量。

package main

import (
	"fmt"
	"time"
)

func main() {
	for i := 0; i < 500; i++ {
		go func() {
			time.Sleep(1 * time.Second)  
			fmt.Println(i)
		}()
	}
	time.Sleep(10 * time.Second) // 等待所有 goroutine 完成
}

该问题的解决方案大致有以下两种:

func main() {
	for i := 0; i < 500; i++ {
        // 申明一个局部变量,该变量的作用域只有本次循环
		index := i
		go func() {
			time.Sleep(1 * time.Second)
			fmt.Println(index)
		}()
	}
	time.Sleep(10 * time.Second) // 等待所有 goroutine 完成
}
func main() {
	for i := 0; i < 500; i++ {
		// 将i当做匿名函数的参数传递
		go func(index int) {
			time.Sleep(1 * time.Second)
			fmt.Println(index)
		}(i)
	}
	time.Sleep(10 * time.Second) // 等待所有 goroutine 完成
}

java虽然也有同样的问题,但编译器会直接告诉你无法编译


http://www.kler.cn/a/320007.html

相关文章:

  • AAPM:基于大型语言模型代理的资产定价模型,夏普比率提高9.6%
  • 【25考研】西南交通大学软件工程复试攻略!
  • 机器人传动力系统介绍
  • Hive集群的安装准备
  • Deep4SNet: deep learning for fake speech classification
  • Navicat Premium 原生支持阿里云 PolarDB 数据库
  • 语音音频(wav)声纹识别-技术实现-python
  • Debian与Ubuntu:深入解读两大Linux发行版的历史与联系
  • react crash course 2024(5) useState钩子
  • mac终端打开报complete 13 command not found compdef异常处理以及命令补全功能实现
  • 详细分析SpringMvc中HandlerInterceptor拦截器的基本知识(附Demo)
  • java知识:什么是GC?GC调优思路又有哪些
  • C++深入学习string类成员函数(1):默认与迭代
  • 聚观早报 | 小米新车规划曝光;北京汽车官宣更换标志
  • Django后台管理复杂模型
  • 【JVM】类加载机制
  • leetcode-189:轮转数组
  • 阿尔兹海默症患者出行随身助手设计_kaic
  • 【洛谷】P10417 [蓝桥杯 2023 国 A] 第 K 小的和 的题解
  • 免费制作证件照的小程序源码
  • 机器学习EDA探查工具Pandas profiling
  • nvm以及npm源配置
  • 注意力机制篇 | YOLOv8改进之在C2f模块引入EffectiveSE注意力模块 | 基于SE注意力
  • 聚观早报 | 豆包视频生成大模型发布;华为纯血鸿蒙将开启公测
  • 基于SpringBoot+Vue的考研百科网站系统
  • QT C++ 自学积累 『非技术文』