<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>functional-programming | 酷 壳 - CoolShell</title>
	<atom:link href="https://coolshell.cn/tag/functional-programming/feed" rel="self" type="application/rss+xml" />
	<link>https://coolshell.cn</link>
	<description>享受编程和技术所带来的快乐 - Coding Your Ambition</description>
	<lastBuildDate>Mon, 28 Dec 2020 15:18:30 +0000</lastBuildDate>
	<language>zh-CN</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.2</generator>
	<item>
		<title>Go编程模式：Map-Reduce</title>
		<link>https://coolshell.cn/articles/21164.html</link>
					<comments>https://coolshell.cn/articles/21164.html#comments</comments>
		
		<dc:creator><![CDATA[陈皓]]></dc:creator>
		<pubDate>Thu, 24 Dec 2020 07:13:52 +0000</pubDate>
				<category><![CDATA[Go 语言]]></category>
		<category><![CDATA[程序设计]]></category>
		<category><![CDATA[编程语言]]></category>
		<category><![CDATA[functional]]></category>
		<category><![CDATA[functional-programming]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[golang]]></category>
		<category><![CDATA[MapReduce]]></category>
		<category><![CDATA[函数式]]></category>
		<category><![CDATA[函数式编程]]></category>
		<guid isPermaLink="false">https://coolshell.cn/?p=21164</guid>

					<description><![CDATA[<p>在本篇文章中，我们学习一下函数式编程的中非常重要的Map、Reduce、Filter的三种操作，这三种操作可以让我们非常方便灵活地进行一些数据处理——我们的程序...</p>
<p class="read-more"><a class="btn btn-default" href="https://coolshell.cn/articles/21164.html"> Read More<span class="screen-reader-text">  Read More</span></a></p>
The post <a href="https://coolshell.cn/articles/21164.html">Go编程模式：Map-Reduce</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></description>
										<content:encoded><![CDATA[<p><script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-3415450859608158"
     crossorigin="anonymous"></script><img decoding="async" loading="lazy" class="alignright size-medium wp-image-21251" src="https://coolshell.cn/wp-content/uploads/2020/12/go.map_.reduce-300x192.png" alt="" width="300" height="192" srcset="https://coolshell.cn/wp-content/uploads/2020/12/go.map_.reduce-300x192.png 300w, https://coolshell.cn/wp-content/uploads/2020/12/go.map_.reduce-768x491.png 768w, https://coolshell.cn/wp-content/uploads/2020/12/go.map_.reduce-422x270.png 422w, https://coolshell.cn/wp-content/uploads/2020/12/go.map_.reduce.png 992w" sizes="(max-width: 300px) 100vw, 300px" />在本篇文章中，我们学习一下函数式编程的中非常重要的Map、Reduce、Filter的三种操作，这三种操作可以让我们非常方便灵活地进行一些数据处理——我们的程序中大多数情况下都是在到倒腾数据，尤其对于一些需要统计的业务场景，Map/Reduce/Filter是非常通用的玩法。下面先来看几个例子：</p>
<section class="post-series"><h3 class="post-series-title">本文是全系列中第5 / 10篇：<a href="https://coolshell.cn/articles/series/go%e7%bc%96%e7%a8%8b%e6%a8%a1%e5%bc%8f">Go编程模式</a></h3><ul class="post-series-list"><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/21128.html">Go编程模式：切片，接口，时间和性能</a></span></li><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/21140.html">Go 编程模式：错误处理</a></span></li><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/21146.html">Go 编程模式：Functional Options</a></span></li><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/21214.html">Go编程模式：委托和反转控制</a></span></li><li class="post-series-item-current post-series-item"><span class="post-series-item-title">Go编程模式：Map-Reduce</span></li><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/21179.html">Go 编程模式：Go Generation</a></span></li><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/17929.html">Go编程模式：修饰器</a></span></li><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/21228.html">Go编程模式：Pipeline</a></span></li><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/21263.html">Go 编程模式：k8s Visitor 模式</a></span></li><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/21615.html">Go编程模式 ： 泛型编程</a></span></li></ul><nav class="post-series-nav"><span class="post-series-nav-prev">&laquo; <a href="https://coolshell.cn/articles/21214.html" rel="prev" title="Go编程模式：委托和反转控制">上一篇文章</a></span><span class="post-series-nav-next"><a href="https://coolshell.cn/articles/21179.html" rel="next" title="Go 编程模式：Go Generation">下一篇文章</a> &raquo;</span></nav></section>
<h4>基本示例</h4>
<h5>Map示例</h5>
<p>下面的程序代码中，我们写了两个Map函数，这两个函数需要两个参数，</p>
<ul>
<li>一个是字符串数组 <code>[]string</code>，说明需要处理的数据一个字符串</li>
<li>另一个是一个函数<code>func(s string) string</code> 或 <code>func(s string) int</code></li>
</ul>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">func MapStrToStr(arr []string, fn func(s string) string) []string {
    var newArray = []string{}
    for _, it := range arr {
        newArray = append(newArray, fn(it))
    }
    return newArray
}

func MapStrToInt(arr []string, fn func(s string) int) []int {
    var newArray = []int{}
    for _, it := range arr {
        newArray = append(newArray, fn(it))
    }
    return newArray
}</pre>
<p>整个Map函数运行逻辑都很相似，函数体都是在遍历第一个参数的数组，然后，调用第二个参数的函数，然后把其值组合成另一个数组返回。</p>
<p><span id="more-21164"></span></p>
<p>于是我们就可以这样使用这两个函数：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">var list = []string{"Hao", "Chen", "MegaEase"}

x := MapStrToStr(list, func(s string) string {
    return strings.ToUpper(s)
})
fmt.Printf("%v\n", x)
//["HAO", "CHEN", "MEGAEASE"]

y := MapStrToInt(list, func(s string) int {
    return len(s)
})
fmt.Printf("%v\n", y)
//[3, 4, 8]</pre>
<p>我们可以看到，我们给第一个 <code>MapStrToStr()</code> 传了函数做的是 转大写，于是出来的数组就成了全大写的，给<code>MapStrToInt()</code> 传的是算其长度，所以出来的数组是每个字符串的长度。</p>
<p>我们再来看一下Reduce和Filter的函数是什么样的。</p>
<h5><strong>Reduce 示例</strong></h5>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">func Reduce(arr []string, fn func(s string) int) int {
    sum := 0
    for _, it := range arr {
        sum += fn(it)
    }
    return sum
}

var list = []string{"Hao", "Chen", "MegaEase"}

x := Reduce(list, func(s string) int {
    return len(s)
})
fmt.Printf("%v\n", x)
// 15
</pre>
<h5><strong>Filter示例</strong></h5>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">func Filter(arr []int, fn func(n int) bool) []int {
    var newArray = []int{}
    for _, it := range arr {
        if fn(it) {
            newArray = append(newArray, it)
        }
    }
    return newArray
}

var intset = []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
out := Filter(intset, func(n int) bool {
   return n%2 == 1
})
fmt.Printf("%v\n", out)

out = Filter(intset, func(n int) bool {
    return n &gt; 5
})
fmt.Printf("%v\n", out)
</pre>
<p>下图是一个比喻，其非常形象地说明了Map-Reduce是的业务语义，其在数据处理中非常有用。</p>
<p><img decoding="async" loading="lazy" class="aligncenter size-full wp-image-21169" src="https://coolshell.cn/wp-content/uploads/2020/12/map-reduce.png" alt="" width="794" height="442" srcset="https://coolshell.cn/wp-content/uploads/2020/12/map-reduce.png 794w, https://coolshell.cn/wp-content/uploads/2020/12/map-reduce-300x167.png 300w, https://coolshell.cn/wp-content/uploads/2020/12/map-reduce-768x428.png 768w, https://coolshell.cn/wp-content/uploads/2020/12/map-reduce-360x200.png 360w, https://coolshell.cn/wp-content/uploads/2020/12/map-reduce-485x270.png 485w" sizes="(max-width: 794px) 100vw, 794px" /></p>
<h4>业务示例</h4>
<p>通过上面的一些示例，你可能有一些明白，Map/Reduce/Filter只是一种控制逻辑，真正的业务逻辑是在传给他们的数据和那个函数来定义的。是的，这是一个很经典的“业务逻辑”和“控制逻辑”分离解耦的编程模式。下面我们来看一个有业务意义的代码，来让大家强化理解一下什么叫“控制逻辑”与业务逻辑分离。</p>
<h5>员工信息</h5>
<p>首先，我们一个员工对象，以及一些数据</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">type Employee struct {
    Name     string
    Age      int
    Vacation int
    Salary   int
}

