In Golang, is it generally preferred to write methods that return new values and then save the result, or methods that mutate receivers and arguments?

agolangf · · 387 次点击    
这是一个分享于 的资源,其中的信息可能已经有所发展或是发生改变。
<p>I&#39;m totally new to Go, and I was hoping someone could give me a bit of guidance as to what the best practices/patterns in it are.</p> <p>Say that I&#39;m writing a tool that connects to public APIs over the internet, and saves response information to a local database. I make a struct, <code>apiClient</code>, and a constructor function, <code>newApiClient()</code>, that initialises a fresh one for me -- it authenticates and gets my access token, sets up a DB connection, starts a timer so I know when my token expires, and so on.</p> <p>Now, setting up a DB connection is a fair chunk of code, what with the error handling and such. So I make <code>newDbConnection()</code> its own function.</p> <p>Here&#39;s my question.</p> <p>Is it generally better to make <code>newDbConnection()</code> a pointer-method (as in, <code>func (client *apiClient) newDbConnection()</code>) that creates a DB connection and then mutates the <code>apiClient</code>&#39;s <code>dbConn</code> field, or to make it a plain old function that just returns a new DB connection, and then call <code>apiClient{dbConn: newDbConnection()}</code> in the constructor?</p> <p>In most languages, I would prefer the latter, because there&#39;s always a preference towards functions as close to pure as you can get them. But I usually work in functional languages with immutable structures, where everything you pass or return is invisibly a reference and it doesn&#39;t matter because you can never change values. In Go&#39;s case, I&#39;m more uncertain: is it more efficient to directly mutate with pointers rather than deal with assignment and return and if so, is the benefit of this generally considered worth the reduced readability? (Or is it actually MORE readable for Go programmers since they&#39;re used to mutation existing?)</p> <hr/>**评论:**<br/><br/>elagergren: <pre><p>Well, first—just to make sure this isn&#39;t an A/B problem—make sure you check out the <code>database/sql</code> and <code>database/sql/driver</code> packages.</p> <p>Second, I&#39;d prefer the latter. However, if this is all local to your <code>NewAPIClient</code> function, it doesn&#39;t really matter—do whatever is the easiest to maintain and expresses what you&#39;re trying to do the best.</p></pre>0xjnml: <pre><p>nit: mutating the receiver/function argument is not observable at the call site.</p></pre>aboukirev: <pre><p>There are a few things to consider in addition to what you already know. First, consider memory allocations: the fewer you make, the easier it is on the garbage collector and fewer possibilities for memory leaks. Second, opening database connection returns a reference to a pool, which will handle multiple actual connections for you. Maybe you do not need to create new connections yourself (unless you are opening one per goroutine). </p></pre>skidooer: <pre><p>If the API client depends on a database handle, pass it in on construction:<code>func newApiClient(db *sql.DB)</code>.</p> <p>But, more importantly, does an API client that connects to public APIs over the internet really depend on a local database connection? I get the impression you are mixing concerns. You may want to rethink your architecture completely (or, at least your naming if I have been given the wrong idea of what the structure is doing).</p></pre>akavel: <pre><p>In the case you described, the &#34;functional-like&#34; approach would be preferable. As a general rule, aiming for readability is highly valuable and &#34;good style&#34; in Go too. (By the way: if you choose the opposite pattern in the end, it&#39;s preferable to not call it <code>newSomething</code>, which by convention is used for functions returning the created object. I tend to use <code>initSomething</code> in the rare case I pick this approach; or possibly <code>resetSomething</code>, or else, depending on exact semantics.)</p> <p>Regarding what is &#34;more efficient&#34;... in general, it&#39;s advised to <em>&#34;measure first[, and only then maybe optimize based on that]&#34;</em>. Technically, <a href="https://golang.org/pkg/testing/#Benchmark" rel="nofollow"><code>testing.Benchmark</code></a> helps here (there&#39;s also some tool to compare results of Benchmark before vs. after code changes, but I don&#39;t recall where to find it). It may well show up that compiler would actually optimize both patterns to same code! Anyway, one should also be looking at &#34;measure&#34; in wider perspective, not just of a microbenchmark, but of a whole program. Specifically: is your function going to be ever called in a hot loop/hot path of some code? Or maybe the connections will be pooled, so small performance differences don&#39;t really matter much? Even in case of a hot path, won&#39;t later calls of the actual SQL queries take long enough to make performance of the constructor irrelevant in comparison anyway?</p> <p>But please note that this comment is specifically aimed at the particular case you asked. If I were to look only at the one-line header/subject of your question, without reading the details, I&#39;d actually answer quite differently!</p> <p>As a closing note, it&#39;s especially worthwhile to read the standard library code from time to time, to get some feeling for what&#39;s generally observed as &#34;good style&#34;. (Though with some caveats in mind, too; e.g. usage of assembly wouldn&#39;t generally be a good idea outside of stdlib.)</p></pre>zandorz: <pre><p><a href="http://tmrts.com/go-patterns/" rel="nofollow">http://tmrts.com/go-patterns/</a></p></pre>adamtanner: <pre><p>The latter is more common.</p> <p>It is easier to test (sql.Open a local/in-memory/mock DB in test and give that to the client) and allows you to reuse the same sql.DB instance which is generally a good idea because it actually represents a pool of connections and is safe for concurrent use.</p> <p>EDIT: just realized you said you were doing the dbConnection() call in the constructor. It&#39;s often a better idea to pass the handle to the database as an argument to the constructor and do the sql.Open call in main or your test. Generally makes the whole system easier to test and reduces coupling.</p></pre>

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

387 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传