文档章节

The Go Type System

Jerikc
 Jerikc
发布于 2014/08/30 11:52
字数 1923
阅读 59
收藏 1
GO
Recently I've become very interested in the Golang programming language. Golang, or Google Go as it's often called, is a new programming language designed by some fairly well-known systems researchers, including Rob Pike of Plan 9 fame, and Ken Thompson, one of the creators of C.

Go has a lot "go"ing for it. Google has begun to use it for big internal projects. There is a lot of interest in Go in the web development and scripting communities. It also has a fairly mature implementation, something that's easy to overlook when considering new programming languages, but vitally important in practice.

So what's the deal with Go? It's a strongly, statically typed imperative language with good concurrency support. But don't we already have a few of those? C++, Java, and .NET come to mind. Well, Go is fundamentally different than those languages. There are a lot of philosophical differences in the way Go handles errors, deals with platform-specific code, makes efficiency tradeoffs, and deals with concurrency. I could easily write an essay on each one of those philosophical excursions, and why Go's choice is the right one. However, in this post, I just want to talk about the Go type system and why it's better than the Java or C++ type system.


More Java, sir?

In Java, you have classes which contain data and instance methods. Code reuse is mostly done through inheritance.

Let's consider InputStream. This is one of the most fundamental Java classes. It's been around since Java 1.0, and is the main method Java developers use to read files.

InputStream is a class which contains some code, but it also has some "abstract" methods which are meant to be overridden.

Let's say we want to some integers from a file. Well, we can't use InputStream directly, but we can use FileInputStream, a class which inherits directly from InputStream. Unfortunately, FileInputStream doesn't do any buffering. So if we want to do a lot of small reads, it may not be very efficient, because each of those reads will turn into a syscall.

Well, that's no problem; we can use BufferedInputStream. BufferedInputStream extends FilterInputStream, which wraps one input stream in another input stream. In the case of BufferedInputStream, the outer stream does buffering.

However, we still do have a problem. BufferedInputStream doesn't let us read integers; it only lets us read bytes and arrays of bytes. In order to get something that reads integers, we need another wrapper: DataInputStream.

So far our program looks like this:

FileInputStream fos = new FileInputStream("myfile"); 
BufferedInputStream bos = new BufferedInputStream(fos, 16384);
DataInputStream dos = new DataInputStream(bos);
int foo = dos.readInt();
Well, it might be a little verbose, but at least it's safe, right? We know that whatever operation we call on our InputStream will be well-supported.

Well... not quite. One operation defined by InputStream, mark, is only supported by some InputStreams, and not others. It so happens that FileInputStream doesn't support it (and will misbehave at runtime if you try to use it). BufferedInputStream does. Our DataInputStream does, but only because it wraps our BufferedInputStream-- DataInputStream itself does not support mark.

Incidentally, this also highlights another problem with this code-- the extreme lack of locality. In order to know what will happen when we make any given call, we have to start with the most derived class, and follow the chain of virtual method invocations, being careful to note where the subclass overrides the superclass and where it doesn't. For example, InputStream#read(byte b[], int off, int len) contains a very naive implementation of bulk reads that just reads a byte at a time in a loop. However, you don't have to worry about that here because it's overridden by FileInputStream. This is just a very simple example-- things can get hairy in the real world where you have 4 or 5 wrapper classes, and each stream is fairly far from the original base class on the inheritance tree.

So anyway, this mark business is quite an omission. Wasn't Java supposed to protect us from runtime errors due to type signatures? How can we say that FileInputStream "is-an" InputStream when FileInputStream doesn't support all the methods of InputStream? If you're a Java true believer, it's a little bit like someone just farted in church. However, you'll find things like this all throughout the Java standard library-- little cases where the hierarchical inheritance-driven model of the world just did not conform to reality, and something had to give.

In this particular case, in order to put the information about markability into the type system, we would have had to double the number of classes. Basically, we would have had to create MarkableInputStream, UnmarkableInputStream, followed by MarkableBufferedInputStream, UnmarkableBufferedInputStream, MarkableFilterInputStream, and UnmarkableFilterInputStream, and so forth.