var list = []Employee{
    {"Hao", 44, 0, 8000},
    {"Bob", 34, 10, 5000},
    {"Alice", 23, 5, 9000},
    {"Jack", 26, 0, 4000},
    {"Tom", 48, 9, 7500},
    {"Marry", 29, 0, 6000},
    {"Mike", 32, 8, 4000},
}</pre>
<h5>相关的Reduce/Fitler函数</h5>
<p>然后，我们有如下的几个函数：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic">func EmployeeCountIf(list []Employee, fn func(e *Employee) bool) int {
    count := 0
    for i, _ := range list {
        if fn(&amp;list[i]) {
            count += 1
        }
    }
    return count
}

func EmployeeFilterIn(list []Employee, fn func(e *Employee) bool) []Employee {
    var newList []Employee
    for i, _ := range list {
        if fn(&amp;list[i]) {
            newList = append(newList, list[i])
        }
    }
    return newList
}

func EmployeeSumIf(list []Employee, fn func(e *Employee) int) int {
    var sum = 0
    for i, _ := range list {
        sum += fn(&amp;list[i])
    }
    return sum
}</pre>
<p>简单说明一下：</p>
<ul>
<li><code>EmployeeConutIf</code> 和 <code>EmployeeSumIf</code> 分别用于统满足某个条件的个数或总数。它们都是Filter + Reduce的语义。</li>
<li><code>EmployeeFilterIn</code> 就是按某种条件过虑。就是Fitler的语义。</li>
</ul>
<h5>各种自定义的统计示例</h5>
<p>于是我们就可以有如下的代码。</p>
<p><strong>1）统计有多少员工大于40岁</strong></p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">old := EmployeeCountIf(list, func(e *Employee) bool {
    return e.Age &gt; 40
})
fmt.Printf("old people: %d\n", old)
//old people: 2
</pre>
<p><strong>2）统计有多少员工薪水大于6000</strong></p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">high_pay := EmployeeCountIf(list, func(e *Employee) bool {
    return e.Salary &gt;= 6000
})
fmt.Printf("High Salary people: %d\n", high_pay)
//High Salary people: 4
</pre>
<p><strong>3）列出有没有休假的员工</strong></p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">no_vacation := EmployeeFilterIn(list, func(e *Employee) bool {
    return e.Vacation == 0
})
fmt.Printf("People no vacation: %v\n", no_vacation)
//People no vacation: [{Hao 44 0 8000} {Jack 26 0 4000} {Marry 29 0 6000}]
</pre>
<p><strong>4）统计所有员工的薪资总和</strong></p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">total_pay := EmployeeSumIf(list, func(e *Employee) int {
    return e.Salary
})

