浅论各种编程语言

本人使用过的编程语言,细数包括5个大类,总计超过28种(参见我的技能栈),有些代码写得多,有些写得少,写得最多的是:Java、JS、HTML、CSS、Golang、……,其次是C++、C、PHP、C#、Lua、SQL……,写得少的是Python、VB、Jade、MatLab、……。


下面说一点心得体会(原创、纯手打),供学习、比较、参考。


    首先,看基础语法。好的基础语法,应该是简单直接的、易于记忆和书写的。“功能强”跟“简单易用”,是两个矛盾面——在基础语法中,加入常用增强功能,确实能方便编程,但是也让基础语法变得臃肿。比如说,for循环,有些语言甚至有5种以上的写法,虽然每种写法特点不一样,但是这增加了语言学习和代码维护的负担。如果,它把for循环精简到只有两种,比如 for(i=0;i<n;i++)和 while(flag) 这种通用的结构,那么所有程序中,只有这一种写法,维护起来要容易些。但是,也有一个弊端,比如一些特殊数据结构的循环,集合、链表、Map等,用通用结构去写不算是最简洁。

    怎么看这个问题?增强语法,绝大多数时候,都可以用封装库函数(工具函数)去解决。我认为,增强程序功能,首先应该从增加库函数入手,而不是增强语法。虽然增强语法比提供库函数,更易用,但要考虑这样做是不是也降低了程序可维护性(因为库函数代码,是标准语法,是大家都看得懂的,而增强语法,如果不学习一下,是不知道怎么回事的)。

    什么时候该增强语法呢?我认为是简单直接的、且常用的、且比提供库函数要好得多的地方,这三个条件缺一不可,反例如下:

  1. 如果这个功能不常用,那么提供一个库函数,哪怕使用麻烦一点又有多大影响呢?何必要去设计一个新语法,用于一个不常用的功能上呢?

  2. 如果这个功能调用库函数的方式也很容易、很方便的做到,为什么要用新语法去做?增加学习和维护成本。

  3. 如果为了某一套功能设计出一个非常复杂的语法,那么和直接用库函数相比又有什么本质区别呢,语法的目的就是简化使用,复杂的语法本身也难于使用、难于升级维护,不如不要。

    举个例子,Java 8新语法出来这么久了,其实也没增加多少内容,我尝试用过很多次,但我就是记不住,最终不想用了。为什么?因为Java7、Java6的语法已经很强大了,Java8的新语法只是锦上添花、“语法糖”而已。

    比如 lambda表达式、方法引用,形如:

// 例1
Function<Integer, int[]> fun = int[]::new;
int[] arr = fun.apply(10);

// 等价于
int[] arr = new int[10];

// 例2:混搭,凸显出方法引用的不通用性
public static <T> Collector<T, ?, List<T>> toList() {
    return new CollectorImpl<>((Supplier<List<T>>) ArrayList::new, List::add,
                               (left, right) -> { left.addAll(right); return left; },
                               CH_ID);
}

// 例3:同上,属于混搭,filter处与map处的处理方式不统一。
public List<String> getIdByColumn(Integer column) {
    return data.entrySet().stream().filter((e) -> {
        return e.getKey().equals(column);
    }).map(Map.Entry::getValue).collect(Collectors.toList());
}
// 例3:方法引用 Map.Entry::getValue 等价于 (e) -> {return e.getValue()}
public List<String> getIdByColumn(Integer column) {
    return data.entrySet().stream().filter((e) -> {
        return e.getKey().equals(column);
    }).map((e) -> {return e.getValue();}).collect(Collectors.toList());
}

// 例4:标准语法
list.sort(new Comparator<Integer>() {
    public int compare(Integer o1, Integer o2) {
      if(o1>o2)
        return -1;
      else if(o1<o2)
        return 1;
      else
        return 0;
    }
} );
// 例4:lambda语法
list.sort( ( o1, o2 ) -> {
    if(o1>o2)
        return -1;
      else if(o1<o2)
        return 1;
      else
        return 0;
} );