The basic problem is that there are a lot of traits an input stream could have: seekable versus non-seekable, buffered versus non-buffered, writable versus non-writable, position tracking versus position-oblivious, and so forth. In general, these traits are independent of one another, and we need 2^n classes to represent all the possible combinations of input stream. This kind of combinatorial explosion of pointless wrapper classes is the fundamental, algorithmic reason why Java is verbose. Even if its keywords were all abbreviated, even if Java supported a C++0x-style auto keyword, Java would still feel like Java.

This may seem bad, but it gets worse. What if you want to both read and write from a file? Unfortunately, InputStream only supports reading. If you want to write to the same file, you'll have to either deal with OutputStream, RandomAccessFile, or FileChannel. None of these classes share a common base class with InputStream, so you essentially have to rewrite your code in order to use them. It gets very awkward. It's not unknown to see people passing around both an InputStream and a FileChannel that both represent the same file!


A generic solution

Java 5.0 got generics. The easiest way to think of generics is as a way of automatically generating the 2^n different implementations we talked about earlier. In C++, generics are actually implemented this way-- by generating extra copies of the templated code. Java only creates one copy of the code, though. The Java way is much more cache-friendly-- one reason why C++'s performance is actually worse than Java's sometimes.

With generics, InputStream could be rewritten as InputStream<IOManipulator>. Then we could insert different IOManipulator classes which had different capabilities-- one might enable seeking and buffering, while another might not. We might have a Java interface named IOManipulator, implemented by BufferedIOManipulator, RandomAccessBufferedIOManipulator, and RandomAccessIOManipulator. However, we still find ourselves obsessing over the inheritance relationships between the IOManipulators-- should RandomAccessBufferedIOManipulator inherit from RandomAccessIOManipulator? Probably. Overall, though, generics make Java much more bearable by providing another "degree of freedom" in the type system. They allow you to avoid deep inheritance trees for just a little bit longer. I think this is the reason why most Java programmers can't imagine ever using a language without generics. However, generics have a steep learning curve for novice programmers, and the error messages you get with generics are extremely poor. Generics also don't fully solve the problems that we have with verbosity, overgrown hierarchies, and in many cases general awkwardness.

The C++ standard library makes extensive use of templating. For example, std::string is really:

namespace std {
  template<class charT, class traits = char_traits<charT>,
    class Allocator = allocator<charT> >
      class basic_string;
}
That's right-- triply templated on the character type, the character "traits" class, and the memory allocator. This often leads to "word salad" compiler error messages.


go-logo


Go's solution

After many years of real-world software engineering, the truth has become clear: inheritance is an antipattern. However, we still need some way of creating relationships between types. What are we going to do?

Go solves this problem by deriving which types correspond to an interface automatically. So, for example, any type which has a functionRead(p []byte) (n int, err error)defined for it conforms to the io.Reader interface Similarly, there is an io.ReaderAt interface for seekable streams. bufio is Go's version of BufferedInputStream.

Notice that the problems we had before melt away. It's easy to have a type which conforms to both io.Reader and io.Writer. No big deal-- just implement the required functions. There's no need to think about inheritance hierarchies, type traits, or any of that. The code is also much less verbose because we avoid the "implements," "extends," and so forth. Some people call this feature "static duck typing."

So far, so good. But what about if we want to extend the functionality provided for another type? For example, perhaps we want to create a buffered Reader that can also return a string describing which file is being read. In C++ or Java, if we wanted to extend the functionality through composition, we'd have to write "forwarding methods" for all the methods of the enclosed type. In Golang, we just use an anonymous field to export all of the functions of the anonymous type to the enclosing type. Later, we can override any of these auto-exported fields in the future if we want to, without breaking the API.

Go's type system has one more subtle features that might surprise you if you grew up with C++ or Java, like I did. You can define methods on types other than structures. For example, I could define a method on the type []int (array of integers).


Conclusion

So there you have it. By making static typing feel more like dynamic typing, Golang eliminates the old tradeoff between the higher productivity of languages like Python, and the increased safety of languages like Java. We can have both! We also avoid the quagmire of deep inheritance hierarchies, incomprehensible error messages, and unsafe compromises likeInputStream#mark.

Go also provides tools to implement code reuse by composition, allowing us to favor composition over inheritance, as Effective C++ recommends.