fmt.Printf("Total Salary: %d\n", total_pay)
//Total Salary: 43500
</pre>
<p><strong>5）统计30岁以下员工的薪资总和</strong></p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">younger_pay := EmployeeSumIf(list, func(e *Employee) int {
    if e.Age &lt; 30 {
        return e.Salary
    } 
    return 0
})</pre>
<h4>泛型Map-Reduce</h4>
<p>我们可以看到，上面的Map-Reduce都因为要处理数据的类型不同而需要写出不同版本的Map-Reduce，虽然他们的代码看上去是很类似的。所以，这里就要带出来泛型编程了，Go语言在本文写作的时候还不支持泛型（注：Go开发团队技术负责人Russ Cox在2012年11月21golang-dev上的mail确认了Go泛型(type parameter)将在Go 1.18版本落地，即2022.2月份）。</p>
<h5>简单版 Generic Map</h5>
<p>所以，目前的Go语言的泛型只能用 <code>interface{}</code> + <code>reflect</code>来完成，<code>interface{}</code> 可以理解为C中的 <code>void*</code>，Java中的 <code>Object</code> ，<code>reflect</code>是Go的反射机制包，用于在运行时检查类型。</p>
<p>下面我们来看一下一个非常简单不作任何类型检查的泛型的Map函数怎么写。</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic">func Map(data interface{}, fn interface{}) []interface{} {
    vfn := reflect.ValueOf(fn)
    vdata := reflect.ValueOf(data)
    result := make([]interface{}, vdata.Len())

    for i := 0; i &lt; vdata.Len(); i++ {
        result[i] = vfn.Call([]reflect.Value{vdata.Index(i)})[0].Interface()
    }
    return result
}</pre>
<p>上面的代码中，</p>
<ul>
<li>通过 <code>reflect.ValueOf()</code> 来获得 <code>interface{}</code> 的值，其中一个是数据 <code>vdata</code>，另一个是函数 <code>vfn</code>，</li>
<li>然后通过 <code>vfn.Call()</code> 方法来调用函数，通过 <code>[]refelct.Value{vdata.Index(i)}</code>来获得数据。</li>
</ul>
<p>Go语言中的反射的语法还是有点令人费解的，但是简单看一下手册还是能够读懂的。我这篇文章不讲反射，所以相关的基础知识还请大家自行Google相关的教程。</p>
<p>于是，我们就可以有下面的代码——不同类型的数据可以使用相同逻辑的<code>Map()</code>代码。</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">square := func(x int) int {
  return x * x
}
nums := []int{1, 2, 3, 4}

squared_arr := Map(nums,square)
fmt.Println(squared_arr)
//[1 4 9 16]



upcase := func(s string) string {
  return strings.ToUpper(s)
}
strs := []string{"Hao", "Chen", "MegaEase"}
upstrs := Map(strs, upcase);
fmt.Println(upstrs)
//[HAO CHEN MEGAEASE]</pre>
<p>但是因为反射是运行时的事，所以，如果类型什么出问题的话，就会有运行时的错误。比如：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">x := Map(5, 5)
fmt.Println(x)</pre>
<p>上面的代码可以很轻松的编译通过，但是在运行时就出问题了，还是panic错误……</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic">panic: reflect: call of reflect.Value.Len on int Value

goroutine 1 [running]:
reflect.Value.Len(0x10b5240, 0x10eeb58, 0x82, 0x10716bc)
        /usr/local/Cellar/go/1.15.3/libexec/src/reflect/value.go:1162 +0x185
main.Map(0x10b5240, 0x10eeb58, 0x10b5240, 0x10eeb60, 0x1, 0x14, 0x0)
        /Users/chenhao/.../map.go:12 +0x16b
main.main()
        /Users/chenhao/.../map.go:42 +0x465
exit status 2</pre>
<h5>健壮版的Generic Map</h5>
<p>所以，如果要写一个健壮的程序，对于这种用<code>interface{}</code> 的“过度泛型”，就需要我们自己来做类型检查。下面是一个有类型检查的Map代码：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">func Transform(slice, function interface{}) interface{} {
  return transform(slice, function, false)
}

func TransformInPlace(slice, function interface{}) interface{} {
  return transform(slice, function, true)
}

func transform(slice, function interface{}, inPlace bool) interface{} {
 
  //check the <code data-enlighter-language="raw" class="EnlighterJSRAW">slice</code> type is Slice
  sliceInType := reflect.ValueOf(slice)
  if sliceInType.Kind() != reflect.Slice {
    panic("transform: not slice")
  }

  //check the function signature
  fn := reflect.ValueOf(function)
  elemType := sliceInType.Type().Elem()
  if !verifyFuncSignature(fn, elemType, nil) {
    panic("trasform: function must be of type func(" + sliceInType.Type().Elem().String() + ") outputElemType")
  }

  sliceOutType := sliceInType
  if !inPlace {
    sliceOutType = reflect.MakeSlice(reflect.SliceOf(fn.Type().Out(0)), sliceInType.Len(), sliceInType.Len())
  }
  for i := 0; i &lt; sliceInType.Len(); i++ {
    sliceOutType.Index(i).Set(fn.Call([]reflect.Value{sliceInType.Index(i)})[0])
  }
  return sliceOutType.Interface()

}

