编程语言的思维方式

2023-03-11 22:50:53 浏览数 (1)

从实现继承和多态开始

之前我是个Java程序员,对OOP那一套可以说很是熟悉了,也习惯了这种常见的编程思维。比如我要实现一个简单的微服务UserService,那么我肯定会首先定义这个对象的能力:

代码语言:java复制
public class UserService {
  void register() {} // 注册会员
  void login(){} // 登录
  void modifyUserInfo(UserInfo userInfo){} // 修改用户信息
  // ...
}

面向对象和面向过程不同的一点就是要着眼于考虑这个对象能提供怎样的能力。任何一本教Java的书里提到OOP的时候讲的大概都是这样意思。

习惯了OOP以后一定就会开始习惯继承和多态,这是OOP的设计精髓。

为了模拟这种精髓,我在转换为Golang的时候也想办法去实现了所谓的继承和多态。但是这样一来我就犯了一个错误,就是用Java的思想来写Golang。在刚学Java的时候,也有人告诉我OOP的代码千万不要去用面向过程的思维来写,一定要继承和多态。

那么为什么我用Golang还是要用Java的思维来写呢?

一个逻辑的两种不同写法

在《Go语言精进之路》这本书的第4条中提到了一个“原生编程思维”的理念,并介绍了“萨丕尔-沃夫假说”,这个假说我倒是很同意,说着汉语的中国人和说着日语的日本人之间思维方式就是差异极大。甚至说着陕西话的陕西人和说着河南话的河南人之间思维方式都有不少的差异。

编程语言也是如此,在我开发了好多年Java以后,难免会带着Java的思维去写Golang。

这本书里列举了一个素数筛的例子,不过他用了不同的语言来说明问题。而我是想用同一种语言不同的思维方式来说明问题。

我们这里不谈算法如何,只谈实现。如果是一个熟练的Java程序员,那么用艾拉托斯特尼算法找前10个素数,应该写出这样的代码:

代码语言:go复制
package main

import "fmt"


func sieve() {
	var numbers []int
	max := 10
	for i := 0; i < max; i   {
		numbers = append(numbers, i 2)
	} 
	fmt.Printf("numbers: %vn", numbers)
	for i := 0; i < max; i   {
		if numbers[i] != -1 {
			for j := 2*numbers[i]-2; j < max; j  = numbers[i] {
				numbers[j] = -1
			}
		}
	}
	var prime []int
	for i := 0; i < max; i   {
		if numbers[i] != -1 {
			prime = append(prime, numbers[i])
		}
	}
	fmt.Printf("prime: %vn", prime)

}

func main() {
	sieve()
}

其实换一种C系的语言,实现起来也是差不多,这样说Golang也是平平无奇,看不出什么特点来。

但是书里给了另外一种不太好懂的实现方式:

代码语言:go复制
package main


func Generate(ch chan<- int) {
	for i := 2; ; i   {
		ch <- i
	}
}

// Filter 筛选素数,从in通道中读取数据,判断是不是prime的倍数,如果不是则可以放在out通道中
func Filter(in <-chan int, out chan<- int, prime int) {
	for {
		i := <-in 
		if i % prime != 0 {
			out <- i
		}
	}
}

func main() {
	ch := make(chan int)
	go Generate(ch) // ch通道中都是自然数递增序列
	for i := 0; i < 10; i   {
		prime := <-ch //从通道中取数字
		print(prime, "n") 
		ch1 := make(chan int)
		go Filter(ch, ch1, prime) //开启通道,并发的筛选素数,ch1中都是素数
		ch = ch1
	}
}

利用了goroutine和channel实现了并发的计算。按照书里的说法,程序从2开始给每个素数建立了一个goroutine,用于作为筛除该素数的倍数。ch指向当前最新输出素数所位于筛子goroutine的源channel。

这段代码按照我现在对Golang的理解来说还有点难以理解。但是不妨碍我了解这门语言的特色和思维方式。

代码中我可以解释的地方已经用注释标注了。

在我看来,既然Golang的并发如此容易实现,那么为什么不尽可能多的使用并发呢?也只有掌握了一种语言的思维方式之后,才能写出优雅的代码。

这是我3月11日新增的部分

上面那段演示Golang思维的代码,我又看了一下午,终于是理解了其中的思想。如果首先去掉main函数中的循环来看,那就更好理解一些了。

首先修改一下main函数,删除掉大部分的逻辑只剩这几条:

代码语言:go复制
ch := make(chan int)
go Generate(ch) 
prime := <-ch //从通道中取数字
print(prime, "n")

这时run这段代码,控制台的打印就只有一个2。这个时候除了Generate没有任何goroutine被创建出来(main不算)。也就是说书里提到的每个素数的goroutine还不存在。

继续放开代码,创造一个goroutine出来:

代码语言:go复制
ch := make(chan int)
go Generate(ch) 
prime := <-ch //从通道中取数字
print(prime, "n")
ch1 := make(chan int)
go Filter(ch, ch1, prime)

打印结果依旧是2,此时不用理会,因为2本身就是在Filter逻辑之前的。事情到了这里变得明了起来,如果要求更多的素数,只需要写个循环。所以代码就会变成最开始的样子:从2到10创造了一系列的goroutine来并发的求解问题。

0 人点赞