抽象出正确性

2020-06-28 21:48:01

这句话并不是以同样的方式引起每个人的共鸣。为了真正理解我所说的仔细的API设计是什么意思,一个人必须经历过这两种情况。

但也有一线希望--一旦你体验了好的设计,就真的很难再回到另一种设计了。即使在承认好的设计不可避免地要付出代价之后,无论是认知负担、编译时间,还是让招聘变得更具挑战性等,都不可避免地要付出代价。

学习新事物,理解它的价值,然后再回到旧的做事方式,这是一种非常困难的经历。这适用于任何主题,而不仅仅是编程。

我知道这对我来说很困难。所以,这里是你的警告:一旦你学会了发现设计缺陷,你就不能忘记它,而且这确实会让你很难把工作做好。这是一个微妙的平衡。

几年前,当我开始使用Go时,我很高兴地发现了一些核心界面。

首先,它从Go1.0开始就有了,它只有一个功能。所以基本上你能想到的任何东西都已经实现了。

//在`main.go`包main import(";io";";log";";os";)func main(){log.。SetFlags(日志。L短文件)f,错误:=os。如果错误!=nil{log.(";main.go";),则打开(";main.go";)。Fatalf(";%+v";,err)}readSome(F)}函数readSome(r io.。读卡器){buf:=make([]byte,4)n,err:=r.Read(Buf)if err!=nil{log.。Printf(";获取错误:%+v";,Err)}其他{日志。Printf(";读取%v字节:%v";,n,buf[:n])}}。

包主要导入(";io";";log";";net";)func main(){log.。SetFlags(日志。L短文件)连接,错误:=NET。如果err!=nil{log!,则拨号(";TCP";,";example.org:80";)。Fatalf(";%+v";,err)}_,err=conn。如果err!=nil{log.,则写入([]byte(";GET/HTTP/1.1\r\n\r\n";))。Fatalf(";%+v";,err)}readSome(Conn)}//省略:readSome

包主要导入(";io";";log";";net/http";)func main(){log.。SetFlags(日志。L短文件),错误:=http。Get(";http://example.org";)if Err!=nil{log.。Fatalf(";%+v";,err)}readSome(分别。BODY)}Funcc readSome(r io.。读卡器){buf:=make([]byte,4)n,err:=r.Read(Buf)if err!=nil{log.。Printf(";获取错误:%+v";,Err)}其他{日志。Printf(";读取%v字节:%v";,n,buf[:n])}}。

我们可以继续下去,但我们不能--基本上,围棋生态系统中任何可以阅读的东西几乎都可以实现io.Reader。

AltReader接口提出了几个问题-让我们通过为*os.File实现它来强调这些问题。

包主导入(";log";";os";)类型AltReader接口{AltRead()([]byte,error)}func(f*os.。文件)AltRead()([]字节,错误){buf:=make([]byte,1024)n,err:=f.read(Buf)return buf[:n],err}func main(){log.。SetFlags(日志。L短文件)f,错误:=os。如果错误!=nil{log.(";main.go";),则打开(";main.go";)。Fatalf(";%+v";,err)}readSome(F)}func readSome(R AltReader){buf,err:=r.AltRead()if err!=nil{log.。Printf(";获取错误:%+v";,Err)}其他{日志。Printf(";读取%v字节:%v";,len(Buf),buf)}}。

$Go运行main.go#命令行参数。\main.go:12:6:无法在非本地类型os.File上定义新方法。\main.go:26:10:不能在参数中使用f(类型*os.File)作为类型AltReader来读取Some:*os.File不实现AltReader(缺少AltRead方法)。

包主导入(";io";";log";";os";)类型AltReader接口{AltRead()([]byte,error)}//新类型:类型AltReadWrapper struct{Internal io.。Reader}//现在接收器是我们的包装器类型func(arw*AltReadWrapper)AltRead()([]byte,error){buf:=make([]byte,1024)n,err:=arw。内部。read(Buf)return buf[:n],err}func main(){log.。SetFlags(日志。L短文件)f,错误:=os。如果错误!=nil{log.(";main.go";),则打开(";main.go";)。Fatalf(";%+v";,err)}//参数现在换行为readSome(&;AltReadWrapper{Internal:f})}func readSome(R AltReader){buf,err:=r.AltRead()if err!=nil{log.。Printf(";获取错误:%+v";,Err)}其他{日志。Printf(";读取%v字节:%v";,len(Buf),buf)}}。