func verifyFuncSignature(fn reflect.Value, types ...reflect.Type) bool {

  //Check it is a funciton
  if fn.Kind() != reflect.Func {
    return false
  }
  // NumIn() - returns a function type's input parameter count.
  // NumOut() - returns a function type's output parameter count.
  if (fn.Type().NumIn() != len(types)-1) || (fn.Type().NumOut() != 1) {
    return false
  }
  // In() - returns the type of a function type's i'th input parameter.
  for i := 0; i &lt; len(types)-1; i++ {
    if fn.Type().In(i) != types[i] {
      return false
    }
  }
  // Out() - returns the type of a function type's i'th output parameter.
  outType := types[len(types)-1]
  if outType != nil &amp;&amp; fn.Type().Out(0) != outType {
    return false
  }
  return true
}
</pre>
<p>上面的代码一下子就复杂起来了，可见，复杂的代码都是在处理异常的地方。我不打算Walk through 所有的代码，别看代码多，但是还是可以读懂的，下面列几个代码中的要点：</p>
<ul>
<li>代码中没有使用Map函数，因为和数据结构和关键有含义冲突的问题，所以使用<code>Transform</code>，这个来源于 C++ STL库中的命名。</li>
<li>有两个版本的函数，一个是返回一个全新的数组 &#8211; <code>Transform()</code>，一个是“就地完成” &#8211; <code>TransformInPlace()</code></li>
<li>在主函数中，用 <code>Kind()</code> 方法检查了数据类型是不是 Slice，函数类型是不是Func</li>
<li>检查函数的参数和返回类型是通过 <code>verifyFuncSignature()</code> 来完成的，其中：
<ul>
<li><code>NumIn()</code> &#8211; 用来检查函数的“入参”</li>
<li> <code>NumOut()</code> 用来检查函数的“返回值”</li>
</ul>
</li>
<li>如果需要新生成一个Slice，会使用 <code>reflect.MakeSlice()</code> 来完成。</li>
</ul>
<p>好了，有了上面的这段代码，我们的代码就很可以很开心的使用了：</p>
<p>可以用于字符串数组</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">list := []string{"1", "2", "3", "4", "5", "6"}
result := Transform(list, func(a string) string{
    return a +a +a
})
//{"111","222","333","444","555","666"}
</pre>
<p>可以用于整形数组</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">list := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
TransformInPlace(list, func (a int) int {
  return a*3
})
//{3, 6, 9, 12, 15, 18, 21, 24, 27}
</pre>
<p>可以用于结构体</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">var list = []Employee{
    {"Hao", 44, 0, 8000},
    {"Bob", 34, 10, 5000},
    {"Alice", 23, 5, 9000},
    {"Jack", 26, 0, 4000},
    {"Tom", 48, 9, 7500},
}

result := TransformInPlace(list, func(e Employee) Employee {
    e.Salary += 1000
    e.Age += 1
    return e
})</pre>
<h5>健壮版的 Generic Reduce</h5>
<p>同样，泛型版的 Reduce 代码如下：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">func Reduce(slice, pairFunc, zero interface{}) interface{} {
  sliceInType := reflect.ValueOf(slice)
  if sliceInType.Kind() != reflect.Slice {
    panic("reduce: wrong type, not slice")
  }

  len := sliceInType.Len()
  if len == 0 {
    return zero
  } else if len == 1 {
    return sliceInType.Index(0)
  }

  elemType := sliceInType.Type().Elem()
  fn := reflect.ValueOf(pairFunc)
  if !verifyFuncSignature(fn, elemType, elemType, elemType) {
    t := elemType.String()
    panic("reduce: function must be of type func(" + t + ", " + t + ") " + t)
  }

  var ins [2]reflect.Value
  ins[0] = sliceInType.Index(0)
  ins[1] = sliceInType.Index(1)
  out := fn.Call(ins[:])[0]

  for i := 2; i &lt; len; i++ {
    ins[0] = out
    ins[1] = sliceInType.Index(i)
    out = fn.Call(ins[:])[0]
  }
  return out.Interface()
}</pre>
<h5>健壮版的 Generic Filter</h5>
<p>同样，泛型版的 Filter 代码如下（同样分是否“就地计算”的两个版本）：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic">func Filter(slice, function interface{}) interface{} {
  result, _ := filter(slice, function, false)
  return result
}

func FilterInPlace(slicePtr, function interface{}) {
  in := reflect.ValueOf(slicePtr)
  if in.Kind() != reflect.Ptr {
    panic("FilterInPlace: wrong type, " +
      "not a pointer to slice")
  }
  _, n := filter(in.Elem().Interface(), function, true)
  in.Elem().SetLen(n)
}

var boolType = reflect.ValueOf(true).Type()