Will Go get generics in the future? I don't really know. The designers have left the question open. As we've seen, Go doesn't need generics as much as C++ and Java needed them. However, generics would still be useful for avoiding typecasts when implementing generic data structures. Basically when you're pulling an instance of a type out of a generic data structure using a get() method or similar, it's nice not to have to cast it back to the type that you know that it is. I think people make a much bigger deal out of this than it really is.

One thing to note is that if generics were ever added to Go, they would not have to be added using type erasure. This was only done in Java to retain backwards compatibility between new Java code and old Java Virtual Machines (JVMs). Since Golang has no JVM, but compiles down to machine code, this would not be necessary.

The design of Go has been influenced by decades of experience in building real-world systems. I think its type system is the best thing going out there for imperative languages, and a good candidate for replacing Python and Java with something much safer and more productive.


Other stuff

There are some other problems with Java's type system that I didn't really go into here. If you haven't read The Kingdom of Nouns, then you should check that out. Likewise, Yossi's C++ FQA ("frequently questioned answers") is a hilarious takedown of the over-inflated claims of C++ partisans. You will also learn quite a bit about the language by reading it-- Yossi knows his stuff.

本文转载自:http://www.club.cc.cmu.edu/~cmccabe/blog_golang_type_system.html

Jerikc
粉丝 98
博文 246
码字总数 22757
作品 0
浦东
程序员
私信 提问
搭建你的Spring.Net+Nhibernate+Asp.Net Mvc 框架 (四)配置全攻略

本篇目的:配置Spring.Net和Nhibernate。配置Spring.Net来注入Nhibernate需要的SessionFactory,配置业务逻辑层所需要的Dao层属性的支持。配置这套框架与web程序关联。 在上一篇我们已经将三...

aicoder
2010/09/07
0
0
impdp+network link 跳过expdp直接导入目标库

说起来惭愧,这还是客户itpub上看到的,是有关10G impdp直接导入的,来邮件问是否可行,自己实验了下,还真可以,确实方便很多。 实验环境:linux下11G,一式两份 (手头只有11G虚拟机) 要点...

hotsmile
2016/11/13
30
0
target triplets

Autoconf-generated configure scripts can make decisions based on a canonical name for the system type, or target triplet, which has the form: ‘cpu-vendor-os’, where os can be......

the_anaconda
2017/05/10
0
0
WindowManager 中 LayoutParams的各种属性

1. public int x ; 如果忽略gravity属性,那么它表示窗口的绝对X位置。 什么是gravity属性呢?简单地说,就是窗口如何停靠。 当设置了 Gravity.LEFT 或 Gravity.RIGHT 之后,x值就表示到特定...

迷途d书童
2012/03/30
488
0
综合应用WPF/WCF/WF/LINQ之十三:LINQ的ORM功能中对使用sp_executesql语句的存储过程的支持

本文转自 Eallies 51CTO博客,原文链接:http://blog.51cto.com/eallies/79030,如需转载请自行联系原作者

余二五
2017/11/07
0
0

没有更多内容

加载失败,请刷新页面

加载更多

nginx学习笔记

中间件位于客户机/ 服务器的操作系统之上,管理计算机资源和网络通讯。 是连接两个独立应用程序或独立系统的软件。 web请求通过中间件可以直接调用操作系统,也可以经过中间件把请求分发到多...

码农实战
今天
5
0
Spring Security 实战干货:玩转自定义登录

1. 前言 前面的关于 Spring Security 相关的文章只是一个预热。为了接下来更好的实战,如果你错过了请从 Spring Security 实战系列 开始。安全访问的第一步就是认证(Authentication),认证...

码农小胖哥
今天
9
0
JAVA 实现雪花算法生成唯一订单号工具类

import lombok.SneakyThrows;import lombok.extern.slf4j.Slf4j;import java.util.Calendar;/** * Default distributed primary key generator. * * <p> * Use snowflake......

huangkejie
昨天
12
0
PhotoShop 色调:RGB/CMYK 颜色模式

一·、 RGB : 三原色:红绿蓝 1.通道:通道中的红绿蓝通道分别对应的是红绿蓝三种原色(RGB)的显示范围 1.差值模式能模拟三种原色叠加之后的效果 2.添加-颜色曲线:调整图像RGB颜色----R色增强...

东方墨天
昨天
11
1
将博客搬至CSDN

将博客搬至CSDN

算法与编程之美
昨天
13
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部