// 例5:lambda看似节省了3行代码,但是相对于整个函数体复杂逻辑,节省3行微不足道
values.stream()
  .mapToInt(e -> {    
    int sum = 0;
    for(int i = 1; i <= e; i++) {
      if(e % i == 0) {
        sum += i;
      }
    }  
    return sum;
  })
  .sum());

    lambda语法这样写省略了3行代码,要简洁一些。但是它让Java代码多了一层不透明的语法包装,而且lambda有使用限制(lambda表达式没有匿名函数的完整功能),不能通用,而且从上面的例3可见,方法引用也是lambda表达式的特例。我认为,一个语法,你不能做到通用、统一,这个语法是很失败的——lambda不能完全取代传统的写法,那么代码中就会存在两种不同的写法,看起来就很恼火。我真切的希望,语法层面能保持简洁,而不是炫技。

    Java8的新语法是个败笔,相反,Java 7的一些语法改进则很好,举个正面的例子,例如泛型的自动推导,非常好,它通用、实用、无歧义,因为编译器是可以帮我们自动推导出泛型类型的,这个改进让我们节省了以前那种“冗余”的写法,即便是删掉定义的泛型部分,语义也没有任何影响,如下所示:

// 改进前
List<String> list = new ArrayList<String>();
// 改进后
List<String> list = new ArrayList<>();

    一门好的语言,语法应该规则严谨,没有歧义,更没什么黑魔法、变异用法。任何人写出的代码都基本一致,这使得这门语言简单易学、易于阅读和维护。放弃部分“灵活”和“自由”,换来更好的维护性,我觉得是值得的。

    我再谈谈一个极端,比如说HTML,这门语言很简单,大家都很容易学习。简单到什么程度呢?它连for循环都没有。就是这么简单的一门语言,用了这么多年,到HTML5才有一个不算大的升级,而且HTML5的写法并没有给之前版本带来多少改变。即便是20年前的代码,现在依然很容易维护。这是相当成功的。

    为什么HTML不官方支持模板渲染、for循环,就像JSP、ASP那样。其实理论上HTML也可以做的,但它为什么不做。历史不再去深究,但事实证明,有些事情语言本身不去做,也有方法去实现。语言本身做好最基础的事情就行了。

    再说说,一个特殊的例子:Shell脚本。Shell的基础语法并不复杂,只是它的语法有些古老,不够简洁易用(比如写个if后面都还得更一个fi,获取数组长度要用${#arr[@]},当然如果你用熟练之后,其实也是OK的),其功能也不是很完善(比如对数据结构的支持,Map,Set等)。它最主要的作用是编排各种Unix/Linux指令。这种语言,其关键已经不在于语言本身的基础语法了。因为linux命令可以扩展它。但是由于它没有包管理之类的东西,它的运行,必须得依赖于操作系统上各种指令。而且也没有断点调试功能,只能用传统的打印变量的方式跟踪执行。我认为,它的灵活和简陋,会导致它的代码很难维护和大工程化使用,通常一个100行的shell脚本已经相当难维护了——灵活而简陋,意味着,很多功能,比如字符串的常见操作,没有官方的标准API去调用,只能借助各种扩展(比如awk、sed、cut、grep等等),可以这么说,一个稍微复杂点的功能,用Shell实现,10个人写出来的代码,很可能有10个不同版本而且Shell编程中,确实有很多黑魔法、变异用法,比如“shopt extglob”、"set -eo pipefail"、“xargs -r”( --no-run-if-empty,this option is a GNU extension),不同的操作系统上,不同的shell版本(bash、ksh、tcsh、ash、dash等)也会有一定区别。

    最后再说一说,面向过程和面向对象的编程语言。我觉得,面向过程的编程语言,适合小型、微型的独立项目,而面向对象的编程语言,更适合中大型以上规模的项目及工具库。我们知道,面向对象的语言有三大特性:封装、继承、多态。这三大特点,可以说是工程化的设计思想体现,对代码进行分类和复用的方法,它是从大工程项目中总结提炼出来的套路。在很小的项目中,也可以按这个思路来,但是会显得有些繁琐和不够直接。如果是做一个大项目,开发的人很多,代码量也很大,强烈建议用面向对象的编程语言,甚至像JavaScript(ES 6以前的版本)这样大规模被使用的语言,其实都不太适合大项目,真的很难维护!所以才有了ES 6、CoffeeScript、TypeScript等改进版本

    

    总结:说了这么多,心里应该有数了。我再补充几点:

1、学一门编程语言,首先只学“基础语法中最基础的那些”,先不要碰那些扩展的语法,和高级语法。

2、遵循大多数人遵守的编程语法和习惯,不要利用语法技巧,写出让人不太好理解的代码。

3、代码量、执行效率、可维护性,三者的选择上,首选可维护性。(下面特别说明!)

    不要为了节省代码,而降低可维护性,也不要为了提高一点点运行效率而降低代码的可维护性(很多人在这一点上会犯错——包括我,在我刚开始工作的那些年,受到《Effective C++》《Effective Java》等的影响,追求代码的极致运行性能,甚至至今,我写if-return时都要节省一个else,用Link List更好的时候我就不会用Array List,还有,我经常用一些奇葩的写法去改造传统的代码,以提升程序的性能。但是后来我经常看别人的代码,经常改别人的代码——各种语言的代码,我站在程序员和管理者的双重角度看,代码的可维护性优先级应该要高于执行效率,这个问题我也同一些资深架构师讨论过,现在的CPU执行速度非常快了,为了节省程序理论上的一两个指令执行时间,而采用非传统的写法,是弊大于利的,甚至大部分时候,O(n)和O(n^2)复杂度,执行时间都没有本质差别,木桶效应告诉我们,要提高效率应该从最慢的地方入手,比如网络、IO,而不要去纠结O(n)和O(n+k)、O(n^2)的差别)。


下面讨论一些优秀的正面例子和糟糕的反面例子:


定义变量的方式(讨论)

Go语言中至少有两种:

a := float64(5)
var a float64 = 5

从 工程学和心理学角度来说,程序员想在程序中定义一个变量(而不是使用一个已存在的变量),他的直接思维顺序是这样的:

  1. 我要定义一个变量,要做一个声明(标记)

  2. 给这个变量取一个名字

  3. 给这个变量声明一个类型

  4. 给这个变量赋一个初值

所以,声明变量的直觉方式为: 标记 名字 类型 值,这样的顺序

比如,我想在for循环之前,声明一个count变量,去获得每个值相加的和。按照常规思维,应该是这样定义:

var count int = 0;  // 依次为 标记、名字、类型、值

有几种情况,1、这个值暂时不设置(比如类变量),由外部函数去设置。此时省略赋值,简化为:

// 省略赋值 Go语言的方式
var count int

// TypeScript的方式
var count:number

这正是Go语言、TypeScript中支持的方式,非常棒!Go+1分,TypeScript+1分!其他语言-1分

2、可以通过赋值的类型,自动确定变量的类型,此时省略类型定义,简化为:

var count = 0; // 省略类型定义

新版Java才支持这种方式,所以TypeScript、Go等都+1分,新版Java+1分

但是Go还支持另外一种方式:

count := 0 // 用:代替var

这种语法糖,完全没必要,而且不太直观,反倒是增加了学习和维护难度,另外,还违背了变量定义的思维顺序。所以,Go语言 -2分。下面是Go语言中Map的初始化方式,有5种,真头痛:

// 方式一 
var m map[int]string // 声明一个map,此时的 map == nil
fmt.Println(m)

m = map[int]string{} // 初始化一个map,此时的 map != nil,是map[]
fmt.Println(m)
// 以上两种的区别在于有没有被初始化容量  

// 方式二
var m2 map[int]string = map[int]string{}
fmt.Println(m2)

// 方式三
m3 := map[int]string{}
fmt.Println(m3)

// 方式四
m4 := map[string]string{
	"name":"Tinywan",
	"school":"BAT_UN"
}
fmt.Println(m4)

// 方式五
m5 := make(map[string]string)
m5["name"] = "Linux"
m5["school"] = "Unix"
// 注意:m4和m5两种初始化的方式等价

3、在代码的固定位置,只能声明变量,不能使用变量。此时可以省略 声明标记。

例如函数的参数声明,类的变量声明。这些地方出现变量,默认都是第一次声明。

class User { // 省略 变量定义的 var 标记
  age int;
  name string;
  sex = 0; // 赋初值,可省略类型
  
  public User(age int, name string, sex = 0) { // 省略var标记
     // ...
  }
}

在Go语言的包全局变量中,始终要用 var,例如 var bb int,同理TypeScript也是,所以-1分,但是注意,Go的结构体参数不需要用var标记,算是弥补了包变量的缺陷吧。

Java类变量中,比较接近于 age int 这种形式,但是不支持 sex =0,所以-0.5分

在函数的参数声明中,Go和TypeScript都不需要var,而且TypeScript支持sex = 0,非常棒,但是Go中函数的参数不支持默认值,所以TypeScript + 1分,Go + 0.5分,新版Java + 0.5分。


总结:

说实话,对新兴编程语言Go的期望大,失望也不小。(后文还会讨论更多方面)

然而微软的TypeScript,从设计上讲还真的非常不错!

新版Java(Java 10、11)也吸取了一些精华。对于一门历史悠久、用户庞大的编程语言来说,语法上的更新一定是非常谨慎、保守的,除非一些非常好的改进(有70%以上的支持者)才会考虑实现(上文说到的Java8的lambda表达式是一个反例,如果做一个公开投票,支持率应该远远低于70%)。


Go语言中的数组切片

我认为这是非常糟糕的设计,明明是可以通过内置函数(例如slice)解决的数据操作问题,非要弄成语法糖,难学又难用。

语言设计中,一定要遵循:Less is More的原则,没有什么是必要的,除非没有它不行。显然Go语言并没有遵循这种哲学

举例:

var arr = [...]int{19, 2, 0} // 定义一个数组
var arr = []int{19, 2, 0} // 定义一个数组的切片

还有:

var a = [5]int{76, 77, 78, 79, 80}
var b []int = a[1:4]
var c = a[:]

i := make([]int, 5, 5)

// 提前知道数组中的值
var a[2]int{11,22}
var a[20]int{19:11} // 索引值为19的元素赋值为 11 ,其他的默认为 0

// 不指定数组的长度 ...
var c = [...]int{11,22,33,44}
var d = [...]int{19:90} // 尽可能的满足索引值得数组

// 第一种方式
n := [10]int{}
n[1] = 10

// 第二种方式
m := new([10]int)
m[1] = 20

看着头疼,而且还有一个很难接受的问题:

数组和数组切换,都是数组形式,但是数组参数是值传递,切片参数是引用,对函数内部来说,它根本不知道传入的参数是值传递的,还是引用传递的。(没有显式声明,例如C语言中的&,不具备一致性

func changeLocal(num [5]int) {  
    num[0] = 55
    fmt.Println("inside function ", num)
}

num := [...]int{5, 6, 7, 8, 8} // 数组
changeLocal(num) // 值传递,num不变

nos := []int{8, 7, 6} // 切片
changeLocal(nos) // 引用,nos会变

参见上面的注释。而且数组切片很容易引起内存泄漏问题。


Go语言中的Map

对比Java、TypeScript 与Go语言中的Map定义:

Map<String, Map<String, Integer>> map = new HashMap<>();
map.put("k1", ?); // 新增
map.remove("k1"); // 删除,remove是Map的一个API

var map1 = make(map[string]map[string]int) // map是语法关键字,变量名不能为 map
map1["k1"] = ? // 新增
delete(map1, "k1") // 删除,delete是系统内置函数

let map = new Map();
map.set("k1", ?); // 新增
map.delete("k1"); // delete是Map的一个API

有两个问题:

  1. 前面说了,Go语言中一共有5种定义map的方式,上面的写法只是其中一种。

  2. Java是面向对象的,Map只是接口,可以有HashMap、TreeMap、LinkedHashMap等多种实现方式。而Go语言中,将map作为了语法关键字,这样通用性、灵活性大打折扣,想要实现TreeMap那就不能用传统map的用法了。

  3. Java和TypeScript是将 接口(方法)定义在对象上的,可扩展性非常强,例如Map有很多API:remove(key, value),replace(key, value)。而Go,只有delete(key),没有delete(key, value),也没有 replace(key,value),这是有本质区别的

我对问题1,是反感,对第2、3问题,则是忧虑(十分忧虑)。


Go语言中的字符串

Go语言中的字符串,是由“字节”数组构成,这完全不同于其他语言。例如在Java中,字符串是由“字符”数组组成。

一个是“字节”,一个是“字符”。显然,我觉得“字符”更好。所谓“字符”,其实是“Unicode Character Representations”(Unicode字符表示法),它是常用的字符标准,在程序内部可以完美的工作。用“字节”在程序内部表示字符串,编程者更难用,而且也没必要。所以,Go为了方便大家使用,新增了内置类型“rune”(int32的别名,配合格式化符c%,可以转换成字符类型)。下面的例子,说明了问题:

// Go语言中,这种打印字符串的方式,得到的结果可能是错误的
func printChars(s string) {  
    for i:= 0; i < len(s); i++ {
        fmt.Printf("%c ",s[i])
    }
}

// 要换成下面的rune方式
func printChars(s string) {  
    runes := []rune(s)
    for i:= 0; i < len(runes); i++ {
        fmt.Printf("%c ",runes[i])
    }
}

// 而Java等其他语言中,通常直接打印char即可
void printChar(String s) {
    for (Character c : s.toCharArray()) {
        System.out.print(c);
    }
}

获取字符串的长度,不能使用len(因为len获取的是字节长度),需要用utf8.RuneCountInString(string)函数(是不是很抓狂?),数字与字符串相加,不能自动转换类型,得转换后才能用:

var l = len(s) // 获取字符串长度,错误的方法
fmt.Println(l)

l = utf8.RuneCountInString(s) // 正确的方法
fmt.Println(l)


var port = 8080 // int
var redio = 3.14 // float64
// Java字符串相加
String str = "price:" + redio + port;

// Go字符串相加
var str = "price:" + strconv.FormatFloat(redio, 'E', -1, 64) + strconv.Itoa(port)
// 爽不爽?


Go语言的其他问题

  1. iota这个特殊的值,很难用

  2. 缺少标准容器,例如队列、堆栈等

  3. 没有泛型,(泛型这个功能,我认为还是很不错的)

  4. 缺乏简单的通用资源(而不仅仅是内存)

  5. 不适合面向对象的编程,继承和多态都十分难用!

  6. 方法有指针接收者和值接收者两种写法,值接收者涉及到隐式内存拷贝,是个坑,没必要。

  7. 指针让人头疼,弊大于利。

  8. Go的代码运行时调试非常垃圾

  9. Go的异常处理(panic、recover)机制非常垃圾

  10. 函数参数问题(多个问题,见后文)

后面有时间再详细来分析这些。总的来说,个人感觉:不能拿Go跟其他高级语言相比,仅仅与C语言比,Go还是有一点先进的,即使和其他高级语言比,Go也有一些可取之处。另外,个人感觉Go不适合大型企业级应用开发,但是作为C语言的升级版,作为中间件及运维工具的开发语言,还是可以的,比起Python、Ruby都有优势,日常写一些微小的Web项目也还凑合。


7.指针问题

下面程序 perimeter(r) 会报错,而 r.perimeter() 则不会。一个接受指针为参数的函数不能接受一个值作为参数,但是r.perimeter() 却可以,这是因为这一行将被 Go 解析为 (&r).perimeter()。这是为了方便 Go 给我们提供的语法糖。

package main

import (  
    "fmt"
)

type rectangle struct {  
    length int
    width  int
}

func perimeter(r *rectangle) {  
    fmt.Println("perimeter function output:", 2*(r.length+r.width))

}

func (r *rectangle) perimeter() {  
    fmt.Println("perimeter method output:", 2*(r.length+r.width))
}

func main() {  
    r := rectangle{
        length: 10,
        width:  5,
    }
    p := &r //pointer to r
    perimeter(p)
    p.perimeter()

    /*
        cannot use r (type rectangle) as type *rectangle in argument to perimeter
    */
    //perimeter(r)

    r.perimeter()//calling pointer receiver with a value
}


10.函数参数问题

问题1:【无法解决】go语言中实现函数的默认参数和可选参数

问题2:go语言可变参数的坑



我们是否需要三元运算符?

正方、反方都有很多论点,比如Rust语言的这个issues就有很大人讨论:

https://github.com/rust-lang/rfcs/issues/1362

理性的思考后,我觉得,三元运算符确实和if-else的语义重复,而且当三元运算符嵌套时,非常难看。因此,基于我上文所说的原则,三元运算符  确实没必要。只不过大家在其他语言中用习惯了而已,但三元运算符也并不是很常用。


说了这么多缺点,说一下Go的部分优点吧

  1. switch-case循环,去掉了令人苦恼的break,换成了fallthrough,这个设计很棒!case多个值也很方便。

  2. 在强类型的语言中,函数还能做到有多返回值,优秀!(但是和TypeScript这种可以随处自定义对象 {a:1, b:2},Go还是显得不够方便)

  3. 统一了for循环的方式(但是for循环的形式也并不是很好)

下面对我熟悉的各种语言的for循环做一个对比,至于优劣,自己感受:

第一种情形,带索引:

// Go语言的for循环用法: for...range
for key, val := range arr {
    Println(key, val)
}

// JavaScript(ES6):forEach
arr.forEach((val, key) => Println(key, val))

// CoffeeScritp的for循环用法:for...in
Println key, val for key, val in arr

第二种情形,不带索引:

// Go语言的for循环用法
for _, val := range arr {
    Println(val)
}

// JavaScript(ES6):for...of
for(val of arr) {
    Println(val)
}

显然,第二种更优。

第三种情形,只根据条件进行循环:

// Go
for i := 0; i <= 10; i++ {
    Println(i)
}

// JavaScript
for(var i = 0; i <= 10; i++) {
    Println(i)
}

总结:

1、for作为一个通用语法关键字,是有必要的,比 forEach() 这种函数要更好、更通用。

2、for的用法,通常只有上面三种场景,对于第1、2种,可以统一为:

for(val in arr) {
    Println(val)
}

for(val, key in arr) { // 类似于CoffeeScript的用法
    Println(val)
}

但是Go语言选择了使用 “ for ? := range ? ” 句式,显得冗余(:=range)但总体思路是对的(统一化)。

对于第3中场景,使用 “for ?;?;? {}" 句式,是大多数编程语言通用的。三个"?"分别代表:初始局部变量、循环开始的判断条件(boolean)、循环结束执行代码”。这个套路是对的,但是语法设计不够优美、灵活。最后一步“循环结束执行代码”,其实可以放在循环体中,下面是我设计的循环方式:

for (var i = 0) when (i <= 10) {
    Println(i);
    i++;
}

支持复杂的场景:

for (
    var pos = 0;
    var count = getCountFromFile(pos);
) when(pos<16 && count!=1) {
    Println(count);
    
    // 每次循环结束时执行
    pos++;
    count = getCountFromFile(pos);
}

这个循环,按照Go语言的写法,只能为:

var pos = 0;
var count = getCountFromFile(pos);
for pos<16 && count!=1 {
    Println(count);
    
    // 每次循环结束时执行
    pos++;
    count = getCountFromFile(pos);
}

显然,前者更好,Go语言的写法要差一些,原因有两个:

  1. Go的for语句不能完美定义“循环体内使用的局部变量”,导致变量被提到了循环体之外

  2. “ for x=10 when(x<10) {} ” 这种语句更自然:for后面跟循环的对象,when后面跟循环的判断条件。对比之下,Go的这种“for 跟判断条件”句式,看着像是 if语句,逻辑上不自然。


让人烦恼的Go语言中的数字类型

因为业务需要,我写了如下一段代码:

var x = 1000
var w = int(int(math.Floor(math.Log10(float64(x)) + 1)))

没办法,最简单的方式写出来就这样。

1宗罪:程序员都知道,括号的层级嵌套多了,体验非常不好,而Go语言中,类型转换需要把值包裹起来,于是括号嵌套就可能非常多!

换成其他语言,比如Java,就简洁很多

int x = 1000
int w = (int) Math.floor( Math.log10(x) + 1);

2宗罪:注意到上述代码,两者都是强类型语言,但是却有一个差别,Java数字类型是可以自动向上转型的(例如int是可以赋值给float变量的,因为float变量范围大于int),而go语言不能,得傻不拉几的手动写一个转换



极容易采坑的nil及隐式类型转换

nil转换有时候是不合常理的,示例如下:

func GetString() *String {
    return nil
}
func CheckString(s Stringer) bool {
    println(s)
    return s == nil
}

var ss *String = GetString()
println(ss) // 输出Nil的地址:0x0
println(ss == nil) // true
println(CheckString(nil)) // (0x0,0x0)  true
println(CheckString(ss)) // (0xb3f820,0x0)  false

注意到:ss == nil,但是 CheckString(ss) 跟 CheckString(nil) 的值却不一样,太无语了!!(原因是ss是动态类型,nil是静态类型,ss的实际值为*String(nil),在静态编译阶段,CheckString(ss) 中的ss就被转换成了Stringer(nil)!)

这个坑,很容易在 if err != nil {} 判断的时候遇到。参见这篇文章:golang的类型转换的坑和分析


关于指针和闭包

    几句话说不清,直接说结论吧:

    1、不得不说,像Java这种屏蔽掉“指针”是很好的做法,指针真的会增加程序的复杂度、降低可维护性,而且Java也证明,没有指针语法,并不是坏事。

    2、闭包也是语言的副作用产品,很难用,看下面例子:

func test() []func() {  
  var funs []func()
  for i:=0;i<2;i++  {
    funs = append(funs,func() {       
       println(&i,i)
    })
  }
  return funs
}
func main(){
  funs:=test()  
  for _,f:=range funs {
    f()
  }
}

这个例子中,闭包是for循环完后的值,即都为2。再看下面例子:

func test(x int)(func(),func()) {    
 return func() {       
      println(x)
      x+=10
    }, func() {       
      println(x)
    }
}
func main() {
  a,b:=test(100)
  a()
  b()
}

输出结果是 100、110。闭包的神奇在于,它使得值类型变量,与函数体绑定,在多个函数中共享,就好像是引用变量一样。

     再看Java的设计,好贴心,屏蔽掉了“闭包”。它是怎么做的呢?它规定,“闭包”内的外部变量,必须是final类型。例如:

interface User {
    int getAge();
}

public void test() {
    int x = 100;
    User u1 = new User() {
        @Override
        public int getAge() {
            x += 10;
            return x;
        }
    };
    User u2 = new User() {
        @Override
        public int getAge() {
            return x;
        }
    };
}

这段代码编译会报错,闭包内不能改变x的值!!x为final类型变量(JDK1.8以下需要显示的声明final)。

    同样,事实也再一次证明,没有闭包和指针的编程语言,例如Java,也是一样强大,闭包和指针并不是什么好东西。上面这个例子可以修改成:

public void test() {
    int x = 100;
    User u1 = new User() {
        int x;
        @Override
        public int getAge() {
            x += 10;
            return x;
        }
        User set(int x) {
            this.x = x;
            return this;
        }
    }.set(x);
}

    任何闭包的写法,都可以用这种 set/get 值的方式来透明的传递值


© 2009-2020 Zollty.com 版权所有。渝ICP备20008982号