func filter(slice, function interface{}, inPlace bool) (interface{}, int) {

  sliceInType := reflect.ValueOf(slice)
  if sliceInType.Kind() != reflect.Slice {
    panic("filter: wrong type, not a slice")
  }

  fn := reflect.ValueOf(function)
  elemType := sliceInType.Type().Elem()
  if !verifyFuncSignature(fn, elemType, boolType) {
    panic("filter: function must be of type func(" + elemType.String() + ") bool")
  }

  var which []int
  for i := 0; i &lt; sliceInType.Len(); i++ {
    if fn.Call([]reflect.Value{sliceInType.Index(i)})[0].Bool() {
      which = append(which, i)
    }
  }

  out := sliceInType

  if !inPlace {
    out = reflect.MakeSlice(sliceInType.Type(), len(which), len(which))
  }
  for i := range which {
    out.Index(i).Set(sliceInType.Index(which[i]))
  }

  return out.Interface(), len(which)
}</pre>
<h4>后记</h4>
<p>还有几个未尽事宜：</p>
<p>1）使用反射来做这些东西，会有一个问题，<strong>那就是代码的性能会很差。所以，上面的代码不能用于你需要高性能的地方</strong>。怎么解决这个问题，我们会在本系列文章的下一篇文章中讨论。</p>
<p>2）上面的代码大量的参考了 Rob Pike的版本，他的代码在 <a href="https://github.com/robpike/filter" target="_blank" rel="noopener">https://github.com/robpike/filter</a></p>
<p>3）其实，在全世界范围内，有大量的程序员都在问Go语言官方什么时候在标准库中支持 Map/Reduce，Rob Pike说，这种东西难写吗？还要我们官方来帮你们写么？这种代码我多少年前就写过了，但是，我从来一次都没有用过，我还是喜欢用“For循环”，我觉得你最好也跟我一起用 “For循环”。</p>
<p>我个人觉得，Map/Reduce在数据处理的时候还是很有用的，Rob Pike可能平时也不怎么写“业务逻辑”的代码，所以，对他来说可能也不太了解业务的变化有多么的频繁……</p>
<p>当然，好还是不好，由你来判断，但多学一些编程模式是对自己的帮助也是很有帮助的。</p>
<p>（全文完）<!--



<p align="center"><a href= target=_blank><img decoding="async" src=""></a></p>





<p align="center"><img decoding="async" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.weixin.jpg"> <img decoding="async" loading="lazy" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.mini_.jpg" width="300" height="300"> <br />关注CoolShell微信公众账号和微信小程序</p>

 

--></p>
<div style="margin-top: 15px; font-size: 16px;color: #cc0000;">
<p align="center"><strong>（转载本站文章请注明作者和出处 <a href="https://coolshell.cn/">酷 壳 &#8211; CoolShell</a> ，请勿用于任何商业用途）</strong></p>
</div>