$GO运行main.gomain.go:42:读取705字节:[112 97 99 107 97 103 101 32 109 97 105(等)]。

请注意,当我们实现AltReader时,我们只声明了一个具有正确签名的名为AltRead()的函数。

好的,所以AltReader接口可以工作了--但是,没有办法指定您想要读取多少数据。我们很高兴早些时候只读取了4个字节,但是现在我们依赖于AltReader的实现者选择的任何缓冲区大小。

类型AltReader接口{AltRead(N Int)([]byte,error)}类型AltReadWrapper结构{内部io。Reader}var_AltReader=(*AltReadWrapper)(Nil)func(arw*AltReadWrapper)AltRead()([]字节,错误){buf:=make([]byte,1024)n,err:=arw。内部。read(Buf)return buf[:n],Err}//省略:其他所有内容。

Func(arw*AltReadWrapper)AltRead()([]字节,错误){buf:=Make([]字节,1024)//⬅此处n,Err:=arw。内部。读取(Buf)返回buf[:n],错误}。

$Go运行main.go#命令行参数。\main.go:17:5:不能使用(*AltReadWrapper)(Nil)(type*AltReadWrapper)作为赋值中的AltReader类型:*AltReadWrapper不实现AltReader(AltRead方法的类型错误)有AltRead()([]byte,error)Want AltRead(Int)([]byte,error)。\main.go:AltReadWrapper文本(type*AltReadWrapper)作为readSome的参数中的类型AltReader:*AltReadWrapper未实现AltReader(AltRead方法的类型错误)Have AltRead()([]byte,Error)Want AltRead(Int)([]Byte,Error)。\main.go:38:23:调用r.AltRead Have()Want(Int)时参数不足。

func(arw*AltReadWrapper)AltRead(N Int)([]byte,error){buf:=make([]byte,n)n,err:=arw。内部。读取(Buf)返回buf[:n],错误}。

func readSome(R AltReader){buf,err:=r.AltRead(4)if err!=nil{log.。Printf(";获取错误:%+v";,Err)}其他{日志。Printf(";读取%v字节:%v";,len(Buf),buf)}}。

现在,一切都重新编译并运行-我们得到了4个字节,就像我们想要的那样:

函数(arw*AltReadWrapper)AltRead(N Int)([]byte,error){//我们要找出GC是否真的很快buf:=make([]byte,n)n,err:=arw。内部。读取(Buf)返回buf[:n],错误}

包主导入(";io";";log";";os";)类型AltReader接口{AltRead(N Int)([]byte,error)}类型AltReadWrapper struct{Internal io.。Reader buf[]byte}var_AltReader=(*AltReadWrapper)(Nil)func(arw*AltReadWrapper)AltRead(N Int)([]byte,error){if len(arw.。buf)<;n{log.。Printf(";分配%v字节";,n)arw。buf=make([]byte,n)}n,Err:=arw。内部。阅读(arw.。buf)返回arw。buf[:n],err}func main(){log.。SetFlags(日志。L短文件)f,错误:=os。如果错误!=nil{log.(";main.go";),则打开(";main.go";)。对于i:=0;i<;4;i++{readSome(Arw)}}函数readSome(R AltReader){buf,err:=r.AltRead(4)if err!=nil{log.,则Fatalf(";%+v";,err)}arw:=&;AltReadWrapper{Internal:f}for i:=0;i<;4;i++{readSome(Arw)}}函数readSome(R AltReader){buf,err:=r.AltRead(4)。Printf(";获取错误:%+v";,Err)}其他{日志。Printf(";读取%v字节:%v";,len(Buf),buf)}}。

$GO运行main.gomain.go:22:分配4字节main.go:49:读取4字节:[112 97 99 107]main.go:49:读取4字节:[97 103 101 32]main.go:49:读取4字节:[109 97 105 110]main.go:49:读取4字节:[10 10 105 109]。

当然,返回内部缓冲区的问题在于没有什么能阻止调用者保留对它的引用。

包主导入(";io";";log";";os";//new!";github.com/davecgh/go-spew/spew";)//某些类型/函数省略了func main(){log.。SetFlags(日志。L短文件)f,错误:=os。如果错误!=nil{log.(";main.go";),则打开(";main.go";)。Fatalf(";%+v";,err)}fc:=readFourChunks(&;AltReadWrapper{Internal:f})输出。dump(Fc)}type FourChunks struct{one[]byte Two[]byte Three[]byte four[]byte}func readFourChunks(R AltReader)FourChunks{masRead:=func()[]byte{r,err:=r.AltRead(4)if err!=nil{log.。Fatalf(";Cannot Not Read:%+v";,Err)}return r}return FourChunks{One:mastRead(),Two:msiRead(),Three:msiRead(),Four:mastRead(),}。

$go run main.gomain.go:24:分配4字节(main.FourChunks){一:([]uint8)(len=4 cap=4){00000000 0A 0A 69 6d|..im|},二:([]uint8)(len=4 cap=4){00000000 0A 0A 69 6d|..im|},三:([]uint8)(len=4 cap=4){00000000 0A 0A 69 6d|..im。

啊哦。FourChunks的所有字段都设置为main.go中找到的第四组偏置字节。

键入AltReadWrapper struct{interi.。Reader}函数(arw*AltReadWrapper)AltRead(N Int)([]字节,错误){buf:=make([]byte,n)n,err:=arw。内部。读取(Buf)返回buf[:n],错误}。

$go run main.go(main.FourChunks){一:([]uint8)(len=4 cap=4){00000000 70 61 63 6b|pack|},二:([]uint8)(len=4 cap=4){00000000 61 67 65 20|age|},三:([]uint8)(len=4 cap=4){00000000 6d 61 69 6e|main|},四:([]uint8)(len=4 cap=4)

而且我们在每次读取设计时都会坚持分配,因为我们不知道调用者可能会保留我们返回的切片多长时间。在我们的AltReader实现类型获取垃圾回收之后,他们可能会保留很长时间。

但我认为Reader没有这个问题。通过采用单个[]字节参数,它可以同时满足这两个要求:

如果我们将示例更改为只使用io.Reader,我们可以看到一切都很正常:

软件包主要导入(";io";";log";os";";github.com/davecgh/go-spew/spew";)func main(){log.。SetFlags(日志。L短文件)f,错误:=os。如果错误!=nil{log.(";main.go";),则打开(";main.go";)。Fatalf(";%+v";,err)}fc:=readFourChunks(F)spew.。dump(Fc)}type FourChunks struct{one[]byte Two[]byte Three[]byte four[]byte}函数读取FourChunks(r io.。Reader)FourChunks{masRead:=func(p[]byte)[]byte{_,err:=io。ReadFull(r,p)if err!=nil{log.。Fatalf(";Cannot Not Read:%+v";,Err)}Return p}return FourChunks{One:mastRead(make([]byte,4)),Two:mastRead(make([]byte,4)),Three:mastRead(make([]byte,4)),Four:mastRead(make([]byte,4)),}}。

$go run main.go(main.FourChunks){一:([]uint8)(len=4 cap=4){00000000 70 61 63 6b|pack|},二:([]uint8)(len=4 cap=4){00000000 61 67 65 20|age|},三:([]uint8)(len=4 cap=4){00000000 6d 61 69 6e|main|},四:([]uint8)(len=4 cap=4)。

软件包主要导入(";io";";log";os";";github.com/davecgh/go-spew/spew";)func main(){log.。SetFlags(日志。L短文件)f,错误:=os。如果错误!=nil{log.(";main.go";),则打开(";main.go";)。Fatalf(";%+v";,err)}readFourTimes(F)}函数readFourTimes(r io.。读卡器){buf:=make([]byte,4)for i:=0;i<;4;i++{_,err:=io。ReadFull(r,buf),如果err!=nil{log.。Fatalf(";无法读取:%+v";,Err)}拼写。转储(Buf)}}。

见鬼,如果我们愿意,我们甚至可以把它读到缓冲区的中间:

func main(){log.。SetFlags(日志。L短文件)f,错误:=os。如果错误!=nil{log.(";main.go";),则打开(";main.go";)。Fatalf(";%+v";,err)}readToMid.(F)}函数readToMid.。读卡器){buf:=[]byte(";.";)_,Err:=io。ReadFull(r,buf[8:20]),如果err!=nil{log.。Fatalf(";无法读取:%+v";,Err)}拼写。转储(Buf)}

$go run main.go([]uint8)(len=30 cap=30){00000000 2e 2e 2e 70 61 63 6b 61 67 65 20|.Package|00000010 6d 61 69 6e 2e 2e 2e|Main.|}。

在这一点上,我认为公平地说,io.Reader是一个更好的设计。

我还没有展示完整的io.Reader界面--只展示了它的函数签名。不幸的是,这还不足以完全指定接口。

任何时候,只要界面的签名本身不足以推断其行为,就会有麻烦出现。

想象一下,你可以拉起或拉下一个火警报警器。如果你把它拉起来-警报器响了,人们都疏散了,一切都很好。

当然,最终人们会想出办法的--他们会把它拉下来,看看什么都没有发生,困惑几秒钟,然后试着把它拉起来。或者也许他们赢了,然后假设它坏了。

当然,除非拉警报器的人什么也听不到。也许大厦管理人员可以确信这是一个潜在的问题,应该做些什么来解决它。

他们没有更换警报器,而是设立了员工培训。所有的火警警报器都必须拉下来,而不是拉起来--不,等等,反过来。

他们还没有解决问题的根源。这充其量只是一个创可贴。任何还没有参加过培训的人,比如新员工,都是大楼的潜在危险。

现在,酷熊,你知道这个类比并不适用于每个人。我可以从这里看到评论.。

不要紧--不管怎样,让我们在这里公平地说:不是所有的事情都是可以解决的。有时你只需要在墙上贴一张告示,上面写着不要戳熊,因为那里有熊是有充分理由的。

不管怎样,我要说的是我们还没有讨论完整的IO.Reader界面。

尽管它只是一个函数,但有一堆可移动的部分。首先,这是Go,在Go中,您没有元组,您没有Result<;T>;类型,您没有异常处理-您有多值回报。

多值回报的问题之一是,没有人抱怨这样的事情:

软件包主要导入(";log";";os";";github.com/davecgh/go-spew/spew";)func main(){log.。SetFlags(日志。lShort file)//这显然不是一个好主意f,_:=os。打开(";woops";)buf:=make([]byte,32)//n,_:=f.read(Buf)spew。转储(buf[:n])}。

编译器不会抱怨,去兽医也不会抱怨,golangci-lint运行的51个短针都不会抱怨。

他们当然不会抱怨,你在想,你只是用_!叫他们闭嘴!

在支持异常的语言中懒于处理错误的方法是.。什么都不做。

//@ts-check const{readFile}=REQUIRED(";fs";)。承诺;异步函数main(){const content=await readFile(";woops";,{coding:";utf-8";});控制台。log(`刚刚读取文件:${Contents。Slice(0,20)}.`);}main();

woops文件不存在,而且我们肯定没有花任何时间考虑错误处理,因此,它导致整个程序中断:

$node--unhanded-rejections=Strict main.jsInternal/process/promises.js:194 riggerUncaughtException(err,true/*from mPromise*/);^[错误:ENOENT:无此类文件或目录,打开';C:\msys64\home\amos\go\aac\woops';]{errno:-4058,code:';ENOENT';,SysCall。

导入java。伊俄。BufferedReader;导入java。伊俄。FileInputStream;导入java。伊俄。InputStreamReader;导入java。尼欧。CharBuffer;public class main{public static void main(string args[])抛出java。朗。异常{BufferedReader Reader=new BufferedReader(new InputStreamReader(new FileInputStream(";woops";),";UTF-8";));CharBuffer BUF=CharBuffer。分配(32);读取器。读(BUF);读。关闭();系统。出去。println(";只需读取文件:";+buf。toString());}}。

$javac Main.java&;&;线程中的java MainException";Main&34;java.io.FileNotFoundException:位于java.base/java.io.FileInputStream.open0(Native方法的Woops(系统找不到指定的文件)位于java.base/java.io.FileInputStream.open(FileInputStream.java:219)位于java.base/java.io.FileInputStream.<;init>;(FileInputStream.java:157)位于java.base/java.io.FileInputStream.<;init>;(FileInputStream.java:112)在Main.main(Main.java:8)。

$python main.pyTraceback(最近一次调用):file";main.py";,第2行,在<;module>;file=open(";woops";,";r";)FileNotFoundError:[Errno 2]没有这样的文件或目录:';woops';

#DEFINE_CRT_SECURE_NO_WARNINGS#include<;stdio.h>;#include<;stdlib.h>;int main(int argc,char**argv){file*f=fopen(";woops";,";r";);const size_t len=32;//(!)。无偿使用sizeof传达意图//作者意识到sizeof(Char)只有1.char*buf=calloc(len,sizeof(Char));fread(buf,sizeof(Char),len,f);printf(";只读取文件的一部分:%.*s\n";,(Int)len,buf);return 0;}。

$lldb./Main(Lldb)目标创建";./Main";当前可执行文件设置为';./Main';(X86_64)r进程14644已启动:';C:\msys64\HOME\amos\go\aac\main';(X86_64)进程14644已停止*线程#1,停止原因=在地址0x7ff6遇到异常0xc0000409。IMM=0xC0000417 0x7ff6f10c6385:LEAL 0x1(%r8),%ecx。

我做的不仅仅是几条下划线。我故意隐藏了一些非常有趣的警告。

$clang main.c-o main.c:5:13:警告:';fopen&39;已弃用:此函数或变量可能不安全。请考虑改用fopen_s。要禁用弃用,请使用_CRT_SECURE_NO_WARNINGS。有关详细信息,请参阅联机帮助。[-W不推荐使用的声明]文件*f=fopen(";woops";,";r";);^C:\Program Files(X86)\Windows Kits\10\Include\10.0.18362.0\ucrt\stdio.h:207:20:备注:';fopen';已被显式标记为已弃用HERE_CHECK_RETURN__CRT_INSECURE_DELPERATE(FOPEN_S)^D:\Programs\MicrosoftVisual Studio\2019\Community\VC\Tools\MSVC\14.26.28801\include\vcruntime.h:316:55:备注:从宏展开;_CRT_INSECURE_DEPREATE';#DEFINE_CRT_INSECURE_DEPREATE(_REPLACE)_CRT_DEPROATE_TEXT(\^D:\PROGRAM\MICROSOFT Visual Studio\2019\Community\VC\Tools\MSVC\14.26.28801\include\vcruntime.h:306:47:注意:从宏';_CRT_DEPREATE_TEXT';#DEFINE_CRT_DEPREATE_TEXT(_TEXT)__DECLSPEC(DEPERATED(_TEXT))^1生成警告。

#include<;stdio.h>;#include<;stdlib.h>;int main(int argc,char**argv){file*f;fopen_s(&;f,";woops";,";r";);//etc返回0;}。

然后..。没有其他警告了。不过,微软(我在Windows上写这篇文章)已经为C开发了一种注释语言。

_check_return_wat__ACRTIMP errno_t__cdecl fopen_s(_Outptr_result_maybenull_file**_Stream,_in_z_char const*_filename,_in_z_char const*_Mode);

(顺便说一句,这是在PowerShell中运行的-我通常在msys2中运行shell sessionin)。

$cl.exe/nlogo/Analyze main.cmain.cC:\msys64\home\amos\go\aac\main.c(8):警告C6031:已忽略返回值:';fopen_s';.C:\msys64\home\amos\go\aac\main.c(14):警告C6387:';buf';可能是';0';:这不符合s。

..