<div class="wp_rp_wrap  wp_rp_vertical_m" id="wp_rp_first"><div class="wp_rp_content"><h3 class="related_post_title">相关文章</h3><ul class="related_post wp_rp"><li ><a href="https://coolshell.cn/articles/21146.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2020/12/go.options-150x150.png" alt="Go 编程模式：Functional Options" width="150" height="150" /></a><a href="https://coolshell.cn/articles/21146.html" class="wp_rp_title">Go 编程模式：Functional Options</a></li><li ><a href="https://coolshell.cn/articles/17929.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2017/06/go-hardhat-150x150.png" alt="Go编程模式：修饰器" width="150" height="150" /></a><a href="https://coolshell.cn/articles/17929.html" class="wp_rp_title">Go编程模式：修饰器</a></li><li ><a href="https://coolshell.cn/articles/21615.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2021/09/go-generics-150x150.png" alt="Go编程模式 ： 泛型编程" width="150" height="150" /></a><a href="https://coolshell.cn/articles/21615.html" class="wp_rp_title">Go编程模式 ： 泛型编程</a></li><li ><a href="https://coolshell.cn/articles/21263.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2020/12/go.k8s-150x150.png" alt="Go 编程模式：k8s Visitor 模式" width="150" height="150" /></a><a href="https://coolshell.cn/articles/21263.html" class="wp_rp_title">Go 编程模式：k8s Visitor 模式</a></li><li ><a href="https://coolshell.cn/articles/21228.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2020/12/go.line_.-150x150.png" alt="Go编程模式：Pipeline" width="150" height="150" /></a><a href="https://coolshell.cn/articles/21228.html" class="wp_rp_title">Go编程模式：Pipeline</a></li><li ><a href="https://coolshell.cn/articles/21214.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2020/12/go.pair_-150x150.png" alt="Go编程模式：委托和反转控制" width="150" height="150" /></a><a href="https://coolshell.cn/articles/21214.html" class="wp_rp_title">Go编程模式：委托和反转控制</a></li></ul></div></div>The post <a href="https://coolshell.cn/articles/21164.html">Go编程模式：Map-Reduce</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></content:encoded>
					
					<wfw:commentRss>https://coolshell.cn/articles/21164.html/feed</wfw:commentRss>
			<slash:comments>19</slash:comments>
		
		
			</item>
		<item>
		<title>Go 编程模式：Functional Options</title>
		<link>https://coolshell.cn/articles/21146.html</link>
					<comments>https://coolshell.cn/articles/21146.html#comments</comments>
		
		<dc:creator><![CDATA[陈皓]]></dc:creator>
		<pubDate>Tue, 22 Dec 2020 15:23:52 +0000</pubDate>
				<category><![CDATA[Go 语言]]></category>
		<category><![CDATA[程序设计]]></category>
		<category><![CDATA[编程语言]]></category>
		<category><![CDATA[functional]]></category>
		<category><![CDATA[functional-programming]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[golang]]></category>
		<category><![CDATA[Pattern]]></category>
		<category><![CDATA[函数式编程]]></category>
		<guid isPermaLink="false">https://coolshell.cn/?p=21146</guid>

					<description><![CDATA[<p>在本篇文章中，我们来讨论一下Functional Options这个编程模式。这是一个函数式编程的应用案例，编程技巧也很好，是目前在Go语言中最流行的一种编程模...</p>
<p class="read-more"><a class="btn btn-default" href="https://coolshell.cn/articles/21146.html"> Read More<span class="screen-reader-text">  Read More</span></a></p>
The post <a href="https://coolshell.cn/articles/21146.html">Go 编程模式：Functional Options</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></description>
										<content:encoded><![CDATA[<p><script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-3415450859608158"
     crossorigin="anonymous"></script><img decoding="async" loading="lazy" class="alignright size-medium wp-image-21241" src="https://coolshell.cn/wp-content/uploads/2020/12/go.options-300x186.png" alt="" width="300" height="186" srcset="https://coolshell.cn/wp-content/uploads/2020/12/go.options-300x186.png 300w, https://coolshell.cn/wp-content/uploads/2020/12/go.options-768x476.png 768w, https://coolshell.cn/wp-content/uploads/2020/12/go.options-436x270.png 436w, https://coolshell.cn/wp-content/uploads/2020/12/go.options.png 1015w" sizes="(max-width: 300px) 100vw, 300px" />在本篇文章中，我们来讨论一下Functional Options这个编程模式。这是一个函数式编程的应用案例，编程技巧也很好，是目前在Go语言中最流行的一种编程模式。但是，在我们正式讨论这个模式之前，我们需要先来看看要解决什么样的问题。</p>
<section class="post-series"><h3 class="post-series-title">本文是全系列中第3 / 10篇：<a href="https://coolshell.cn/articles/series/go%e7%bc%96%e7%a8%8b%e6%a8%a1%e5%bc%8f">Go编程模式</a></h3><ul class="post-series-list"><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/21128.html">Go编程模式：切片，接口，时间和性能</a></span></li><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/21140.html">Go 编程模式：错误处理</a></span></li><li class="post-series-item-current post-series-item"><span class="post-series-item-title">Go 编程模式：Functional Options</span></li><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/21214.html">Go编程模式：委托和反转控制</a></span></li><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/21164.html">Go编程模式：Map-Reduce</a></span></li><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/21179.html">Go 编程模式：Go Generation</a></span></li><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/17929.html">Go编程模式：修饰器</a></span></li><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/21228.html">Go编程模式：Pipeline</a></span></li><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/21263.html">Go 编程模式：k8s Visitor 模式</a></span></li><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/21615.html">Go编程模式 ： 泛型编程</a></span></li></ul><nav class="post-series-nav"><span class="post-series-nav-prev">&laquo; <a href="https://coolshell.cn/articles/21140.html" rel="prev" title="Go 编程模式：错误处理">上一篇文章</a></span><span class="post-series-nav-next"><a href="https://coolshell.cn/articles/21214.html" rel="next" title="Go编程模式：委托和反转控制">下一篇文章</a> &raquo;</span></nav></section>
<h4>配置选项问题</h4>
<p>在我们编程中，我们会经常性的需要对一个对象（或是业务实体）进行相关的配置。比如下面这个业务实体（注意，这仅只是一个示例）：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">type Server struct {
    Addr     string
    Port     int
    Protocol string
    Timeout  time.Duration
    MaxConns int
    TLS      *tls.Config
}</pre>
<p>在这个 <code>Server</code> 对象中，我们可以看到：</p>
<p><span id="more-21146"></span></p>
<ul>
<li>要有侦听的IP地址 <code>Addr</code> 和端口号 <code>Port</code> ，这两个配置选项是必填的（当然，IP地址和端口号都可以有默认值，当这里我们用于举例认为是没有默认值，而且不能为空，需要必填的）。</li>
<li>然后，还有协议 <code>Protocol</code> 、 <code>Timeout</code> 和<code>MaxConns</code> 字段，这几个字段是不能为空的，但是有默认值的，比如：协议是<code>tcp</code>, 超时<code>30</code>秒 和 最大链接数<code>1024</code>个。</li>
<li>还有一个 <code>TLS</code> 这个是安全链接，需要配置相关的证书和私钥。这个是可以为空的。</li>
</ul>
<p>所以，针对于上述这样的配置，我们需要有多种不同的创建不同配置 <code>Server</code> 的函数签名，如下所示（代码比较宽，需要左右滚动浏览）：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">func NewDefaultServer(addr string, port int) (*Server, error) {
  return &amp;Server{addr, port, "tcp", 30 * time.Second, 100, nil}, nil
}

func NewTLSServer(addr string, port int, tls *tls.Config) (*Server, error) {
  return &amp;Server{addr, port, "tcp", 30 * time.Second, 100, tls}, nil
}

func NewServerWithTimeout(addr string, port int, timeout time.Duration) (*Server, error) {
  return &amp;Server{addr, port, "tcp", timeout, 100, nil}, nil
}

func NewTLSServerWithMaxConnAndTimeout(addr string, port int, maxconns int, timeout time.Duration, tls *tls.Config) (*Server, error) {
  return &amp;Server{addr, port, "tcp", 30 * time.Second, maxconns, tls}, nil
}</pre>
<p>因为Go语言不支持重载函数，所以，你得用不同的函数名来应对不同的配置选项。</p>
<h4>配置对象方案</h4>
<p>要解决这个问题，最常见的方式是使用一个配置对象，如下所示：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic">type Config struct {
    Protocol string
    Timeout  time.Duration
    Maxconns int
    TLS      *tls.Config
}</pre>
<p>我们把那些非必输的选项都移到一个结构体里，于是 <code>Server</code> 对象变成了：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic">type Server struct {
    Addr string
    Port int
    Conf *Config
}</pre>
<p>于是，我们只需要一个 <code>NewServer()</code> 的函数了，在使用前需要构造 <code>Config</code> 对象。</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">func NewServer(addr string, port int, conf *Config) (*Server, error) {
    //...
}

//Using the default configuratrion
srv1, _ := NewServer("localhost", 9000, nil) 

conf := ServerConfig{Protocol:"tcp", Timeout: 60*time.Duration}
srv2, _ := NewServer("locahost", 9000, &amp;conf)</pre>
<p>这段代码算是不错了，大多数情况下，我们可能就止步于此了。但是，对于有洁癖的有追求的程序员来说，他们能看到其中有一点不好的是，<code>Config</code> 并不是必需的，所以，你需要判断是否是 <code>nil</code> 或是 Empty &#8211; <code> Config{}</code>这让我们的代码感觉还是有点不是很干净。</p>
<h4>Builder模式</h4>
<p>如果你是一个Java程序员，熟悉设计模式的一定会很自然地使用上Builder模式。比如如下的代码：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="java">User user = new User.Builder()
  .name("Hao Chen")
  .email("haoel@hotmail.com")
  .nickname("左耳朵")
  .build();</pre>
<p>仿照上面这个模式，我们可以把上面代码改写成如下的代码（注：下面的代码没有考虑出错处理，其中关于出错处理的更多内容，请参看《<a title="GO 编程模式：错误处理" href="https://coolshell.cn/articles/21140.html" target="_blank" rel="noopener">Go 编程模式：出错处理</a>》）：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">//使用一个builder类来做包装
type ServerBuilder struct {
  Server
}

func (sb *ServerBuilder) Create(addr string, port int) *ServerBuilder {
  sb.Server.Addr = addr
  sb.Server.Port = port
  //其它代码设置其它成员的默认值
  return sb
}

func (sb *ServerBuilder) WithProtocol(protocol string) *ServerBuilder {
  sb.Server.Protocol = protocol 
  return sb
}

func (sb *ServerBuilder) WithMaxConn( maxconn int) *ServerBuilder {
  sb.Server.MaxConns = maxconn
  return sb
}

func (sb *ServerBuilder) WithTimeOut( timeout time.Duration) *ServerBuilder {
  sb.Server.Timeout = timeout
  return sb
}

func (sb *ServerBuilder) WithTLS( tls *tls.Config) *ServerBuilder {
  sb.Server.TLS = tls
  return sb
}

func (sb *ServerBuilder) Build() (Server) {
  return  sb.Server
}
</pre>
<p>于是就可以以如下的方式来使用了</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic">sb := ServerBuilder{}
server, err := sb.Create("127.0.0.1", 8080).
  WithProtocol("udp").
  WithMaxConn(1024).
  WithTimeOut(30*time.Second).
  Build()</pre>
<p>上面这样的方式也很清楚，不需要额外的Config类，使用链式的函数调用的方式来构造一个对象，只需要多加一个Builder类，这个Builder类似乎有点多余，我们似乎可以直接在<code>Server</code> 上进行这样的 Builder 构造，的确是这样的。但是在处理错误的时候可能就有点麻烦（需要为Server结构增加一个error 成员，破坏了Server结构体的“纯洁”），不如一个包装类更好一些。</p>
<p>如果我们想省掉这个包装的结构体，那么就轮到我们的Functional Options上场了，函数式编程。</p>
<h4>Functional Options</h4>
<p>首先，我们先定义一个函数类型：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">type Option func(*Server)</pre>
<p>然后，我们可以使用函数式的方式定义一组如下的函数：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">func Protocol(p string) Option {
    return func(s *Server) {
        s.Protocol = p
    }
}
func Timeout(timeout time.Duration) Option {
    return func(s *Server) {
        s.Timeout = timeout
    }
}
func MaxConns(maxconns int) Option {
    return func(s *Server) {
        s.MaxConns = maxconns
    }
}
func TLS(tls *tls.Config) Option {
    return func(s *Server) {
        s.TLS = tls
    }
}</pre>
<p>上面这组代码传入一个参数，然后返回一个函数，返回的这个函数会设置自己的 <code>Server</code> 参数。例如：</p>
<ul>
<li>当我们调用其中的一个函数用 <code>MaxConns(30)</code> 时</li>
<li>其返回值是一个 <code>func(s* Server) { s.MaxConns = 30 }</code> 的函数。</li>
</ul>
<p>这个叫高阶函数。在数学上，就好像这样的数学定义，计算长方形面积的公式为： <code>rect(width, height) = width * height;</code> 这个函数需要两个参数，我们包装一下，就可以变成计算正方形面积的公式：<code>square(width) = rect(width, width)</code> 也就是说，<code>squre(width)</code>返回了另外一个函数，这个函数就是<code>rect(w,h)</code> 只不过他的两个参数是一样的。即：<code>f(x)  = g(x, x)</code></p>
<p>好了，现在我们再定一个 <code>NewServer()</code>的函数，其中，有一个可变参数 <code>options</code> 其可以传出多个上面上的函数，然后使用一个for-loop来设置我们的 <code>Server</code> 对象。</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">func NewServer(addr string, port int, options ...func(*Server)) (*Server, error) {

  srv := Server{
    Addr:     addr,
    Port:     port,
    Protocol: "tcp",
    Timeout:  30 * time.Second,
    MaxConns: 1000,
    TLS:      nil,
  }
  for _, option := range options {
    option(&amp;srv)
  }
  //...
  return &amp;srv, nil
}</pre>
<p>于是，我们在创建 <code>Server</code> 对象的时候，我们就可以这样来了。</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">s1, _ := NewServer("localhost", 1024)
s2, _ := NewServer("localhost", 2048, Protocol("udp"))
s3, _ := NewServer("0.0.0.0", 8080, Timeout(300*time.Second), MaxConns(1000))</pre>
<p>怎么样，是不是高度的整洁和优雅？不但解决了使用 <code>Config</code> 对象方式 的需要有一个config参数，但在不需要的时候，是放 <code>nil</code> 还是放 <code>Config{}</code>的选择困难，也不需要引用一个Builder的控制对象，直接使用函数式编程的试，在代码阅读上也很优雅。</p>
<p>所以，以后，大家在要玩类似的代码时，强烈推荐使用Functional Options这种方式，这种方式至少带来了如下的好处：</p>
<ul>
<li>直觉式的编程</li>
<li>高度的可配置化</li>
<li>很容易维护和扩展</li>
<li>自文档</li>
<li>对于新来的人很容易上手</li>
<li>没有什么令人困惑的事（是nil 还是空）</li>
</ul>
<h4>参考文档</h4>
<ul>
<li><b>“Self referential functions and design” by Rob Pike<br />
</b><a href="http://commandcenter.blogspot.com.au/2014/01/self-referential-functions-and-design.html">http://commandcenter.blogspot.com.au/2014/01/self-referential-functions-and-design.html</a></li>
</ul>
<p>(全文完)<!--



<p align="center"><a href= target=_blank><img decoding="async" src=""></a></p>





<p align="center"><img decoding="async" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.weixin.jpg"> <img decoding="async" loading="lazy" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.mini_.jpg" width="300" height="300"> <br />关注CoolShell微信公众账号和微信小程序</p>

 

--></p>
<div style="margin-top: 15px; font-size: 16px;color: #cc0000;">
<p align="center"><strong>（转载本站文章请注明作者和出处 <a href="https://coolshell.cn/">酷 壳 &#8211; CoolShell</a> ，请勿用于任何商业用途）</strong></p>
</div>

<div class="wp_rp_wrap  wp_rp_vertical_m" ><div class="wp_rp_content"><h3 class="related_post_title">相关文章</h3><ul class="related_post wp_rp"><li ><a href="https://coolshell.cn/articles/21164.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2020/12/go.map_.reduce-150x150.png" alt="Go编程模式：Map-Reduce" width="150" height="150" /></a><a href="https://coolshell.cn/articles/21164.html" class="wp_rp_title">Go编程模式：Map-Reduce</a></li><li ><a href="https://coolshell.cn/articles/17929.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2017/06/go-hardhat-150x150.png" alt="Go编程模式：修饰器" width="150" height="150" /></a><a href="https://coolshell.cn/articles/17929.html" class="wp_rp_title">Go编程模式：修饰器</a></li><li ><a href="https://coolshell.cn/articles/21615.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2021/09/go-generics-150x150.png" alt="Go编程模式 ： 泛型编程" width="150" height="150" /></a><a href="https://coolshell.cn/articles/21615.html" class="wp_rp_title">Go编程模式 ： 泛型编程</a></li><li ><a href="https://coolshell.cn/articles/21263.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2020/12/go.k8s-150x150.png" alt="Go 编程模式：k8s Visitor 模式" width="150" height="150" /></a><a href="https://coolshell.cn/articles/21263.html" class="wp_rp_title">Go 编程模式：k8s Visitor 模式</a></li><li ><a href="https://coolshell.cn/articles/21228.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2020/12/go.line_.-150x150.png" alt="Go编程模式：Pipeline" width="150" height="150" /></a><a href="https://coolshell.cn/articles/21228.html" class="wp_rp_title">Go编程模式：Pipeline</a></li><li ><a href="https://coolshell.cn/articles/21214.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2020/12/go.pair_-150x150.png" alt="Go编程模式：委托和反转控制" width="150" height="150" /></a><a href="https://coolshell.cn/articles/21214.html" class="wp_rp_title">Go编程模式：委托和反转控制</a></li></ul></div></div>The post <a href="https://coolshell.cn/articles/21146.html">Go 编程模式：Functional Options</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></content:encoded>
					
					<wfw:commentRss>https://coolshell.cn/articles/21146.html/feed</wfw:commentRss>
			<slash:comments>15</slash:comments>
		
		
			</item>
	</channel>
</rss>
