commit ca571eb69156ef1c20f296dc0d631be505b3588e Author: github-actions[bot] Date: Thu Aug 4 06:59:33 2022 +0000 deploy: 06a1bdf7353d7ab131c932b22e155c41f763dde3 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..5282307 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.c linguist-language=Go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1e773c2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/_book +/book + +*.out* +_zz* diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 0000000..e69de29 diff --git a/404.html b/404.html new file mode 100644 index 0000000..57c552c --- /dev/null +++ b/404.html @@ -0,0 +1,225 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

Document not found (404)

+

This URL is invalid, sorry. Please use the navigation bar or search to continue.

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/FontAwesome/css/font-awesome.css b/FontAwesome/css/font-awesome.css new file mode 100644 index 0000000..540440c --- /dev/null +++ b/FontAwesome/css/font-awesome.css @@ -0,0 +1,4 @@ +/*! + * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome + * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) + */@font-face{font-family:'FontAwesome';src:url('../fonts/fontawesome-webfont.eot?v=4.7.0');src:url('../fonts/fontawesome-webfont.eot?#iefix&v=4.7.0') format('embedded-opentype'),url('../fonts/fontawesome-webfont.woff2?v=4.7.0') format('woff2'),url('../fonts/fontawesome-webfont.woff?v=4.7.0') format('woff'),url('../fonts/fontawesome-webfont.ttf?v=4.7.0') format('truetype'),url('../fonts/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}.fa{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571429em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left{margin-right:.3em}.fa.fa-pull-right{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scale(1, -1);-ms-transform:scale(1, -1);transform:scale(1, -1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-remove:before,.fa-close:before,.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook-f:before,.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-feed:before,.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-desc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-asc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before,.fa-gratipay:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-institution:before,.fa-bank:before,.fa-university:before{content:"\f19c"}.fa-mortar-board:before,.fa-graduation-cap:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:"\f1c5"}.fa-file-zip-o:before,.fa-file-archive-o:before{content:"\f1c6"}.fa-file-sound-o:before,.fa-file-audio-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-resistance:before,.fa-rebel:before{content:"\f1d0"}.fa-ge:before,.fa-empire:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-y-combinator-square:before,.fa-yc-square:before,.fa-hacker-news:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-send:before,.fa-paper-plane:before{content:"\f1d8"}.fa-send-o:before,.fa-paper-plane-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-circle-thin:before{content:"\f1db"}.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:"\f1e3"}.fa-tty:before{content:"\f1e4"}.fa-binoculars:before{content:"\f1e5"}.fa-plug:before{content:"\f1e6"}.fa-slideshare:before{content:"\f1e7"}.fa-twitch:before{content:"\f1e8"}.fa-yelp:before{content:"\f1e9"}.fa-newspaper-o:before{content:"\f1ea"}.fa-wifi:before{content:"\f1eb"}.fa-calculator:before{content:"\f1ec"}.fa-paypal:before{content:"\f1ed"}.fa-google-wallet:before{content:"\f1ee"}.fa-cc-visa:before{content:"\f1f0"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-bell-slash:before{content:"\f1f6"}.fa-bell-slash-o:before{content:"\f1f7"}.fa-trash:before{content:"\f1f8"}.fa-copyright:before{content:"\f1f9"}.fa-at:before{content:"\f1fa"}.fa-eyedropper:before{content:"\f1fb"}.fa-paint-brush:before{content:"\f1fc"}.fa-birthday-cake:before{content:"\f1fd"}.fa-area-chart:before{content:"\f1fe"}.fa-pie-chart:before{content:"\f200"}.fa-line-chart:before{content:"\f201"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-bicycle:before{content:"\f206"}.fa-bus:before{content:"\f207"}.fa-ioxhost:before{content:"\f208"}.fa-angellist:before{content:"\f209"}.fa-cc:before{content:"\f20a"}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:"\f20b"}.fa-meanpath:before{content:"\f20c"}.fa-buysellads:before{content:"\f20d"}.fa-connectdevelop:before{content:"\f20e"}.fa-dashcube:before{content:"\f210"}.fa-forumbee:before{content:"\f211"}.fa-leanpub:before{content:"\f212"}.fa-sellsy:before{content:"\f213"}.fa-shirtsinbulk:before{content:"\f214"}.fa-simplybuilt:before{content:"\f215"}.fa-skyatlas:before{content:"\f216"}.fa-cart-plus:before{content:"\f217"}.fa-cart-arrow-down:before{content:"\f218"}.fa-diamond:before{content:"\f219"}.fa-ship:before{content:"\f21a"}.fa-user-secret:before{content:"\f21b"}.fa-motorcycle:before{content:"\f21c"}.fa-street-view:before{content:"\f21d"}.fa-heartbeat:before{content:"\f21e"}.fa-venus:before{content:"\f221"}.fa-mars:before{content:"\f222"}.fa-mercury:before{content:"\f223"}.fa-intersex:before,.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-venus-double:before{content:"\f226"}.fa-mars-double:before{content:"\f227"}.fa-venus-mars:before{content:"\f228"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-neuter:before{content:"\f22c"}.fa-genderless:before{content:"\f22d"}.fa-facebook-official:before{content:"\f230"}.fa-pinterest-p:before{content:"\f231"}.fa-whatsapp:before{content:"\f232"}.fa-server:before{content:"\f233"}.fa-user-plus:before{content:"\f234"}.fa-user-times:before{content:"\f235"}.fa-hotel:before,.fa-bed:before{content:"\f236"}.fa-viacoin:before{content:"\f237"}.fa-train:before{content:"\f238"}.fa-subway:before{content:"\f239"}.fa-medium:before{content:"\f23a"}.fa-yc:before,.fa-y-combinator:before{content:"\f23b"}.fa-optin-monster:before{content:"\f23c"}.fa-opencart:before{content:"\f23d"}.fa-expeditedssl:before{content:"\f23e"}.fa-battery-4:before,.fa-battery:before,.fa-battery-full:before{content:"\f240"}.fa-battery-3:before,.fa-battery-three-quarters:before{content:"\f241"}.fa-battery-2:before,.fa-battery-half:before{content:"\f242"}.fa-battery-1:before,.fa-battery-quarter:before{content:"\f243"}.fa-battery-0:before,.fa-battery-empty:before{content:"\f244"}.fa-mouse-pointer:before{content:"\f245"}.fa-i-cursor:before{content:"\f246"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-sticky-note:before{content:"\f249"}.fa-sticky-note-o:before{content:"\f24a"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-diners-club:before{content:"\f24c"}.fa-clone:before{content:"\f24d"}.fa-balance-scale:before{content:"\f24e"}.fa-hourglass-o:before{content:"\f250"}.fa-hourglass-1:before,.fa-hourglass-start:before{content:"\f251"}.fa-hourglass-2:before,.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-3:before,.fa-hourglass-end:before{content:"\f253"}.fa-hourglass:before{content:"\f254"}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:"\f255"}.fa-hand-stop-o:before,.fa-hand-paper-o:before{content:"\f256"}.fa-hand-scissors-o:before{content:"\f257"}.fa-hand-lizard-o:before{content:"\f258"}.fa-hand-spock-o:before{content:"\f259"}.fa-hand-pointer-o:before{content:"\f25a"}.fa-hand-peace-o:before{content:"\f25b"}.fa-trademark:before{content:"\f25c"}.fa-registered:before{content:"\f25d"}.fa-creative-commons:before{content:"\f25e"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-tripadvisor:before{content:"\f262"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-get-pocket:before{content:"\f265"}.fa-wikipedia-w:before{content:"\f266"}.fa-safari:before{content:"\f267"}.fa-chrome:before{content:"\f268"}.fa-firefox:before{content:"\f269"}.fa-opera:before{content:"\f26a"}.fa-internet-explorer:before{content:"\f26b"}.fa-tv:before,.fa-television:before{content:"\f26c"}.fa-contao:before{content:"\f26d"}.fa-500px:before{content:"\f26e"}.fa-amazon:before{content:"\f270"}.fa-calendar-plus-o:before{content:"\f271"}.fa-calendar-minus-o:before{content:"\f272"}.fa-calendar-times-o:before{content:"\f273"}.fa-calendar-check-o:before{content:"\f274"}.fa-industry:before{content:"\f275"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-map-o:before{content:"\f278"}.fa-map:before{content:"\f279"}.fa-commenting:before{content:"\f27a"}.fa-commenting-o:before{content:"\f27b"}.fa-houzz:before{content:"\f27c"}.fa-vimeo:before{content:"\f27d"}.fa-black-tie:before{content:"\f27e"}.fa-fonticons:before{content:"\f280"}.fa-reddit-alien:before{content:"\f281"}.fa-edge:before{content:"\f282"}.fa-credit-card-alt:before{content:"\f283"}.fa-codiepie:before{content:"\f284"}.fa-modx:before{content:"\f285"}.fa-fort-awesome:before{content:"\f286"}.fa-usb:before{content:"\f287"}.fa-product-hunt:before{content:"\f288"}.fa-mixcloud:before{content:"\f289"}.fa-scribd:before{content:"\f28a"}.fa-pause-circle:before{content:"\f28b"}.fa-pause-circle-o:before{content:"\f28c"}.fa-stop-circle:before{content:"\f28d"}.fa-stop-circle-o:before{content:"\f28e"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-hashtag:before{content:"\f292"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-percent:before{content:"\f295"}.fa-gitlab:before{content:"\f296"}.fa-wpbeginner:before{content:"\f297"}.fa-wpforms:before{content:"\f298"}.fa-envira:before{content:"\f299"}.fa-universal-access:before{content:"\f29a"}.fa-wheelchair-alt:before{content:"\f29b"}.fa-question-circle-o:before{content:"\f29c"}.fa-blind:before{content:"\f29d"}.fa-audio-description:before{content:"\f29e"}.fa-volume-control-phone:before{content:"\f2a0"}.fa-braille:before{content:"\f2a1"}.fa-assistive-listening-systems:before{content:"\f2a2"}.fa-asl-interpreting:before,.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-deafness:before,.fa-hard-of-hearing:before,.fa-deaf:before{content:"\f2a4"}.fa-glide:before{content:"\f2a5"}.fa-glide-g:before{content:"\f2a6"}.fa-signing:before,.fa-sign-language:before{content:"\f2a7"}.fa-low-vision:before{content:"\f2a8"}.fa-viadeo:before{content:"\f2a9"}.fa-viadeo-square:before{content:"\f2aa"}.fa-snapchat:before{content:"\f2ab"}.fa-snapchat-ghost:before{content:"\f2ac"}.fa-snapchat-square:before{content:"\f2ad"}.fa-pied-piper:before{content:"\f2ae"}.fa-first-order:before{content:"\f2b0"}.fa-yoast:before{content:"\f2b1"}.fa-themeisle:before{content:"\f2b2"}.fa-google-plus-circle:before,.fa-google-plus-official:before{content:"\f2b3"}.fa-fa:before,.fa-font-awesome:before{content:"\f2b4"}.fa-handshake-o:before{content:"\f2b5"}.fa-envelope-open:before{content:"\f2b6"}.fa-envelope-open-o:before{content:"\f2b7"}.fa-linode:before{content:"\f2b8"}.fa-address-book:before{content:"\f2b9"}.fa-address-book-o:before{content:"\f2ba"}.fa-vcard:before,.fa-address-card:before{content:"\f2bb"}.fa-vcard-o:before,.fa-address-card-o:before{content:"\f2bc"}.fa-user-circle:before{content:"\f2bd"}.fa-user-circle-o:before{content:"\f2be"}.fa-user-o:before{content:"\f2c0"}.fa-id-badge:before{content:"\f2c1"}.fa-drivers-license:before,.fa-id-card:before{content:"\f2c2"}.fa-drivers-license-o:before,.fa-id-card-o:before{content:"\f2c3"}.fa-quora:before{content:"\f2c4"}.fa-free-code-camp:before{content:"\f2c5"}.fa-telegram:before{content:"\f2c6"}.fa-thermometer-4:before,.fa-thermometer:before,.fa-thermometer-full:before{content:"\f2c7"}.fa-thermometer-3:before,.fa-thermometer-three-quarters:before{content:"\f2c8"}.fa-thermometer-2:before,.fa-thermometer-half:before{content:"\f2c9"}.fa-thermometer-1:before,.fa-thermometer-quarter:before{content:"\f2ca"}.fa-thermometer-0:before,.fa-thermometer-empty:before{content:"\f2cb"}.fa-shower:before{content:"\f2cc"}.fa-bathtub:before,.fa-s15:before,.fa-bath:before{content:"\f2cd"}.fa-podcast:before{content:"\f2ce"}.fa-window-maximize:before{content:"\f2d0"}.fa-window-minimize:before{content:"\f2d1"}.fa-window-restore:before{content:"\f2d2"}.fa-times-rectangle:before,.fa-window-close:before{content:"\f2d3"}.fa-times-rectangle-o:before,.fa-window-close-o:before{content:"\f2d4"}.fa-bandcamp:before{content:"\f2d5"}.fa-grav:before{content:"\f2d6"}.fa-etsy:before{content:"\f2d7"}.fa-imdb:before{content:"\f2d8"}.fa-ravelry:before{content:"\f2d9"}.fa-eercast:before{content:"\f2da"}.fa-microchip:before{content:"\f2db"}.fa-snowflake-o:before{content:"\f2dc"}.fa-superpowers:before{content:"\f2dd"}.fa-wpexplorer:before{content:"\f2de"}.fa-meetup:before{content:"\f2e0"}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto} diff --git a/FontAwesome/fonts/FontAwesome.ttf b/FontAwesome/fonts/FontAwesome.ttf new file mode 100644 index 0000000..35acda2 Binary files /dev/null and b/FontAwesome/fonts/FontAwesome.ttf differ diff --git a/FontAwesome/fonts/fontawesome-webfont.eot b/FontAwesome/fonts/fontawesome-webfont.eot new file mode 100644 index 0000000..e9f60ca Binary files /dev/null and b/FontAwesome/fonts/fontawesome-webfont.eot differ diff --git a/FontAwesome/fonts/fontawesome-webfont.svg b/FontAwesome/fonts/fontawesome-webfont.svg new file mode 100644 index 0000000..855c845 --- /dev/null +++ b/FontAwesome/fonts/fontawesome-webfont.svg @@ -0,0 +1,2671 @@ + + + + +Created by FontForge 20120731 at Mon Oct 24 17:37:40 2016 + By ,,, +Copyright Dave Gandy 2016. All rights reserved. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/FontAwesome/fonts/fontawesome-webfont.ttf b/FontAwesome/fonts/fontawesome-webfont.ttf new file mode 100644 index 0000000..35acda2 Binary files /dev/null and b/FontAwesome/fonts/fontawesome-webfont.ttf differ diff --git a/FontAwesome/fonts/fontawesome-webfont.woff b/FontAwesome/fonts/fontawesome-webfont.woff new file mode 100644 index 0000000..400014a Binary files /dev/null and b/FontAwesome/fonts/fontawesome-webfont.woff differ diff --git a/FontAwesome/fonts/fontawesome-webfont.woff2 b/FontAwesome/fonts/fontawesome-webfont.woff2 new file mode 100644 index 0000000..4d13fc6 Binary files /dev/null and b/FontAwesome/fonts/fontawesome-webfont.woff2 differ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..29b69f9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2015 Golang-China. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..5bdc5c2 --- /dev/null +++ b/Makefile @@ -0,0 +1,12 @@ +# Copyright 2015 Golang-China. All rights reserved. +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file. + +# install mkbook + +# http://www.imagemagick.org/ + +default: + mdbook serve + +clean: diff --git a/appendix/appendix-a-errata.html b/appendix/appendix-a-errata.html new file mode 100644 index 0000000..38656c4 --- /dev/null +++ b/appendix/appendix-a-errata.html @@ -0,0 +1,272 @@ + + + + + + 附录A:原文勘误 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

附录A:原文勘误

+

p.9, ¶2: for "can compared", read "can be compared". (Thanks to Antonio Macías Ojeda, 2015-10-22. Corrected in the second printing.)

+

p.13: As printed, the gopl.io/ch1/lissajous program is deterministic, not random. We've added the statement below to the downloadable program so that it prints a pseudo-random image each time it is run. (Thanks to Randall McPherson, 2015-10-19.)

+

rand.Seed(time.Now().UTC().UnixNano())

+

p.15, ¶2: For "inner loop", read "outer loop". (Thanks to Ralph Corderoy, 2015-11-28. Corrected in the third printing.)

+

p.19, ¶2: For "Go's libraries makes", read "Go's library makes". (Thanks to Victor Farazdagi, 2015-11-30. Corrected in the third printing.)

+

p.40, ¶4: For "value of the underlying type", read "value of an unnamed type with the same underlying type". (Thanks to Carlos Romero Brox, 2015-12-19.)

+

p.40, ¶1: The paragraph should end with a period, not a comma. (Thanks to Victor Farazdagi, 2015-11-30. Corrected in the third printing.)

+

p.43, ¶3: Import declarations are explained in §10.4, not §10.3. (Thanks to Peter Jurgensen, 2015-11-21. Corrected in the third printing.)

+

p.48: f.ReadByte() serves as an example of a reference to f, but *os.File has no such method. For "ReadByte", read "Stat", four times. (Thanks to Peter Olsen, 2016-01-06. Corrected in the third printing.)

+

p.52, ¶2: for "an synonym", read "a synonym", twice. (Corrected in the second printing.)

+

p.52, ¶9: for "The integer arithmetic operators", read "The arithmetic operators". (Thanks to Yoshiki Shibata, 2015-12-20.)

+

p.68: the table of UTF-8 encodings is missing a bit from each first byte. The corrected table is shown below. (Thanks to Akshay Kumar, 2015-11-02. Corrected in the second printing.)

+
0xxxxxxx                             runes 0−127     (ASCII)
+110xxxxx 10xxxxxx                    128−2047        (values <128 unused)
+1110xxxx 10xxxxxx 10xxxxxx           2048−65535      (values <2048 unused)
+11110xxx 10xxxxxx 10xxxxxx 10xxxxxx  65536−0x10ffff  (other values unused)
+
+

p.73, ¶1: For "a exercise", read "an exercise". (Thanks to vrajmohan, 2015-12-28.)

+

p.74: the comment in gopl.io/ch3/printints should say fmt.Sprint, not fmt.Sprintf. (Corrected in the second printing.)

+

p.75, ¶4: for "%u", read "%o". (Thanks to William Hannish, 2015-12-21.)

+

p.76: the comment // "time.Duration 5m0s should have a closing double-quotation mark. (Corrected in the second printing.)

+

p.79, ¶4: "When an untyped constant is assigned to a variable, as in the first statement below, or +appears on the right-hand side of a variable declaration with an explicit type, as in the other three statements, ..." has it backwards: the first statement is a declaration; the other three are assignments. (Thanks to Yoshiki Shibata, 2015-11-09. Corrected in the third printing.)

+

p.112: Exercise 4.11 calls for a "CRUD" (create, read, update, delete) tool for GitHub Issues. Since GitHub does not currently allow Issues to be deleted, for "delete", read "close". (Thanks to Yoshiki Shibata, 2016-01-18.)

+

p.115: The anchor element in gopl.io/ch4/issueshtml's template is missing a closing </a> tag. (Thanks to Taj Khattra, 2016-01-19.)

+

p.132, code display following ¶3: the final comment should read: // compile error: can't assign func(int, int) int to func(int) int (Thanks to Toni Suter, 2015-11-21. Corrected in the third printing.)

+

p.160, ¶4: For Get("item")), read Get("item"). (Thanks to Yoshiki Shibata, 2016-02-01.)

+

p.166, ¶2: for "way", read "a way". (Corrected in the third printing.)

+

p.200, TestEval function: the format string in the final call to t.Errorf should format test.env with %v, not %s. (Thanks to Mitsuteru Sawa, 2015-12-07. Corrected in the third printing.)

+

p.222, Exercise 8.1: The port numbers for London and Tokyo should be swapped in the final command to match the earlier commands. (Thanks to Kiyoshi Kamishima, 2016-01-08.)

+

p.272, ¶3: for "the request body", read "the response body". (Thanks to 曹春晖, 2016-01-19.)

+

p.288, code display following ¶4: In the import declaration, for "database/mysql", read "database/sql". (Thanks to Jose Colon Rodriguez, 2016-01-09.)

+

p.347, Exercise 12.8: for "like json.Marshal", read "like json.Unmarshal". (Thanks to chai2010, 2016-01-01.)

+

p.362: the gopl.io/ch13/bzip program does not comply with the proposed rules for passing pointers between Go and C code because the C function bz2compress temporarily stores a Go pointer (in) into the Go heap (the bz_stream variable). The bz_stream variable should be allocated, and explicitly freed after the call to BZ2_bzCompressEnd, by C functions. (Thanks to Joe Tsai, 2015-11-18. Corrected in the third printing.)

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/appendix/appendix-b-author.html b/appendix/appendix-b-author.html new file mode 100644 index 0000000..5da194c --- /dev/null +++ b/appendix/appendix-b-author.html @@ -0,0 +1,251 @@ + + + + + + 附录B:作者译者 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

附录B:作者/译者

+

英文作者

+ +
+

中文译者

+ + + + + +
中文译者章节
chai2010 <chaishushan@gmail.com>前言/第2 ~ 4章/第10 ~ 13章
Xargin <cao1988228@163.com>第1章/第6章/第8 ~ 9章
CrazySssst第5章
foreversmart <njutree@gmail.com>第7章
+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/appendix/appendix-c-cpoyright.html b/appendix/appendix-c-cpoyright.html new file mode 100644 index 0000000..a0ab6be --- /dev/null +++ b/appendix/appendix-c-cpoyright.html @@ -0,0 +1,240 @@ + + + + + + 附录C:译文授权 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

附录C:译文授权

+

除特别注明外,本站内容均采用知识共享-署名(CC-BY) 3.0协议授权,代码遵循Go项目的BSD协议授权。

+

Creative Commons License

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/appendix/appendix-d-translations.html b/appendix/appendix-d-translations.html new file mode 100644 index 0000000..d6de9a3 --- /dev/null +++ b/appendix/appendix-d-translations.html @@ -0,0 +1,243 @@ + + + + + + 附录D:其它语言 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

附录D:其它语言

+

下表是 The Go Programming Language 其它语言版本:

+ + + + + + + + + +
语言链接时间译者ISBN
中文《Go语言圣经》2016/2/1chai2010, Xargin, CrazySssst, foreversmart?
韩语Acorn Publishing (Korea)2016Seung Lee9788960778320
俄语Williams Publishing (Russia)2016?9785845920515
波兰语Helion (Poland)2016??
日语Maruzen Publishing (Japan)2017Yoshiki Shibata9784621300251
葡萄牙语Novatec Editora (Brazil)2017??
中文简体Pearson Education Asia2017??
中文繁体Gotop Information (Taiwan)2017??
+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/appendix/appendix.html b/appendix/appendix.html new file mode 100644 index 0000000..e710519 --- /dev/null +++ b/appendix/appendix.html @@ -0,0 +1,240 @@ + + + + + + 附录 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

附录

+

英文原版并没有包含附录部分,只有一个索引部分。中文版增加附录部分主要用于收录一些和本书相关的内容,比如英文原版的勘误(有些读者可能会对照中文和英文原阅读)、英文作者和中文译者、译文授权等内容。以后还可能会考虑增加一些习题解答相关的内容。

+

需要特别说明的是,中文版附录并没有包含英文原版的索引信息。因为英文原版的索引信息主要是记录每个索引所在的英文页面位置,而中文版是以GitBook方式组织的html网页形式,将英文页面位置转为章节位置可能会更合理,不过这个会涉及到繁琐的手工操作。如果大家有更好的建议,请告知我们。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ayu-highlight.css b/ayu-highlight.css new file mode 100644 index 0000000..0c45c6f --- /dev/null +++ b/ayu-highlight.css @@ -0,0 +1,79 @@ +/* +Based off of the Ayu theme +Original by Dempfi (https://github.com/dempfi/ayu) +*/ + +.hljs { + display: block; + overflow-x: auto; + background: #191f26; + color: #e6e1cf; + padding: 0.5em; +} + +.hljs-comment, +.hljs-quote { + color: #5c6773; + font-style: italic; +} + +.hljs-variable, +.hljs-template-variable, +.hljs-attribute, +.hljs-attr, +.hljs-regexp, +.hljs-link, +.hljs-selector-id, +.hljs-selector-class { + color: #ff7733; +} + +.hljs-number, +.hljs-meta, +.hljs-builtin-name, +.hljs-literal, +.hljs-type, +.hljs-params { + color: #ffee99; +} + +.hljs-string, +.hljs-bullet { + color: #b8cc52; +} + +.hljs-title, +.hljs-built_in, +.hljs-section { + color: #ffb454; +} + +.hljs-keyword, +.hljs-selector-tag, +.hljs-symbol { + color: #ff7733; +} + +.hljs-name { + color: #36a3d9; +} + +.hljs-tag { + color: #00568d; +} + +.hljs-emphasis { + font-style: italic; +} + +.hljs-strong { + font-weight: bold; +} + +.hljs-addition { + color: #91b362; +} + +.hljs-deletion { + color: #d96c75; +} diff --git a/book.js b/book.js new file mode 100644 index 0000000..5e38636 --- /dev/null +++ b/book.js @@ -0,0 +1,660 @@ +"use strict"; + +// Fix back button cache problem +window.onunload = function () { }; + +// Global variable, shared between modules +function playground_text(playground) { + let code_block = playground.querySelector("code"); + + if (window.ace && code_block.classList.contains("editable")) { + let editor = window.ace.edit(code_block); + return editor.getValue(); + } else { + return code_block.textContent; + } +} + +(function codeSnippets() { + function fetch_with_timeout(url, options, timeout = 6000) { + return Promise.race([ + fetch(url, options), + new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), timeout)) + ]); + } + + var playgrounds = Array.from(document.querySelectorAll(".playground")); + if (playgrounds.length > 0) { + fetch_with_timeout("https://play.rust-lang.org/meta/crates", { + headers: { + 'Content-Type': "application/json", + }, + method: 'POST', + mode: 'cors', + }) + .then(response => response.json()) + .then(response => { + // get list of crates available in the rust playground + let playground_crates = response.crates.map(item => item["id"]); + playgrounds.forEach(block => handle_crate_list_update(block, playground_crates)); + }); + } + + function handle_crate_list_update(playground_block, playground_crates) { + // update the play buttons after receiving the response + update_play_button(playground_block, playground_crates); + + // and install on change listener to dynamically update ACE editors + if (window.ace) { + let code_block = playground_block.querySelector("code"); + if (code_block.classList.contains("editable")) { + let editor = window.ace.edit(code_block); + editor.addEventListener("change", function (e) { + update_play_button(playground_block, playground_crates); + }); + // add Ctrl-Enter command to execute rust code + editor.commands.addCommand({ + name: "run", + bindKey: { + win: "Ctrl-Enter", + mac: "Ctrl-Enter" + }, + exec: _editor => run_rust_code(playground_block) + }); + } + } + } + + // updates the visibility of play button based on `no_run` class and + // used crates vs ones available on http://play.rust-lang.org + function update_play_button(pre_block, playground_crates) { + var play_button = pre_block.querySelector(".play-button"); + + // skip if code is `no_run` + if (pre_block.querySelector('code').classList.contains("no_run")) { + play_button.classList.add("hidden"); + return; + } + + // get list of `extern crate`'s from snippet + var txt = playground_text(pre_block); + var re = /extern\s+crate\s+([a-zA-Z_0-9]+)\s*;/g; + var snippet_crates = []; + var item; + while (item = re.exec(txt)) { + snippet_crates.push(item[1]); + } + + // check if all used crates are available on play.rust-lang.org + var all_available = snippet_crates.every(function (elem) { + return playground_crates.indexOf(elem) > -1; + }); + + if (all_available) { + play_button.classList.remove("hidden"); + } else { + play_button.classList.add("hidden"); + } + } + + function run_rust_code(code_block) { + var result_block = code_block.querySelector(".result"); + if (!result_block) { + result_block = document.createElement('code'); + result_block.className = 'result hljs language-bash'; + + code_block.append(result_block); + } + + let text = playground_text(code_block); + let classes = code_block.querySelector('code').classList; + let has_2018 = classes.contains("edition2018"); + let edition = has_2018 ? "2018" : "2015"; + + var params = { + version: "stable", + optimize: "0", + code: text, + edition: edition + }; + + if (text.indexOf("#![feature") !== -1) { + params.version = "nightly"; + } + + result_block.innerText = "Running..."; + + fetch_with_timeout("https://play.rust-lang.org/evaluate.json", { + headers: { + 'Content-Type': "application/json", + }, + method: 'POST', + mode: 'cors', + body: JSON.stringify(params) + }) + .then(response => response.json()) + .then(response => result_block.innerText = response.result) + .catch(error => result_block.innerText = "Playground Communication: " + error.message); + } + + // Syntax highlighting Configuration + hljs.configure({ + tabReplace: ' ', // 4 spaces + languages: [], // Languages used for auto-detection + }); + + let code_nodes = Array + .from(document.querySelectorAll('code')) + // Don't highlight `inline code` blocks in headers. + .filter(function (node) {return !node.parentElement.classList.contains("header"); }); + + if (window.ace) { + // language-rust class needs to be removed for editable + // blocks or highlightjs will capture events + Array + .from(document.querySelectorAll('code.editable')) + .forEach(function (block) { block.classList.remove('language-rust'); }); + + Array + .from(document.querySelectorAll('code:not(.editable)')) + .forEach(function (block) { hljs.highlightBlock(block); }); + } else { + code_nodes.forEach(function (block) { hljs.highlightBlock(block); }); + } + + // Adding the hljs class gives code blocks the color css + // even if highlighting doesn't apply + code_nodes.forEach(function (block) { block.classList.add('hljs'); }); + + Array.from(document.querySelectorAll("code.language-rust")).forEach(function (block) { + + var lines = Array.from(block.querySelectorAll('.boring')); + // If no lines were hidden, return + if (!lines.length) { return; } + block.classList.add("hide-boring"); + + var buttons = document.createElement('div'); + buttons.className = 'buttons'; + buttons.innerHTML = ""; + + // add expand button + var pre_block = block.parentNode; + pre_block.insertBefore(buttons, pre_block.firstChild); + + pre_block.querySelector('.buttons').addEventListener('click', function (e) { + if (e.target.classList.contains('fa-eye')) { + e.target.classList.remove('fa-eye'); + e.target.classList.add('fa-eye-slash'); + e.target.title = 'Hide lines'; + e.target.setAttribute('aria-label', e.target.title); + + block.classList.remove('hide-boring'); + } else if (e.target.classList.contains('fa-eye-slash')) { + e.target.classList.remove('fa-eye-slash'); + e.target.classList.add('fa-eye'); + e.target.title = 'Show hidden lines'; + e.target.setAttribute('aria-label', e.target.title); + + block.classList.add('hide-boring'); + } + }); + }); + + if (window.playground_copyable) { + Array.from(document.querySelectorAll('pre code')).forEach(function (block) { + var pre_block = block.parentNode; + if (!pre_block.classList.contains('playground')) { + var buttons = pre_block.querySelector(".buttons"); + if (!buttons) { + buttons = document.createElement('div'); + buttons.className = 'buttons'; + pre_block.insertBefore(buttons, pre_block.firstChild); + } + + var clipButton = document.createElement('button'); + clipButton.className = 'fa fa-copy clip-button'; + clipButton.title = 'Copy to clipboard'; + clipButton.setAttribute('aria-label', clipButton.title); + clipButton.innerHTML = ''; + + buttons.insertBefore(clipButton, buttons.firstChild); + } + }); + } + + // Process playground code blocks + Array.from(document.querySelectorAll(".playground")).forEach(function (pre_block) { + // Add play button + var buttons = pre_block.querySelector(".buttons"); + if (!buttons) { + buttons = document.createElement('div'); + buttons.className = 'buttons'; + pre_block.insertBefore(buttons, pre_block.firstChild); + } + + var runCodeButton = document.createElement('button'); + runCodeButton.className = 'fa fa-play play-button'; + runCodeButton.hidden = true; + runCodeButton.title = 'Run this code'; + runCodeButton.setAttribute('aria-label', runCodeButton.title); + + buttons.insertBefore(runCodeButton, buttons.firstChild); + runCodeButton.addEventListener('click', function (e) { + run_rust_code(pre_block); + }); + + if (window.playground_copyable) { + var copyCodeClipboardButton = document.createElement('button'); + copyCodeClipboardButton.className = 'fa fa-copy clip-button'; + copyCodeClipboardButton.innerHTML = ''; + copyCodeClipboardButton.title = 'Copy to clipboard'; + copyCodeClipboardButton.setAttribute('aria-label', copyCodeClipboardButton.title); + + buttons.insertBefore(copyCodeClipboardButton, buttons.firstChild); + } + + let code_block = pre_block.querySelector("code"); + if (window.ace && code_block.classList.contains("editable")) { + var undoChangesButton = document.createElement('button'); + undoChangesButton.className = 'fa fa-history reset-button'; + undoChangesButton.title = 'Undo changes'; + undoChangesButton.setAttribute('aria-label', undoChangesButton.title); + + buttons.insertBefore(undoChangesButton, buttons.firstChild); + + undoChangesButton.addEventListener('click', function () { + let editor = window.ace.edit(code_block); + editor.setValue(editor.originalCode); + editor.clearSelection(); + }); + } + }); +})(); + +(function themes() { + var html = document.querySelector('html'); + var themeToggleButton = document.getElementById('theme-toggle'); + var themePopup = document.getElementById('theme-list'); + var themeColorMetaTag = document.querySelector('meta[name="theme-color"]'); + var stylesheets = { + ayuHighlight: document.querySelector("[href$='ayu-highlight.css']"), + tomorrowNight: document.querySelector("[href$='tomorrow-night.css']"), + highlight: document.querySelector("[href$='highlight.css']"), + }; + + function showThemes() { + themePopup.style.display = 'block'; + themeToggleButton.setAttribute('aria-expanded', true); + themePopup.querySelector("button#" + get_theme()).focus(); + } + + function hideThemes() { + themePopup.style.display = 'none'; + themeToggleButton.setAttribute('aria-expanded', false); + themeToggleButton.focus(); + } + + function get_theme() { + var theme; + try { theme = localStorage.getItem('mdbook-theme'); } catch (e) { } + if (theme === null || theme === undefined) { + return default_theme; + } else { + return theme; + } + } + + function set_theme(theme, store = true) { + let ace_theme; + + if (theme == 'coal' || theme == 'navy') { + stylesheets.ayuHighlight.disabled = true; + stylesheets.tomorrowNight.disabled = false; + stylesheets.highlight.disabled = true; + + ace_theme = "ace/theme/tomorrow_night"; + } else if (theme == 'ayu') { + stylesheets.ayuHighlight.disabled = false; + stylesheets.tomorrowNight.disabled = true; + stylesheets.highlight.disabled = true; + ace_theme = "ace/theme/tomorrow_night"; + } else { + stylesheets.ayuHighlight.disabled = true; + stylesheets.tomorrowNight.disabled = true; + stylesheets.highlight.disabled = false; + ace_theme = "ace/theme/dawn"; + } + + setTimeout(function () { + themeColorMetaTag.content = getComputedStyle(document.body).backgroundColor; + }, 1); + + if (window.ace && window.editors) { + window.editors.forEach(function (editor) { + editor.setTheme(ace_theme); + }); + } + + var previousTheme = get_theme(); + + if (store) { + try { localStorage.setItem('mdbook-theme', theme); } catch (e) { } + } + + html.classList.remove(previousTheme); + html.classList.add(theme); + } + + // Set theme + var theme = get_theme(); + + set_theme(theme, false); + + themeToggleButton.addEventListener('click', function () { + if (themePopup.style.display === 'block') { + hideThemes(); + } else { + showThemes(); + } + }); + + themePopup.addEventListener('click', function (e) { + var theme = e.target.id || e.target.parentElement.id; + set_theme(theme); + }); + + themePopup.addEventListener('focusout', function(e) { + // e.relatedTarget is null in Safari and Firefox on macOS (see workaround below) + if (!!e.relatedTarget && !themeToggleButton.contains(e.relatedTarget) && !themePopup.contains(e.relatedTarget)) { + hideThemes(); + } + }); + + // Should not be needed, but it works around an issue on macOS & iOS: https://github.com/rust-lang/mdBook/issues/628 + document.addEventListener('click', function(e) { + if (themePopup.style.display === 'block' && !themeToggleButton.contains(e.target) && !themePopup.contains(e.target)) { + hideThemes(); + } + }); + + document.addEventListener('keydown', function (e) { + if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) { return; } + if (!themePopup.contains(e.target)) { return; } + + switch (e.key) { + case 'Escape': + e.preventDefault(); + hideThemes(); + break; + case 'ArrowUp': + e.preventDefault(); + var li = document.activeElement.parentElement; + if (li && li.previousElementSibling) { + li.previousElementSibling.querySelector('button').focus(); + } + break; + case 'ArrowDown': + e.preventDefault(); + var li = document.activeElement.parentElement; + if (li && li.nextElementSibling) { + li.nextElementSibling.querySelector('button').focus(); + } + break; + case 'Home': + e.preventDefault(); + themePopup.querySelector('li:first-child button').focus(); + break; + case 'End': + e.preventDefault(); + themePopup.querySelector('li:last-child button').focus(); + break; + } + }); +})(); + +(function sidebar() { + var html = document.querySelector("html"); + var sidebar = document.getElementById("sidebar"); + var sidebarLinks = document.querySelectorAll('#sidebar a'); + var sidebarToggleButton = document.getElementById("sidebar-toggle"); + var sidebarResizeHandle = document.getElementById("sidebar-resize-handle"); + var firstContact = null; + + function showSidebar() { + html.classList.remove('sidebar-hidden') + html.classList.add('sidebar-visible'); + Array.from(sidebarLinks).forEach(function (link) { + link.setAttribute('tabIndex', 0); + }); + sidebarToggleButton.setAttribute('aria-expanded', true); + sidebar.setAttribute('aria-hidden', false); + try { localStorage.setItem('mdbook-sidebar', 'visible'); } catch (e) { } + } + + + var sidebarAnchorToggles = document.querySelectorAll('#sidebar a.toggle'); + + function toggleSection(ev) { + ev.currentTarget.parentElement.classList.toggle('expanded'); + } + + Array.from(sidebarAnchorToggles).forEach(function (el) { + el.addEventListener('click', toggleSection); + }); + + function hideSidebar() { + html.classList.remove('sidebar-visible') + html.classList.add('sidebar-hidden'); + Array.from(sidebarLinks).forEach(function (link) { + link.setAttribute('tabIndex', -1); + }); + sidebarToggleButton.setAttribute('aria-expanded', false); + sidebar.setAttribute('aria-hidden', true); + try { localStorage.setItem('mdbook-sidebar', 'hidden'); } catch (e) { } + } + + // Toggle sidebar + sidebarToggleButton.addEventListener('click', function sidebarToggle() { + if (html.classList.contains("sidebar-hidden")) { + var current_width = parseInt( + document.documentElement.style.getPropertyValue('--sidebar-width'), 10); + if (current_width < 150) { + document.documentElement.style.setProperty('--sidebar-width', '150px'); + } + showSidebar(); + } else if (html.classList.contains("sidebar-visible")) { + hideSidebar(); + } else { + if (getComputedStyle(sidebar)['transform'] === 'none') { + hideSidebar(); + } else { + showSidebar(); + } + } + }); + + sidebarResizeHandle.addEventListener('mousedown', initResize, false); + + function initResize(e) { + window.addEventListener('mousemove', resize, false); + window.addEventListener('mouseup', stopResize, false); + html.classList.add('sidebar-resizing'); + } + function resize(e) { + var pos = (e.clientX - sidebar.offsetLeft); + if (pos < 20) { + hideSidebar(); + } else { + if (html.classList.contains("sidebar-hidden")) { + showSidebar(); + } + pos = Math.min(pos, window.innerWidth - 100); + document.documentElement.style.setProperty('--sidebar-width', pos + 'px'); + } + } + //on mouseup remove windows functions mousemove & mouseup + function stopResize(e) { + html.classList.remove('sidebar-resizing'); + window.removeEventListener('mousemove', resize, false); + window.removeEventListener('mouseup', stopResize, false); + } + + document.addEventListener('touchstart', function (e) { + firstContact = { + x: e.touches[0].clientX, + time: Date.now() + }; + }, { passive: true }); + + document.addEventListener('touchmove', function (e) { + if (!firstContact) + return; + + var curX = e.touches[0].clientX; + var xDiff = curX - firstContact.x, + tDiff = Date.now() - firstContact.time; + + if (tDiff < 250 && Math.abs(xDiff) >= 150) { + if (xDiff >= 0 && firstContact.x < Math.min(document.body.clientWidth * 0.25, 300)) + showSidebar(); + else if (xDiff < 0 && curX < 300) + hideSidebar(); + + firstContact = null; + } + }, { passive: true }); + + // Scroll sidebar to current active section + var activeSection = document.getElementById("sidebar").querySelector(".active"); + if (activeSection) { + // https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView + activeSection.scrollIntoView({ block: 'center' }); + } +})(); + +(function chapterNavigation() { + document.addEventListener('keydown', function (e) { + if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) { return; } + if (window.search && window.search.hasFocus()) { return; } + + switch (e.key) { + case 'ArrowRight': + e.preventDefault(); + var nextButton = document.querySelector('.nav-chapters.next'); + if (nextButton) { + window.location.href = nextButton.href; + } + break; + case 'ArrowLeft': + e.preventDefault(); + var previousButton = document.querySelector('.nav-chapters.previous'); + if (previousButton) { + window.location.href = previousButton.href; + } + break; + } + }); +})(); + +(function clipboard() { + var clipButtons = document.querySelectorAll('.clip-button'); + + function hideTooltip(elem) { + elem.firstChild.innerText = ""; + elem.className = 'fa fa-copy clip-button'; + } + + function showTooltip(elem, msg) { + elem.firstChild.innerText = msg; + elem.className = 'fa fa-copy tooltipped'; + } + + var clipboardSnippets = new ClipboardJS('.clip-button', { + text: function (trigger) { + hideTooltip(trigger); + let playground = trigger.closest("pre"); + return playground_text(playground); + } + }); + + Array.from(clipButtons).forEach(function (clipButton) { + clipButton.addEventListener('mouseout', function (e) { + hideTooltip(e.currentTarget); + }); + }); + + clipboardSnippets.on('success', function (e) { + e.clearSelection(); + showTooltip(e.trigger, "Copied!"); + }); + + clipboardSnippets.on('error', function (e) { + showTooltip(e.trigger, "Clipboard error!"); + }); +})(); + +(function scrollToTop () { + var menuTitle = document.querySelector('.menu-title'); + + menuTitle.addEventListener('click', function () { + document.scrollingElement.scrollTo({ top: 0, behavior: 'smooth' }); + }); +})(); + +(function controllMenu() { + var menu = document.getElementById('menu-bar'); + + (function controllPosition() { + var scrollTop = document.scrollingElement.scrollTop; + var prevScrollTop = scrollTop; + var minMenuY = -menu.clientHeight - 50; + // When the script loads, the page can be at any scroll (e.g. if you reforesh it). + menu.style.top = scrollTop + 'px'; + // Same as parseInt(menu.style.top.slice(0, -2), but faster + var topCache = menu.style.top.slice(0, -2); + menu.classList.remove('sticky'); + var stickyCache = false; // Same as menu.classList.contains('sticky'), but faster + document.addEventListener('scroll', function () { + scrollTop = Math.max(document.scrollingElement.scrollTop, 0); + // `null` means that it doesn't need to be updated + var nextSticky = null; + var nextTop = null; + var scrollDown = scrollTop > prevScrollTop; + var menuPosAbsoluteY = topCache - scrollTop; + if (scrollDown) { + nextSticky = false; + if (menuPosAbsoluteY > 0) { + nextTop = prevScrollTop; + } + } else { + if (menuPosAbsoluteY > 0) { + nextSticky = true; + } else if (menuPosAbsoluteY < minMenuY) { + nextTop = prevScrollTop + minMenuY; + } + } + if (nextSticky === true && stickyCache === false) { + menu.classList.add('sticky'); + stickyCache = true; + } else if (nextSticky === false && stickyCache === true) { + menu.classList.remove('sticky'); + stickyCache = false; + } + if (nextTop !== null) { + menu.style.top = nextTop + 'px'; + topCache = nextTop; + } + prevScrollTop = scrollTop; + }, { passive: true }); + })(); + (function controllBorder() { + menu.classList.remove('bordered'); + document.addEventListener('scroll', function () { + if (menu.offsetTop === 0) { + menu.classList.remove('bordered'); + } else { + menu.classList.add('bordered'); + } + }, { passive: true }); + })(); +})(); diff --git a/book.toml b/book.toml new file mode 100644 index 0000000..e36aa55 --- /dev/null +++ b/book.toml @@ -0,0 +1,20 @@ +# https://giscus.app +# https://github.com/badboy/mdbook-mermaid + +[book] +title = "Go语言圣经" +authors = ["译者:", "chai2010", "Xargin", "CrazySssst", "foreversmart"] +description = "中文版" +language = "zh" +multilingual = false +src = "." + +[build] +build-dir = "book" + +[output.html] +additional-css = ["style.css"] +additional-js = ["js/custom.js", "js/bigPicture.js"] +git-repository-url = "https://github.com/gopl-zh/gopl-zh.github.com" +edit-url-template = "https://github.com/gopl-zh/gopl-zh.github.com/edit/master/{path}" +git-repository-icon = "fa-github" diff --git a/builder.go b/builder.go new file mode 100644 index 0000000..8cd754d --- /dev/null +++ b/builder.go @@ -0,0 +1,172 @@ +// Copyright 2015 ChaiShushan . All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build ingore + +// 打包 gopl-zh 为 zip 文件. +// +// 文件名格式: gopl-zh-20151001-2ae607.zip +package main + +import ( + "archive/zip" + "fmt" + "io" + "io/ioutil" + "log" + "os" + "os/exec" + "path/filepath" + "strings" + "time" +) + +func main() { + // Git版本号 + gitVersion := getGitCommitVersion() + + // zip文件名 + zipBaseName := fmt.Sprintf("gopl-zh-%s-%s", time.Now().Format("20060102"), gitVersion[:6]) + + // 导出Git + exportGitToZip("./_book/" + "gopl-zh-" + gitVersion + ".zip") + + os.Remove(zipBaseName + ".zip") + file, err := os.Create(zipBaseName + ".zip") + if err != nil { + log.Fatal("os.Create: ", err) + } + defer file.Close() + + zipFile := zip.NewWriter(file) + defer zipFile.Close() + + // create /gopl-zh-20151001-2ae607/ + f, err := zipFile.Create(zipBaseName + "/") + if err != nil { + log.Fatal(err) + } + if _, err = f.Write([]byte("")); err != nil { + log.Fatal(err) + } + + dir := "_book" + filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { + if err != nil { + log.Fatal("filepath.Walk: ", err) + } + if info.IsDir() { + return nil + } + relpath, err := filepath.Rel(dir, path) + if err != nil { + log.Fatal("filepath.Rel: ", err) + } + + filename := filepath.ToSlash(relpath) + if isIngoreFile(filename) { + return nil + } + + data, err := ioutil.ReadFile(path) + if err != nil { + log.Fatal("ioutil.ReadFile: ", err) + } + + f, err := zipFile.Create(zipBaseName + "/" + filename) + if err != nil { + log.Fatal(err) + } + if _, err = f.Write(data); err != nil { + log.Fatal(err) + } + + fmt.Printf("%s\n", filename) + return nil + }) + + fmt.Printf("Done\n") +} + +// 获取Git最新的版本号 +// +// git log -1 +// commit 0460c1b3bb8fbb7e2fc88961e69aa37f4041d6c1 +// Merge: b2d582a e826457 +// Author: chai2010 +// Date: Mon Feb 1 08:04:44 2016 +0800 +// +// Merge pull request #249 from sunclx/patch-3 +// +// fix typo +func getGitCommitVersion() (version string) { + cmdOut, err := exec.Command(`git`, `log`, `-1`).CombinedOutput() + if err != nil { + return "unknown" + } + for _, line := range strings.Split(string(cmdOut), "\n") { + line := strings.TrimSpace(line) + if strings.HasPrefix(line, "commit") { + version = strings.TrimSpace(line[len("commit"):]) + return + } + } + return "unknown" +} + +// 导出Git到Zip文件 +func exportGitToZip(filename string) error { + if !strings.HasSuffix(filename, ".zip") { + filename += ".zip" + } + if _, err := exec.Command(`git`, `archive`, `--format`, `zip`, `--output`, filename, `master`).CombinedOutput(); err != nil { + return err + } + return nil +} + +func cpFile(dst, src string) { + err := os.MkdirAll(filepath.Dir(dst), 0666) + if err != nil && !os.IsExist(err) { + log.Fatal("cpFile: ", err) + } + fsrc, err := os.Open(src) + if err != nil { + log.Fatal("cpFile: ", err) + } + defer fsrc.Close() + + fdst, err := os.Create(dst) + if err != nil { + log.Fatal("cpFile: ", err) + } + defer fdst.Close() + if _, err = io.Copy(fdst, fsrc); err != nil { + log.Fatal("cpFile: ", err) + } +} + +func isIngoreFile(path string) bool { + if strings.HasPrefix(path, ".git") { + return true + } + if strings.HasSuffix(path, ".gitignore") { + return true + } + + if strings.HasPrefix(path, "vendor") { + return true + } + if strings.HasPrefix(path, "tools") { + return true + } + if strings.HasPrefix(path, "testdata") { + return true + } + if strings.HasSuffix(path, ".go") { + return true + } + + return false +} diff --git a/cch.png b/cch.png new file mode 100644 index 0000000..d5f83e9 Binary files /dev/null and b/cch.png differ diff --git a/ch1/ch1-01.html b/ch1/ch1-01.html new file mode 100644 index 0000000..e1f7e87 --- /dev/null +++ b/ch1/ch1-01.html @@ -0,0 +1,279 @@ + + + + + + Hello, World - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

1.1. Hello, World

+

我们以现已成为传统的“hello world”案例来开始吧,这个例子首次出现于1978年出版的C语言圣经《The C Programming Language》(译注:本书作者之一Brian W. Kernighan也是《The C Programming Language》一书的作者)。C语言是直接影响Go语言设计的语言之一。这个例子体现了Go语言一些核心理念。

+

gopl.io/ch1/helloworld

+
package main
+
+import "fmt"
+
+func main() {
+	fmt.Println("Hello, 世界")
+}
+
+

Go是一门编译型语言,Go语言的工具链将源代码及其依赖转换成计算机的机器指令(译注:静态编译)。Go语言提供的工具都通过一个单独的命令go调用,go命令有一系列子命令。最简单的一个子命令就是run。这个命令编译一个或多个以.go结尾的源文件,链接库文件,并运行最终生成的可执行文件。(本书使用$表示命令行提示符。)

+
$ go run helloworld.go
+
+

毫无意外,这个命令会输出:

+
Hello, 世界
+
+

Go语言原生支持Unicode,它可以处理全世界任何语言的文本。

+

如果不只是一次性实验,你肯定希望能够编译这个程序,保存编译结果以备将来之用。可以用build子命令:

+
$ go build helloworld.go
+
+

这个命令生成一个名为helloworld的可执行的二进制文件(译注:Windows系统下生成的可执行文件是helloworld.exe,增加了.exe后缀名),之后你可以随时运行它(译注:在Windows系统下在命令行直接输入helloworld.exe命令运行),不需任何处理(译注:因为静态编译,所以不用担心在系统库更新的时候冲突,幸福感满满)。

+
$ ./helloworld
+Hello, 世界
+
+

本书中所有示例代码上都有一行标记,利用这些标记可以从gopl.io网站上本书源码仓库里获取代码:

+
gopl.io/ch1/helloworld
+
+

执行 go get gopl.io/ch1/helloworld 命令,就会从网上获取代码,并放到对应目录中(需要先安装Git或Hg之类的版本管理工具,并将对应的命令添加到PATH环境变量中。序言已经提及,需要先设置好GOPATH环境变量,下载的代码会放在$GOPATH/src/gopl.io/ch1/helloworld目录)。2.6和10.7节有这方面更详细的介绍。

+

来讨论下程序本身。Go语言的代码通过(package)组织,包类似于其它语言里的库(libraries)或者模块(modules)。一个包由位于单个目录下的一个或多个.go源代码文件组成,目录定义包的作用。每个源文件都以一条package声明语句开始,这个例子里就是package main,表示该文件属于哪个包,紧跟着一系列导入(import)的包,之后是存储在这个文件里的程序语句。

+

Go的标准库提供了100多个包,以支持常见功能,如输入、输出、排序以及文本处理。比如fmt包,就含有格式化输出、接收输入的函数。Println是其中一个基础函数,可以打印以空格间隔的一个或多个值,并在最后添加一个换行符,从而输出一整行。

+

main包比较特殊。它定义了一个独立可执行的程序,而不是一个库。在main里的main 函数 也很特殊,它是整个程序执行时的入口(译注:C系语言差不多都这样)。main函数所做的事情就是程序做的。当然了,main函数一般调用其它包里的函数完成很多工作(如:fmt.Println)。

+

必须告诉编译器源文件需要哪些包,这就是跟随在package声明后面的import声明扮演的角色。hello world例子只用到了一个包,大多数程序需要导入多个包。

+

必须恰当导入需要的包,缺少了必要的包或者导入了不需要的包,程序都无法编译通过。这项严格要求避免了程序开发过程中引入未使用的包(译注:Go语言编译过程没有警告信息,争议特性之一)。

+

import声明必须跟在文件的package声明之后。随后,则是组成程序的函数、变量、常量、类型的声明语句(分别由关键字funcvarconsttype定义)。这些内容的声明顺序并不重要(译注:最好还是定一下规范)。这个例子的程序已经尽可能短了,只声明了一个函数,其中只调用了一个其他函数。为了节省篇幅,有些时候示例程序会省略packageimport声明,但是,这些声明在源代码里有,并且必须得有才能编译。

+

一个函数的声明由func关键字、函数名、参数列表、返回值列表(这个例子里的main函数参数列表和返回值都是空的)以及包含在大括号里的函数体组成。第五章进一步考察函数。

+

Go语言不需要在语句或者声明的末尾添加分号,除非一行上有多条语句。实际上,编译器会主动把特定符号后的换行符转换为分号,因此换行符添加的位置会影响Go代码的正确解析(译注:比如行末是标识符、整数、浮点数、虚数、字符或字符串文字、关键字breakcontinuefallthroughreturn中的一个、运算符和分隔符++--)]}中的一个)。举个例子,函数的左括号{必须和func函数声明在同一行上,且位于末尾,不能独占一行,而在表达式x + y中,可在+后换行,不能在+前换行(译注:以+结尾的话不会被插入分号分隔符,但是以x结尾的话则会被分号分隔符,从而导致编译错误)。

+

Go语言在代码格式上采取了很强硬的态度。gofmt工具把代码格式化为标准格式(译注:这个格式化工具没有任何可以调整代码格式的参数,Go语言就是这么任性),并且go工具中的fmt子命令会对指定包,否则默认为当前目录中所有.go源文件应用gofmt命令。本书中的所有代码都被gofmt过。你也应该养成格式化自己的代码的习惯。以法令方式规定标准的代码格式可以避免无尽的无意义的琐碎争执(译注:也导致了Go语言的TIOBE排名较低,因为缺少撕逼的话题)。更重要的是,这样可以做多种自动源码转换,如果放任Go语言代码格式,这些转换就不大可能了。

+

很多文本编辑器都可以配置为保存文件时自动执行gofmt,这样你的源代码总会被恰当地格式化。还有个相关的工具,goimports,可以根据代码需要,自动地添加或删除import声明。这个工具并没有包含在标准的分发包中,可以用下面的命令安装:

+
$ go get golang.org/x/tools/cmd/goimports
+
+

对于大多数用户来说,下载、编译包、运行测试用例、察看Go语言的文档等等常用功能都可以用go的工具完成。10.7节详细介绍这些知识。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch1/ch1-02.html b/ch1/ch1-02.html new file mode 100644 index 0000000..10e1b36 --- /dev/null +++ b/ch1/ch1-02.html @@ -0,0 +1,338 @@ + + + + + + 命令行参数 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

1.2. 命令行参数

+

大多数的程序都是处理输入,产生输出;这也正是“计算”的定义。但是,程序如何获取要处理的输入数据呢?一些程序生成自己的数据,但通常情况下,输入来自于程序外部:文件、网络连接、其它程序的输出、敲键盘的用户、命令行参数或其它类似输入源。下面几个例子会讨论其中几个输入源,首先是命令行参数。

+

os包以跨平台的方式,提供了一些与操作系统交互的函数和变量。程序的命令行参数可从os包的Args变量获取;os包外部使用os.Args访问该变量。

+

os.Args变量是一个字符串(string)的切片(slice)(译注:slice和Python语言中的切片类似,是一个简版的动态数组),切片是Go语言的基础概念,稍后详细介绍。现在先把切片s当作数组元素序列,序列的长度动态变化,用s[i]访问单个元素,用s[m:n]获取子序列(译注:和python里的语法差不多)。序列的元素数目为len(s)。和大多数编程语言类似,区间索引时,Go语言里也采用左闭右开形式,即,区间包括第一个索引元素,不包括最后一个,因为这样可以简化逻辑。(译注:比如a = [1, 2, 3, 4, 5], a[0:3] = [1, 2, 3],不包含最后一个元素)。比如s[m:n]这个切片,0 ≤ m ≤ n ≤ len(s),包含n-m个元素。

+

os.Args的第一个元素:os.Args[0],是命令本身的名字;其它的元素则是程序启动时传给它的参数。s[m:n]形式的切片表达式,产生从第m个元素到第n-1个元素的切片,下个例子用到的元素包含在os.Args[1:len(os.Args)]切片中。如果省略切片表达式的m或n,会默认传入0或len(s),因此前面的切片可以简写成os.Args[1:]。

+

下面是Unix里echo命令的一份实现,echo把它的命令行参数打印成一行。程序导入了两个包,用括号把它们括起来写成列表形式,而没有分开写成独立的import声明。两种形式都合法,列表形式习惯上用得多。包导入顺序并不重要;gofmt工具格式化时按照字母顺序对包名排序。(示例有多个版本时,我们会对示例编号,这样可以明确当前正在讨论的是哪个。)

+

gopl.io/ch1/echo1

+
// Echo1 prints its command-line arguments.
+package main
+
+import (
+	"fmt"
+	"os"
+)
+
+func main() {
+	var s, sep string
+	for i := 1; i < len(os.Args); i++ {
+		s += sep + os.Args[i]
+		sep = " "
+	}
+	fmt.Println(s)
+}
+
+

注释语句以//开头。对于程序员来说,//之后到行末之间所有的内容都是注释,被编译器忽略。按照惯例,我们在每个包的包声明前添加注释;对于main package,注释包含一句或几句话,从整体角度对程序做个描述。

+

var声明定义了两个string类型的变量s和sep。变量会在声明时直接初始化。如果变量没有显式初始化,则被隐式地赋予其类型的零值(zero value),数值类型是0,字符串类型是空字符串""。这个例子里,声明把s和sep隐式地初始化成空字符串。第2章再来详细地讲解变量和声明。

+

对数值类型,Go语言提供了常规的数值和逻辑运算符。而对string类型,+运算符连接字符串(译注:和C++或者js是一样的)。所以表达式:

+
sep + os.Args[i]
+
+

表示连接字符串sep和os.Args。程序中使用的语句:

+
s += sep + os.Args[i]
+
+

是一条赋值语句,将s的旧值跟sep与os.Args[i]连接后赋值回s,等价于:

+
s = s + sep + os.Args[i]
+
+

运算符+=是赋值运算符(assignment operator),每种数值运算符或逻辑运算符,如+*,都有对应的赋值运算符。

+

echo程序可以每循环一次输出一个参数,这个版本却是不断地把新文本追加到末尾来构造字符串。字符串s开始为空,即值为"",每次循环会添加一些文本;第一次迭代之后,还会再插入一个空格,因此循环结束时每个参数中间都有一个空格。这是一种二次加工(quadratic process),当参数数量庞大时,开销很大,但是对于echo,这种情形不大可能出现。本章会介绍echo的若干改进版,下一章解决低效问题。

+

循环索引变量i在for循环的第一部分中定义。符号:=短变量声明(short variable declaration)的一部分,这是定义一个或多个变量并根据它们的初始值为这些变量赋予适当类型的语句。下一章有这方面更多说明。

+

自增语句i++i加1;这和i += 1以及i = i + 1都是等价的。对应的还有i--i减1。它们是语句,而不像C系的其它语言那样是表达式。所以j = i++非法,而且++和--都只能放在变量名后面,因此--i也非法。

+

Go语言只有for循环这一种循环语句。for循环有多种形式,其中一种如下所示:

+
for initialization; condition; post {
+	// zero or more statements
+}
+
+

for循环三个部分不需括号包围。大括号强制要求,左大括号必须和post语句在同一行。

+

initialization语句是可选的,在循环开始前执行。initalization如果存在,必须是一条简单语句(simple statement),即,短变量声明、自增语句、赋值语句或函数调用。condition是一个布尔表达式(boolean expression),其值在每次循环迭代开始时计算。如果为true则执行循环体语句。post语句在循环体执行结束后执行,之后再次对condition求值。condition值为false时,循环结束。

+

for循环的这三个部分每个都可以省略,如果省略initializationpost,分号也可以省略:

+
// a traditional "while" loop
+for condition {
+	// ...
+}
+
+

如果连condition也省略了,像下面这样:

+
// a traditional infinite loop
+for {
+	// ...
+}
+
+

这就变成一个无限循环,尽管如此,还可以用其他方式终止循环,如一条breakreturn语句。

+

for循环的另一种形式,在某种数据类型的区间(range)上遍历,如字符串或切片。echo的第二版本展示了这种形式:

+

gopl.io/ch1/echo2

+
// Echo2 prints its command-line arguments.
+package main
+
+import (
+	"fmt"
+    "os"
+)
+
+func main() {
+	s, sep := "", ""
+	for _, arg := range os.Args[1:] {
+		s += sep + arg
+		sep = " "
+	}
+	fmt.Println(s)
+}
+
+

每次循环迭代,range产生一对值;索引以及在该索引处的元素值。这个例子不需要索引,但range的语法要求,要处理元素,必须处理索引。一种思路是把索引赋值给一个临时变量(如temp)然后忽略它的值,但Go语言不允许使用无用的局部变量(local variables),因为这会导致编译错误。

+

Go语言中这种情况的解决方法是用空标识符(blank identifier),即_(也就是下划线)。空标识符可用于在任何语法需要变量名但程序逻辑不需要的时候(如:在循环里)丢弃不需要的循环索引,并保留元素值。大多数的Go程序员都会像上面这样使用range_echo程序,因为隐式地而非显式地索引os.Args,容易写对。

+

echo的这个版本使用一条短变量声明来声明并初始化sseps,也可以将这两个变量分开声明,声明一个变量有好几种方式,下面这些都等价:

+
s := ""
+var s string
+var s = ""
+var s string = ""
+
+

用哪种不用哪种,为什么呢?第一种形式,是一条短变量声明,最简洁,但只能用在函数内部,而不能用于包变量。第二种形式依赖于字符串的默认初始化零值机制,被初始化为""。第三种形式用得很少,除非同时声明多个变量。第四种形式显式地标明变量的类型,当变量类型与初值类型相同时,类型冗余,但如果两者类型不同,变量类型就必须了。实践中一般使用前两种形式中的某个,初始值重要的话就显式地指定变量的类型,否则使用隐式初始化。

+

如前文所述,每次循环迭代字符串s的内容都会更新。+=连接原字符串、空格和下个参数,产生新字符串,并把它赋值给ss原来的内容已经不再使用,将在适当时机对它进行垃圾回收。

+

如果连接涉及的数据量很大,这种方式代价高昂。一种简单且高效的解决方案是使用strings包的Join函数:

+

gopl.io/ch1/echo3

+
func main() {
+	fmt.Println(strings.Join(os.Args[1:], " "))
+}
+
+

最后,如果不关心输出格式,只想看看输出值,或许只是为了调试,可以用Println为我们格式化输出。

+
fmt.Println(os.Args[1:])
+
+

这条语句的输出结果跟strings.Join得到的结果很像,只是被放到了一对方括号里。切片都会被打印成这种格式。

+

练习 1.1: 修改echo程序,使其能够打印os.Args[0],即被执行命令本身的名字。

+

练习 1.2: 修改echo程序,使其打印每个参数的索引和值,每个一行。

+

练习 1.3: 做实验测量潜在低效的版本和使用了strings.Join的版本的运行时间差异。(1.6节讲解了部分time包,11.4节展示了如何写标准测试程序,以得到系统性的性能评测。)

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch1/ch1-03.html b/ch1/ch1-03.html new file mode 100644 index 0000000..56d6e6e --- /dev/null +++ b/ch1/ch1-03.html @@ -0,0 +1,375 @@ + + + + + + 查找重复的行 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

1.3. 查找重复的行

+

对文件做拷贝、打印、搜索、排序、统计或类似事情的程序都有一个差不多的程序结构:一个处理输入的循环,在每个元素上执行计算处理,在处理的同时或最后产生输出。我们会展示一个名为dup的程序的三个版本;灵感来自于Unix的uniq命令,其寻找相邻的重复行。该程序使用的结构和包是个参考范例,可以方便地修改。

+

dup的第一个版本打印标准输入中多次出现的行,以重复次数开头。该程序将引入if语句,map数据类型以及bufio包。

+

gopl.io/ch1/dup1

+
// Dup1 prints the text of each line that appears more than
+// once in the standard input, preceded by its count.
+package main
+
+import (
+	"bufio"
+	"fmt"
+	"os"
+)
+
+func main() {
+	counts := make(map[string]int)
+	input := bufio.NewScanner(os.Stdin)
+	for input.Scan() {
+		counts[input.Text()]++
+	}
+	// NOTE: ignoring potential errors from input.Err()
+	for line, n := range counts {
+		if n > 1 {
+			fmt.Printf("%d\t%s\n", n, line)
+		}
+	}
+}
+
+

正如for循环一样,if语句条件两边也不加括号,但是主体部分需要加。if语句的else部分是可选的,在if的条件为false时执行。

+

map存储了键/值(key/value)的集合,对集合元素,提供常数时间的存、取或测试操作。键可以是任意类型,只要其值能用==运算符比较,最常见的例子是字符串;值则可以是任意类型。这个例子中的键是字符串,值是整数。内置函数make创建空map,此外,它还有别的作用。4.3节讨论map

+

(译注:从功能和实现上说,Gomap类似于Java语言中的HashMap,Python语言中的dictLua语言中的table,通常使用hash实现。遗憾的是,对于该词的翻译并不统一,数学界术语为映射,而计算机界众说纷纭莫衷一是。为了防止对读者造成误解,保留不译。)

+

每次dup读取一行输入,该行被当做键存入map,其对应的值递增。counts[input.Text()]++语句等价下面两句:

+
line := input.Text()
+counts[line] = counts[line] + 1
+
+

map中不含某个键时不用担心,首次读到新行时,等号右边的表达式counts[line]的值将被计算为其类型的零值,对于int即0。

+

为了打印结果,我们使用了基于range的循环,并在counts这个map上迭代。跟之前类似,每次迭代得到两个结果,键和其在map中对应的值。map的迭代顺序并不确定,从实践来看,该顺序随机,每次运行都会变化。这种设计是有意为之的,因为能防止程序依赖特定遍历顺序,而这是无法保证的。(译注:具体可以参见这里http://stackoverflow.com/questions/11853396/google-go-lang-assignment-order)

+

继续来看bufio包,它使处理输入和输出方便又高效。Scanner类型是该包最有用的特性之一,它读取输入并将其拆成行或单词;通常是处理行形式的输入最简单的方法。

+

程序使用短变量声明创建bufio.Scanner类型的变量input

+
input := bufio.NewScanner(os.Stdin)
+
+

该变量从程序的标准输入中读取内容。每次调用input.Scan(),即读入下一行,并移除行末的换行符;读取的内容可以调用input.Text()得到。Scan函数在读到一行时返回true,不再有输入时返回false

+

类似于C或其它语言里的printf函数,fmt.Printf函数对一些表达式产生格式化输出。该函数的首个参数是个格式字符串,指定后续参数被如何格式化。各个参数的格式取决于“转换字符”(conversion character),形式为百分号后跟一个字母。举个例子,%d表示以十进制形式打印一个整型操作数,而%s则表示把字符串型操作数的值展开。

+

Printf有一大堆这种转换,Go程序员称之为动词(verb)。下面的表格虽然远不是完整的规范,但展示了可用的很多特性:

+
%d          十进制整数
+%x, %o, %b  十六进制,八进制,二进制整数。
+%f, %g, %e  浮点数: 3.141593 3.141592653589793 3.141593e+00
+%t          布尔:true或false
+%c          字符(rune) (Unicode码点)
+%s          字符串
+%q          带双引号的字符串"abc"或带单引号的字符'c'
+%v          变量的自然形式(natural format)
+%T          变量的类型
+%%          字面上的百分号标志(无操作数)
+
+

dup1的格式字符串中还含有制表符\t和换行符\n。字符串字面上可能含有这些代表不可见字符的转义字符(escape sequences)。默认情况下,Printf不会换行。按照惯例,以字母f结尾的格式化函数,如log.Printffmt.Errorf,都采用fmt.Printf的格式化准则。而以ln结尾的格式化函数,则遵循Println的方式,以跟%v差不多的方式格式化参数,并在最后添加一个换行符。(译注:后缀fformatlnline。)

+

很多程序要么从标准输入中读取数据,如上面的例子所示,要么从一系列具名文件中读取数据。dup程序的下个版本读取标准输入或是使用os.Open打开各个具名文件,并操作它们。

+

gopl.io/ch1/dup2

+
// Dup2 prints the count and text of lines that appear more than once
+// in the input.  It reads from stdin or from a list of named files.
+package main
+
+import (
+	"bufio"
+	"fmt"
+	"os"
+)
+
+func main() {
+	counts := make(map[string]int)
+	files := os.Args[1:]
+	if len(files) == 0 {
+		countLines(os.Stdin, counts)
+	} else {
+		for _, arg := range files {
+			f, err := os.Open(arg)
+			if err != nil {
+				fmt.Fprintf(os.Stderr, "dup2: %v\n", err)
+				continue
+			}
+			countLines(f, counts)
+			f.Close()
+		}
+	}
+	for line, n := range counts {
+		if n > 1 {
+			fmt.Printf("%d\t%s\n", n, line)
+		}
+	}
+}
+
+func countLines(f *os.File, counts map[string]int) {
+	input := bufio.NewScanner(f)
+	for input.Scan() {
+		counts[input.Text()]++
+	}
+	// NOTE: ignoring potential errors from input.Err()
+}
+
+

os.Open函数返回两个值。第一个值是被打开的文件(*os.File),其后被Scanner读取。

+

os.Open返回的第二个值是内置error类型的值。如果err等于内置值nil(译注:相当于其它语言里的NULL),那么文件被成功打开。读取文件,直到文件结束,然后调用Close关闭该文件,并释放占用的所有资源。相反的话,如果err的值不是nil,说明打开文件时出错了。这种情况下,错误值描述了所遇到的问题。我们的错误处理非常简单,只是使用Fprintf与表示任意类型默认格式值的动词%v,向标准错误流打印一条信息,然后dup继续处理下一个文件;continue语句直接跳到for循环的下个迭代开始执行。

+

为了使示例代码保持合理的大小,本书开始的一些示例有意简化了错误处理,显而易见的是,应该检查os.Open返回的错误值,然而,使用input.Scan读取文件过程中,不大可能出现错误,因此我们忽略了错误处理。我们会在跳过错误检查的地方做说明。5.4节中深入介绍错误处理。

+

注意countLines函数在其声明前被调用。函数和包级别的变量(package-level entities)可以任意顺序声明,并不影响其被调用。(译注:最好还是遵循一定的规范)

+

map是一个由make函数创建的数据结构的引用。map作为参数传递给某函数时,该函数接收这个引用的一份拷贝(copy,或译为副本),被调用函数对map底层数据结构的任何修改,调用者函数都可以通过持有的map引用看到。在我们的例子中,countLines函数向counts插入的值,也会被main函数看到。(译注:类似于C++里的引用传递,实际上指针是另一个指针了,但内部存的值指向同一块内存)

+

dup的前两个版本以"流”模式读取输入,并根据需要拆分成多个行。理论上,这些程序可以处理任意数量的输入数据。还有另一个方法,就是一口气把全部输入数据读到内存中,一次分割为多行,然后处理它们。下面这个版本,dup3,就是这么操作的。这个例子引入了ReadFile函数(来自于io/ioutil包),其读取指定文件的全部内容,strings.Split函数把字符串分割成子串的切片。(Split的作用与前文提到的strings.Join相反。)

+

我们略微简化了dup3。首先,由于ReadFile函数需要文件名作为参数,因此只读指定文件,不读标准输入。其次,由于行计数代码只在一处用到,故将其移回main函数。

+

gopl.io/ch1/dup3

+
package main
+
+import (
+	"fmt"
+	"io/ioutil"
+	"os"
+	"strings"
+)
+
+func main() {
+	counts := make(map[string]int)
+	for _, filename := range os.Args[1:] {
+		data, err := ioutil.ReadFile(filename)
+		if err != nil {
+			fmt.Fprintf(os.Stderr, "dup3: %v\n", err)
+			continue
+		}
+		for _, line := range strings.Split(string(data), "\n") {
+			counts[line]++
+		}
+	}
+	for line, n := range counts {
+		if n > 1 {
+			fmt.Printf("%d\t%s\n", n, line)
+		}
+	}
+}
+
+

ReadFile函数返回一个字节切片(byte slice),必须把它转换为string,才能用strings.Split分割。我们会在3.5.4节详细讲解字符串和字节切片。

+

实现上,bufio.Scannerioutil.ReadFileioutil.WriteFile都使用*os.FileReadWrite方法,但是,大多数程序员很少需要直接调用那些低级(lower-level)函数。高级(higher-level)函数,像bufioio/ioutil包中所提供的那些,用起来要容易点。

+

练习 1.4: 修改dup2,出现重复的行时打印文件名称。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch1/ch1-04.html b/ch1/ch1-04.html new file mode 100644 index 0000000..9b6c429 --- /dev/null +++ b/ch1/ch1-04.html @@ -0,0 +1,314 @@ + + + + + + GIF动画 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

1.4. GIF动画

+

下面的程序会演示Go语言标准库里的image这个package的用法,我们会用这个包来生成一系列的bit-mapped图,然后将这些图片编码为一个GIF动画。我们生成的图形名字叫利萨如图形(Lissajous figures),这种效果是在1960年代的老电影里出现的一种视觉特效。它们是协振子在两个纬度上振动所产生的曲线,比如两个sin正弦波分别在x轴和y轴输入会产生的曲线。图1.1是这样的一个例子:

+

+

译注:要看这个程序的结果,需要将标准输出重定向到一个GIF图像文件(使用 ./lissajous > output.gif 命令)。下面是GIF图像动画效果:

+

+

这段代码里我们用了一些新的结构,包括const声明,struct结构体类型,复合声明。和我们举的其它的例子不太一样,这一个例子包含了浮点数运算。这些概念我们只在这里简单地说明一下,之后的章节会更详细地讲解。

+

gopl.io/ch1/lissajous

+
// Lissajous generates GIF animations of random Lissajous figures.
+package main
+
+import (
+	"image"
+	"image/color"
+	"image/gif"
+	"io"
+	"math"
+	"math/rand"
+	"os"
+	"time"
+)
+
+var palette = []color.Color{color.White, color.Black}
+
+const (
+	whiteIndex = 0 // first color in palette
+	blackIndex = 1 // next color in palette
+)
+
+func main() {
+	// The sequence of images is deterministic unless we seed
+	// the pseudo-random number generator using the current time.
+	// Thanks to Randall McPherson for pointing out the omission.
+	rand.Seed(time.Now().UTC().UnixNano())
+	lissajous(os.Stdout)
+}
+
+func lissajous(out io.Writer) {
+	const (
+		cycles  = 5     // number of complete x oscillator revolutions
+		res     = 0.001 // angular resolution
+		size    = 100   // image canvas covers [-size..+size]
+		nframes = 64    // number of animation frames
+		delay   = 8     // delay between frames in 10ms units
+	)
+
+	freq := rand.Float64() * 3.0 // relative frequency of y oscillator
+	anim := gif.GIF{LoopCount: nframes}
+	phase := 0.0 // phase difference
+	for i := 0; i < nframes; i++ {
+		rect := image.Rect(0, 0, 2*size+1, 2*size+1)
+		img := image.NewPaletted(rect, palette)
+		for t := 0.0; t < cycles*2*math.Pi; t += res {
+			x := math.Sin(t)
+			y := math.Sin(t*freq + phase)
+			img.SetColorIndex(size+int(x*size+0.5), size+int(y*size+0.5),
+				blackIndex)
+		}
+		phase += 0.1
+		anim.Delay = append(anim.Delay, delay)
+		anim.Image = append(anim.Image, img)
+	}
+	gif.EncodeAll(out, &anim) // NOTE: ignoring encoding errors
+}
+
+
+

当我们import了一个包路径包含有多个单词的package时,比如image/color(image和color两个单词),通常我们只需要用最后那个单词表示这个包就可以。所以当我们写color.White时,这个变量指向的是image/color包里的变量,同理gif.GIF是属于image/gif包里的变量。

+

这个程序里的常量声明给出了一系列的常量值,常量是指在程序编译后运行时始终都不会变化的值,比如圈数、帧数、延迟值。常量声明和变量声明一般都会出现在包级别,所以这些常量在整个包中都是可以共享的,或者你也可以把常量声明定义在函数体内部,那么这种常量就只能在函数体内用。目前常量声明的值必须是一个数字值、字符串或者一个固定的boolean值。

+

[]color.Color{...}和gif.GIF{...}这两个表达式就是我们说的复合声明(4.2和4.4.1节有说明)。这是实例化Go语言里的复合类型的一种写法。这里的前者生成的是一个slice切片,后者生成的是一个struct结构体。

+

gif.GIF是一个struct类型(参考4.4节)。struct是一组值或者叫字段的集合,不同的类型集合在一个struct可以让我们以一个统一的单元进行处理。anim是一个gif.GIF类型的struct变量。这种写法会生成一个struct变量,并且其内部变量LoopCount字段会被设置为nframes;而其它的字段会被设置为各自类型默认的零值。struct内部的变量可以以一个点(.)来进行访问,就像在最后两个赋值语句中显式地更新了anim这个struct的Delay和Image字段。

+

lissajous函数内部有两层嵌套的for循环。外层循环会循环64次,每一次都会生成一个单独的动画帧。它生成了一个包含两种颜色的201*201大小的图片,白色和黑色。所有像素点都会被默认设置为其零值(也就是调色板palette里的第0个值),这里我们设置的是白色。每次外层循环都会生成一张新图片,并将一些像素设置为黑色。其结果会append到之前结果之后。这里我们用到了append(参考4.2.1)内置函数,将结果append到anim中的帧列表末尾,并设置一个默认的80ms的延迟值。循环结束后所有的延迟值被编码进了GIF图片中,并将结果写入到输出流。out这个变量是io.Writer类型,这个类型支持把输出结果写到很多目标,很快我们就可以看到例子。

+

内层循环设置两个偏振值。x轴偏振使用sin函数。y轴偏振也是正弦波,但其相对x轴的偏振是一个0-3的随机值,初始偏振值是一个零值,随着动画的每一帧逐渐增加。循环会一直跑到x轴完成五次完整的循环。每一步它都会调用SetColorIndex来为(x,y)点来染黑色。

+

main函数调用lissajous函数,用它来向标准输出流打印信息,所以下面这个命令会像图1.1中产生一个GIF动画。

+
$ go build gopl.io/ch1/lissajous
+$ ./lissajous >out.gif
+
+

练习 1.5: 修改前面的Lissajous程序里的调色板,由黑色改为绿色。我们可以用color.RGBA{0xRR, 0xGG, 0xBB, 0xff}来得到#RRGGBB这个色值,三个十六进制的字符串分别代表红、绿、蓝像素。

+

练习 1.6: 修改Lissajous程序,修改其调色板来生成更丰富的颜色,然后修改SetColorIndex的第三个参数,看看显示结果吧。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch1/ch1-05.html b/ch1/ch1-05.html new file mode 100644 index 0000000..2272751 --- /dev/null +++ b/ch1/ch1-05.html @@ -0,0 +1,288 @@ + + + + + + 获取URL - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

1.5. 获取URL

+

对于很多现代应用来说,访问互联网上的信息和访问本地文件系统一样重要。Go语言在net这个强大package的帮助下提供了一系列的package来做这件事情,使用这些包可以更简单地用网络收发信息,还可以建立更底层的网络连接,编写服务器程序。在这些情景下,Go语言原生的并发特性(在第八章中会介绍)显得尤其好用。

+

为了最简单地展示基于HTTP获取信息的方式,下面给出一个示例程序fetch,这个程序将获取对应的url,并将其源文本打印出来;这个例子的灵感来源于curl工具(译注:unix下的一个用来发http请求的工具,具体可以man curl)。当然,curl提供的功能更为复杂丰富,这里只编写最简单的样例。这个样例之后还会多次被用到。

+

gopl.io/ch1/fetch

+
// Fetch prints the content found at a URL.
+package main
+
+import (
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"os"
+)
+
+func main() {
+	for _, url := range os.Args[1:] {
+		resp, err := http.Get(url)
+		if err != nil {
+			fmt.Fprintf(os.Stderr, "fetch: %v\n", err)
+			os.Exit(1)
+		}
+		b, err := ioutil.ReadAll(resp.Body)
+		resp.Body.Close()
+		if err != nil {
+			fmt.Fprintf(os.Stderr, "fetch: reading %s: %v\n", url, err)
+			os.Exit(1)
+		}
+		fmt.Printf("%s", b)
+	}
+}
+
+

这个程序从两个package中导入了函数,net/http和io/ioutil包,http.Get函数是创建HTTP请求的函数,如果获取过程没有出错,那么会在resp这个结构体中得到访问的请求结果。resp的Body字段包括一个可读的服务器响应流。ioutil.ReadAll函数从response中读取到全部内容;将其结果保存在变量b中。resp.Body.Close关闭resp的Body流,防止资源泄露,Printf函数会将结果b写出到标准输出流中。

+
$ go build gopl.io/ch1/fetch
+$ ./fetch http://gopl.io
+<html>
+<head>
+<title>The Go Programming Language</title>title>
+...
+
+

HTTP请求如果失败了的话,会得到下面这样的结果:

+
$ ./fetch http://bad.gopl.io
+fetch: Get http://bad.gopl.io: dial tcp: lookup bad.gopl.io: no such host
+
+

译注:在大天朝的网络环境下很容易重现这种错误,下面是Windows下运行得到的错误信息:

+
$ go run main.go http://gopl.io
+fetch: Get http://gopl.io: dial tcp: lookup gopl.io: getaddrinfow: No such host is known.
+
+

无论哪种失败原因,我们的程序都用了os.Exit函数来终止进程,并且返回一个status错误码,其值为1。

+

练习 1.7: 函数调用io.Copy(dst, src)会从src中读取内容,并将读到的结果写入到dst中,使用这个函数替代掉例子中的ioutil.ReadAll来拷贝响应结构体到os.Stdout,避免申请一个缓冲区(例子中的b)来存储。记得处理io.Copy返回结果中的错误。

+

练习 1.8: 修改fetch这个范例,如果输入的url参数没有 http:// 前缀的话,为这个url加上该前缀。你可能会用到strings.HasPrefix这个函数。

+

练习 1.9: 修改fetch打印出HTTP协议的状态码,可以从resp.Status变量得到该状态码。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch1/ch1-06.html b/ch1/ch1-06.html new file mode 100644 index 0000000..5b224bd --- /dev/null +++ b/ch1/ch1-06.html @@ -0,0 +1,295 @@ + + + + + + 并发获取多个URL - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

1.6. 并发获取多个URL

+

Go语言最有意思并且最新奇的特性就是对并发编程的支持。并发编程是一个大话题,在第八章和第九章中会专门讲到。这里我们只浅尝辄止地来体验一下Go语言里的goroutine和channel。

+

下面的例子fetchall,和前面小节的fetch程序所要做的工作基本一致,fetchall的特别之处在于它会同时去获取所有的URL,所以这个程序的总执行时间不会超过执行时间最长的那一个任务,前面的fetch程序执行时间则是所有任务执行时间之和。fetchall程序只会打印获取的内容大小和经过的时间,不会像之前那样打印获取的内容。

+

gopl.io/ch1/fetchall

+
// Fetchall fetches URLs in parallel and reports their times and sizes.
+package main
+
+import (
+	"fmt"
+	"io"
+	"io/ioutil"
+	"net/http"
+	"os"
+	"time"
+)
+
+func main() {
+	start := time.Now()
+	ch := make(chan string)
+	for _, url := range os.Args[1:] {
+		go fetch(url, ch) // start a goroutine
+	}
+	for range os.Args[1:] {
+		fmt.Println(<-ch) // receive from channel ch
+	}
+	fmt.Printf("%.2fs elapsed\n", time.Since(start).Seconds())
+}
+
+func fetch(url string, ch chan<- string) {
+	start := time.Now()
+	resp, err := http.Get(url)
+	if err != nil {
+		ch <- fmt.Sprint(err) // send to channel ch
+		return
+	}
+	nbytes, err := io.Copy(ioutil.Discard, resp.Body)
+	resp.Body.Close() // don't leak resources
+	if err != nil {
+		ch <- fmt.Sprintf("while reading %s: %v", url, err)
+		return
+	}
+	secs := time.Since(start).Seconds()
+	ch <- fmt.Sprintf("%.2fs  %7d  %s", secs, nbytes, url)
+}
+
+

下面使用fetchall来请求几个地址:

+
$ go build gopl.io/ch1/fetchall
+$ ./fetchall https://golang.org http://gopl.io https://godoc.org
+0.14s     6852  https://godoc.org
+0.16s     7261  https://golang.org
+0.48s     2475  http://gopl.io
+0.48s elapsed
+
+

goroutine是一种函数的并发执行方式,而channel是用来在goroutine之间进行参数传递。main函数本身也运行在一个goroutine中,而go function则表示创建一个新的goroutine,并在这个新的goroutine中执行这个函数。

+

main函数中用make函数创建了一个传递string类型参数的channel,对每一个命令行参数,我们都用go这个关键字来创建一个goroutine,并且让函数在这个goroutine异步执行http.Get方法。这个程序里的io.Copy会把响应的Body内容拷贝到ioutil.Discard输出流中(译注:可以把这个变量看作一个垃圾桶,可以向里面写一些不需要的数据),因为我们需要这个方法返回的字节数,但是又不想要其内容。每当请求返回内容时,fetch函数都会往ch这个channel里写入一个字符串,由main函数里的第二个for循环来处理并打印channel里的这个字符串。

+

当一个goroutine尝试在一个channel上做send或者receive操作时,这个goroutine会阻塞在调用处,直到另一个goroutine从这个channel里接收或者写入值,这样两个goroutine才会继续执行channel操作之后的逻辑。在这个例子中,每一个fetch函数在执行时都会往channel里发送一个值(ch <- expression),主函数负责接收这些值(<-ch)。这个程序中我们用main函数来完整地处理/接收所有fetch函数传回的字符串,可以避免因为有两个goroutine同时完成而使得其输出交错在一起的危险。

+

练习 1.10: 找一个数据量比较大的网站,用本小节中的程序调研网站的缓存策略,对每个URL执行两遍请求,查看两次时间是否有较大的差别,并且每次获取到的响应内容是否一致,修改本节中的程序,将响应结果输出到文件,以便于进行对比。

+

练习 1.11: 在fetchall中尝试使用长一些的参数列表,比如使用在alexa.com的上百万网站里排名靠前的。如果一个网站没有回应,程序将采取怎样的行为?(Section8.9 描述了在这种情况下的应对机制)。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch1/ch1-07.html b/ch1/ch1-07.html new file mode 100644 index 0000000..e888a34 --- /dev/null +++ b/ch1/ch1-07.html @@ -0,0 +1,362 @@ + + + + + + Web服务 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

1.7. Web服务

+

Go语言的内置库使得写一个类似fetch的web服务器变得异常地简单。在本节中,我们会展示一个微型服务器,这个服务器的功能是返回当前用户正在访问的URL。比如用户访问的是 http://localhost:8000/hello ,那么响应是URL.Path = "hello"。

+

gopl.io/ch1/server1

+
// Server1 is a minimal "echo" server.
+package main
+
+import (
+	"fmt"
+	"log"
+	"net/http"
+)
+
+func main() {
+	http.HandleFunc("/", handler) // each request calls handler
+	log.Fatal(http.ListenAndServe("localhost:8000", nil))
+}
+
+// handler echoes the Path component of the request URL r.
+func handler(w http.ResponseWriter, r *http.Request) {
+	fmt.Fprintf(w, "URL.Path = %q\n", r.URL.Path)
+}
+
+

我们只用了八九行代码就实现了一个Web服务程序,这都是多亏了标准库里的方法已经帮我们完成了大量工作。main函数将所有发送到/路径下的请求和handler函数关联起来,/开头的请求其实就是所有发送到当前站点上的请求,服务监听8000端口。发送到这个服务的“请求”是一个http.Request类型的对象,这个对象中包含了请求中的一系列相关字段,其中就包括我们需要的URL。当请求到达服务器时,这个请求会被传给handler函数来处理,这个函数会将/hello这个路径从请求的URL中解析出来,然后把其发送到响应中,这里我们用的是标准输出流的fmt.Fprintf。Web服务会在第7.7节中做更详细的阐述。

+

让我们在后台运行这个服务程序。如果你的操作系统是Mac OS X或者Linux,那么在运行命令的末尾加上一个&符号,即可让程序简单地跑在后台,windows下可以在另外一个命令行窗口去运行这个程序。

+
$ go run src/gopl.io/ch1/server1/main.go &
+
+

现在可以通过命令行来发送客户端请求了:

+
$ go build gopl.io/ch1/fetch
+$ ./fetch http://localhost:8000
+URL.Path = "/"
+$ ./fetch http://localhost:8000/help
+URL.Path = "/help"
+
+

还可以直接在浏览器里访问这个URL,然后得到返回结果,如图1.2:

+

+

在这个服务的基础上叠加特性是很容易的。一种比较实用的修改是为访问的url添加某种状态。比如,下面这个版本输出了同样的内容,但是会对请求的次数进行计算;对URL的请求结果会包含各种URL被访问的总次数,直接对/count这个URL的访问要除外。

+

gopl.io/ch1/server2

+
// Server2 is a minimal "echo" and counter server.
+package main
+
+import (
+	"fmt"
+	"log"
+	"net/http"
+	"sync"
+)
+
+var mu sync.Mutex
+var count int
+
+func main() {
+	http.HandleFunc("/", handler)
+	http.HandleFunc("/count", counter)
+	log.Fatal(http.ListenAndServe("localhost:8000", nil))
+}
+
+// handler echoes the Path component of the requested URL.
+func handler(w http.ResponseWriter, r *http.Request) {
+	mu.Lock()
+	count++
+	mu.Unlock()
+	fmt.Fprintf(w, "URL.Path = %q\n", r.URL.Path)
+}
+
+// counter echoes the number of calls so far.
+func counter(w http.ResponseWriter, r *http.Request) {
+	mu.Lock()
+	fmt.Fprintf(w, "Count %d\n", count)
+	mu.Unlock()
+}
+
+

这个服务器有两个请求处理函数,根据请求的url不同会调用不同的函数:对/count这个url的请求会调用到counter这个函数,其它的url都会调用默认的处理函数。如果你的请求pattern是以/结尾,那么所有以该url为前缀的url都会被这条规则匹配。在这些代码的背后,服务器每一次接收请求处理时都会另起一个goroutine,这样服务器就可以同一时间处理多个请求。然而在并发情况下,假如真的有两个请求同一时刻去更新count,那么这个值可能并不会被正确地增加;这个程序可能会引发一个严重的bug:竞态条件(参见9.1)。为了避免这个问题,我们必须保证每次修改变量的最多只能有一个goroutine,这也就是代码里的mu.Lock()和mu.Unlock()调用将修改count的所有行为包在中间的目的。第九章中我们会进一步讲解共享变量。

+

下面是一个更为丰富的例子,handler函数会把请求的http头和请求的form数据都打印出来,这样可以使检查和调试这个服务更为方便:

+

gopl.io/ch1/server3

+
// handler echoes the HTTP request.
+func handler(w http.ResponseWriter, r *http.Request) {
+	fmt.Fprintf(w, "%s %s %s\n", r.Method, r.URL, r.Proto)
+	for k, v := range r.Header {
+		fmt.Fprintf(w, "Header[%q] = %q\n", k, v)
+	}
+	fmt.Fprintf(w, "Host = %q\n", r.Host)
+	fmt.Fprintf(w, "RemoteAddr = %q\n", r.RemoteAddr)
+	if err := r.ParseForm(); err != nil {
+		log.Print(err)
+	}
+	for k, v := range r.Form {
+		fmt.Fprintf(w, "Form[%q] = %q\n", k, v)
+	}
+}
+
+

我们用http.Request这个struct里的字段来输出下面这样的内容:

+
GET /?q=query HTTP/1.1
+Header["Accept-Encoding"] = ["gzip, deflate, sdch"]
+Header["Accept-Language"] = ["en-US,en;q=0.8"]
+Header["Connection"] = ["keep-alive"]
+Header["Accept"] = ["text/html,application/xhtml+xml,application/xml;..."]
+Header["User-Agent"] = ["Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_5)..."]
+Host = "localhost:8000"
+RemoteAddr = "127.0.0.1:59911"
+Form["q"] = ["query"]
+
+

可以看到这里的ParseForm被嵌套在了if语句中。Go语言允许这样的一个简单的语句结果作为局部的变量声明出现在if语句的最前面,这一点对错误处理很有用处。我们还可以像下面这样写(当然看起来就长了一些):

+
err := r.ParseForm()
+if err != nil {
+	log.Print(err)
+}
+
+

用if和ParseForm结合可以让代码更加简单,并且可以限制err这个变量的作用域,这么做是很不错的。我们会在2.7节中讲解作用域。

+

在这些程序中,我们看到了很多不同的类型被输出到标准输出流中。比如前面的fetch程序,把HTTP的响应数据拷贝到了os.Stdout,lissajous程序里我们输出的是一个文件。fetchall程序则完全忽略到了HTTP的响应Body,只是计算了一下响应Body的大小,这个程序中把响应Body拷贝到了ioutil.Discard。在本节的web服务器程序中则是用fmt.Fprintf直接写到了http.ResponseWriter中。

+

尽管三种具体的实现流程并不太一样,他们都实现一个共同的接口,即当它们被调用需要一个标准流输出时都可以满足。这个接口叫作io.Writer,在7.1节中会详细讨论。

+

Go语言的接口机制会在第7章中讲解,为了在这里简单说明接口能做什么,让我们简单地将这里的web服务器和之前写的lissajous函数结合起来,这样GIF动画可以被写到HTTP的客户端,而不是之前的标准输出流。只要在web服务器的代码里加入下面这几行。

+
handler := func(w http.ResponseWriter, r *http.Request) {
+	lissajous(w)
+}
+http.HandleFunc("/", handler)
+
+

或者另一种等价形式:

+
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+	lissajous(w)
+})
+
+

HandleFunc函数的第二个参数是一个函数的字面值,也就是一个在使用时定义的匿名函数。这些内容我们会在5.6节中讲解。

+

做完这些修改之后,在浏览器里访问 http://localhost:8000 。每次你载入这个页面都可以看到一个像图1.3那样的动画。

+

+

练习 1.12: 修改Lissajour服务,从URL读取变量,比如你可以访问 http://localhost:8000/?cycles=20 这个URL,这样访问可以将程序里的cycles默认的5修改为20。字符串转换为数字可以调用strconv.Atoi函数。你可以在godoc里查看strconv.Atoi的详细说明。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch1/ch1-08.html b/ch1/ch1-08.html new file mode 100644 index 0000000..8419ddd --- /dev/null +++ b/ch1/ch1-08.html @@ -0,0 +1,285 @@ + + + + + + 本章要点 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

1.8. 本章要点

+

本章对Go语言做了一些介绍,Go语言很多方面在有限的篇幅中无法覆盖到。本节会把没有讲到的内容也做一些简单的介绍,这样读者在读到完整的内容之前,可以有个简单的印象。

+

控制流: 在本章我们只介绍了if控制和for,但是没有提到switch多路选择。这里是一个简单的switch的例子:

+
switch coinflip() {
+case "heads":
+	heads++
+case "tails":
+	tails++
+default:
+	fmt.Println("landed on edge!")
+}
+
+

在翻转硬币的时候,例子里的coinflip函数返回几种不同的结果,每一个case都会对应一个返回结果,这里需要注意,Go语言并不需要显式地在每一个case后写break,语言默认执行完case后的逻辑语句会自动退出。当然了,如果你想要相邻的几个case都执行同一逻辑的话,需要自己显式地写上一个fallthrough语句来覆盖这种默认行为。不过fallthrough语句在一般的程序中很少用到。

+

Go语言里的switch还可以不带操作对象(译注:switch不带操作对象时默认用true值代替,然后将每个case的表达式和true值进行比较);可以直接罗列多种条件,像其它语言里面的多个if else一样,下面是一个例子:

+
func Signum(x int) int {
+	switch {
+	case x > 0:
+		return +1
+	default:
+		return 0
+	case x < 0:
+		return -1
+	}
+}
+
+

这种形式叫做无tag switch(tagless switch);这和switch true是等价的。

+

像for和if控制语句一样,switch也可以紧跟一个简短的变量声明,一个自增表达式、赋值语句,或者一个函数调用(译注:比其它语言丰富)。

+

break和continue语句会改变控制流。和其它语言中的break和continue一样,break会中断当前的循环,并开始执行循环之后的内容,而continue会跳过当前循环,并开始执行下一次循环。这两个语句除了可以控制for循环,还可以用来控制switch和select语句(之后会讲到),在1.3节中我们看到,continue会跳过内层的循环,如果我们想跳过的是更外层的循环的话,我们可以在相应的位置加上label,这样break和continue就可以根据我们的想法来continue和break任意循环。这看起来甚至有点像goto语句的作用了。当然,一般程序员也不会用到这种操作。这两种行为更多地被用到机器生成的代码中。

+

命名类型: 类型声明使得我们可以很方便地给一个特殊类型一个名字。因为struct类型声明通常非常地长,所以我们总要给这种struct取一个名字。本章中就有这样一个例子,二维点类型:

+
type Point struct {
+	X, Y int
+}
+var p Point
+
+

类型声明和命名类型会在第二章中介绍。

+

指针: Go语言提供了指针。指针是一种直接存储了变量的内存地址的数据类型。在其它语言中,比如C语言,指针操作是完全不受约束的。在另外一些语言中,指针一般被处理为“引用”,除了到处传递这些指针之外,并不能对这些指针做太多事情。Go语言在这两种范围中取了一种平衡。指针是可见的内存地址,&操作符可以返回一个变量的内存地址,并且*操作符可以获取指针指向的变量内容,但是在Go语言里没有指针运算,也就是不能像c语言里可以对指针进行加或减操作。我们会在2.3.2中进行详细介绍。

+

方法和接口: 方法是和命名类型关联的一类函数。Go语言里比较特殊的是方法可以被关联到任意一种命名类型。在第六章我们会详细地讲方法。接口是一种抽象类型,这种类型可以让我们以同样的方式来处理不同的固有类型,不用关心它们的具体实现,而只需要关注它们提供的方法。第七章中会详细说明这些内容。

+

包(packages): Go语言提供了一些很好用的package,并且这些package是可以扩展的。Go语言社区已经创造并且分享了很多很多。所以Go语言编程大多数情况下就是用已有的package来写我们自己的代码。通过这本书,我们会讲解一些重要的标准库内的package,但是还是有很多限于篇幅没有去说明,因为我们没法在这样的厚度的书里去做一部代码大全。

+

在你开始写一个新程序之前,最好先去检查一下是不是已经有了现成的库可以帮助你更高效地完成这件事情。你可以在 https://golang.org/pkg 和 https://godoc.org 中找到标准库和社区写的package。godoc这个工具可以让你直接在本地命令行阅读标准库的文档。比如下面这个例子。

+
$ go doc http.ListenAndServe
+package http // import "net/http"
+func ListenAndServe(addr string, handler Handler) error
+    ListenAndServe listens on the TCP network address addr and then
+    calls Serve with handler to handle requests on incoming connections.
+...
+
+

注释: 我们之前已经提到过了在源文件的开头写的注释是这个源文件的文档。在每一个函数之前写一个说明函数行为的注释也是一个好习惯。这些惯例很重要,因为这些内容会被像godoc这样的工具检测到,并且在执行命令时显示这些注释。具体可以参考10.7.4。

+

多行注释可以用 /* ... */ 来包裹,和其它大多数语言一样。在文件一开头的注释一般都是这种形式,或者一大段的解释性的注释文字也会被这符号包住,来避免每一行都需要加//。在注释中//和/*是没什么意义的,所以不要在注释中再嵌入注释。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch1/ch1.html b/ch1/ch1.html new file mode 100644 index 0000000..e7e8388 --- /dev/null +++ b/ch1/ch1.html @@ -0,0 +1,240 @@ + + + + + + 入门 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

第1章 入门

+

本章介绍Go语言的基础组件。本章提供了足够的信息和示例程序,希望可以帮你尽快入门,写出有用的程序。本章和之后章节的示例程序都针对你可能遇到的现实案例。先了解几个Go程序,涉及的主题从简单的文件处理、图像处理到互联网客户端和服务端并发。当然,第一章不会解释细枝末节,但用这些程序来学习一门新语言还是很有效的。

+

学习一门新语言时,会有一种自然的倾向,按照自己熟悉的语言的套路写新语言程序。学习Go语言的过程中,请警惕这种想法,尽量别这么做。我们会演示怎么写好Go语言程序,所以,请使用本书的代码作为你自己写程序时的指南。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch10/ch10-01.html b/ch10/ch10-01.html new file mode 100644 index 0000000..ff8b30a --- /dev/null +++ b/ch10/ch10-01.html @@ -0,0 +1,242 @@ + + + + + + 包简介 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

10.1. 包简介

+

任何包系统设计的目的都是为了简化大型程序的设计和维护工作,通过将一组相关的特性放进一个独立的单元以便于理解和更新,在每个单元更新的同时保持和程序中其它单元的相对独立性。这种模块化的特性允许每个包可以被其它的不同项目共享和重用,在项目范围内、甚至全球范围统一的分发和复用。

+

每个包一般都定义了一个不同的名字空间用于它内部的每个标识符的访问。每个名字空间关联到一个特定的包,让我们给类型、函数等选择简短明了的名字,这样可以在使用它们的时候减少和其它部分名字的冲突。

+

每个包还通过控制包内名字的可见性和是否导出来实现封装特性。通过限制包成员的可见性并隐藏包API的具体实现,将允许包的维护者在不影响外部包用户的前提下调整包的内部实现。通过限制包内变量的可见性,还可以强制用户通过某些特定函数来访问和更新内部变量,这样可以保证内部变量的一致性和并发时的互斥约束。

+

当我们修改了一个源文件,我们必须重新编译该源文件对应的包和所有依赖该包的其他包。即使是从头构建,Go语言编译器的编译速度也明显快于其它编译语言。Go语言的闪电般的编译速度主要得益于三个语言特性。第一点,所有导入的包必须在每个文件的开头显式声明,这样的话编译器就没有必要读取和分析整个源文件来判断包的依赖关系。第二点,禁止包的环状依赖,因为没有循环依赖,包的依赖关系形成一个有向无环图,每个包可以被独立编译,而且很可能是被并发编译。第三点,编译后包的目标文件不仅仅记录包本身的导出信息,目标文件同时还记录了包的依赖关系。因此,在编译一个包的时候,编译器只需要读取每个直接导入包的目标文件,而不需要遍历所有依赖的的文件(译注:很多都是重复的间接依赖)。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch10/ch10-02.html b/ch10/ch10-02.html new file mode 100644 index 0000000..0a2fa12 --- /dev/null +++ b/ch10/ch10-02.html @@ -0,0 +1,251 @@ + + + + + + 导入路径 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

10.2. 导入路径

+

每个包是由一个全局唯一的字符串所标识的导入路径定位。出现在import语句中的导入路径也是字符串。

+
import (
+	"fmt"
+	"math/rand"
+	"encoding/json"
+
+	"golang.org/x/net/html"
+
+	"github.com/go-sql-driver/mysql"
+)
+
+

就像我们在2.6.1节提到过的,Go语言的规范并没有指明包的导入路径字符串的具体含义,导入路径的具体含义是由构建工具来解释的。在本章,我们将深入讨论Go语言工具箱的功能,包括大家经常使用的构建测试等功能。当然,也有第三方扩展的工具箱存在。例如,Google公司内部的Go语言码农,他们就使用内部的多语言构建系统(译注:Google公司使用的是类似Bazel的构建系统,支持多种编程语言,目前该构件系统还不能完整支持Windows环境),用不同的规则来处理包名字和定位包,用不同的规则来处理单元测试等等,因为这样可以更紧密适配他们内部环境。

+

如果你计划分享或发布包,那么导入路径最好是全球唯一的。为了避免冲突,所有非标准库包的导入路径建议以所在组织的互联网域名为前缀;而且这样也有利于包的检索。例如,上面的import语句导入了Go团队维护的HTML解析器和一个流行的第三方维护的MySQL驱动。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch10/ch10-03.html b/ch10/ch10-03.html new file mode 100644 index 0000000..ae90c87 --- /dev/null +++ b/ch10/ch10-03.html @@ -0,0 +1,255 @@ + + + + + + 包声明 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

10.3. 包声明

+

在每个Go语言源文件的开头都必须有包声明语句。包声明语句的主要目的是确定当前包被其它包导入时默认的标识符(也称为包名)。

+

例如,math/rand包的每个源文件的开头都包含package rand包声明语句,所以当你导入这个包,你就可以用rand.Int、rand.Float64类似的方式访问包的成员。

+
package main
+
+import (
+	"fmt"
+	"math/rand"
+)
+
+func main() {
+	fmt.Println(rand.Int())
+}
+
+

通常来说,默认的包名就是包导入路径名的最后一段,因此即使两个包的导入路径不同,它们依然可能有一个相同的包名。例如,math/rand包和crypto/rand包的包名都是rand。稍后我们将看到如何同时导入两个有相同包名的包。

+

关于默认包名一般采用导入路径名的最后一段的约定也有三种例外情况。第一个例外,包对应一个可执行程序,也就是main包,这时候main包本身的导入路径是无关紧要的。名字为main的包是给go build(§10.7.3)构建命令一个信息,这个包编译完之后必须调用连接器生成一个可执行程序。

+

第二个例外,包所在的目录中可能有一些文件名是以_test.go为后缀的Go源文件(译注:前面必须有其它的字符,因为以_.开头的源文件会被构建工具忽略),并且这些源文件声明的包名也是以_test为后缀名的。这种目录可以包含两种包:一种是普通包,另一种则是测试的外部扩展包。所有以_test为后缀包名的测试外部扩展包都由go test命令独立编译,普通包和测试的外部扩展包是相互独立的。测试的外部扩展包一般用来避免测试代码中的循环导入依赖,具体细节我们将在11.2.4节中介绍。

+

第三个例外,一些依赖版本号的管理工具会在导入路径后追加版本号信息,例如“gopkg.in/yaml.v2”。这种情况下包的名字并不包含版本号后缀,而是yaml。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch10/ch10-04.html b/ch10/ch10-04.html new file mode 100644 index 0000000..a017060 --- /dev/null +++ b/ch10/ch10-04.html @@ -0,0 +1,266 @@ + + + + + + 导入声明 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

10.4. 导入声明

+

可以在一个Go语言源文件包声明语句之后,其它非导入声明语句之前,包含零到多个导入包声明语句。每个导入声明可以单独指定一个导入路径,也可以通过圆括号同时导入多个导入路径。下面两个导入形式是等价的,但是第二种形式更为常见。

+
import "fmt"
+import "os"
+
+import (
+	"fmt"
+	"os"
+)
+
+

导入的包之间可以通过添加空行来分组;通常将来自不同组织的包独自分组。包的导入顺序无关紧要,但是在每个分组中一般会根据字符串顺序排列。(gofmt和goimports工具都可以将不同分组导入的包独立排序。)

+
import (
+	"fmt"
+	"html/template"
+	"os"
+
+	"golang.org/x/net/html"
+	"golang.org/x/net/ipv4"
+)
+
+

如果我们想同时导入两个有着名字相同的包,例如math/rand包和crypto/rand包,那么导入声明必须至少为一个同名包指定一个新的包名以避免冲突。这叫做导入包的重命名。

+
import (
+	"crypto/rand"
+	mrand "math/rand" // alternative name mrand avoids conflict
+)
+
+

导入包的重命名只影响当前的源文件。其它的源文件如果导入了相同的包,可以用导入包原本默认的名字或重命名为另一个完全不同的名字。

+

导入包重命名是一个有用的特性,它不仅仅只是为了解决名字冲突。如果导入的一个包名很笨重,特别是在一些自动生成的代码中,这时候用一个简短名称会更方便。选择用简短名称重命名导入包时候最好统一,以避免包名混乱。选择另一个包名称还可以帮助避免和本地普通变量名产生冲突。例如,如果文件中已经有了一个名为path的变量,那么我们可以将“path”标准包重命名为pathpkg。

+

每个导入声明语句都明确指定了当前包和被导入包之间的依赖关系。如果遇到包循环导入的情况,Go语言的构建工具将报告错误。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch10/ch10-05.html b/ch10/ch10-05.html new file mode 100644 index 0000000..9f6b51e --- /dev/null +++ b/ch10/ch10-05.html @@ -0,0 +1,309 @@ + + + + + + 包的匿名导入 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

10.5. 包的匿名导入

+

如果只是导入一个包而并不使用导入的包将会导致一个编译错误。但是有时候我们只是想利用导入包而产生的副作用:它会计算包级变量的初始化表达式和执行导入包的init初始化函数(§2.6.2)。这时候我们需要抑制“unused import”编译错误,我们可以用下划线_来重命名导入的包。像往常一样,下划线_为空白标识符,并不能被访问。

+
import _ "image/png" // register PNG decoder
+
+

这个被称为包的匿名导入。它通常是用来实现一个编译时机制,然后通过在main主程序入口选择性地导入附加的包。首先,让我们看看如何使用该特性,然后再看看它是如何工作的。

+

标准库的image图像包包含了一个Decode函数,用于从io.Reader接口读取数据并解码图像,它调用底层注册的图像解码器来完成任务,然后返回image.Image类型的图像。使用image.Decode很容易编写一个图像格式的转换工具,读取一种格式的图像,然后编码为另一种图像格式:

+

gopl.io/ch10/jpeg

+
// The jpeg command reads a PNG image from the standard input
+// and writes it as a JPEG image to the standard output.
+package main
+
+import (
+	"fmt"
+	"image"
+	"image/jpeg"
+	_ "image/png" // register PNG decoder
+	"io"
+	"os"
+)
+
+func main() {
+	if err := toJPEG(os.Stdin, os.Stdout); err != nil {
+		fmt.Fprintf(os.Stderr, "jpeg: %v\n", err)
+		os.Exit(1)
+	}
+}
+
+func toJPEG(in io.Reader, out io.Writer) error {
+	img, kind, err := image.Decode(in)
+	if err != nil {
+		return err
+	}
+	fmt.Fprintln(os.Stderr, "Input format =", kind)
+	return jpeg.Encode(out, img, &jpeg.Options{Quality: 95})
+}
+
+

如果我们将gopl.io/ch3/mandelbrot(§3.3)的输出导入到这个程序的标准输入,它将解码输入的PNG格式图像,然后转换为JPEG格式的图像输出(图3.3)。

+
$ go build gopl.io/ch3/mandelbrot
+$ go build gopl.io/ch10/jpeg
+$ ./mandelbrot | ./jpeg >mandelbrot.jpg
+Input format = png
+
+

要注意image/png包的匿名导入语句。如果没有这一行语句,程序依然可以编译和运行,但是它将不能正确识别和解码PNG格式的图像:

+
$ go build gopl.io/ch10/jpeg
+$ ./mandelbrot | ./jpeg >mandelbrot.jpg
+jpeg: image: unknown format
+
+

下面的代码演示了它的工作机制。标准库还提供了GIF、PNG和JPEG等格式图像的解码器,用户也可以提供自己的解码器,但是为了保持程序体积较小,很多解码器并没有被全部包含,除非是明确需要支持的格式。image.Decode函数在解码时会依次查询支持的格式列表。每个格式驱动列表的每个入口指定了四件事情:格式的名称;一个用于描述这种图像数据开头部分模式的字符串,用于解码器检测识别;一个Decode函数用于完成解码图像工作;一个DecodeConfig函数用于解码图像的大小和颜色空间的信息。每个驱动入口是通过调用image.RegisterFormat函数注册,一般是在每个格式包的init初始化函数中调用,例如image/png包是这样注册的:

+
package png // image/png
+
+func Decode(r io.Reader) (image.Image, error)
+func DecodeConfig(r io.Reader) (image.Config, error)
+
+func init() {
+	const pngHeader = "\x89PNG\r\n\x1a\n"
+	image.RegisterFormat("png", pngHeader, Decode, DecodeConfig)
+}
+
+

最终的效果是,主程序只需要匿名导入特定图像驱动包就可以用image.Decode解码对应格式的图像了。

+

数据库包database/sql也是采用了类似的技术,让用户可以根据自己需要选择导入必要的数据库驱动。例如:

+
import (
+	"database/sql"
+	_ "github.com/lib/pq"              // enable support for Postgres
+	_ "github.com/go-sql-driver/mysql" // enable support for MySQL
+)
+
+db, err = sql.Open("postgres", dbname) // OK
+db, err = sql.Open("mysql", dbname)    // OK
+db, err = sql.Open("sqlite3", dbname)  // returns error: unknown driver "sqlite3"
+
+

练习 10.1: 扩展jpeg程序,以支持任意图像格式之间的相互转换,使用image.Decode检测支持的格式类型,然后通过flag命令行标志参数选择输出的格式。

+

练习 10.2: 设计一个通用的压缩文件读取框架,用来读取ZIP(archive/zip)和POSIX tar(archive/tar)格式压缩的文档。使用类似上面的注册技术来扩展支持不同的压缩格式,然后根据需要通过匿名导入选择导入要支持的压缩格式的驱动包。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch10/ch10-06.html b/ch10/ch10-06.html new file mode 100644 index 0000000..cd82ef5 --- /dev/null +++ b/ch10/ch10-06.html @@ -0,0 +1,266 @@ + + + + + + 包和命名 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

10.6. 包和命名

+

在本节中,我们将提供一些关于Go语言独特的包和成员命名的约定。

+

当创建一个包,一般要用短小的包名,但也不能太短导致难以理解。标准库中最常用的包有bufio、bytes、flag、fmt、http、io、json、os、sort、sync和time等包。

+

尽可能让命名有描述性且无歧义。例如,类似imageutil或ioutilis的工具包命名已经足够简洁了,就无须再命名为util了。要尽量避免包名使用可能被经常用于局部变量的名字,这样可能导致用户重命名导入包,例如前面看到的path包。

+

包名一般采用单数的形式。标准库的bytes、errors和strings使用了复数形式,这是为了避免和预定义的类型冲突,同样还有go/types是为了避免和type关键字冲突。

+

要避免包名有其它的含义。例如,2.5节中我们的温度转换包最初使用了temp包名,虽然并没有持续多久。但这是一个糟糕的尝试,因为temp几乎是临时变量的同义词。然后我们有一段时间使用了temperature作为包名,显然名字并没有表达包的真实用途。最后我们改成了和strconv标准包类似的tempconv包名,这个名字比之前的就好多了。

+

现在让我们看看如何命名包的成员。由于是通过包的导入名字引入包里面的成员,例如fmt.Println,同时包含了包名和成员名信息。因此,我们一般并不需要关注Println的具体内容,因为fmt包名已经包含了这个信息。当设计一个包的时候,需要考虑包名和成员名两个部分如何很好地配合。下面有一些例子:

+
bytes.Equal    flag.Int    http.Get    json.Marshal
+
+

我们可以看到一些常用的命名模式。strings包提供了和字符串相关的诸多操作:

+
package strings
+
+func Index(needle, haystack string) int
+
+type Replacer struct{ /* ... */ }
+func NewReplacer(oldnew ...string) *Replacer
+
+type Reader struct{ /* ... */ }
+func NewReader(s string) *Reader
+
+

包名strings并没有出现在任何成员名字中。因为用户会这样引用这些成员strings.Index、strings.Replacer等。

+

其它一些包,可能只描述了单一的数据类型,例如html/template和math/rand等,只暴露一个主要的数据结构和与它相关的方法,还有一个以New命名的函数用于创建实例。

+
package rand // "math/rand"
+
+type Rand struct{ /* ... */ }
+func New(source Source) *Rand
+
+

这可能导致一些名字重复,例如template.Template或rand.Rand,这就是这些种类的包名往往特别短的原因之一。

+

在另一个极端,还有像net/http包那样含有非常多的名字和种类不多的数据类型,因为它们都是要执行一个复杂的复合任务。尽管有将近二十种类型和更多的函数,但是包中最重要的成员名字却是简单明了的:Get、Post、Handle、Error、Client、Server等。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch10/ch10-07.html b/ch10/ch10-07.html new file mode 100644 index 0000000..bc79a6e --- /dev/null +++ b/ch10/ch10-07.html @@ -0,0 +1,535 @@ + + + + + + 工具 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

10.7. 工具

+

本章剩下的部分将讨论Go语言工具箱的具体功能,包括如何下载、格式化、构建、测试和安装Go语言编写的程序。

+

Go语言的工具箱集合了一系列功能的命令集。它可以看作是一个包管理器(类似于Linux中的apt和rpm工具),用于包的查询、计算包的依赖关系、从远程版本控制系统下载它们等任务。它也是一个构建系统,计算文件的依赖关系,然后调用编译器、汇编器和链接器构建程序,虽然它故意被设计成没有标准的make命令那么复杂。它也是一个单元测试和基准测试的驱动程序,我们将在第11章讨论测试话题。

+

Go语言工具箱的命令有着类似“瑞士军刀”的风格,带着一打的子命令,有一些我们经常用到,例如get、run、build和fmt等。你可以运行go或go help命令查看内置的帮助文档,为了查询方便,我们列出了最常用的命令:

+
$ go
+...
+	build            compile packages and dependencies
+	clean            remove object files
+	doc              show documentation for package or symbol
+	env              print Go environment information
+	fmt              run gofmt on package sources
+	get              download and install packages and dependencies
+	install          compile and install packages and dependencies
+	list             list packages
+	run              compile and run Go program
+	test             test packages
+	version          print Go version
+	vet              run go tool vet on packages
+
+Use "go help [command]" for more information about a command.
+...
+
+

为了达到零配置的设计目标,Go语言的工具箱很多地方都依赖各种约定。例如,根据给定的源文件的名称,Go语言的工具可以找到源文件对应的包,因为每个目录只包含了单一的包,并且包的导入路径和工作区的目录结构是对应的。给定一个包的导入路径,Go语言的工具可以找到与之对应的存储着实体文件的目录。它还可以根据导入路径找到存储代码的仓库的远程服务器URL。

+

10.7.1. 工作区结构

+

对于大多数的Go语言用户,只需要配置一个名叫GOPATH的环境变量,用来指定当前工作目录即可。当需要切换到不同工作区的时候,只要更新GOPATH就可以了。例如,我们在编写本书时将GOPATH设置为$HOME/gobook

+
$ export GOPATH=$HOME/gobook
+$ go get gopl.io/...
+
+

当你用前面介绍的命令下载本书全部的例子源码之后,你的当前工作区的目录结构应该是这样的:

+
GOPATH/
+	src/
+		gopl.io/
+			.git/
+			ch1/
+				helloworld/
+					main.go
+				dup/
+					main.go
+				...
+		golang.org/x/net/
+			.git/
+			html/
+				parse.go
+				node.go
+				...
+	bin/
+		helloworld
+		dup
+	pkg/
+		darwin_amd64/
+		...
+
+

GOPATH对应的工作区目录有三个子目录。其中src子目录用于存储源代码。每个包被保存在与$GOPATH/src的相对路径为包导入路径的子目录中,例如gopl.io/ch1/helloworld相对应的路径目录。我们看到,一个GOPATH工作区的src目录中可能有多个独立的版本控制系统,例如gopl.io和golang.org分别对应不同的Git仓库。其中pkg子目录用于保存编译后的包的目标文件,bin子目录用于保存编译后的可执行程序,例如helloworld可执行程序。

+

第二个环境变量GOROOT用来指定Go的安装目录,还有它自带的标准库包的位置。GOROOT的目录结构和GOPATH类似,因此存放fmt包的源代码对应目录应该为$GOROOT/src/fmt。用户一般不需要设置GOROOT,默认情况下Go语言安装工具会将其设置为安装的目录路径。

+

其中go env命令用于查看Go语言工具涉及的所有环境变量的值,包括未设置环境变量的默认值。GOOS环境变量用于指定目标操作系统(例如android、linux、darwin或windows),GOARCH环境变量用于指定处理器的类型,例如amd64、386或arm等。虽然GOPATH环境变量是唯一必须要设置的,但是其它环境变量也会偶尔用到。

+
$ go env
+GOPATH="/home/gopher/gobook"
+GOROOT="/usr/local/go"
+GOARCH="amd64"
+GOOS="darwin"
+...
+
+

10.7.2. 下载包

+

使用Go语言工具箱的go命令,不仅可以根据包导入路径找到本地工作区的包,甚至可以从互联网上找到和更新包。

+

使用命令go get可以下载一个单一的包或者用...下载整个子目录里面的每个包。Go语言工具箱的go命令同时计算并下载所依赖的每个包,这也是前一个例子中golang.org/x/net/html自动出现在本地工作区目录的原因。

+

一旦go get命令下载了包,然后就是安装包或包对应的可执行的程序。我们将在下一节再关注它的细节,现在只是展示整个下载过程是如何的简单。第一个命令是获取golint工具,它用于检测Go源代码的编程风格是否有问题。第二个命令是用golint命令对2.6.2节的gopl.io/ch2/popcount包代码进行编码风格检查。它友好地报告了忘记了包的文档:

+
$ go get github.com/golang/lint/golint
+$ $GOPATH/bin/golint gopl.io/ch2/popcount
+src/gopl.io/ch2/popcount/main.go:1:1:
+  package comment should be of the form "Package popcount ..."
+
+

go get命令支持当前流行的托管网站GitHub、Bitbucket和Launchpad,可以直接向它们的版本控制系统请求代码。对于其它的网站,你可能需要指定版本控制系统的具体路径和协议,例如 Git或Mercurial。运行go help importpath获取相关的信息。

+

go get命令获取的代码是真实的本地存储仓库,而不仅仅只是复制源文件,因此你依然可以使用版本管理工具比较本地代码的变更或者切换到其它的版本。例如golang.org/x/net包目录对应一个Git仓库:

+
$ cd $GOPATH/src/golang.org/x/net
+$ git remote -v
+origin  https://go.googlesource.com/net (fetch)
+origin  https://go.googlesource.com/net (push)
+
+

需要注意的是导入路径含有的网站域名和本地Git仓库对应远程服务地址并不相同,真实的Git地址是go.googlesource.com。这其实是Go语言工具的一个特性,可以让包用一个自定义的导入路径,但是真实的代码却是由更通用的服务提供,例如googlesource.com或github.com。因为页面 https://golang.org/x/net/html 包含了如下的元数据,它告诉Go语言的工具当前包真实的Git仓库托管地址:

+
$ go build gopl.io/ch1/fetch
+$ ./fetch https://golang.org/x/net/html | grep go-import
+<meta name="go-import"
+      content="golang.org/x/net git https://go.googlesource.com/net">
+
+

如果指定-u命令行标志参数,go get命令将确保所有的包和依赖的包的版本都是最新的,然后重新编译和安装它们。如果不包含该标志参数的话,而且如果包已经在本地存在,那么代码将不会被自动更新。

+

go get -u命令只是简单地保证每个包是最新版本,如果是第一次下载包则是比较方便的;但是对于发布程序则可能是不合适的,因为本地程序可能需要对依赖的包做精确的版本依赖管理。通常的解决方案是使用vendor的目录用于存储依赖包的固定版本的源代码,对本地依赖的包的版本更新也是谨慎和持续可控的。在Go1.5之前,一般需要修改包的导入路径,所以复制后golang.org/x/net/html导入路径可能会变为gopl.io/vendor/golang.org/x/net/html。最新的Go语言命令已经支持vendor特性,但限于篇幅这里并不讨论vendor的具体细节。不过可以通过go help gopath命令查看Vendor的帮助文档。

+

(译注:墙内用户在上面这些命令的基础上,还需要学习用翻墙来go get。)

+

练习 10.3: 从 http://gopl.io/ch1/helloworld?go-get=1 获取内容,查看本书的代码的真实托管的网址(go get请求HTML页面时包含了go-get参数,以区别普通的浏览器请求)。

+

10.7.3. 构建包

+

go build命令编译命令行参数指定的每个包。如果包是一个库,则忽略输出结果;这可以用于检测包是可以正确编译的。如果包的名字是main,go build将调用链接器在当前目录创建一个可执行程序;以导入路径的最后一段作为可执行程序的名字。

+

由于每个目录只包含一个包,因此每个对应可执行程序或者叫Unix术语中的命令的包,会要求放到一个独立的目录中。这些目录有时候会放在名叫cmd目录的子目录下面,例如用于提供Go文档服务的golang.org/x/tools/cmd/godoc命令就是放在cmd子目录(§10.7.4)。

+

每个包可以由它们的导入路径指定,就像前面看到的那样,或者用一个相对目录的路径名指定,相对路径必须以...开头。如果没有指定参数,那么默认指定为当前目录对应的包。下面的命令用于构建同一个包,虽然它们的写法各不相同:

+
$ cd $GOPATH/src/gopl.io/ch1/helloworld
+$ go build
+
+

或者:

+
$ cd anywhere
+$ go build gopl.io/ch1/helloworld
+
+

或者:

+
$ cd $GOPATH
+$ go build ./src/gopl.io/ch1/helloworld
+
+

但不能这样:

+
$ cd $GOPATH
+$ go build src/gopl.io/ch1/helloworld
+Error: cannot find package "src/gopl.io/ch1/helloworld".
+
+

也可以指定包的源文件列表,这一般只用于构建一些小程序或做一些临时性的实验。如果是main包,将会以第一个Go源文件的基础文件名作为最终的可执行程序的名字。

+
$ cat quoteargs.go
+package main
+
+import (
+	"fmt"
+	"os"
+)
+
+func main() {
+	fmt.Printf("%q\n", os.Args[1:])
+}
+$ go build quoteargs.go
+$ ./quoteargs one "two three" four\ five
+["one" "two three" "four five"]
+
+

特别是对于这类一次性运行的程序,我们希望尽快的构建并运行它。go run命令实际上是结合了构建和运行的两个步骤:

+
$ go run quoteargs.go one "two three" four\ five
+["one" "two three" "four five"]
+
+

(译注:其实也可以偷懒,直接go run *.go)

+

第一行的参数列表中,第一个不是以.go结尾的将作为可执行程序的参数运行。

+

默认情况下,go build命令构建指定的包和它依赖的包,然后丢弃除了最后的可执行文件之外所有的中间编译结果。依赖分析和编译过程虽然都是很快的,但是随着项目增加到几十个包和成千上万行代码,依赖关系分析和编译时间的消耗将变的可观,有时候可能需要几秒种,即使这些依赖项没有改变。

+

go install命令和go build命令很相似,但是它会保存每个包的编译成果,而不是将它们都丢弃。被编译的包会被保存到$GOPATH/pkg目录下,目录路径和 src目录路径对应,可执行程序被保存到$GOPATH/bin目录。(很多用户会将$GOPATH/bin添加到可执行程序的搜索列表中。)还有,go install命令和go build命令都不会重新编译没有发生变化的包,这可以使后续构建更快捷。为了方便编译依赖的包,go build -i命令将安装每个目标所依赖的包。

+

因为编译对应不同的操作系统平台和CPU架构,go install命令会将编译结果安装到GOOS和GOARCH对应的目录。例如,在Mac系统,golang.org/x/net/html包将被安装到$GOPATH/pkg/darwin_amd64目录下的golang.org/x/net/html.a文件。

+

针对不同操作系统或CPU的交叉构建也是很简单的。只需要设置好目标对应的GOOS和GOARCH,然后运行构建命令即可。下面交叉编译的程序将输出它在编译时的操作系统和CPU类型:

+

gopl.io/ch10/cross

+
func main() {
+	fmt.Println(runtime.GOOS, runtime.GOARCH)
+}
+
+

下面以64位和32位环境分别编译和执行:

+
$ go build gopl.io/ch10/cross
+$ ./cross
+darwin amd64
+$ GOARCH=386 go build gopl.io/ch10/cross
+$ ./cross
+darwin 386
+
+

有些包可能需要针对不同平台和处理器类型使用不同版本的代码文件,以便于处理底层的可移植性问题或为一些特定代码提供优化。如果一个文件名包含了一个操作系统或处理器类型名字,例如net_linux.go或asm_amd64.s,Go语言的构建工具将只在对应的平台编译这些文件。还有一个特别的构建注释参数可以提供更多的构建过程控制。例如,文件中可能包含下面的注释:

+
// +build linux darwin
+
+

在包声明和包注释的前面,该构建注释参数告诉go build只在编译程序对应的目标操作系统是Linux或Mac OS X时才编译这个文件。下面的构建注释则表示不编译这个文件:

+
// +build ignore
+
+

更多细节,可以参考go/build包的构建约束部分的文档。

+
$ go doc go/build
+
+

10.7.4. 包文档

+

Go语言的编码风格鼓励为每个包提供良好的文档。包中每个导出的成员和包声明前都应该包含目的和用法说明的注释。

+

Go语言中的文档注释一般是完整的句子,第一行通常是摘要说明,以被注释者的名字开头。注释中函数的参数或其它的标识符并不需要额外的引号或其它标记注明。例如,下面是fmt.Fprintf的文档注释。

+
// Fprintf formats according to a format specifier and writes to w.
+// It returns the number of bytes written and any write error encountered.
+func Fprintf(w io.Writer, format string, a ...interface{}) (int, error)
+
+

Fprintf函数格式化的细节在fmt包文档中描述。如果注释后紧跟着包声明语句,那注释对应整个包的文档。包文档对应的注释只能有一个(译注:其实可以有多个,它们会组合成一个包文档注释),包注释可以出现在任何一个源文件中。如果包的注释内容比较长,一般会放到一个独立的源文件中;fmt包注释就有300行之多。这个专门用于保存包文档的源文件通常叫doc.go。

+

好的文档并不需要面面俱到,文档本身应该是简洁但不可忽略的。事实上,Go语言的风格更喜欢简洁的文档,并且文档也是需要像代码一样维护的。对于一组声明语句,可以用一个精炼的句子描述,如果是显而易见的功能则并不需要注释。

+

在本书中,只要空间允许,我们之前很多包声明都包含了注释文档,但你可以从标准库中发现很多更好的例子。有两个工具可以帮到你。

+

首先是go doc命令,该命令打印其后所指定的实体的声明与文档注释,该实体可能是一个包:

+
$ go doc time
+package time // import "time"
+
+Package time provides functionality for measuring and displaying time.
+
+const Nanosecond Duration = 1 ...
+func After(d Duration) <-chan Time
+func Sleep(d Duration)
+func Since(t Time) Duration
+func Now() Time
+type Duration int64
+type Time struct { ... }
+...many more...
+
+

或者是某个具体的包成员:

+
$ go doc time.Since
+func Since(t Time) Duration
+
+	Since returns the time elapsed since t.
+	It is shorthand for time.Now().Sub(t).
+
+

或者是一个方法:

+
$ go doc time.Duration.Seconds
+func (d Duration) Seconds() float64
+
+	Seconds returns the duration as a floating-point number of seconds.
+
+

该命令并不需要输入完整的包导入路径或正确的大小写。下面的命令将打印encoding/json包的(*json.Decoder).Decode方法的文档:

+
$ go doc json.decode
+func (dec *Decoder) Decode(v interface{}) error
+
+	Decode reads the next JSON-encoded value from its input and stores
+	it in the value pointed to by v.
+
+

第二个工具,名字也叫godoc,它提供可以相互交叉引用的HTML页面,但是包含和go doc命令相同以及更多的信息。图10.1演示了time包的文档,11.6节将看到godoc演示可以交互的示例程序。godoc的在线服务 https://godoc.org ,包含了成千上万的开源包的检索工具。

+

+

你也可以在自己的工作区目录运行godoc服务。运行下面的命令,然后在浏览器查看 http://localhost:8000/pkg 页面:

+
$ godoc -http :8000
+
+

其中-analysis=type-analysis=pointer命令行标志参数用于打开文档和代码中关于静态分析的结果。

+

10.7.5. 内部包

+

在Go语言程序中,包是最重要的封装机制。没有导出的标识符只在同一个包内部可以访问,而导出的标识符则是面向全宇宙都是可见的。

+

有时候,一个中间的状态可能也是有用的,标识符对于一小部分信任的包是可见的,但并不是对所有调用者都可见。例如,当我们计划将一个大的包拆分为很多小的更容易维护的子包,但是我们并不想将内部的子包结构也完全暴露出去。同时,我们可能还希望在内部子包之间共享一些通用的处理包,或者我们只是想实验一个新包的还并不稳定的接口,暂时只暴露给一些受限制的用户使用。

+

为了满足这些需求,Go语言的构建工具对包含internal名字的路径段的包导入路径做了特殊处理。这种包叫internal包,一个internal包只能被和internal目录有同一个父目录的包所导入。例如,net/http/internal/chunked内部包只能被net/http/httputil或net/http包导入,但是不能被net/url包导入。不过net/url包却可以导入net/http/httputil包。

+
net/http
+net/http/internal/chunked
+net/http/httputil
+net/url
+
+

10.7.6. 查询包

+

go list命令可以查询可用包的信息。其最简单的形式,可以测试包是否在工作区并打印它的导入路径:

+
$ go list github.com/go-sql-driver/mysql
+github.com/go-sql-driver/mysql
+
+

go list命令的参数还可以用"..."表示匹配任意的包的导入路径。我们可以用它来列出工作区中的所有包:

+
$ go list ...
+archive/tar
+archive/zip
+bufio
+bytes
+cmd/addr2line
+cmd/api
+...many more...
+
+

或者是特定子目录下的所有包:

+
$ go list gopl.io/ch3/...
+gopl.io/ch3/basename1
+gopl.io/ch3/basename2
+gopl.io/ch3/comma
+gopl.io/ch3/mandelbrot
+gopl.io/ch3/netflag
+gopl.io/ch3/printints
+gopl.io/ch3/surface
+
+

或者是和某个主题相关的所有包:

+
$ go list ...xml...
+encoding/xml
+gopl.io/ch7/xmlselect
+
+

go list命令还可以获取每个包完整的元信息,而不仅仅只是导入路径,这些元信息可以以不同格式提供给用户。其中-json命令行参数表示用JSON格式打印每个包的元信息。

+
$ go list -json hash
+{
+	"Dir": "/home/gopher/go/src/hash",
+	"ImportPath": "hash",
+	"Name": "hash",
+	"Doc": "Package hash provides interfaces for hash functions.",
+	"Target": "/home/gopher/go/pkg/darwin_amd64/hash.a",
+	"Goroot": true,
+	"Standard": true,
+	"Root": "/home/gopher/go",
+	"GoFiles": [
+			"hash.go"
+	],
+	"Imports": [
+		"io"
+	],
+	"Deps": [
+		"errors",
+		"io",
+		"runtime",
+		"sync",
+		"sync/atomic",
+		"unsafe"
+	]
+}
+
+

命令行参数-f则允许用户使用text/template包(§4.6)的模板语言定义输出文本的格式。下面的命令将打印strconv包的依赖的包,然后用join模板函数将结果链接为一行,连接时每个结果之间用一个空格分隔:

+
$ go list -f '{{join .Deps " "}}' strconv
+errors math runtime unicode/utf8 unsafe
+
+

{% endraw %}

+

译注:上面的命令在Windows的命令行运行会遇到template: main:1: unclosed action的错误。产生这个错误的原因是因为命令行对命令中的" "参数进行了转义处理。可以按照下面的方法解决转义字符串的问题:

+
$ go list -f "{{join .Deps \" \"}}" strconv
+
+

{% endraw %}

+

下面的命令打印compress子目录下所有包的导入包列表:

+
$ go list -f '{{.ImportPath}} -> {{join .Imports " "}}' compress/...
+compress/bzip2 -> bufio io sort
+compress/flate -> bufio fmt io math sort strconv
+compress/gzip -> bufio compress/flate errors fmt hash hash/crc32 io time
+compress/lzw -> bufio errors fmt io
+compress/zlib -> bufio compress/flate errors fmt hash hash/adler32 io
+
+

{% endraw %}

+

译注:Windows下有同样有问题,要避免转义字符串的干扰:

+
$ go list -f "{{.ImportPath}} -> {{join .Imports \" \"}}" compress/...
+
+

{% endraw %}

+

go list命令对于一次性的交互式查询或自动化构建或测试脚本都很有帮助。我们将在11.2.4节中再次使用它。每个子命令的更多信息,包括可设置的字段和意义,可以用go help list命令查看。

+

在本章,我们解释了Go语言工具中除了测试命令之外的所有重要的子命令。在下一章,我们将看到如何用go test命令去运行Go语言程序中的测试代码。

+

练习 10.4: 创建一个工具,根据命令行指定的参数,报告工作区所有依赖包指定的其它包集合。提示:你需要运行go list命令两次,一次用于初始化包,一次用于所有包。你可能需要用encoding/json(§4.5)包来分析输出的JSON格式的信息。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch10/ch10.html b/ch10/ch10.html new file mode 100644 index 0000000..61ae59b --- /dev/null +++ b/ch10/ch10.html @@ -0,0 +1,241 @@ + + + + + + 包和工具 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

第10章 包和工具

+

现在随便一个小程序的实现都可能包含超过10000个函数。然而作者一般只需要考虑其中很小的一部分和做很少的设计,因为绝大部分代码都是由他人编写的,它们通过类似包或模块的方式被重用。

+

Go语言有超过100个的标准包(译注:可以用go list std | wc -l命令查看标准包的具体数目),标准库为大多数的程序提供了必要的基础构件。在Go的社区,有很多成熟的包被设计、共享、重用和改进,目前互联网上已经发布了非常多的Go语言开源包,它们可以通过 http://godoc.org 检索。在本章,我们将演示如何使用已有的包和创建新的包。

+

Go还自带了工具箱,里面有很多用来简化工作区和包管理的小工具。在本书开始的时候,我们已经见识过如何使用工具箱自带的工具来下载、构建和运行我们的演示程序了。在本章,我们将看看这些工具的基本设计理论和尝试更多的功能,例如打印工作区中包的文档和查询相关的元数据等。在下一章,我们将探讨testing包的单元测试用法。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch11/ch11-01.html b/ch11/ch11-01.html new file mode 100644 index 0000000..b5671c8 --- /dev/null +++ b/ch11/ch11-01.html @@ -0,0 +1,241 @@ + + + + + + go test - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

11.1. go test

+

go test命令是一个按照一定的约定和组织来测试代码的程序。在包目录内,所有以_test.go为后缀名的源文件在执行go build时不会被构建成包的一部分,它们是go test测试的一部分。

+

*_test.go文件中,有三种类型的函数:测试函数、基准测试(benchmark)函数、示例函数。一个测试函数是以Test为函数名前缀的函数,用于测试程序的一些逻辑行为是否正确;go test命令会调用这些测试函数并报告测试结果是PASS或FAIL。基准测试函数是以Benchmark为函数名前缀的函数,它们用于衡量一些函数的性能;go test命令会多次运行基准测试函数以计算一个平均的执行时间。示例函数是以Example为函数名前缀的函数,提供一个由编译器保证正确性的示例文档。我们将在11.2节讨论测试函数的所有细节,并在11.4节讨论基准测试函数的细节,然后在11.6节讨论示例函数的细节。

+

go test命令会遍历所有的*_test.go文件中符合上述命名规则的函数,生成一个临时的main包用于调用相应的测试函数,接着构建并运行、报告测试结果,最后清理测试中生成的临时文件。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch11/ch11-02.html b/ch11/ch11-02.html new file mode 100644 index 0000000..f7e381f --- /dev/null +++ b/ch11/ch11-02.html @@ -0,0 +1,715 @@ + + + + + + 测试函数 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

11.2. 测试函数

+

每个测试函数必须导入testing包。测试函数有如下的签名:

+
func TestName(t *testing.T) {
+	// ...
+}
+
+

测试函数的名字必须以Test开头,可选的后缀名必须以大写字母开头:

+
func TestSin(t *testing.T) { /* ... */ }
+func TestCos(t *testing.T) { /* ... */ }
+func TestLog(t *testing.T) { /* ... */ }
+
+

其中t参数用于报告测试失败和附加的日志信息。让我们定义一个实例包gopl.io/ch11/word1,其中只有一个函数IsPalindrome用于检查一个字符串是否从前向后和从后向前读都是一样的。(下面这个实现对于一个字符串是否是回文字符串前后重复测试了两次;我们稍后会再讨论这个问题。)

+

gopl.io/ch11/word1

+
// Package word provides utilities for word games.
+package word
+
+// IsPalindrome reports whether s reads the same forward and backward.
+// (Our first attempt.)
+func IsPalindrome(s string) bool {
+	for i := range s {
+		if s[i] != s[len(s)-1-i] {
+			return false
+		}
+	}
+	return true
+}
+
+

在相同的目录下,word_test.go测试文件中包含了TestPalindrome和TestNonPalindrome两个测试函数。每一个都是测试IsPalindrome是否给出正确的结果,并使用t.Error报告失败信息:

+
package word
+
+import "testing"
+
+func TestPalindrome(t *testing.T) {
+	if !IsPalindrome("detartrated") {
+		t.Error(`IsPalindrome("detartrated") = false`)
+	}
+	if !IsPalindrome("kayak") {
+		t.Error(`IsPalindrome("kayak") = false`)
+	}
+}
+
+func TestNonPalindrome(t *testing.T) {
+	if IsPalindrome("palindrome") {
+		t.Error(`IsPalindrome("palindrome") = true`)
+	}
+}
+
+

go test命令如果没有参数指定包那么将默认采用当前目录对应的包(和go build命令一样)。我们可以用下面的命令构建和运行测试。

+
$ cd $GOPATH/src/gopl.io/ch11/word1
+$ go test
+ok   gopl.io/ch11/word1  0.008s
+
+

结果还比较满意,我们运行了这个程序, 不过没有提前退出是因为还没有遇到BUG报告。不过一个法国名为“Noelle Eve Elleon”的用户会抱怨IsPalindrome函数不能识别“été”。另外一个来自美国中部用户的抱怨则是不能识别“A man, a plan, a canal: Panama.”。执行特殊和小的BUG报告为我们提供了新的更自然的测试用例。

+
func TestFrenchPalindrome(t *testing.T) {
+	if !IsPalindrome("été") {
+		t.Error(`IsPalindrome("été") = false`)
+	}
+}
+
+func TestCanalPalindrome(t *testing.T) {
+	input := "A man, a plan, a canal: Panama"
+	if !IsPalindrome(input) {
+		t.Errorf(`IsPalindrome(%q) = false`, input)
+	}
+}
+
+

为了避免两次输入较长的字符串,我们使用了提供了有类似Printf格式化功能的 Errorf函数来汇报错误结果。

+

当添加了这两个测试用例之后,go test返回了测试失败的信息。

+
$ go test
+--- FAIL: TestFrenchPalindrome (0.00s)
+    word_test.go:28: IsPalindrome("été") = false
+--- FAIL: TestCanalPalindrome (0.00s)
+    word_test.go:35: IsPalindrome("A man, a plan, a canal: Panama") = false
+FAIL
+FAIL    gopl.io/ch11/word1  0.014s
+
+

先编写测试用例并观察到测试用例触发了和用户报告的错误相同的描述是一个好的测试习惯。只有这样,我们才能定位我们要真正解决的问题。

+

先写测试用例的另外的好处是,运行测试通常会比手工描述报告的处理更快,这让我们可以进行快速地迭代。如果测试集有很多运行缓慢的测试,我们可以通过只选择运行某些特定的测试来加快测试速度。

+

参数-v可用于打印每个测试函数的名字和运行时间:

+
$ go test -v
+=== RUN TestPalindrome
+--- PASS: TestPalindrome (0.00s)
+=== RUN TestNonPalindrome
+--- PASS: TestNonPalindrome (0.00s)
+=== RUN TestFrenchPalindrome
+--- FAIL: TestFrenchPalindrome (0.00s)
+    word_test.go:28: IsPalindrome("été") = false
+=== RUN TestCanalPalindrome
+--- FAIL: TestCanalPalindrome (0.00s)
+    word_test.go:35: IsPalindrome("A man, a plan, a canal: Panama") = false
+FAIL
+exit status 1
+FAIL    gopl.io/ch11/word1  0.017s
+
+

参数-run对应一个正则表达式,只有测试函数名被它正确匹配的测试函数才会被go test测试命令运行:

+
$ go test -v -run="French|Canal"
+=== RUN TestFrenchPalindrome
+--- FAIL: TestFrenchPalindrome (0.00s)
+    word_test.go:28: IsPalindrome("été") = false
+=== RUN TestCanalPalindrome
+--- FAIL: TestCanalPalindrome (0.00s)
+    word_test.go:35: IsPalindrome("A man, a plan, a canal: Panama") = false
+FAIL
+exit status 1
+FAIL    gopl.io/ch11/word1  0.014s
+
+

当然,一旦我们已经修复了失败的测试用例,在我们提交代码更新之前,我们应该以不带参数的go test命令运行全部的测试用例,以确保修复失败测试的同时没有引入新的问题。

+

我们现在的任务就是修复这些错误。简要分析后发现第一个BUG的原因是我们采用了 byte而不是rune序列,所以像“été”中的é等非ASCII字符不能正确处理。第二个BUG是因为没有忽略空格和字母的大小写导致的。

+

针对上述两个BUG,我们仔细重写了函数:

+

gopl.io/ch11/word2

+
// Package word provides utilities for word games.
+package word
+
+import "unicode"
+
+// IsPalindrome reports whether s reads the same forward and backward.
+// Letter case is ignored, as are non-letters.
+func IsPalindrome(s string) bool {
+	var letters []rune
+	for _, r := range s {
+		if unicode.IsLetter(r) {
+			letters = append(letters, unicode.ToLower(r))
+		}
+	}
+	for i := range letters {
+		if letters[i] != letters[len(letters)-1-i] {
+			return false
+		}
+	}
+	return true
+}
+
+

同时我们也将之前的所有测试数据合并到了一个测试中的表格中。

+
func TestIsPalindrome(t *testing.T) {
+	var tests = []struct {
+		input string
+		want  bool
+	}{
+		{"", true},
+		{"a", true},
+		{"aa", true},
+		{"ab", false},
+		{"kayak", true},
+		{"detartrated", true},
+		{"A man, a plan, a canal: Panama", true},
+		{"Evil I did dwell; lewd did I live.", true},
+		{"Able was I ere I saw Elba", true},
+		{"été", true},
+		{"Et se resservir, ivresse reste.", true},
+		{"palindrome", false}, // non-palindrome
+		{"desserts", false},   // semi-palindrome
+	}
+	for _, test := range tests {
+		if got := IsPalindrome(test.input); got != test.want {
+			t.Errorf("IsPalindrome(%q) = %v", test.input, got)
+		}
+	}
+}
+
+

现在我们的新测试都通过了:

+
$ go test gopl.io/ch11/word2
+ok      gopl.io/ch11/word2      0.015s
+
+

这种表格驱动的测试在Go语言中很常见。我们可以很容易地向表格添加新的测试数据,并且后面的测试逻辑也没有冗余,这样我们可以有更多的精力去完善错误信息。

+

失败测试的输出并不包括调用t.Errorf时刻的堆栈调用信息。和其他编程语言或测试框架的assert断言不同,t.Errorf调用也没有引起panic异常或停止测试的执行。即使表格中前面的数据导致了测试的失败,表格后面的测试数据依然会运行测试,因此在一个测试中我们可能了解多个失败的信息。

+

如果我们真的需要停止测试,或许是因为初始化失败或可能是早先的错误导致了后续错误等原因,我们可以使用t.Fatal或t.Fatalf停止当前测试函数。它们必须在和测试函数同一个goroutine内调用。

+

测试失败的信息一般的形式是“f(x) = y, want z”,其中f(x)解释了失败的操作和对应的输入,y是实际的运行结果,z是期望的正确的结果。就像前面检查回文字符串的例子,实际的函数用于f(x)部分。显示x是表格驱动型测试中比较重要的部分,因为同一个断言可能对应不同的表格项执行多次。要避免无用和冗余的信息。在测试类似IsPalindrome返回布尔类型的函数时,可以忽略并没有额外信息的z部分。如果x、y或z是y的长度,输出一个相关部分的简明总结即可。测试的作者应该要努力帮助程序员诊断测试失败的原因。

+

练习 11.1: 为4.3节中的charcount程序编写测试。

+

练习 11.2: 为(§6.5)的IntSet编写一组测试,用于检查每个操作后的行为和基于内置map的集合等价,后面练习11.7将会用到。

+

11.2.1. 随机测试

+

表格驱动的测试便于构造基于精心挑选的测试数据的测试用例。另一种测试思路是随机测试,也就是通过构造更广泛的随机输入来测试探索函数的行为。

+

那么对于一个随机的输入,我们如何能知道希望的输出结果呢?这里有两种处理策略。第一个是编写另一个对照函数,使用简单和清晰的算法,虽然效率较低但是行为和要测试的函数是一致的,然后针对相同的随机输入检查两者的输出结果。第二种是生成的随机输入的数据遵循特定的模式,这样我们就可以知道期望的输出的模式。

+

下面的例子使用的是第二种方法:randomPalindrome函数用于随机生成回文字符串。

+
import "math/rand"
+
+// randomPalindrome returns a palindrome whose length and contents
+// are derived from the pseudo-random number generator rng.
+func randomPalindrome(rng *rand.Rand) string {
+	n := rng.Intn(25) // random length up to 24
+	runes := make([]rune, n)
+	for i := 0; i < (n+1)/2; i++ {
+		r := rune(rng.Intn(0x1000)) // random rune up to '\u0999'
+		runes[i] = r
+		runes[n-1-i] = r
+	}
+	return string(runes)
+}
+
+func TestRandomPalindromes(t *testing.T) {
+	// Initialize a pseudo-random number generator.
+	seed := time.Now().UTC().UnixNano()
+	t.Logf("Random seed: %d", seed)
+	rng := rand.New(rand.NewSource(seed))
+
+	for i := 0; i < 1000; i++ {
+		p := randomPalindrome(rng)
+		if !IsPalindrome(p) {
+			t.Errorf("IsPalindrome(%q) = false", p)
+		}
+	}
+}
+
+

虽然随机测试会有不确定因素,但是它也是至关重要的,我们可以从失败测试的日志获取足够的信息。在我们的例子中,输入IsPalindrome的p参数将告诉我们真实的数据,但是对于函数将接受更复杂的输入,不需要保存所有的输入,只要日志中简单地记录随机数种子即可(像上面的方式)。有了这些随机数初始化种子,我们可以很容易修改测试代码以重现失败的随机测试。

+

通过使用当前时间作为随机种子,在整个过程中的每次运行测试命令时都将探索新的随机数据。如果你使用的是定期运行的自动化测试集成系统,随机测试将特别有价值。

+

练习 11.3: TestRandomPalindromes测试函数只测试了回文字符串。编写新的随机测试生成器,用于测试随机生成的非回文字符串。

+

练习 11.4: 修改randomPalindrome函数,以探索IsPalindrome是否对标点和空格做了正确处理。

+

译者注:拓展阅读感兴趣的读者可以再了解一下go-fuzz

+

11.2.2. 测试一个命令

+

对于测试包go test是一个有用的工具,但是稍加努力我们也可以用它来测试可执行程序。如果一个包的名字是 main,那么在构建时会生成一个可执行程序,不过main包可以作为一个包被测试器代码导入。

+

让我们为2.3.2节的echo程序编写一个测试。我们先将程序拆分为两个函数:echo函数完成真正的工作,main函数用于处理命令行输入参数和echo可能返回的错误。

+

gopl.io/ch11/echo

+
// Echo prints its command-line arguments.
+package main
+
+import (
+	"flag"
+	"fmt"
+	"io"
+	"os"
+	"strings"
+)
+
+var (
+	n = flag.Bool("n", false, "omit trailing newline")
+	s = flag.String("s", " ", "separator")
+)
+
+var out io.Writer = os.Stdout // modified during testing
+
+func main() {
+	flag.Parse()
+	if err := echo(!*n, *s, flag.Args()); err != nil {
+		fmt.Fprintf(os.Stderr, "echo: %v\n", err)
+		os.Exit(1)
+	}
+}
+
+func echo(newline bool, sep string, args []string) error {
+	fmt.Fprint(out, strings.Join(args, sep))
+	if newline {
+		fmt.Fprintln(out)
+	}
+	return nil
+}
+
+

在测试中我们可以用各种参数和标志调用echo函数,然后检测它的输出是否正确,我们通过增加参数来减少echo函数对全局变量的依赖。我们还增加了一个全局名为out的变量来替代直接使用os.Stdout,这样测试代码可以根据需要将out修改为不同的对象以便于检查。下面就是echo_test.go文件中的测试代码:

+
package main
+
+import (
+	"bytes"
+	"fmt"
+	"testing"
+)
+
+func TestEcho(t *testing.T) {
+	var tests = []struct {
+		newline bool
+		sep     string
+		args    []string
+		want    string
+	}{
+		{true, "", []string{}, "\n"},
+		{false, "", []string{}, ""},
+		{true, "\t", []string{"one", "two", "three"}, "one\ttwo\tthree\n"},
+		{true, ",", []string{"a", "b", "c"}, "a,b,c\n"},
+		{false, ":", []string{"1", "2", "3"}, "1:2:3"},
+	}
+	for _, test := range tests {
+		descr := fmt.Sprintf("echo(%v, %q, %q)",
+			test.newline, test.sep, test.args)
+
+		out = new(bytes.Buffer) // captured output
+		if err := echo(test.newline, test.sep, test.args); err != nil {
+			t.Errorf("%s failed: %v", descr, err)
+			continue
+		}
+		got := out.(*bytes.Buffer).String()
+		if got != test.want {
+			t.Errorf("%s = %q, want %q", descr, got, test.want)
+		}
+	}
+}
+
+

要注意的是测试代码和产品代码在同一个包。虽然是main包,也有对应的main入口函数,但是在测试的时候main包只是TestEcho测试函数导入的一个普通包,里面main函数并没有被导出,而是被忽略的。

+

通过将测试放到表格中,我们很容易添加新的测试用例。让我通过增加下面的测试用例来看看失败的情况是怎么样的:

+
{true, ",", []string{"a", "b", "c"}, "a b c\n"}, // NOTE: wrong expectation!
+
+

go test输出如下:

+
$ go test gopl.io/ch11/echo
+--- FAIL: TestEcho (0.00s)
+    echo_test.go:31: echo(true, ",", ["a" "b" "c"]) = "a,b,c", want "a b c\n"
+FAIL
+FAIL        gopl.io/ch11/echo         0.006s
+
+

错误信息描述了尝试的操作(使用Go类似语法),实际的结果和期望的结果。通过这样的错误信息,你可以在检视代码之前就很容易定位错误的原因。

+

要注意的是在测试代码中并没有调用log.Fatal或os.Exit,因为调用这类函数会导致程序提前退出;调用这些函数的特权应该放在main函数中。如果真的有意外的事情导致函数发生panic异常,测试驱动应该尝试用recover捕获异常,然后将当前测试当作失败处理。如果是可预期的错误,例如非法的用户输入、找不到文件或配置文件不当等应该通过返回一个非空的error的方式处理。幸运的是(上面的意外只是一个插曲),我们的echo示例是比较简单的也没有需要返回非空error的情况。

+

11.2.3. 白盒测试

+

一种测试分类的方法是基于测试者是否需要了解被测试对象的内部工作原理。黑盒测试只需要测试包公开的文档和API行为,内部实现对测试代码是透明的。相反,白盒测试有访问包内部函数和数据结构的权限,因此可以做到一些普通客户端无法实现的测试。例如,一个白盒测试可以在每个操作之后检测不变量的数据类型。(白盒测试只是一个传统的名称,其实称为clear box测试会更准确。)

+

黑盒和白盒这两种测试方法是互补的。黑盒测试一般更健壮,随着软件实现的完善测试代码很少需要更新。它们可以帮助测试者了解真实客户的需求,也可以帮助发现API设计的一些不足之处。相反,白盒测试则可以对内部一些棘手的实现提供更多的测试覆盖。

+

我们已经看到两种测试的例子。TestIsPalindrome测试仅仅使用导出的IsPalindrome函数,因此这是一个黑盒测试。TestEcho测试则调用了内部的echo函数,并且更新了内部的out包级变量,这两个都是未导出的,因此这是白盒测试。

+

当我们准备TestEcho测试的时候,我们修改了echo函数使用包级的out变量作为输出对象,因此测试代码可以用另一个实现代替标准输出,这样可以方便对比echo输出的数据。使用类似的技术,我们可以将产品代码的其他部分也替换为一个容易测试的伪对象。使用伪对象的好处是我们可以方便配置,容易预测,更可靠,也更容易观察。同时也可以避免一些不良的副作用,例如更新生产数据库或信用卡消费行为。

+

下面的代码演示了为用户提供网络存储的web服务中的配额检测逻辑。当用户使用了超过90%的存储配额之后将发送提醒邮件。(译注:一般在实现业务机器监控,包括磁盘、cpu、网络等的时候,需要类似的到达阈值=>触发报警的逻辑,所以是很实用的案例。)

+

gopl.io/ch11/storage1

+
package storage
+
+import (
+	"fmt"
+	"log"
+	"net/smtp"
+)
+
+func bytesInUse(username string) int64 { return 0 /* ... */ }
+
+// Email sender configuration.
+// NOTE: never put passwords in source code!
+const sender = "notifications@example.com"
+const password = "correcthorsebatterystaple"
+const hostname = "smtp.example.com"
+
+const template = `Warning: you are using %d bytes of storage,
+%d%% of your quota.`
+
+func CheckQuota(username string) {
+	used := bytesInUse(username)
+	const quota = 1000000000 // 1GB
+	percent := 100 * used / quota
+	if percent < 90 {
+		return // OK
+	}
+	msg := fmt.Sprintf(template, used, percent)
+	auth := smtp.PlainAuth("", sender, password, hostname)
+	err := smtp.SendMail(hostname+":587", auth, sender,
+		[]string{username}, []byte(msg))
+	if err != nil {
+		log.Printf("smtp.SendMail(%s) failed: %s", username, err)
+	}
+}
+
+

我们想测试这段代码,但是我们并不希望发送真实的邮件。因此我们将邮件处理逻辑放到一个私有的notifyUser函数中。

+

gopl.io/ch11/storage2

+
var notifyUser = func(username, msg string) {
+	auth := smtp.PlainAuth("", sender, password, hostname)
+	err := smtp.SendMail(hostname+":587", auth, sender,
+		[]string{username}, []byte(msg))
+	if err != nil {
+		log.Printf("smtp.SendEmail(%s) failed: %s", username, err)
+	}
+}
+
+func CheckQuota(username string) {
+	used := bytesInUse(username)
+	const quota = 1000000000 // 1GB
+	percent := 100 * used / quota
+	if percent < 90 {
+		return // OK
+	}
+	msg := fmt.Sprintf(template, used, percent)
+	notifyUser(username, msg)
+}
+
+

现在我们可以在测试中用伪邮件发送函数替代真实的邮件发送函数。它只是简单记录要通知的用户和邮件的内容。

+
package storage
+
+import (
+	"strings"
+	"testing"
+)
+func TestCheckQuotaNotifiesUser(t *testing.T) {
+	var notifiedUser, notifiedMsg string
+	notifyUser = func(user, msg string) {
+		notifiedUser, notifiedMsg = user, msg
+	}
+
+	// ...simulate a 980MB-used condition...
+
+	const user = "joe@example.org"
+	CheckQuota(user)
+	if notifiedUser == "" && notifiedMsg == "" {
+		t.Fatalf("notifyUser not called")
+	}
+	if notifiedUser != user {
+		t.Errorf("wrong user (%s) notified, want %s",
+			notifiedUser, user)
+	}
+	const wantSubstring = "98% of your quota"
+	if !strings.Contains(notifiedMsg, wantSubstring) {
+		t.Errorf("unexpected notification message <<%s>>, "+
+			"want substring %q", notifiedMsg, wantSubstring)
+	}
+}
+
+

这里有一个问题:当测试函数返回后,CheckQuota将不能正常工作,因为notifyUsers依然使用的是测试函数的伪发送邮件函数(当更新全局对象的时候总会有这种风险)。 我们必须修改测试代码恢复notifyUsers原先的状态以便后续其他的测试没有影响,要确保所有的执行路径后都能恢复,包括测试失败或panic异常的情形。在这种情况下,我们建议使用defer语句来延后执行处理恢复的代码。

+
func TestCheckQuotaNotifiesUser(t *testing.T) {
+	// Save and restore original notifyUser.
+	saved := notifyUser
+	defer func() { notifyUser = saved }()
+
+	// Install the test's fake notifyUser.
+	var notifiedUser, notifiedMsg string
+	notifyUser = func(user, msg string) {
+		notifiedUser, notifiedMsg = user, msg
+	}
+	// ...rest of test...
+}
+
+

这种处理模式可以用来暂时保存和恢复所有的全局变量,包括命令行标志参数、调试选项和优化参数;安装和移除导致生产代码产生一些调试信息的钩子函数;还有有些诱导生产代码进入某些重要状态的改变,比如超时、错误,甚至是一些刻意制造的并发行为等因素。

+

以这种方式使用全局变量是安全的,因为go test命令并不会同时并发地执行多个测试。

+

11.2.4. 外部测试包

+

考虑下这两个包:net/url包,提供了URL解析的功能;net/http包,提供了web服务和HTTP客户端的功能。如我们所料,上层的net/http包依赖下层的net/url包。然后,net/url包中的一个测试是演示不同URL和HTTP客户端的交互行为。也就是说,一个下层包的测试代码导入了上层的包。

+

+

这样的行为在net/url包的测试代码中会导致包的循环依赖,正如图11.1中向上箭头所示,同时正如我们在10.1节所讲的,Go语言规范是禁止包的循环依赖的。

+

不过我们可以通过外部测试包的方式解决循环依赖的问题,也就是在net/url包所在的目录声明一个独立的url_test测试包。其中包名的_test后缀告诉go test工具它应该建立一个额外的包来运行测试。我们将这个外部测试包的导入路径视作是net/url_test会更容易理解,但实际上它并不能被其他任何包导入。

+

因为外部测试包是一个独立的包,所以能够导入那些依赖待测代码本身的其他辅助包;包内的测试代码就无法做到这点。在设计层面,外部测试包是在所有它依赖的包的上层,正如图11.2所示。

+

+

通过避免循环的导入依赖,外部测试包可以更灵活地编写测试,特别是集成测试(需要测试多个组件之间的交互),可以像普通应用程序那样自由地导入其他包。

+

我们可以用go list命令查看包对应目录中哪些Go源文件是产品代码,哪些是包内测试,还有哪些是外部测试包。我们以fmt包作为一个例子:GoFiles表示产品代码对应的Go源文件列表;也就是go build命令要编译的部分。

+
$ go list -f={{.GoFiles}} fmt
+[doc.go format.go print.go scan.go]
+
+

{% endraw %}

+

TestGoFiles表示的是fmt包内部测试代码,以_test.go为后缀文件名,不过只在测试时被构建:

+
$ go list -f={{.TestGoFiles}} fmt
+[export_test.go]
+
+

{% endraw %}

+

包的测试代码通常都在这些文件中,不过fmt包并非如此;稍后我们再解释export_test.go文件的作用。

+

XTestGoFiles表示的是属于外部测试包的测试代码,也就是fmt_test包,因此它们必须先导入fmt包。同样,这些文件也只是在测试时被构建运行:

+
$ go list -f={{.XTestGoFiles}} fmt
+[fmt_test.go scan_test.go stringer_test.go]
+
+

{% endraw %}

+

有时候外部测试包也需要访问被测试包内部的代码,例如在一个为了避免循环导入而被独立到外部测试包的白盒测试。在这种情况下,我们可以通过一些技巧解决:我们在包内的一个_test.go文件中导出一个内部的实现给外部测试包。因为这些代码只有在测试时才需要,因此一般会放在export_test.go文件中。

+

例如,fmt包的fmt.Scanf函数需要unicode.IsSpace函数提供的功能。但是为了避免太多的依赖,fmt包并没有导入包含巨大表格数据的unicode包;相反fmt包有一个叫isSpace内部的简易实现。

+

为了确保fmt.isSpace和unicode.IsSpace函数的行为保持一致,fmt包谨慎地包含了一个测试。一个在外部测试包内的白盒测试,是无法直接访问到isSpace内部函数的,因此fmt通过一个后门导出了isSpace函数。export_test.go文件就是专门用于外部测试包的后门。

+
package fmt
+
+var IsSpace = isSpace
+
+

这个测试文件并没有定义测试代码;它只是通过fmt.IsSpace简单导出了内部的isSpace函数,提供给外部测试包使用。这个技巧可以广泛用于位于外部测试包的白盒测试。

+

11.2.5. 编写有效的测试

+

许多Go语言新人会惊异于Go语言极简的测试框架。很多其它语言的测试框架都提供了识别测试函数的机制(通常使用反射或元数据),通过设置一些“setup”和“teardown”的钩子函数来执行测试用例运行的初始化和之后的清理操作,同时测试工具箱还提供了很多类似assert断言、值比较函数、格式化输出错误信息和停止一个失败的测试等辅助函数(通常使用异常机制)。虽然这些机制可以使得测试非常简洁,但是测试输出的日志却会像火星文一般难以理解。此外,虽然测试最终也会输出PASS或FAIL的报告,但是它们提供的信息格式却非常不利于代码维护者快速定位问题,因为失败信息的具体含义非常隐晦,比如“assert: 0 == 1”或成页的海量跟踪日志。

+

Go语言的测试风格则形成鲜明对比。它期望测试者自己完成大部分的工作,定义函数避免重复,就像普通编程那样。编写测试并不是一个机械的填空过程;一个测试也有自己的接口,尽管它的维护者也是测试仅有的一个用户。一个好的测试不应该引发其他无关的错误信息,它只要清晰简洁地描述问题的症状即可,有时候可能还需要一些上下文信息。在理想情况下,维护者可以在不看代码的情况下就能根据错误信息定位错误产生的原因。一个好的测试不应该在遇到一点小错误时就立刻退出测试,它应该尝试报告更多的相关的错误信息,因为我们可能从多个失败测试的模式中发现错误产生的规律。

+

下面的断言函数比较两个值,然后生成一个通用的错误信息,并停止程序。它很好用也确实有效,但是当测试失败的时候,打印的错误信息却几乎是没有价值的。它并没有为快速解决问题提供一个很好的入口。

+
import (
+	"fmt"
+	"strings"
+	"testing"
+)
+// A poor assertion function.
+func assertEqual(x, y int) {
+	if x != y {
+		panic(fmt.Sprintf("%d != %d", x, y))
+	}
+}
+func TestSplit(t *testing.T) {
+	words := strings.Split("a:b:c", ":")
+	assertEqual(len(words), 3)
+	// ...
+}
+
+

从这个意义上说,断言函数犯了过早抽象的错误:仅仅测试两个整数是否相同,而没能根据上下文提供更有意义的错误信息。我们可以根据具体的错误打印一个更有价值的错误信息,就像下面例子那样。只有在测试中出现重复模式时才采用抽象。

+
func TestSplit(t *testing.T) {
+	s, sep := "a:b:c", ":"
+	words := strings.Split(s, sep)
+	if got, want := len(words), 3; got != want {
+		t.Errorf("Split(%q, %q) returned %d words, want %d",
+			s, sep, got, want)
+	}
+	// ...
+}
+
+

现在的测试不仅报告了调用的具体函数、它的输入和结果的意义;并且打印的真实返回的值和期望返回的值;并且即使断言失败依然会继续尝试运行更多的测试。一旦我们写了这样结构的测试,下一步自然不是用更多的if语句来扩展测试用例,我们可以用像IsPalindrome的表驱动测试那样来准备更多的s和sep测试用例。

+

前面的例子并不需要额外的辅助函数,如果有可以使测试代码更简单的方法我们也乐意接受。(我们将在13.3节看到一个类似reflect.DeepEqual辅助函数。)一个好的测试的关键是首先实现你期望的具体行为,然后才是考虑简化测试代码、避免重复。如果直接从抽象、通用的测试库着手,很难取得良好结果。

+

练习11.5: 用表格驱动的技术扩展TestSplit测试,并打印期望的输出结果。

+

11.2.6. 避免脆弱的测试

+

如果一个应用程序对于新出现的但有效的输入经常失败说明程序容易出bug(不够稳健);同样,如果一个测试仅仅对程序做了微小变化就失败则称为脆弱。就像一个不够稳健的程序会挫败它的用户一样,一个脆弱的测试同样会激怒它的维护者。最脆弱的测试代码会在程序没有任何变化的时候产生不同的结果,时好时坏,处理它们会耗费大量的时间但是并不会得到任何好处。

+

当一个测试函数会产生一个复杂的输出如一个很长的字符串、一个精心设计的数据结构或一个文件时,人们很容易想预先写下一系列固定的用于对比的标杆数据。但是随着项目的发展,有些输出可能会发生变化,尽管很可能是一个改进的实现导致的。而且不仅仅是输出部分,函数复杂的输入部分可能也跟着变化了,因此测试使用的输入也就不再有效了。

+

避免脆弱测试代码的方法是只检测你真正关心的属性。保持测试代码的简洁和内部结构的稳定。特别是对断言部分要有所选择。不要对字符串进行全字匹配,而是针对那些在项目的发展中是比较稳定不变的子串。很多时候值得花力气来编写一个从复杂输出中提取用于断言的必要信息的函数,虽然这可能会带来很多前期的工作,但是它可以帮助迅速及时修复因为项目演化而导致的不合逻辑的失败测试。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch11/ch11-03.html b/ch11/ch11-03.html new file mode 100644 index 0000000..51550ff --- /dev/null +++ b/ch11/ch11-03.html @@ -0,0 +1,310 @@ + + + + + + 测试覆盖率 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

11.3. 测试覆盖率

+

就其性质而言,测试不可能是完整的。计算机科学家Edsger Dijkstra曾说过:“测试能证明缺陷存在,而无法证明没有缺陷。”再多的测试也不能证明一个程序没有BUG。在最好的情况下,测试可以增强我们的信心:代码在很多重要场景下是可以正常工作的。

+

对待测程序执行的测试的程度称为测试的覆盖率。测试覆盖率并不能量化——即使最简单的程序的动态也是难以精确测量的——但是有启发式方法来帮助我们编写有效的测试代码。

+

这些启发式方法中,语句的覆盖率是最简单和最广泛使用的。语句的覆盖率是指在测试中至少被运行一次的代码占总代码数的比例。在本节中,我们使用go test命令中集成的测试覆盖率工具,来度量下面代码的测试覆盖率,帮助我们识别测试和我们期望间的差距。

+

下面的代码是一个表格驱动的测试,用于测试第七章的表达式求值程序:

+

gopl.io/ch7/eval

+
func TestCoverage(t *testing.T) {
+	var tests = []struct {
+		input string
+		env   Env
+		want  string // expected error from Parse/Check or result from Eval
+	}{
+		{"x % 2", nil, "unexpected '%'"},
+		{"!true", nil, "unexpected '!'"},
+		{"log(10)", nil, `unknown function "log"`},
+		{"sqrt(1, 2)", nil, "call to sqrt has 2 args, want 1"},
+		{"sqrt(A / pi)", Env{"A": 87616, "pi": math.Pi}, "167"},
+		{"pow(x, 3) + pow(y, 3)", Env{"x": 9, "y": 10}, "1729"},
+		{"5 / 9 * (F - 32)", Env{"F": -40}, "-40"},
+	}
+
+	for _, test := range tests {
+		expr, err := Parse(test.input)
+		if err == nil {
+			err = expr.Check(map[Var]bool{})
+		}
+		if err != nil {
+			if err.Error() != test.want {
+				t.Errorf("%s: got %q, want %q", test.input, err, test.want)
+			}
+			continue
+		}
+		got := fmt.Sprintf("%.6g", expr.Eval(test.env))
+		if got != test.want {
+			t.Errorf("%s: %v => %s, want %s",
+				test.input, test.env, got, test.want)
+		}
+	}
+}
+
+

首先,我们要确保所有的测试都正常通过:

+
$ go test -v -run=Coverage gopl.io/ch7/eval
+=== RUN TestCoverage
+--- PASS: TestCoverage (0.00s)
+PASS
+ok      gopl.io/ch7/eval         0.011s
+
+

下面这个命令可以显示测试覆盖率工具的使用用法:

+
$ go tool cover
+Usage of 'go tool cover':
+Given a coverage profile produced by 'go test':
+    go test -coverprofile=c.out
+
+Open a web browser displaying annotated source code:
+    go tool cover -html=c.out
+...
+
+

go tool命令运行Go工具链的底层可执行程序。这些底层可执行程序放在$GOROOT/pkg/tool/${GOOS}_${GOARCH}目录。因为有go build命令的原因,我们很少直接调用这些底层工具。

+

现在我们可以用-coverprofile标志参数重新运行测试:

+
$ go test -run=Coverage -coverprofile=c.out gopl.io/ch7/eval
+ok      gopl.io/ch7/eval         0.032s      coverage: 68.5% of statements
+
+

这个标志参数通过在测试代码中插入生成钩子来统计覆盖率数据。也就是说,在运行每个测试前,它将待测代码拷贝一份并做修改,在每个词法块都会设置一个布尔标志变量。当被修改后的被测试代码运行退出时,将统计日志数据写入c.out文件,并打印一部分执行的语句的一个总结。(如果你需要的是摘要,使用go test -cover。)

+

如果使用了-covermode=count标志参数,那么将在每个代码块插入一个计数器而不是布尔标志量。在统计结果中记录了每个块的执行次数,这可以用于衡量哪些是被频繁执行的热点代码。

+

为了收集数据,我们运行了测试覆盖率工具,打印了测试日志,生成一个HTML报告,然后在浏览器中打开(图11.3)。

+
$ go tool cover -html=c.out
+
+

+

绿色的代码块被测试覆盖到了,红色的则表示没有被覆盖到。为了清晰起见,我们将背景红色文本的背景设置成了阴影效果。我们可以马上发现unary操作的Eval方法并没有被执行到。如果我们针对这部分未被覆盖的代码添加下面的测试用例,然后重新运行上面的命令,那么我们将会看到那个红色部分的代码也变成绿色了:

+
{"-x * -x", eval.Env{"x": 2}, "4"}
+
+

不过两个panic语句依然是红色的。这是没有问题的,因为这两个语句并不会被执行到。

+

实现100%的测试覆盖率听起来很美,但是在具体实践中通常是不可行的,也不是值得推荐的做法。因为那只能说明代码被执行过而已,并不意味着代码就是没有BUG的;因为对于逻辑复杂的语句需要针对不同的输入执行多次。有一些语句,例如上面的panic语句则永远都不会被执行到。另外,还有一些隐晦的错误在现实中很少遇到也很难编写对应的测试代码。测试从本质上来说是一个比较务实的工作,编写测试代码和编写应用代码的成本对比是需要考虑的。测试覆盖率工具可以帮助我们快速识别测试薄弱的地方,但是设计好的测试用例和编写应用代码一样需要严密的思考。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch11/ch11-04.html b/ch11/ch11-04.html new file mode 100644 index 0000000..ea8d0fd --- /dev/null +++ b/ch11/ch11-04.html @@ -0,0 +1,309 @@ + + + + + + 基准测试 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

11.4. 基准测试

+

基准测试是测量一个程序在固定工作负载下的性能。在Go语言中,基准测试函数和普通测试函数写法类似,但是以Benchmark为前缀名,并且带有一个*testing.B类型的参数;*testing.B参数除了提供和*testing.T类似的方法,还有额外一些和性能测量相关的方法。它还提供了一个整数N,用于指定操作执行的循环次数。

+

下面是IsPalindrome函数的基准测试,其中循环将执行N次。

+
import "testing"
+
+func BenchmarkIsPalindrome(b *testing.B) {
+	for i := 0; i < b.N; i++ {
+		IsPalindrome("A man, a plan, a canal: Panama")
+	}
+}
+
+

我们用下面的命令运行基准测试。和普通测试不同的是,默认情况下不运行任何基准测试。我们需要通过-bench命令行标志参数手工指定要运行的基准测试函数。该参数是一个正则表达式,用于匹配要执行的基准测试函数的名字,默认值是空的。其中“.”模式将可以匹配所有基准测试函数,但因为这里只有一个基准测试函数,因此和-bench=IsPalindrome参数是等价的效果。

+
$ cd $GOPATH/src/gopl.io/ch11/word2
+$ go test -bench=.
+PASS
+BenchmarkIsPalindrome-8 1000000                1035 ns/op
+ok      gopl.io/ch11/word2      2.179s
+
+

结果中基准测试名的数字后缀部分,这里是8,表示运行时对应的GOMAXPROCS的值,这对于一些与并发相关的基准测试是重要的信息。

+

报告显示每次调用IsPalindrome函数花费1.035微秒,是执行1,000,000次的平均时间。因为基准测试驱动器开始时并不知道每个基准测试函数运行所花的时间,它会尝试在真正运行基准测试前先尝试用较小的N运行测试来估算基准测试函数所需要的时间,然后推断一个较大的时间保证稳定的测量结果。

+

循环在基准测试函数内实现,而不是放在基准测试框架内实现,这样可以让每个基准测试函数有机会在循环启动前执行初始化代码,这样并不会显著影响每次迭代的平均运行时间。如果还是担心初始化代码部分对测量时间带来干扰,那么可以通过testing.B参数提供的方法来临时关闭或重置计时器,不过这些一般很少会用到。

+

现在我们有了一个基准测试和普通测试,我们可以很容易测试改进程序运行速度的想法。也许最明显的优化是在IsPalindrome函数中第二个循环的停止检查,这样可以避免每个比较都做两次:

+
n := len(letters)/2
+for i := 0; i < n; i++ {
+	if letters[i] != letters[len(letters)-1-i] {
+		return false
+	}
+}
+return true
+
+

不过很多情况下,一个显而易见的优化未必能带来预期的效果。这个改进在基准测试中只带来了4%的性能提升。

+
$ go test -bench=.
+PASS
+BenchmarkIsPalindrome-8 1000000              992 ns/op
+ok      gopl.io/ch11/word2      2.093s
+
+

另一个改进想法是在开始为每个字符预先分配一个足够大的数组,这样就可以避免在append调用时可能会导致内存的多次重新分配。声明一个letters数组变量,并指定合适的大小,像下面这样,

+
letters := make([]rune, 0, len(s))
+for _, r := range s {
+	if unicode.IsLetter(r) {
+		letters = append(letters, unicode.ToLower(r))
+	}
+}
+
+

这个改进提升性能约35%,报告结果是基于2,000,000次迭代的平均运行时间统计。

+
$ go test -bench=.
+PASS
+BenchmarkIsPalindrome-8 2000000                      697 ns/op
+ok      gopl.io/ch11/word2      1.468s
+
+

如这个例子所示,快的程序往往是伴随着较少的内存分配。-benchmem命令行标志参数将在报告中包含内存的分配数据统计。我们可以比较优化前后内存的分配情况:

+
$ go test -bench=. -benchmem
+PASS
+BenchmarkIsPalindrome    1000000   1026 ns/op    304 B/op  4 allocs/op
+
+

这是优化之后的结果:

+
$ go test -bench=. -benchmem
+PASS
+BenchmarkIsPalindrome    2000000    807 ns/op    128 B/op  1 allocs/op
+
+

用一次内存分配代替多次的内存分配节省了75%的分配调用次数和减少近一半的内存需求。

+

这个基准测试告诉了我们某个具体操作所需的绝对时间,但我们往往想知道的是两个不同的操作的时间对比。例如,如果一个函数需要1ms处理1,000个元素,那么处理10000或1百万将需要多少时间呢?这样的比较揭示了渐近增长函数的运行时间。另一个例子:I/O缓存该设置为多大呢?基准测试可以帮助我们选择在性能达标情况下所需的最小内存。第三个例子:对于一个确定的工作哪种算法更好?基准测试可以评估两种不同算法对于相同的输入在不同的场景和负载下的优缺点。

+

比较型的基准测试就是普通程序代码。它们通常是单参数的函数,由几个不同数量级的基准测试函数调用,就像这样:

+
func benchmark(b *testing.B, size int) { /* ... */ }
+func Benchmark10(b *testing.B)         { benchmark(b, 10) }
+func Benchmark100(b *testing.B)        { benchmark(b, 100) }
+func Benchmark1000(b *testing.B)       { benchmark(b, 1000) }
+
+

通过函数参数来指定输入的大小,但是参数变量对于每个具体的基准测试都是固定的。要避免直接修改b.N来控制输入的大小。除非你将它作为一个固定大小的迭代计算输入,否则基准测试的结果将毫无意义。

+

比较型的基准测试反映出的模式在程序设计阶段是很有帮助的,但是即使程序完工了也应当保留基准测试代码。因为随着项目的发展,或者是输入的增加,或者是部署到新的操作系统或不同的处理器,我们可以再次用基准测试来帮助我们改进设计。

+

练习 11.6: 为2.6.2节的练习2.4和练习2.5的PopCount函数编写基准测试。看看基于表格算法在不同情况下对提升性能会有多大帮助。

+

练习 11.7:*IntSet(§6.5)的Add、UnionWith和其他方法编写基准测试,使用大量随机输入。你可以让这些方法跑多快?选择字的大小对于性能的影响如何?IntSet和基于内建map的实现相比有多快?

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch11/ch11-05.html b/ch11/ch11-05.html new file mode 100644 index 0000000..f2ff024 --- /dev/null +++ b/ch11/ch11-05.html @@ -0,0 +1,284 @@ + + + + + + 剖析 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

11.5. 剖析

+

基准测试(Benchmark)对于衡量特定操作的性能是有帮助的,但是当我们试图让程序跑的更快的时候,我们通常并不知道从哪里开始优化。每个码农都应该知道Donald Knuth在1974年的“Structured Programming with go to Statements”上所说的格言。虽然经常被解读为不重视性能的意思,但是从原文我们可以看到不同的含义:

+
+

毫无疑问,对效率的片面追求会导致各种滥用。程序员会浪费大量的时间在非关键程序的速度上,实际上这些尝试提升效率的行为反倒可能产生很大的负面影响,特别是当调试和维护的时候。我们不应该过度纠结于细节的优化,应该说约97%的场景:过早的优化是万恶之源。

+

当然我们也不应该放弃对那关键3%的优化。一个好的程序员不会因为这个比例小就裹足不前,他们会明智地观察和识别哪些是关键的代码;但是仅当关键代码已经被确认的前提下才会进行优化。对于很多程序员来说,判断哪部分是关键的性能瓶颈,是很容易犯经验上的错误的,因此一般应该借助测量工具来证明。

+
+

当我们想仔细观察我们程序的运行速度的时候,最好的方法是性能剖析。剖析技术是基于程序执行期间一些自动抽样,然后在收尾时进行推断;最后产生的统计结果就称为剖析数据。

+

Go语言支持多种类型的剖析性能分析,每一种关注不同的方面,但它们都涉及到每个采样记录的感兴趣的一系列事件消息,每个事件都包含函数调用时函数调用堆栈的信息。内建的go test工具对几种分析方式都提供了支持。

+

CPU剖析数据标识了最耗CPU时间的函数。在每个CPU上运行的线程在每隔几毫秒都会遇到操作系统的中断事件,每次中断时都会记录一个剖析数据然后恢复正常的运行。

+

堆剖析则标识了最耗内存的语句。剖析库会记录调用内部内存分配的操作,平均每512KB的内存申请会触发一个剖析数据。

+

阻塞剖析则记录阻塞goroutine最久的操作,例如系统调用、管道发送和接收,还有获取锁等。每当goroutine被这些操作阻塞时,剖析库都会记录相应的事件。

+

只需要开启下面其中一个标志参数就可以生成各种分析文件。当同时使用多个标志参数时需要当心,因为一项分析操作可能会影响其他项的分析结果。

+
$ go test -cpuprofile=cpu.out
+$ go test -blockprofile=block.out
+$ go test -memprofile=mem.out
+
+

对于一些非测试程序也很容易进行剖析,具体的实现方式,与程序是短时间运行的小工具还是长时间运行的服务会有很大不同。剖析对于长期运行的程序尤其有用,因此可以通过调用Go的runtime API来启用运行时剖析。

+

一旦我们已经收集到了用于分析的采样数据,我们就可以使用pprof来分析这些数据。这是Go工具箱自带的一个工具,但并不是一个日常工具,它对应go tool pprof命令。该命令有许多特性和选项,但是最基本的是两个参数:生成这个概要文件的可执行程序和对应的剖析数据。

+

为了提高分析效率和减少空间,分析日志本身并不包含函数的名字;它只包含函数对应的地址。也就是说pprof需要对应的可执行程序来解读剖析数据。虽然go test通常在测试完成后就丢弃临时用的测试程序,但是在启用分析的时候会将测试程序保存为foo.test文件,其中foo部分对应待测包的名字。

+

下面的命令演示了如何收集并展示一个CPU分析文件。我们选择net/http包的一个基准测试为例。通常最好是对业务关键代码的部分设计专门的基准测试。因为简单的基准测试几乎没法代表业务场景,因此我们用-run=NONE参数禁止那些简单测试。

+
$ go test -run=NONE -bench=ClientServerParallelTLS64 \
+    -cpuprofile=cpu.log net/http
+ PASS
+ BenchmarkClientServerParallelTLS64-8  1000
+    3141325 ns/op  143010 B/op  1747 allocs/op
+ok       net/http       3.395s
+
+$ go tool pprof -text -nodecount=10 ./http.test cpu.log
+2570ms of 3590ms total (71.59%)
+Dropped 129 nodes (cum <= 17.95ms)
+Showing top 10 nodes out of 166 (cum >= 60ms)
+    flat  flat%   sum%     cum   cum%
+  1730ms 48.19% 48.19%  1750ms 48.75%  crypto/elliptic.p256ReduceDegree
+   230ms  6.41% 54.60%   250ms  6.96%  crypto/elliptic.p256Diff
+   120ms  3.34% 57.94%   120ms  3.34%  math/big.addMulVVW
+   110ms  3.06% 61.00%   110ms  3.06%  syscall.Syscall
+    90ms  2.51% 63.51%  1130ms 31.48%  crypto/elliptic.p256Square
+    70ms  1.95% 65.46%   120ms  3.34%  runtime.scanobject
+    60ms  1.67% 67.13%   830ms 23.12%  crypto/elliptic.p256Mul
+    60ms  1.67% 68.80%   190ms  5.29%  math/big.nat.montgomery
+    50ms  1.39% 70.19%    50ms  1.39%  crypto/elliptic.p256ReduceCarry
+    50ms  1.39% 71.59%    60ms  1.67%  crypto/elliptic.p256Sum
+
+

参数-text用于指定输出格式,在这里每行是一个函数,根据使用CPU的时间长短来排序。其中-nodecount=10参数限制了只输出前10行的结果。对于严重的性能问题,这个文本格式基本可以帮助查明原因了。

+

这个概要文件告诉我们,HTTPS基准测试中crypto/elliptic.p256ReduceDegree函数占用了将近一半的CPU资源,对性能占很大比重。相比之下,如果一个概要文件中主要是runtime包的内存分配的函数,那么减少内存消耗可能是一个值得尝试的优化策略。

+

对于一些更微妙的问题,你可能需要使用pprof的图形显示功能。这个需要安装GraphViz工具,可以从 http://www.graphviz.org 下载。参数-web用于生成函数的有向图,标注有CPU的使用和最热点的函数等信息。

+

这一节我们只是简单看了下Go语言的数据分析工具。如果想了解更多,可以阅读Go官方博客的“Profiling Go Programs”一文。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch11/ch11-06.html b/ch11/ch11-06.html new file mode 100644 index 0000000..6e47e83 --- /dev/null +++ b/ch11/ch11-06.html @@ -0,0 +1,253 @@ + + + + + + 示例函数 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

11.6. 示例函数

+

第三种被go test特别对待的函数是示例函数,以Example为函数名开头。示例函数没有函数参数和返回值。下面是IsPalindrome函数对应的示例函数:

+
func ExampleIsPalindrome() {
+	fmt.Println(IsPalindrome("A man, a plan, a canal: Panama"))
+	fmt.Println(IsPalindrome("palindrome"))
+	// Output:
+	// true
+	// false
+}
+
+

示例函数有三个用处。最主要的一个是作为文档:一个包的例子可以更简洁直观的方式来演示函数的用法,比文字描述更直接易懂,特别是作为一个提醒或快速参考时。一个示例函数也可以方便展示属于同一个接口的几种类型或函数之间的关系,所有的文档都必须关联到一个地方,就像一个类型或函数声明都统一到包一样。同时,示例函数和注释并不一样,示例函数是真实的Go代码,需要接受编译器的编译时检查,这样可以保证源代码更新时,示例代码不会脱节。

+

根据示例函数的后缀名部分,godoc这个web文档服务器会将示例函数关联到某个具体函数或包本身,因此ExampleIsPalindrome示例函数将是IsPalindrome函数文档的一部分,Example示例函数将是包文档的一部分。

+

示例函数的第二个用处是,在go test执行测试的时候也会运行示例函数测试。如果示例函数内含有类似上面例子中的// Output:格式的注释,那么测试工具会执行这个示例函数,然后检查示例函数的标准输出与注释是否匹配。

+

示例函数的第三个目的提供一个真实的演练场。 http://golang.org 就是由godoc提供的文档服务,它使用了Go Playground让用户可以在浏览器中在线编辑和运行每个示例函数,就像图11.4所示的那样。这通常是学习函数使用或Go语言特性最快捷的方式。

+

+

本书最后的两章是讨论reflect和unsafe包,一般的Go程序员很少使用它们,事实上也很少需要用到。因此,如果你还没有写过任何真实的Go程序的话,现在可以先去写些代码了。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch11/ch11.html b/ch11/ch11.html new file mode 100644 index 0000000..026a331 --- /dev/null +++ b/ch11/ch11.html @@ -0,0 +1,244 @@ + + + + + + 测试 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

第11章 测试

+

Maurice Wilkes,第一个存储程序计算机EDSAC的设计者,1949年他在实验室爬楼梯时有一个顿悟。在《计算机先驱回忆录》(Memoirs of a Computer Pioneer)里,他回忆到:“忽然间有一种醍醐灌顶的感觉,我整个后半生的美好时光都将在寻找程序BUG中度过了”。肯定从那之后的大部分正常的码农都会同情Wilkes过分悲观的想法,虽然也许会有人困惑于他对软件开发的难度的天真看法。

+

现在的程序已经远比Wilkes时代的更大也更复杂,也有许多技术可以让软件的复杂性可得到控制。其中有两种技术在实践中证明是比较有效的。第一种是代码在被正式部署前需要进行代码评审。第二种则是测试,也就是本章的讨论主题。

+

我们说测试的时候一般是指自动化测试,也就是写一些小的程序用来检测被测试代码(产品代码)的行为和预期的一样,这些通常都是精心设计的执行某些特定的功能或者是通过随机性的输入待验证边界的处理。

+

软件测试是一个巨大的领域。测试的任务可能已经占据了一些程序员的部分时间和另一些程序员的全部时间。和软件测试技术相关的图书或博客文章有成千上万之多。对于每一种主流的编程语言,都会有一打的用于测试的软件包,同时也有大量的测试相关的理论,而且每种都吸引了大量技术先驱和追随者。这些都足以说服那些想要编写有效测试的程序员重新学习一套全新的技能。

+

Go语言的测试技术是相对低级的。它依赖一个go test测试命令和一组按照约定方式编写的测试函数,测试命令可以运行这些测试函数。编写相对轻量级的纯测试代码是有效的,而且它很容易延伸到基准测试和示例文档。

+

在实践中,编写测试代码和编写程序本身并没有多大区别。我们编写的每一个函数也是针对每个具体的任务。我们必须小心处理边界条件,思考合适的数据结构,推断合适的输入应该产生什么样的结果输出。编写测试代码和编写普通的Go代码过程是类似的;它并不需要学习新的符号、规则和工具。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch12/ch12-01.html b/ch12/ch12-01.html new file mode 100644 index 0000000..7eaa2c8 --- /dev/null +++ b/ch12/ch12-01.html @@ -0,0 +1,266 @@ + + + + + + 为何需要反射? - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

12.1. 为何需要反射?

+

有时候我们需要编写一个函数能够处理一类并不满足普通公共接口的类型的值,也可能是因为它们并没有确定的表示方式,或者是在我们设计该函数的时候这些类型可能还不存在。

+

一个大家熟悉的例子是fmt.Fprintf函数提供的字符串格式化处理逻辑,它可以用来对任意类型的值格式化并打印,甚至支持用户自定义的类型。让我们也来尝试实现一个类似功能的函数。为了简单起见,我们的函数只接收一个参数,然后返回和fmt.Sprint类似的格式化后的字符串。我们实现的函数名也叫Sprint。

+

我们首先用switch类型分支来测试输入参数是否实现了String方法,如果是的话就调用该方法。然后继续增加类型测试分支,检查这个值的动态类型是否是string、int、bool等基础类型,并在每种情况下执行相应的格式化操作。

+
func Sprint(x interface{}) string {
+	type stringer interface {
+		String() string
+	}
+	switch x := x.(type) {
+	case stringer:
+		return x.String()
+	case string:
+		return x
+	case int:
+		return strconv.Itoa(x)
+	// ...similar cases for int16, uint32, and so on...
+	case bool:
+		if x {
+			return "true"
+		}
+		return "false"
+	default:
+		// array, chan, func, map, pointer, slice, struct
+		return "???"
+	}
+}
+
+

但是我们如何处理其它类似[]float64、map[string][]string等类型呢?我们当然可以添加更多的测试分支,但是这些组合类型的数目基本是无穷的。还有如何处理类似url.Values这样的具名类型呢?即使类型分支可以识别出底层的基础类型是map[string][]string,但是它并不匹配url.Values类型,因为它们是两种不同的类型,而且switch类型分支也不可能包含每个类似url.Values的类型,这会导致对这些库的依赖。

+

没有办法来检查未知类型的表示方式,我们被卡住了。这就是我们需要反射的原因。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch12/ch12-02.html b/ch12/ch12-02.html new file mode 100644 index 0000000..fbce6f0 --- /dev/null +++ b/ch12/ch12-02.html @@ -0,0 +1,316 @@ + + + + + + reflect.Type和reflect.Value - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

12.2. reflect.Type 和 reflect.Value

+

反射是由 reflect 包提供的。它定义了两个重要的类型,Type 和 Value。一个 Type 表示一个Go类型。它是一个接口,有许多方法来区分类型以及检查它们的组成部分,例如一个结构体的成员或一个函数的参数等。唯一能反映 reflect.Type 实现的是接口的类型描述信息(§7.5),也正是这个实体标识了接口值的动态类型。

+

函数 reflect.TypeOf 接受任意的 interface{} 类型,并以 reflect.Type 形式返回其动态类型:

+
t := reflect.TypeOf(3)  // a reflect.Type
+fmt.Println(t.String()) // "int"
+fmt.Println(t)          // "int"
+
+

其中 TypeOf(3) 调用将值 3 传给 interface{} 参数。回到 7.5节 的将一个具体的值转为接口类型会有一个隐式的接口转换操作,它会创建一个包含两个信息的接口值:操作数的动态类型(这里是 int)和它的动态的值(这里是 3)。

+

因为 reflect.TypeOf 返回的是一个动态类型的接口值,它总是返回具体的类型。因此,下面的代码将打印 "*os.File" 而不是 "io.Writer"。稍后,我们将看到能够表达接口类型的 reflect.Type。

+
var w io.Writer = os.Stdout
+fmt.Println(reflect.TypeOf(w)) // "*os.File"
+
+

要注意的是 reflect.Type 接口是满足 fmt.Stringer 接口的。因为打印一个接口的动态类型对于调试和日志是有帮助的, fmt.Printf 提供了一个缩写 %T 参数,内部使用 reflect.TypeOf 来输出:

+
fmt.Printf("%T\n", 3) // "int"
+
+

reflect 包中另一个重要的类型是 Value。一个 reflect.Value 可以装载任意类型的值。函数 reflect.ValueOf 接受任意的 interface{} 类型,并返回一个装载着其动态值的 reflect.Value。和 reflect.TypeOf 类似,reflect.ValueOf 返回的结果也是具体的类型,但是 reflect.Value 也可以持有一个接口值。

+
v := reflect.ValueOf(3) // a reflect.Value
+fmt.Println(v)          // "3"
+fmt.Printf("%v\n", v)   // "3"
+fmt.Println(v.String()) // NOTE: "<int Value>"
+
+

和 reflect.Type 类似,reflect.Value 也满足 fmt.Stringer 接口,但是除非 Value 持有的是字符串,否则 String 方法只返回其类型。而使用 fmt 包的 %v 标志参数会对 reflect.Values 特殊处理。

+

对 Value 调用 Type 方法将返回具体类型所对应的 reflect.Type:

+
t := v.Type()           // a reflect.Type
+fmt.Println(t.String()) // "int"
+
+

reflect.ValueOf 的逆操作是 reflect.Value.Interface 方法。它返回一个 interface{} 类型,装载着与 reflect.Value 相同的具体值:

+
v := reflect.ValueOf(3) // a reflect.Value
+x := v.Interface()      // an interface{}
+i := x.(int)            // an int
+fmt.Printf("%d\n", i)   // "3"
+
+

reflect.Value 和 interface{} 都能装载任意的值。所不同的是,一个空的接口隐藏了值内部的表示方式和所有方法,因此只有我们知道具体的动态类型才能使用类型断言来访问内部的值(就像上面那样),内部值我们没法访问。相比之下,一个 Value 则有很多方法来检查其内容,无论它的具体类型是什么。让我们再次尝试实现我们的格式化函数 format.Any。

+

我们使用 reflect.Value 的 Kind 方法来替代之前的类型 switch。虽然还是有无穷多的类型,但是它们的 kinds 类型却是有限的:Bool、String 和 所有数字类型的基础类型;Array 和 Struct 对应的聚合类型;Chan、Func、Ptr、Slice 和 Map 对应的引用类型;interface 类型;还有表示空值的 Invalid 类型。(空的 reflect.Value 的 kind 即为 Invalid。)

+

gopl.io/ch12/format

+
package format
+
+import (
+	"reflect"
+	"strconv"
+)
+
+// Any formats any value as a string.
+func Any(value interface{}) string {
+	return formatAtom(reflect.ValueOf(value))
+}
+
+// formatAtom formats a value without inspecting its internal structure.
+func formatAtom(v reflect.Value) string {
+	switch v.Kind() {
+	case reflect.Invalid:
+		return "invalid"
+	case reflect.Int, reflect.Int8, reflect.Int16,
+		reflect.Int32, reflect.Int64:
+		return strconv.FormatInt(v.Int(), 10)
+	case reflect.Uint, reflect.Uint8, reflect.Uint16,
+		reflect.Uint32, reflect.Uint64, reflect.Uintptr:
+		return strconv.FormatUint(v.Uint(), 10)
+	// ...floating-point and complex cases omitted for brevity...
+	case reflect.Bool:
+		return strconv.FormatBool(v.Bool())
+	case reflect.String:
+		return strconv.Quote(v.String())
+	case reflect.Chan, reflect.Func, reflect.Ptr, reflect.Slice, reflect.Map:
+		return v.Type().String() + " 0x" +
+			strconv.FormatUint(uint64(v.Pointer()), 16)
+	default: // reflect.Array, reflect.Struct, reflect.Interface
+		return v.Type().String() + " value"
+	}
+}
+
+

到目前为止,我们的函数将每个值视作一个不可分割没有内部结构的物品,因此它叫 formatAtom。对于聚合类型(结构体和数组)和接口,只是打印值的类型,对于引用类型(channels、functions、pointers、slices 和 maps),打印类型和十六进制的引用地址。虽然还不够理想,但是依然是一个重大的进步,并且 Kind 只关心底层表示,format.Any 也支持具名类型。例如:

+
var x int64 = 1
+var d time.Duration = 1 * time.Nanosecond
+fmt.Println(format.Any(x))                  // "1"
+fmt.Println(format.Any(d))                  // "1"
+fmt.Println(format.Any([]int64{x}))         // "[]int64 0x8202b87b0"
+fmt.Println(format.Any([]time.Duration{d})) // "[]time.Duration 0x8202b87e0"
+
+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch12/ch12-03.html b/ch12/ch12-03.html new file mode 100644 index 0000000..c7ceb50 --- /dev/null +++ b/ch12/ch12-03.html @@ -0,0 +1,412 @@ + + + + + + Display递归打印 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

12.3. Display,一个递归的值打印器

+

接下来,让我们看看如何改善聚合数据类型的显示。我们并不想完全克隆一个fmt.Sprint函数,我们只是构建一个用于调试用的Display函数:给定任意一个复杂类型 x,打印这个值对应的完整结构,同时标记每个元素的发现路径。让我们从一个例子开始。

+
e, _ := eval.Parse("sqrt(A / pi)")
+Display("e", e)
+
+

在上面的调用中,传入Display函数的参数是在7.9节一个表达式求值函数返回的语法树。Display函数的输出如下:

+
Display e (eval.call):
+e.fn = "sqrt"
+e.args[0].type = eval.binary
+e.args[0].value.op = 47
+e.args[0].value.x.type = eval.Var
+e.args[0].value.x.value = "A"
+e.args[0].value.y.type = eval.Var
+e.args[0].value.y.value = "pi"
+
+

你应该尽量避免在一个包的API中暴露涉及反射的接口。我们将定义一个未导出的display函数用于递归处理工作,导出的是Display函数,它只是display函数简单的包装以接受interface{}类型的参数:

+

gopl.io/ch12/display

+
func Display(name string, x interface{}) {
+	fmt.Printf("Display %s (%T):\n", name, x)
+	display(name, reflect.ValueOf(x))
+}
+
+

在display函数中,我们使用了前面定义的打印基础类型——基本类型、函数和chan等——元素值的formatAtom函数,但是我们会使用reflect.Value的方法来递归显示复杂类型的每一个成员。在递归下降过程中,path字符串,从最开始传入的起始值(这里是“e”),将逐步增长来表示是如何达到当前值(例如“e.args[0].value”)的。

+

因为我们不再模拟fmt.Sprint函数,我们将直接使用fmt包来简化我们的例子实现。

+
func display(path string, v reflect.Value) {
+	switch v.Kind() {
+	case reflect.Invalid:
+		fmt.Printf("%s = invalid\n", path)
+	case reflect.Slice, reflect.Array:
+		for i := 0; i < v.Len(); i++ {
+			display(fmt.Sprintf("%s[%d]", path, i), v.Index(i))
+		}
+	case reflect.Struct:
+		for i := 0; i < v.NumField(); i++ {
+			fieldPath := fmt.Sprintf("%s.%s", path, v.Type().Field(i).Name)
+			display(fieldPath, v.Field(i))
+		}
+	case reflect.Map:
+		for _, key := range v.MapKeys() {
+			display(fmt.Sprintf("%s[%s]", path,
+				formatAtom(key)), v.MapIndex(key))
+		}
+	case reflect.Ptr:
+		if v.IsNil() {
+			fmt.Printf("%s = nil\n", path)
+		} else {
+			display(fmt.Sprintf("(*%s)", path), v.Elem())
+		}
+	case reflect.Interface:
+		if v.IsNil() {
+			fmt.Printf("%s = nil\n", path)
+		} else {
+			fmt.Printf("%s.type = %s\n", path, v.Elem().Type())
+			display(path+".value", v.Elem())
+		}
+	default: // basic types, channels, funcs
+		fmt.Printf("%s = %s\n", path, formatAtom(v))
+	}
+}
+
+

让我们针对不同类型分别讨论。

+

Slice和数组: 两种的处理逻辑是一样的。Len方法返回slice或数组值中的元素个数,Index(i)获得索引i对应的元素,返回的也是一个reflect.Value;如果索引i超出范围的话将导致panic异常,这与数组或slice类型内建的len(a)和a[i]操作类似。display针对序列中的每个元素递归调用自身处理,我们通过在递归处理时向path附加“[i]”来表示访问路径。

+

虽然reflect.Value类型带有很多方法,但是只有少数的方法能对任意值都安全调用。例如,Index方法只能对Slice、数组或字符串类型的值调用,如果对其它类型调用则会导致panic异常。

+

结构体: NumField方法报告结构体中成员的数量,Field(i)以reflect.Value类型返回第i个成员的值。成员列表也包括通过匿名字段提升上来的成员。为了在path添加“.f”来表示成员路径,我们必须获得结构体对应的reflect.Type类型信息,然后访问结构体第i个成员的名字。

+

Maps: MapKeys方法返回一个reflect.Value类型的slice,每一个元素对应map的一个key。和往常一样,遍历map时顺序是随机的。MapIndex(key)返回map中key对应的value。我们向path添加“[key]”来表示访问路径。(我们这里有一个未完成的工作。其实map的key的类型并不局限于formatAtom能完美处理的类型;数组、结构体和接口都可以作为map的key。针对这种类型,完善key的显示信息是练习12.1的任务。)

+

指针: Elem方法返回指针指向的变量,依然是reflect.Value类型。即使指针是nil,这个操作也是安全的,在这种情况下指针是Invalid类型,但是我们可以用IsNil方法来显式地测试一个空指针,这样我们可以打印更合适的信息。我们在path前面添加“*”,并用括弧包含以避免歧义。

+

接口: 再一次,我们使用IsNil方法来测试接口是否是nil,如果不是,我们可以调用v.Elem()来获取接口对应的动态值,并且打印对应的类型和值。

+

现在我们的Display函数总算完工了,让我们看看它的表现吧。下面的Movie类型是在4.5节的电影类型上演变来的:

+
type Movie struct {
+	Title, Subtitle string
+	Year            int
+	Color           bool
+	Actor           map[string]string
+	Oscars          []string
+	Sequel          *string
+}
+
+

让我们声明一个该类型的变量,然后看看Display函数如何显示它:

+
strangelove := Movie{
+	Title:    "Dr. Strangelove",
+	Subtitle: "How I Learned to Stop Worrying and Love the Bomb",
+	Year:     1964,
+	Color:    false,
+	Actor: map[string]string{
+		"Dr. Strangelove":            "Peter Sellers",
+		"Grp. Capt. Lionel Mandrake": "Peter Sellers",
+		"Pres. Merkin Muffley":       "Peter Sellers",
+		"Gen. Buck Turgidson":        "George C. Scott",
+		"Brig. Gen. Jack D. Ripper":  "Sterling Hayden",
+		`Maj. T.J. "King" Kong`:      "Slim Pickens",
+	},
+
+	Oscars: []string{
+		"Best Actor (Nomin.)",
+		"Best Adapted Screenplay (Nomin.)",
+		"Best Director (Nomin.)",
+		"Best Picture (Nomin.)",
+	},
+}
+
+

Display("strangelove", strangelove)调用将显示(strangelove电影对应的中文名是《奇爱博士》):

+
Display strangelove (display.Movie):
+strangelove.Title = "Dr. Strangelove"
+strangelove.Subtitle = "How I Learned to Stop Worrying and Love the Bomb"
+strangelove.Year = 1964
+strangelove.Color = false
+strangelove.Actor["Gen. Buck Turgidson"] = "George C. Scott"
+strangelove.Actor["Brig. Gen. Jack D. Ripper"] = "Sterling Hayden"
+strangelove.Actor["Maj. T.J. \"King\" Kong"] = "Slim Pickens"
+strangelove.Actor["Dr. Strangelove"] = "Peter Sellers"
+strangelove.Actor["Grp. Capt. Lionel Mandrake"] = "Peter Sellers"
+strangelove.Actor["Pres. Merkin Muffley"] = "Peter Sellers"
+strangelove.Oscars[0] = "Best Actor (Nomin.)"
+strangelove.Oscars[1] = "Best Adapted Screenplay (Nomin.)"
+strangelove.Oscars[2] = "Best Director (Nomin.)"
+strangelove.Oscars[3] = "Best Picture (Nomin.)"
+strangelove.Sequel = nil
+
+

我们也可以使用Display函数来显示标准库中类型的内部结构,例如*os.File类型:

+
Display("os.Stderr", os.Stderr)
+// Output:
+// Display os.Stderr (*os.File):
+// (*(*os.Stderr).file).fd = 2
+// (*(*os.Stderr).file).name = "/dev/stderr"
+// (*(*os.Stderr).file).nepipe = 0
+
+

可以看出,反射能够访问到结构体中未导出的成员。需要当心的是这个例子的输出在不同操作系统上可能是不同的,并且随着标准库的发展也可能导致结果不同。(这也是将这些成员定义为私有成员的原因之一!)我们甚至可以用Display函数来显示reflect.Value 的内部构造(在这里设置为*os.File的类型描述体)。Display("rV", reflect.ValueOf(os.Stderr))调用的输出如下,当然不同环境得到的结果可能有差异:

+
Display rV (reflect.Value):
+(*rV.typ).size = 8
+(*rV.typ).hash = 871609668
+(*rV.typ).align = 8
+(*rV.typ).fieldAlign = 8
+(*rV.typ).kind = 22
+(*(*rV.typ).string) = "*os.File"
+
+(*(*(*rV.typ).uncommonType).methods[0].name) = "Chdir"
+(*(*(*(*rV.typ).uncommonType).methods[0].mtyp).string) = "func() error"
+(*(*(*(*rV.typ).uncommonType).methods[0].typ).string) = "func(*os.File) error"
+...
+
+

观察下面两个例子的区别:

+
var i interface{} = 3
+
+Display("i", i)
+// Output:
+// Display i (int):
+// i = 3
+
+Display("&i", &i)
+// Output:
+// Display &i (*interface {}):
+// (*&i).type = int
+// (*&i).value = 3
+
+

在第一个例子中,Display函数调用reflect.ValueOf(i),它返回一个Int类型的值。正如我们在12.2节中提到的,reflect.ValueOf总是返回一个具体类型的 Value,因为它是从一个接口值提取的内容。

+

在第二个例子中,Display函数调用的是reflect.ValueOf(&i),它返回一个指向i的指针,对应Ptr类型。在switch的Ptr分支中,对这个值调用 Elem 方法,返回一个Value来表示变量 i 本身,对应Interface类型。像这样一个间接获得的Value,可能代表任意类型的值,包括接口类型。display函数递归调用自身,这次它分别打印了这个接口的动态类型和值。

+

对于目前的实现,如果遇到对象图中含有回环,Display将会陷入死循环,例如下面这个首尾相连的链表:

+
// a struct that points to itself
+type Cycle struct{ Value int; Tail *Cycle }
+var c Cycle
+c = Cycle{42, &c}
+Display("c", c)
+
+

Display会永远不停地进行深度递归打印:

+
Display c (display.Cycle):
+c.Value = 42
+(*c.Tail).Value = 42
+(*(*c.Tail).Tail).Value = 42
+(*(*(*c.Tail).Tail).Tail).Value = 42
+...ad infinitum...
+
+

许多Go语言程序都包含了一些循环的数据。让Display支持这类带环的数据结构需要些技巧,需要额外记录迄今访问的路径;相应会带来成本。通用的解决方案是采用 unsafe 的语言特性,我们将在13.3节看到具体的解决方案。

+

带环的数据结构很少会对fmt.Sprint函数造成问题,因为它很少尝试打印完整的数据结构。例如,当它遇到一个指针的时候,它只是简单地打印指针的数字值。在打印包含自身的slice或map时可能卡住,但是这种情况很罕见,不值得付出为了处理回环所需的开销。

+

练习 12.1: 扩展Display函数,使它可以显示包含以结构体或数组作为map的key类型的值。

+

练习 12.2: 增强display函数的稳健性,通过记录边界的步数来确保在超出一定限制后放弃递归。(在13.3节,我们会看到另一种探测数据结构是否存在环的技术。)

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch12/ch12-04.html b/ch12/ch12-04.html new file mode 100644 index 0000000..3ff06a6 --- /dev/null +++ b/ch12/ch12-04.html @@ -0,0 +1,362 @@ + + + + + + 示例: 编码S表达式 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

12.4. 示例: 编码为S表达式

+

Display是一个用于显示结构化数据的调试工具,但是它并不能将任意的Go语言对象编码为通用消息然后用于进程间通信。

+

正如我们在4.5节中中看到的,Go语言的标准库支持了包括JSON、XML和ASN.1等多种编码格式。还有另一种依然被广泛使用的格式是S表达式格式,采用Lisp语言的语法。但是和其他编码格式不同的是,Go语言自带的标准库并不支持S表达式,主要是因为它没有一个公认的标准规范。

+

在本节中,我们将定义一个包用于将任意的Go语言对象编码为S表达式格式,它支持以下结构:

+
42          integer
+"hello"     string(带有Go风格的引号)
+foo         symbol(未用引号括起来的名字)
+(1 2 3)     list  (括号包起来的0个或多个元素)
+
+

布尔型习惯上使用t符号表示true,空列表或nil符号表示false,但是为了简单起见,我们暂时忽略布尔类型。同时忽略的还有chan管道和函数,因为通过反射并无法知道它们的确切状态。我们忽略的还有浮点数、复数和interface。支持它们是练习12.3的任务。

+

我们将Go语言的类型编码为S表达式的方法如下。整数和字符串以显而易见的方式编码。空值编码为nil符号。数组和slice被编码为列表。

+

结构体被编码为成员对象的列表,每个成员对象对应一个有两个元素的子列表,子列表的第一个元素是成员的名字,第二个元素是成员的值。Map被编码为键值对的列表。传统上,S表达式使用点状符号列表(key . value)结构来表示key/value对,而不是用一个含双元素的列表,不过为了简单我们忽略了点状符号列表。

+

编码是由一个encode递归函数完成,如下所示。它的结构本质上和前面的Display函数类似:

+

gopl.io/ch12/sexpr

+
func encode(buf *bytes.Buffer, v reflect.Value) error {
+	switch v.Kind() {
+	case reflect.Invalid:
+		buf.WriteString("nil")
+
+	case reflect.Int, reflect.Int8, reflect.Int16,
+		reflect.Int32, reflect.Int64:
+		fmt.Fprintf(buf, "%d", v.Int())
+
+	case reflect.Uint, reflect.Uint8, reflect.Uint16,
+		reflect.Uint32, reflect.Uint64, reflect.Uintptr:
+		fmt.Fprintf(buf, "%d", v.Uint())
+
+	case reflect.String:
+		fmt.Fprintf(buf, "%q", v.String())
+
+	case reflect.Ptr:
+		return encode(buf, v.Elem())
+
+	case reflect.Array, reflect.Slice: // (value ...)
+		buf.WriteByte('(')
+		for i := 0; i < v.Len(); i++ {
+			if i > 0 {
+				buf.WriteByte(' ')
+			}
+			if err := encode(buf, v.Index(i)); err != nil {
+				return err
+			}
+		}
+		buf.WriteByte(')')
+
+	case reflect.Struct: // ((name value) ...)
+		buf.WriteByte('(')
+		for i := 0; i < v.NumField(); i++ {
+			if i > 0 {
+				buf.WriteByte(' ')
+			}
+			fmt.Fprintf(buf, "(%s ", v.Type().Field(i).Name)
+			if err := encode(buf, v.Field(i)); err != nil {
+				return err
+			}
+			buf.WriteByte(')')
+		}
+		buf.WriteByte(')')
+
+	case reflect.Map: // ((key value) ...)
+		buf.WriteByte('(')
+		for i, key := range v.MapKeys() {
+			if i > 0 {
+				buf.WriteByte(' ')
+			}
+			buf.WriteByte('(')
+			if err := encode(buf, key); err != nil {
+				return err
+			}
+			buf.WriteByte(' ')
+			if err := encode(buf, v.MapIndex(key)); err != nil {
+				return err
+			}
+			buf.WriteByte(')')
+		}
+		buf.WriteByte(')')
+
+	default: // float, complex, bool, chan, func, interface
+		return fmt.Errorf("unsupported type: %s", v.Type())
+	}
+	return nil
+}
+
+

Marshal函数是对encode的包装,以保持和encoding/...下其它包有着相似的API:

+
// Marshal encodes a Go value in S-expression form.
+func Marshal(v interface{}) ([]byte, error) {
+	var buf bytes.Buffer
+	if err := encode(&buf, reflect.ValueOf(v)); err != nil {
+		return nil, err
+	}
+	return buf.Bytes(), nil
+}
+
+

下面是Marshal对12.3节的strangelove变量编码后的结果:

+
((Title "Dr. Strangelove") (Subtitle "How I Learned to Stop Worrying and Lo
+ve the Bomb") (Year 1964) (Actor (("Grp. Capt. Lionel Mandrake" "Peter Sell
+ers") ("Pres. Merkin Muffley" "Peter Sellers") ("Gen. Buck Turgidson" "Geor
+ge C. Scott") ("Brig. Gen. Jack D. Ripper" "Sterling Hayden") ("Maj. T.J. \
+"King\" Kong" "Slim Pickens") ("Dr. Strangelove" "Peter Sellers"))) (Oscars
+("Best Actor (Nomin.)" "Best Adapted Screenplay (Nomin.)" "Best Director (N
+omin.)" "Best Picture (Nomin.)")) (Sequel nil))
+
+

整个输出编码为一行中以减少输出的大小,但是也很难阅读。下面是对S表达式手动格式化的结果。编写一个S表达式的美化格式化函数将作为一个具有挑战性的练习任务;不过 http://gopl.io 也提供了一个简单的版本。

+
((Title "Dr. Strangelove")
+ (Subtitle "How I Learned to Stop Worrying and Love the Bomb")
+ (Year 1964)
+ (Actor (("Grp. Capt. Lionel Mandrake" "Peter Sellers")
+         ("Pres. Merkin Muffley" "Peter Sellers")
+         ("Gen. Buck Turgidson" "George C. Scott")
+         ("Brig. Gen. Jack D. Ripper" "Sterling Hayden")
+         ("Maj. T.J. \"King\" Kong" "Slim Pickens")
+         ("Dr. Strangelove" "Peter Sellers")))
+ (Oscars ("Best Actor (Nomin.)"
+          "Best Adapted Screenplay (Nomin.)"
+          "Best Director (Nomin.)"
+          "Best Picture (Nomin.)"))
+ (Sequel nil))
+
+

和fmt.Print、json.Marshal、Display函数类似,sexpr.Marshal函数处理带环的数据结构也会陷入死循环。

+

在12.6节中,我们将给出S表达式解码器的实现步骤,但是在那之前,我们还需要先了解如何通过反射技术来更新程序的变量。

+

练习 12.3: 实现encode函数缺少的分支。将布尔类型编码为t和nil,浮点数编码为Go语言的格式,复数1+2i编码为#C(1.0 2.0)格式。接口编码为类型名和值对,例如("[]int" (1 2 3)),但是这个形式可能会造成歧义:reflect.Type.String方法对于不同的类型可能返回相同的结果。

+

练习 12.4: 修改encode函数,以上面的格式化形式输出S表达式。

+

练习 12.5: 修改encode函数,用JSON格式代替S表达式格式。然后使用标准库提供的json.Unmarshal解码器来验证函数是正确的。

+

练习 12.6: 修改encode,作为一个优化,忽略对是零值对象的编码。

+

练习 12.7: 创建一个基于流式的API,用于S表达式的解码,和json.Decoder(§4.5)函数功能类似。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch12/ch12-05.html b/ch12/ch12-05.html new file mode 100644 index 0000000..c8601e4 --- /dev/null +++ b/ch12/ch12-05.html @@ -0,0 +1,304 @@ + + + + + + 通过reflect.Value修改值 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

12.5. 通过reflect.Value修改值

+

到目前为止,反射还只是程序中变量的另一种读取方式。然而,在本节中我们将重点讨论如何通过反射机制来修改变量。

+

回想一下,Go语言中类似x、x.f[1]和*p形式的表达式都可以表示变量,但是其它如x + 1和f(2)则不是变量。一个变量就是一个可寻址的内存空间,里面存储了一个值,并且存储的值可以通过内存地址来更新。

+

对于reflect.Values也有类似的区别。有一些reflect.Values是可取地址的;其它一些则不可以。考虑以下的声明语句:

+
x := 2                   // value   type    variable?
+a := reflect.ValueOf(2)  // 2       int     no
+b := reflect.ValueOf(x)  // 2       int     no
+c := reflect.ValueOf(&x) // &x      *int    no
+d := c.Elem()            // 2       int     yes (x)
+
+

其中a对应的变量不可取地址。因为a中的值仅仅是整数2的拷贝副本。b中的值也同样不可取地址。c中的值还是不可取地址,它只是一个指针&x的拷贝。实际上,所有通过reflect.ValueOf(x)返回的reflect.Value都是不可取地址的。但是对于d,它是c的解引用方式生成的,指向另一个变量,因此是可取地址的。我们可以通过调用reflect.ValueOf(&x).Elem(),来获取任意变量x对应的可取地址的Value。

+

我们可以通过调用reflect.Value的CanAddr方法来判断其是否可以被取地址:

+
fmt.Println(a.CanAddr()) // "false"
+fmt.Println(b.CanAddr()) // "false"
+fmt.Println(c.CanAddr()) // "false"
+fmt.Println(d.CanAddr()) // "true"
+
+

每当我们通过指针间接地获取的reflect.Value都是可取地址的,即使开始的是一个不可取地址的Value。在反射机制中,所有关于是否支持取地址的规则都是类似的。例如,slice的索引表达式e[i]将隐式地包含一个指针,它就是可取地址的,即使开始的e表达式不支持也没有关系。以此类推,reflect.ValueOf(e).Index(i)对应的值也是可取地址的,即使原始的reflect.ValueOf(e)不支持也没有关系。

+

要从变量对应的可取地址的reflect.Value来访问变量需要三个步骤。第一步是调用Addr()方法,它返回一个Value,里面保存了指向变量的指针。然后是在Value上调用Interface()方法,也就是返回一个interface{},里面包含指向变量的指针。最后,如果我们知道变量的类型,我们可以使用类型的断言机制将得到的interface{}类型的接口强制转为普通的类型指针。这样我们就可以通过这个普通指针来更新变量了:

+
x := 2
+d := reflect.ValueOf(&x).Elem()   // d refers to the variable x
+px := d.Addr().Interface().(*int) // px := &x
+*px = 3                           // x = 3
+fmt.Println(x)                    // "3"
+
+

或者,不使用指针,而是通过调用可取地址的reflect.Value的reflect.Value.Set方法来更新对应的值:

+
d.Set(reflect.ValueOf(4))
+fmt.Println(x) // "4"
+
+

Set方法将在运行时执行和编译时进行类似的可赋值性约束的检查。以上代码,变量和值都是int类型,但是如果变量是int64类型,那么程序将抛出一个panic异常,所以关键问题是要确保改类型的变量可以接受对应的值:

+
d.Set(reflect.ValueOf(int64(5))) // panic: int64 is not assignable to int
+
+

同样,对一个不可取地址的reflect.Value调用Set方法也会导致panic异常:

+
x := 2
+b := reflect.ValueOf(x)
+b.Set(reflect.ValueOf(3)) // panic: Set using unaddressable value
+
+

这里有很多用于基本数据类型的Set方法:SetInt、SetUint、SetString和SetFloat等。

+
d := reflect.ValueOf(&x).Elem()
+d.SetInt(3)
+fmt.Println(x) // "3"
+
+

从某种程度上说,这些Set方法总是尽可能地完成任务。以SetInt为例,只要变量是某种类型的有符号整数就可以工作,即使是一些命名的类型、甚至只要底层数据类型是有符号整数就可以,而且如果对于变量类型值太大的话会被自动截断。但需要谨慎的是:对于一个引用interface{}类型的reflect.Value调用SetInt会导致panic异常,即使那个interface{}变量对于整数类型也不行。

+
x := 1
+rx := reflect.ValueOf(&x).Elem()
+rx.SetInt(2)                     // OK, x = 2
+rx.Set(reflect.ValueOf(3))       // OK, x = 3
+rx.SetString("hello")            // panic: string is not assignable to int
+rx.Set(reflect.ValueOf("hello")) // panic: string is not assignable to int
+
+var y interface{}
+ry := reflect.ValueOf(&y).Elem()
+ry.SetInt(2)                     // panic: SetInt called on interface Value
+ry.Set(reflect.ValueOf(3))       // OK, y = int(3)
+ry.SetString("hello")            // panic: SetString called on interface Value
+ry.Set(reflect.ValueOf("hello")) // OK, y = "hello"
+
+

当我们用Display显示os.Stdout结构时,我们发现反射可以越过Go语言的导出规则的限制读取结构体中未导出的成员,比如在类Unix系统上os.File结构体中的fd int成员。然而,利用反射机制并不能修改这些未导出的成员:

+
stdout := reflect.ValueOf(os.Stdout).Elem() // *os.Stdout, an os.File var
+fmt.Println(stdout.Type())                  // "os.File"
+fd := stdout.FieldByName("fd")
+fmt.Println(fd.Int()) // "1"
+fd.SetInt(2)          // panic: unexported field
+
+

一个可取地址的reflect.Value会记录一个结构体成员是否是未导出成员,如果是的话则拒绝修改操作。因此,CanAddr方法并不能正确反映一个变量是否是可以被修改的。另一个相关的方法CanSet是用于检查对应的reflect.Value是否是可取地址并可被修改的:

+
fmt.Println(fd.CanAddr(), fd.CanSet()) // "true false"
+
+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch12/ch12-06.html b/ch12/ch12-06.html new file mode 100644 index 0000000..48f278a --- /dev/null +++ b/ch12/ch12-06.html @@ -0,0 +1,371 @@ + + + + + + 示例: 解码S表达式 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

12.6. 示例: 解码S表达式

+

标准库中encoding/...下每个包中提供的Marshal编码函数都有一个对应的Unmarshal函数用于解码。例如,我们在4.5节中看到的,要将包含JSON编码格式的字节slice数据解码为我们自己的Movie类型(§12.3),我们可以这样做:

+
data := []byte{/* ... */}
+var movie Movie
+err := json.Unmarshal(data, &movie)
+
+

Unmarshal函数使用了反射机制类修改movie变量的每个成员,根据输入的内容为Movie成员创建对应的map、结构体和slice。

+

现在让我们为S表达式编码实现一个简易的Unmarshal,类似于前面的json.Unmarshal标准库函数,对应我们之前实现的sexpr.Marshal函数的逆操作。我们必须提醒一下,一个健壮的和通用的实现通常需要比例子更多的代码,为了便于演示我们采用了精简的实现。我们只支持S表达式有限的子集,同时处理错误的方式也比较粗暴,代码的目的是为了演示反射的用法,而不是构造一个实用的S表达式的解码器。

+

词法分析器lexer使用了标准库中的text/scanner包将输入流的字节数据解析为一个个类似注释、标识符、字符串面值和数字面值之类的标记。输入扫描器scanner的Scan方法将提前扫描和返回下一个记号,对于rune类型。大多数记号,比如“(”,对应一个单一rune可表示的Unicode字符,但是text/scanner也可以用小的负数表示记号标识符、字符串等由多个字符组成的记号。调用Scan方法将返回这些记号的类型,接着调用TokenText方法将返回记号对应的文本内容。

+

因为每个解析器可能需要多次使用当前的记号,但是Scan会一直向前扫描,所以我们包装了一个lexer扫描器辅助类型,用于跟踪最近由Scan方法返回的记号。

+

gopl.io/ch12/sexpr

+
type lexer struct {
+	scan  scanner.Scanner
+	token rune // the current token
+}
+
+func (lex *lexer) next()        { lex.token = lex.scan.Scan() }
+func (lex *lexer) text() string { return lex.scan.TokenText() }
+
+func (lex *lexer) consume(want rune) {
+	if lex.token != want { // NOTE: Not an example of good error handling.
+		panic(fmt.Sprintf("got %q, want %q", lex.text(), want))
+	}
+	lex.next()
+}
+
+

现在让我们转到语法解析器。它主要包含两个功能。第一个是read函数,用于读取S表达式的当前标记,然后根据S表达式的当前标记更新可取地址的reflect.Value对应的变量v。

+
func read(lex *lexer, v reflect.Value) {
+	switch lex.token {
+	case scanner.Ident:
+		// The only valid identifiers are
+		// "nil" and struct field names.
+		if lex.text() == "nil" {
+			v.Set(reflect.Zero(v.Type()))
+			lex.next()
+			return
+		}
+	case scanner.String:
+		s, _ := strconv.Unquote(lex.text()) // NOTE: ignoring errors
+		v.SetString(s)
+		lex.next()
+		return
+	case scanner.Int:
+		i, _ := strconv.Atoi(lex.text()) // NOTE: ignoring errors
+		v.SetInt(int64(i))
+		lex.next()
+		return
+	case '(':
+		lex.next()
+		readList(lex, v)
+		lex.next() // consume ')'
+		return
+	}
+	panic(fmt.Sprintf("unexpected token %q", lex.text()))
+}
+
+

我们的S表达式使用标识符区分两个不同类型,结构体成员名和nil值的指针。read函数值处理nil类型的标识符。当遇到scanner.Ident为“nil”时,使用reflect.Zero函数将变量v设置为零值。而其它任何类型的标识符,我们都作为错误处理。后面的readList函数将处理结构体的成员名。

+

一个“(”标记对应一个列表的开始。第二个函数readList,将一个列表解码到一个聚合类型中(map、结构体、slice或数组),具体类型依赖于传入待填充变量的类型。每次遇到这种情况,循环继续解析每个元素直到遇到于开始标记匹配的结束标记“)”,endList函数用于检测结束标记。

+

最有趣的部分是递归。最简单的是对数组类型的处理。直到遇到“)”结束标记,我们使用Index函数来获取数组每个元素的地址,然后递归调用read函数处理。和其它错误类似,如果输入数据导致解码器的引用超出了数组的范围,解码器将抛出panic异常。slice也采用类似方法解析,不同的是我们将为每个元素创建新的变量,然后将元素添加到slice的末尾。

+

在循环处理结构体和map每个元素时必须解码一个(key value)格式的对应子列表。对于结构体,key部分对于成员的名字。和数组类似,我们使用FieldByName找到结构体对应成员的变量,然后递归调用read函数处理。对于map,key可能是任意类型,对元素的处理方式和slice类似,我们创建一个新的变量,然后递归填充它,最后将新解析到的key/value对添加到map。

+
func readList(lex *lexer, v reflect.Value) {
+	switch v.Kind() {
+	case reflect.Array: // (item ...)
+		for i := 0; !endList(lex); i++ {
+			read(lex, v.Index(i))
+		}
+
+	case reflect.Slice: // (item ...)
+		for !endList(lex) {
+			item := reflect.New(v.Type().Elem()).Elem()
+			read(lex, item)
+			v.Set(reflect.Append(v, item))
+		}
+
+	case reflect.Struct: // ((name value) ...)
+		for !endList(lex) {
+			lex.consume('(')
+			if lex.token != scanner.Ident {
+				panic(fmt.Sprintf("got token %q, want field name", lex.text()))
+			}
+			name := lex.text()
+			lex.next()
+			read(lex, v.FieldByName(name))
+			lex.consume(')')
+		}
+
+	case reflect.Map: // ((key value) ...)
+		v.Set(reflect.MakeMap(v.Type()))
+		for !endList(lex) {
+			lex.consume('(')
+			key := reflect.New(v.Type().Key()).Elem()
+			read(lex, key)
+			value := reflect.New(v.Type().Elem()).Elem()
+			read(lex, value)
+			v.SetMapIndex(key, value)
+			lex.consume(')')
+		}
+
+	default:
+		panic(fmt.Sprintf("cannot decode list into %v", v.Type()))
+	}
+}
+
+func endList(lex *lexer) bool {
+	switch lex.token {
+	case scanner.EOF:
+		panic("end of file")
+	case ')':
+		return true
+	}
+	return false
+}
+
+

最后,我们将解析器包装为导出的Unmarshal解码函数,隐藏了一些初始化和清理等边缘处理。内部解析器以panic的方式抛出错误,但是Unmarshal函数通过在defer语句调用recover函数来捕获内部panic(§5.10),然后返回一个对panic对应的错误信息。

+
// Unmarshal parses S-expression data and populates the variable
+// whose address is in the non-nil pointer out.
+func Unmarshal(data []byte, out interface{}) (err error) {
+	lex := &lexer{scan: scanner.Scanner{Mode: scanner.GoTokens}}
+	lex.scan.Init(bytes.NewReader(data))
+	lex.next() // get the first token
+	defer func() {
+		// NOTE: this is not an example of ideal error handling.
+		if x := recover(); x != nil {
+			err = fmt.Errorf("error at %s: %v", lex.scan.Position, x)
+		}
+	}()
+	read(lex, reflect.ValueOf(out).Elem())
+	return nil
+}
+
+

生产实现不应该对任何输入问题都用panic形式报告,而且应该报告一些错误相关的信息,例如出现错误输入的行号和位置等。尽管如此,我们希望通过这个例子来展示类似encoding/json等包底层代码的实现思路,以及如何使用反射机制来填充数据结构。

+

练习 12.8: sexpr.Unmarshal函数和json.Unmarshal一样,都要求在解码前输入完整的字节slice。定义一个和json.Decoder类似的sexpr.Decoder类型,支持从一个io.Reader流解码。修改sexpr.Unmarshal函数,使用这个新的类型实现。

+

练习 12.9: 编写一个基于标记的API用于解码S表达式,参考xml.Decoder(7.14)的风格。你将需要五种类型的标记:Symbol、String、Int、StartList和EndList。

+

练习 12.10: 扩展sexpr.Unmarshal函数,支持布尔型、浮点数和interface类型的解码,使用 练习 12.3: 的方案。(提示:要解码接口,你需要将name映射到每个支持类型的reflect.Type。)

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch12/ch12-08.html b/ch12/ch12-08.html new file mode 100644 index 0000000..f3c92ff --- /dev/null +++ b/ch12/ch12-08.html @@ -0,0 +1,270 @@ + + + + + + 显示一个类型的方法集 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

12.8. 显示一个类型的方法集

+

我们的最后一个例子是使用reflect.Type来打印任意值的类型和枚举它的方法:

+

gopl.io/ch12/methods

+
// Print prints the method set of the value x.
+func Print(x interface{}) {
+	v := reflect.ValueOf(x)
+	t := v.Type()
+	fmt.Printf("type %s\n", t)
+
+	for i := 0; i < v.NumMethod(); i++ {
+		methType := v.Method(i).Type()
+		fmt.Printf("func (%s) %s%s\n", t, t.Method(i).Name,
+			strings.TrimPrefix(methType.String(), "func"))
+	}
+}
+
+

reflect.Type和reflect.Value都提供了一个Method方法。每次t.Method(i)调用将一个reflect.Method的实例,对应一个用于描述一个方法的名称和类型的结构体。每次v.Method(i)方法调用都返回一个reflect.Value以表示对应的值(§6.4),也就是一个方法是绑到它的接收者的。使用reflect.Value.Call方法(我们这里没有演示),将可以调用一个Func类型的Value,但是这个例子中只用到了它的类型。

+

这是属于time.Duration和*strings.Replacer两个类型的方法:

+
methods.Print(time.Hour)
+// Output:
+// type time.Duration
+// func (time.Duration) Hours() float64
+// func (time.Duration) Minutes() float64
+// func (time.Duration) Nanoseconds() int64
+// func (time.Duration) Seconds() float64
+// func (time.Duration) String() string
+
+methods.Print(new(strings.Replacer))
+// Output:
+// type *strings.Replacer
+// func (*strings.Replacer) Replace(string) string
+// func (*strings.Replacer) WriteString(io.Writer, string) (int, error)
+
+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch12/ch12-09.html b/ch12/ch12-09.html new file mode 100644 index 0000000..9d86466 --- /dev/null +++ b/ch12/ch12-09.html @@ -0,0 +1,247 @@ + + + + + + 几点忠告 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

12.9. 几点忠告

+

虽然反射提供的API远多于我们讲到的,我们前面的例子主要是给出了一个方向,通过反射可以实现哪些功能。反射是一个强大并富有表达力的工具,但是它应该被小心地使用,原因有三。

+

第一个原因是,基于反射的代码是比较脆弱的。对于每一个会导致编译器报告类型错误的问题,在反射中都有与之相对应的误用问题,不同的是编译器会在构建时马上报告错误,而反射则是在真正运行到的时候才会抛出panic异常,可能是写完代码很久之后了,而且程序也可能运行了很长的时间。

+

以前面的readList函数(§12.6)为例,为了从输入读取字符串并填充int类型的变量而调用的reflect.Value.SetString方法可能导致panic异常。绝大多数使用反射的程序都有类似的风险,需要非常小心地检查每个reflect.Value的对应值的类型、是否可取地址,还有是否可以被修改等。

+

避免这种因反射而导致的脆弱性的问题的最好方法,是将所有的反射相关的使用控制在包的内部,如果可能的话避免在包的API中直接暴露reflect.Value类型,这样可以限制一些非法输入。如果无法做到这一点,在每个有风险的操作前指向额外的类型检查。以标准库中的代码为例,当fmt.Printf收到一个非法的操作数时,它并不会抛出panic异常,而是打印相关的错误信息。程序虽然还有BUG,但是会更加容易诊断。

+
fmt.Printf("%d %s\n", "hello", 42) // "%!d(string=hello) %!s(int=42)"
+
+

反射同样降低了程序的安全性,还影响了自动化重构和分析工具的准确性,因为它们无法识别运行时才能确认的类型信息。

+

避免使用反射的第二个原因是,即使对应类型提供了相同文档,但是反射的操作不能做静态类型检查,而且大量反射的代码通常难以理解。总是需要小心翼翼地为每个导出的类型和其它接受interface{}或reflect.Value类型参数的函数维护说明文档。

+

第三个原因,基于反射的代码通常比正常的代码运行速度慢一到两个数量级。对于一个典型的项目,大部分函数的性能和程序的整体性能关系不大,所以当反射能使程序更加清晰的时候可以考虑使用。测试是一个特别适合使用反射的场景,因为每个测试的数据集都很小。但是对于性能关键路径的函数,最好避免使用反射。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch12/ch12.html b/ch12/ch12.html new file mode 100644 index 0000000..08deec0 --- /dev/null +++ b/ch12/ch12.html @@ -0,0 +1,240 @@ + + + + + + 反射 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

第12章 反射

+

Go语言提供了一种机制,能够在运行时更新变量和检查它们的值、调用它们的方法和它们支持的内在操作,而不需要在编译时就知道这些变量的具体类型。这种机制被称为反射。反射也可以让我们将类型本身作为第一类的值类型处理。

+

在本章,我们将探讨Go语言的反射特性,看看它可以给语言增加哪些表达力,以及在两个至关重要的API是如何使用反射机制的:一个是fmt包提供的字符串格式化功能,另一个是类似encoding/json和encoding/xml提供的针对特定协议的编解码功能。对于我们在4.6节中看到过的text/template和html/template包,它们的实现也是依赖反射技术的。然后,反射是一个复杂的内省技术,不应该随意使用,因此,尽管上面这些包内部都是用反射技术实现的,但是它们自己的API都没有公开反射相关的接口。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch13/ch13-01.html b/ch13/ch13-01.html new file mode 100644 index 0000000..b84317c --- /dev/null +++ b/ch13/ch13-01.html @@ -0,0 +1,288 @@ + + + + + + unsafe.Sizeof, Alignof 和 Offsetof - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

13.1. unsafe.Sizeof, Alignof 和 Offsetof

+

unsafe.Sizeof函数返回操作数在内存中的字节大小,参数可以是任意类型的表达式,但是它并不会对表达式进行求值。一个Sizeof函数调用是一个对应uintptr类型的常量表达式,因此返回的结果可以用作数组类型的长度大小,或者用作计算其他的常量。

+
import "unsafe"
+fmt.Println(unsafe.Sizeof(float64(0))) // "8"
+
+

Sizeof函数返回的大小只包括数据结构中固定的部分,例如字符串对应结构体中的指针和字符串长度部分,但是并不包含指针指向的字符串的内容。Go语言中非聚合类型通常有一个固定的大小,尽管在不同工具链下生成的实际大小可能会有所不同。考虑到可移植性,引用类型或包含引用类型的大小在32位平台上是4个字节,在64位平台上是8个字节。

+

计算机在加载和保存数据时,如果内存地址合理地对齐的将会更有效率。例如2字节大小的int16类型的变量地址应该是偶数,一个4字节大小的rune类型变量的地址应该是4的倍数,一个8字节大小的float64、uint64或64-bit指针类型变量的地址应该是8字节对齐的。但是对于再大的地址对齐倍数则是不需要的,即使是complex128等较大的数据类型最多也只是8字节对齐。

+

由于地址对齐这个因素,一个聚合类型(结构体或数组)的大小至少是所有字段或元素大小的总和,或者更大因为可能存在内存空洞。内存空洞是编译器自动添加的没有被使用的内存空间,用于保证后面每个字段或元素的地址相对于结构或数组的开始地址能够合理地对齐(译注:内存空洞可能会存在一些随机数据,可能会对用unsafe包直接操作内存的处理产生影响)。

+ + + + + + + + + + + +
类型大小
bool1个字节
intN, uintN, floatN, complexNN/8个字节(例如float64是8个字节)
int, uint, uintptr1个机器字
*T1个机器字
string2个机器字(data、len)
[]T3个机器字(data、len、cap)
map1个机器字
func1个机器字
chan1个机器字
interface2个机器字(type、value)
+

Go语言的规范并没有要求一个字段的声明顺序和内存中的顺序是一致的,所以理论上一个编译器可以随意地重新排列每个字段的内存位置,虽然在写作本书的时候编译器还没有这么做。下面的三个结构体虽然有着相同的字段,但是第一种写法比另外的两个需要多50%的内存。

+
                               // 64-bit  32-bit
+struct{ bool; float64; int16 } // 3 words 4words
+struct{ float64; int16; bool } // 2 words 3words
+struct{ bool; int16; float64 } // 2 words 3words
+
+

关于内存地址对齐算法的细节超出了本书的范围,也不是每一个结构体都需要担心这个问题,不过有效的包装可以使数据结构更加紧凑(译注:未来的Go语言编译器应该会默认优化结构体的顺序,当然应该也能够指定具体的内存布局,相同讨论请参考 Issue10014 ),内存使用率和性能都可能会受益。

+

unsafe.Alignof 函数返回对应参数的类型需要对齐的倍数。和 Sizeof 类似, Alignof 也是返回一个常量表达式,对应一个常量。通常情况下布尔和数字类型需要对齐到它们本身的大小(最多8个字节),其它的类型对齐到机器字大小。

+

unsafe.Offsetof 函数的参数必须是一个字段 x.f,然后返回 f 字段相对于 x 起始地址的偏移量,包括可能的空洞。

+

图 13.1 显示了一个结构体变量 x 以及其在32位和64位机器上的典型的内存。灰色区域是空洞。

+
var x struct {
+	a bool
+	b int16
+	c []int
+}
+
+

下面显示了对x和它的三个字段调用unsafe包相关函数的计算结果:

+

+

32位系统:

+
Sizeof(x)   = 16  Alignof(x)   = 4
+Sizeof(x.a) = 1   Alignof(x.a) = 1 Offsetof(x.a) = 0
+Sizeof(x.b) = 2   Alignof(x.b) = 2 Offsetof(x.b) = 2
+Sizeof(x.c) = 12  Alignof(x.c) = 4 Offsetof(x.c) = 4
+
+

64位系统:

+
Sizeof(x)   = 32  Alignof(x)   = 8
+Sizeof(x.a) = 1   Alignof(x.a) = 1 Offsetof(x.a) = 0
+Sizeof(x.b) = 2   Alignof(x.b) = 2 Offsetof(x.b) = 2
+Sizeof(x.c) = 24  Alignof(x.c) = 8 Offsetof(x.c) = 8
+
+

虽然这几个函数在不安全的unsafe包,但是这几个函数调用并不是真的不安全,特别在需要优化内存空间时它们返回的结果对于理解原生的内存布局很有帮助。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch13/ch13-02.html b/ch13/ch13-02.html new file mode 100644 index 0000000..b5efc3e --- /dev/null +++ b/ch13/ch13-02.html @@ -0,0 +1,282 @@ + + + + + + unsafe.Pointer - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

13.2. unsafe.Pointer

+

大多数指针类型会写成*T,表示是“一个指向T类型变量的指针”。unsafe.Pointer是特别定义的一种指针类型(译注:类似C语言中的void*类型的指针),它可以包含任意类型变量的地址。当然,我们不可以直接通过*p来获取unsafe.Pointer指针指向的真实变量的值,因为我们并不知道变量的具体类型。和普通指针一样,unsafe.Pointer指针也是可以比较的,并且支持和nil常量比较判断是否为空指针。

+

一个普通的*T类型指针可以被转化为unsafe.Pointer类型指针,并且一个unsafe.Pointer类型指针也可以被转回普通的指针,被转回普通的指针类型并不需要和原始的*T类型相同。通过将*float64类型指针转化为*uint64类型指针,我们可以查看一个浮点数变量的位模式。

+
package math
+
+func Float64bits(f float64) uint64 { return *(*uint64)(unsafe.Pointer(&f)) }
+
+fmt.Printf("%#016x\n", Float64bits(1.0)) // "0x3ff0000000000000"
+
+

通过转为新类型指针,我们可以更新浮点数的位模式。通过位模式操作浮点数是可以的,但是更重要的意义是指针转换语法让我们可以在不破坏类型系统的前提下向内存写入任意的值。

+

一个unsafe.Pointer指针也可以被转化为uintptr类型,然后保存到指针型数值变量中(译注:这只是和当前指针相同的一个数字值,并不是一个指针),然后用以做必要的指针数值运算。(第三章内容,uintptr是一个无符号的整型数,足以保存一个地址)这种转换虽然也是可逆的,但是将uintptr转为unsafe.Pointer指针可能会破坏类型系统,因为并不是所有的数字都是有效的内存地址。

+

许多将unsafe.Pointer指针转为原生数字,然后再转回为unsafe.Pointer类型指针的操作也是不安全的。比如下面的例子需要将变量x的地址加上b字段地址偏移量转化为*int16类型指针,然后通过该指针更新x.b:

+

gopl.io/ch13/unsafeptr

+
var x struct {
+	a bool
+	b int16
+	c []int
+}
+
+// 和 pb := &x.b 等价
+pb := (*int16)(unsafe.Pointer(
+	uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)))
+*pb = 42
+fmt.Println(x.b) // "42"
+
+

上面的写法尽管很繁琐,但在这里并不是一件坏事,因为这些功能应该很谨慎地使用。不要试图引入一个uintptr类型的临时变量,因为它可能会破坏代码的安全性(译注:这是真正可以体会unsafe包为何不安全的例子)。下面段代码是错误的:

+
// NOTE: subtly incorrect!
+tmp := uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)
+pb := (*int16)(unsafe.Pointer(tmp))
+*pb = 42
+
+

产生错误的原因很微妙。有时候垃圾回收器会移动一些变量以降低内存碎片等问题。这类垃圾回收器被称为移动GC。当一个变量被移动,所有的保存该变量旧地址的指针必须同时被更新为变量移动后的新地址。从垃圾收集器的视角来看,一个unsafe.Pointer是一个指向变量的指针,因此当变量被移动时对应的指针也必须被更新;但是uintptr类型的临时变量只是一个普通的数字,所以其值不应该被改变。上面错误的代码因为引入一个非指针的临时变量tmp,导致垃圾收集器无法正确识别这个是一个指向变量x的指针。当第二个语句执行时,变量x可能已经被转移,这时候临时变量tmp也就不再是现在的&x.b地址。第三个向之前无效地址空间的赋值语句将彻底摧毁整个程序!

+

还有很多类似原因导致的错误。例如这条语句:

+
pT := uintptr(unsafe.Pointer(new(T))) // 提示: 错误!
+
+

这里并没有指针引用new新创建的变量,因此该语句执行完成之后,垃圾收集器有权马上回收其内存空间,所以返回的pT将是无效的地址。

+

虽然目前的Go语言实现还没有使用移动GC(译注:未来可能实现),但这不该是编写错误代码侥幸的理由:当前的Go语言实现已经有移动变量的场景。在5.2节我们提到goroutine的栈是根据需要动态增长的。当发生栈动态增长的时候,原来栈中的所有变量可能需要被移动到新的更大的栈中,所以我们并不能确保变量的地址在整个使用周期内是不变的。

+

在编写本文时,还没有清晰的原则来指引Go程序员,什么样的unsafe.Pointer和uintptr的转换是不安全的(参考 Issue7192 ). 译注: 该问题已经关闭),因此我们强烈建议按照最坏的方式处理。将所有包含变量地址的uintptr类型变量当作BUG处理,同时减少不必要的unsafe.Pointer类型到uintptr类型的转换。在第一个例子中,有三个转换——字段偏移量到uintptr的转换和转回unsafe.Pointer类型的操作——所有的转换全在一个表达式完成。

+

当调用一个库函数,并且返回的是uintptr类型地址时(译注:普通方法实现的函数尽量不要返回该类型。下面例子是reflect包的函数,reflect包和unsafe包一样都是采用特殊技术实现的,编译器可能给它们开了后门),比如下面反射包中的相关函数,返回的结果应该立即转换为unsafe.Pointer以确保指针指向的是相同的变量。

+
package reflect
+
+func (Value) Pointer() uintptr
+func (Value) UnsafeAddr() uintptr
+func (Value) InterfaceData() [2]uintptr // (index 1)
+
+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch13/ch13-03.html b/ch13/ch13-03.html new file mode 100644 index 0000000..0c36d4b --- /dev/null +++ b/ch13/ch13-03.html @@ -0,0 +1,341 @@ + + + + + + 示例: 深度相等判断 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

13.3. 示例: 深度相等判断

+

来自reflect包的DeepEqual函数可以对两个值进行深度相等判断。DeepEqual函数使用内建的==比较操作符对基础类型进行相等判断,对于复合类型则递归该变量的每个基础类型然后做类似的比较判断。因为它可以工作在任意的类型上,甚至对于一些不支持==操作运算符的类型也可以工作,因此在一些测试代码中广泛地使用该函数。比如下面的代码是用DeepEqual函数比较两个字符串slice是否相等。

+
func TestSplit(t *testing.T) {
+	got := strings.Split("a:b:c", ":")
+	want := []string{"a", "b", "c"};
+	if !reflect.DeepEqual(got, want) { /* ... */ }
+}
+
+

尽管DeepEqual函数很方便,而且可以支持任意的数据类型,但是它也有不足之处。例如,它将一个nil值的map和非nil值但是空的map视作不相等,同样nil值的slice 和非nil但是空的slice也视作不相等。

+
var a, b []string = nil, []string{}
+fmt.Println(reflect.DeepEqual(a, b)) // "false"
+
+var c, d map[string]int = nil, make(map[string]int)
+fmt.Println(reflect.DeepEqual(c, d)) // "false"
+
+

我们希望在这里实现一个自己的Equal函数,用于比较类型的值。和DeepEqual函数类似的地方是它也是基于slice和map的每个元素进行递归比较,不同之处是它将nil值的slice(map类似)和非nil值但是空的slice视作相等的值。基础部分的比较可以基于reflect包完成,和12.3章的Display函数的实现方法类似。同样,我们也定义了一个内部函数equal,用于内部的递归比较。读者目前不用关心seen参数的具体含义。对于每一对需要比较的x和y,equal函数首先检测它们是否都有效(或都无效),然后检测它们是否是相同的类型。剩下的部分是一个巨大的switch分支,用于相同基础类型的元素比较。因为页面空间的限制,我们省略了一些相似的分支。

+

gopl.io/ch13/equal

+
func equal(x, y reflect.Value, seen map[comparison]bool) bool {
+	if !x.IsValid() || !y.IsValid() {
+		return x.IsValid() == y.IsValid()
+	}
+	if x.Type() != y.Type() {
+		return false
+	}
+
+	// ...cycle check omitted (shown later)...
+
+	switch x.Kind() {
+	case reflect.Bool:
+		return x.Bool() == y.Bool()
+	case reflect.String:
+		return x.String() == y.String()
+
+	// ...numeric cases omitted for brevity...
+
+	case reflect.Chan, reflect.UnsafePointer, reflect.Func:
+		return x.Pointer() == y.Pointer()
+	case reflect.Ptr, reflect.Interface:
+		return equal(x.Elem(), y.Elem(), seen)
+	case reflect.Array, reflect.Slice:
+		if x.Len() != y.Len() {
+			return false
+		}
+		for i := 0; i < x.Len(); i++ {
+			if !equal(x.Index(i), y.Index(i), seen) {
+				return false
+			}
+		}
+		return true
+
+	// ...struct and map cases omitted for brevity...
+	}
+	panic("unreachable")
+}
+
+

和前面的建议一样,我们并不公开reflect包相关的接口,所以导出的函数需要在内部自己将变量转为reflect.Value类型。

+
// Equal reports whether x and y are deeply equal.
+func Equal(x, y interface{}) bool {
+	seen := make(map[comparison]bool)
+	return equal(reflect.ValueOf(x), reflect.ValueOf(y), seen)
+}
+
+type comparison struct {
+	x, y unsafe.Pointer
+	t reflect.Type
+}
+
+

为了确保算法对于有环的数据结构也能正常退出,我们必须记录每次已经比较的变量,从而避免进入第二次的比较。Equal函数分配了一组用于比较的结构体,包含每对比较对象的地址(unsafe.Pointer形式保存)和类型。我们要记录类型的原因是,有些不同的变量可能对应相同的地址。例如,如果x和y都是数组类型,那么x和x[0]将对应相同的地址,y和y[0]也是对应相同的地址,这可以用于区分x与y之间的比较或x[0]与y[0]之间的比较是否进行过了。

+
// cycle check
+if x.CanAddr() && y.CanAddr() {
+	xptr := unsafe.Pointer(x.UnsafeAddr())
+	yptr := unsafe.Pointer(y.UnsafeAddr())
+	if xptr == yptr {
+		return true // identical references
+	}
+	c := comparison{xptr, yptr, x.Type()}
+	if seen[c] {
+		return true // already seen
+	}
+	seen[c] = true
+}
+
+

这是Equal函数用法的例子:

+
fmt.Println(Equal([]int{1, 2, 3}, []int{1, 2, 3}))        // "true"
+fmt.Println(Equal([]string{"foo"}, []string{"bar"}))      // "false"
+fmt.Println(Equal([]string(nil), []string{}))             // "true"
+fmt.Println(Equal(map[string]int(nil), map[string]int{})) // "true"
+
+

Equal函数甚至可以处理类似12.3章中导致Display陷入死循环的带有环的数据。

+
// Circular linked lists a -> b -> a and c -> c.
+type link struct {
+	value string
+	tail *link
+}
+a, b, c := &link{value: "a"}, &link{value: "b"}, &link{value: "c"}
+a.tail, b.tail, c.tail = b, a, c
+fmt.Println(Equal(a, a)) // "true"
+fmt.Println(Equal(b, b)) // "true"
+fmt.Println(Equal(c, c)) // "true"
+fmt.Println(Equal(a, b)) // "false"
+fmt.Println(Equal(a, c)) // "false"
+
+

练习 13.1: 定义一个深比较函数,对于十亿以内的数字比较,忽略类型差异。

+

练习 13.2: 编写一个函数,报告其参数是否为循环数据结构。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch13/ch13-04.html b/ch13/ch13-04.html new file mode 100644 index 0000000..cefe6c2 --- /dev/null +++ b/ch13/ch13-04.html @@ -0,0 +1,410 @@ + + + + + + 通过cgo调用C代码 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

13.4. 通过cgo调用C代码

+

Go程序可能会遇到要访问C语言的某些硬件驱动函数的场景,或者是从一个C++语言实现的嵌入式数据库查询记录的场景,或者是使用Fortran语言实现的一些线性代数库的场景。C语言作为一个通用语言,很多库会选择提供一个C兼容的API,然后用其他不同的编程语言实现(译者:Go语言需要也应该拥抱这些巨大的代码遗产)。

+

在本节中,我们将构建一个简易的数据压缩程序,使用了一个Go语言自带的叫cgo的用于支援C语言函数调用的工具。这类工具一般被称为 foreign-function interfaces (简称ffi),并且在类似工具中cgo也不是唯一的。SWIG(http://swig.org)是另一个类似的且被广泛使用的工具,SWIG提供了很多复杂特性以支援C++的特性,但SWIG并不是我们要讨论的主题。

+

在标准库的compress/...子包有很多流行的压缩算法的编码和解码实现,包括流行的LZW压缩算法(Unix的compress命令用的算法)和DEFLATE压缩算法(GNU gzip命令用的算法)。这些包的API的细节虽然有些差异,但是它们都提供了针对 io.Writer类型输出的压缩接口和提供了针对io.Reader类型输入的解压缩接口。例如:

+
package gzip // compress/gzip
+func NewWriter(w io.Writer) io.WriteCloser
+func NewReader(r io.Reader) (io.ReadCloser, error)
+
+

bzip2压缩算法,是基于优雅的Burrows-Wheeler变换算法,运行速度比gzip要慢,但是可以提供更高的压缩比。标准库的compress/bzip2包目前还没有提供bzip2压缩算法的实现。完全从头开始实现一个压缩算法是一件繁琐的工作,而且 http://bzip.org 已经有现成的libbzip2的开源实现,不仅文档齐全而且性能又好。

+

如果是比较小的C语言库,我们完全可以用纯Go语言重新实现一遍。如果我们对性能也没有特殊要求的话,我们还可以用os/exec包的方法将C编写的应用程序作为一个子进程运行。只有当你需要使用复杂而且性能更高的底层C接口时,就是使用cgo的场景了(译注:用os/exec包调用子进程的方法会导致程序运行时依赖那个应用程序)。下面我们将通过一个例子讲述cgo的具体用法。

+

译注:本章采用的代码都是最新的。因为之前已经出版的书中包含的代码只能在Go1.5之前使用。从Go1.6开始,Go语言已经明确规定了哪些Go语言指针可以直接传入C语言函数。新代码重点是增加了bz2alloc和bz2free的两个函数,用于bz_stream对象空间的申请和释放操作。下面是新代码中增加的注释,说明这个问题:

+
// The version of this program that appeared in the first and second
+// printings did not comply with the proposed rules for passing
+// pointers between Go and C, described here:
+// https://github.com/golang/proposal/blob/master/design/12416-cgo-pointers.md
+//
+// The rules forbid a C function like bz2compress from storing 'in'
+// and 'out' (pointers to variables allocated by Go) into the Go
+// variable 's', even temporarily.
+//
+// The version below, which appears in the third printing, has been
+// corrected.  To comply with the rules, the bz_stream variable must
+// be allocated by C code.  We have introduced two C functions,
+// bz2alloc and bz2free, to allocate and free instances of the
+// bz_stream type.  Also, we have changed bz2compress so that before
+// it returns, it clears the fields of the bz_stream that contain
+// pointers to Go variables.
+
+

要使用libbzip2,我们需要先构建一个bz_stream结构体,用于保持输入和输出缓存。然后有三个函数:BZ2_bzCompressInit用于初始化缓存,BZ2_bzCompress用于将输入缓存的数据压缩到输出缓存,BZ2_bzCompressEnd用于释放不需要的缓存。(目前不要担心包的具体结构,这个例子的目的就是演示各个部分如何组合在一起的。)

+

我们可以在Go代码中直接调用BZ2_bzCompressInit和BZ2_bzCompressEnd,但是对于BZ2_bzCompress,我们将定义一个C语言的包装函数,用它完成真正的工作。下面是C代码,对应一个独立的文件。

+

gopl.io/ch13/bzip

+
/* This file is gopl.io/ch13/bzip/bzip2.c,         */
+/* a simple wrapper for libbzip2 suitable for cgo. */
+#include <bzlib.h>
+
+int bz2compress(bz_stream *s, int action,
+                char *in, unsigned *inlen, char *out, unsigned *outlen) {
+	s->next_in = in;
+	s->avail_in = *inlen;
+	s->next_out = out;
+	s->avail_out = *outlen;
+	int r = BZ2_bzCompress(s, action);
+	*inlen -= s->avail_in;
+	*outlen -= s->avail_out;
+	s->next_in = s->next_out = NULL;
+	return r;
+}
+
+

现在让我们转到Go语言部分,第一部分如下所示。其中import "C"的语句是比较特别的。其实并没有一个叫C的包,但是这行语句会让Go编译程序在编译之前先运行cgo工具。

+
// Package bzip provides a writer that uses bzip2 compression (bzip.org).
+package bzip
+
+/*
+#cgo CFLAGS: -I/usr/include
+#cgo LDFLAGS: -L/usr/lib -lbz2
+#include <bzlib.h>
+#include <stdlib.h>
+bz_stream* bz2alloc() { return calloc(1, sizeof(bz_stream)); }
+int bz2compress(bz_stream *s, int action,
+                char *in, unsigned *inlen, char *out, unsigned *outlen);
+void bz2free(bz_stream* s) { free(s); }
+*/
+import "C"
+
+import (
+	"io"
+	"unsafe"
+)
+
+type writer struct {
+	w      io.Writer // underlying output stream
+	stream *C.bz_stream
+	outbuf [64 * 1024]byte
+}
+
+// NewWriter returns a writer for bzip2-compressed streams.
+func NewWriter(out io.Writer) io.WriteCloser {
+	const blockSize = 9
+	const verbosity = 0
+	const workFactor = 30
+	w := &writer{w: out, stream: C.bz2alloc()}
+	C.BZ2_bzCompressInit(w.stream, blockSize, verbosity, workFactor)
+	return w
+}
+
+

在预处理过程中,cgo工具生成一个临时包用于包含所有在Go语言中访问的C语言的函数或类型。例如C.bz_stream和C.BZ2_bzCompressInit。cgo工具通过以某种特殊的方式调用本地的C编译器来发现在Go源文件导入声明前的注释中包含的C头文件中的内容(译注:import "C"语句前紧挨着的注释是对应cgo的特殊语法,对应必要的构建参数选项和C语言代码)。

+

在cgo注释中还可以包含#cgo指令,用于给C语言工具链指定特殊的参数。例如CFLAGS和LDFLAGS分别对应传给C语言编译器的编译参数和链接器参数,使它们可以从特定目录找到bzlib.h头文件和libbz2.a库文件。这个例子假设你已经在/usr目录成功安装了bzip2库。如果bzip2库是安装在不同的位置,你需要更新这些参数(译注:这里有一个从纯C代码生成的cgo绑定,不依赖bzip2静态库和操作系统的具体环境,具体请访问 https://github.com/chai2010/bzip2 )。

+

NewWriter函数通过调用C语言的BZ2_bzCompressInit函数来初始化stream中的缓存。在writer结构中还包括了另一个buffer,用于输出缓存。

+

下面是Write方法的实现,返回成功压缩数据的大小,主体是一个循环中调用C语言的bz2compress函数实现的。从代码可以看到,Go程序可以访问C语言的bz_stream、char和uint类型,还可以访问bz2compress等函数,甚至可以访问C语言中像BZ_RUN那样的宏定义,全部都是以C.x语法访问。其中C.uint类型和Go语言的uint类型并不相同,即使它们具有相同的大小也是不同的类型。

+
func (w *writer) Write(data []byte) (int, error) {
+	if w.stream == nil {
+		panic("closed")
+	}
+	var total int // uncompressed bytes written
+
+	for len(data) > 0 {
+		inlen, outlen := C.uint(len(data)), C.uint(cap(w.outbuf))
+		C.bz2compress(w.stream, C.BZ_RUN,
+			(*C.char)(unsafe.Pointer(&data[0])), &inlen,
+			(*C.char)(unsafe.Pointer(&w.outbuf)), &outlen)
+		total += int(inlen)
+		data = data[inlen:]
+		if _, err := w.w.Write(w.outbuf[:outlen]); err != nil {
+			return total, err
+		}
+	}
+	return total, nil
+}
+
+

在循环的每次迭代中,向bz2compress传入数据的地址和剩余部分的长度,还有输出缓存w.outbuf的地址和容量。这两个长度信息通过它们的地址传入而不是值传入,因为bz2compress函数可能会根据已经压缩的数据和压缩后数据的大小来更新这两个值。每个块压缩后的数据被写入到底层的io.Writer。

+

Close方法和Write方法有着类似的结构,通过一个循环将剩余的压缩数据刷新到输出缓存。

+
// Close flushes the compressed data and closes the stream.
+// It does not close the underlying io.Writer.
+func (w *writer) Close() error {
+	if w.stream == nil {
+		panic("closed")
+	}
+	defer func() {
+		C.BZ2_bzCompressEnd(w.stream)
+		C.bz2free(w.stream)
+		w.stream = nil
+	}()
+	for {
+		inlen, outlen := C.uint(0), C.uint(cap(w.outbuf))
+		r := C.bz2compress(w.stream, C.BZ_FINISH, nil, &inlen,
+			(*C.char)(unsafe.Pointer(&w.outbuf)), &outlen)
+		if _, err := w.w.Write(w.outbuf[:outlen]); err != nil {
+			return err
+		}
+		if r == C.BZ_STREAM_END {
+			return nil
+		}
+	}
+}
+
+

压缩完成后,Close方法用了defer函数确保函数退出前调用C.BZ2_bzCompressEnd和C.bz2free释放相关的C语言运行时资源。此刻w.stream指针将不再有效,我们将它设置为nil以保证安全,然后在每个方法中增加了nil检测,以防止用户在关闭后依然错误使用相关方法。

+

上面的实现中,不仅仅写是非并发安全的,甚至并发调用Close和Write方法也可能导致程序的的崩溃。修复这个问题是练习13.3的内容。

+

下面的bzipper程序,使用我们自己包实现的bzip2压缩命令。它的行为和许多Unix系统的bzip2命令类似。

+

gopl.io/ch13/bzipper

+
// Bzipper reads input, bzip2-compresses it, and writes it out.
+package main
+
+import (
+	"io"
+	"log"
+	"os"
+	"gopl.io/ch13/bzip"
+)
+
+func main() {
+	w := bzip.NewWriter(os.Stdout)
+	if _, err := io.Copy(w, os.Stdin); err != nil {
+		log.Fatalf("bzipper: %v\n", err)
+	}
+	if err := w.Close(); err != nil {
+		log.Fatalf("bzipper: close: %v\n", err)
+	}
+}
+
+

在上面的场景中,我们使用bzipper压缩了/usr/share/dict/words系统自带的词典,从938,848字节压缩到335,405字节。大约是原始数据大小的三分之一。然后使用系统自带的bunzip2命令进行解压。压缩前后文件的SHA256哈希码是相同了,这也说明了我们的压缩工具是正确的。(如果你的系统没有sha256sum命令,那么请先按照练习4.2实现一个类似的工具)

+
$ go build gopl.io/ch13/bzipper
+$ wc -c < /usr/share/dict/words
+938848
+$ sha256sum < /usr/share/dict/words
+126a4ef38493313edc50b86f90dfdaf7c59ec6c948451eac228f2f3a8ab1a6ed -
+$ ./bzipper < /usr/share/dict/words | wc -c
+335405
+$ ./bzipper < /usr/share/dict/words | bunzip2 | sha256sum
+126a4ef38493313edc50b86f90dfdaf7c59ec6c948451eac228f2f3a8ab1a6ed -
+
+

我们演示了如何将一个C语言库链接到Go语言程序。相反,将Go编译为静态库然后链接到C程序,或者将Go程序编译为动态库然后在C程序中动态加载也都是可行的(译注:在Go1.5中,Windows系统的Go语言实现并不支持生成C语言动态库或静态库的特性。不过好消息是,目前已经有人在尝试解决这个问题,具体请访问 Issue11058 )。这里我们只展示的cgo很小的一些方面,更多的关于内存管理、指针、回调函数、中断信号处理、字符串、errno处理、终结器,以及goroutines和系统线程的关系等,有很多细节可以讨论。特别是如何将Go语言的指针传入C函数的规则也是异常复杂的(译注:简单来说,要传入C函数的Go指针指向的数据本身不能包含指针或其他引用类型;并且C函数在返回后不能继续持有Go指针;并且在C函数返回之前,Go指针是被锁定的,不能导致对应指针数据被移动或栈的调整),部分的原因在13.2节有讨论到,但是在Go1.5中还没有被明确(译注:Go1.6将会明确cgo中的指针使用规则)。如果要进一步阅读,可以从 https://golang.org/cmd/cgo 开始。

+

练习 13.3: 使用sync.Mutex以保证bzip2.writer在多个goroutines中被并发调用是安全的。

+

练习 13.4: 因为C库依赖的限制。 使用os/exec包启动/bin/bzip2命令作为一个子进程,提供一个纯Go的bzip.NewWriter的替代实现(译注:虽然是纯Go实现,但是运行时将依赖/bin/bzip2命令,其他操作系统可能无法运行)。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch13/ch13-05.html b/ch13/ch13-05.html new file mode 100644 index 0000000..1265c51 --- /dev/null +++ b/ch13/ch13-05.html @@ -0,0 +1,243 @@ + + + + + + 几点忠告 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

13.5. 几点忠告

+

我们在前一章结尾的时候,我们警告要谨慎使用reflect包。那些警告同样适用于本章的unsafe包。

+

高级语言使得程序员不用再关心真正运行程序的指令细节,同时也不再需要关注许多如内存布局之类的实现细节。因为高级语言这个绝缘的抽象层,我们可以编写安全健壮的,并且可以运行在不同操作系统上的具有高度可移植性的程序。

+

但是unsafe包,它让程序员可以透过这个绝缘的抽象层直接使用一些必要的功能,虽然可能是为了获得更好的性能。但是代价就是牺牲了可移植性和程序安全,因此使用unsafe包是一个危险的行为。我们对何时以及如何使用unsafe包的建议和我们在11.5节提到的Knuth对过早优化的建议类似。大多数Go程序员可能永远不会需要直接使用unsafe包。当然,也永远都会有一些需要使用unsafe包实现会更简单的场景。如果确实认为使用unsafe包是最理想的方式,那么应该尽可能将它限制在较小的范围,这样其它代码就可以忽略unsafe的影响。

+

现在,赶紧将最后两章抛入脑后吧。编写一些实实在在的应用是真理。请远离reflect和unsafe包,除非你确实需要它们。

+

最后,用Go快乐地编程。我们希望你能像我们一样喜欢Go语言。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch13/ch13.html b/ch13/ch13.html new file mode 100644 index 0000000..aa73acd --- /dev/null +++ b/ch13/ch13.html @@ -0,0 +1,247 @@ + + + + + + 底层编程 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

第13章 底层编程

+

Go语言的设计包含了诸多安全策略,限制了可能导致程序运行出错的用法。编译时类型检查可以发现大多数类型不匹配的操作,例如两个字符串做减法的错误。字符串、map、slice和chan等所有的内置类型,都有严格的类型转换规则。

+

对于无法静态检测到的错误,例如数组访问越界或使用空指针,运行时动态检测可以保证程序在遇到问题的时候立即终止并打印相关的错误信息。自动内存管理(垃圾内存自动回收)可以消除大部分野指针和内存泄漏相关的问题。

+

Go语言的实现刻意隐藏了很多底层细节。我们无法知道一个结构体真实的内存布局,也无法获取一个运行时函数对应的机器码,也无法知道当前的goroutine是运行在哪个操作系统线程之上。事实上,Go语言的调度器会自己决定是否需要将某个goroutine从一个操作系统线程转移到另一个操作系统线程。一个指向变量的指针也并没有展示变量真实的地址。因为垃圾回收器可能会根据需要移动变量的内存位置,当然变量对应的地址也会被自动更新。

+

总的来说,Go语言的这些特性使得Go程序相比较低级的C语言来说更容易预测和理解,程序也不容易崩溃。通过隐藏底层的实现细节,也使得Go语言编写的程序具有高度的可移植性,因为语言的语义在很大程度上是独立于任何编译器实现、操作系统和CPU系统结构的(当然也不是完全绝对独立:例如int等类型就依赖于CPU机器字的大小,某些表达式求值的具体顺序,还有编译器实现的一些额外的限制等)。

+

有时候我们可能会放弃使用部分语言特性而优先选择具有更好性能的方法,例如需要与其他语言编写的库进行互操作,或者用纯Go语言无法实现的某些函数。

+

在本章,我们将展示如何使用unsafe包来摆脱Go语言规则带来的限制,讲述如何创建C语言函数库的绑定,以及如何进行系统调用。

+

本章提供的方法不应该轻易使用(译注:属于黑魔法,虽然功能很强大,但是也容易误伤到自己)。如果没有处理好细节,它们可能导致各种不可预测的并且隐晦的错误,甚至连有经验的C语言程序员也无法理解这些错误。使用unsafe包的同时也放弃了Go语言保证与未来版本的兼容性的承诺,因为它必然会有意无意中使用很多非公开的实现细节,而这些实现的细节在未来的Go语言中很可能会被改变。

+

要注意的是,unsafe包是一个采用特殊方式实现的包。虽然它可以和普通包一样的导入和使用,但它实际上是由编译器实现的。它提供了一些访问语言内部特性的方法,特别是内存布局相关的细节。将这些特性封装到一个独立的包中,是为在极少数情况下需要使用的时候,同时引起人们的注意(译注:因为看包的名字就知道使用unsafe包是不安全的)。此外,有一些环境因为安全的因素可能限制这个包的使用。

+

不过unsafe包被广泛地用于比较低级的包,例如runtime、os、syscall还有net包等,因为它们需要和操作系统密切配合,但是对于普通的程序一般是不需要使用unsafe包的。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch2/ch2-01.html b/ch2/ch2-01.html new file mode 100644 index 0000000..ff904ce --- /dev/null +++ b/ch2/ch2-01.html @@ -0,0 +1,262 @@ + + + + + + 命名 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

2.1. 命名

+

Go语言中的函数名、变量名、常量名、类型名、语句标号和包名等所有的命名,都遵循一个简单的命名规则:一个名字必须以一个字母(Unicode字母)或下划线开头,后面可以跟任意数量的字母、数字或下划线。大写字母和小写字母是不同的:heapSort和Heapsort是两个不同的名字。

+

Go语言中类似if和switch的关键字有25个;关键字不能用于自定义名字,只能在特定语法结构中使用。

+
break      default       func     interface   select
+case       defer         go       map         struct
+chan       else          goto     package     switch
+const      fallthrough   if       range       type
+continue   for           import   return      var
+
+

此外,还有大约30多个预定义的名字,比如int和true等,主要对应内建的常量、类型和函数。

+
内建常量: true false iota nil
+
+内建类型: int int8 int16 int32 int64
+          uint uint8 uint16 uint32 uint64 uintptr
+          float32 float64 complex128 complex64
+          bool byte rune string error
+
+内建函数: make len cap new append copy close delete
+          complex real imag
+          panic recover
+
+

这些内部预先定义的名字并不是关键字,你可以在定义中重新使用它们。在一些特殊的场景中重新定义它们也是有意义的,但是也要注意避免过度而引起语义混乱。

+

如果一个名字是在函数内部定义,那么它就只在函数内部有效。如果是在函数外部定义,那么将在当前包的所有文件中都可以访问。名字的开头字母的大小写决定了名字在包外的可见性。如果一个名字是大写字母开头的(译注:必须是在函数外部定义的包级名字;包级函数名本身也是包级名字),那么它将是导出的,也就是说可以被外部的包访问,例如fmt包的Printf函数就是导出的,可以在fmt包外部访问。包本身的名字一般总是用小写字母。

+

名字的长度没有逻辑限制,但是Go语言的风格是尽量使用短小的名字,对于局部变量尤其是这样;你会经常看到i之类的短名字,而不是冗长的theLoopIndex命名。通常来说,如果一个名字的作用域比较大,生命周期也比较长,那么用长的名字将会更有意义。

+

在习惯上,Go语言程序员推荐使用 驼峰式 命名,当名字由几个单词组成时优先使用大小写分隔,而不是优先用下划线分隔。因此,在标准库有QuoteRuneToASCII和parseRequestLine这样的函数命名,但是一般不会用quote_rune_to_ASCII和parse_request_line这样的命名。而像ASCII和HTML这样的缩略词则避免使用大小写混合的写法,它们可能被称为htmlEscape、HTMLEscape或escapeHTML,但不会是escapeHtml。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch2/ch2-02.html b/ch2/ch2-02.html new file mode 100644 index 0000000..882f745 --- /dev/null +++ b/ch2/ch2-02.html @@ -0,0 +1,275 @@ + + + + + + 声明 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

2.2. 声明

+

声明语句定义了程序的各种实体对象以及部分或全部的属性。Go语言主要有四种类型的声明语句:var、const、type和func,分别对应变量、常量、类型和函数实体对象的声明。这一章我们重点讨论变量和类型的声明,第三章将讨论常量的声明,第五章将讨论函数的声明。

+

一个Go语言编写的程序对应一个或多个以.go为文件后缀名的源文件。每个源文件中以包的声明语句开始,说明该源文件是属于哪个包。包声明语句之后是import语句导入依赖的其它包,然后是包一级的类型、变量、常量、函数的声明语句,包一级的各种类型的声明语句的顺序无关紧要(译注:函数内部的名字则必须先声明之后才能使用)。例如,下面的例子中声明了一个常量、一个函数和两个变量:

+

gopl.io/ch2/boiling

+
// Boiling prints the boiling point of water.
+package main
+
+import "fmt"
+
+const boilingF = 212.0
+
+func main() {
+	var f = boilingF
+	var c = (f - 32) * 5 / 9
+	fmt.Printf("boiling point = %g°F or %g°C\n", f, c)
+	// Output:
+	// boiling point = 212°F or 100°C
+}
+
+

其中常量boilingF是在包一级范围声明语句声明的,然后f和c两个变量是在main函数内部声明的声明语句声明的。在包一级声明语句声明的名字可在整个包对应的每个源文件中访问,而不是仅仅在其声明语句所在的源文件中访问。相比之下,局部声明的名字就只能在函数内部很小的范围被访问。

+

一个函数的声明由一个函数名字、参数列表(由函数的调用者提供参数变量的具体值)、一个可选的返回值列表和包含函数定义的函数体组成。如果函数没有返回值,那么返回值列表是省略的。执行函数从函数的第一个语句开始,依次顺序执行直到遇到return返回语句,如果没有返回语句则是执行到函数末尾,然后返回到函数调用者。

+

我们已经看到过很多函数声明和函数调用的例子了,在第五章将深入讨论函数的相关细节,这里只简单解释下。下面的fToC函数封装了温度转换的处理逻辑,这样它只需要被定义一次,就可以在多个地方多次被使用。在这个例子中,main函数就调用了两次fToC函数,分别使用在局部定义的两个常量作为调用函数的参数。

+

gopl.io/ch2/ftoc

+
// Ftoc prints two Fahrenheit-to-Celsius conversions.
+package main
+
+import "fmt"
+
+func main() {
+	const freezingF, boilingF = 32.0, 212.0
+	fmt.Printf("%g°F = %g°C\n", freezingF, fToC(freezingF)) // "32°F = 0°C"
+	fmt.Printf("%g°F = %g°C\n", boilingF, fToC(boilingF))   // "212°F = 100°C"
+}
+
+func fToC(f float64) float64 {
+	return (f - 32) * 5 / 9
+}
+
+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch2/ch2-03.html b/ch2/ch2-03.html new file mode 100644 index 0000000..55f495a --- /dev/null +++ b/ch2/ch2-03.html @@ -0,0 +1,440 @@ + + + + + + 变量 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

2.3. 变量

+

var声明语句可以创建一个特定类型的变量,然后给变量附加一个名字,并且设置变量的初始值。变量声明的一般语法如下:

+
var 变量名字 类型 = 表达式
+
+

其中“类型”或“= 表达式”两个部分可以省略其中的一个。如果省略的是类型信息,那么将根据初始化表达式来推导变量的类型信息。如果初始化表达式被省略,那么将用零值初始化该变量。 数值类型变量对应的零值是0,布尔类型变量对应的零值是false,字符串类型对应的零值是空字符串,接口或引用类型(包括slice、指针、map、chan和函数)变量对应的零值是nil。数组或结构体等聚合类型对应的零值是每个元素或字段都是对应该类型的零值。

+

零值初始化机制可以确保每个声明的变量总是有一个良好定义的值,因此在Go语言中不存在未初始化的变量。这个特性可以简化很多代码,而且可以在没有增加额外工作的前提下确保边界条件下的合理行为。例如:

+
var s string
+fmt.Println(s) // ""
+
+

这段代码将打印一个空字符串,而不是导致错误或产生不可预知的行为。Go语言程序员应该让一些聚合类型的零值也具有意义,这样可以保证不管任何类型的变量总是有一个合理有效的零值状态。

+

也可以在一个声明语句中同时声明一组变量,或用一组初始化表达式声明并初始化一组变量。如果省略每个变量的类型,将可以声明多个类型不同的变量(类型由初始化表达式推导):

+
var i, j, k int                 // int, int, int
+var b, f, s = true, 2.3, "four" // bool, float64, string
+
+

初始化表达式可以是字面量或任意的表达式。在包级别声明的变量会在main入口函数执行前完成初始化(§2.6.2),局部变量将在声明语句被执行到的时候完成初始化。

+

一组变量也可以通过调用一个函数,由函数返回的多个返回值初始化:

+
var f, err = os.Open(name) // os.Open returns a file and an error
+
+

2.3.1. 简短变量声明

+

在函数内部,有一种称为简短变量声明语句的形式可用于声明和初始化局部变量。它以“名字 := 表达式”形式声明变量,变量的类型根据表达式来自动推导。下面是lissajous函数中的三个简短变量声明语句(§1.4):

+
anim := gif.GIF{LoopCount: nframes}
+freq := rand.Float64() * 3.0
+t := 0.0
+
+

因为简洁和灵活的特点,简短变量声明被广泛用于大部分的局部变量的声明和初始化。var形式的声明语句往往是用于需要显式指定变量类型的地方,或者因为变量稍后会被重新赋值而初始值无关紧要的地方。

+
i := 100                  // an int
+var boiling float64 = 100 // a float64
+var names []string
+var err error
+var p Point
+
+

和var形式声明语句一样,简短变量声明语句也可以用来声明和初始化一组变量:

+
i, j := 0, 1
+
+

但是这种同时声明多个变量的方式应该限制只在可以提高代码可读性的地方使用,比如for语句的循环的初始化语句部分。

+

请记住“:=”是一个变量声明语句,而“=”是一个变量赋值操作。也不要混淆多个变量的声明和元组的多重赋值(§2.4.1),后者是将右边各个表达式的值赋值给左边对应位置的各个变量:

+
i, j = j, i // 交换 i 和 j 的值
+
+

和普通var形式的变量声明语句一样,简短变量声明语句也可以用函数的返回值来声明和初始化变量,像下面的os.Open函数调用将返回两个值:

+
f, err := os.Open(name)
+if err != nil {
+	return err
+}
+// ...use f...
+f.Close()
+
+

这里有一个比较微妙的地方:简短变量声明左边的变量可能并不是全部都是刚刚声明的。如果有一些已经在相同的词法域声明过了(§2.7),那么简短变量声明语句对这些已经声明过的变量就只有赋值行为了。

+

在下面的代码中,第一个语句声明了in和err两个变量。在第二个语句只声明了out一个变量,然后对已经声明的err进行了赋值操作。

+
in, err := os.Open(infile)
+// ...
+out, err := os.Create(outfile)
+
+

简短变量声明语句中必须至少要声明一个新的变量,下面的代码将不能编译通过:

+
f, err := os.Open(infile)
+// ...
+f, err := os.Create(outfile) // compile error: no new variables
+
+

解决的方法是第二个简短变量声明语句改用普通的多重赋值语句。

+

简短变量声明语句只有对已经在同级词法域声明过的变量才和赋值操作语句等价,如果变量是在外部词法域声明的,那么简短变量声明语句将会在当前词法域重新声明一个新的变量。我们在本章后面将会看到类似的例子。

+

2.3.2. 指针

+

一个变量对应一个保存了变量对应类型值的内存空间。普通变量在声明语句创建时被绑定到一个变量名,比如叫x的变量,但是还有很多变量始终以表达式方式引入,例如x[i]或x.f变量。所有这些表达式一般都是读取一个变量的值,除非它们是出现在赋值语句的左边,这种时候是给对应变量赋予一个新的值。

+

一个指针的值是另一个变量的地址。一个指针对应变量在内存中的存储位置。并不是每一个值都会有一个内存地址,但是对于每一个变量必然有对应的内存地址。通过指针,我们可以直接读或更新对应变量的值,而不需要知道该变量的名字(如果变量有名字的话)。

+

如果用“var x int”声明语句声明一个x变量,那么&x表达式(取x变量的内存地址)将产生一个指向该整数变量的指针,指针对应的数据类型是*int,指针被称之为“指向int类型的指针”。如果指针名字为p,那么可以说“p指针指向变量x”,或者说“p指针保存了x变量的内存地址”。同时*p表达式对应p指针指向的变量的值。一般*p表达式读取指针指向的变量的值,这里为int类型的值,同时因为*p对应一个变量,所以该表达式也可以出现在赋值语句的左边,表示更新指针所指向的变量的值。

+
x := 1
+p := &x         // p, of type *int, points to x
+fmt.Println(*p) // "1"
+*p = 2          // equivalent to x = 2
+fmt.Println(x)  // "2"
+
+

对于聚合类型每个成员——比如结构体的每个字段、或者是数组的每个元素——也都是对应一个变量,因此可以被取地址。

+

变量有时候被称为可寻址的值。即使变量由表达式临时生成,那么表达式也必须能接受&取地址操作。

+

任何类型的指针的零值都是nil。如果p指向某个有效变量,那么p != nil测试为真。指针之间也是可以进行相等测试的,只有当它们指向同一个变量或全部是nil时才相等。

+
var x, y int
+fmt.Println(&x == &x, &x == &y, &x == nil) // "true false false"
+
+

在Go语言中,返回函数中局部变量的地址也是安全的。例如下面的代码,调用f函数时创建局部变量v,在局部变量地址被返回之后依然有效,因为指针p依然引用这个变量。

+
var p = f()
+
+func f() *int {
+	v := 1
+	return &v
+}
+
+

每次调用f函数都将返回不同的结果:

+
fmt.Println(f() == f()) // "false"
+
+

因为指针包含了一个变量的地址,因此如果将指针作为参数调用函数,那将可以在函数中通过该指针来更新变量的值。例如下面这个例子就是通过指针来更新变量的值,然后返回更新后的值,可用在一个表达式中(译注:这是对C语言中++v操作的模拟,这里只是为了说明指针的用法,incr函数模拟的做法并不推荐):

+
func incr(p *int) int {
+	*p++ // 非常重要:只是增加p指向的变量的值,并不改变p指针!!!
+	return *p
+}
+
+v := 1
+incr(&v)              // side effect: v is now 2
+fmt.Println(incr(&v)) // "3" (and v is 3)
+
+

每次我们对一个变量取地址,或者复制指针,我们都是为原变量创建了新的别名。例如,*p就是变量v的别名。指针特别有价值的地方在于我们可以不用名字而访问一个变量,但是这是一把双刃剑:要找到一个变量的所有访问者并不容易,我们必须知道变量全部的别名(译注:这是Go语言的垃圾回收器所做的工作)。不仅仅是指针会创建别名,很多其他引用类型也会创建别名,例如slice、map和chan,甚至结构体、数组和接口都会创建所引用变量的别名。

+

指针是实现标准库中flag包的关键技术,它使用命令行参数来设置对应变量的值,而这些对应命令行标志参数的变量可能会零散分布在整个程序中。为了说明这一点,在早些的echo版本中,就包含了两个可选的命令行参数:-n用于忽略行尾的换行符,-s sep用于指定分隔字符(默认是空格)。下面这是第四个版本,对应包路径为gopl.io/ch2/echo4。

+

gopl.io/ch2/echo4

+
// Echo4 prints its command-line arguments.
+package main
+
+import (
+	"flag"
+	"fmt"
+	"strings"
+)
+
+var n = flag.Bool("n", false, "omit trailing newline")
+var sep = flag.String("s", " ", "separator")
+
+func main() {
+	flag.Parse()
+	fmt.Print(strings.Join(flag.Args(), *sep))
+	if !*n {
+		fmt.Println()
+	}
+}
+
+

调用flag.Bool函数会创建一个新的对应布尔型标志参数的变量。它有三个属性:第一个是命令行标志参数的名字“n”,然后是该标志参数的默认值(这里是false),最后是该标志参数对应的描述信息。如果用户在命令行输入了一个无效的标志参数,或者输入-h-help参数,那么将打印所有标志参数的名字、默认值和描述信息。类似的,调用flag.String函数将创建一个对应字符串类型的标志参数变量,同样包含命令行标志参数对应的参数名、默认值、和描述信息。程序中的sepn变量分别是指向对应命令行标志参数变量的指针,因此必须用*sep*n形式的指针语法间接引用它们。

+

当程序运行时,必须在使用标志参数对应的变量之前先调用flag.Parse函数,用于更新每个标志参数对应变量的值(之前是默认值)。对于非标志参数的普通命令行参数可以通过调用flag.Args()函数来访问,返回值对应一个字符串类型的slice。如果在flag.Parse函数解析命令行参数时遇到错误,默认将打印相关的提示信息,然后调用os.Exit(2)终止程序。

+

让我们运行一些echo测试用例:

+
$ go build gopl.io/ch2/echo4
+$ ./echo4 a bc def
+a bc def
+$ ./echo4 -s / a bc def
+a/bc/def
+$ ./echo4 -n a bc def
+a bc def$
+$ ./echo4 -help
+Usage of ./echo4:
+  -n    omit trailing newline
+  -s string
+        separator (default " ")
+
+

2.3.3. new函数

+

另一个创建变量的方法是调用内建的new函数。表达式new(T)将创建一个T类型的匿名变量,初始化为T类型的零值,然后返回变量地址,返回的指针类型为*T

+
p := new(int)   // p, *int 类型, 指向匿名的 int 变量
+fmt.Println(*p) // "0"
+*p = 2          // 设置 int 匿名变量的值为 2
+fmt.Println(*p) // "2"
+
+

用new创建变量和普通变量声明语句方式创建变量没有什么区别,除了不需要声明一个临时变量的名字外,我们还可以在表达式中使用new(T)。换言之,new函数类似是一种语法糖,而不是一个新的基础概念。

+

下面的两个newInt函数有着相同的行为:

+
func newInt() *int {
+	return new(int)
+}
+
+func newInt() *int {
+	var dummy int
+	return &dummy
+}
+
+

每次调用new函数都是返回一个新的变量的地址,因此下面两个地址是不同的:

+
p := new(int)
+q := new(int)
+fmt.Println(p == q) // "false"
+
+

当然也可能有特殊情况:如果两个类型都是空的,也就是说类型的大小是0,例如struct{}[0]int,有可能有相同的地址(依赖具体的语言实现)(译注:请谨慎使用大小为0的类型,因为如果类型的大小为0的话,可能导致Go语言的自动垃圾回收器有不同的行为,具体请查看runtime.SetFinalizer函数相关文档)。

+

new函数使用通常相对比较少,因为对于结构体来说,直接用字面量语法创建新变量的方法会更灵活(§4.4.1)。

+

由于new只是一个预定义的函数,它并不是一个关键字,因此我们可以将new名字重新定义为别的类型。例如下面的例子:

+
func delta(old, new int) int { return new - old }
+
+

由于new被定义为int类型的变量名,因此在delta函数内部是无法使用内置的new函数的。

+

2.3.4. 变量的生命周期

+

变量的生命周期指的是在程序运行期间变量有效存在的时间段。对于在包一级声明的变量来说,它们的生命周期和整个程序的运行周期是一致的。而相比之下,局部变量的生命周期则是动态的:每次从创建一个新变量的声明语句开始,直到该变量不再被引用为止,然后变量的存储空间可能被回收。函数的参数变量和返回值变量都是局部变量。它们在函数每次被调用的时候创建。

+

例如,下面是从1.4节的Lissajous程序摘录的代码片段:

+
for t := 0.0; t < cycles*2*math.Pi; t += res {
+	x := math.Sin(t)
+	y := math.Sin(t*freq + phase)
+	img.SetColorIndex(size+int(x*size+0.5), size+int(y*size+0.5),
+		blackIndex)
+}
+
+

译注:函数的右小括弧也可以另起一行缩进,同时为了防止编译器在行尾自动插入分号而导致的编译错误,可以在末尾的参数变量后面显式插入逗号。像下面这样:

+
for t := 0.0; t < cycles*2*math.Pi; t += res {
+	x := math.Sin(t)
+	y := math.Sin(t*freq + phase)
+	img.SetColorIndex(
+		size+int(x*size+0.5), size+int(y*size+0.5),
+		blackIndex, // 最后插入的逗号不会导致编译错误,这是Go编译器的一个特性
+	)               // 小括弧另起一行缩进,和大括弧的风格保存一致
+}
+
+

在每次循环的开始会创建临时变量t,然后在每次循环迭代中创建临时变量x和y。

+

那么Go语言的自动垃圾收集器是如何知道一个变量是何时可以被回收的呢?这里我们可以避开完整的技术细节,基本的实现思路是,从每个包级的变量和每个当前运行函数的每一个局部变量开始,通过指针或引用的访问路径遍历,是否可以找到该变量。如果不存在这样的访问路径,那么说明该变量是不可达的,也就是说它是否存在并不会影响程序后续的计算结果。

+

因为一个变量的有效周期只取决于是否可达,因此一个循环迭代内部的局部变量的生命周期可能超出其局部作用域。同时,局部变量可能在函数返回之后依然存在。

+

编译器会自动选择在栈上还是在堆上分配局部变量的存储空间,但可能令人惊讶的是,这个选择并不是由用var还是new声明变量的方式决定的。

+
var global *int
+
+func f() {
+	var x int
+	x = 1
+	global = &x
+}
+
+func g() {
+	y := new(int)
+	*y = 1
+}
+
+

f函数里的x变量必须在堆上分配,因为它在函数退出后依然可以通过包一级的global变量找到,虽然它是在函数内部定义的;用Go语言的术语说,这个x局部变量从函数f中逃逸了。相反,当g函数返回时,变量*y将是不可达的,也就是说可以马上被回收的。因此,*y并没有从函数g中逃逸,编译器可以选择在栈上分配*y的存储空间(译注:也可以选择在堆上分配,然后由Go语言的GC回收这个变量的内存空间),虽然这里用的是new方式。其实在任何时候,你并不需为了编写正确的代码而要考虑变量的逃逸行为,要记住的是,逃逸的变量需要额外分配内存,同时对性能的优化可能会产生细微的影响。

+

Go语言的自动垃圾收集器对编写正确的代码是一个巨大的帮助,但也并不是说你完全不用考虑内存了。你虽然不需要显式地分配和释放内存,但是要编写高效的程序你依然需要了解变量的生命周期。例如,如果将指向短生命周期对象的指针保存到具有长生命周期的对象中,特别是保存到全局变量时,会阻止对短生命周期对象的垃圾回收(从而可能影响程序的性能)。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch2/ch2-04.html b/ch2/ch2-04.html new file mode 100644 index 0000000..7f54b0c --- /dev/null +++ b/ch2/ch2-04.html @@ -0,0 +1,314 @@ + + + + + + 赋值 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

2.4. 赋值

+

使用赋值语句可以更新一个变量的值,最简单的赋值语句是将要被赋值的变量放在=的左边,新值的表达式放在=的右边。

+
x = 1                       // 命名变量的赋值
+*p = true                   // 通过指针间接赋值
+person.name = "bob"         // 结构体字段赋值
+count[x] = count[x] * scale // 数组、slice或map的元素赋值
+
+

特定的二元算术运算符和赋值语句的复合操作有一个简洁形式,例如上面最后的语句可以重写为:

+
count[x] *= scale
+
+

这样可以省去对变量表达式的重复计算。

+

数值变量也可以支持++递增和--递减语句(译注:自增和自减是语句,而不是表达式,因此x = i++之类的表达式是错误的):

+
v := 1
+v++    // 等价方式 v = v + 1;v 变成 2
+v--    // 等价方式 v = v - 1;v 变成 1
+
+

2.4.1. 元组赋值

+

元组赋值是另一种形式的赋值语句,它允许同时更新多个变量的值。在赋值之前,赋值语句右边的所有表达式将会先进行求值,然后再统一更新左边对应变量的值。这对于处理有些同时出现在元组赋值语句左右两边的变量很有帮助,例如我们可以这样交换两个变量的值:

+
x, y = y, x
+
+a[i], a[j] = a[j], a[i]
+
+

或者是计算两个整数值的的最大公约数(GCD)(译注:GCD不是那个敏感字,而是greatest common divisor的缩写,欧几里德的GCD是最早的非平凡算法):

+
func gcd(x, y int) int {
+	for y != 0 {
+		x, y = y, x%y
+	}
+	return x
+}
+
+

或者是计算斐波纳契数列(Fibonacci)的第N个数:

+
func fib(n int) int {
+	x, y := 0, 1
+	for i := 0; i < n; i++ {
+		x, y = y, x+y
+	}
+	return x
+}
+
+

元组赋值也可以使一系列琐碎赋值更加紧凑(译注: 特别是在for循环的初始化部分),

+
i, j, k = 2, 3, 5
+
+

但如果表达式太复杂的话,应该尽量避免过度使用元组赋值;因为每个变量单独赋值语句的写法可读性会更好。

+

有些表达式会产生多个值,比如调用一个有多个返回值的函数。当这样一个函数调用出现在元组赋值右边的表达式中时(译注:右边不能再有其它表达式),左边变量的数目必须和右边一致。

+
f, err = os.Open("foo.txt") // function call returns two values
+
+

通常,这类函数会用额外的返回值来表达某种错误类型,例如os.Open是用额外的返回值返回一个error类型的错误,还有一些是用来返回布尔值,通常被称为ok。在稍后我们将看到的三个操作都是类似的用法。如果map查找(§4.3)、类型断言(§7.10)或通道接收(§8.4.2)出现在赋值语句的右边,它们都可能会产生两个结果,有一个额外的布尔结果表示操作是否成功:

+
v, ok = m[key]             // map lookup
+v, ok = x.(T)              // type assertion
+v, ok = <-ch               // channel receive
+
+

译注:map查找(§4.3)、类型断言(§7.10)或通道接收(§8.4.2)出现在赋值语句的右边时,并不一定是产生两个结果,也可能只产生一个结果。对于只产生一个结果的情形,map查找失败时会返回零值,类型断言失败时会发生运行时panic异常,通道接收失败时会返回零值(阻塞不算是失败)。例如下面的例子:

+
v = m[key]                // map查找,失败时返回零值
+v = x.(T)                 // type断言,失败时panic异常
+v = <-ch                  // 管道接收,失败时返回零值(阻塞不算是失败)
+
+_, ok = m[key]            // map返回2个值
+_, ok = mm[""], false     // map返回1个值
+_ = mm[""]                // map返回1个值
+
+

和变量声明一样,我们可以用下划线空白标识符_来丢弃不需要的值。

+
_, err = io.Copy(dst, src) // 丢弃字节数
+_, ok = x.(T)              // 只检测类型,忽略具体值
+
+

2.4.2. 可赋值性

+

赋值语句是显式的赋值形式,但是程序中还有很多地方会发生隐式的赋值行为:函数调用会隐式地将调用参数的值赋值给函数的参数变量,一个返回语句会隐式地将返回操作的值赋值给结果变量,一个复合类型的字面量(§4.2)也会产生赋值行为。例如下面的语句:

+
medals := []string{"gold", "silver", "bronze"}
+
+

隐式地对slice的每个元素进行赋值操作,类似这样写的行为:

+
medals[0] = "gold"
+medals[1] = "silver"
+medals[2] = "bronze"
+
+

map和chan的元素,虽然不是普通的变量,但是也有类似的隐式赋值行为。

+

不管是隐式还是显式地赋值,在赋值语句左边的变量和右边最终的求到的值必须有相同的数据类型。更直白地说,只有右边的值对于左边的变量是可赋值的,赋值语句才是允许的。

+

可赋值性的规则对于不同类型有着不同要求,对每个新类型特殊的地方我们会专门解释。对于目前我们已经讨论过的类型,它的规则是简单的:类型必须完全匹配,nil可以赋值给任何指针或引用类型的变量。常量(§3.6)则有更灵活的赋值规则,因为这样可以避免不必要的显式的类型转换。

+

对于两个值是否可以用==!=进行相等比较的能力也和可赋值能力有关系:对于任何类型的值的相等比较,第二个值必须是对第一个值类型对应的变量是可赋值的,反之亦然。和前面一样,我们会对每个新类型比较特殊的地方做专门的解释。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch2/ch2-05.html b/ch2/ch2-05.html new file mode 100644 index 0000000..6fc53f5 --- /dev/null +++ b/ch2/ch2-05.html @@ -0,0 +1,301 @@ + + + + + + 类型 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

2.5. 类型

+

变量或表达式的类型定义了对应存储值的属性特征,例如数值在内存的存储大小(或者是元素的bit个数),它们在内部是如何表达的,是否支持一些操作符,以及它们自己关联的方法集等。

+

在任何程序中都会存在一些变量有着相同的内部结构,但是却表示完全不同的概念。例如,一个int类型的变量可以用来表示一个循环的迭代索引、或者一个时间戳、或者一个文件描述符、或者一个月份;一个float64类型的变量可以用来表示每秒移动几米的速度、或者是不同温度单位下的温度;一个字符串可以用来表示一个密码或者一个颜色的名称。

+

一个类型声明语句创建了一个新的类型名称,和现有类型具有相同的底层结构。新命名的类型提供了一个方法,用来分隔不同概念的类型,这样即使它们底层类型相同也是不兼容的。

+
type 类型名字 底层类型
+
+

类型声明语句一般出现在包一级,因此如果新创建的类型名字的首字符大写,则在包外部也可以使用。

+

译注:对于中文汉字,Unicode标志都作为小写字母处理,因此中文的命名默认不能导出;不过国内的用户针对该问题提出了不同的看法,根据RobPike的回复,在Go2中有可能会将中日韩等字符当作大写字母处理。下面是RobPik在 Issue763 的回复:

+
+

A solution that's been kicking around for a while:

+

For Go 2 (can't do it before then): Change the definition to “lower case letters and _ are package-local; all else is exported”. Then with non-cased languages, such as Japanese, we can write 日本语 for an exported name and _日本语 for a local name. This rule has no effect, relative to the Go 1 rule, with cased languages. They behave exactly the same.

+
+

为了说明类型声明,我们将不同温度单位分别定义为不同的类型:

+

gopl.io/ch2/tempconv0

+
// Package tempconv performs Celsius and Fahrenheit temperature computations.
+package tempconv
+
+import "fmt"
+
+type Celsius float64    // 摄氏温度
+type Fahrenheit float64 // 华氏温度
+
+const (
+	AbsoluteZeroC Celsius = -273.15 // 绝对零度
+	FreezingC     Celsius = 0       // 结冰点温度
+	BoilingC      Celsius = 100     // 沸水温度
+)
+
+func CToF(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) }
+
+func FToC(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) }
+
+

我们在这个包声明了两种类型:Celsius和Fahrenheit分别对应不同的温度单位。它们虽然有着相同的底层类型float64,但是它们是不同的数据类型,因此它们不可以被相互比较或混在一个表达式运算。刻意区分类型,可以避免一些像无意中使用不同单位的温度混合计算导致的错误;因此需要一个类似Celsius(t)或Fahrenheit(t)形式的显式转型操作才能将float64转为对应的类型。Celsius(t)和Fahrenheit(t)是类型转换操作,它们并不是函数调用。类型转换不会改变值本身,但是会使它们的语义发生变化。另一方面,CToF和FToC两个函数则是对不同温度单位下的温度进行换算,它们会返回不同的值。

+

对于每一个类型T,都有一个对应的类型转换操作T(x),用于将x转为T类型(译注:如果T是指针类型,可能会需要用小括弧包装T,比如(*int)(0))。只有当两个类型的底层基础类型相同时,才允许这种转型操作,或者是两者都是指向相同底层结构的指针类型,这些转换只改变类型而不会影响值本身。如果x是可以赋值给T类型的值,那么x必然也可以被转为T类型,但是一般没有这个必要。

+

数值类型之间的转型也是允许的,并且在字符串和一些特定类型的slice之间也是可以转换的,在下一章我们会看到这样的例子。这类转换可能改变值的表现。例如,将一个浮点数转为整数将丢弃小数部分,将一个字符串转为[]byte类型的slice将拷贝一个字符串数据的副本。在任何情况下,运行时不会发生转换失败的错误(译注: 错误只会发生在编译阶段)。

+

底层数据类型决定了内部结构和表达方式,也决定是否可以像底层类型一样对内置运算符的支持。这意味着,Celsius和Fahrenheit类型的算术运算行为和底层的float64类型是一样的,正如我们所期望的那样。

+
fmt.Printf("%g\n", BoilingC-FreezingC) // "100" °C
+boilingF := CToF(BoilingC)
+fmt.Printf("%g\n", boilingF-CToF(FreezingC)) // "180" °F
+fmt.Printf("%g\n", boilingF-FreezingC)       // compile error: type mismatch
+
+

比较运算符==<也可以用来比较一个命名类型的变量和另一个有相同类型的变量,或有着相同底层类型的未命名类型的值之间做比较。但是如果两个值有着不同的类型,则不能直接进行比较:

+
var c Celsius
+var f Fahrenheit
+fmt.Println(c == 0)          // "true"
+fmt.Println(f >= 0)          // "true"
+fmt.Println(c == f)          // compile error: type mismatch
+fmt.Println(c == Celsius(f)) // "true"!
+
+

注意最后那个语句。尽管看起来像函数调用,但是Celsius(f)是类型转换操作,它并不会改变值,仅仅是改变值的类型而已。测试为真的原因是因为c和f都是零值。

+

一个命名的类型可以提供书写方便,特别是可以避免一遍又一遍地书写复杂类型(译注:例如用匿名的结构体定义变量)。虽然对于像float64这种简单的底层类型没有简洁很多,但是如果是复杂的类型将会简洁很多,特别是我们即将讨论的结构体类型。

+

命名类型还可以为该类型的值定义新的行为。这些行为表示为一组关联到该类型的函数集合,我们称为类型的方法集。我们将在第六章中讨论方法的细节,这里只说些简单用法。

+

下面的声明语句,Celsius类型的参数c出现在了函数名的前面,表示声明的是Celsius类型的一个名叫String的方法,该方法返回该类型对象c带着°C温度单位的字符串:

+
func (c Celsius) String() string { return fmt.Sprintf("%g°C", c) }
+
+

许多类型都会定义一个String方法,因为当使用fmt包的打印方法时,将会优先使用该类型对应的String方法返回的结果打印,我们将在7.1节讲述。

+
c := FToC(212.0)
+fmt.Println(c.String()) // "100°C"
+fmt.Printf("%v\n", c)   // "100°C"; no need to call String explicitly
+fmt.Printf("%s\n", c)   // "100°C"
+fmt.Println(c)          // "100°C"
+fmt.Printf("%g\n", c)   // "100"; does not call String
+fmt.Println(float64(c)) // "100"; does not call String
+
+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch2/ch2-06.html b/ch2/ch2-06.html new file mode 100644 index 0000000..b071a22 --- /dev/null +++ b/ch2/ch2-06.html @@ -0,0 +1,378 @@ + + + + + + 包和文件 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

2.6. 包和文件

+

Go语言中的包和其他语言的库或模块的概念类似,目的都是为了支持模块化、封装、单独编译和代码重用。一个包的源代码保存在一个或多个以.go为文件后缀名的源文件中,通常一个包所在目录路径的后缀是包的导入路径;例如包gopl.io/ch1/helloworld对应的目录路径是$GOPATH/src/gopl.io/ch1/helloworld。

+

每个包都对应一个独立的名字空间。例如,在image包中的Decode函数和在unicode/utf16包中的 Decode函数是不同的。要在外部引用该函数,必须显式使用image.Decode或utf16.Decode形式访问。

+

包还可以让我们通过控制哪些名字是外部可见的来隐藏内部实现信息。在Go语言中,一个简单的规则是:如果一个名字是大写字母开头的,那么该名字是导出的(译注:因为汉字不区分大小写,因此汉字开头的名字是没有导出的)。

+

为了演示包基本的用法,先假设我们的温度转换软件已经很流行,我们希望到Go语言社区也能使用这个包。我们该如何做呢?

+

让我们创建一个名为gopl.io/ch2/tempconv的包,这是前面例子的一个改进版本。(这里我们没有按照惯例按顺序对例子进行编号,因此包路径看起来更像一个真实的包)包代码存储在两个源文件中,用来演示如何在一个源文件声明然后在其他的源文件访问;虽然在现实中,这样小的包一般只需要一个文件。

+

我们把变量的声明、对应的常量,还有方法都放到tempconv.go源文件中:

+

gopl.io/ch2/tempconv

+
// Package tempconv performs Celsius and Fahrenheit conversions.
+package tempconv
+
+import "fmt"
+
+type Celsius float64
+type Fahrenheit float64
+
+const (
+	AbsoluteZeroC Celsius = -273.15
+	FreezingC     Celsius = 0
+	BoilingC      Celsius = 100
+)
+
+func (c Celsius) String() string    { return fmt.Sprintf("%g°C", c) }
+func (f Fahrenheit) String() string { return fmt.Sprintf("%g°F", f) }
+
+

转换函数则放在另一个conv.go源文件中:

+
package tempconv
+
+// CToF converts a Celsius temperature to Fahrenheit.
+func CToF(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) }
+
+// FToC converts a Fahrenheit temperature to Celsius.
+func FToC(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) }
+
+

每个源文件都是以包的声明语句开始,用来指明包的名字。当包被导入的时候,包内的成员将通过类似tempconv.CToF的形式访问。而包级别的名字,例如在一个文件声明的类型和常量,在同一个包的其他源文件也是可以直接访问的,就好像所有代码都在一个文件一样。要注意的是tempconv.go源文件导入了fmt包,但是conv.go源文件并没有,因为这个源文件中的代码并没有用到fmt包。

+

因为包级别的常量名都是以大写字母开头,它们可以像tempconv.AbsoluteZeroC这样被外部代码访问:

+
fmt.Printf("Brrrr! %v\n", tempconv.AbsoluteZeroC) // "Brrrr! -273.15°C"
+
+

要将摄氏温度转换为华氏温度,需要先用import语句导入gopl.io/ch2/tempconv包,然后就可以使用下面的代码进行转换了:

+
fmt.Println(tempconv.CToF(tempconv.BoilingC)) // "212°F"
+
+

在每个源文件的包声明前紧跟着的注释是包注释(§10.7.4)。通常,包注释的第一句应该先是包的功能概要说明。一个包通常只有一个源文件有包注释(译注:如果有多个包注释,目前的文档工具会根据源文件名的先后顺序将它们链接为一个包注释)。如果包注释很大,通常会放到一个独立的doc.go文件中。

+

练习 2.1: 向tempconv包添加类型、常量和函数用来处理Kelvin绝对温度的转换,Kelvin 绝对零度是−273.15°C,Kelvin绝对温度1K和摄氏度1°C的单位间隔是一样的。

+

2.6.1. 导入包

+

在Go语言程序中,每个包都有一个全局唯一的导入路径。导入语句中类似"gopl.io/ch2/tempconv"的字符串对应包的导入路径。Go语言的规范并没有定义这些字符串的具体含义或包来自哪里,它们是由构建工具来解释的。当使用Go语言自带的go工具箱时(第十章),一个导入路径代表一个目录中的一个或多个Go源文件。

+

除了包的导入路径,每个包还有一个包名,包名一般是短小的名字(并不要求包名是唯一的),包名在包的声明处指定。按照惯例,一个包的名字和包的导入路径的最后一个字段相同,例如gopl.io/ch2/tempconv包的名字一般是tempconv。

+

要使用gopl.io/ch2/tempconv包,需要先导入:

+

gopl.io/ch2/cf

+
// Cf converts its numeric argument to Celsius and Fahrenheit.
+package main
+
+import (
+	"fmt"
+	"os"
+	"strconv"
+
+	"gopl.io/ch2/tempconv"
+)
+
+func main() {
+	for _, arg := range os.Args[1:] {
+		t, err := strconv.ParseFloat(arg, 64)
+		if err != nil {
+			fmt.Fprintf(os.Stderr, "cf: %v\n", err)
+			os.Exit(1)
+		}
+		f := tempconv.Fahrenheit(t)
+		c := tempconv.Celsius(t)
+		fmt.Printf("%s = %s, %s = %s\n",
+			f, tempconv.FToC(f), c, tempconv.CToF(c))
+	}
+}
+
+

导入语句将导入的包绑定到一个短小的名字,然后通过该短小的名字就可以引用包中导出的全部内容。上面的导入声明将允许我们以tempconv.CToF的形式来访问gopl.io/ch2/tempconv包中的内容。在默认情况下,导入的包绑定到tempconv名字(译注:指包声明语句指定的名字),但是我们也可以绑定到另一个名称,以避免名字冲突(§10.4)。

+

cf程序将命令行输入的一个温度在Celsius和Fahrenheit温度单位之间转换:

+
$ go build gopl.io/ch2/cf
+$ ./cf 32
+32°F = 0°C, 32°C = 89.6°F
+$ ./cf 212
+212°F = 100°C, 212°C = 413.6°F
+$ ./cf -40
+-40°F = -40°C, -40°C = -40°F
+
+

如果导入了一个包,但是又没有使用该包将被当作一个编译错误处理。这种强制规则可以有效减少不必要的依赖,虽然在调试期间可能会让人讨厌,因为删除一个类似log.Print("got here!")的打印语句可能导致需要同时删除log包导入声明,否则,编译器将会发出一个错误。在这种情况下,我们需要将不必要的导入删除或注释掉。

+

不过有更好的解决方案,我们可以使用golang.org/x/tools/cmd/goimports导入工具,它可以根据需要自动添加或删除导入的包;许多编辑器都可以集成goimports工具,然后在保存文件的时候自动运行。类似的还有gofmt工具,可以用来格式化Go源文件。

+

练习 2.2: 写一个通用的单位转换程序,用类似cf程序的方式从命令行读取参数,如果缺省的话则是从标准输入读取参数,然后做类似Celsius和Fahrenheit的单位转换,长度单位可以对应英尺和米,重量单位可以对应磅和公斤等。

+

2.6.2. 包的初始化

+

包的初始化首先是解决包级变量的依赖顺序,然后按照包级变量声明出现的顺序依次初始化:

+
var a = b + c // a 第三个初始化, 为 3
+var b = f()   // b 第二个初始化, 为 2, 通过调用 f (依赖c)
+var c = 1     // c 第一个初始化, 为 1
+
+func f() int { return c + 1 }
+
+

如果包中含有多个.go源文件,它们将按照发给编译器的顺序进行初始化,Go语言的构建工具首先会将.go文件根据文件名排序,然后依次调用编译器编译。

+

对于在包级别声明的变量,如果有初始化表达式则用表达式初始化,还有一些没有初始化表达式的,例如某些表格数据初始化并不是一个简单的赋值过程。在这种情况下,我们可以用一个特殊的init初始化函数来简化初始化工作。每个文件都可以包含多个init初始化函数

+
func init() { /* ... */ }
+
+

这样的init初始化函数除了不能被调用或引用外,其他行为和普通函数类似。在每个文件中的init初始化函数,在程序开始执行时按照它们声明的顺序被自动调用。

+

每个包在解决依赖的前提下,以导入声明的顺序初始化,每个包只会被初始化一次。因此,如果一个p包导入了q包,那么在p包初始化的时候可以认为q包必然已经初始化过了。初始化工作是自下而上进行的,main包最后被初始化。以这种方式,可以确保在main函数执行之前,所有依赖的包都已经完成初始化工作了。

+

下面的代码定义了一个PopCount函数,用于返回一个数字中含二进制1bit的个数。它使用init初始化函数来生成辅助表格pc,pc表格用于处理每个8bit宽度的数字含二进制的1bit的bit个数,这样的话在处理64bit宽度的数字时就没有必要循环64次,只需要8次查表就可以了。(这并不是最快的统计1bit数目的算法,但是它可以方便演示init函数的用法,并且演示了如何预生成辅助表格,这是编程中常用的技术)。

+

gopl.io/ch2/popcount

+
package popcount
+
+// pc[i] is the population count of i.
+var pc [256]byte
+
+func init() {
+	for i := range pc {
+		pc[i] = pc[i/2] + byte(i&1)
+	}
+}
+
+// PopCount returns the population count (number of set bits) of x.
+func PopCount(x uint64) int {
+	return int(pc[byte(x>>(0*8))] +
+		pc[byte(x>>(1*8))] +
+		pc[byte(x>>(2*8))] +
+		pc[byte(x>>(3*8))] +
+		pc[byte(x>>(4*8))] +
+		pc[byte(x>>(5*8))] +
+		pc[byte(x>>(6*8))] +
+		pc[byte(x>>(7*8))])
+}
+
+

译注:对于pc这类需要复杂处理的初始化,可以通过将初始化逻辑包装为一个匿名函数处理,像下面这样:

+
// pc[i] is the population count of i.
+var pc [256]byte = func() (pc [256]byte) {
+	for i := range pc {
+		pc[i] = pc[i/2] + byte(i&1)
+	}
+	return
+}()
+
+

要注意的是在init函数中,range循环只使用了索引,省略了没有用到的值部分。循环也可以这样写:

+
for i, _ := range pc {
+
+

我们在下一节和10.5节还将看到其它使用init函数的地方。

+

练习 2.3: 重写PopCount函数,用一个循环代替单一的表达式。比较两个版本的性能。(11.4节将展示如何系统地比较两个不同实现的性能。)

+

练习 2.4: 用移位算法重写PopCount函数,每次测试最右边的1bit,然后统计总数。比较和查表算法的性能差异。

+

练习 2.5: 表达式x&(x-1)用于将x的最低的一个非零的bit位清零。使用这个算法重写PopCount函数,然后比较性能。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch2/ch2-07.html b/ch2/ch2-07.html new file mode 100644 index 0000000..5ba9e22 --- /dev/null +++ b/ch2/ch2-07.html @@ -0,0 +1,352 @@ + + + + + + 作用域 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

2.7. 作用域

+

一个声明语句将程序中的实体和一个名字关联,比如一个函数或一个变量。声明语句的作用域是指源代码中可以有效使用这个名字的范围。

+

不要将作用域和生命周期混为一谈。声明语句的作用域对应的是一个源代码的文本区域;它是一个编译时的属性。一个变量的生命周期是指程序运行时变量存在的有效时间段,在此时间区域内它可以被程序的其他部分引用;是一个运行时的概念。

+

句法块是由花括弧所包含的一系列语句,就像函数体或循环体花括弧包裹的内容一样。句法块内部声明的名字是无法被外部块访问的。这个块决定了内部声明的名字的作用域范围。我们可以把块(block)的概念推广到包括其他声明的群组,这些声明在代码中并未显式地使用花括号包裹起来,我们称之为词法块。对全局的源代码来说,存在一个整体的词法块,称为全局词法块;对于每个包;每个for、if和switch语句,也都有对应词法块;每个switch或select的分支也有独立的词法块;当然也包括显式书写的词法块(花括弧包含的语句)。

+

声明语句对应的词法域决定了作用域范围的大小。对于内置的类型、函数和常量,比如int、len和true等是在全局作用域的,因此可以在整个程序中直接使用。任何在函数外部(也就是包级语法域)声明的名字可以在同一个包的任何源文件中访问的。对于导入的包,例如tempconv导入的fmt包,则是对应源文件级的作用域,因此只能在当前的文件中访问导入的fmt包,当前包的其它源文件无法访问在当前源文件导入的包。还有许多声明语句,比如tempconv.CToF函数中的变量c,则是局部作用域的,它只能在函数内部(甚至只能是局部的某些部分)访问。

+

控制流标号,就是break、continue或goto语句后面跟着的那种标号,则是函数级的作用域。

+

一个程序可能包含多个同名的声明,只要它们在不同的词法域就没有关系。例如,你可以声明一个局部变量,和包级的变量同名。或者是像2.3.3节的例子那样,你可以将一个函数参数的名字声明为new,虽然内置的new是全局作用域的。但是物极必反,如果滥用不同词法域可重名的特性的话,可能导致程序很难阅读。

+

当编译器遇到一个名字引用时,它会对其定义进行查找,查找过程从最内层的词法域向全局的作用域进行。如果查找失败,则报告“未声明的名字”这样的错误。如果该名字在内部和外部的块分别声明过,则内部块的声明首先被找到。在这种情况下,内部声明屏蔽了外部同名的声明,让外部的声明的名字无法被访问:

+
func f() {}
+
+var g = "g"
+
+func main() {
+	f := "f"
+	fmt.Println(f) // "f"; local var f shadows package-level func f
+	fmt.Println(g) // "g"; package-level var
+	fmt.Println(h) // compile error: undefined: h
+}
+
+

在函数中词法域可以深度嵌套,因此内部的一个声明可能屏蔽外部的声明。还有许多语法块是if或for等控制流语句构造的。下面的代码有三个不同的变量x,因为它们是定义在不同的词法域(这个例子只是为了演示作用域规则,但不是好的编程风格)。

+
func main() {
+	x := "hello!"
+	for i := 0; i < len(x); i++ {
+		x := x[i]
+		if x != '!' {
+			x := x + 'A' - 'a'
+			fmt.Printf("%c", x) // "HELLO" (one letter per iteration)
+		}
+	}
+}
+
+

x[i]x + 'A' - 'a'声明语句的初始化的表达式中都引用了外部作用域声明的x变量,稍后我们会解释这个。(注意,后面的表达式与unicode.ToUpper并不等价。)

+

正如上面例子所示,并不是所有的词法域都显式地对应到由花括弧包含的语句;还有一些隐含的规则。上面的for语句创建了两个词法域:花括弧包含的是显式的部分,是for的循环体部分词法域,另外一个隐式的部分则是循环的初始化部分,比如用于迭代变量i的初始化。隐式的词法域部分的作用域还包含条件测试部分和循环后的迭代部分(i++),当然也包含循环体词法域。

+

下面的例子同样有三个不同的x变量,每个声明在不同的词法域,一个在函数体词法域,一个在for隐式的初始化词法域,一个在for循环体词法域;只有两个块是显式创建的:

+
func main() {
+	x := "hello"
+	for _, x := range x {
+		x := x + 'A' - 'a'
+		fmt.Printf("%c", x) // "HELLO" (one letter per iteration)
+	}
+}
+
+

和for循环类似,if和switch语句也会在条件部分创建隐式词法域,还有它们对应的执行体词法域。下面的if-else测试链演示了x和y的有效作用域范围:

+
if x := f(); x == 0 {
+	fmt.Println(x)
+} else if y := g(x); x == y {
+	fmt.Println(x, y)
+} else {
+	fmt.Println(x, y)
+}
+fmt.Println(x, y) // compile error: x and y are not visible here
+
+

第二个if语句嵌套在第一个内部,因此第一个if语句条件初始化词法域声明的变量在第二个if中也可以访问。switch语句的每个分支也有类似的词法域规则:条件部分为一个隐式词法域,然后是每个分支的词法域。

+

在包级别,声明的顺序并不会影响作用域范围,因此一个先声明的可以引用它自身或者是引用后面的一个声明,这可以让我们定义一些相互嵌套或递归的类型或函数。但是如果一个变量或常量递归引用了自身,则会产生编译错误。

+

在这个程序中:

+
if f, err := os.Open(fname); err != nil { // compile error: unused: f
+	return err
+}
+f.ReadByte() // compile error: undefined f
+f.Close()    // compile error: undefined f
+
+

变量f的作用域只在if语句内,因此后面的语句将无法引入它,这将导致编译错误。你可能会收到一个局部变量f没有声明的错误提示,具体错误信息依赖编译器的实现。

+

通常需要在if之前声明变量,这样可以确保后面的语句依然可以访问变量:

+
f, err := os.Open(fname)
+if err != nil {
+	return err
+}
+f.ReadByte()
+f.Close()
+
+

你可能会考虑通过将ReadByte和Close移动到if的else块来解决这个问题:

+
if f, err := os.Open(fname); err != nil {
+	return err
+} else {
+	// f and err are visible here too
+	f.ReadByte()
+	f.Close()
+}
+
+

但这不是Go语言推荐的做法,Go语言的习惯是在if中处理错误然后直接返回,这样可以确保正常执行的语句不需要代码缩进。

+

要特别注意短变量声明语句的作用域范围,考虑下面的程序,它的目的是获取当前的工作目录然后保存到一个包级的变量中。这本来可以通过直接调用os.Getwd完成,但是将这个从主逻辑中分离出来可能会更好,特别是在需要处理错误的时候。函数log.Fatalf用于打印日志信息,然后调用os.Exit(1)终止程序。

+
var cwd string
+
+func init() {
+	cwd, err := os.Getwd() // compile error: unused: cwd
+	if err != nil {
+		log.Fatalf("os.Getwd failed: %v", err)
+	}
+}
+
+

虽然cwd在外部已经声明过,但是:=语句还是将cwd和err重新声明为新的局部变量。因为内部声明的cwd将屏蔽外部的声明,因此上面的代码并不会正确更新包级声明的cwd变量。

+

由于当前的编译器会检测到局部声明的cwd并没有使用,然后报告这可能是一个错误,但是这种检测并不可靠。因为一些小的代码变更,例如增加一个局部cwd的打印语句,就可能导致这种检测失效。

+
var cwd string
+
+func init() {
+	cwd, err := os.Getwd() // NOTE: wrong!
+	if err != nil {
+		log.Fatalf("os.Getwd failed: %v", err)
+	}
+	log.Printf("Working directory = %s", cwd)
+}
+
+

全局的cwd变量依然是没有被正确初始化的,而且看似正常的日志输出更是让这个BUG更加隐晦。

+

有许多方式可以避免出现类似潜在的问题。最直接的方法是通过单独声明err变量,来避免使用:=的简短声明方式:

+
var cwd string
+
+func init() {
+	var err error
+	cwd, err = os.Getwd()
+	if err != nil {
+		log.Fatalf("os.Getwd failed: %v", err)
+	}
+}
+
+

我们已经看到包、文件、声明和语句如何来表达一个程序结构。在下面的两个章节,我们将探讨数据的结构。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch2/ch2.html b/ch2/ch2.html new file mode 100644 index 0000000..646b631 --- /dev/null +++ b/ch2/ch2.html @@ -0,0 +1,240 @@ + + + + + + 程序结构 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

第2章 程序结构

+

Go语言和其他编程语言一样,一个大的程序是由很多小的基础构件组成的。变量保存值,简单的加法和减法运算被组合成较复杂的表达式。基础类型被聚合为数组或结构体等更复杂的数据结构。然后使用if和for之类的控制语句来组织和控制表达式的执行流程。然后多个语句被组织到一个个函数中,以便代码的隔离和复用。函数以源文件和包的方式被组织。

+

我们已经在前面章节的例子中看到了很多例子。在本章中,我们将深入讨论Go程序基础结构方面的一些细节。每个示例程序都是刻意写的简单,这样我们可以减少复杂的算法或数据结构等不相关的问题带来的干扰,从而可以专注于Go语言本身的学习。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch3/ch3-01.html b/ch3/ch3-01.html new file mode 100644 index 0000000..e94a01c --- /dev/null +++ b/ch3/ch3-01.html @@ -0,0 +1,359 @@ + + + + + + 整型 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

3.1. 整型

+

Go语言的数值类型包括几种不同大小的整数、浮点数和复数。每种数值类型都决定了对应的大小范围和是否支持正负符号。让我们先从整数类型开始介绍。

+

Go语言同时提供了有符号和无符号类型的整数运算。这里有int8、int16、int32和int64四种截然不同大小的有符号整数类型,分别对应8、16、32、64bit大小的有符号整数,与此对应的是uint8、uint16、uint32和uint64四种无符号整数类型。

+

这里还有两种一般对应特定CPU平台机器字大小的有符号和无符号整数int和uint;其中int是应用最广泛的数值类型。这两种类型都有同样的大小,32或64bit,但是我们不能对此做任何的假设;因为不同的编译器即使在相同的硬件平台上可能产生不同的大小。

+

Unicode字符rune类型是和int32等价的类型,通常用于表示一个Unicode码点。这两个名称可以互换使用。同样byte也是uint8类型的等价类型,byte类型一般用于强调数值是一个原始的数据而不是一个小的整数。

+

最后,还有一种无符号的整数类型uintptr,没有指定具体的bit大小但是足以容纳指针。uintptr类型只有在底层编程时才需要,特别是Go语言和C语言函数库或操作系统接口相交互的地方。我们将在第十三章的unsafe包相关部分看到类似的例子。

+

不管它们的具体大小,int、uint和uintptr是不同类型的兄弟类型。其中int和int32也是不同的类型,即使int的大小也是32bit,在需要将int当作int32类型的地方需要一个显式的类型转换操作,反之亦然。

+

其中有符号整数采用2的补码形式表示,也就是最高bit位用来表示符号位,一个n-bit的有符号数的值域是从$-2^{n-1}$到$2^{n-1}-1$。无符号整数的所有bit位都用于表示非负数,值域是0到$2^n-1$。例如,int8类型整数的值域是从-128到127,而uint8类型整数的值域是从0到255。

+

下面是Go语言中关于算术运算、逻辑运算和比较运算的二元运算符,它们按照优先级递减的顺序排列:

+
*      /      %      <<       >>     &       &^
++      -      |      ^
+==     !=     <      <=       >      >=
+&&
+||
+
+

二元运算符有五种优先级。在同一个优先级,使用左优先结合规则,但是使用括号可以明确优先顺序,使用括号也可以用于提升优先级,例如mask & (1 << 28)

+

对于上表中前两行的运算符,例如+运算符还有一个与赋值相结合的对应运算符+=,可以用于简化赋值语句。

+

算术运算符+-*/可以适用于整数、浮点数和复数,但是取模运算符%仅用于整数间的运算。对于不同编程语言,%取模运算的行为可能并不相同。在Go语言中,%取模运算符的符号和被取模数的符号总是一致的,因此-5%3-5%-3结果都是-2。除法运算符/的行为则依赖于操作数是否全为整数,比如5.0/4.0的结果是1.25,但是5/4的结果是1,因为整数除法会向着0方向截断余数。

+

一个算术运算的结果,不管是有符号或者是无符号的,如果需要更多的bit位才能正确表示的话,就说明计算结果是溢出了。超出的高位的bit位部分将被丢弃。如果原始的数值是有符号类型,而且最左边的bit位是1的话,那么最终结果可能是负的,例如int8的例子:

+
var u uint8 = 255
+fmt.Println(u, u+1, u*u) // "255 0 1"
+
+var i int8 = 127
+fmt.Println(i, i+1, i*i) // "127 -128 1"
+
+

两个相同的整数类型可以使用下面的二元比较运算符进行比较;比较表达式的结果是布尔类型。

+
==    等于
+!=    不等于
+<     小于
+<=    小于等于
+>     大于
+>=    大于等于
+
+

事实上,布尔型、数字类型和字符串等基本类型都是可比较的,也就是说两个相同类型的值可以用==和!=进行比较。此外,整数、浮点数和字符串可以根据比较结果排序。许多其它类型的值可能是不可比较的,因此也就可能是不可排序的。对于我们遇到的每种类型,我们需要保证规则的一致性。

+

这里是一元的加法和减法运算符:

+
+      一元加法(无效果)
+-      负数
+
+

对于整数,+x是0+x的简写,-x则是0-x的简写;对于浮点数和复数,+x就是x,-x则是x 的负数。

+

Go语言还提供了以下的bit位操作运算符,前面4个操作运算符并不区分是有符号还是无符号数:

+
&      位运算 AND
+|      位运算 OR
+^      位运算 XOR
+&^     位清空(AND NOT)
+<<     左移
+>>     右移
+
+

位操作运算符^作为二元运算符时是按位异或(XOR),当用作一元运算符时表示按位取反;也就是说,它返回一个每个bit位都取反的数。位操作运算符&^用于按位置零(AND NOT):如果对应y中bit位为1的话,表达式z = x &^ y结果z的对应的bit位为0,否则z对应的bit位等于x相应的bit位的值。

+

下面的代码演示了如何使用位操作解释uint8类型值的8个独立的bit位。它使用了Printf函数的%b参数打印二进制格式的数字;其中%08b中08表示打印至少8个字符宽度,不足的前缀部分用0填充。

+
var x uint8 = 1<<1 | 1<<5
+var y uint8 = 1<<1 | 1<<2
+
+fmt.Printf("%08b\n", x) // "00100010", the set {1, 5}
+fmt.Printf("%08b\n", y) // "00000110", the set {1, 2}
+
+fmt.Printf("%08b\n", x&y)  // "00000010", the intersection {1}
+fmt.Printf("%08b\n", x|y)  // "00100110", the union {1, 2, 5}
+fmt.Printf("%08b\n", x^y)  // "00100100", the symmetric difference {2, 5}
+fmt.Printf("%08b\n", x&^y) // "00100000", the difference {5}
+
+for i := uint(0); i < 8; i++ {
+	if x&(1<<i) != 0 { // membership test
+		fmt.Println(i) // "1", "5"
+	}
+}
+
+fmt.Printf("%08b\n", x<<1) // "01000100", the set {2, 6}
+fmt.Printf("%08b\n", x>>1) // "00010001", the set {0, 4}
+
+

(6.5节给出了一个可以远大于一个字节的整数集的实现。)

+

x<<nx>>n移位运算中,决定了移位操作的bit数部分必须是无符号数;被操作的x可以是有符号数或无符号数。算术上,一个x<<n左移运算等价于乘以$2^n$,一个x>>n右移运算等价于除以$2^n$。

+

左移运算用零填充右边空缺的bit位,无符号数的右移运算也是用0填充左边空缺的bit位,但是有符号数的右移运算会用符号位的值填充左边空缺的bit位。因为这个原因,最好用无符号运算,这样你可以将整数完全当作一个bit位模式处理。

+

尽管Go语言提供了无符号数的运算,但即使数值本身不可能出现负数,我们还是倾向于使用有符号的int类型,就像数组的长度那样,虽然使用uint无符号类型似乎是一个更合理的选择。事实上,内置的len函数返回一个有符号的int,我们可以像下面例子那样处理逆序循环。

+
medals := []string{"gold", "silver", "bronze"}
+for i := len(medals) - 1; i >= 0; i-- {
+	fmt.Println(medals[i]) // "bronze", "silver", "gold"
+}
+
+

另一个选择对于上面的例子来说将是灾难性的。如果len函数返回一个无符号数,那么i也将是无符号的uint类型,然后条件i >= 0则永远为真。在三次迭代之后,也就是i == 0时,i--语句将不会产生-1,而是变成一个uint类型的最大值(可能是$2^64-1$),然后medals[i]表达式运行时将发生panic异常(§5.9),也就是试图访问一个slice范围以外的元素。

+

出于这个原因,无符号数往往只有在位运算或其它特殊的运算场景才会使用,就像bit集合、分析二进制文件格式或者是哈希和加密操作等。它们通常并不用于仅仅是表达非负数量的场合。

+

一般来说,需要一个显式的转换将一个值从一种类型转化为另一种类型,并且算术和逻辑运算的二元操作中必须是相同的类型。虽然这偶尔会导致需要很长的表达式,但是它消除了所有和类型相关的问题,而且也使得程序容易理解。

+

在很多场景,会遇到类似下面代码的常见的错误:

+
var apples int32 = 1
+var oranges int16 = 2
+var compote int = apples + oranges // compile error
+
+

当尝试编译这三个语句时,将产生一个错误信息:

+
invalid operation: apples + oranges (mismatched types int32 and int16)
+
+

这种类型不匹配的问题可以有几种不同的方法修复,最常见方法是将它们都显式转型为一个常见类型:

+
var compote = int(apples) + int(oranges)
+
+

如2.5节所述,对于每种类型T,如果转换允许的话,类型转换操作T(x)将x转换为T类型。许多整数之间的相互转换并不会改变数值;它们只是告诉编译器如何解释这个值。但是对于将一个大尺寸的整数类型转为一个小尺寸的整数类型,或者是将一个浮点数转为整数,可能会改变数值或丢失精度:

+
f := 3.141 // a float64
+i := int(f)
+fmt.Println(f, i) // "3.141 3"
+f = 1.99
+fmt.Println(int(f)) // "1"
+
+

浮点数到整数的转换将丢失任何小数部分,然后向数轴零方向截断。你应该避免对可能会超出目标类型表示范围的数值做类型转换,因为截断的行为可能依赖于具体的实现:

+
f := 1e100  // a float64
+i := int(f) // 结果依赖于具体实现
+
+

任何大小的整数字面值都可以用以0开始的八进制格式书写,例如0666;或用以0x或0X开头的十六进制格式书写,例如0xdeadbeef。十六进制数字可以用大写或小写字母。如今八进制数据通常用于POSIX操作系统上的文件访问权限标志,十六进制数字则更强调数字值的bit位模式。

+

当使用fmt包打印一个数值时,我们可以用%d、%o或%x参数控制输出的进制格式,就像下面的例子:

+
o := 0666
+fmt.Printf("%d %[1]o %#[1]o\n", o) // "438 666 0666"
+x := int64(0xdeadbeef)
+fmt.Printf("%d %[1]x %#[1]x %#[1]X\n", x)
+// Output:
+// 3735928559 deadbeef 0xdeadbeef 0XDEADBEEF
+
+

请注意fmt的两个使用技巧。通常Printf格式化字符串包含多个%参数时将会包含对应相同数量的额外操作数,但是%之后的[1]副词告诉Printf函数再次使用第一个操作数。第二,%后的#副词告诉Printf在用%o、%x或%X输出时生成0、0x或0X前缀。

+

字符面值通过一对单引号直接包含对应字符。最简单的例子是ASCII中类似'a'写法的字符面值,但是我们也可以通过转义的数值来表示任意的Unicode码点对应的字符,马上将会看到这样的例子。

+

字符使用%c参数打印,或者是用%q参数打印带单引号的字符:

+
ascii := 'a'
+unicode := '国'
+newline := '\n'
+fmt.Printf("%d %[1]c %[1]q\n", ascii)   // "97 a 'a'"
+fmt.Printf("%d %[1]c %[1]q\n", unicode) // "22269 国 '国'"
+fmt.Printf("%d %[1]q\n", newline)       // "10 '\n'"
+
+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch3/ch3-02.html b/ch3/ch3-02.html new file mode 100644 index 0000000..b749f72 --- /dev/null +++ b/ch3/ch3-02.html @@ -0,0 +1,355 @@ + + + + + + 浮点数 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

3.2. 浮点数

+

Go语言提供了两种精度的浮点数,float32和float64。它们的算术规范由IEEE754浮点数国际标准定义,该浮点数规范被所有现代的CPU支持。

+

这些浮点数类型的取值范围可以从很微小到很巨大。浮点数的范围极限值可以在math包找到。常量math.MaxFloat32表示float32能表示的最大数值,大约是 3.4e38;对应的math.MaxFloat64常量大约是1.8e308。它们分别能表示的最小值近似为1.4e-45和4.9e-324。

+

一个float32类型的浮点数可以提供大约6个十进制数的精度,而float64则可以提供约15个十进制数的精度;通常应该优先使用float64类型,因为float32类型的累计计算误差很容易扩散,并且float32能精确表示的正整数并不是很大(译注:因为float32的有效bit位只有23个,其它的bit位用于指数和符号;当整数大于23bit能表达的范围时,float32的表示将出现误差):

+
var f float32 = 16777216 // 1 << 24
+fmt.Println(f == f+1)    // "true"!
+
+

浮点数的字面值可以直接写小数部分,像这样:

+
const e = 2.71828 // (approximately)
+
+

小数点前面或后面的数字都可能被省略(例如.707或1.)。很小或很大的数最好用科学计数法书写,通过e或E来指定指数部分:

+
const Avogadro = 6.02214129e23  // 阿伏伽德罗常数
+const Planck   = 6.62606957e-34 // 普朗克常数
+
+

用Printf函数的%g参数打印浮点数,将采用更紧凑的表示形式打印,并提供足够的精度,但是对应表格的数据,使用%e(带指数)或%f的形式打印可能更合适。所有的这三个打印形式都可以指定打印的宽度和控制打印精度。

+
for x := 0; x < 8; x++ {
+	fmt.Printf("x = %d e^x = %8.3f\n", x, math.Exp(float64(x)))
+}
+
+

上面代码打印e的幂,打印精度是小数点后三个小数精度和8个字符宽度:

+
x = 0       e^x =    1.000
+x = 1       e^x =    2.718
+x = 2       e^x =    7.389
+x = 3       e^x =   20.086
+x = 4       e^x =   54.598
+x = 5       e^x =  148.413
+x = 6       e^x =  403.429
+x = 7       e^x = 1096.633
+
+

math包中除了提供大量常用的数学函数外,还提供了IEEE754浮点数标准中定义的特殊值的创建和测试:正无穷大和负无穷大,分别用于表示太大溢出的数字和除零的结果;还有NaN非数,一般用于表示无效的除法操作结果0/0或Sqrt(-1).

+
var z float64
+fmt.Println(z, -z, 1/z, -1/z, z/z) // "0 -0 +Inf -Inf NaN"
+
+

函数math.IsNaN用于测试一个数是否是非数NaN,math.NaN则返回非数对应的值。虽然可以用math.NaN来表示一个非法的结果,但是测试一个结果是否是非数NaN则是充满风险的,因为NaN和任何数都是不相等的(译注:在浮点数中,NaN、正无穷大和负无穷大都不是唯一的,每个都有非常多种的bit模式表示):

+
nan := math.NaN()
+fmt.Println(nan == nan, nan < nan, nan > nan) // "false false false"
+
+

如果一个函数返回的浮点数结果可能失败,最好的做法是用单独的标志报告失败,像这样:

+
func compute() (value float64, ok bool) {
+	// ...
+	if failed {
+		return 0, false
+	}
+	return result, true
+}
+
+

接下来的程序演示了通过浮点计算生成的图形。它是带有两个参数的z = f(x, y)函数的三维形式,使用了可缩放矢量图形(SVG)格式输出,SVG是一个用于矢量线绘制的XML标准。图3.1显示了sin(r)/r函数的输出图形,其中r是sqrt(x*x+y*y)

+

+

gopl.io/ch3/surface

+
// Surface computes an SVG rendering of a 3-D surface function.
+package main
+
+import (
+	"fmt"
+	"math"
+)
+
+const (
+	width, height = 600, 320            // canvas size in pixels
+	cells         = 100                 // number of grid cells
+	xyrange       = 30.0                // axis ranges (-xyrange..+xyrange)
+	xyscale       = width / 2 / xyrange // pixels per x or y unit
+	zscale        = height * 0.4        // pixels per z unit
+	angle         = math.Pi / 6         // angle of x, y axes (=30°)
+)
+
+var sin30, cos30 = math.Sin(angle), math.Cos(angle) // sin(30°), cos(30°)
+
+func main() {
+	fmt.Printf("<svg xmlns='http://www.w3.org/2000/svg' "+
+		"style='stroke: grey; fill: white; stroke-width: 0.7' "+
+		"width='%d' height='%d'>", width, height)
+	for i := 0; i < cells; i++ {
+		for j := 0; j < cells; j++ {
+			ax, ay := corner(i+1, j)
+			bx, by := corner(i, j)
+			cx, cy := corner(i, j+1)
+			dx, dy := corner(i+1, j+1)
+			fmt.Printf("<polygon points='%g,%g %g,%g %g,%g %g,%g'/>\n",
+				ax, ay, bx, by, cx, cy, dx, dy)
+		}
+	}
+	fmt.Println("</svg>")
+}
+
+func corner(i, j int) (float64, float64) {
+	// Find point (x,y) at corner of cell (i,j).
+	x := xyrange * (float64(i)/cells - 0.5)
+	y := xyrange * (float64(j)/cells - 0.5)
+
+	// Compute surface height z.
+	z := f(x, y)
+
+	// Project (x,y,z) isometrically onto 2-D SVG canvas (sx,sy).
+	sx := width/2 + (x-y)*cos30*xyscale
+	sy := height/2 + (x+y)*sin30*xyscale - z*zscale
+	return sx, sy
+}
+
+func f(x, y float64) float64 {
+	r := math.Hypot(x, y) // distance from (0,0)
+	return math.Sin(r) / r
+}
+
+

要注意的是corner函数返回了两个结果,分别对应每个网格顶点的坐标参数。

+

要解释这个程序是如何工作的需要一些基本的几何学知识,但是我们可以跳过几何学原理,因为程序的重点是演示浮点数运算。程序的本质是三个不同的坐标系中映射关系,如图3.2所示。第一个是100x100的二维网格,对应整数坐标(i,j),从远处的(0,0)位置开始。我们从远处向前面绘制,因此远处先绘制的多边形有可能被前面后绘制的多边形覆盖。

+

第二个坐标系是一个三维的网格浮点坐标(x,y,z),其中x和y是i和j的线性函数,通过平移转换为网格单元的中心,然后用xyrange系数缩放。高度z是函数f(x,y)的值。

+

第三个坐标系是一个二维的画布,起点(0,0)在左上角。画布中点的坐标用(sx,sy)表示。我们使用等角投影将三维点(x,y,z)投影到二维的画布中。

+

+

画布中从远处到右边的点对应较大的x值和较大的y值。并且画布中x和y值越大,则对应的z值越小。x和y的垂直和水平缩放系数来自30度角的正弦和余弦值。z的缩放系数0.4,是一个任意选择的参数。

+

对于二维网格中的每一个网格单元,main函数计算单元的四个顶点在画布中对应多边形ABCD的顶点,其中B对应(i,j)顶点位置,A、C和D是其它相邻的顶点,然后输出SVG的绘制指令。

+

练习 3.1: 如果f函数返回的是无限制的float64值,那么SVG文件可能输出无效的多边形元素(虽然许多SVG渲染器会妥善处理这类问题)。修改程序跳过无效的多边形。

+

练习 3.2: 试验math包中其他函数的渲染图形。你是否能输出一个egg box、moguls或a saddle图案?

+

练习 3.3: 根据高度给每个多边形上色,那样峰值部将是红色(#ff0000),谷部将是蓝色(#0000ff)。

+

练习 3.4: 参考1.7节Lissajous例子的函数,构造一个web服务器,用于计算函数曲面然后返回SVG数据给客户端。服务器必须设置Content-Type头部:

+
w.Header().Set("Content-Type", "image/svg+xml")
+
+

(这一步在Lissajous例子中不是必须的,因为服务器使用标准的PNG图像格式,可以根据前面的512个字节自动输出对应的头部。)允许客户端通过HTTP请求参数设置高度、宽度和颜色等参数。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch3/ch3-03.html b/ch3/ch3-03.html new file mode 100644 index 0000000..29a838d --- /dev/null +++ b/ch3/ch3-03.html @@ -0,0 +1,310 @@ + + + + + + 复数 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

3.3. 复数

+

Go语言提供了两种精度的复数类型:complex64和complex128,分别对应float32和float64两种浮点数精度。内置的complex函数用于构建复数,内建的real和imag函数分别返回复数的实部和虚部:

+
var x complex128 = complex(1, 2) // 1+2i
+var y complex128 = complex(3, 4) // 3+4i
+fmt.Println(x*y)                 // "(-5+10i)"
+fmt.Println(real(x*y))           // "-5"
+fmt.Println(imag(x*y))           // "10"
+
+

如果一个浮点数面值或一个十进制整数面值后面跟着一个i,例如3.141592i或2i,它将构成一个复数的虚部,复数的实部是0:

+
fmt.Println(1i * 1i) // "(-1+0i)", i^2 = -1
+
+

在常量算术规则下,一个复数常量可以加到另一个普通数值常量(整数或浮点数、实部或虚部),我们可以用自然的方式书写复数,就像1+2i或与之等价的写法2i+1。上面x和y的声明语句还可以简化:

+
x := 1 + 2i
+y := 3 + 4i
+
+

复数也可以用==和!=进行相等比较。只有两个复数的实部和虚部都相等的时候它们才是相等的(译注:浮点数的相等比较是危险的,需要特别小心处理精度问题)。

+

math/cmplx包提供了复数处理的许多函数,例如求复数的平方根函数和求幂函数。

+
fmt.Println(cmplx.Sqrt(-1)) // "(0+1i)"
+
+

下面的程序使用complex128复数算法来生成一个Mandelbrot图像。

+

gopl.io/ch3/mandelbrot

+
// Mandelbrot emits a PNG image of the Mandelbrot fractal.
+package main
+
+import (
+	"image"
+	"image/color"
+	"image/png"
+	"math/cmplx"
+	"os"
+)
+
+
+func main() {
+	const (
+		xmin, ymin, xmax, ymax = -2, -2, +2, +2
+		width, height          = 1024, 1024
+	)
+
+	img := image.NewRGBA(image.Rect(0, 0, width, height))
+	for py := 0; py < height; py++ {
+		y := float64(py)/height*(ymax-ymin) + ymin
+		for px := 0; px < width; px++ {
+			x := float64(px)/width*(xmax-xmin) + xmin
+			z := complex(x, y)
+			// Image point (px, py) represents complex value z.
+			img.Set(px, py, mandelbrot(z))
+		}
+	}
+	png.Encode(os.Stdout, img) // NOTE: ignoring errors
+}
+
+func mandelbrot(z complex128) color.Color {
+	const iterations = 200
+	const contrast = 15
+
+	var v complex128
+	for n := uint8(0); n < iterations; n++ {
+		v = v*v + z
+		if cmplx.Abs(v) > 2 {
+			return color.Gray{255 - contrast*n}
+		}
+	}
+	return color.Black
+}
+
+

用于遍历1024x1024图像每个点的两个嵌套的循环对应-2到+2区间的复数平面。程序反复测试每个点对应复数值平方值加一个增量值对应的点是否超出半径为2的圆。如果超过了,通过根据预设置的逃逸迭代次数对应的灰度颜色来代替。如果不是,那么该点属于Mandelbrot集合,使用黑色颜色标记。最终程序将生成的PNG格式分形图像输出到标准输出,如图3.3所示。

+

+

练习 3.5: 实现一个彩色的Mandelbrot图像,使用image.NewRGBA创建图像,使用color.RGBA或color.YCbCr生成颜色。

+

练习 3.6: 升采样技术可以降低每个像素对计算颜色值和平均值的影响。简单的方法是将每个像素分成四个子像素,实现它。

+

练习 3.7: 另一个生成分形图像的方式是使用牛顿法来求解一个复数方程,例如$z^4-1=0$。每个起点到四个根的迭代次数对应阴影的灰度。方程根对应的点用颜色表示。

+

练习 3.8: 通过提高精度来生成更多级别的分形。使用四种不同精度类型的数字实现相同的分形:complex64、complex128、big.Float和big.Rat。(后面两种类型在math/big包声明。Float是有指定限精度的浮点数;Rat是无限精度的有理数。)它们间的性能和内存使用对比如何?当渲染图可见时缩放的级别是多少?

+

练习 3.9: 编写一个web服务器,用于给客户端生成分形的图像。运行客户端通过HTTP参数指定x、y和zoom参数。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch3/ch3-04.html b/ch3/ch3-04.html new file mode 100644 index 0000000..3c46233 --- /dev/null +++ b/ch3/ch3-04.html @@ -0,0 +1,269 @@ + + + + + + 布尔型 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

3.4. 布尔型

+

一个布尔类型的值只有两种:true和false。if和for语句的条件部分都是布尔类型的值,并且==和<等比较操作也会产生布尔型的值。一元操作符!对应逻辑非操作,因此!true的值为false,更罗嗦的说法是(!true==false)==true,虽然表达方式不一样,不过我们一般会采用简洁的布尔表达式,就像用x来表示x==true

+

布尔值可以和&&(AND)和||(OR)操作符结合,并且有短路行为:如果运算符左边值已经可以确定整个布尔表达式的值,那么运算符右边的值将不再被求值,因此下面的表达式总是安全的:

+
s != "" && s[0] == 'x'
+
+

其中s[0]操作如果应用于空字符串将会导致panic异常。

+

因为&&的优先级比||高(助记:&&对应逻辑乘法,||对应逻辑加法,乘法比加法优先级要高),下面形式的布尔表达式是不需要加小括弧的:

+
if 'a' <= c && c <= 'z' ||
+	'A' <= c && c <= 'Z' ||
+	'0' <= c && c <= '9' {
+	// ...ASCII letter or digit...
+}
+
+

布尔值并不会隐式转换为数字值0或1,反之亦然。必须使用一个显式的if语句辅助转换:

+
i := 0
+if b {
+	i = 1
+}
+
+

如果需要经常做类似的转换,包装成一个函数会更方便:

+
// btoi returns 1 if b is true and 0 if false.
+func btoi(b bool) int {
+	if b {
+		return 1
+	}
+	return 0
+}
+
+

数字到布尔型的逆转换则非常简单,不过为了保持对称,我们也可以包装一个函数:

+
// itob reports whether i is non-zero.
+func itob(i int) bool { return i != 0 }
+
+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch3/ch3-05.html b/ch3/ch3-05.html new file mode 100644 index 0000000..667150a --- /dev/null +++ b/ch3/ch3-05.html @@ -0,0 +1,525 @@ + + + + + + 字符串 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

3.5. 字符串

+

一个字符串是一个不可改变的字节序列。字符串可以包含任意的数据,包括byte值0,但是通常是用来包含人类可读的文本。文本字符串通常被解释为采用UTF8编码的Unicode码点(rune)序列,我们稍后会详细讨论这个问题。

+

内置的len函数可以返回一个字符串中的字节数目(不是rune字符数目),索引操作s[i]返回第i个字节的字节值,i必须满足0 ≤ i< len(s)条件约束。

+
s := "hello, world"
+fmt.Println(len(s))     // "12"
+fmt.Println(s[0], s[7]) // "104 119" ('h' and 'w')
+
+

如果试图访问超出字符串索引范围的字节将会导致panic异常:

+
c := s[len(s)] // panic: index out of range
+
+

第i个字节并不一定是字符串的第i个字符,因为对于非ASCII字符的UTF8编码会要两个或多个字节。我们先简单说下字符的工作方式。

+

子字符串操作s[i:j]基于原始的s字符串的第i个字节开始到第j个字节(并不包含j本身)生成一个新字符串。生成的新字符串将包含j-i个字节。

+
fmt.Println(s[0:5]) // "hello"
+
+

同样,如果索引超出字符串范围或者j小于i的话将导致panic异常。

+

不管i还是j都可能被忽略,当它们被忽略时将采用0作为开始位置,采用len(s)作为结束的位置。

+
fmt.Println(s[:5]) // "hello"
+fmt.Println(s[7:]) // "world"
+fmt.Println(s[:])  // "hello, world"
+
+

其中+操作符将两个字符串连接构造一个新字符串:

+
fmt.Println("goodbye" + s[5:]) // "goodbye, world"
+
+

字符串可以用==和<进行比较;比较通过逐个字节比较完成的,因此比较的结果是字符串自然编码的顺序。

+

字符串的值是不可变的:一个字符串包含的字节序列永远不会被改变,当然我们也可以给一个字符串变量分配一个新字符串值。可以像下面这样将一个字符串追加到另一个字符串:

+
s := "left foot"
+t := s
+s += ", right foot"
+
+

这并不会导致原始的字符串值被改变,但是变量s将因为+=语句持有一个新的字符串值,但是t依然是包含原先的字符串值。

+
fmt.Println(s) // "left foot, right foot"
+fmt.Println(t) // "left foot"
+
+

因为字符串是不可修改的,因此尝试修改字符串内部数据的操作也是被禁止的:

+
s[0] = 'L' // compile error: cannot assign to s[0]
+
+

不变性意味着如果两个字符串共享相同的底层数据的话也是安全的,这使得复制任何长度的字符串代价是低廉的。同样,一个字符串s和对应的子字符串切片s[7:]的操作也可以安全地共享相同的内存,因此字符串切片操作代价也是低廉的。在这两种情况下都没有必要分配新的内存。 图3.4演示了一个字符串和两个子串共享相同的底层数据。

+

3.5.1. 字符串面值

+

字符串值也可以用字符串面值方式编写,只要将一系列字节序列包含在双引号内即可:

+
"Hello, 世界"
+
+

+

因为Go语言源文件总是用UTF8编码,并且Go语言的文本字符串也以UTF8编码的方式处理,因此我们可以将Unicode码点也写到字符串面值中。

+

在一个双引号包含的字符串面值中,可以用以反斜杠\开头的转义序列插入任意的数据。下面的换行、回车和制表符等是常见的ASCII控制代码的转义方式:

+
\a      响铃
+\b      退格
+\f      换页
+\n      换行
+\r      回车
+\t      制表符
+\v      垂直制表符
+\'      单引号(只用在 '\'' 形式的rune符号面值中)
+\"      双引号(只用在 "..." 形式的字符串面值中)
+\\      反斜杠
+
+

可以通过十六进制或八进制转义在字符串面值中包含任意的字节。一个十六进制的转义形式是\xhh,其中两个h表示十六进制数字(大写或小写都可以)。一个八进制转义形式是\ooo,包含三个八进制的o数字(0到7),但是不能超过\377(译注:对应一个字节的范围,十进制为255)。每一个单一的字节表达一个特定的值。稍后我们将看到如何将一个Unicode码点写到字符串面值中。

+

一个原生的字符串面值形式是`...`,使用反引号代替双引号。在原生的字符串面值中,没有转义操作;全部的内容都是字面的意思,包含退格和换行,因此一个程序中的原生字符串面值可能跨越多行(译注:在原生字符串面值内部是无法直接写`字符的,可以用八进制或十六进制转义或+"`"连接字符串常量完成)。唯一的特殊处理是会删除回车以保证在所有平台上的值都是一样的,包括那些把回车也放入文本文件的系统(译注:Windows系统会把回车和换行一起放入文本文件中)。

+

原生字符串面值用于编写正则表达式会很方便,因为正则表达式往往会包含很多反斜杠。原生字符串面值同时被广泛应用于HTML模板、JSON面值、命令行提示信息以及那些需要扩展到多行的场景。

+
const GoUsage = `Go is a tool for managing Go source code.
+
+Usage:
+	go command [arguments]
+...`
+
+

3.5.2. Unicode

+

在很久以前,世界还是比较简单的,起码计算机世界就只有一个ASCII字符集:美国信息交换标准代码。ASCII,更准确地说是美国的ASCII,使用7bit来表示128个字符:包含英文字母的大小写、数字、各种标点符号和设备控制符。对于早期的计算机程序来说,这些就足够了,但是这也导致了世界上很多其他地区的用户无法直接使用自己的符号系统。随着互联网的发展,混合多种语言的数据变得很常见(译注:比如本身的英文原文或中文翻译都包含了ASCII、中文、日文等多种语言字符)。如何有效处理这些包含了各种语言的丰富多样的文本数据呢?

+

答案就是使用Unicode( http://unicode.org ),它收集了这个世界上所有的符号系统,包括重音符号和其它变音符号,制表符和回车符,还有很多神秘的符号,每个符号都分配一个唯一的Unicode码点,Unicode码点对应Go语言中的rune整数类型(译注:rune是int32等价类型)。

+

在第八版本的Unicode标准里收集了超过120,000个字符,涵盖超过100多种语言。这些在计算机程序和数据中是如何体现的呢?通用的表示一个Unicode码点的数据类型是int32,也就是Go语言中rune对应的类型;它的同义词rune符文正是这个意思。

+

我们可以将一个符文序列表示为一个int32序列。这种编码方式叫UTF-32或UCS-4,每个Unicode码点都使用同样大小的32bit来表示。这种方式比较简单统一,但是它会浪费很多存储空间,因为大多数计算机可读的文本是ASCII字符,本来每个ASCII字符只需要8bit或1字节就能表示。而且即使是常用的字符也远少于65,536个,也就是说用16bit编码方式就能表达常用字符。但是,还有其它更好的编码方法吗?

+

3.5.3. UTF-8

+

UTF8是一个将Unicode码点编码为字节序列的变长编码。UTF8编码是由Go语言之父Ken Thompson和Rob Pike共同发明的,现在已经是Unicode的标准。UTF8编码使用1到4个字节来表示每个Unicode码点,ASCII部分字符只使用1个字节,常用字符部分使用2或3个字节表示。每个符号编码后第一个字节的高端bit位用于表示编码总共有多少个字节。如果第一个字节的高端bit为0,则表示对应7bit的ASCII字符,ASCII字符每个字符依然是一个字节,和传统的ASCII编码兼容。如果第一个字节的高端bit是110,则说明需要2个字节;后续的每个高端bit都以10开头。更大的Unicode码点也是采用类似的策略处理。

+
0xxxxxxx                             runes 0-127    (ASCII)
+110xxxxx 10xxxxxx                    128-2047       (values <128 unused)
+1110xxxx 10xxxxxx 10xxxxxx           2048-65535     (values <2048 unused)
+11110xxx 10xxxxxx 10xxxxxx 10xxxxxx  65536-0x10ffff (other values unused)
+
+

变长的编码无法直接通过索引来访问第n个字符,但是UTF8编码获得了很多额外的优点。首先UTF8编码比较紧凑,完全兼容ASCII码,并且可以自动同步:它可以通过向前回朔最多3个字节就能确定当前字符编码的开始字节的位置。它也是一个前缀编码,所以当从左向右解码时不会有任何歧义也并不需要向前查看(译注:像GBK之类的编码,如果不知道起点位置则可能会出现歧义)。没有任何字符的编码是其它字符编码的子串,或是其它编码序列的字串,因此搜索一个字符时只要搜索它的字节编码序列即可,不用担心前后的上下文会对搜索结果产生干扰。同时UTF8编码的顺序和Unicode码点的顺序一致,因此可以直接排序UTF8编码序列。同时因为没有嵌入的NUL(0)字节,可以很好地兼容那些使用NUL作为字符串结尾的编程语言。

+

Go语言的源文件采用UTF8编码,并且Go语言处理UTF8编码的文本也很出色。unicode包提供了诸多处理rune字符相关功能的函数(比如区分字母和数字,或者是字母的大写和小写转换等),unicode/utf8包则提供了用于rune字符序列的UTF8编码和解码的功能。

+

有很多Unicode字符很难直接从键盘输入,并且还有很多字符有着相似的结构;有一些甚至是不可见的字符(译注:中文和日文就有很多相似但不同的字)。Go语言字符串面值中的Unicode转义字符让我们可以通过Unicode码点输入特殊的字符。有两种形式:\uhhhh对应16bit的码点值,\Uhhhhhhhh对应32bit的码点值,其中h是一个十六进制数字;一般很少需要使用32bit的形式。每一个对应码点的UTF8编码。例如:下面的字母串面值都表示相同的值:

+
"世界"
+"\xe4\xb8\x96\xe7\x95\x8c"
+"\u4e16\u754c"
+"\U00004e16\U0000754c"
+
+

上面三个转义序列都为第一个字符串提供替代写法,但是它们的值都是相同的。

+

Unicode转义也可以使用在rune字符中。下面三个字符是等价的:

+
'世' '\u4e16' '\U00004e16'
+
+

对于小于256的码点值可以写在一个十六进制转义字节中,例如\x41对应字符'A',但是对于更大的码点则必须使用\u\U转义形式。因此,\xe4\xb8\x96并不是一个合法的rune字符,虽然这三个字节对应一个有效的UTF8编码的码点。

+

得益于UTF8编码优良的设计,诸多字符串操作都不需要解码操作。我们可以不用解码直接测试一个字符串是否是另一个字符串的前缀:

+
func HasPrefix(s, prefix string) bool {
+	return len(s) >= len(prefix) && s[:len(prefix)] == prefix
+}
+
+

或者是后缀测试:

+
func HasSuffix(s, suffix string) bool {
+	return len(s) >= len(suffix) && s[len(s)-len(suffix):] == suffix
+}
+
+

或者是包含子串测试:

+
func Contains(s, substr string) bool {
+	for i := 0; i < len(s); i++ {
+		if HasPrefix(s[i:], substr) {
+			return true
+		}
+	}
+	return false
+}
+
+

对于UTF8编码后文本的处理和原始的字节处理逻辑是一样的。但是对应很多其它编码则并不是这样的。(上面的函数都来自strings字符串处理包,真实的代码包含了一个用哈希技术优化的Contains 实现。)

+

另一方面,如果我们真的关心每个Unicode字符,我们可以使用其它处理方式。考虑前面的第一个例子中的字符串,它混合了中西两种字符。图3.5展示了它的内存表示形式。字符串包含13个字节,以UTF8形式编码,但是只对应9个Unicode字符:

+
import "unicode/utf8"
+
+s := "Hello, 世界"
+fmt.Println(len(s))                    // "13"
+fmt.Println(utf8.RuneCountInString(s)) // "9"
+
+

为了处理这些真实的字符,我们需要一个UTF8解码器。unicode/utf8包提供了该功能,我们可以这样使用:

+
for i := 0; i < len(s); {
+	r, size := utf8.DecodeRuneInString(s[i:])
+	fmt.Printf("%d\t%c\n", i, r)
+	i += size
+}
+
+

每一次调用DecodeRuneInString函数都返回一个r和长度,r对应字符本身,长度对应r采用UTF8编码后的编码字节数目。长度可以用于更新第i个字符在字符串中的字节索引位置。但是这种编码方式是笨拙的,我们需要更简洁的语法。幸运的是,Go语言的range循环在处理字符串的时候,会自动隐式解码UTF8字符串。下面的循环运行如图3.5所示;需要注意的是对于非ASCII,索引更新的步长将超过1个字节。

+

+
for i, r := range "Hello, 世界" {
+	fmt.Printf("%d\t%q\t%d\n", i, r, r)
+}
+
+

我们可以使用一个简单的循环来统计字符串中字符的数目,像这样:

+
n := 0
+for _, _ = range s {
+	n++
+}
+
+

像其它形式的循环那样,我们也可以忽略不需要的变量:

+
n := 0
+for range s {
+	n++
+}
+
+

或者我们可以直接调用utf8.RuneCountInString(s)函数。

+

正如我们前面提到的,文本字符串采用UTF8编码只是一种惯例,但是对于循环的真正字符串并不是一个惯例,这是正确的。如果用于循环的字符串只是一个普通的二进制数据,或者是含有错误编码的UTF8数据,将会发生什么呢?

+

每一个UTF8字符解码,不管是显式地调用utf8.DecodeRuneInString解码或是在range循环中隐式地解码,如果遇到一个错误的UTF8编码输入,将生成一个特别的Unicode字符\uFFFD,在印刷中这个符号通常是一个黑色六角或钻石形状,里面包含一个白色的问号"?"。当程序遇到这样的一个字符,通常是一个危险信号,说明输入并不是一个完美没有错误的UTF8字符串。

+

UTF8字符串作为交换格式是非常方便的,但是在程序内部采用rune序列可能更方便,因为rune大小一致,支持数组索引和方便切割。

+

将[]rune类型转换应用到UTF8编码的字符串,将返回字符串编码的Unicode码点序列:

+
// "program" in Japanese katakana
+s := "プログラム"
+fmt.Printf("% x\n", s) // "e3 83 97 e3 83 ad e3 82 b0 e3 83 a9 e3 83 a0"
+r := []rune(s)
+fmt.Printf("%x\n", r)  // "[30d7 30ed 30b0 30e9 30e0]"
+
+

(在第一个Printf中的% x参数用于在每个十六进制数字前插入一个空格。)

+

如果是将一个[]rune类型的Unicode字符slice或数组转为string,则对它们进行UTF8编码:

+
fmt.Println(string(r)) // "プログラム"
+
+

将一个整数转型为字符串意思是生成以只包含对应Unicode码点字符的UTF8字符串:

+
fmt.Println(string(65))     // "A", not "65"
+fmt.Println(string(0x4eac)) // "京"
+
+

如果对应码点的字符是无效的,则用\uFFFD无效字符作为替换:

+
fmt.Println(string(1234567)) // "?"
+
+

3.5.4. 字符串和Byte切片

+

标准库中有四个包对字符串处理尤为重要:bytes、strings、strconv和unicode包。strings包提供了许多如字符串的查询、替换、比较、截断、拆分和合并等功能。

+

bytes包也提供了很多类似功能的函数,但是针对和字符串有着相同结构的[]byte类型。因为字符串是只读的,因此逐步构建字符串会导致很多分配和复制。在这种情况下,使用bytes.Buffer类型将会更有效,稍后我们将展示。

+

strconv包提供了布尔型、整型数、浮点数和对应字符串的相互转换,还提供了双引号转义相关的转换。

+

unicode包提供了IsDigit、IsLetter、IsUpper和IsLower等类似功能,它们用于给字符分类。每个函数有一个单一的rune类型的参数,然后返回一个布尔值。而像ToUpper和ToLower之类的转换函数将用于rune字符的大小写转换。所有的这些函数都是遵循Unicode标准定义的字母、数字等分类规范。strings包也有类似的函数,它们是ToUpper和ToLower,将原始字符串的每个字符都做相应的转换,然后返回新的字符串。

+

下面例子的basename函数灵感源于Unix shell的同名工具。在我们实现的版本中,basename(s)将看起来像是系统路径的前缀删除,同时将看似文件类型的后缀名部分删除:

+
fmt.Println(basename("a/b/c.go")) // "c"
+fmt.Println(basename("c.d.go"))   // "c.d"
+fmt.Println(basename("abc"))      // "abc"
+
+

第一个版本并没有使用任何库,全部手工硬编码实现:

+

gopl.io/ch3/basename1

+
// basename removes directory components and a .suffix.
+// e.g., a => a, a.go => a, a/b/c.go => c, a/b.c.go => b.c
+func basename(s string) string {
+	// Discard last '/' and everything before.
+	for i := len(s) - 1; i >= 0; i-- {
+		if s[i] == '/' {
+			s = s[i+1:]
+			break
+		}
+	}
+	// Preserve everything before last '.'.
+	for i := len(s) - 1; i >= 0; i-- {
+		if s[i] == '.' {
+			s = s[:i]
+			break
+		}
+	}
+	return s
+}
+
+

这个简化版本使用了strings.LastIndex库函数:

+

gopl.io/ch3/basename2

+
func basename(s string) string {
+	slash := strings.LastIndex(s, "/") // -1 if "/" not found
+	s = s[slash+1:]
+	if dot := strings.LastIndex(s, "."); dot >= 0 {
+		s = s[:dot]
+	}
+	return s
+}
+
+

path和path/filepath包提供了关于文件路径名更一般的函数操作。使用斜杠分隔路径可以在任何操作系统上工作。斜杠本身不应该用于文件名,但是在其他一些领域可能会用于文件名,例如URL路径组件。相比之下,path/filepath包则使用操作系统本身的路径规则,例如POSIX系统使用/foo/bar,而Microsoft Windows使用c:\foo\bar等。

+

让我们继续另一个字符串的例子。函数的功能是将一个表示整数值的字符串,每隔三个字符插入一个逗号分隔符,例如“12345”处理后成为“12,345”。这个版本只适用于整数类型;支持浮点数类型的留作练习。

+

gopl.io/ch3/comma

+
// comma inserts commas in a non-negative decimal integer string.
+func comma(s string) string {
+	n := len(s)
+	if n <= 3 {
+		return s
+	}
+	return comma(s[:n-3]) + "," + s[n-3:]
+}
+
+

输入comma函数的参数是一个字符串。如果输入字符串的长度小于或等于3的话,则不需要插入逗号分隔符。否则,comma函数将在最后三个字符前的位置将字符串切割为两个子串并插入逗号分隔符,然后通过递归调用自身来得出前面的子串。

+

一个字符串是包含只读字节的数组,一旦创建,是不可变的。相比之下,一个字节slice的元素则可以自由地修改。

+

字符串和字节slice之间可以相互转换:

+
s := "abc"
+b := []byte(s)
+s2 := string(b)
+
+

从概念上讲,一个[]byte(s)转换是分配了一个新的字节数组用于保存字符串数据的拷贝,然后引用这个底层的字节数组。编译器的优化可以避免在一些场景下分配和复制字符串数据,但总的来说需要确保在变量b被修改的情况下,原始的s字符串也不会改变。将一个字节slice转换到字符串的string(b)操作则是构造一个字符串拷贝,以确保s2字符串是只读的。

+

为了避免转换中不必要的内存分配,bytes包和strings同时提供了许多实用函数。下面是strings包中的六个函数:

+
func Contains(s, substr string) bool
+func Count(s, sep string) int
+func Fields(s string) []string
+func HasPrefix(s, prefix string) bool
+func Index(s, sep string) int
+func Join(a []string, sep string) string
+
+

bytes包中也对应的六个函数:

+
func Contains(b, subslice []byte) bool
+func Count(s, sep []byte) int
+func Fields(s []byte) [][]byte
+func HasPrefix(s, prefix []byte) bool
+func Index(s, sep []byte) int
+func Join(s [][]byte, sep []byte) []byte
+
+

它们之间唯一的区别是字符串类型参数被替换成了字节slice类型的参数。

+

bytes包还提供了Buffer类型用于字节slice的缓存。一个Buffer开始是空的,但是随着string、byte或[]byte等类型数据的写入可以动态增长,一个bytes.Buffer变量并不需要初始化,因为零值也是有效的:

+

gopl.io/ch3/printints

+
// intsToString is like fmt.Sprint(values) but adds commas.
+func intsToString(values []int) string {
+	var buf bytes.Buffer
+	buf.WriteByte('[')
+	for i, v := range values {
+		if i > 0 {
+			buf.WriteString(", ")
+		}
+		fmt.Fprintf(&buf, "%d", v)
+	}
+	buf.WriteByte(']')
+	return buf.String()
+}
+
+func main() {
+	fmt.Println(intsToString([]int{1, 2, 3})) // "[1, 2, 3]"
+}
+
+

当向bytes.Buffer添加任意字符的UTF8编码时,最好使用bytes.Buffer的WriteRune方法,但是WriteByte方法对于写入类似'['和']'等ASCII字符则会更加有效。

+

bytes.Buffer类型有着很多实用的功能,我们在第七章讨论接口时将会涉及到,我们将看看如何将它用作一个I/O的输入和输出对象,例如当做Fprintf的io.Writer输出对象,或者当作io.Reader类型的输入源对象。

+

练习 3.10: 编写一个非递归版本的comma函数,使用bytes.Buffer代替字符串链接操作。

+

练习 3.11: 完善comma函数,以支持浮点数处理和一个可选的正负号的处理。

+

练习 3.12: 编写一个函数,判断两个字符串是否是相互打乱的,也就是说它们有着相同的字符,但是对应不同的顺序。

+

3.5.5. 字符串和数字的转换

+

除了字符串、字符、字节之间的转换,字符串和数值之间的转换也比较常见。由strconv包提供这类转换功能。

+

将一个整数转为字符串,一种方法是用fmt.Sprintf返回一个格式化的字符串;另一个方法是用strconv.Itoa(“整数到ASCII”):

+
x := 123
+y := fmt.Sprintf("%d", x)
+fmt.Println(y, strconv.Itoa(x)) // "123 123"
+
+

FormatInt和FormatUint函数可以用不同的进制来格式化数字:

+
fmt.Println(strconv.FormatInt(int64(x), 2)) // "1111011"
+
+

fmt.Printf函数的%b、%d、%o和%x等参数提供功能往往比strconv包的Format函数方便很多,特别是在需要包含有附加额外信息的时候:

+
s := fmt.Sprintf("x=%b", x) // "x=1111011"
+
+

如果要将一个字符串解析为整数,可以使用strconv包的Atoi或ParseInt函数,还有用于解析无符号整数的ParseUint函数:

+
x, err := strconv.Atoi("123")             // x is an int
+y, err := strconv.ParseInt("123", 10, 64) // base 10, up to 64 bits
+
+

ParseInt函数的第三个参数是用于指定整型数的大小;例如16表示int16,0则表示int。在任何情况下,返回的结果y总是int64类型,你可以通过强制类型转换将它转为更小的整数类型。

+

有时候也会使用fmt.Scanf来解析输入的字符串和数字,特别是当字符串和数字混合在一行的时候,它可以灵活处理不完整或不规则的输入。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch3/ch3-06.html b/ch3/ch3-06.html new file mode 100644 index 0000000..3e23073 --- /dev/null +++ b/ch3/ch3-06.html @@ -0,0 +1,402 @@ + + + + + + 常量 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

3.6. 常量

+

常量表达式的值在编译期计算,而不是在运行期。每种常量的潜在类型都是基础类型:boolean、string或数字。

+

一个常量的声明语句定义了常量的名字,和变量的声明语法类似,常量的值不可修改,这样可以防止在运行期被意外或恶意的修改。例如,常量比变量更适合用于表达像π之类的数学常数,因为它们的值不会发生变化:

+
const pi = 3.14159 // approximately; math.Pi is a better approximation
+
+

和变量声明一样,可以批量声明多个常量;这比较适合声明一组相关的常量:

+
const (
+	e  = 2.71828182845904523536028747135266249775724709369995957496696763
+	pi = 3.14159265358979323846264338327950288419716939937510582097494459
+)
+
+

所有常量的运算都可以在编译期完成,这样可以减少运行时的工作,也方便其他编译优化。当操作数是常量时,一些运行时的错误也可以在编译时被发现,例如整数除零、字符串索引越界、任何导致无效浮点数的操作等。

+

常量间的所有算术运算、逻辑运算和比较运算的结果也是常量,对常量的类型转换操作或以下函数调用都是返回常量结果:len、cap、real、imag、complex和unsafe.Sizeof(§13.1)。

+

因为它们的值是在编译期就确定的,因此常量可以是构成类型的一部分,例如用于指定数组类型的长度:

+
const IPv4Len = 4
+
+// parseIPv4 parses an IPv4 address (d.d.d.d).
+func parseIPv4(s string) IP {
+	var p [IPv4Len]byte
+	// ...
+}
+
+

一个常量的声明也可以包含一个类型和一个值,但是如果没有显式指明类型,那么将从右边的表达式推断类型。在下面的代码中,time.Duration是一个命名类型,底层类型是int64,time.Minute是对应类型的常量。下面声明的两个常量都是time.Duration类型,可以通过%T参数打印类型信息:

+
const noDelay time.Duration = 0
+const timeout = 5 * time.Minute
+fmt.Printf("%T %[1]v\n", noDelay)     // "time.Duration 0"
+fmt.Printf("%T %[1]v\n", timeout)     // "time.Duration 5m0s"
+fmt.Printf("%T %[1]v\n", time.Minute) // "time.Duration 1m0s"
+
+

如果是批量声明的常量,除了第一个外其它的常量右边的初始化表达式都可以省略,如果省略初始化表达式则表示使用前面常量的初始化表达式写法,对应的常量类型也一样的。例如:

+
const (
+	a = 1
+	b
+	c = 2
+	d
+)
+
+fmt.Println(a, b, c, d) // "1 1 2 2"
+
+

如果只是简单地复制右边的常量表达式,其实并没有太实用的价值。但是它可以带来其它的特性,那就是iota常量生成器语法。

+

3.6.1. iota 常量生成器

+

常量声明可以使用iota常量生成器初始化,它用于生成一组以相似规则初始化的常量,但是不用每行都写一遍初始化表达式。在一个const声明语句中,在第一个声明的常量所在的行,iota将会被置为0,然后在每一个有常量声明的行加一。

+

下面是来自time包的例子,它首先定义了一个Weekday命名类型,然后为一周的每天定义了一个常量,从周日0开始。在其它编程语言中,这种类型一般被称为枚举类型。

+
type Weekday int
+
+const (
+	Sunday Weekday = iota
+	Monday
+	Tuesday
+	Wednesday
+	Thursday
+	Friday
+	Saturday
+)
+
+

周日将对应0,周一为1,如此等等。

+

我们也可以在复杂的常量表达式中使用iota,下面是来自net包的例子,用于给一个无符号整数的最低5bit的每个bit指定一个名字:

+
type Flags uint
+
+const (
+	FlagUp Flags = 1 << iota // is up
+	FlagBroadcast            // supports broadcast access capability
+	FlagLoopback             // is a loopback interface
+	FlagPointToPoint         // belongs to a point-to-point link
+	FlagMulticast            // supports multicast access capability
+)
+
+

随着iota的递增,每个常量对应表达式1 << iota,是连续的2的幂,分别对应一个bit位置。使用这些常量可以用于测试、设置或清除对应的bit位的值:

+

gopl.io/ch3/netflag

+
func IsUp(v Flags) bool     { return v&FlagUp == FlagUp }
+func TurnDown(v *Flags)     { *v &^= FlagUp }
+func SetBroadcast(v *Flags) { *v |= FlagBroadcast }
+func IsCast(v Flags) bool   { return v&(FlagBroadcast|FlagMulticast) != 0 }
+
+func main() {
+	var v Flags = FlagMulticast | FlagUp
+	fmt.Printf("%b %t\n", v, IsUp(v)) // "10001 true"
+	TurnDown(&v)
+	fmt.Printf("%b %t\n", v, IsUp(v)) // "10000 false"
+	SetBroadcast(&v)
+	fmt.Printf("%b %t\n", v, IsUp(v))   // "10010 false"
+	fmt.Printf("%b %t\n", v, IsCast(v)) // "10010 true"
+}
+
+

下面是一个更复杂的例子,每个常量都是1024的幂:

+
const (
+	_ = 1 << (10 * iota)
+	KiB // 1024
+	MiB // 1048576
+	GiB // 1073741824
+	TiB // 1099511627776             (exceeds 1 << 32)
+	PiB // 1125899906842624
+	EiB // 1152921504606846976
+	ZiB // 1180591620717411303424    (exceeds 1 << 64)
+	YiB // 1208925819614629174706176
+)
+
+

不过iota常量生成规则也有其局限性。例如,它并不能用于产生1000的幂(KB、MB等),因为Go语言并没有计算幂的运算符。

+

练习 3.13: 编写KB、MB的常量声明,然后扩展到YB。

+

3.6.2. 无类型常量

+

Go语言的常量有个不同寻常之处。虽然一个常量可以有任意一个确定的基础类型,例如int或float64,或者是类似time.Duration这样命名的基础类型,但是许多常量并没有一个明确的基础类型。编译器为这些没有明确基础类型的数字常量提供比基础类型更高精度的算术运算;你可以认为至少有256bit的运算精度。这里有六种未明确类型的常量类型,分别是无类型的布尔型、无类型的整数、无类型的字符、无类型的浮点数、无类型的复数、无类型的字符串。

+

通过延迟明确常量的具体类型,无类型的常量不仅可以提供更高的运算精度,而且可以直接用于更多的表达式而不需要显式的类型转换。例如,例子中的ZiB和YiB的值已经超出任何Go语言中整数类型能表达的范围,但是它们依然是合法的常量,而且像下面的常量表达式依然有效(译注:YiB/ZiB是在编译期计算出来的,并且结果常量是1024,是Go语言int变量能有效表示的):

+
fmt.Println(YiB/ZiB) // "1024"
+
+

另一个例子,math.Pi无类型的浮点数常量,可以直接用于任意需要浮点数或复数的地方:

+
var x float32 = math.Pi
+var y float64 = math.Pi
+var z complex128 = math.Pi
+
+

如果math.Pi被确定为特定类型,比如float64,那么结果精度可能会不一样,同时对于需要float32或complex128类型值的地方则会强制需要一个明确的类型转换:

+
const Pi64 float64 = math.Pi
+
+var x float32 = float32(Pi64)
+var y float64 = Pi64
+var z complex128 = complex128(Pi64)
+
+

对于常量面值,不同的写法可能会对应不同的类型。例如0、0.0、0i和\u0000虽然有着相同的常量值,但是它们分别对应无类型的整数、无类型的浮点数、无类型的复数和无类型的字符等不同的常量类型。同样,true和false也是无类型的布尔类型,字符串面值常量是无类型的字符串类型。

+

前面说过除法运算符/会根据操作数的类型生成对应类型的结果。因此,不同写法的常量除法表达式可能对应不同的结果:

+
var f float64 = 212
+fmt.Println((f - 32) * 5 / 9)     // "100"; (f - 32) * 5 is a float64
+fmt.Println(5 / 9 * (f - 32))     // "0";   5/9 is an untyped integer, 0
+fmt.Println(5.0 / 9.0 * (f - 32)) // "100"; 5.0/9.0 is an untyped float
+
+

只有常量可以是无类型的。当一个无类型的常量被赋值给一个变量的时候,就像下面的第一行语句,或者出现在有明确类型的变量声明的右边,如下面的其余三行语句,无类型的常量将会被隐式转换为对应的类型,如果转换合法的话。

+
var f float64 = 3 + 0i // untyped complex -> float64
+f = 2                  // untyped integer -> float64
+f = 1e123              // untyped floating-point -> float64
+f = 'a'                // untyped rune -> float64
+
+

上面的语句相当于:

+
var f float64 = float64(3 + 0i)
+f = float64(2)
+f = float64(1e123)
+f = float64('a')
+
+

无论是隐式或显式转换,将一种类型转换为另一种类型都要求目标可以表示原始值。对于浮点数和复数,可能会有舍入处理:

+
const (
+	deadbeef = 0xdeadbeef // untyped int with value 3735928559
+	a = uint32(deadbeef)  // uint32 with value 3735928559
+	b = float32(deadbeef) // float32 with value 3735928576 (rounded up)
+	c = float64(deadbeef) // float64 with value 3735928559 (exact)
+	d = int32(deadbeef)   // compile error: constant overflows int32
+	e = float64(1e309)    // compile error: constant overflows float64
+	f = uint(-1)          // compile error: constant underflows uint
+)
+
+

对于一个没有显式类型的变量声明(包括简短变量声明),常量的形式将隐式决定变量的默认类型,就像下面的例子:

+
i := 0      // untyped integer;        implicit int(0)
+r := '\000' // untyped rune;           implicit rune('\000')
+f := 0.0    // untyped floating-point; implicit float64(0.0)
+c := 0i     // untyped complex;        implicit complex128(0i)
+
+

注意有一点不同:无类型整数常量转换为int,它的内存大小是不确定的,但是无类型浮点数和复数常量则转换为内存大小明确的float64和complex128。 +如果不知道浮点数类型的内存大小是很难写出正确的数值算法的,因此Go语言不存在整型类似的不确定内存大小的浮点数和复数类型。

+

如果要给变量一个不同的类型,我们必须显式地将无类型的常量转化为所需的类型,或给声明的变量指定明确的类型,像下面例子这样:

+
var i = int8(0)
+var i int8 = 0
+
+

当尝试将这些无类型的常量转为一个接口值时(见第7章),这些默认类型将显得尤为重要,因为要靠它们明确接口对应的动态类型。

+
fmt.Printf("%T\n", 0)      // "int"
+fmt.Printf("%T\n", 0.0)    // "float64"
+fmt.Printf("%T\n", 0i)     // "complex128"
+fmt.Printf("%T\n", '\000') // "int32" (rune)
+
+

现在我们已经讲述了Go语言中全部的基础数据类型。下一步将演示如何用基础数据类型组合成数组或结构体等复杂数据类型,然后构建用于解决实际编程问题的数据结构,这将是第四章的讨论主题。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch3/ch3.html b/ch3/ch3.html new file mode 100644 index 0000000..cfb6424 --- /dev/null +++ b/ch3/ch3.html @@ -0,0 +1,240 @@ + + + + + + 基础数据类型 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

第3章 基础数据类型

+

虽然从底层而言,所有的数据都是由比特组成,但计算机一般操作的是固定大小的数,如整数、浮点数、比特数组、内存地址等。进一步将这些数组织在一起,就可表达更多的对象,例如数据包、像素点、诗歌,甚至其他任何对象。Go语言提供了丰富的数据组织形式,这依赖于Go语言内置的数据类型。这些内置的数据类型,兼顾了硬件的特性和表达复杂数据结构的便捷性。

+

Go语言将数据类型分为四类:基础类型、复合类型、引用类型和接口类型。本章介绍基础类型,包括:数字、字符串和布尔型。复合数据类型——数组(§4.1)和结构体(§4.2)——是通过组合简单类型,来表达更加复杂的数据结构。引用类型包括指针(§2.3.2)、切片(§4.2))、字典(§4.3)、函数(§5)、通道(§8),虽然数据种类很多,但它们都是对程序中一个变量或状态的间接引用。这意味着对任一引用类型数据的修改都会影响所有该引用的拷贝。我们将在第7章介绍接口类型。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch4/ch4-01.html b/ch4/ch4-01.html new file mode 100644 index 0000000..ff42c41 --- /dev/null +++ b/ch4/ch4-01.html @@ -0,0 +1,325 @@ + + + + + + 数组 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

4.1. 数组

+

数组是一个由固定长度的特定类型元素组成的序列,一个数组可以由零个或多个元素组成。因为数组的长度是固定的,因此在Go语言中很少直接使用数组。和数组对应的类型是Slice(切片),它是可以增长和收缩的动态序列,slice功能也更灵活,但是要理解slice工作原理的话需要先理解数组。

+

数组的每个元素可以通过索引下标来访问,索引下标的范围是从0开始到数组长度减1的位置。内置的len函数将返回数组中元素的个数。

+
var a [3]int             // array of 3 integers
+fmt.Println(a[0])        // print the first element
+fmt.Println(a[len(a)-1]) // print the last element, a[2]
+
+// Print the indices and elements.
+for i, v := range a {
+	fmt.Printf("%d %d\n", i, v)
+}
+
+// Print the elements only.
+for _, v := range a {
+	fmt.Printf("%d\n", v)
+}
+
+

默认情况下,数组的每个元素都被初始化为元素类型对应的零值,对于数字类型来说就是0。我们也可以使用数组字面值语法用一组值来初始化数组:

+
var q [3]int = [3]int{1, 2, 3}
+var r [3]int = [3]int{1, 2}
+fmt.Println(r[2]) // "0"
+
+

在数组字面值中,如果在数组的长度位置出现的是“...”省略号,则表示数组的长度是根据初始化值的个数来计算。因此,上面q数组的定义可以简化为

+
q := [...]int{1, 2, 3}
+fmt.Printf("%T\n", q) // "[3]int"
+
+

数组的长度是数组类型的一个组成部分,因此[3]int和[4]int是两种不同的数组类型。数组的长度必须是常量表达式,因为数组的长度需要在编译阶段确定。

+
q := [3]int{1, 2, 3}
+q = [4]int{1, 2, 3, 4} // compile error: cannot assign [4]int to [3]int
+
+

我们将会发现,数组、slice、map和结构体字面值的写法都很相似。上面的形式是直接提供顺序初始化值序列,但是也可以指定一个索引和对应值列表的方式初始化,就像下面这样:

+
type Currency int
+
+const (
+	USD Currency = iota // 美元
+	EUR                 // 欧元
+	GBP                 // 英镑
+	RMB                 // 人民币
+)
+
+symbol := [...]string{USD: "$", EUR: "€", GBP: "£", RMB: "¥"}
+
+fmt.Println(RMB, symbol[RMB]) // "3 ¥"
+
+

在这种形式的数组字面值形式中,初始化索引的顺序是无关紧要的,而且没用到的索引可以省略,和前面提到的规则一样,未指定初始值的元素将用零值初始化。例如,

+
r := [...]int{99: -1}
+
+

定义了一个含有100个元素的数组r,最后一个元素被初始化为-1,其它元素都是用0初始化。

+

如果一个数组的元素类型是可以相互比较的,那么数组类型也是可以相互比较的,这时候我们可以直接通过==比较运算符来比较两个数组,只有当两个数组的所有元素都是相等的时候数组才是相等的。不相等比较运算符!=遵循同样的规则。

+
a := [2]int{1, 2}
+b := [...]int{1, 2}
+c := [2]int{1, 3}
+fmt.Println(a == b, a == c, b == c) // "true false false"
+d := [3]int{1, 2}
+fmt.Println(a == d) // compile error: cannot compare [2]int == [3]int
+
+

作为一个真实的例子,crypto/sha256包的Sum256函数对一个任意的字节slice类型的数据生成一个对应的消息摘要。消息摘要有256bit大小,因此对应[32]byte数组类型。如果两个消息摘要是相同的,那么可以认为两个消息本身也是相同(译注:理论上有HASH码碰撞的情况,但是实际应用可以基本忽略);如果消息摘要不同,那么消息本身必然也是不同的。下面的例子用SHA256算法分别生成“x”和“X”两个信息的摘要:

+

gopl.io/ch4/sha256

+
import "crypto/sha256"
+
+func main() {
+	c1 := sha256.Sum256([]byte("x"))
+	c2 := sha256.Sum256([]byte("X"))
+	fmt.Printf("%x\n%x\n%t\n%T\n", c1, c2, c1 == c2, c1)
+	// Output:
+	// 2d711642b726b04401627ca9fbac32f5c8530fb1903cc4db02258717921a4881
+	// 4b68ab3847feda7d6c62c1fbcbeebfa35eab7351ed5e78f4ddadea5df64b8015
+	// false
+	// [32]uint8
+}
+
+

上面例子中,两个消息虽然只有一个字符的差异,但是生成的消息摘要则几乎有一半的bit位是不相同的。需要注意Printf函数的%x副词参数,它用于指定以十六进制的格式打印数组或slice全部的元素,%t副词参数是用于打印布尔型数据,%T副词参数是用于显示一个值对应的数据类型。

+

当调用一个函数的时候,函数的每个调用参数将会被赋值给函数内部的参数变量,所以函数参数变量接收的是一个复制的副本,并不是原始调用的变量。因为函数参数传递的机制导致传递大的数组类型将是低效的,并且对数组参数的任何的修改都是发生在复制的数组上,并不能直接修改调用时原始的数组变量。在这个方面,Go语言对待数组的方式和其它很多编程语言不同,其它编程语言可能会隐式地将数组作为引用或指针对象传入被调用的函数。

+

当然,我们可以显式地传入一个数组指针,那样的话函数通过指针对数组的任何修改都可以直接反馈到调用者。下面的函数用于给[32]byte类型的数组清零:

+
func zero(ptr *[32]byte) {
+	for i := range ptr {
+		ptr[i] = 0
+	}
+}
+
+

其实数组字面值[32]byte{}就可以生成一个32字节的数组。而且每个数组的元素都是零值初始化,也就是0。因此,我们可以将上面的zero函数写的更简洁一点:

+
func zero(ptr *[32]byte) {
+	*ptr = [32]byte{}
+}
+
+

虽然通过指针来传递数组参数是高效的,而且也允许在函数内部修改数组的值,但是数组依然是僵化的类型,因为数组的类型包含了僵化的长度信息。上面的zero函数并不能接收指向[16]byte类型数组的指针,而且也没有任何添加或删除数组元素的方法。由于这些原因,除了像SHA256这类需要处理特定大小数组的特例外,数组依然很少用作函数参数;相反,我们一般使用slice来替代数组。

+

练习 4.1: 编写一个函数,计算两个SHA256哈希码中不同bit的数目。(参考2.6.2节的PopCount函数。)

+

练习 4.2: 编写一个程序,默认情况下打印标准输入的SHA256编码,并支持通过命令行flag定制,输出SHA384或SHA512哈希算法。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch4/ch4-02.html b/ch4/ch4-02.html new file mode 100644 index 0000000..e03ee7f --- /dev/null +++ b/ch4/ch4-02.html @@ -0,0 +1,482 @@ + + + + + + Slice - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

4.2. Slice

+

Slice(切片)代表变长的序列,序列中每个元素都有相同的类型。一个slice类型一般写作[]T,其中T代表slice中元素的类型;slice的语法和数组很像,只是没有固定长度而已。

+

数组和slice之间有着紧密的联系。一个slice是一个轻量级的数据结构,提供了访问数组子序列(或者全部)元素的功能,而且slice的底层确实引用一个数组对象。一个slice由三个部分构成:指针、长度和容量。指针指向第一个slice元素对应的底层数组元素的地址,要注意的是slice的第一个元素并不一定就是数组的第一个元素。长度对应slice中元素的数目;长度不能超过容量,容量一般是从slice的开始位置到底层数据的结尾位置。内置的len和cap函数分别返回slice的长度和容量。

+

多个slice之间可以共享底层的数据,并且引用的数组部分区间可能重叠。图4.1显示了表示一年中每个月份名字的字符串数组,还有重叠引用了该数组的两个slice。数组这样定义

+
months := [...]string{1: "January", /* ... */, 12: "December"}
+
+

因此一月份是months[1],十二月份是months[12]。通常,数组的第一个元素从索引0开始,但是月份一般是从1开始的,因此我们声明数组时直接跳过第0个元素,第0个元素会被自动初始化为空字符串。

+

slice的切片操作s[i:j],其中0 ≤ i≤ j≤ cap(s),用于创建一个新的slice,引用s的从第i个元素开始到第j-1个元素的子序列。新的slice将只有j-i个元素。如果i位置的索引被省略的话将使用0代替,如果j位置的索引被省略的话将使用len(s)代替。因此,months[1:13]切片操作将引用全部有效的月份,和months[1:]操作等价;months[:]切片操作则是引用整个数组。让我们分别定义表示第二季度和北方夏天月份的slice,它们有重叠部分:

+

+
Q2 := months[4:7]
+summer := months[6:9]
+fmt.Println(Q2)     // ["April" "May" "June"]
+fmt.Println(summer) // ["June" "July" "August"]
+
+

两个slice都包含了六月份,下面的代码是一个包含相同月份的测试(性能较低):

+
for _, s := range summer {
+	for _, q := range Q2 {
+		if s == q {
+			fmt.Printf("%s appears in both\n", s)
+		}
+	}
+}
+
+

如果切片操作超出cap(s)的上限将导致一个panic异常,但是超出len(s)则是意味着扩展了slice,因为新slice的长度会变大:

+
fmt.Println(summer[:20]) // panic: out of range
+
+endlessSummer := summer[:5] // extend a slice (within capacity)
+fmt.Println(endlessSummer)  // "[June July August September October]"
+
+

另外,字符串的切片操作和[]byte字节类型切片的切片操作是类似的。都写作x[m:n],并且都是返回一个原始字节序列的子序列,底层都是共享之前的底层数组,因此这种操作都是常量时间复杂度。x[m:n]切片操作对于字符串则生成一个新字符串,如果x是[]byte的话则生成一个新的[]byte。

+

因为slice值包含指向第一个slice元素的指针,因此向函数传递slice将允许在函数内部修改底层数组的元素。换句话说,复制一个slice只是对底层的数组创建了一个新的slice别名(§2.3.2)。下面的reverse函数在原内存空间将[]int类型的slice反转,而且它可以用于任意长度的slice。

+

gopl.io/ch4/rev

+
// reverse reverses a slice of ints in place.
+func reverse(s []int) {
+	for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
+		s[i], s[j] = s[j], s[i]
+	}
+}
+
+

这里我们反转数组的应用:

+
a := [...]int{0, 1, 2, 3, 4, 5}
+reverse(a[:])
+fmt.Println(a) // "[5 4 3 2 1 0]"
+
+

一种将slice元素循环向左旋转n个元素的方法是三次调用reverse反转函数,第一次是反转开头的n个元素,然后是反转剩下的元素,最后是反转整个slice的元素。(如果是向右循环旋转,则将第三个函数调用移到第一个调用位置就可以了。)

+
s := []int{0, 1, 2, 3, 4, 5}
+// Rotate s left by two positions.
+reverse(s[:2])
+reverse(s[2:])
+reverse(s)
+fmt.Println(s) // "[2 3 4 5 0 1]"
+
+

要注意的是slice类型的变量s和数组类型的变量a的初始化语法的差异。slice和数组的字面值语法很类似,它们都是用花括弧包含一系列的初始化元素,但是对于slice并没有指明序列的长度。这会隐式地创建一个合适大小的数组,然后slice的指针指向底层的数组。就像数组字面值一样,slice的字面值也可以按顺序指定初始化值序列,或者是通过索引和元素值指定,或者用两种风格的混合语法初始化。

+

和数组不同的是,slice之间不能比较,因此我们不能使用==操作符来判断两个slice是否含有全部相等元素。不过标准库提供了高度优化的bytes.Equal函数来判断两个字节型slice是否相等([]byte),但是对于其他类型的slice,我们必须自己展开每个元素进行比较:

+
func equal(x, y []string) bool {
+	if len(x) != len(y) {
+		return false
+	}
+	for i := range x {
+		if x[i] != y[i] {
+			return false
+		}
+	}
+	return true
+}
+
+

上面关于两个slice的深度相等测试,运行的时间并不比支持==操作的数组或字符串更多,但是为何slice不直接支持比较运算符呢?这方面有两个原因。第一个原因,一个slice的元素是间接引用的,一个slice甚至可以包含自身(译注:当slice声明为[]interface{}时,slice的元素可以是自身)。虽然有很多办法处理这种情形,但是没有一个是简单有效的。

+

第二个原因,因为slice的元素是间接引用的,一个固定的slice值(译注:指slice本身的值,不是元素的值)在不同的时刻可能包含不同的元素,因为底层数组的元素可能会被修改。而例如Go语言中map的key只做简单的浅拷贝,它要求key在整个生命周期内保持不变性(译注:例如slice扩容,就会导致其本身的值/地址变化)。而用深度相等判断的话,显然在map的key这种场合不合适。对于像指针或chan之类的引用类型,==相等测试可以判断两个是否是引用相同的对象。一个针对slice的浅相等测试的==操作符可能是有一定用处的,也能临时解决map类型的key问题,但是slice和数组不同的相等测试行为会让人困惑。因此,安全的做法是直接禁止slice之间的比较操作。

+

slice唯一合法的比较操作是和nil比较,例如:

+
if summer == nil { /* ... */ }
+
+

一个零值的slice等于nil。一个nil值的slice并没有底层数组。一个nil值的slice的长度和容量都是0,但是也有非nil值的slice的长度和容量也是0的,例如[]int{}或make([]int, 3)[3:]。与任意类型的nil值一样,我们可以用[]int(nil)类型转换表达式来生成一个对应类型slice的nil值。

+
var s []int    // len(s) == 0, s == nil
+s = nil        // len(s) == 0, s == nil
+s = []int(nil) // len(s) == 0, s == nil
+s = []int{}    // len(s) == 0, s != nil
+
+

如果你需要测试一个slice是否是空的,使用len(s) == 0来判断,而不应该用s == nil来判断。除了和nil相等比较外,一个nil值的slice的行为和其它任意0长度的slice一样;例如reverse(nil)也是安全的。除了文档已经明确说明的地方,所有的Go语言函数应该以相同的方式对待nil值的slice和0长度的slice。

+

内置的make函数创建一个指定元素类型、长度和容量的slice。容量部分可以省略,在这种情况下,容量将等于长度。

+
make([]T, len)
+make([]T, len, cap) // same as make([]T, cap)[:len]
+
+

在底层,make创建了一个匿名的数组变量,然后返回一个slice;只有通过返回的slice才能引用底层匿名的数组变量。在第一种语句中,slice是整个数组的view。在第二个语句中,slice只引用了底层数组的前len个元素,但是容量将包含整个的数组。额外的元素是留给未来的增长用的。

+

4.2.1. append函数

+

内置的append函数用于向slice追加元素:

+
var runes []rune
+for _, r := range "Hello, 世界" {
+	runes = append(runes, r)
+}
+fmt.Printf("%q\n", runes) // "['H' 'e' 'l' 'l' 'o' ',' ' ' '世' '界']"
+
+

在循环中使用append函数构建一个由九个rune字符构成的slice,当然对应这个特殊的问题我们可以通过Go语言内置的[]rune("Hello, 世界")转换操作完成。

+

append函数对于理解slice底层是如何工作的非常重要,所以让我们仔细查看究竟是发生了什么。下面是第一个版本的appendInt函数,专门用于处理[]int类型的slice:

+

gopl.io/ch4/append

+
func appendInt(x []int, y int) []int {
+	var z []int
+	zlen := len(x) + 1
+	if zlen <= cap(x) {
+		// There is room to grow.  Extend the slice.
+		z = x[:zlen]
+	} else {
+		// There is insufficient space.  Allocate a new array.
+		// Grow by doubling, for amortized linear complexity.
+		zcap := zlen
+		if zcap < 2*len(x) {
+			zcap = 2 * len(x)
+		}
+		z = make([]int, zlen, zcap)
+		copy(z, x) // a built-in function; see text
+	}
+	z[len(x)] = y
+	return z
+}
+
+

每次调用appendInt函数,必须先检测slice底层数组是否有足够的容量来保存新添加的元素。如果有足够空间的话,直接扩展slice(依然在原有的底层数组之上),将新添加的y元素复制到新扩展的空间,并返回slice。因此,输入的x和输出的z共享相同的底层数组。

+

如果没有足够的增长空间的话,appendInt函数则会先分配一个足够大的slice用于保存新的结果,先将输入的x复制到新的空间,然后添加y元素。结果z和输入的x引用的将是不同的底层数组。

+

虽然通过循环复制元素更直接,不过内置的copy函数可以方便地将一个slice复制另一个相同类型的slice。copy函数的第一个参数是要复制的目标slice,第二个参数是源slice,目标和源的位置顺序和dst = src赋值语句是一致的。两个slice可以共享同一个底层数组,甚至有重叠也没有问题。copy函数将返回成功复制的元素的个数(我们这里没有用到),等于两个slice中较小的长度,所以我们不用担心覆盖会超出目标slice的范围。

+

为了提高内存使用效率,新分配的数组一般略大于保存x和y所需要的最低大小。通过在每次扩展数组时直接将长度翻倍从而避免了多次内存分配,也确保了添加单个元素操作的平均时间是一个常数时间。这个程序演示了效果:

+
func main() {
+	var x, y []int
+	for i := 0; i < 10; i++ {
+		y = appendInt(x, i)
+		fmt.Printf("%d cap=%d\t%v\n", i, cap(y), y)
+		x = y
+	}
+}
+
+

每一次容量的变化都会导致重新分配内存和copy操作:

+
0  cap=1    [0]
+1  cap=2    [0 1]
+2  cap=4    [0 1 2]
+3  cap=4    [0 1 2 3]
+4  cap=8    [0 1 2 3 4]
+5  cap=8    [0 1 2 3 4 5]
+6  cap=8    [0 1 2 3 4 5 6]
+7  cap=8    [0 1 2 3 4 5 6 7]
+8  cap=16   [0 1 2 3 4 5 6 7 8]
+9  cap=16   [0 1 2 3 4 5 6 7 8 9]
+
+

让我们仔细查看i=3次的迭代。当时x包含了[0 1 2]三个元素,但是容量是4,因此可以简单将新的元素添加到末尾,不需要新的内存分配。然后新的y的长度和容量都是4,并且和x引用着相同的底层数组,如图4.2所示。

+

+

在下一次迭代时i=4,现在没有新的空余的空间了,因此appendInt函数分配一个容量为8的底层数组,将x的4个元素[0 1 2 3]复制到新空间的开头,然后添加新的元素i,新元素的值是4。新的y的长度是5,容量是8;后面有3个空闲的位置,三次迭代都不需要分配新的空间。当前迭代中,y和x是对应不同底层数组的view。这次操作如图4.3所示。

+

+

内置的append函数可能使用比appendInt更复杂的内存扩展策略。因此,通常我们并不知道append调用是否导致了内存的重新分配,因此我们也不能确认新的slice和原始的slice是否引用的是相同的底层数组空间。同样,我们不能确认在原先的slice上的操作是否会影响到新的slice。因此,通常是将append返回的结果直接赋值给输入的slice变量:

+
runes = append(runes, r)
+
+

更新slice变量不仅对调用append函数是必要的,实际上对应任何可能导致长度、容量或底层数组变化的操作都是必要的。要正确地使用slice,需要记住尽管底层数组的元素是间接访问的,但是slice对应结构体本身的指针、长度和容量部分是直接访问的。要更新这些信息需要像上面例子那样一个显式的赋值操作。从这个角度看,slice并不是一个纯粹的引用类型,它实际上是一个类似下面结构体的聚合类型:

+
type IntSlice struct {
+	ptr      *int
+	len, cap int
+}
+
+

我们的appendInt函数每次只能向slice追加一个元素,但是内置的append函数则可以追加多个元素,甚至追加一个slice。

+
var x []int
+x = append(x, 1)
+x = append(x, 2, 3)
+x = append(x, 4, 5, 6)
+x = append(x, x...) // append the slice x
+fmt.Println(x)      // "[1 2 3 4 5 6 1 2 3 4 5 6]"
+
+

通过下面的小修改,我们可以达到append函数类似的功能。其中在appendInt函数参数中的最后的“...”省略号表示接收变长的参数为slice。我们将在5.7节详细解释这个特性。

+
func appendInt(x []int, y ...int) []int {
+	var z []int
+	zlen := len(x) + len(y)
+	// ...expand z to at least zlen...
+	copy(z[len(x):], y)
+	return z
+}
+
+

为了避免重复,和前面相同的代码并没有显示。

+

4.2.2. Slice内存技巧

+

让我们看看更多的例子,比如旋转slice、反转slice或在slice原有内存空间修改元素。给定一个字符串列表,下面的nonempty函数将在原有slice内存空间之上返回不包含空字符串的列表:

+

gopl.io/ch4/nonempty

+
// Nonempty is an example of an in-place slice algorithm.
+package main
+
+import "fmt"
+
+// nonempty returns a slice holding only the non-empty strings.
+// The underlying array is modified during the call.
+func nonempty(strings []string) []string {
+	i := 0
+	for _, s := range strings {
+		if s != "" {
+			strings[i] = s
+			i++
+		}
+	}
+	return strings[:i]
+}
+
+

比较微妙的地方是,输入的slice和输出的slice共享一个底层数组。这可以避免分配另一个数组,不过原来的数据将可能会被覆盖,正如下面两个打印语句看到的那样:

+
data := []string{"one", "", "three"}
+fmt.Printf("%q\n", nonempty(data)) // `["one" "three"]`
+fmt.Printf("%q\n", data)           // `["one" "three" "three"]`
+
+

因此我们通常会这样使用nonempty函数:data = nonempty(data)

+

nonempty函数也可以使用append函数实现:

+
func nonempty2(strings []string) []string {
+	out := strings[:0] // zero-length slice of original
+	for _, s := range strings {
+		if s != "" {
+			out = append(out, s)
+		}
+	}
+	return out
+}
+
+

无论如何实现,以这种方式重用一个slice一般都要求最多为每个输入值产生一个输出值,事实上很多这类算法都是用来过滤或合并序列中相邻的元素。这种slice用法是比较复杂的技巧,虽然使用到了slice的一些技巧,但是对于某些场合是比较清晰和有效的。

+

一个slice可以用来模拟一个stack。最初给定的空slice对应一个空的stack,然后可以使用append函数将新的值压入stack:

+
stack = append(stack, v) // push v
+
+

stack的顶部位置对应slice的最后一个元素:

+
top := stack[len(stack)-1] // top of stack
+
+

通过收缩stack可以弹出栈顶的元素

+
stack = stack[:len(stack)-1] // pop
+
+

要删除slice中间的某个元素并保存原有的元素顺序,可以通过内置的copy函数将后面的子slice向前依次移动一位完成:

+
func remove(slice []int, i int) []int {
+	copy(slice[i:], slice[i+1:])
+	return slice[:len(slice)-1]
+}
+
+func main() {
+	s := []int{5, 6, 7, 8, 9}
+	fmt.Println(remove(s, 2)) // "[5 6 8 9]"
+}
+
+

如果删除元素后不用保持原来顺序的话,我们可以简单的用最后一个元素覆盖被删除的元素:

+
func remove(slice []int, i int) []int {
+	slice[i] = slice[len(slice)-1]
+	return slice[:len(slice)-1]
+}
+
+func main() {
+	s := []int{5, 6, 7, 8, 9}
+	fmt.Println(remove(s, 2)) // "[5 6 9 8]
+}
+
+

练习 4.3: 重写reverse函数,使用数组指针代替slice。

+

练习 4.4: 编写一个rotate函数,通过一次循环完成旋转。

+

练习 4.5: 写一个函数在原地完成消除[]string中相邻重复的字符串的操作。

+

练习 4.6: 编写一个函数,原地将一个UTF-8编码的[]byte类型的slice中相邻的空格(参考unicode.IsSpace)替换成一个空格返回

+

练习 4.7: 修改reverse函数用于原地反转UTF-8编码的[]byte。是否可以不用分配额外的内存?

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch4/ch4-03.html b/ch4/ch4-03.html new file mode 100644 index 0000000..2d211b0 --- /dev/null +++ b/ch4/ch4-03.html @@ -0,0 +1,442 @@ + + + + + + Map - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

4.3. Map

+

哈希表是一种巧妙并且实用的数据结构。它是一个无序的key/value对的集合,其中所有的key都是不同的,然后通过给定的key可以在常数时间复杂度内检索、更新或删除对应的value。

+

在Go语言中,一个map就是一个哈希表的引用,map类型可以写为map[K]V,其中K和V分别对应key和value。map中所有的key都有相同的类型,所有的value也有着相同的类型,但是key和value之间可以是不同的数据类型。其中K对应的key必须是支持==比较运算符的数据类型,所以map可以通过测试key是否相等来判断是否已经存在。虽然浮点数类型也是支持相等运算符比较的,但是将浮点数用做key类型则是一个坏的想法,正如第三章提到的,最坏的情况是可能出现的NaN和任何浮点数都不相等。对于V对应的value数据类型则没有任何的限制。

+

内置的make函数可以创建一个map:

+
ages := make(map[string]int) // mapping from strings to ints
+
+

我们也可以用map字面值的语法创建map,同时还可以指定一些最初的key/value:

+
ages := map[string]int{
+	"alice":   31,
+	"charlie": 34,
+}
+
+

这相当于

+
ages := make(map[string]int)
+ages["alice"] = 31
+ages["charlie"] = 34
+
+

因此,另一种创建空的map的表达式是map[string]int{}

+

Map中的元素通过key对应的下标语法访问:

+
ages["alice"] = 32
+fmt.Println(ages["alice"]) // "32"
+
+

使用内置的delete函数可以删除元素:

+
delete(ages, "alice") // remove element ages["alice"]
+
+

所有这些操作是安全的,即使这些元素不在map中也没有关系;如果一个查找失败将返回value类型对应的零值,例如,即使map中不存在“bob”下面的代码也可以正常工作,因为ages["bob"]失败时将返回0。

+
ages["bob"] = ages["bob"] + 1 // happy birthday!
+
+

而且x += yx++等简短赋值语法也可以用在map上,所以上面的代码可以改写成

+
ages["bob"] += 1
+
+

更简单的写法

+
ages["bob"]++
+
+

但是map中的元素并不是一个变量,因此我们不能对map的元素进行取址操作:

+
_ = &ages["bob"] // compile error: cannot take address of map element
+
+

禁止对map元素取址的原因是map可能随着元素数量的增长而重新分配更大的内存空间,从而可能导致之前的地址无效。

+

要想遍历map中全部的key/value对的话,可以使用range风格的for循环实现,和之前的slice遍历语法类似。下面的迭代语句将在每次迭代时设置name和age变量,它们对应下一个键/值对:

+
for name, age := range ages {
+	fmt.Printf("%s\t%d\n", name, age)
+}
+
+

Map的迭代顺序是不确定的,并且不同的哈希函数实现可能导致不同的遍历顺序。在实践中,遍历的顺序是随机的,每一次遍历的顺序都不相同。这是故意的,每次都使用随机的遍历顺序可以强制要求程序不会依赖具体的哈希函数实现。如果要按顺序遍历key/value对,我们必须显式地对key进行排序,可以使用sort包的Strings函数对字符串slice进行排序。下面是常见的处理方式:

+
import "sort"
+
+var names []string
+for name := range ages {
+	names = append(names, name)
+}
+sort.Strings(names)
+for _, name := range names {
+	fmt.Printf("%s\t%d\n", name, ages[name])
+}
+
+

因为我们一开始就知道names的最终大小,因此给slice分配一个合适的大小将会更有效。下面的代码创建了一个空的slice,但是slice的容量刚好可以放下map中全部的key:

+
names := make([]string, 0, len(ages))
+
+

在上面的第一个range循环中,我们只关心map中的key,所以我们忽略了第二个循环变量。在第二个循环中,我们只关心names中的名字,所以我们使用“_”空白标识符来忽略第一个循环变量,也就是迭代slice时的索引。

+

map类型的零值是nil,也就是没有引用任何哈希表。

+
var ages map[string]int
+fmt.Println(ages == nil)    // "true"
+fmt.Println(len(ages) == 0) // "true"
+
+

map上的大部分操作,包括查找、删除、len和range循环都可以安全工作在nil值的map上,它们的行为和一个空的map类似。但是向一个nil值的map存入元素将导致一个panic异常:

+
ages["carol"] = 21 // panic: assignment to entry in nil map
+
+

在向map存数据前必须先创建map。

+

通过key作为索引下标来访问map将产生一个value。如果key在map中是存在的,那么将得到与key对应的value;如果key不存在,那么将得到value对应类型的零值,正如我们前面看到的ages["bob"]那样。这个规则很实用,但是有时候可能需要知道对应的元素是否真的是在map之中。例如,如果元素类型是一个数字,你可能需要区分一个已经存在的0,和不存在而返回零值的0,可以像下面这样测试:

+
age, ok := ages["bob"]
+if !ok { /* "bob" is not a key in this map; age == 0. */ }
+
+

你会经常看到将这两个结合起来使用,像这样:

+
if age, ok := ages["bob"]; !ok { /* ... */ }
+
+

在这种场景下,map的下标语法将产生两个值;第二个是一个布尔值,用于报告元素是否真的存在。布尔变量一般命名为ok,特别适合马上用于if条件判断部分。

+

和slice一样,map之间也不能进行相等比较;唯一的例外是和nil进行比较。要判断两个map是否包含相同的key和value,我们必须通过一个循环实现:

+
func equal(x, y map[string]int) bool {
+	if len(x) != len(y) {
+		return false
+	}
+	for k, xv := range x {
+		if yv, ok := y[k]; !ok || yv != xv {
+			return false
+		}
+	}
+	return true
+}
+
+

从例子中可以看到如何用!ok来区分元素不存在,与元素存在但为0的。我们不能简单地用xv != y[k]判断,那样会导致在判断下面两个map时产生错误的结果:

+
// True if equal is written incorrectly.
+equal(map[string]int{"A": 0}, map[string]int{"B": 42})
+
+

Go语言中并没有提供一个set类型,但是map中的key也是不相同的,可以用map实现类似set的功能。为了说明这一点,下面的dedup程序读取多行输入,但是只打印第一次出现的行。(它是1.3节中出现的dup程序的变体。)dedup程序通过map来表示所有的输入行所对应的set集合,以确保已经在集合存在的行不会被重复打印。

+

gopl.io/ch4/dedup

+
func main() {
+	seen := make(map[string]bool) // a set of strings
+	input := bufio.NewScanner(os.Stdin)
+	for input.Scan() {
+		line := input.Text()
+		if !seen[line] {
+			seen[line] = true
+			fmt.Println(line)
+		}
+	}
+
+	if err := input.Err(); err != nil {
+		fmt.Fprintf(os.Stderr, "dedup: %v\n", err)
+		os.Exit(1)
+	}
+}
+
+

Go程序员将这种忽略value的map当作一个字符串集合,并非所有map[string]bool类型value都是无关紧要的;有一些则可能会同时包含true和false的值。

+

有时候我们需要一个map或set的key是slice类型,但是map的key必须是可比较的类型,但是slice并不满足这个条件。不过,我们可以通过两个步骤绕过这个限制。第一步,定义一个辅助函数k,将slice转为map对应的string类型的key,确保只有x和y相等时k(x) == k(y)才成立。然后创建一个key为string类型的map,在每次对map操作时先用k辅助函数将slice转化为string类型。

+

下面的例子演示了如何使用map来记录提交相同的字符串列表的次数。它使用了fmt.Sprintf函数将字符串列表转换为一个字符串以用于map的key,通过%q参数忠实地记录每个字符串元素的信息:

+
var m = make(map[string]int)
+
+func k(list []string) string { return fmt.Sprintf("%q", list) }
+
+func Add(list []string)       { m[k(list)]++ }
+func Count(list []string) int { return m[k(list)] }
+
+

使用同样的技术可以处理任何不可比较的key类型,而不仅仅是slice类型。这种技术对于想使用自定义key比较函数的时候也很有用,例如在比较字符串的时候忽略大小写。同时,辅助函数k(x)也不一定是字符串类型,它可以返回任何可比较的类型,例如整数、数组或结构体等。

+

这是map的另一个例子,下面的程序用于统计输入中每个Unicode码点出现的次数。虽然Unicode全部码点的数量巨大,但是出现在特定文档中的字符种类并没有多少,使用map可以用比较自然的方式来跟踪那些出现过的字符的次数。

+

gopl.io/ch4/charcount

+
// Charcount computes counts of Unicode characters.
+package main
+
+import (
+	"bufio"
+	"fmt"
+	"io"
+	"os"
+	"unicode"
+	"unicode/utf8"
+)
+
+func main() {
+	counts := make(map[rune]int)    // counts of Unicode characters
+	var utflen [utf8.UTFMax + 1]int // count of lengths of UTF-8 encodings
+	invalid := 0                    // count of invalid UTF-8 characters
+
+	in := bufio.NewReader(os.Stdin)
+	for {
+		r, n, err := in.ReadRune() // returns rune, nbytes, error
+		if err == io.EOF {
+			break
+		}
+		if err != nil {
+			fmt.Fprintf(os.Stderr, "charcount: %v\n", err)
+			os.Exit(1)
+		}
+		if r == unicode.ReplacementChar && n == 1 {
+			invalid++
+			continue
+		}
+		counts[r]++
+		utflen[n]++
+	}
+	fmt.Printf("rune\tcount\n")
+	for c, n := range counts {
+		fmt.Printf("%q\t%d\n", c, n)
+	}
+	fmt.Print("\nlen\tcount\n")
+	for i, n := range utflen {
+		if i > 0 {
+			fmt.Printf("%d\t%d\n", i, n)
+		}
+	}
+	if invalid > 0 {
+		fmt.Printf("\n%d invalid UTF-8 characters\n", invalid)
+	}
+}
+
+

ReadRune方法执行UTF-8解码并返回三个值:解码的rune字符的值,字符UTF-8编码后的长度,和一个错误值。我们可预期的错误值只有对应文件结尾的io.EOF。如果输入的是无效的UTF-8编码的字符,返回的将是unicode.ReplacementChar表示无效字符,并且编码长度是1。

+

charcount程序同时打印不同UTF-8编码长度的字符数目。对此,map并不是一个合适的数据结构;因为UTF-8编码的长度总是从1到utf8.UTFMax(最大是4个字节),使用数组将更有效。

+

作为一个实验,我们用charcount程序对英文版原稿的字符进行了统计。虽然大部分是英语,但是也有一些非ASCII字符。下面是排名前10的非ASCII字符:

+

+

下面是不同UTF-8编码长度的字符的数目:

+
len count
+1   765391
+2   60
+3   70
+4   0
+
+

Map的value类型也可以是一个聚合类型,比如是一个map或slice。在下面的代码中,图graph的key类型是一个字符串,value类型map[string]bool代表一个字符串集合。从概念上讲,graph将一个字符串类型的key映射到一组相关的字符串集合,它们指向新的graph的key。

+

gopl.io/ch4/graph

+
var graph = make(map[string]map[string]bool)
+
+func addEdge(from, to string) {
+	edges := graph[from]
+	if edges == nil {
+		edges = make(map[string]bool)
+		graph[from] = edges
+	}
+	edges[to] = true
+}
+
+func hasEdge(from, to string) bool {
+	return graph[from][to]
+}
+
+

其中addEdge函数惰性初始化map是一个惯用方式,也就是说在每个值首次作为key时才初始化。hasEdge函数显示了如何让map的零值也能正常工作;即使from到to的边不存在,graph[from][to]依然可以返回一个有意义的结果。

+

练习 4.8: 修改charcount程序,使用unicode.IsLetter等相关的函数,统计字母、数字等Unicode中不同的字符类别。

+

练习 4.9: 编写一个程序wordfreq程序,报告输入文本中每个单词出现的频率。在第一次调用Scan前先调用input.Split(bufio.ScanWords)函数,这样可以按单词而不是按行输入。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch4/ch4-04.html b/ch4/ch4-04.html new file mode 100644 index 0000000..ae07c44 --- /dev/null +++ b/ch4/ch4-04.html @@ -0,0 +1,495 @@ + + + + + + 结构体 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

4.4. 结构体

+

结构体是一种聚合的数据类型,是由零个或多个任意类型的值聚合成的实体。每个值称为结构体的成员。用结构体的经典案例是处理公司的员工信息,每个员工信息包含一个唯一的员工编号、员工的名字、家庭住址、出生日期、工作岗位、薪资、上级领导等等。所有的这些信息都需要绑定到一个实体中,可以作为一个整体单元被复制,作为函数的参数或返回值,或者是被存储到数组中,等等。

+

下面两个语句声明了一个叫Employee的命名的结构体类型,并且声明了一个Employee类型的变量dilbert:

+
type Employee struct {
+	ID        int
+	Name      string
+	Address   string
+	DoB       time.Time
+	Position  string
+	Salary    int
+	ManagerID int
+}
+
+var dilbert Employee
+
+

dilbert结构体变量的成员可以通过点操作符访问,比如dilbert.Name和dilbert.DoB。因为dilbert是一个变量,它所有的成员也同样是变量,我们可以直接对每个成员赋值:

+
dilbert.Salary -= 5000 // demoted, for writing too few lines of code
+
+

或者是对成员取地址,然后通过指针访问:

+
position := &dilbert.Position
+*position = "Senior " + *position // promoted, for outsourcing to Elbonia
+
+

点操作符也可以和指向结构体的指针一起工作:

+
var employeeOfTheMonth *Employee = &dilbert
+employeeOfTheMonth.Position += " (proactive team player)"
+
+

相当于下面语句

+
(*employeeOfTheMonth).Position += " (proactive team player)"
+
+

下面的EmployeeByID函数将根据给定的员工ID返回对应的员工信息结构体的指针。我们可以使用点操作符来访问它里面的成员:

+
func EmployeeByID(id int) *Employee { /* ... */ }
+
+fmt.Println(EmployeeByID(dilbert.ManagerID).Position) // "Pointy-haired boss"
+
+id := dilbert.ID
+EmployeeByID(id).Salary = 0 // fired for... no real reason
+
+

后面的语句通过EmployeeByID返回的结构体指针更新了Employee结构体的成员。如果将EmployeeByID函数的返回值从*Employee指针类型改为Employee值类型,那么更新语句将不能编译通过,因为在赋值语句的左边并不确定是一个变量(译注:调用函数返回的是值,并不是一个可取地址的变量)。

+

通常一行对应一个结构体成员,成员的名字在前类型在后,不过如果相邻的成员类型如果相同的话可以被合并到一行,就像下面的Name和Address成员那样:

+
type Employee struct {
+	ID            int
+	Name, Address string
+	DoB           time.Time
+	Position      string
+	Salary        int
+	ManagerID     int
+}
+
+

结构体成员的输入顺序也有重要的意义。我们也可以将Position成员合并(因为也是字符串类型),或者是交换Name和Address出现的先后顺序,那样的话就是定义了不同的结构体类型。通常,我们只是将相关的成员写到一起。

+

如果结构体成员名字是以大写字母开头的,那么该成员就是导出的;这是Go语言导出规则决定的。一个结构体可能同时包含导出和未导出的成员。

+

结构体类型往往是冗长的,因为它的每个成员可能都会占一行。虽然我们每次都可以重写整个结构体成员,但是重复会令人厌烦。因此,完整的结构体写法通常只在类型声明语句的地方出现,就像Employee类型声明语句那样。

+

一个命名为S的结构体类型将不能再包含S类型的成员:因为一个聚合的值不能包含它自身。(该限制同样适用于数组。)但是S类型的结构体可以包含*S指针类型的成员,这可以让我们创建递归的数据结构,比如链表和树结构等。在下面的代码中,我们使用一个二叉树来实现一个插入排序:

+

gopl.io/ch4/treesort

+
type tree struct {
+	value       int
+	left, right *tree
+}
+
+// Sort sorts values in place.
+func Sort(values []int) {
+	var root *tree
+	for _, v := range values {
+		root = add(root, v)
+	}
+	appendValues(values[:0], root)
+}
+
+// appendValues appends the elements of t to values in order
+// and returns the resulting slice.
+func appendValues(values []int, t *tree) []int {
+	if t != nil {
+		values = appendValues(values, t.left)
+		values = append(values, t.value)
+		values = appendValues(values, t.right)
+	}
+	return values
+}
+
+func add(t *tree, value int) *tree {
+	if t == nil {
+		// Equivalent to return &tree{value: value}.
+		t = new(tree)
+		t.value = value
+		return t
+	}
+	if value < t.value {
+		t.left = add(t.left, value)
+	} else {
+		t.right = add(t.right, value)
+	}
+	return t
+}
+
+

结构体类型的零值是每个成员都是零值。通常会将零值作为最合理的默认值。例如,对于bytes.Buffer类型,结构体初始值就是一个随时可用的空缓存,还有在第9章将会讲到的sync.Mutex的零值也是有效的未锁定状态。有时候这种零值可用的特性是自然获得的,但是也有些类型需要一些额外的工作。

+

如果结构体没有任何成员的话就是空结构体,写作struct{}。它的大小为0,也不包含任何信息,但是有时候依然是有价值的。有些Go语言程序员用map来模拟set数据结构时,用它来代替map中布尔类型的value,只是强调key的重要性,但是因为节约的空间有限,而且语法比较复杂,所以我们通常会避免这样的用法。

+
seen := make(map[string]struct{}) // set of strings
+// ...
+if _, ok := seen[s]; !ok {
+	seen[s] = struct{}{}
+	// ...first time seeing s...
+}
+
+

4.4.1. 结构体字面值

+

结构体值也可以用结构体字面值表示,结构体字面值可以指定每个成员的值。

+
type Point struct{ X, Y int }
+
+p := Point{1, 2}
+
+

这里有两种形式的结构体字面值语法,上面的是第一种写法,要求以结构体成员定义的顺序为每个结构体成员指定一个字面值。它要求写代码和读代码的人要记住结构体的每个成员的类型和顺序,不过结构体成员有细微的调整就可能导致上述代码不能编译。因此,上述的语法一般只在定义结构体的包内部使用,或者是在较小的结构体中使用,这些结构体的成员排列比较规则,比如image.Point{x, y}或color.RGBA{red, green, blue, alpha}。

+

其实更常用的是第二种写法,以成员名字和相应的值来初始化,可以包含部分或全部的成员,如1.4节的Lissajous程序的写法:

+
anim := gif.GIF{LoopCount: nframes}
+
+

在这种形式的结构体字面值写法中,如果成员被忽略的话将默认用零值。因为提供了成员的名字,所以成员出现的顺序并不重要。

+

两种不同形式的写法不能混合使用。而且,你不能企图在外部包中用第一种顺序赋值的技巧来偷偷地初始化结构体中未导出的成员。

+
package p
+type T struct{ a, b int } // a and b are not exported
+
+package q
+import "p"
+var _ = p.T{a: 1, b: 2} // compile error: can't reference a, b
+var _ = p.T{1, 2}       // compile error: can't reference a, b
+
+

虽然上面最后一行代码的编译错误信息中并没有显式提到未导出的成员,但是这样企图隐式使用未导出成员的行为也是不允许的。

+

结构体可以作为函数的参数和返回值。例如,这个Scale函数将Point类型的值缩放后返回:

+
func Scale(p Point, factor int) Point {
+	return Point{p.X * factor, p.Y * factor}
+}
+
+fmt.Println(Scale(Point{1, 2}, 5)) // "{5 10}"
+
+

如果考虑效率的话,较大的结构体通常会用指针的方式传入和返回,

+
func Bonus(e *Employee, percent int) int {
+	return e.Salary * percent / 100
+}
+
+

如果要在函数内部修改结构体成员的话,用指针传入是必须的;因为在Go语言中,所有的函数参数都是值拷贝传入的,函数参数将不再是函数调用时的原始变量。

+
func AwardAnnualRaise(e *Employee) {
+	e.Salary = e.Salary * 105 / 100
+}
+
+

因为结构体通常通过指针处理,可以用下面的写法来创建并初始化一个结构体变量,并返回结构体的地址:

+
pp := &Point{1, 2}
+
+

它和下面的语句是等价的

+
pp := new(Point)
+*pp = Point{1, 2}
+
+

不过&Point{1, 2}写法可以直接在表达式中使用,比如一个函数调用。

+

4.4.2. 结构体比较

+

如果结构体的全部成员都是可以比较的,那么结构体也是可以比较的,那样的话两个结构体将可以使用==或!=运算符进行比较。相等比较运算符==将比较两个结构体的每个成员,因此下面两个比较的表达式是等价的:

+
type Point struct{ X, Y int }
+
+p := Point{1, 2}
+q := Point{2, 1}
+fmt.Println(p.X == q.X && p.Y == q.Y) // "false"
+fmt.Println(p == q)                   // "false"
+
+

可比较的结构体类型和其他可比较的类型一样,可以用于map的key类型。

+
type address struct {
+	hostname string
+	port     int
+}
+
+hits := make(map[address]int)
+hits[address{"golang.org", 443}]++
+
+

4.4.3. 结构体嵌入和匿名成员

+

在本节中,我们将看到如何使用Go语言提供的不同寻常的结构体嵌入机制让一个命名的结构体包含另一个结构体类型的匿名成员,这样就可以通过简单的点运算符x.f来访问匿名成员链中嵌套的x.d.e.f成员。

+

考虑一个二维的绘图程序,提供了一个各种图形的库,例如矩形、椭圆形、星形和轮形等几何形状。这里是其中两个的定义:

+
type Circle struct {
+	X, Y, Radius int
+}
+
+type Wheel struct {
+	X, Y, Radius, Spokes int
+}
+
+

一个Circle代表的圆形类型包含了标准圆心的X和Y坐标信息,和一个Radius表示的半径信息。一个Wheel轮形除了包含Circle类型所有的全部成员外,还增加了Spokes表示径向辐条的数量。我们可以这样创建一个wheel变量:

+
var w Wheel
+w.X = 8
+w.Y = 8
+w.Radius = 5
+w.Spokes = 20
+
+

随着库中几何形状数量的增多,我们一定会注意到它们之间的相似和重复之处,所以我们可能为了便于维护而将相同的属性独立出来:

+
type Point struct {
+	X, Y int
+}
+
+type Circle struct {
+	Center Point
+	Radius int
+}
+
+type Wheel struct {
+	Circle Circle
+	Spokes int
+}
+
+

这样改动之后结构体类型变的清晰了,但是这种修改同时也导致了访问每个成员变得繁琐:

+
var w Wheel
+w.Circle.Center.X = 8
+w.Circle.Center.Y = 8
+w.Circle.Radius = 5
+w.Spokes = 20
+
+

Go语言有一个特性让我们只声明一个成员对应的数据类型而不指名成员的名字;这类成员就叫匿名成员。匿名成员的数据类型必须是命名的类型或指向一个命名的类型的指针。下面的代码中,Circle和Wheel各自都有一个匿名成员。我们可以说Point类型被嵌入到了Circle结构体,同时Circle类型被嵌入到了Wheel结构体。

+
type Circle struct {
+	Point
+	Radius int
+}
+
+type Wheel struct {
+	Circle
+	Spokes int
+}
+
+

得益于匿名嵌入的特性,我们可以直接访问叶子属性而不需要给出完整的路径:

+
var w Wheel
+w.X = 8            // equivalent to w.Circle.Point.X = 8
+w.Y = 8            // equivalent to w.Circle.Point.Y = 8
+w.Radius = 5       // equivalent to w.Circle.Radius = 5
+w.Spokes = 20
+
+

在右边的注释中给出的显式形式访问这些叶子成员的语法依然有效,因此匿名成员并不是真的无法访问了。其中匿名成员Circle和Point都有自己的名字——就是命名的类型名字——但是这些名字在点操作符中是可选的。我们在访问子成员的时候可以忽略任何匿名成员部分。

+

不幸的是,结构体字面值并没有简短表示匿名成员的语法, 因此下面的语句都不能编译通过:

+
w = Wheel{8, 8, 5, 20}                       // compile error: unknown fields
+w = Wheel{X: 8, Y: 8, Radius: 5, Spokes: 20} // compile error: unknown fields
+
+

结构体字面值必须遵循形状类型声明时的结构,所以我们只能用下面的两种语法,它们彼此是等价的:

+

gopl.io/ch4/embed

+
w = Wheel{Circle{Point{8, 8}, 5}, 20}
+
+w = Wheel{
+	Circle: Circle{
+		Point:  Point{X: 8, Y: 8},
+		Radius: 5,
+	},
+	Spokes: 20, // NOTE: trailing comma necessary here (and at Radius)
+}
+
+fmt.Printf("%#v\n", w)
+// Output:
+// Wheel{Circle:Circle{Point:Point{X:8, Y:8}, Radius:5}, Spokes:20}
+
+w.X = 42
+
+fmt.Printf("%#v\n", w)
+// Output:
+// Wheel{Circle:Circle{Point:Point{X:42, Y:8}, Radius:5}, Spokes:20}
+
+

需要注意的是Printf函数中%v参数包含的#副词,它表示用和Go语言类似的语法打印值。对于结构体类型来说,将包含每个成员的名字。

+

因为匿名成员也有一个隐式的名字,因此不能同时包含两个类型相同的匿名成员,这会导致名字冲突。同时,因为成员的名字是由其类型隐式地决定的,所以匿名成员也有可见性的规则约束。在上面的例子中,Point和Circle匿名成员都是导出的。即使它们不导出(比如改成小写字母开头的point和circle),我们依然可以用简短形式访问匿名成员嵌套的成员

+
w.X = 8 // equivalent to w.circle.point.X = 8
+
+

但是在包外部,因为circle和point没有导出,不能访问它们的成员,因此简短的匿名成员访问语法也是禁止的。

+

到目前为止,我们看到匿名成员特性只是对访问嵌套成员的点运算符提供了简短的语法糖。稍后,我们将会看到匿名成员并不要求是结构体类型;其实任何命名的类型都可以作为结构体的匿名成员。但是为什么要嵌入一个没有任何子成员类型的匿名成员类型呢?

+

答案是匿名类型的方法集。简短的点运算符语法可以用于选择匿名成员嵌套的成员,也可以用于访问它们的方法。实际上,外层的结构体不仅仅是获得了匿名成员类型的所有成员,而且也获得了该类型导出的全部的方法。这个机制可以用于将一些有简单行为的对象组合成有复杂行为的对象。组合是Go语言中面向对象编程的核心,我们将在6.3节中专门讨论。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch4/ch4-05.html b/ch4/ch4-05.html new file mode 100644 index 0000000..ca15f65 --- /dev/null +++ b/ch4/ch4-05.html @@ -0,0 +1,448 @@ + + + + + + JSON - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

4.5. JSON

+

JavaScript对象表示法(JSON)是一种用于发送和接收结构化信息的标准协议。在类似的协议中,JSON并不是唯一的一个标准协议。 XML(§7.14)、ASN.1和Google的Protocol Buffers都是类似的协议,并且有各自的特色,但是由于简洁性、可读性和流行程度等原因,JSON是应用最广泛的一个。

+

Go语言对于这些标准格式的编码和解码都有良好的支持,由标准库中的encoding/json、encoding/xml、encoding/asn1等包提供支持(译注:Protocol Buffers的支持由 github.com/golang/protobuf 包提供),并且这类包都有着相似的API接口。本节,我们将对重要的encoding/json包的用法做个概述。

+

JSON是对JavaScript中各种类型的值——字符串、数字、布尔值和对象——Unicode本文编码。它可以用有效可读的方式表示第三章的基础数据类型和本章的数组、slice、结构体和map等聚合数据类型。

+

基本的JSON类型有数字(十进制或科学记数法)、布尔值(true或false)、字符串,其中字符串是以双引号包含的Unicode字符序列,支持和Go语言类似的反斜杠转义特性,不过JSON使用的是\Uhhhh转义数字来表示一个UTF-16编码(译注:UTF-16和UTF-8一样是一种变长的编码,有些Unicode码点较大的字符需要用4个字节表示;而且UTF-16还有大端和小端的问题),而不是Go语言的rune类型。

+

这些基础类型可以通过JSON的数组和对象类型进行递归组合。一个JSON数组是一个有序的值序列,写在一个方括号中并以逗号分隔;一个JSON数组可以用于编码Go语言的数组和slice。一个JSON对象是一个字符串到值的映射,写成一系列的name:value对形式,用花括号包含并以逗号分隔;JSON的对象类型可以用于编码Go语言的map类型(key类型是字符串)和结构体。例如:

+
boolean         true
+number          -273.15
+string          "She said \"Hello, BF\""
+array           ["gold", "silver", "bronze"]
+object          {"year": 1980,
+                 "event": "archery",
+                 "medals": ["gold", "silver", "bronze"]}
+
+

考虑一个应用程序,该程序负责收集各种电影评论并提供反馈功能。它的Movie数据类型和一个典型的表示电影的值列表如下所示。(在结构体声明中,Year和Color成员后面的字符串面值是结构体成员Tag;我们稍后会解释它的作用。)

+

gopl.io/ch4/movie

+
type Movie struct {
+	Title  string
+	Year   int  `json:"released"`
+	Color  bool `json:"color,omitempty"`
+	Actors []string
+}
+
+var movies = []Movie{
+	{Title: "Casablanca", Year: 1942, Color: false,
+		Actors: []string{"Humphrey Bogart", "Ingrid Bergman"}},
+	{Title: "Cool Hand Luke", Year: 1967, Color: true,
+		Actors: []string{"Paul Newman"}},
+	{Title: "Bullitt", Year: 1968, Color: true,
+		Actors: []string{"Steve McQueen", "Jacqueline Bisset"}},
+	// ...
+}
+
+

这样的数据结构特别适合JSON格式,并且在两者之间相互转换也很容易。将一个Go语言中类似movies的结构体slice转为JSON的过程叫编组(marshaling)。编组通过调用json.Marshal函数完成:

+
data, err := json.Marshal(movies)
+if err != nil {
+	log.Fatalf("JSON marshaling failed: %s", err)
+}
+fmt.Printf("%s\n", data)
+
+

Marshal函数返回一个编码后的字节slice,包含很长的字符串,并且没有空白缩进;我们将它折行以便于显示:

+
[{"Title":"Casablanca","released":1942,"Actors":["Humphrey Bogart","Ingr
+id Bergman"]},{"Title":"Cool Hand Luke","released":1967,"color":true,"Ac
+tors":["Paul Newman"]},{"Title":"Bullitt","released":1968,"color":true,"
+Actors":["Steve McQueen","Jacqueline Bisset"]}]
+
+

这种紧凑的表示形式虽然包含了全部的信息,但是很难阅读。为了生成便于阅读的格式,另一个json.MarshalIndent函数将产生整齐缩进的输出。该函数有两个额外的字符串参数用于表示每一行输出的前缀和每一个层级的缩进:

+
data, err := json.MarshalIndent(movies, "", "    ")
+if err != nil {
+	log.Fatalf("JSON marshaling failed: %s", err)
+}
+fmt.Printf("%s\n", data)
+
+

上面的代码将产生这样的输出(译注:在最后一个成员或元素后面并没有逗号分隔符):

+
[
+	{
+		"Title": "Casablanca",
+		"released": 1942,
+		"Actors": [
+			"Humphrey Bogart",
+			"Ingrid Bergman"
+		]
+	},
+	{
+		"Title": "Cool Hand Luke",
+		"released": 1967,
+		"color": true,
+		"Actors": [
+			"Paul Newman"
+		]
+	},
+	{
+		"Title": "Bullitt",
+		"released": 1968,
+		"color": true,
+		"Actors": [
+			"Steve McQueen",
+			"Jacqueline Bisset"
+		]
+	}
+]
+
+

在编码时,默认使用Go语言结构体的成员名字作为JSON的对象(通过reflect反射技术,我们将在12.6节讨论)。只有导出的结构体成员才会被编码,这也就是我们为什么选择用大写字母开头的成员名称。

+

细心的读者可能已经注意到,其中Year名字的成员在编码后变成了released,还有Color成员编码后变成了小写字母开头的color。这是因为结构体成员Tag所导致的。一个结构体成员Tag是和在编译阶段关联到该成员的元信息字符串:

+
Year  int  `json:"released"`
+Color bool `json:"color,omitempty"`
+
+

结构体的成员Tag可以是任意的字符串面值,但是通常是一系列用空格分隔的key:"value"键值对序列;因为值中含有双引号字符,因此成员Tag一般用原生字符串面值的形式书写。json开头键名对应的值用于控制encoding/json包的编码和解码的行为,并且encoding/...下面其它的包也遵循这个约定。成员Tag中json对应值的第一部分用于指定JSON对象的名字,比如将Go语言中的TotalCount成员对应到JSON中的total_count对象。Color成员的Tag还带了一个额外的omitempty选项,表示当Go语言结构体成员为空或零值时不生成该JSON对象(这里false为零值)。果然,Casablanca是一个黑白电影,并没有输出Color成员。

+

编码的逆操作是解码,对应将JSON数据解码为Go语言的数据结构,Go语言中一般叫unmarshaling,通过json.Unmarshal函数完成。下面的代码将JSON格式的电影数据解码为一个结构体slice,结构体中只有Title成员。通过定义合适的Go语言数据结构,我们可以选择性地解码JSON中感兴趣的成员。当Unmarshal函数调用返回,slice将被只含有Title信息的值填充,其它JSON成员将被忽略。

+
var titles []struct{ Title string }
+if err := json.Unmarshal(data, &titles); err != nil {
+	log.Fatalf("JSON unmarshaling failed: %s", err)
+}
+fmt.Println(titles) // "[{Casablanca} {Cool Hand Luke} {Bullitt}]"
+
+

许多web服务都提供JSON接口,通过HTTP接口发送JSON格式请求并返回JSON格式的信息。为了说明这一点,我们通过Github的issue查询服务来演示类似的用法。首先,我们要定义合适的类型和常量:

+

gopl.io/ch4/github

+
// Package github provides a Go API for the GitHub issue tracker.
+// See https://developer.github.com/v3/search/#search-issues.
+package github
+
+import "time"
+
+const IssuesURL = "https://api.github.com/search/issues"
+
+type IssuesSearchResult struct {
+	TotalCount int `json:"total_count"`
+	Items          []*Issue
+}
+
+type Issue struct {
+	Number    int
+	HTMLURL   string `json:"html_url"`
+	Title     string
+	State     string
+	User      *User
+	CreatedAt time.Time `json:"created_at"`
+	Body      string    // in Markdown format
+}
+
+type User struct {
+	Login   string
+	HTMLURL string `json:"html_url"`
+}
+
+

和前面一样,即使对应的JSON对象名是小写字母,每个结构体的成员名也是声明为大写字母开头的。因为有些JSON成员名字和Go结构体成员名字并不相同,因此需要Go语言结构体成员Tag来指定对应的JSON名字。同样,在解码的时候也需要做同样的处理,GitHub服务返回的信息比我们定义的要多很多。

+

SearchIssues函数发出一个HTTP请求,然后解码返回的JSON格式的结果。因为用户提供的查询条件可能包含类似?&之类的特殊字符,为了避免对URL造成冲突,我们用url.QueryEscape来对查询中的特殊字符进行转义操作。

+

gopl.io/ch4/github

+
package github
+
+import (
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"net/url"
+	"strings"
+)
+
+// SearchIssues queries the GitHub issue tracker.
+func SearchIssues(terms []string) (*IssuesSearchResult, error) {
+	q := url.QueryEscape(strings.Join(terms, " "))
+	resp, err := http.Get(IssuesURL + "?q=" + q)
+	if err != nil {
+		return nil, err
+	}
+
+	// We must close resp.Body on all execution paths.
+	// (Chapter 5 presents 'defer', which makes this simpler.)
+	if resp.StatusCode != http.StatusOK {
+		resp.Body.Close()
+		return nil, fmt.Errorf("search query failed: %s", resp.Status)
+	}
+
+	var result IssuesSearchResult
+	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
+		resp.Body.Close()
+		return nil, err
+	}
+	resp.Body.Close()
+	return &result, nil
+}
+
+

在早些的例子中,我们使用了json.Unmarshal函数来将JSON格式的字符串解码为字节slice。但是这个例子中,我们使用了基于流式的解码器json.Decoder,它可以从一个输入流解码JSON数据,尽管这不是必须的。如您所料,还有一个针对输出流的json.Encoder编码对象。

+

我们调用Decode方法来填充变量。这里有多种方法可以格式化结构。下面是最简单的一种,以一个固定宽度打印每个issue,但是在下一节我们将看到如何利用模板来输出复杂的格式。

+

gopl.io/ch4/issues

+
// Issues prints a table of GitHub issues matching the search terms.
+package main
+
+import (
+	"fmt"
+	"log"
+	"os"
+
+	"gopl.io/ch4/github"
+)
+
+func main() {
+	result, err := github.SearchIssues(os.Args[1:])
+	if err != nil {
+		log.Fatal(err)
+	}
+	fmt.Printf("%d issues:\n", result.TotalCount)
+	for _, item := range result.Items {
+		fmt.Printf("#%-5d %9.9s %.55s\n",
+			item.Number, item.User.Login, item.Title)
+	}
+}
+
+

通过命令行参数指定检索条件。下面的命令是查询Go语言项目中和JSON解码相关的问题,还有查询返回的结果:

+
$ go build gopl.io/ch4/issues
+$ ./issues repo:golang/go is:open json decoder
+13 issues:
+#5680    eaigner encoding/json: set key converter on en/decoder
+#6050  gopherbot encoding/json: provide tokenizer
+#8658  gopherbot encoding/json: use bufio
+#8462  kortschak encoding/json: UnmarshalText confuses json.Unmarshal
+#5901        rsc encoding/json: allow override type marshaling
+#9812  klauspost encoding/json: string tag not symmetric
+#7872  extempora encoding/json: Encoder internally buffers full output
+#9650    cespare encoding/json: Decoding gives errPhase when unmarshalin
+#6716  gopherbot encoding/json: include field name in unmarshal error me
+#6901  lukescott encoding/json, encoding/xml: option to treat unknown fi
+#6384    joeshaw encoding/json: encode precise floating point integers u
+#6647    btracey x/tools/cmd/godoc: display type kind of each named type
+#4237  gjemiller encoding/base64: URLEncoding padding is optional
+
+

GitHub的Web服务接口 https://developer.github.com/v3/ 包含了更多的特性。

+

练习 4.10: 修改issues程序,根据问题的时间进行分类,比如不到一个月的、不到一年的、超过一年。

+

练习 4.11: 编写一个工具,允许用户在命令行创建、读取、更新和关闭GitHub上的issue,当必要的时候自动打开用户默认的编辑器用于输入文本信息。

+

练习 4.12: 流行的web漫画服务xkcd也提供了JSON接口。例如,一个 https://xkcd.com/571/info.0.json 请求将返回一个很多人喜爱的571编号的详细描述。下载每个链接(只下载一次)然后创建一个离线索引。编写一个xkcd工具,使用这些离线索引,打印和命令行输入的检索词相匹配的漫画的URL。

+

练习 4.13: 使用开放电影数据库的JSON服务接口,允许你检索和下载 https://omdbapi.com/ 上电影的名字和对应的海报图像。编写一个poster工具,通过命令行输入的电影名字,下载对应的海报。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch4/ch4-06.html b/ch4/ch4-06.html new file mode 100644 index 0000000..1135f51 --- /dev/null +++ b/ch4/ch4-06.html @@ -0,0 +1,360 @@ + + + + + + 文本和HTML模板 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

4.6. 文本和HTML模板

+

前面的例子,只是最简单的格式化,使用Printf是完全足够的。但是有时候会需要复杂的打印格式,这时候一般需要将格式化代码分离出来以便更安全地修改。这些功能是由text/template和html/template等模板包提供的,它们提供了一个将变量值填充到一个文本或HTML格式的模板的机制。

+

一个模板是一个字符串或一个文件,里面包含了一个或多个由双花括号包含的{{action}}对象。大部分的字符串只是按字面值打印,但是对于actions部分将触发其它的行为。每个actions都包含了一个用模板语言书写的表达式,一个action虽然简短但是可以输出复杂的打印值,模板语言包含通过选择结构体的成员、调用函数或方法、表达式控制流if-else语句和range循环语句,还有其它实例化模板等诸多特性。下面是一个简单的模板字符串:

+

gopl.io/ch4/issuesreport

+
const templ = `{{.TotalCount}} issues:
+{{range .Items}}----------------------------------------
+Number: {{.Number}}
+User:   {{.User.Login}}
+Title:  {{.Title | printf "%.64s"}}
+Age:    {{.CreatedAt | daysAgo}} days
+{{end}}`
+
+

{% endraw %}

+

这个模板先打印匹配到的issue总数,然后打印每个issue的编号、创建用户、标题还有存在的时间。对于每一个action,都有一个当前值的概念,对应点操作符,写作“.”。当前值“.”最初被初始化为调用模板时的参数,在当前例子中对应github.IssuesSearchResult类型的变量。模板中{{.TotalCount}}对应action将展开为结构体中TotalCount成员以默认的方式打印的值。模板中{{range .Items}}{{end}}对应一个循环action,因此它们之间的内容可能会被展开多次,循环每次迭代的当前值对应当前的Items元素的值。

+

{% endraw %}

+

在一个action中,|操作符表示将前一个表达式的结果作为后一个函数的输入,类似于UNIX中管道的概念。在Title这一行的action中,第二个操作是一个printf函数,是一个基于fmt.Sprintf实现的内置函数,所有模板都可以直接使用。对于Age部分,第二个动作是一个叫daysAgo的函数,通过time.Since函数将CreatedAt成员转换为过去的时间长度:

+
func daysAgo(t time.Time) int {
+	return int(time.Since(t).Hours() / 24)
+}
+
+

需要注意的是CreatedAt的参数类型是time.Time,并不是字符串。以同样的方式,我们可以通过定义一些方法来控制字符串的格式化(§2.5),一个类型同样可以定制自己的JSON编码和解码行为。time.Time类型对应的JSON值是一个标准时间格式的字符串。

+

生成模板的输出需要两个处理步骤。第一步是要分析模板并转为内部表示,然后基于指定的输入执行模板。分析模板部分一般只需要执行一次。下面的代码创建并分析上面定义的模板templ。注意方法调用链的顺序:template.New先创建并返回一个模板;Funcs方法将daysAgo等自定义函数注册到模板中,并返回模板;最后调用Parse函数分析模板。

+
report, err := template.New("report").
+	Funcs(template.FuncMap{"daysAgo": daysAgo}).
+	Parse(templ)
+if err != nil {
+	log.Fatal(err)
+}
+
+

因为模板通常在编译时就测试好了,如果模板解析失败将是一个致命的错误。template.Must辅助函数可以简化这个致命错误的处理:它接受一个模板和一个error类型的参数,检测error是否为nil(如果不是nil则发出panic异常),然后返回传入的模板。我们将在5.9节再讨论这个话题。

+

一旦模板已经创建、注册了daysAgo函数、并通过分析和检测,我们就可以使用github.IssuesSearchResult作为输入源、os.Stdout作为输出源来执行模板:

+
var report = template.Must(template.New("issuelist").
+	Funcs(template.FuncMap{"daysAgo": daysAgo}).
+	Parse(templ))
+
+func main() {
+	result, err := github.SearchIssues(os.Args[1:])
+	if err != nil {
+		log.Fatal(err)
+	}
+	if err := report.Execute(os.Stdout, result); err != nil {
+		log.Fatal(err)
+	}
+}
+
+

程序输出一个纯文本报告:

+
$ go build gopl.io/ch4/issuesreport
+$ ./issuesreport repo:golang/go is:open json decoder
+13 issues:
+----------------------------------------
+Number: 5680
+User:      eaigner
+Title:     encoding/json: set key converter on en/decoder
+Age:       750 days
+----------------------------------------
+Number: 6050
+User:      gopherbot
+Title:     encoding/json: provide tokenizer
+Age:       695 days
+----------------------------------------
+...
+
+

现在让我们转到html/template模板包。它使用和text/template包相同的API和模板语言,但是增加了一个将字符串自动转义特性,这可以避免输入字符串和HTML、JavaScript、CSS或URL语法产生冲突的问题。这个特性还可以避免一些长期存在的安全问题,比如通过生成HTML注入攻击,通过构造一个含有恶意代码的问题标题,这些都可能让模板输出错误的输出,从而让他们控制页面。

+

下面的模板以HTML格式输出issue列表。注意import语句的不同:

+

gopl.io/ch4/issueshtml

+
import "html/template"
+
+var issueList = template.Must(template.New("issuelist").Parse(`
+<h1>{{.TotalCount}} issues</h1>
+<table>
+<tr style='text-align: left'>
+  <th>#</th>
+  <th>State</th>
+  <th>User</th>
+  <th>Title</th>
+</tr>
+{{range .Items}}
+<tr>
+  <td><a href='{{.HTMLURL}}'>{{.Number}}</a></td>
+  <td>{{.State}}</td>
+  <td><a href='{{.User.HTMLURL}}'>{{.User.Login}}</a></td>
+  <td><a href='{{.HTMLURL}}'>{{.Title}}</a></td>
+</tr>
+{{end}}
+</table>
+`))
+
+

{% endraw %}

+

下面的命令将在新的模板上执行一个稍微不同的查询:

+
$ go build gopl.io/ch4/issueshtml
+$ ./issueshtml repo:golang/go commenter:gopherbot json encoder >issues.html
+
+

图4.4显示了在web浏览器中的效果图。每个issue包含到Github对应页面的链接。

+

+

图4.4中issue没有包含会对HTML格式产生冲突的特殊字符,但是我们马上将看到标题中含有&<字符的issue。下面的命令选择了两个这样的issue:

+
$ ./issueshtml repo:golang/go 3133 10535 >issues2.html
+
+

图4.5显示了该查询的结果。注意,html/template包已经自动将特殊字符转义,因此我们依然可以看到正确的字面值。如果我们使用text/template包的话,这2个issue将会产生错误,其中“&lt;”四个字符将会被当作小于字符“<”处理,同时“<link>”字符串将会被当作一个链接元素处理,它们都会导致HTML文档结构的改变,从而导致有未知的风险。

+

我们也可以通过对信任的HTML字符串使用template.HTML类型来抑制这种自动转义的行为。还有很多采用类型命名的字符串类型分别对应信任的JavaScript、CSS和URL。下面的程序演示了两个使用不同类型的相同字符串产生的不同结果:A是一个普通字符串,B是一个信任的template.HTML字符串类型。

+

+

gopl.io/ch4/autoescape

+
func main() {
+	const templ = `<p>A: {{.A}}</p><p>B: {{.B}}</p>`
+	t := template.Must(template.New("escape").Parse(templ))
+	var data struct {
+		A string        // untrusted plain text
+		B template.HTML // trusted HTML
+	}
+	data.A = "<b>Hello!</b>"
+	data.B = "<b>Hello!</b>"
+	if err := t.Execute(os.Stdout, data); err != nil {
+		log.Fatal(err)
+	}
+}
+
+

{% endraw %}

+

图4.6显示了出现在浏览器中的模板输出。我们看到A的黑体标记被转义失效了,但是B没有。

+

+

我们这里只讲述了模板系统中最基本的特性。一如既往,如果想了解更多的信息,请自己查看包文档:

+
$ go doc text/template
+$ go doc html/template
+
+

练习 4.14: 创建一个web服务器,查询一次GitHub,然后生成BUG报告、里程碑和对应的用户信息。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch4/ch4.html b/ch4/ch4.html new file mode 100644 index 0000000..7c00cf7 --- /dev/null +++ b/ch4/ch4.html @@ -0,0 +1,240 @@ + + + + + + 复合数据类型 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

第4章 复合数据类型

+

在第三章我们讨论了基本数据类型,它们可以用于构建程序中数据的结构,是Go语言世界的原子。在本章,我们将讨论复合数据类型,它是以不同的方式组合基本类型而构造出来的复合数据类型。我们主要讨论四种类型——数组、slice、map和结构体——同时在本章的最后,我们将演示如何使用结构体来解码和编码到对应JSON格式的数据,并且通过结合使用模板来生成HTML页面。

+

数组和结构体是聚合类型;它们的值由许多元素或成员字段的值组成。数组是由同构的元素组成——每个数组元素都是完全相同的类型——结构体则是由异构的元素组成的。数组和结构体都是有固定内存大小的数据结构。相比之下,slice和map则是动态的数据结构,它们将根据需要动态增长。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch5/ch5-01.html b/ch5/ch5-01.html new file mode 100644 index 0000000..db8a1e6 --- /dev/null +++ b/ch5/ch5-01.html @@ -0,0 +1,276 @@ + + + + + + 函数声明 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

5.1. 函数声明

+

函数声明包括函数名、形式参数列表、返回值列表(可省略)以及函数体。

+
func name(parameter-list) (result-list) {
+	body
+}
+
+

形式参数列表描述了函数的参数名以及参数类型。这些参数作为局部变量,其值由参数调用者提供。返回值列表描述了函数返回值的变量名以及类型。如果函数返回一个无名变量或者没有返回值,返回值列表的括号是可以省略的。如果一个函数声明不包括返回值列表,那么函数体执行完毕后,不会返回任何值。在hypot函数中:

+
func hypot(x, y float64) float64 {
+	return math.Sqrt(x*x + y*y)
+}
+fmt.Println(hypot(3,4)) // "5"
+
+

x和y是形参名,3和4是调用时的传入的实参,函数返回了一个float64类型的值。 +返回值也可以像形式参数一样被命名。在这种情况下,每个返回值被声明成一个局部变量,并根据该返回值的类型,将其初始化为该类型的零值。 +如果一个函数在声明时,包含返回值列表,该函数必须以 return语句结尾,除非函数明显无法运行到结尾处。例如函数在结尾时调用了panic异常或函数中存在无限循环。

+

正如hypot一样,如果一组形参或返回值有相同的类型,我们不必为每个形参都写出参数类型。下面2个声明是等价的:

+
func f(i, j, k int, s, t string)                 { /* ... */ }
+func f(i int, j int, k int,  s string, t string) { /* ... */ }
+
+

下面,我们给出4种方法声明拥有2个int型参数和1个int型返回值的函数.blank identifier(译者注:即下文的_符号)可以强调某个参数未被使用。

+
func add(x int, y int) int   {return x + y}
+func sub(x, y int) (z int)   { z = x - y; return}
+func first(x int, _ int) int { return x }
+func zero(int, int) int      { return 0 }
+
+fmt.Printf("%T\n", add)   // "func(int, int) int"
+fmt.Printf("%T\n", sub)   // "func(int, int) int"
+fmt.Printf("%T\n", first) // "func(int, int) int"
+fmt.Printf("%T\n", zero)  // "func(int, int) int"
+
+

函数的类型被称为函数的签名。如果两个函数形式参数列表和返回值列表中的变量类型一一对应,那么这两个函数被认为有相同的类型或签名。形参和返回值的变量名不影响函数签名,也不影响它们是否可以以省略参数类型的形式表示。

+

每一次函数调用都必须按照声明顺序为所有参数提供实参(参数值)。在函数调用时,Go语言没有默认参数值,也没有任何方法可以通过参数名指定形参,因此形参和返回值的变量名对于函数调用者而言没有意义。

+

在函数体中,函数的形参作为局部变量,被初始化为调用者提供的值。函数的形参和有名返回值作为函数最外层的局部变量,被存储在相同的词法块中。

+

实参通过值的方式传递,因此函数的形参是实参的拷贝。对形参进行修改不会影响实参。但是,如果实参包括引用类型,如指针,slice(切片)、map、function、channel等类型,实参可能会由于函数的间接引用被修改。

+

你可能会偶尔遇到没有函数体的函数声明,这表示该函数不是以Go实现的。这样的声明定义了函数签名。

+
package math
+
+func Sin(x float64) float //implemented in assembly language
+
+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch5/ch5-02.html b/ch5/ch5-02.html new file mode 100644 index 0000000..8117c1a --- /dev/null +++ b/ch5/ch5-02.html @@ -0,0 +1,370 @@ + + + + + + 递归 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

5.2. 递归

+

函数可以是递归的,这意味着函数可以直接或间接的调用自身。对许多问题而言,递归是一种强有力的技术,例如处理递归的数据结构。在4.4节,我们通过遍历二叉树来实现简单的插入排序,在本章节,我们再次使用它来处理HTML文件。

+

下文的示例代码使用了非标准包 golang.org/x/net/html ,解析HTML。golang.org/x/... 目录下存储了一些由Go团队设计、维护,对网络编程、国际化文件处理、移动平台、图像处理、加密解密、开发者工具提供支持的扩展包。未将这些扩展包加入到标准库原因有二,一是部分包仍在开发中,二是对大多数Go语言的开发者而言,扩展包提供的功能很少被使用。

+

例子中调用golang.org/x/net/html的部分api如下所示。html.Parse函数读入一组bytes解析后,返回html.Node类型的HTML页面树状结构根节点。HTML拥有很多类型的结点如text(文本)、commnets(注释)类型,在下面的例子中,我们 只关注< name key='value' >形式的结点。

+

golang.org/x/net/html

+
package html
+
+type Node struct {
+	Type                    NodeType
+	Data                    string
+	Attr                    []Attribute
+	FirstChild, NextSibling *Node
+}
+
+type NodeType int32
+
+const (
+	ErrorNode NodeType = iota
+	TextNode
+	DocumentNode
+	ElementNode
+	CommentNode
+	DoctypeNode
+)
+
+type Attribute struct {
+	Key, Val string
+}
+
+func Parse(r io.Reader) (*Node, error)
+
+

main函数解析HTML标准输入,通过递归函数visit获得links(链接),并打印出这些links:

+

gopl.io/ch5/findlinks1

+
// Findlinks1 prints the links in an HTML document read from standard input.
+package main
+
+import (
+	"fmt"
+	"os"
+
+	"golang.org/x/net/html"
+)
+
+func main() {
+	doc, err := html.Parse(os.Stdin)
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "findlinks1: %v\n", err)
+		os.Exit(1)
+	}
+	for _, link := range visit(nil, doc) {
+		fmt.Println(link)
+	}
+}
+
+

visit函数遍历HTML的节点树,从每一个anchor元素的href属性获得link,将这些links存入字符串数组中,并返回这个字符串数组。

+
// visit appends to links each link found in n and returns the result.
+func visit(links []string, n *html.Node) []string {
+	if n.Type == html.ElementNode && n.Data == "a" {
+		for _, a := range n.Attr {
+			if a.Key == "href" {
+				links = append(links, a.Val)
+			}
+		}
+	}
+	for c := n.FirstChild; c != nil; c = c.NextSibling {
+		links = visit(links, c)
+	}
+	return links
+}
+
+

为了遍历结点n的所有后代结点,每次遇到n的孩子结点时,visit递归的调用自身。这些孩子结点存放在FirstChild链表中。

+

让我们以Go的主页(golang.org)作为目标,运行findlinks。我们以fetch(1.5章)的输出作为findlinks的输入。下面的输出做了简化处理。

+
$ go build gopl.io/ch1/fetch
+$ go build gopl.io/ch5/findlinks1
+$ ./fetch https://golang.org | ./findlinks1
+#
+/doc/
+/pkg/
+/help/
+/blog/
+http://play.golang.org/
+//tour.golang.org/
+https://golang.org/dl/
+//blog.golang.org/
+/LICENSE
+/doc/tos.html
+http://www.google.com/intl/en/policies/privacy/
+
+

注意在页面中出现的链接格式,在之后我们会介绍如何将这些链接,根据根路径( https://golang.org )生成可以直接访问的url。

+

在函数outline中,我们通过递归的方式遍历整个HTML结点树,并输出树的结构。在outline内部,每遇到一个HTML元素标签,就将其入栈,并输出。

+

gopl.io/ch5/outline

+
func main() {
+	doc, err := html.Parse(os.Stdin)
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "outline: %v\n", err)
+		os.Exit(1)
+	}
+	outline(nil, doc)
+}
+func outline(stack []string, n *html.Node) {
+	if n.Type == html.ElementNode {
+		stack = append(stack, n.Data) // push tag
+		fmt.Println(stack)
+	}
+	for c := n.FirstChild; c != nil; c = c.NextSibling {
+		outline(stack, c)
+	}
+}
+
+

有一点值得注意:outline有入栈操作,但没有相对应的出栈操作。当outline调用自身时,被调用者接收的是stack的拷贝。被调用者对stack的元素追加操作,修改的是stack的拷贝,其可能会修改slice底层的数组甚至是申请一块新的内存空间进行扩容;但这个过程并不会修改调用方的stack。因此当函数返回时,调用方的stack与其调用自身之前完全一致。

+

下面是 https://golang.org 页面的简要结构:

+
$ go build gopl.io/ch5/outline
+$ ./fetch https://golang.org | ./outline
+[html]
+[html head]
+[html head meta]
+[html head title]
+[html head link]
+[html body]
+[html body div]
+[html body div]
+[html body div div]
+[html body div div form]
+[html body div div form div]
+[html body div div form div a]
+...
+
+

正如你在上面实验中所见,大部分HTML页面只需几层递归就能被处理,但仍然有些页面需要深层次的递归。

+

大部分编程语言使用固定大小的函数调用栈,常见的大小从64KB到2MB不等。固定大小栈会限制递归的深度,当你用递归处理大量数据时,需要避免栈溢出;除此之外,还会导致安全性问题。与此相反,Go语言使用可变栈,栈的大小按需增加(初始时很小)。这使得我们使用递归时不必考虑溢出和安全问题。

+

练习 5.1: 修改findlinks代码中遍历n.FirstChild链表的部分,将循环调用visit,改成递归调用。

+

练习 5.2: 编写函数,记录在HTML树中出现的同名元素的次数。

+

练习 5.3: 编写函数输出所有text结点的内容。注意不要访问<script><style>元素,因为这些元素对浏览者是不可见的。

+

练习 5.4: 扩展visit函数,使其能够处理其他类型的结点,如images、scripts和style sheets。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch5/ch5-03.html b/ch5/ch5-03.html new file mode 100644 index 0000000..a0672ac --- /dev/null +++ b/ch5/ch5-03.html @@ -0,0 +1,323 @@ + + + + + + 多返回值 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

5.3. 多返回值

+

在Go中,一个函数可以返回多个值。我们已经在之前例子中看到,许多标准库中的函数返回2个值,一个是期望得到的返回值,另一个是函数出错时的错误信息。下面的例子会展示如何编写多返回值的函数。

+

下面的程序是findlinks的改进版本。修改后的findlinks可以自己发起HTTP请求,这样我们就不必再运行fetch。因为HTTP请求和解析操作可能会失败,因此findlinks声明了2个返回值:链接列表和错误信息。一般而言,HTML的解析器可以处理HTML页面的错误结点,构造出HTML页面结构,所以解析HTML很少失败。这意味着如果findlinks函数失败了,很可能是由于I/O的错误导致的。

+

gopl.io/ch5/findlinks2

+
func main() {
+	for _, url := range os.Args[1:] {
+		links, err := findLinks(url)
+		if err != nil {
+			fmt.Fprintf(os.Stderr, "findlinks2: %v\n", err)
+			continue
+		}
+		for _, link := range links {
+			fmt.Println(link)
+		}
+	}
+}
+
+// findLinks performs an HTTP GET request for url, parses the
+// response as HTML, and extracts and returns the links.
+func findLinks(url string) ([]string, error) {
+	resp, err := http.Get(url)
+	if err != nil {
+		return nil, err
+	}
+	if resp.StatusCode != http.StatusOK {
+		resp.Body.Close()
+		return nil, fmt.Errorf("getting %s: %s", url, resp.Status)
+	}
+	doc, err := html.Parse(resp.Body)
+	resp.Body.Close()
+	if err != nil {
+		return nil, fmt.Errorf("parsing %s as HTML: %v", url, err)
+	}
+	return visit(nil, doc), nil
+}
+
+

在findlinks中,有4处return语句,每一处return都返回了一组值。前三处return,将http和html包中的错误信息传递给findlinks的调用者。第一处return直接返回错误信息,其他两处通过fmt.Errorf(§7.8)输出详细的错误信息。如果findlinks成功结束,最后的return语句将一组解析获得的连接返回给用户。

+

在findlinks中,我们必须确保resp.Body被关闭,释放网络资源。虽然Go的垃圾回收机制会回收不被使用的内存,但是这不包括操作系统层面的资源,比如打开的文件、网络连接。因此我们必须显式的释放这些资源。

+

调用多返回值函数时,返回给调用者的是一组值,调用者必须显式的将这些值分配给变量:

+
links, err := findLinks(url)
+
+

如果某个值不被使用,可以将其分配给blank identifier:

+
links, _ := findLinks(url) // errors ignored
+
+

一个函数内部可以将另一个有多返回值的函数调用作为返回值,下面的例子展示了与findLinks有相同功能的函数,两者的区别在于下面的例子先输出参数:

+
func findLinksLog(url string) ([]string, error) {
+	log.Printf("findLinks %s", url)
+	return findLinks(url)
+}
+
+

当你调用接受多参数的函数时,可以将一个返回多参数的函数调用作为该函数的参数。虽然这很少出现在实际生产代码中,但这个特性在debug时很方便,我们只需要一条语句就可以输出所有的返回值。下面的代码是等价的:

+
log.Println(findLinks(url))
+links, err := findLinks(url)
+log.Println(links, err)
+
+

准确的变量名可以传达函数返回值的含义。尤其在返回值的类型都相同时,就像下面这样:

+
func Size(rect image.Rectangle) (width, height int)
+func Split(path string) (dir, file string)
+func HourMinSec(t time.Time) (hour, minute, second int)
+
+

虽然良好的命名很重要,但你也不必为每一个返回值都取一个适当的名字。比如,按照惯例,函数的最后一个bool类型的返回值表示函数是否运行成功,error类型的返回值代表函数的错误信息,对于这些类似的惯例,我们不必思考合适的命名,它们都无需解释。

+

如果一个函数所有的返回值都有显式的变量名,那么该函数的return语句可以省略操作数。这称之为bare return。

+
// CountWordsAndImages does an HTTP GET request for the HTML
+// document url and returns the number of words and images in it.
+func CountWordsAndImages(url string) (words, images int, err error) {
+	resp, err := http.Get(url)
+	if err != nil {
+		return
+	}
+	doc, err := html.Parse(resp.Body)
+	resp.Body.Close()
+	if err != nil {
+		err = fmt.Errorf("parsing HTML: %s", err)
+		return
+	}
+	words, images = countWordsAndImages(doc)
+	return
+}
+func countWordsAndImages(n *html.Node) (words, images int) { /* ... */ }
+
+

按照返回值列表的次序,返回所有的返回值,在上面的例子中,每一个return语句等价于:

+
return words, images, err
+
+

当一个函数有多处return语句以及许多返回值时,bare return 可以减少代码的重复,但是使得代码难以被理解。举个例子,如果你没有仔细的审查代码,很难发现前2处return等价于 return 0,0,err(Go会将返回值 words和images在函数体的开始处,根据它们的类型,将其初始化为0),最后一处return等价于 return words, image, nil。基于以上原因,不宜过度使用bare return。

+

练习 5.5: 实现countWordsAndImages。(参考练习4.9如何分词)

+

练习 5.6: 修改gopl.io/ch3/surface(§3.2)中的corner函数,将返回值命名,并使用bare return。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch5/ch5-04.html b/ch5/ch5-04.html new file mode 100644 index 0000000..b164a0c --- /dev/null +++ b/ch5/ch5-04.html @@ -0,0 +1,362 @@ + + + + + + 错误 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

5.4. 错误

+

在Go中有一部分函数总是能成功的运行。比如strings.Contains和strconv.FormatBool函数,对各种可能的输入都做了良好的处理,使得运行时几乎不会失败,除非遇到灾难性的、不可预料的情况,比如运行时的内存溢出。导致这种错误的原因很复杂,难以处理,从错误中恢复的可能性也很低。

+

还有一部分函数只要输入的参数满足一定条件,也能保证运行成功。比如time.Date函数,该函数将年月日等参数构造成time.Time对象,除非最后一个参数(时区)是nil。这种情况下会引发panic异常。panic是来自被调用函数的信号,表示发生了某个已知的bug。一个良好的程序永远不应该发生panic异常。

+

对于大部分函数而言,永远无法确保能否成功运行。这是因为错误的原因超出了程序员的控制。举个例子,任何进行I/O操作的函数都会面临出现错误的可能,只有没有经验的程序员才会相信读写操作不会失败,即使是简单的读写。因此,当本该可信的操作出乎意料的失败后,我们必须弄清楚导致失败的原因。

+

在Go的错误处理中,错误是软件包API和应用程序用户界面的一个重要组成部分,程序运行失败仅被认为是几个预期的结果之一。

+

对于那些将运行失败看作是预期结果的函数,它们会返回一个额外的返回值,通常是最后一个,来传递错误信息。如果导致失败的原因只有一个,额外的返回值可以是一个布尔值,通常被命名为ok。比如,cache.Lookup失败的唯一原因是key不存在,那么代码可以按照下面的方式组织:

+
value, ok := cache.Lookup(key)
+if !ok {
+	// ...cache[key] does not exist…
+}
+
+

通常,导致失败的原因不止一种,尤其是对I/O操作而言,用户需要了解更多的错误信息。因此,额外的返回值不再是简单的布尔类型,而是error类型。

+

内置的error是接口类型。我们将在第七章了解接口类型的含义,以及它对错误处理的影响。现在我们只需要明白error类型可能是nil或者non-nil。nil意味着函数运行成功,non-nil表示失败。对于non-nil的error类型,我们可以通过调用error的Error函数或者输出函数获得字符串类型的错误信息。

+
fmt.Println(err)
+fmt.Printf("%v", err)
+
+

通常,当函数返回non-nil的error时,其他的返回值是未定义的(undefined),这些未定义的返回值应该被忽略。然而,有少部分函数在发生错误时,仍然会返回一些有用的返回值。比如,当读取文件发生错误时,Read函数会返回可以读取的字节数以及错误信息。对于这种情况,正确的处理方式应该是先处理这些不完整的数据,再处理错误。因此对函数的返回值要有清晰的说明,以便于其他人使用。

+

在Go中,函数运行失败时会返回错误信息,这些错误信息被认为是一种预期的值而非异常(exception),这使得Go有别于那些将函数运行失败看作是异常的语言。虽然Go有各种异常机制,但这些机制仅被使用在处理那些未被预料到的错误,即bug,而不是那些在健壮程序中应该被避免的程序错误。对于Go的异常机制我们将在5.9介绍。

+

Go这样设计的原因是由于对于某个应该在控制流程中处理的错误而言,将这个错误以异常的形式抛出会混乱对错误的描述,这通常会导致一些糟糕的后果。当某个程序错误被当作异常处理后,这个错误会将堆栈跟踪信息返回给终端用户,这些信息复杂且无用,无法帮助定位错误。

+

正因此,Go使用控制流机制(如if和return)处理错误,这使得编码人员能更多的关注错误处理。

+

5.4.1. 错误处理策略

+

当一次函数调用返回错误时,调用者应该选择合适的方式处理错误。根据情况的不同,有很多处理方式,让我们来看看常用的五种方式。

+

首先,也是最常用的方式是传播错误。这意味着函数中某个子程序的失败,会变成该函数的失败。下面,我们以5.3节的findLinks函数作为例子。如果findLinks对http.Get的调用失败,findLinks会直接将这个HTTP错误返回给调用者:

+
resp, err := http.Get(url)
+if err != nil{
+	return nil, err
+}
+
+

当对html.Parse的调用失败时,findLinks不会直接返回html.Parse的错误,因为缺少两条重要信息:1、发生错误时的解析器(html parser);2、发生错误的url。因此,findLinks构造了一个新的错误信息,既包含了这两项,也包括了底层的解析出错的信息。

+
doc, err := html.Parse(resp.Body)
+resp.Body.Close()
+if err != nil {
+	return nil, fmt.Errorf("parsing %s as HTML: %v", url,err)
+}
+
+

fmt.Errorf函数使用fmt.Sprintf格式化错误信息并返回。我们使用该函数添加额外的前缀上下文信息到原始错误信息。当错误最终由main函数处理时,错误信息应提供清晰的从原因到后果的因果链,就像美国宇航局事故调查时做的那样:

+
genesis: crashed: no parachute: G-switch failed: bad relay orientation
+
+

由于错误信息经常是以链式组合在一起的,所以错误信息中应避免大写和换行符。最终的错误信息可能很长,我们可以通过类似grep的工具处理错误信息(译者注:grep是一种文本搜索工具)。

+

编写错误信息时,我们要确保错误信息对问题细节的描述是详尽的。尤其是要注意错误信息表达的一致性,即相同的函数或同包内的同一组函数返回的错误在构成和处理方式上是相似的。

+

以os包为例,os包确保文件操作(如os.Open、Read、Write、Close)返回的每个错误的描述不仅仅包含错误的原因(如无权限,文件目录不存在)也包含文件名,这样调用者在构造新的错误信息时无需再添加这些信息。

+

一般而言,被调用函数f(x)会将调用信息和参数信息作为发生错误时的上下文放在错误信息中并返回给调用者,调用者需要添加一些错误信息中不包含的信息,比如添加url到html.Parse返回的错误中。

+

让我们来看看处理错误的第二种策略。如果错误的发生是偶然性的,或由不可预知的问题导致的。一个明智的选择是重新尝试失败的操作。在重试时,我们需要限制重试的时间间隔或重试的次数,防止无限制的重试。

+

gopl.io/ch5/wait

+
// WaitForServer attempts to contact the server of a URL.
+// It tries for one minute using exponential back-off.
+// It reports an error if all attempts fail.
+func WaitForServer(url string) error {
+	const timeout = 1 * time.Minute
+	deadline := time.Now().Add(timeout)
+	for tries := 0; time.Now().Before(deadline); tries++ {
+		_, err := http.Head(url)
+		if err == nil {
+			return nil // success
+		}
+		log.Printf("server not responding (%s);retrying…", err)
+		time.Sleep(time.Second << uint(tries)) // exponential back-off
+	}
+	return fmt.Errorf("server %s failed to respond after %s", url, timeout)
+}
+
+

如果错误发生后,程序无法继续运行,我们就可以采用第三种策略:输出错误信息并结束程序。需要注意的是,这种策略只应在main中执行。对库函数而言,应仅向上传播错误,除非该错误意味着程序内部包含不一致性,即遇到了bug,才能在库函数中结束程序。

+
// (In function main.)
+if err := WaitForServer(url); err != nil {
+	fmt.Fprintf(os.Stderr, "Site is down: %v\n", err)
+	os.Exit(1)
+}
+
+

调用log.Fatalf可以更简洁的代码达到与上文相同的效果。log中的所有函数,都默认会在错误信息之前输出时间信息。

+
if err := WaitForServer(url); err != nil {
+	log.Fatalf("Site is down: %v\n", err)
+}
+
+

长时间运行的服务器常采用默认的时间格式,而交互式工具很少采用包含如此多信息的格式。

+
2006/01/02 15:04:05 Site is down: no such domain:
+bad.gopl.io
+
+

我们可以设置log的前缀信息屏蔽时间信息,一般而言,前缀信息会被设置成命令名。

+
log.SetPrefix("wait: ")
+log.SetFlags(0)
+
+

第四种策略:有时,我们只需要输出错误信息就足够了,不需要中断程序的运行。我们可以通过log包提供函数

+
if err := Ping(); err != nil {
+	log.Printf("ping failed: %v; networking disabled",err)
+}
+
+

或者标准错误流输出错误信息。

+
if err := Ping(); err != nil {
+	fmt.Fprintf(os.Stderr, "ping failed: %v; networking disabled\n", err)
+}
+
+

log包中的所有函数会为没有换行符的字符串增加换行符。

+

第五种,也是最后一种策略:我们可以直接忽略掉错误。

+
dir, err := ioutil.TempDir("", "scratch")
+if err != nil {
+	return fmt.Errorf("failed to create temp dir: %v",err)
+}
+// ...use temp dir…
+os.RemoveAll(dir) // ignore errors; $TMPDIR is cleaned periodically
+
+

尽管os.RemoveAll会失败,但上面的例子并没有做错误处理。这是因为操作系统会定期的清理临时目录。正因如此,虽然程序没有处理错误,但程序的逻辑不会因此受到影响。我们应该在每次函数调用后,都养成考虑错误处理的习惯,当你决定忽略某个错误时,你应该清晰地写下你的意图。

+

在Go中,错误处理有一套独特的编码风格。检查某个子函数是否失败后,我们通常将处理失败的逻辑代码放在处理成功的代码之前。如果某个错误会导致函数返回,那么成功时的逻辑代码不应放在else语句块中,而应直接放在函数体中。Go中大部分函数的代码结构几乎相同,首先是一系列的初始检查,防止错误发生,之后是函数的实际逻辑。

+

5.4.2. 文件结尾错误(EOF)

+

函数经常会返回多种错误,这对终端用户来说可能会很有趣,但对程序而言,这使得情况变得复杂。很多时候,程序必须根据错误类型,作出不同的响应。让我们考虑这样一个例子:从文件中读取n个字节。如果n等于文件的长度,读取过程的任何错误都表示失败。如果n小于文件的长度,调用者会重复的读取固定大小的数据直到文件结束。这会导致调用者必须分别处理由文件结束引起的各种错误。基于这样的原因,io包保证任何由文件结束引起的读取失败都返回同一个错误——io.EOF,该错误在io包中定义:

+
package io
+
+import "errors"
+
+// EOF is the error returned by Read when no more input is available.
+var EOF = errors.New("EOF")
+
+

调用者只需通过简单的比较,就可以检测出这个错误。下面的例子展示了如何从标准输入中读取字符,以及判断文件结束。(4.3的chartcount程序展示了更加复杂的代码)

+
in := bufio.NewReader(os.Stdin)
+for {
+	r, _, err := in.ReadRune()
+	if err == io.EOF {
+		break // finished reading
+	}
+	if err != nil {
+		return fmt.Errorf("read failed:%v", err)
+	}
+	// ...use r…
+}
+
+

因为文件结束这种错误不需要更多的描述,所以io.EOF有固定的错误信息——“EOF”。对于其他错误,我们可能需要在错误信息中描述错误的类型和数量,这使得我们不能像io.EOF一样采用固定的错误信息。在7.11节中,我们会提出更系统的方法区分某些固定的错误值。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch5/ch5-05.html b/ch5/ch5-05.html new file mode 100644 index 0000000..857c225 --- /dev/null +++ b/ch5/ch5-05.html @@ -0,0 +1,336 @@ + + + + + + 函数值 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

5.5. 函数值

+

在Go中,函数被看作第一类值(first-class values):函数像其他值一样,拥有类型,可以被赋值给其他变量,传递给函数,从函数返回。对函数值(function value)的调用类似函数调用。例子如下:

+
	func square(n int) int { return n * n }
+	func negative(n int) int { return -n }
+	func product(m, n int) int { return m * n }
+
+	f := square
+	fmt.Println(f(3)) // "9"
+
+	f = negative
+	fmt.Println(f(3))     // "-3"
+	fmt.Printf("%T\n", f) // "func(int) int"
+
+	f = product // compile error: can't assign func(int, int) int to func(int) int
+
+

函数类型的零值是nil。调用值为nil的函数值会引起panic错误:

+
	var f func(int) int
+	f(3) // 此处f的值为nil, 会引起panic错误
+
+

函数值可以与nil比较:

+
	var f func(int) int
+	if f != nil {
+		f(3)
+	}
+
+

但是函数值之间是不可比较的,也不能用函数值作为map的key。

+

函数值使得我们不仅仅可以通过数据来参数化函数,亦可通过行为。标准库中包含许多这样的例子。下面的代码展示了如何使用这个技巧。strings.Map对字符串中的每个字符调用add1函数,并将每个add1函数的返回值组成一个新的字符串返回给调用者。

+
	func add1(r rune) rune { return r + 1 }
+
+	fmt.Println(strings.Map(add1, "HAL-9000")) // "IBM.:111"
+	fmt.Println(strings.Map(add1, "VMS"))      // "WNT"
+	fmt.Println(strings.Map(add1, "Admix"))    // "Benjy"
+
+

5.2节的findLinks函数使用了辅助函数visit,遍历和操作了HTML页面的所有结点。使用函数值,我们可以将遍历结点的逻辑和操作结点的逻辑分离,使得我们可以复用遍历的逻辑,从而对结点进行不同的操作。

+

gopl.io/ch5/outline2

+
// forEachNode针对每个结点x,都会调用pre(x)和post(x)。
+// pre和post都是可选的。
+// 遍历孩子结点之前,pre被调用
+// 遍历孩子结点之后,post被调用
+func forEachNode(n *html.Node, pre, post func(n *html.Node)) {
+	if pre != nil {
+		pre(n)
+	}
+	for c := n.FirstChild; c != nil; c = c.NextSibling {
+		forEachNode(c, pre, post)
+	}
+	if post != nil {
+		post(n)
+	}
+}
+
+

该函数接收2个函数作为参数,分别在结点的孩子被访问前和访问后调用。这样的设计给调用者更大的灵活性。举个例子,现在我们有startElement和endElement两个函数用于输出HTML元素的开始标签和结束标签<b>...</b>

+
var depth int
+func startElement(n *html.Node) {
+	if n.Type == html.ElementNode {
+		fmt.Printf("%*s<%s>\n", depth*2, "", n.Data)
+		depth++
+	}
+}
+func endElement(n *html.Node) {
+	if n.Type == html.ElementNode {
+		depth--
+		fmt.Printf("%*s</%s>\n", depth*2, "", n.Data)
+	}
+}
+
+

上面的代码利用fmt.Printf的一个小技巧控制输出的缩进。%*s中的*会在字符串之前填充一些空格。在例子中,每次输出会先填充depth*2数量的空格,再输出"",最后再输出HTML标签。

+

如果我们像下面这样调用forEachNode:

+
forEachNode(doc, startElement, endElement)
+
+

与之前的outline程序相比,我们得到了更加详细的页面结构:

+
$ go build gopl.io/ch5/outline2
+$ ./outline2 http://gopl.io
+<html>
+  <head>
+    <meta>
+    </meta>
+    <title>
+	</title>
+	<style>
+	</style>
+  </head>
+  <body>
+    <table>
+      <tbody>
+        <tr>
+          <td>
+            <a>
+              <img>
+              </img>
+...
+
+

练习 5.7: 完善startElement和endElement函数,使其成为通用的HTML输出器。要求:输出注释结点,文本结点以及每个元素的属性(< a href='...'>)。使用简略格式输出没有孩子结点的元素(即用<img/>代替<img></img>)。编写测试,验证程序输出的格式正确。(详见11章)

+

练习 5.8: 修改pre和post函数,使其返回布尔类型的返回值。返回false时,中止forEachNoded的遍历。使用修改后的代码编写ElementByID函数,根据用户输入的id查找第一个拥有该id元素的HTML元素,查找成功后,停止遍历。

+
func ElementByID(doc *html.Node, id string) *html.Node
+
+

练习 5.9: 编写函数expand,将s中的"foo"替换为f("foo")的返回值。

+
func expand(s string, f func(string) string) string
+
+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch5/ch5-06.html b/ch5/ch5-06.html new file mode 100644 index 0000000..9344713 --- /dev/null +++ b/ch5/ch5-06.html @@ -0,0 +1,479 @@ + + + + + + 匿名函数 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

5.6. 匿名函数

+

拥有函数名的函数只能在包级语法块中被声明,通过函数字面量(function literal),我们可绕过这一限制,在任何表达式中表示一个函数值。函数字面量的语法和函数声明相似,区别在于func关键字后没有函数名。函数值字面量是一种表达式,它的值被称为匿名函数(anonymous function)。

+

函数字面量允许我们在使用函数时,再定义它。通过这种技巧,我们可以改写之前对strings.Map的调用:

+
strings.Map(func(r rune) rune { return r + 1 }, "HAL-9000")
+
+

更为重要的是,通过这种方式定义的函数可以访问完整的词法环境(lexical environment),这意味着在函数中定义的内部函数可以引用该函数的变量,如下例所示:

+

gopl.io/ch5/squares

+
// squares返回一个匿名函数。
+// 该匿名函数每次被调用时都会返回下一个数的平方。
+func squares() func() int {
+	var x int
+	return func() int {
+		x++
+		return x * x
+	}
+}
+func main() {
+	f := squares()
+	fmt.Println(f()) // "1"
+	fmt.Println(f()) // "4"
+	fmt.Println(f()) // "9"
+	fmt.Println(f()) // "16"
+}
+
+

函数squares返回另一个类型为 func() int 的函数。对squares的一次调用会生成一个局部变量x并返回一个匿名函数。每次调用匿名函数时,该函数都会先使x的值加1,再返回x的平方。第二次调用squares时,会生成第二个x变量,并返回一个新的匿名函数。新匿名函数操作的是第二个x变量。

+

squares的例子证明,函数值不仅仅是一串代码,还记录了状态。在squares中定义的匿名内部函数可以访问和更新squares中的局部变量,这意味着匿名函数和squares中,存在变量引用。这就是函数值属于引用类型和函数值不可比较的原因。Go使用闭包(closures)技术实现函数值,Go程序员也把函数值叫做闭包。

+

通过这个例子,我们看到变量的生命周期不由它的作用域决定:squares返回后,变量x仍然隐式的存在于f中。

+

接下来,我们讨论一个有点学术性的例子,考虑这样一个问题:给定一些计算机课程,每个课程都有前置课程,只有完成了前置课程才可以开始当前课程的学习;我们的目标是选择出一组课程,这组课程必须确保按顺序学习时,能全部被完成。每个课程的前置课程如下:

+

gopl.io/ch5/toposort

+
// prereqs记录了每个课程的前置课程
+var prereqs = map[string][]string{
+	"algorithms": {"data structures"},
+	"calculus": {"linear algebra"},
+	"compilers": {
+		"data structures",
+		"formal languages",
+		"computer organization",
+	},
+	"data structures":       {"discrete math"},
+	"databases":             {"data structures"},
+	"discrete math":         {"intro to programming"},
+	"formal languages":      {"discrete math"},
+	"networks":              {"operating systems"},
+	"operating systems":     {"data structures", "computer organization"},
+	"programming languages": {"data structures", "computer organization"},
+}
+
+

这类问题被称作拓扑排序。从概念上说,前置条件可以构成有向图。图中的顶点表示课程,边表示课程间的依赖关系。显然,图中应该无环,这也就是说从某点出发的边,最终不会回到该点。下面的代码用深度优先搜索了整张图,获得了符合要求的课程序列。

+
func main() {
+	for i, course := range topoSort(prereqs) {
+		fmt.Printf("%d:\t%s\n", i+1, course)
+	}
+}
+
+func topoSort(m map[string][]string) []string {
+	var order []string
+	seen := make(map[string]bool)
+	var visitAll func(items []string)
+	visitAll = func(items []string) {
+		for _, item := range items {
+			if !seen[item] {
+				seen[item] = true
+				visitAll(m[item])
+				order = append(order, item)
+			}
+		}
+	}
+	var keys []string
+	for key := range m {
+		keys = append(keys, key)
+	}
+	sort.Strings(keys)
+	visitAll(keys)
+	return order
+}
+
+

当匿名函数需要被递归调用时,我们必须首先声明一个变量(在上面的例子中,我们首先声明了 visitAll),再将匿名函数赋值给这个变量。如果不分成两步,函数字面量无法与visitAll绑定,我们也无法递归调用该匿名函数。

+
visitAll := func(items []string) {
+	// ...
+	visitAll(m[item]) // compile error: undefined: visitAll
+	// ...
+}
+
+

在toposort程序的输出如下所示,它的输出顺序是大多人想看到的固定顺序输出,但是这需要我们多花点心思才能做到。哈希表prepreqs的value是遍历顺序固定的切片,而不再试遍历顺序随机的map,所以我们对prereqs的key值进行排序,保证每次运行toposort程序,都以相同的遍历顺序遍历prereqs。

+
1: intro to programming
+2: discrete math
+3: data structures
+4: algorithms
+5: linear algebra
+6: calculus
+7: formal languages
+8: computer organization
+9: compilers
+10: databases
+11: operating systems
+12: networks
+13: programming languages
+
+

让我们回到findLinks这个例子。我们将代码移动到了links包下,将函数重命名为Extract,在第八章我们会再次用到这个函数。新的匿名函数被引入,用于替换原来的visit函数。该匿名函数负责将新连接添加到切片中。在Extract中,使用forEachNode遍历HTML页面,由于Extract只需要在遍历结点前操作结点,所以forEachNode的post参数被传入nil。

+

gopl.io/ch5/links

+
// Package links provides a link-extraction function.
+package links
+import (
+	"fmt"
+	"net/http"
+	"golang.org/x/net/html"
+)
+// Extract makes an HTTP GET request to the specified URL, parses
+// the response as HTML, and returns the links in the HTML document.
+func Extract(url string) ([]string, error) {
+	resp, err := http.Get(url)
+	if err != nil {
+		return nil, err
+	}
+	if resp.StatusCode != http.StatusOK {
+	resp.Body.Close()
+		return nil, fmt.Errorf("getting %s: %s", url, resp.Status)
+	}
+	doc, err := html.Parse(resp.Body)
+	resp.Body.Close()
+	if err != nil {
+		return nil, fmt.Errorf("parsing %s as HTML: %v", url, err)
+	}
+	var links []string
+	visitNode := func(n *html.Node) {
+		if n.Type == html.ElementNode && n.Data == "a" {
+			for _, a := range n.Attr {
+				if a.Key != "href" {
+					continue
+				}
+				link, err := resp.Request.URL.Parse(a.Val)
+				if err != nil {
+					continue // ignore bad URLs
+				}
+				links = append(links, link.String())
+			}
+		}
+	}
+	forEachNode(doc, visitNode, nil)
+	return links, nil
+}
+
+

上面的代码对之前的版本做了改进,现在links中存储的不是href属性的原始值,而是通过resp.Request.URL解析后的值。解析后,这些连接以绝对路径的形式存在,可以直接被http.Get访问。

+

网页抓取的核心问题就是如何遍历图。在topoSort的例子中,已经展示了深度优先遍历,在网页抓取中,我们会展示如何用广度优先遍历图。在第8章,我们会介绍如何将深度优先和广度优先结合使用。

+

下面的函数实现了广度优先算法。调用者需要输入一个初始的待访问列表和一个函数f。待访问列表中的每个元素被定义为string类型。广度优先算法会为每个元素调用一次f。每次f执行完毕后,会返回一组待访问元素。这些元素会被加入到待访问列表中。当待访问列表中的所有元素都被访问后,breadthFirst函数运行结束。为了避免同一个元素被访问两次,代码中维护了一个map。

+

gopl.io/ch5/findlinks3

+
// breadthFirst calls f for each item in the worklist.
+// Any items returned by f are added to the worklist.
+// f is called at most once for each item.
+func breadthFirst(f func(item string) []string, worklist []string) {
+	seen := make(map[string]bool)
+	for len(worklist) > 0 {
+		items := worklist
+		worklist = nil
+		for _, item := range items {
+			if !seen[item] {
+				seen[item] = true
+				worklist = append(worklist, f(item)...)
+			}
+		}
+	}
+}
+
+

就像我们在章节3解释的那样,append的参数“f(item)...”,会将f返回的一组元素一个个添加到worklist中。

+

在我们网页抓取器中,元素的类型是url。crawl函数会将URL输出,提取其中的新链接,并将这些新链接返回。我们会将crawl作为参数传递给breadthFirst。

+
func crawl(url string) []string {
+	fmt.Println(url)
+	list, err := links.Extract(url)
+	if err != nil {
+		log.Print(err)
+	}
+	return list
+}
+
+

为了使抓取器开始运行,我们用命令行输入的参数作为初始的待访问url。

+
func main() {
+	// Crawl the web breadth-first,
+	// starting from the command-line arguments.
+	breadthFirst(crawl, os.Args[1:])
+}
+
+

让我们从 https://golang.org 开始,下面是程序的输出结果:

+
$ go build gopl.io/ch5/findlinks3
+$ ./findlinks3 https://golang.org
+https://golang.org/
+https://golang.org/doc/
+https://golang.org/pkg/
+https://golang.org/project/
+https://code.google.com/p/go-tour/
+https://golang.org/doc/code.html
+https://www.youtube.com/watch?v=XCsL89YtqCs
+http://research.swtch.com/gotour
+
+

当所有发现的链接都已经被访问或电脑的内存耗尽时,程序运行结束。

+

练习5.10: 重写topoSort函数,用map代替切片并移除对key的排序代码。验证结果的正确性(结果不唯一)。

+

练习5.11: 现在线性代数的老师把微积分设为了前置课程。完善topSort,使其能检测有向图中的环。

+

练习5.12: gopl.io/ch5/outline2(5.5节)的startElement和endElement共用了全局变量depth,将它们修改为匿名函数,使其共享outline中的局部变量。

+

练习5.13: 修改crawl,使其能保存发现的页面,必要时,可以创建目录来保存这些页面。只保存来自原始域名下的页面。假设初始页面在golang.org下,就不要保存vimeo.com下的页面。

+

练习5.14: 使用breadthFirst遍历其他数据结构。比如,topoSort例子中的课程依赖关系(有向图)、个人计算机的文件层次结构(树);你所在城市的公交或地铁线路(无向图)。

+

5.6.1. 警告:捕获迭代变量

+

本节,将介绍Go词法作用域的一个陷阱。请务必仔细的阅读,弄清楚发生问题的原因。即使是经验丰富的程序员也会在这个问题上犯错误。

+

考虑这样一个问题:你被要求首先创建一些目录,再将目录删除。在下面的例子中我们用函数值来完成删除操作。下面的示例代码需要引入os包。为了使代码简单,我们忽略了所有的异常处理。

+
var rmdirs []func()
+for _, d := range tempDirs() {
+	dir := d // NOTE: necessary!
+	os.MkdirAll(dir, 0755) // creates parent directories too
+	rmdirs = append(rmdirs, func() {
+		os.RemoveAll(dir)
+	})
+}
+// ...do some work…
+for _, rmdir := range rmdirs {
+	rmdir() // clean up
+}
+
+

你可能会感到困惑,为什么要在循环体中用循环变量d赋值一个新的局部变量,而不是像下面的代码一样直接使用循环变量dir。需要注意,下面的代码是错误的。

+
var rmdirs []func()
+for _, dir := range tempDirs() {
+	os.MkdirAll(dir, 0755)
+	rmdirs = append(rmdirs, func() {
+		os.RemoveAll(dir) // NOTE: incorrect!
+	})
+}
+
+

问题的原因在于循环变量的作用域。在上面的程序中,for循环语句引入了新的词法块,循环变量dir在这个词法块中被声明。在该循环中生成的所有函数值都共享相同的循环变量。需要注意,函数值中记录的是循环变量的内存地址,而不是循环变量某一时刻的值。以dir为例,后续的迭代会不断更新dir的值,当删除操作执行时,for循环已完成,dir中存储的值等于最后一次迭代的值。这意味着,每次对os.RemoveAll的调用删除的都是相同的目录。

+

通常,为了解决这个问题,我们会引入一个与循环变量同名的局部变量,作为循环变量的副本。比如下面的变量dir,虽然这看起来很奇怪,但却很有用。

+
for _, dir := range tempDirs() {
+	dir := dir // declares inner dir, initialized to outer dir
+	// ...
+}
+
+

这个问题不仅存在基于range的循环,在下面的例子中,对循环变量i的使用也存在同样的问题:

+
var rmdirs []func()
+dirs := tempDirs()
+for i := 0; i < len(dirs); i++ {
+	os.MkdirAll(dirs[i], 0755) // OK
+	rmdirs = append(rmdirs, func() {
+		os.RemoveAll(dirs[i]) // NOTE: incorrect!
+	})
+}
+
+

如果你使用go语句(第八章)或者defer语句(5.8节)会经常遇到此类问题。这不是go或defer本身导致的,而是因为它们都会等待循环结束后,再执行函数值。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch5/ch5-07.html b/ch5/ch5-07.html new file mode 100644 index 0000000..24a8229 --- /dev/null +++ b/ch5/ch5-07.html @@ -0,0 +1,281 @@ + + + + + + 可变参数 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

5.7. 可变参数

+

参数数量可变的函数称为可变参数函数。典型的例子就是fmt.Printf和类似函数。Printf首先接收一个必备的参数,之后接收任意个数的后续参数。

+

在声明可变参数函数时,需要在参数列表的最后一个参数类型之前加上省略符号“...”,这表示该函数会接收任意数量的该类型参数。

+

gopl.io/ch5/sum

+
func sum(vals ...int) int {
+	total := 0
+	for _, val := range vals {
+		total += val
+	}
+	return total
+}
+
+

sum函数返回任意个int型参数的和。在函数体中,vals被看作是类型为[] int的切片。sum可以接收任意数量的int型参数:

+
fmt.Println(sum())           // "0"
+fmt.Println(sum(3))          // "3"
+fmt.Println(sum(1, 2, 3, 4)) // "10"
+
+

在上面的代码中,调用者隐式的创建一个数组,并将原始参数复制到数组中,再把数组的一个切片作为参数传给被调用函数。如果原始参数已经是切片类型,我们该如何传递给sum?只需在最后一个参数后加上省略符。下面的代码功能与上个例子中最后一条语句相同。

+
values := []int{1, 2, 3, 4}
+fmt.Println(sum(values...)) // "10"
+
+

虽然在可变参数函数内部,...int 型参数的行为看起来很像切片类型,但实际上,可变参数函数和以切片作为参数的函数是不同的。

+
func f(...int) {}
+func g([]int) {}
+fmt.Printf("%T\n", f) // "func(...int)"
+fmt.Printf("%T\n", g) // "func([]int)"
+
+

可变参数函数经常被用于格式化字符串。下面的errorf函数构造了一个以行号开头的,经过格式化的错误信息。函数名的后缀f是一种通用的命名规范,代表该可变参数函数可以接收Printf风格的格式化字符串。

+
func errorf(linenum int, format string, args ...interface{}) {
+	fmt.Fprintf(os.Stderr, "Line %d: ", linenum)
+	fmt.Fprintf(os.Stderr, format, args...)
+	fmt.Fprintln(os.Stderr)
+}
+linenum, name := 12, "count"
+errorf(linenum, "undefined: %s", name) // "Line 12: undefined: count"
+
+

interface{}表示函数的最后一个参数可以接收任意类型,我们会在第7章详细介绍。

+

练习5.15: 编写类似sum的可变参数函数max和min。考虑不传参时,max和min该如何处理,再编写至少接收1个参数的版本。

+

**练习5.16:**编写多参数版本的strings.Join。

+

**练习5.17:**编写多参数版本的ElementsByTagName,函数接收一个HTML结点树以及任意数量的标签名,返回与这些标签名匹配的所有元素。下面给出了2个例子:

+
func ElementsByTagName(doc *html.Node, name...string) []*html.Node
+images := ElementsByTagName(doc, "img")
+headings := ElementsByTagName(doc, "h1", "h2", "h3", "h4")
+
+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch5/ch5-08.html b/ch5/ch5-08.html new file mode 100644 index 0000000..0217c67 --- /dev/null +++ b/ch5/ch5-08.html @@ -0,0 +1,415 @@ + + + + + + Deferred函数 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

5.8. Deferred函数

+

在findLinks的例子中,我们用http.Get的输出作为html.Parse的输入。只有url的内容的确是HTML格式的,html.Parse才可以正常工作,但实际上,url指向的内容很丰富,可能是图片,纯文本或是其他。将这些格式的内容传递给html.parse,会产生不良后果。

+

下面的例子获取HTML页面并输出页面的标题。title函数会检查服务器返回的Content-Type字段,如果发现页面不是HTML,将终止函数运行,返回错误。

+

gopl.io/ch5/title1

+
func title(url string) error {
+	resp, err := http.Get(url)
+	if err != nil {
+		return err
+	}
+	// Check Content-Type is HTML (e.g., "text/html;charset=utf-8").
+	ct := resp.Header.Get("Content-Type")
+	if ct != "text/html" && !strings.HasPrefix(ct,"text/html;") {
+		resp.Body.Close()
+		return fmt.Errorf("%s has type %s, not text/html",url, ct)
+	}
+	doc, err := html.Parse(resp.Body)
+	resp.Body.Close()
+	if err != nil {
+		return fmt.Errorf("parsing %s as HTML: %v", url,err)
+	}
+	visitNode := func(n *html.Node) {
+		if n.Type == html.ElementNode && n.Data == "title"&&n.FirstChild != nil {
+			fmt.Println(n.FirstChild.Data)
+		}
+	}
+	forEachNode(doc, visitNode, nil)
+	return nil
+}
+
+

下面展示了运行效果:

+
$ go build gopl.io/ch5/title1
+$ ./title1 http://gopl.io
+The Go Programming Language
+$ ./title1 https://golang.org/doc/effective_go.html
+Effective Go - The Go Programming Language
+$ ./title1 https://golang.org/doc/gopher/frontpage.png
+title1: https://golang.org/doc/gopher/frontpage.png has type image/png, not text/html
+
+

resp.Body.close调用了多次,这是为了确保title在所有执行路径下(即使函数运行失败)都关闭了网络连接。随着函数变得复杂,需要处理的错误也变多,维护清理逻辑变得越来越困难。而Go语言独有的defer机制可以让事情变得简单。

+

你只需要在调用普通函数或方法前加上关键字defer,就完成了defer所需要的语法。当执行到该条语句时,函数和参数表达式得到计算,但直到包含该defer语句的函数执行完毕时,defer后的函数才会被执行,不论包含defer语句的函数是通过return正常结束,还是由于panic导致的异常结束。你可以在一个函数中执行多条defer语句,它们的执行顺序与声明顺序相反。

+

defer语句经常被用于处理成对的操作,如打开、关闭、连接、断开连接、加锁、释放锁。通过defer机制,不论函数逻辑多复杂,都能保证在任何执行路径下,资源被释放。释放资源的defer应该直接跟在请求资源的语句后。在下面的代码中,一条defer语句替代了之前的所有resp.Body.Close

+

gopl.io/ch5/title2

+
func title(url string) error {
+	resp, err := http.Get(url)
+	if err != nil {
+		return err
+	}
+	defer resp.Body.Close()
+	ct := resp.Header.Get("Content-Type")
+	if ct != "text/html" && !strings.HasPrefix(ct,"text/html;") {
+		return fmt.Errorf("%s has type %s, not text/html",url, ct)
+	}
+	doc, err := html.Parse(resp.Body)
+	if err != nil {
+		return fmt.Errorf("parsing %s as HTML: %v", url,err)
+	}
+	// ...print doc's title element…
+	return nil
+}
+
+

在处理其他资源时,也可以采用defer机制,比如对文件的操作:

+

io/ioutil

+
package ioutil
+func ReadFile(filename string) ([]byte, error) {
+	f, err := os.Open(filename)
+	if err != nil {
+		return nil, err
+	}
+	defer f.Close()
+	return ReadAll(f)
+}
+
+

或是处理互斥锁(9.2章)

+
var mu sync.Mutex
+var m = make(map[string]int)
+func lookup(key string) int {
+	mu.Lock()
+	defer mu.Unlock()
+	return m[key]
+}
+
+

调试复杂程序时,defer机制也常被用于记录何时进入和退出函数。下例中的bigSlowOperation函数,直接调用trace记录函数的被调情况。bigSlowOperation被调时,trace会返回一个函数值,该函数值会在bigSlowOperation退出时被调用。通过这种方式, 我们可以只通过一条语句控制函数的入口和所有的出口,甚至可以记录函数的运行时间,如例子中的start。需要注意一点:不要忘记defer语句后的圆括号,否则本该在进入时执行的操作会在退出时执行,而本该在退出时执行的,永远不会被执行。

+

gopl.io/ch5/trace

+
func bigSlowOperation() {
+	defer trace("bigSlowOperation")() // don't forget the extra parentheses
+	// ...lots of work…
+	time.Sleep(10 * time.Second) // simulate slow operation by sleeping
+}
+func trace(msg string) func() {
+	start := time.Now()
+	log.Printf("enter %s", msg)
+	return func() { 
+		log.Printf("exit %s (%s)", msg,time.Since(start)) 
+	}
+}
+
+

每一次bigSlowOperation被调用,程序都会记录函数的进入,退出,持续时间。(我们用time.Sleep模拟一个耗时的操作)

+
$ go build gopl.io/ch5/trace
+$ ./trace
+2015/11/18 09:53:26 enter bigSlowOperation
+2015/11/18 09:53:36 exit bigSlowOperation (10.000589217s)
+
+

我们知道,defer语句中的函数会在return语句更新返回值变量后再执行,又因为在函数中定义的匿名函数可以访问该函数包括返回值变量在内的所有变量,所以,对匿名函数采用defer机制,可以使其观察函数的返回值。

+

以double函数为例:

+
func double(x int) int {
+	return x + x
+}
+
+

我们只需要首先命名double的返回值,再增加defer语句,我们就可以在double每次被调用时,输出参数以及返回值。

+
func double(x int) (result int) {
+	defer func() { fmt.Printf("double(%d) = %d\n", x,result) }()
+	return x + x
+}
+_ = double(4)
+// Output:
+// "double(4) = 8"
+
+

可能double函数过于简单,看不出这个小技巧的作用,但对于有许多return语句的函数而言,这个技巧很有用。

+

被延迟执行的匿名函数甚至可以修改函数返回给调用者的返回值:

+
func triple(x int) (result int) {
+	defer func() { result += x }()
+	return double(x)
+}
+fmt.Println(triple(4)) // "12"
+
+

在循环体中的defer语句需要特别注意,因为只有在函数执行完毕后,这些被延迟的函数才会执行。下面的代码会导致系统的文件描述符耗尽,因为在所有文件都被处理之前,没有文件会被关闭。

+
for _, filename := range filenames {
+	f, err := os.Open(filename)
+	if err != nil {
+		return err
+	}
+	defer f.Close() // NOTE: risky; could run out of file descriptors
+	// ...process f…
+}
+
+

一种解决方法是将循环体中的defer语句移至另外一个函数。在每次循环时,调用这个函数。

+
for _, filename := range filenames {
+	if err := doFile(filename); err != nil {
+		return err
+	}
+}
+func doFile(filename string) error {
+	f, err := os.Open(filename)
+	if err != nil {
+		return err
+	}
+	defer f.Close()
+	// ...process f…
+}
+
+

下面的代码是fetch(1.5节)的改进版,我们将http响应信息写入本地文件而不是从标准输出流输出。我们通过path.Base提出url路径的最后一段作为文件名。

+

gopl.io/ch5/fetch

+
// Fetch downloads the URL and returns the
+// name and length of the local file.
+func fetch(url string) (filename string, n int64, err error) {
+	resp, err := http.Get(url)
+	if err != nil {
+		return "", 0, err
+	}
+	defer resp.Body.Close()
+	local := path.Base(resp.Request.URL.Path)
+	if local == "/" {
+		local = "index.html"
+	}
+	f, err := os.Create(local)
+	if err != nil {
+		return "", 0, err
+	}
+	n, err = io.Copy(f, resp.Body)
+	// Close file, but prefer error from Copy, if any.
+	if closeErr := f.Close(); err == nil {
+		err = closeErr
+	}
+	return local, n, err
+}
+
+

对resp.Body.Close延迟调用我们已经见过了,在此不做解释。上例中,通过os.Create打开文件进行写入,在关闭文件时,我们没有对f.close采用defer机制,因为这会产生一些微妙的错误。许多文件系统,尤其是NFS,写入文件时发生的错误会被延迟到文件关闭时反馈。如果没有检查文件关闭时的反馈信息,可能会导致数据丢失,而我们还误以为写入操作成功。如果io.Copy和f.close都失败了,我们倾向于将io.Copy的错误信息反馈给调用者,因为它先于f.close发生,更有可能接近问题的本质。

+

**练习5.18:**不修改fetch的行为,重写fetch函数,要求使用defer机制关闭文件。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch5/ch5-09.html b/ch5/ch5-09.html new file mode 100644 index 0000000..7d98922 --- /dev/null +++ b/ch5/ch5-09.html @@ -0,0 +1,334 @@ + + + + + + Panic异常 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

5.9. Panic异常

+

Go的类型系统会在编译时捕获很多错误,但有些错误只能在运行时检查,如数组访问越界、空指针引用等。这些运行时错误会引起panic异常。

+

一般而言,当panic异常发生时,程序会中断运行,并立即执行在该goroutine(可以先理解成线程,在第8章会详细介绍)中被延迟的函数(defer 机制)。随后,程序崩溃并输出日志信息。日志信息包括panic value和函数调用的堆栈跟踪信息。panic value通常是某种错误信息。对于每个goroutine,日志信息中都会有与之相对的,发生panic时的函数调用堆栈跟踪信息。通常,我们不需要再次运行程序去定位问题,日志信息已经提供了足够的诊断依据。因此,在我们填写问题报告时,一般会将panic异常和日志信息一并记录。

+

不是所有的panic异常都来自运行时,直接调用内置的panic函数也会引发panic异常;panic函数接受任何值作为参数。当某些不应该发生的场景发生时,我们就应该调用panic。比如,当程序到达了某条逻辑上不可能到达的路径:

+
switch s := suit(drawCard()); s {
+case "Spades":                                // ...
+case "Hearts":                                // ...
+case "Diamonds":                              // ...
+case "Clubs":                                 // ...
+default:
+	panic(fmt.Sprintf("invalid suit %q", s)) // Joker?
+}
+
+

断言函数必须满足的前置条件是明智的做法,但这很容易被滥用。除非你能提供更多的错误信息,或者能更快速的发现错误,否则不需要使用断言,编译器在运行时会帮你检查代码。

+
func Reset(x *Buffer) {
+	if x == nil {
+		panic("x is nil") // unnecessary!
+	}
+	x.elements = nil
+}
+
+

虽然Go的panic机制类似于其他语言的异常,但panic的适用场景有一些不同。由于panic会引起程序的崩溃,因此panic一般用于严重错误,如程序内部的逻辑不一致。勤奋的程序员认为任何崩溃都表明代码中存在漏洞,所以对于大部分漏洞,我们应该使用Go提供的错误机制,而不是panic,尽量避免程序的崩溃。在健壮的程序中,任何可以预料到的错误,如不正确的输入、错误的配置或是失败的I/O操作都应该被优雅的处理,最好的处理方式,就是使用Go的错误机制。

+

考虑regexp.Compile函数,该函数将正则表达式编译成有效的可匹配格式。当输入的正则表达式不合法时,该函数会返回一个错误。当调用者明确的知道正确的输入不会引起函数错误时,要求调用者检查这个错误是不必要和累赘的。我们应该假设函数的输入一直合法,就如前面的断言一样:当调用者输入了不应该出现的输入时,触发panic异常。

+

在程序源码中,大多数正则表达式是字符串字面值(string literals),因此regexp包提供了包装函数regexp.MustCompile检查输入的合法性。

+
package regexp
+func Compile(expr string) (*Regexp, error) { /* ... */ }
+func MustCompile(expr string) *Regexp {
+	re, err := Compile(expr)
+	if err != nil {
+		panic(err)
+	}
+	return re
+}
+
+

包装函数使得调用者可以便捷的用一个编译后的正则表达式为包级别的变量赋值:

+
var httpSchemeRE = regexp.MustCompile(`^https?:`) //"http:" or "https:"
+
+

显然,MustCompile不能接收不合法的输入。函数名中的Must前缀是一种针对此类函数的命名约定,比如template.Must(4.6节)

+
func main() {
+	f(3)
+}
+func f(x int) {
+	fmt.Printf("f(%d)\n", x+0/x) // panics if x == 0
+	defer fmt.Printf("defer %d\n", x)
+	f(x - 1)
+}
+
+

上例中的运行输出如下:

+
f(3)
+f(2)
+f(1)
+defer 1
+defer 2
+defer 3
+
+

当f(0)被调用时,发生panic异常,之前被延迟执行的3个fmt.Printf被调用。程序中断执行后,panic信息和堆栈信息会被输出(下面是简化的输出):

+
panic: runtime error: integer divide by zero
+main.f(0)
+src/gopl.io/ch5/defer1/defer.go:14
+main.f(1)
+src/gopl.io/ch5/defer1/defer.go:16
+main.f(2)
+src/gopl.io/ch5/defer1/defer.go:16
+main.f(3)
+src/gopl.io/ch5/defer1/defer.go:16
+main.main()
+src/gopl.io/ch5/defer1/defer.go:10
+
+

我们在下一节将看到,如何使程序从panic异常中恢复,阻止程序的崩溃。

+

为了方便诊断问题,runtime包允许程序员输出堆栈信息。在下面的例子中,我们通过在main函数中延迟调用printStack输出堆栈信息。

+

gopl.io/ch5/defer2

+
func main() {
+	defer printStack()
+	f(3)
+}
+func printStack() {
+	var buf [4096]byte
+	n := runtime.Stack(buf[:], false)
+	os.Stdout.Write(buf[:n])
+}
+
+

printStack的简化输出如下(下面只是printStack的输出,不包括panic的日志信息):

+
goroutine 1 [running]:
+main.printStack()
+src/gopl.io/ch5/defer2/defer.go:20
+main.f(0)
+src/gopl.io/ch5/defer2/defer.go:27
+main.f(1)
+src/gopl.io/ch5/defer2/defer.go:29
+main.f(2)
+src/gopl.io/ch5/defer2/defer.go:29
+main.f(3)
+src/gopl.io/ch5/defer2/defer.go:29
+main.main()
+src/gopl.io/ch5/defer2/defer.go:15
+
+

将panic机制类比其他语言异常机制的读者可能会惊讶,runtime.Stack为何能输出已经被释放函数的信息?在Go的panic机制中,延迟函数的调用在释放堆栈信息之前。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch5/ch5-10.html b/ch5/ch5-10.html new file mode 100644 index 0000000..f973bd2 --- /dev/null +++ b/ch5/ch5-10.html @@ -0,0 +1,288 @@ + + + + + + Recover捕获异常 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

5.10. Recover捕获异常

+

通常来说,不应该对panic异常做任何处理,但有时,也许我们可以从异常中恢复,至少我们可以在程序崩溃前,做一些操作。举个例子,当web服务器遇到不可预料的严重问题时,在崩溃前应该将所有的连接关闭;如果不做任何处理,会使得客户端一直处于等待状态。如果web服务器还在开发阶段,服务器甚至可以将异常信息反馈到客户端,帮助调试。

+

如果在deferred函数中调用了内置函数recover,并且定义该defer语句的函数发生了panic异常,recover会使程序从panic中恢复,并返回panic value。导致panic异常的函数不会继续运行,但能正常返回。在未发生panic时调用recover,recover会返回nil。

+

让我们以语言解析器为例,说明recover的使用场景。考虑到语言解析器的复杂性,即使某个语言解析器目前工作正常,也无法肯定它没有漏洞。因此,当某个异常出现时,我们不会选择让解析器崩溃,而是会将panic异常当作普通的解析错误,并附加额外信息提醒用户报告此错误。

+
func Parse(input string) (s *Syntax, err error) {
+	defer func() {
+		if p := recover(); p != nil {
+			err = fmt.Errorf("internal error: %v", p)
+		}
+	}()
+	// ...parser...
+}
+
+

deferred函数帮助Parse从panic中恢复。在deferred函数内部,panic value被附加到错误信息中;并用err变量接收错误信息,返回给调用者。我们也可以通过调用runtime.Stack往错误信息中添加完整的堆栈调用信息。

+

不加区分的恢复所有的panic异常,不是可取的做法;因为在panic之后,无法保证包级变量的状态仍然和我们预期一致。比如,对数据结构的一次重要更新没有被完整完成、文件或者网络连接没有被关闭、获得的锁没有被释放。此外,如果写日志时产生的panic被不加区分的恢复,可能会导致漏洞被忽略。

+

虽然把对panic的处理都集中在一个包下,有助于简化对复杂和不可以预料问题的处理,但作为被广泛遵守的规范,你不应该试图去恢复其他包引起的panic。公有的API应该将函数的运行失败作为error返回,而不是panic。同样的,你也不应该恢复一个由他人开发的函数引起的panic,比如说调用者传入的回调函数,因为你无法确保这样做是安全的。

+

有时我们很难完全遵循规范,举个例子,net/http包中提供了一个web服务器,将收到的请求分发给用户提供的处理函数。很显然,我们不能因为某个处理函数引发的panic异常,杀掉整个进程;web服务器遇到处理函数导致的panic时会调用recover,输出堆栈信息,继续运行。这样的做法在实践中很便捷,但也会引起资源泄漏,或是因为recover操作,导致其他问题。

+

基于以上原因,安全的做法是有选择性的recover。换句话说,只恢复应该被恢复的panic异常,此外,这些异常所占的比例应该尽可能的低。为了标识某个panic是否应该被恢复,我们可以将panic value设置成特殊类型。在recover时对panic value进行检查,如果发现panic value是特殊类型,就将这个panic作为error处理,如果不是,则按照正常的panic进行处理(在下面的例子中,我们会看到这种方式)。

+

下面的例子是title函数的变形,如果HTML页面包含多个<title>,该函数会给调用者返回一个错误(error)。在soleTitle内部处理时,如果检测到有多个<title>,会调用panic,阻止函数继续递归,并将特殊类型bailout作为panic的参数。

+
// soleTitle returns the text of the first non-empty title element
+// in doc, and an error if there was not exactly one.
+func soleTitle(doc *html.Node) (title string, err error) {
+	type bailout struct{}
+	defer func() {
+		switch p := recover(); p {
+		case nil:       // no panic
+		case bailout{}: // "expected" panic
+			err = fmt.Errorf("multiple title elements")
+		default:
+			panic(p) // unexpected panic; carry on panicking
+		}
+	}()
+	// Bail out of recursion if we find more than one nonempty title.
+	forEachNode(doc, func(n *html.Node) {
+		if n.Type == html.ElementNode && n.Data == "title" &&
+			n.FirstChild != nil {
+			if title != "" {
+				panic(bailout{}) // multiple titleelements
+			}
+			title = n.FirstChild.Data
+		}
+	}, nil)
+	if title == "" {
+		return "", fmt.Errorf("no title element")
+	}
+	return title, nil
+}
+
+

在上例中,deferred函数调用recover,并检查panic value。当panic value是bailout{}类型时,deferred函数生成一个error返回给调用者。当panic value是其他non-nil值时,表示发生了未知的panic异常,deferred函数将调用panic函数并将当前的panic value作为参数传入;此时,等同于recover没有做任何操作。(请注意:在例子中,对可预期的错误采用了panic,这违反了之前的建议,我们在此只是想向读者演示这种机制。)

+

有些情况下,我们无法恢复。某些致命错误会导致Go在运行时终止程序,如内存不足。

+

练习5.19: 使用panic和recover编写一个不包含return语句但能返回一个非零值的函数。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch5/ch5.html b/ch5/ch5.html new file mode 100644 index 0000000..22cab70 --- /dev/null +++ b/ch5/ch5.html @@ -0,0 +1,240 @@ + + + + + + 函数 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

第5章 函数

+

函数可以让我们将一个语句序列打包为一个单元,然后可以从程序中其它地方多次调用。函数的机制可以让我们将一个大的工作分解为小的任务,这样的小任务可以让不同程序员在不同时间、不同地方独立完成。一个函数同时对用户隐藏了其实现细节。由于这些因素,对于任何编程语言来说,函数都是一个至关重要的部分。

+

我们已经见过许多函数了。现在,让我们多花一点时间来彻底地讨论函数特性。本章的运行示例是一个网络蜘蛛,也就是web搜索引擎中负责抓取网页部分的组件,它们根据抓取网页中的链接继续抓取链接指向的页面。一个网络蜘蛛的例子给我们足够的机会去探索递归函数、匿名函数、错误处理和函数其它的很多特性。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch6/ch6-01.html b/ch6/ch6-01.html new file mode 100644 index 0000000..d7be2c7 --- /dev/null +++ b/ch6/ch6-01.html @@ -0,0 +1,301 @@ + + + + + + 方法声明 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

6.1. 方法声明

+

在函数声明时,在其名字之前放上一个变量,即是一个方法。这个附加的参数会将该函数附加到这种类型上,即相当于为这种类型定义了一个独占的方法。

+

下面来写我们第一个方法的例子,这个例子在package geometry下:

+

gopl.io/ch6/geometry

+
package geometry
+
+import "math"
+
+type Point struct{ X, Y float64 }
+
+// traditional function
+func Distance(p, q Point) float64 {
+	return math.Hypot(q.X-p.X, q.Y-p.Y)
+}
+
+// same thing, but as a method of the Point type
+func (p Point) Distance(q Point) float64 {
+	return math.Hypot(q.X-p.X, q.Y-p.Y)
+}
+
+

上面的代码里那个附加的参数p,叫做方法的接收器(receiver),早期的面向对象语言留下的遗产将调用一个方法称为“向一个对象发送消息”。

+

在Go语言中,我们并不会像其它语言那样用this或者self作为接收器;我们可以任意的选择接收器的名字。由于接收器的名字经常会被使用到,所以保持其在方法间传递时的一致性和简短性是不错的主意。这里的建议是可以使用其类型的第一个字母,比如这里使用了Point的首字母p。

+

在方法调用过程中,接收器参数一般会在方法名之前出现。这和方法声明是一样的,都是接收器参数在方法名字之前。下面是例子:

+
p := Point{1, 2}
+q := Point{4, 6}
+fmt.Println(Distance(p, q)) // "5", function call
+fmt.Println(p.Distance(q))  // "5", method call
+
+

可以看到,上面的两个函数调用都是Distance,但是却没有发生冲突。第一个Distance的调用实际上用的是包级别的函数geometry.Distance,而第二个则是使用刚刚声明的Point,调用的是Point类下声明的Point.Distance方法。

+

这种p.Distance的表达式叫做选择器,因为他会选择合适的对应p这个对象的Distance方法来执行。选择器也会被用来选择一个struct类型的字段,比如p.X。由于方法和字段都是在同一命名空间,所以如果我们在这里声明一个X方法的话,编译器会报错,因为在调用p.X时会有歧义(译注:这里确实挺奇怪的)。

+

因为每种类型都有其方法的命名空间,我们在用Distance这个名字的时候,不同的Distance调用指向了不同类型里的Distance方法。让我们来定义一个Path类型,这个Path代表一个线段的集合,并且也给这个Path定义一个叫Distance的方法。

+
// A Path is a journey connecting the points with straight lines.
+type Path []Point
+// Distance returns the distance traveled along the path.
+func (path Path) Distance() float64 {
+	sum := 0.0
+	for i := range path {
+		if i > 0 {
+			sum += path[i-1].Distance(path[i])
+		}
+	}
+	return sum
+}
+
+

Path是一个命名的slice类型,而不是Point那样的struct类型,然而我们依然可以为它定义方法。在能够给任意类型定义方法这一点上,Go和很多其它的面向对象的语言不太一样。因此在Go语言里,我们为一些简单的数值、字符串、slice、map来定义一些附加行为很方便。我们可以给同一个包内的任意命名类型定义方法,只要这个命名类型的底层类型(译注:这个例子里,底层类型是指[]Point这个slice,Path就是命名类型)不是指针或者interface。

+

两个Distance方法有不同的类型。他们两个方法之间没有任何关系,尽管Path的Distance方法会在内部调用Point.Distance方法来计算每个连接邻接点的线段的长度。

+

让我们来调用一个新方法,计算三角形的周长:

+
perim := Path{
+	{1, 1},
+	{5, 1},
+	{5, 4},
+	{1, 1},
+}
+fmt.Println(perim.Distance()) // "12"
+
+

在上面两个对Distance名字的方法的调用中,编译器会根据方法的名字以及接收器来决定具体调用的是哪一个函数。第一个例子中path[i-1]数组中的类型是Point,因此Point.Distance这个方法被调用;在第二个例子中perim的类型是Path,因此Distance调用的是Path.Distance。

+

对于一个给定的类型,其内部的方法都必须有唯一的方法名,但是不同的类型却可以有同样的方法名,比如我们这里Point和Path就都有Distance这个名字的方法;所以我们没有必要非在方法名之前加类型名来消除歧义,比如PathDistance。这里我们已经看到了方法比之函数的一些好处:方法名可以简短。当我们在包外调用的时候这种好处就会被放大,因为我们可以使用这个短名字,而可以省略掉包的名字,下面是例子:

+
import "gopl.io/ch6/geometry"
+
+perim := geometry.Path{{1, 1}, {5, 1}, {5, 4}, {1, 1}}
+fmt.Println(geometry.PathDistance(perim)) // "12", standalone function
+fmt.Println(perim.Distance())             // "12", method of geometry.Path
+
+

译注: 如果我们要用方法去计算perim的distance,还需要去写全geometry的包名,和其函数名,但是因为Path这个类型定义了一个可以直接用的Distance方法,所以我们可以直接写perim.Distance()。相当于可以少打很多字,作者应该是这个意思。因为在Go里包外调用函数需要带上包名,还是挺麻烦的。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch6/ch6-02.html b/ch6/ch6-02.html new file mode 100644 index 0000000..414800e --- /dev/null +++ b/ch6/ch6-02.html @@ -0,0 +1,347 @@ + + + + + + 基于指针对象的方法 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

6.2. 基于指针对象的方法

+

当调用一个函数时,会对其每一个参数值进行拷贝,如果一个函数需要更新一个变量,或者函数的其中一个参数实在太大我们希望能够避免进行这种默认的拷贝,这种情况下我们就需要用到指针了。对应到我们这里用来更新接收器的对象的方法,当这个接受者变量本身比较大时,我们就可以用其指针而不是对象来声明方法,如下:

+
func (p *Point) ScaleBy(factor float64) {
+	p.X *= factor
+	p.Y *= factor
+}
+
+

这个方法的名字是(*Point).ScaleBy。这里的括号是必须的;没有括号的话这个表达式可能会被理解为*(Point.ScaleBy)

+

在现实的程序里,一般会约定如果Point这个类有一个指针作为接收器的方法,那么所有Point的方法都必须有一个指针接收器,即使是那些并不需要这个指针接收器的函数。我们在这里打破了这个约定只是为了展示一下两种方法的异同而已。

+

只有类型(Point)和指向他们的指针(*Point),才可能是出现在接收器声明里的两种接收器。此外,为了避免歧义,在声明方法时,如果一个类型名本身是一个指针的话,是不允许其出现在接收器中的,比如下面这个例子:

+
type P *int
+func (P) f() { /* ... */ } // compile error: invalid receiver type
+
+

想要调用指针类型方法(*Point).ScaleBy,只要提供一个Point类型的指针即可,像下面这样。

+
r := &Point{1, 2}
+r.ScaleBy(2)
+fmt.Println(*r) // "{2, 4}"
+
+

或者这样:

+
p := Point{1, 2}
+pptr := &p
+pptr.ScaleBy(2)
+fmt.Println(p) // "{2, 4}"
+
+

或者这样:

+
p := Point{1, 2}
+(&p).ScaleBy(2)
+fmt.Println(p) // "{2, 4}"
+
+

不过后面两种方法有些笨拙。幸运的是,go语言本身在这种地方会帮到我们。如果接收器p是一个Point类型的变量,并且其方法需要一个Point指针作为接收器,我们可以用下面这种简短的写法:

+
p.ScaleBy(2)
+
+

编译器会隐式地帮我们用&p去调用ScaleBy这个方法。这种简写方法只适用于“变量”,包括struct里的字段比如p.X,以及array和slice内的元素比如perim[0]。我们不能通过一个无法取到地址的接收器来调用指针方法,比如临时变量的内存地址就无法获取得到:

+
Point{1, 2}.ScaleBy(2) // compile error: can't take address of Point literal
+
+

但是我们可以用一个*Point这样的接收器来调用Point的方法,因为我们可以通过地址来找到这个变量,只要用解引用符号*来取到该变量即可。编译器在这里也会给我们隐式地插入*这个操作符,所以下面这两种写法等价的:

+
pptr.Distance(q)
+(*pptr).Distance(q)
+
+

这里的几个例子可能让你有些困惑,所以我们总结一下:在每一个合法的方法调用表达式中,也就是下面三种情况里的任意一种情况都是可以的:

+

要么接收器的实际参数和其形式参数是相同的类型,比如两者都是类型T或者都是类型*T

+
Point{1, 2}.Distance(q) //  Point
+pptr.ScaleBy(2)         // *Point
+
+

或者接收器实参是类型T,但接收器形参是类型*T,这种情况下编译器会隐式地为我们取变量的地址:

+
p.ScaleBy(2) // implicit (&p)
+
+

或者接收器实参是类型*T,形参是类型T。编译器会隐式地为我们解引用,取到指针指向的实际变量:

+
pptr.Distance(q) // implicit (*pptr)
+
+

如果命名类型T(译注:用type xxx定义的类型)的所有方法都是用T类型自己来做接收器(而不是*T),那么拷贝这种类型的实例就是安全的;调用他的任何一个方法也就会产生一个值的拷贝。比如time.Duration的这个类型,在调用其方法时就会被全部拷贝一份,包括在作为参数传入函数的时候。但是如果一个方法使用指针作为接收器,你需要避免对其进行拷贝,因为这样可能会破坏掉该类型内部的不变性。比如你对bytes.Buffer对象进行了拷贝,那么可能会引起原始对象和拷贝对象只是别名而已,实际上它们指向的对象是一样的。紧接着对拷贝后的变量进行修改可能会有让你有意外的结果。

+

译注: 作者这里说的比较绕,其实有两点:

+
    +
  1. 不管你的method的receiver是指针类型还是非指针类型,都是可以通过指针/非指针类型进行调用的,编译器会帮你做类型转换。
  2. +
  3. 在声明一个method的receiver该是指针还是非指针类型时,你需要考虑两方面的因素,第一方面是这个对象本身是不是特别大,如果声明为非指针变量时,调用会产生一次拷贝;第二方面是如果你用指针类型作为receiver,那么你一定要注意,这种指针类型指向的始终是一块内存地址,就算你对其进行了拷贝。熟悉C或者C++的人这里应该很快能明白。
  4. +
+

6.2.1. Nil也是一个合法的接收器类型

+

就像一些函数允许nil指针作为参数一样,方法理论上也可以用nil指针作为其接收器,尤其当nil对于对象来说是合法的零值时,比如map或者slice。在下面的简单int链表的例子里,nil代表的是空链表:

+
// An IntList is a linked list of integers.
+// A nil *IntList represents the empty list.
+type IntList struct {
+	Value int
+	Tail  *IntList
+}
+// Sum returns the sum of the list elements.
+func (list *IntList) Sum() int {
+	if list == nil {
+		return 0
+	}
+	return list.Value + list.Tail.Sum()
+}
+
+

当你定义一个允许nil作为接收器值的方法的类型时,在类型前面的注释中指出nil变量代表的意义是很有必要的,就像我们上面例子里做的这样。

+

下面是net/url包里Values类型定义的一部分。

+

net/url

+
package url
+
+// Values maps a string key to a list of values.
+type Values map[string][]string
+// Get returns the first value associated with the given key,
+// or "" if there are none.
+func (v Values) Get(key string) string {
+	if vs := v[key]; len(vs) > 0 {
+		return vs[0]
+	}
+	return ""
+}
+// Add adds the value to key.
+// It appends to any existing values associated with key.
+func (v Values) Add(key, value string) {
+	v[key] = append(v[key], value)
+}
+
+

这个定义向外部暴露了一个map的命名类型,并且提供了一些能够简单操作这个map的方法。这个map的value字段是一个string的slice,所以这个Values是一个多维map。客户端使用这个变量的时候可以使用map固有的一些操作(make,切片,m[key]等等),也可以使用这里提供的操作方法,或者两者并用,都是可以的:

+

gopl.io/ch6/urlvalues

+
m := url.Values{"lang": {"en"}} // direct construction
+m.Add("item", "1")
+m.Add("item", "2")
+
+fmt.Println(m.Get("lang")) // "en"
+fmt.Println(m.Get("q"))    // ""
+fmt.Println(m.Get("item")) // "1"      (first value)
+fmt.Println(m["item"])     // "[1 2]"  (direct map access)
+
+m = nil
+fmt.Println(m.Get("item")) // ""
+m.Add("item", "3")         // panic: assignment to entry in nil map
+
+

对Get的最后一次调用中,nil接收器的行为即是一个空map的行为。我们可以等价地将这个操作写成Value(nil).Get("item"),但是如果你直接写nil.Get("item")的话是无法通过编译的,因为nil的字面量编译器无法判断其准确类型。所以相比之下,最后的那行m.Add的调用就会产生一个panic,因为他尝试更新一个空map。

+

由于url.Values是一个map类型,并且间接引用了其key/value对,因此url.Values.Add对这个map里的元素做任何的更新、删除操作对调用方都是可见的。实际上,就像在普通函数中一样,虽然可以通过引用来操作内部值,但在方法想要修改引用本身时是不会影响原始值的,比如把他置换为nil,或者让这个引用指向了其它的对象,调用方都不会受影响。(译注:因为传入的是存储了内存地址的变量,你改变这个变量本身是影响不了原始的变量的,想想C语言,是差不多的)

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch6/ch6-03.html b/ch6/ch6-03.html new file mode 100644 index 0000000..15a6b58 --- /dev/null +++ b/ch6/ch6-03.html @@ -0,0 +1,331 @@ + + + + + + 通过嵌入结构体来扩展类型 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

6.3. 通过嵌入结构体来扩展类型

+

来看看ColoredPoint这个类型:

+

gopl.io/ch6/coloredpoint

+
import "image/color"
+
+type Point struct{ X, Y float64 }
+
+type ColoredPoint struct {
+	Point
+	Color color.RGBA
+}
+
+

我们完全可以将ColoredPoint定义为一个有三个字段的struct,但是我们却将Point这个类型嵌入到ColoredPoint来提供X和Y这两个字段。像我们在4.4节中看到的那样,内嵌可以使我们在定义ColoredPoint时得到一种句法上的简写形式,并使其包含Point类型所具有的一切字段,然后再定义一些自己的。如果我们想要的话,我们可以直接认为通过嵌入的字段就是ColoredPoint自身的字段,而完全不需要在调用时指出Point,比如下面这样。

+
var cp ColoredPoint
+cp.X = 1
+fmt.Println(cp.Point.X) // "1"
+cp.Point.Y = 2
+fmt.Println(cp.Y) // "2"
+
+

对于Point中的方法我们也有类似的用法,我们可以把ColoredPoint类型当作接收器来调用Point里的方法,即使ColoredPoint里没有声明这些方法:

+
red := color.RGBA{255, 0, 0, 255}
+blue := color.RGBA{0, 0, 255, 255}
+var p = ColoredPoint{Point{1, 1}, red}
+var q = ColoredPoint{Point{5, 4}, blue}
+fmt.Println(p.Distance(q.Point)) // "5"
+p.ScaleBy(2)
+q.ScaleBy(2)
+fmt.Println(p.Distance(q.Point)) // "10"
+
+

Point类的方法也被引入了ColoredPoint。用这种方式,内嵌可以使我们定义字段特别多的复杂类型,我们可以将字段先按小类型分组,然后定义小类型的方法,之后再把它们组合起来。

+

读者如果对基于类来实现面向对象的语言比较熟悉的话,可能会倾向于将Point看作一个基类,而ColoredPoint看作其子类或者继承类,或者将ColoredPoint看作"is a" Point类型。但这是错误的理解。请注意上面例子中对Distance方法的调用。Distance有一个参数是Point类型,但q并不是一个Point类,所以尽管q有着Point这个内嵌类型,我们也必须要显式地选择它。尝试直接传q的话你会看到下面这样的错误:

+
p.Distance(q) // compile error: cannot use q (ColoredPoint) as Point
+
+

一个ColoredPoint并不是一个Point,但他"has a"Point,并且它有从Point类里引入的Distance和ScaleBy方法。如果你喜欢从实现的角度来考虑问题,内嵌字段会指导编译器去生成额外的包装方法来委托已经声明好的方法,和下面的形式是等价的:

+
func (p ColoredPoint) Distance(q Point) float64 {
+	return p.Point.Distance(q)
+}
+
+func (p *ColoredPoint) ScaleBy(factor float64) {
+	p.Point.ScaleBy(factor)
+}
+
+

当Point.Distance被第一个包装方法调用时,它的接收器值是p.Point,而不是p,当然了,在Point类的方法里,你是访问不到ColoredPoint的任何字段的。

+

在类型中内嵌的匿名字段也可能是一个命名类型的指针,这种情况下字段和方法会被间接地引入到当前的类型中(译注:访问需要通过该指针指向的对象去取)。添加这一层间接关系让我们可以共享通用的结构并动态地改变对象之间的关系。下面这个ColoredPoint的声明内嵌了一个*Point的指针。

+
type ColoredPoint struct {
+	*Point
+	Color color.RGBA
+}
+
+p := ColoredPoint{&Point{1, 1}, red}
+q := ColoredPoint{&Point{5, 4}, blue}
+fmt.Println(p.Distance(*q.Point)) // "5"
+q.Point = p.Point                 // p and q now share the same Point
+p.ScaleBy(2)
+fmt.Println(*p.Point, *q.Point) // "{2 2} {2 2}"
+
+

一个struct类型也可能会有多个匿名字段。我们将ColoredPoint定义为下面这样:

+
type ColoredPoint struct {
+	Point
+	color.RGBA
+}
+
+

然后这种类型的值便会拥有Point和RGBA类型的所有方法,以及直接定义在ColoredPoint中的方法。当编译器解析一个选择器到方法时,比如p.ScaleBy,它会首先去找直接定义在这个类型里的ScaleBy方法,然后找被ColoredPoint的内嵌字段们引入的方法,然后去找Point和RGBA的内嵌字段引入的方法,然后一直递归向下找。如果选择器有二义性的话编译器会报错,比如你在同一级里有两个同名的方法。

+

方法只能在命名类型(像Point)或者指向类型的指针上定义,但是多亏了内嵌,有些时候我们给匿名struct类型来定义方法也有了手段。

+

下面是一个小trick。这个例子展示了简单的cache,其使用两个包级别的变量来实现,一个mutex互斥量(§9.2)和它所操作的cache:

+
var (
+	mu sync.Mutex // guards mapping
+	mapping = make(map[string]string)
+)
+
+func Lookup(key string) string {
+	mu.Lock()
+	v := mapping[key]
+	mu.Unlock()
+	return v
+}
+
+

下面这个版本在功能上是一致的,但将两个包级别的变量放在了cache这个struct一组内:

+
var cache = struct {
+	sync.Mutex
+	mapping map[string]string
+}{
+	mapping: make(map[string]string),
+}
+
+
+func Lookup(key string) string {
+	cache.Lock()
+	v := cache.mapping[key]
+	cache.Unlock()
+	return v
+}
+
+

我们给新的变量起了一个更具表达性的名字:cache。因为sync.Mutex字段也被嵌入到了这个struct里,其Lock和Unlock方法也就都被引入到了这个匿名结构中了,这让我们能够以一个简单明了的语法来对其进行加锁解锁操作。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch6/ch6-04.html b/ch6/ch6-04.html new file mode 100644 index 0000000..f126104 --- /dev/null +++ b/ch6/ch6-04.html @@ -0,0 +1,302 @@ + + + + + + 方法值和方法表达式 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

6.4. 方法值和方法表达式

+

我们经常选择一个方法,并且在同一个表达式里执行,比如常见的p.Distance()形式,实际上将其分成两步来执行也是可能的。p.Distance叫作“选择器”,选择器会返回一个方法“值”->一个将方法(Point.Distance)绑定到特定接收器变量的函数。这个函数可以不通过指定其接收器即可被调用;即调用时不需要指定接收器(译注:因为已经在前文中指定过了),只要传入函数的参数即可:

+
p := Point{1, 2}
+q := Point{4, 6}
+
+distanceFromP := p.Distance        // method value
+fmt.Println(distanceFromP(q))      // "5"
+var origin Point                   // {0, 0}
+fmt.Println(distanceFromP(origin)) // "2.23606797749979", sqrt(5)
+
+scaleP := p.ScaleBy // method value
+scaleP(2)           // p becomes (2, 4)
+scaleP(3)           //      then (6, 12)
+scaleP(10)          //      then (60, 120)
+
+

在一个包的API需要一个函数值、且调用方希望操作的是某一个绑定了对象的方法的话,方法“值”会非常实用(``=_=`真是绕)。举例来说,下面例子中的time.AfterFunc这个函数的功能是在指定的延迟时间之后来执行一个(译注:另外的)函数。且这个函数操作的是一个Rocket对象r

+
type Rocket struct { /* ... */ }
+func (r *Rocket) Launch() { /* ... */ }
+r := new(Rocket)
+time.AfterFunc(10 * time.Second, func() { r.Launch() })
+
+

直接用方法“值”传入AfterFunc的话可以更为简短:

+
time.AfterFunc(10 * time.Second, r.Launch)
+
+

译注:省掉了上面那个例子里的匿名函数。

+

和方法“值”相关的还有方法表达式。当调用一个方法时,与调用一个普通的函数相比,我们必须要用选择器(p.Distance)语法来指定方法的接收器。

+

当T是一个类型时,方法表达式可能会写作T.f或者(*T).f,会返回一个函数“值”,这种函数会将其第一个参数用作接收器,所以可以用通常(译注:不写选择器)的方式来对其进行调用:

+
p := Point{1, 2}
+q := Point{4, 6}
+
+distance := Point.Distance   // method expression
+fmt.Println(distance(p, q))  // "5"
+fmt.Printf("%T\n", distance) // "func(Point, Point) float64"
+
+scale := (*Point).ScaleBy
+scale(&p, 2)
+fmt.Println(p)            // "{2 4}"
+fmt.Printf("%T\n", scale) // "func(*Point, float64)"
+
+// 译注:这个Distance实际上是指定了Point对象为接收器的一个方法func (p Point) Distance(),
+// 但通过Point.Distance得到的函数需要比实际的Distance方法多一个参数,
+// 即其需要用第一个额外参数指定接收器,后面排列Distance方法的参数。
+// 看起来本书中函数和方法的区别是指有没有接收器,而不像其他语言那样是指有没有返回值。
+
+

当你根据一个变量来决定调用同一个类型的哪个函数时,方法表达式就显得很有用了。你可以根据选择来调用接收器各不相同的方法。下面的例子,变量op代表Point类型的addition或者subtraction方法,Path.TranslateBy方法会为其Path数组中的每一个Point来调用对应的方法:

+
type Point struct{ X, Y float64 }
+
+func (p Point) Add(q Point) Point { return Point{p.X + q.X, p.Y + q.Y} }
+func (p Point) Sub(q Point) Point { return Point{p.X - q.X, p.Y - q.Y} }
+
+type Path []Point
+
+func (path Path) TranslateBy(offset Point, add bool) {
+	var op func(p, q Point) Point
+	if add {
+		op = Point.Add
+	} else {
+		op = Point.Sub
+	}
+	for i := range path {
+		// Call either path[i].Add(offset) or path[i].Sub(offset).
+		path[i] = op(path[i], offset)
+	}
+}
+
+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch6/ch6-05.html b/ch6/ch6-05.html new file mode 100644 index 0000000..60b3e32 --- /dev/null +++ b/ch6/ch6-05.html @@ -0,0 +1,329 @@ + + + + + + 示例: Bit数组 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

6.5. 示例: Bit数组

+

Go语言里的集合一般会用map[T]bool这种形式来表示,T代表元素类型。集合用map类型来表示虽然非常灵活,但我们可以以一种更好的形式来表示它。例如在数据流分析领域,集合元素通常是一个非负整数,集合会包含很多元素,并且集合会经常进行并集、交集操作,这种情况下,bit数组会比map表现更加理想。(译注:这里再补充一个例子,比如我们执行一个http下载任务,把文件按照16kb一块划分为很多块,需要有一个全局变量来标识哪些块下载完成了,这种时候也需要用到bit数组。)

+

一个bit数组通常会用一个无符号数或者称之为“字”的slice来表示,每一个元素的每一位都表示集合里的一个值。当集合的第i位被设置时,我们才说这个集合包含元素i。下面的这个程序展示了一个简单的bit数组类型,并且实现了三个函数来对这个bit数组来进行操作:

+

gopl.io/ch6/intset

+
// An IntSet is a set of small non-negative integers.
+// Its zero value represents the empty set.
+type IntSet struct {
+	words []uint64
+}
+
+// Has reports whether the set contains the non-negative value x.
+func (s *IntSet) Has(x int) bool {
+	word, bit := x/64, uint(x%64)
+	return word < len(s.words) && s.words[word]&(1<<bit) != 0
+}
+
+// Add adds the non-negative value x to the set.
+func (s *IntSet) Add(x int) {
+	word, bit := x/64, uint(x%64)
+	for word >= len(s.words) {
+		s.words = append(s.words, 0)
+	}
+	s.words[word] |= 1 << bit
+}
+
+// UnionWith sets s to the union of s and t.
+func (s *IntSet) UnionWith(t *IntSet) {
+	for i, tword := range t.words {
+		if i < len(s.words) {
+			s.words[i] |= tword
+		} else {
+			s.words = append(s.words, tword)
+		}
+	}
+}
+
+
+

因为每一个字都有64个二进制位,所以为了定位x的bit位,我们用了x/64的商作为字的下标,并且用x%64得到的值作为这个字内的bit的所在位置。UnionWith这个方法里用到了bit位的“或”逻辑操作符号|来一次完成64个元素的或计算。(在练习6.5中我们还会有程序用到这个64位字的例子。)

+

当前这个实现还缺少了很多必要的特性,我们把其中一些作为练习题列在本小节之后。但是有一个方法如果缺失的话我们的bit数组可能会比较难混:将IntSet作为一个字符串来打印。这里我们来实现它,让我们来给上面的例子添加一个String方法,类似2.5节中做的那样:

+
// String returns the set as a string of the form "{1 2 3}".
+func (s *IntSet) String() string {
+	var buf bytes.Buffer
+	buf.WriteByte('{')
+	for i, word := range s.words {
+		if word == 0 {
+			continue
+		}
+		for j := 0; j < 64; j++ {
+			if word&(1<<uint(j)) != 0 {
+				if buf.Len() > len("{") {
+					buf.WriteByte(' ')
+				}
+				fmt.Fprintf(&buf, "%d", 64*i+j)
+			}
+		}
+	}
+	buf.WriteByte('}')
+	return buf.String()
+}
+
+

这里留意一下String方法,是不是和3.5.4节中的intsToString方法很相似;bytes.Buffer在String方法里经常这么用。当你为一个复杂的类型定义了一个String方法时,fmt包就会特殊对待这种类型的值,这样可以让这些类型在打印的时候看起来更加友好,而不是直接打印其原始的值。fmt会直接调用用户定义的String方法。这种机制依赖于接口和类型断言,在第7章中我们会详细介绍。

+

现在我们就可以在实战中直接用上面定义好的IntSet了:

+
var x, y IntSet
+x.Add(1)
+x.Add(144)
+x.Add(9)
+fmt.Println(x.String()) // "{1 9 144}"
+
+y.Add(9)
+y.Add(42)
+fmt.Println(y.String()) // "{9 42}"
+
+x.UnionWith(&y)
+fmt.Println(x.String()) // "{1 9 42 144}"
+fmt.Println(x.Has(9), x.Has(123)) // "true false"
+
+

这里要注意:我们声明的String和Has两个方法都是以指针类型*IntSet来作为接收器的,但实际上对于这两个类型来说,把接收器声明为指针类型也没什么必要。不过另外两个函数就不是这样了,因为另外两个函数操作的是s.words对象,如果你不把接收器声明为指针对象,那么实际操作的是拷贝对象,而不是原来的那个对象。因此,因为我们的String方法定义在IntSet指针上,所以当我们的变量是IntSet类型而不是IntSet指针时,可能会有下面这样让人意外的情况:

+
fmt.Println(&x)         // "{1 9 42 144}"
+fmt.Println(x.String()) // "{1 9 42 144}"
+fmt.Println(x)          // "{[4398046511618 0 65536]}"
+
+

在第一个Println中,我们打印一个*IntSet的指针,这个类型的指针确实有自定义的String方法。第二Println,我们直接调用了x变量的String()方法;这种情况下编译器会隐式地在x前插入&操作符,这样相当于我们还是调用的IntSet指针的String方法。在第三个Println中,因为IntSet类型没有String方法,所以Println方法会直接以原始的方式理解并打印。所以在这种情况下&符号是不能忘的。在我们这种场景下,你把String方法绑定到IntSet对象上,而不是IntSet指针上可能会更合适一些,不过这也需要具体问题具体分析。

+

练习6.1: 为bit数组实现下面这些方法

+
func (*IntSet) Len() int      // return the number of elements
+func (*IntSet) Remove(x int)  // remove x from the set
+func (*IntSet) Clear()        // remove all elements from the set
+func (*IntSet) Copy() *IntSet // return a copy of the set
+
+

练习 6.2: 定义一个变参方法(*IntSet).AddAll(...int),这个方法可以添加一组IntSet,比如s.AddAll(1,2,3)。

+

练习 6.3: (*IntSet).UnionWith会用|操作符计算两个集合的并集,我们再为IntSet实现另外的几个函数IntersectWith(交集:元素在A集合B集合均出现),DifferenceWith(差集:元素出现在A集合,未出现在B集合),SymmetricDifference(并差集:元素出现在A但没有出现在B,或者出现在B没有出现在A)。

+

***练习6.4: ** 实现一个Elems方法,返回集合中的所有元素,用于做一些range之类的遍历操作。

+

练习 6.5: 我们这章定义的IntSet里的每个字都是用的uint64类型,但是64位的数值可能在32位的平台上不高效。修改程序,使其使用uint类型,这种类型对于32位平台来说更合适。当然了,这里我们可以不用简单粗暴地除64,可以定义一个常量来决定是用32还是64,这里你可能会用到平台的自动判断的一个智能表达式:32 << (^uint(0) >> 63)

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch6/ch6-06.html b/ch6/ch6-06.html new file mode 100644 index 0000000..6016036 --- /dev/null +++ b/ch6/ch6-06.html @@ -0,0 +1,299 @@ + + + + + + 封装 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

6.6. 封装

+

一个对象的变量或者方法如果对调用方是不可见的话,一般就被定义为“封装”。封装有时候也被叫做信息隐藏,同时也是面向对象编程最关键的一个方面。

+

Go语言只有一种控制可见性的手段:大写首字母的标识符会从定义它们的包中被导出,小写字母的则不会。这种限制包内成员的方式同样适用于struct或者一个类型的方法。因而如果我们想要封装一个对象,我们必须将其定义为一个struct。

+

这也就是前面的小节中IntSet被定义为struct类型的原因,尽管它只有一个字段:

+
type IntSet struct {
+    words []uint64
+}
+
+

当然,我们也可以把IntSet定义为一个slice类型,但这样我们就需要把代码中所有方法里用到的s.words用*s替换掉了:

+
type IntSet []uint64
+
+

尽管这个版本的IntSet在本质上是一样的,但它也允许其它包中可以直接读取并编辑这个slice。换句话说,相对于*s这个表达式会出现在所有的包中,s.words只需要在定义IntSet的包中出现(译注:所以还是推荐后者吧的意思)。

+

这种基于名字的手段使得在语言中最小的封装单元是package,而不是像其它语言一样的类型。一个struct类型的字段对同一个包的所有代码都有可见性,无论你的代码是写在一个函数还是一个方法里。

+

封装提供了三方面的优点。首先,因为调用方不能直接修改对象的变量值,其只需要关注少量的语句并且只要弄懂少量变量的可能的值即可。

+

第二,隐藏实现的细节,可以防止调用方依赖那些可能变化的具体实现,这样使设计包的程序员在不破坏对外的api情况下能得到更大的自由。

+

把bytes.Buffer这个类型作为例子来考虑。这个类型在做短字符串叠加的时候很常用,所以在设计的时候可以做一些预先的优化,比如提前预留一部分空间,来避免反复的内存分配。又因为Buffer是一个struct类型,这些额外的空间可以用附加的字节数组来保存,且放在一个小写字母开头的字段中。这样在外部的调用方只能看到性能的提升,但并不会得到这个附加变量。Buffer和其增长算法我们列在这里,为了简洁性稍微做了一些精简:

+
type Buffer struct {
+    buf     []byte
+    initial [64]byte
+    /* ... */
+}
+
+// Grow expands the buffer's capacity, if necessary,
+// to guarantee space for another n bytes. [...]
+func (b *Buffer) Grow(n int) {
+    if b.buf == nil {
+        b.buf = b.initial[:0] // use preallocated space initially
+    }
+    if len(b.buf)+n > cap(b.buf) {
+        buf := make([]byte, b.Len(), 2*cap(b.buf) + n)
+        copy(buf, b.buf)
+        b.buf = buf
+    }
+}
+
+

封装的第三个优点也是最重要的优点,是阻止了外部调用方对对象内部的值任意地进行修改。因为对象内部变量只可以被同一个包内的函数修改,所以包的作者可以让这些函数确保对象内部的一些值的不变性。比如下面的Counter类型允许调用方来增加counter变量的值,并且允许将这个值reset为0,但是不允许随便设置这个值(译注:因为压根就访问不到):

+
type Counter struct { n int }
+func (c *Counter) N() int     { return c.n }
+func (c *Counter) Increment() { c.n++ }
+func (c *Counter) Reset()     { c.n = 0 }
+
+

只用来访问或修改内部变量的函数被称为setter或者getter,例子如下,比如log包里的Logger类型对应的一些函数。在命名一个getter方法时,我们通常会省略掉前面的Get前缀。这种简洁上的偏好也可以推广到各种类型的前缀比如Fetch,Find或者Lookup。

+
package log
+type Logger struct {
+	flags  int
+	prefix string
+	// ...
+}
+func (l *Logger) Flags() int
+func (l *Logger) SetFlags(flag int)
+func (l *Logger) Prefix() string
+func (l *Logger) SetPrefix(prefix string)
+
+

Go的编码风格不禁止直接导出字段。当然,一旦进行了导出,就没有办法在保证API兼容的情况下去除对其的导出,所以在一开始的选择一定要经过深思熟虑并且要考虑到包内部的一些不变量的保证,未来可能的变化,以及调用方的代码质量是否会因为包的一点修改而变差。

+

封装并不总是理想的。 +虽然封装在有些情况是必要的,但有时候我们也需要暴露一些内部内容,比如:time.Duration将其表现暴露为一个int64数字的纳秒,使得我们可以用一般的数值操作来对时间进行对比,甚至可以定义这种类型的常量:

+
const day = 24 * time.Hour
+fmt.Println(day.Seconds()) // "86400"
+
+

另一个例子,将IntSet和本章开头的geometry.Path进行对比。Path被定义为一个slice类型,这允许其调用slice的字面方法来对其内部的points用range进行迭代遍历;在这一点上,IntSet是没有办法让你这么做的。

+

这两种类型决定性的不同:geometry.Path的本质是一个坐标点的序列,不多也不少,我们可以预见到之后也并不会给他增加额外的字段,所以在geometry包中将Path暴露为一个slice。相比之下,IntSet仅仅是在这里用了一个[]uint64的slice。这个类型还可以用[]uint类型来表示,或者我们甚至可以用其它完全不同的占用更小内存空间的东西来表示这个集合,所以我们可能还会需要额外的字段来在这个类型中记录元素的个数。也正是因为这些原因,我们让IntSet对调用方不透明。

+

在这章中,我们学到了如何将方法与命名类型进行组合,并且知道了如何调用这些方法。尽管方法对于OOP编程来说至关重要,但他们只是OOP编程里的半边天。为了完成OOP,我们还需要接口。Go里的接口会在下一章中介绍。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch6/ch6.html b/ch6/ch6.html new file mode 100644 index 0000000..d74b5be --- /dev/null +++ b/ch6/ch6.html @@ -0,0 +1,248 @@ + + + + + + 方法 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

第6章 方法

+

从90年代早期开始,面向对象编程(OOP)就成为了称霸工程界和教育界的编程范式,所以之后几乎所有大规模被应用的语言都包含了对OOP的支持,go语言也不例外。

+

尽管没有被大众所接受的明确的OOP的定义,从我们的理解来讲,一个对象其实也就是一个简单的值或者一个变量,在这个对象中会包含一些方法,而一个方法则是一个一个和特殊类型关联的函数。一个面向对象的程序会用方法来表达其属性和对应的操作,这样使用这个对象的用户就不需要直接去操作对象,而是借助方法来做这些事情。

+

在早些的章节中,我们已经使用了标准库提供的一些方法,比如time.Duration这个类型的Seconds方法:

+
const day = 24 * time.Hour
+fmt.Println(day.Seconds()) // "86400"
+
+

并且在2.5节中,我们定义了一个自己的方法,Celsius类型的String方法:

+
func (c Celsius) String() string { return fmt.Sprintf("%g°C", c) }
+
+

在本章中,OOP编程的第一方面,我们会向你展示如何有效地定义和使用方法。我们会覆盖到OOP编程的两个关键点,封装和组合。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch7/ch7-01.html b/ch7/ch7-01.html new file mode 100644 index 0000000..f97c87f --- /dev/null +++ b/ch7/ch7-01.html @@ -0,0 +1,306 @@ + + + + + + 接口是合约 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

7.1. 接口约定

+

目前为止,我们看到的类型都是具体的类型。一个具体的类型可以准确的描述它所代表的值,并且展示出对类型本身的一些操作方式:就像数字类型的算术操作,切片类型的取下标、添加元素和范围获取操作。具体的类型还可以通过它的内置方法提供额外的行为操作。总的来说,当你拿到一个具体的类型时你就知道它的本身是什么和你可以用它来做什么。

+

在Go语言中还存在着另外一种类型:接口类型。接口类型是一种抽象的类型。它不会暴露出它所代表的对象的内部值的结构和这个对象支持的基础操作的集合;它们只会表现出它们自己的方法。也就是说当你有看到一个接口类型的值时,你不知道它是什么,唯一知道的就是可以通过它的方法来做什么。

+

在本书中,我们一直使用两个相似的函数来进行字符串的格式化:fmt.Printf,它会把结果写到标准输出,和fmt.Sprintf,它会把结果以字符串的形式返回。得益于使用接口,我们不必可悲的因为返回结果在使用方式上的一些浅显不同就必需把格式化这个最困难的过程复制一份。实际上,这两个函数都使用了另一个函数fmt.Fprintf来进行封装。fmt.Fprintf这个函数对它的计算结果会被怎么使用是完全不知道的。

+
package fmt
+
+func Fprintf(w io.Writer, format string, args ...interface{}) (int, error)
+func Printf(format string, args ...interface{}) (int, error) {
+	return Fprintf(os.Stdout, format, args...)
+}
+func Sprintf(format string, args ...interface{}) string {
+	var buf bytes.Buffer
+	Fprintf(&buf, format, args...)
+	return buf.String()
+}
+
+

Fprintf的前缀F表示文件(File)也表明格式化输出结果应该被写入第一个参数提供的文件中。在Printf函数中的第一个参数os.Stdout是*os.File类型;在Sprintf函数中的第一个参数&buf是一个指向可以写入字节的内存缓冲区,然而它 +并不是一个文件类型尽管它在某种意义上和文件类型相似。

+

即使Fprintf函数中的第一个参数也不是一个文件类型。它是io.Writer类型,这是一个接口类型定义如下:

+
package io
+
+// Writer is the interface that wraps the basic Write method.
+type Writer interface {
+	// Write writes len(p) bytes from p to the underlying data stream.
+	// It returns the number of bytes written from p (0 <= n <= len(p))
+	// and any error encountered that caused the write to stop early.
+	// Write must return a non-nil error if it returns n < len(p).
+	// Write must not modify the slice data, even temporarily.
+	//
+	// Implementations must not retain p.
+	Write(p []byte) (n int, err error)
+}
+
+

io.Writer类型定义了函数Fprintf和这个函数调用者之间的约定。一方面这个约定需要调用者提供具体类型的值就像*os.File*bytes.Buffer,这些类型都有一个特定签名和行为的Write的函数。另一方面这个约定保证了Fprintf接受任何满足io.Writer接口的值都可以工作。Fprintf函数可能没有假定写入的是一个文件或是一段内存,而是写入一个可以调用Write函数的值。

+

因为fmt.Fprintf函数没有对具体操作的值做任何假设,而是仅仅通过io.Writer接口的约定来保证行为,所以第一个参数可以安全地传入一个只需要满足io.Writer接口的任意具体类型的值。一个类型可以自由地被另一个满足相同接口的类型替换,被称作可替换性(LSP里氏替换)。这是一个面向对象的特征。

+

让我们通过一个新的类型来进行校验,下面*ByteCounter类型里的Write方法,仅仅在丢弃写向它的字节前统计它们的长度。(在这个+=赋值语句中,让len(p)的类型和*c的类型匹配的转换是必须的。)

+

gopl.io/ch7/bytecounter

+
type ByteCounter int
+
+func (c *ByteCounter) Write(p []byte) (int, error) {
+	*c += ByteCounter(len(p)) // convert int to ByteCounter
+	return len(p), nil
+}
+
+

因为*ByteCounter满足io.Writer的约定,我们可以把它传入Fprintf函数中;Fprintf函数执行字符串格式化的过程不会去关注ByteCounter正确的累加结果的长度。

+
var c ByteCounter
+c.Write([]byte("hello"))
+fmt.Println(c) // "5", = len("hello")
+c = 0          // reset the counter
+var name = "Dolly"
+fmt.Fprintf(&c, "hello, %s", name)
+fmt.Println(c) // "12", = len("hello, Dolly")
+
+

除了io.Writer这个接口类型,还有另一个对fmt包很重要的接口类型。Fprintf和Fprintln函数向类型提供了一种控制它们值输出的途径。在2.5节中,我们为Celsius类型提供了一个String方法以便于可以打印成这样"100°C" ,在6.5节中我们给*IntSet添加一个String方法,这样集合可以用传统的符号来进行表示就像"{1 2 3}"。给一个类型定义String方法,可以让它满足最广泛使用之一的接口类型fmt.Stringer:

+
package fmt
+
+// The String method is used to print values passed
+// as an operand to any format that accepts a string
+// or to an unformatted printer such as Print.
+type Stringer interface {
+	String() string
+}
+
+

我们会在7.10节解释fmt包怎么发现哪些值是满足这个接口类型的。

+

练习 7.1: 使用来自ByteCounter的思路,实现一个针对单词和行数的计数器。你会发现bufio.ScanWords非常的有用。

+

练习 7.2: 写一个带有如下函数签名的函数CountingWriter,传入一个io.Writer接口类型,返回一个把原来的Writer封装在里面的新的Writer类型和一个表示新的写入字节数的int64类型指针。

+
func CountingWriter(w io.Writer) (io.Writer, *int64)
+
+

练习 7.3: 为在gopl.io/ch4/treesort(§4.4)中的*tree类型实现一个String方法去展示tree类型的值序列。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch7/ch7-02.html b/ch7/ch7-02.html new file mode 100644 index 0000000..0e9099a --- /dev/null +++ b/ch7/ch7-02.html @@ -0,0 +1,276 @@ + + + + + + 接口类型 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

7.2. 接口类型

+

接口类型具体描述了一系列方法的集合,一个实现了这些方法的具体类型是这个接口类型的实例。

+

io.Writer类型是用得最广泛的接口之一,因为它提供了所有类型的写入bytes的抽象,包括文件类型,内存缓冲区,网络链接,HTTP客户端,压缩工具,哈希等等。io包中定义了很多其它有用的接口类型。Reader可以代表任意可以读取bytes的类型,Closer可以是任意可以关闭的值,例如一个文件或是网络链接。(到现在你可能注意到了很多Go语言中单方法接口的命名习惯)

+
package io
+type Reader interface {
+	Read(p []byte) (n int, err error)
+}
+type Closer interface {
+	Close() error
+}
+
+

再往下看,我们发现有些新的接口类型通过组合已有的接口来定义。下面是两个例子:

+
type ReadWriter interface {
+	Reader
+	Writer
+}
+type ReadWriteCloser interface {
+	Reader
+	Writer
+	Closer
+}
+
+

上面用到的语法和结构内嵌相似,我们可以用这种方式以一个简写命名一个接口,而不用声明它所有的方法。这种方式称为接口内嵌。尽管略失简洁,我们可以像下面这样,不使用内嵌来声明io.ReadWriter接口。

+
type ReadWriter interface {
+	Read(p []byte) (n int, err error)
+	Write(p []byte) (n int, err error)
+}
+
+

或者甚至使用一种混合的风格:

+
type ReadWriter interface {
+	Read(p []byte) (n int, err error)
+	Writer
+}
+
+

上面3种定义方式都是一样的效果。方法顺序的变化也没有影响,唯一重要的就是这个集合里面的方法。

+

练习 7.4: strings.NewReader函数通过读取一个string参数返回一个满足io.Reader接口类型的值(和其它值)。实现一个简单版本的NewReader,用它来构造一个接收字符串输入的HTML解析器(§5.2)

+

练习 7.5: io包里面的LimitReader函数接收一个io.Reader接口类型的r和字节数n,并且返回另一个从r中读取字节但是当读完n个字节后就表示读到文件结束的Reader。实现这个LimitReader函数:

+
func LimitReader(r io.Reader, n int64) io.Reader
+
+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch7/ch7-03.html b/ch7/ch7-03.html new file mode 100644 index 0000000..2ec9da8 --- /dev/null +++ b/ch7/ch7-03.html @@ -0,0 +1,341 @@ + + + + + + 实现接口的条件 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

7.3. 实现接口的条件

+

一个类型如果拥有一个接口需要的所有方法,那么这个类型就实现了这个接口。例如,*os.File类型实现了io.Reader,Writer,Closer,和ReadWriter接口。*bytes.Buffer实现了Reader,Writer,和ReadWriter这些接口,但是它没有实现Closer接口因为它不具有Close方法。Go的程序员经常会简要的把一个具体的类型描述成一个特定的接口类型。举个例子,*bytes.Buffer是io.Writer;*os.Files是io.ReadWriter。

+

接口指定的规则非常简单:表达一个类型属于某个接口只要这个类型实现这个接口。所以:

+
var w io.Writer
+w = os.Stdout           // OK: *os.File has Write method
+w = new(bytes.Buffer)   // OK: *bytes.Buffer has Write method
+w = time.Second         // compile error: time.Duration lacks Write method
+
+var rwc io.ReadWriteCloser
+rwc = os.Stdout         // OK: *os.File has Read, Write, Close methods
+rwc = new(bytes.Buffer) // compile error: *bytes.Buffer lacks Close method
+
+

这个规则甚至适用于等式右边本身也是一个接口类型

+
w = rwc                 // OK: io.ReadWriteCloser has Write method
+rwc = w                 // compile error: io.Writer lacks Close method
+
+

因为ReadWriter和ReadWriteCloser包含有Writer的方法,所以任何实现了ReadWriter和ReadWriteCloser的类型必定也实现了Writer接口

+

在进一步学习前,必须先解释一个类型持有一个方法的表示当中的细节。回想在6.2章中,对于每一个命名过的具体类型T;它的一些方法的接收者是类型T本身然而另一些则是一个*T的指针。还记得在T类型的参数上调用一个*T的方法是合法的,只要这个参数是一个变量;编译器隐式的获取了它的地址。但这仅仅是一个语法糖:T类型的值不拥有所有*T指针的方法,这样它就可能只实现了更少的接口。

+

举个例子可能会更清晰一点。在第6.5章中,IntSet类型的String方法的接收者是一个指针类型,所以我们不能在一个不能寻址的IntSet值上调用这个方法:

+
type IntSet struct { /* ... */ }
+func (*IntSet) String() string
+var _ = IntSet{}.String() // compile error: String requires *IntSet receiver
+
+

但是我们可以在一个IntSet变量上调用这个方法:

+
var s IntSet
+var _ = s.String() // OK: s is a variable and &s has a String method
+
+

然而,由于只有*IntSet类型有String方法,所以也只有*IntSet类型实现了fmt.Stringer接口:

+
var _ fmt.Stringer = &s // OK
+var _ fmt.Stringer = s  // compile error: IntSet lacks String method
+
+

12.8章包含了一个打印出任意值的所有方法的程序,然后可以使用godoc -analysis=type tool(§10.7.4)展示每个类型的方法和具体类型和接口之间的关系

+

就像信封封装和隐藏起信件来一样,接口类型封装和隐藏具体类型和它的值。即使具体类型有其它的方法,也只有接口类型暴露出来的方法会被调用到:

+
os.Stdout.Write([]byte("hello")) // OK: *os.File has Write method
+os.Stdout.Close()                // OK: *os.File has Close method
+
+var w io.Writer
+w = os.Stdout
+w.Write([]byte("hello")) // OK: io.Writer has Write method
+w.Close()                // compile error: io.Writer lacks Close method
+
+

一个有更多方法的接口类型,比如io.ReadWriter,和少一些方法的接口类型例如io.Reader,进行对比;更多方法的接口类型会告诉我们更多关于它的值持有的信息,并且对实现它的类型要求更加严格。那么关于interface{}类型,它没有任何方法,请讲出哪些具体的类型实现了它?

+

这看上去好像没有用,但实际上interface{}被称为空接口类型是不可或缺的。因为空接口类型对实现它的类型没有要求,所以我们可以将任意一个值赋给空接口类型。

+
var any interface{}
+any = true
+any = 12.34
+any = "hello"
+any = map[string]int{"one": 1}
+any = new(bytes.Buffer)
+
+

尽管不是很明显,从本书最早的例子中我们就已经在使用空接口类型。它允许像fmt.Println或者5.7章中的errorf函数接受任何类型的参数。

+

对于创建的一个interface{}值持有一个boolean,float,string,map,pointer,或者任意其它的类型;我们当然不能直接对它持有的值做操作,因为interface{}没有任何方法。我们会在7.10章中学到一种用类型断言来获取interface{}中值的方法。

+

因为接口与实现只依赖于判断两个类型的方法,所以没有必要定义一个具体类型和它实现的接口之间的关系。也就是说,有意地在文档里说明或者程序上断言这种关系偶尔是有用的,但程序上不强制这么做。下面的定义在编译期断言一个*bytes.Buffer的值实现了io.Writer接口类型:

+
// *bytes.Buffer must satisfy io.Writer
+var w io.Writer = new(bytes.Buffer)
+
+

因为任意*bytes.Buffer的值,甚至包括nil通过(*bytes.Buffer)(nil)进行显示的转换都实现了这个接口,所以我们不必分配一个新的变量。并且因为我们绝不会引用变量w,我们可以使用空标识符来进行代替。总的看,这些变化可以让我们得到一个更朴素的版本:

+
// *bytes.Buffer must satisfy io.Writer
+var _ io.Writer = (*bytes.Buffer)(nil)
+
+

非空的接口类型比如io.Writer经常被指针类型实现,尤其当一个或多个接口方法像Write方法那样隐式的给接收者带来变化的时候。一个结构体的指针是非常常见的承载方法的类型。

+

但是并不意味着只有指针类型满足接口类型,甚至连一些有设置方法的接口类型也可能会被Go语言中其它的引用类型实现。我们已经看过slice类型的方法(geometry.Path,§6.1)和map类型的方法(url.Values,§6.2.1),后面还会看到函数类型的方法的例子(http.HandlerFunc,§7.7)。甚至基本的类型也可能会实现一些接口;就如我们在7.4章中看到的time.Duration类型实现了fmt.Stringer接口。

+

一个具体的类型可能实现了很多不相关的接口。考虑在一个组织出售数字文化产品比如音乐,电影和书籍的程序中可能定义了下列的具体类型:

+
Album
+Book
+Movie
+Magazine
+Podcast
+TVEpisode
+Track
+
+

我们可以把每个抽象的特点用接口来表示。一些特性对于所有的这些文化产品都是共通的,例如标题,创作日期和作者列表。

+
type Artifact interface {
+	Title() string
+	Creators() []string
+	Created() time.Time
+}
+
+

其它的一些特性只对特定类型的文化产品才有。和文字排版特性相关的只有books和magazines,还有只有movies和TV剧集和屏幕分辨率相关。

+
type Text interface {
+	Pages() int
+	Words() int
+	PageSize() int
+}
+type Audio interface {
+	Stream() (io.ReadCloser, error)
+	RunningTime() time.Duration
+	Format() string // e.g., "MP3", "WAV"
+}
+type Video interface {
+	Stream() (io.ReadCloser, error)
+	RunningTime() time.Duration
+	Format() string // e.g., "MP4", "WMV"
+	Resolution() (x, y int)
+}
+
+

这些接口不止是一种有用的方式来分组相关的具体类型和表示他们之间的共同特点。我们后面可能会发现其它的分组。举例,如果我们发现我们需要以同样的方式处理Audio和Video,我们可以定义一个Streamer接口来代表它们之间相同的部分而不必对已经存在的类型做改变。

+
type Streamer interface {
+	Stream() (io.ReadCloser, error)
+	RunningTime() time.Duration
+	Format() string
+}
+
+

每一个具体类型的组基于它们相同的行为可以表示成一个接口类型。不像基于类的语言,他们一个类实现的接口集合需要进行显式的定义,在Go语言中我们可以在需要的时候定义一个新的抽象或者特定特点的组,而不需要修改具体类型的定义。当具体的类型来自不同的作者时这种方式会特别有用。当然也确实没有必要在具体的类型中指出这些共性。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch7/ch7-04.html b/ch7/ch7-04.html new file mode 100644 index 0000000..612b962 --- /dev/null +++ b/ch7/ch7-04.html @@ -0,0 +1,334 @@ + + + + + + flag.Value接口 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

7.4. flag.Value接口

+

在本章,我们会学到另一个标准的接口类型flag.Value是怎么帮助命令行标记定义新的符号的。思考下面这个会休眠特定时间的程序:

+

gopl.io/ch7/sleep

+
var period = flag.Duration("period", 1*time.Second, "sleep period")
+
+func main() {
+	flag.Parse()
+	fmt.Printf("Sleeping for %v...", *period)
+	time.Sleep(*period)
+	fmt.Println()
+}
+
+

在它休眠前它会打印出休眠的时间周期。fmt包调用time.Duration的String方法打印这个时间周期是以用户友好的注解方式,而不是一个纳秒数字:

+
$ go build gopl.io/ch7/sleep
+$ ./sleep
+Sleeping for 1s...
+
+

默认情况下,休眠周期是一秒,但是可以通过 -period 这个命令行标记来控制。flag.Duration函数创建一个time.Duration类型的标记变量并且允许用户通过多种用户友好的方式来设置这个变量的大小,这种方式还包括和String方法相同的符号排版形式。这种对称设计使得用户交互良好。

+
$ ./sleep -period 50ms
+Sleeping for 50ms...
+$ ./sleep -period 2m30s
+Sleeping for 2m30s...
+$ ./sleep -period 1.5h
+Sleeping for 1h30m0s...
+$ ./sleep -period "1 day"
+invalid value "1 day" for flag -period: time: invalid duration 1 day
+
+

因为时间周期标记值非常的有用,所以这个特性被构建到了flag包中;但是我们为我们自己的数据类型定义新的标记符号是简单容易的。我们只需要定义一个实现flag.Value接口的类型,如下:

+
package flag
+
+// Value is the interface to the value stored in a flag.
+type Value interface {
+	String() string
+	Set(string) error
+}
+
+

String方法格式化标记的值用在命令行帮助消息中;这样每一个flag.Value也是一个fmt.Stringer。Set方法解析它的字符串参数并且更新标记变量的值。实际上,Set方法和String是两个相反的操作,所以最好的办法就是对他们使用相同的注解方式。

+

让我们定义一个允许通过摄氏度或者华氏温度变换的形式指定温度的celsiusFlag类型。注意celsiusFlag内嵌了一个Celsius类型(§2.5),因此不用实现本身就已经有String方法了。为了实现flag.Value,我们只需要定义Set方法:

+

gopl.io/ch7/tempconv

+
// *celsiusFlag satisfies the flag.Value interface.
+type celsiusFlag struct{ Celsius }
+
+func (f *celsiusFlag) Set(s string) error {
+	var unit string
+	var value float64
+	fmt.Sscanf(s, "%f%s", &value, &unit) // no error check needed
+	switch unit {
+	case "C", "°C":
+		f.Celsius = Celsius(value)
+		return nil
+	case "F", "°F":
+		f.Celsius = FToC(Fahrenheit(value))
+		return nil
+	}
+	return fmt.Errorf("invalid temperature %q", s)
+}
+
+

调用fmt.Sscanf函数从输入s中解析一个浮点数(value)和一个字符串(unit)。虽然通常必须检查Sscanf的错误返回,但是在这个例子中我们不需要。因为如果有错误发生,就没有switch case会匹配到。

+

下面的CelsiusFlag函数将所有逻辑都封装在一起。它返回一个内嵌在celsiusFlag变量f中的Celsius指针给调用者。Celsius字段是一个会通过Set方法在标记处理的过程中更新的变量。调用Var方法将标记加入应用的命令行标记集合中,有异常复杂命令行接口的全局变量flag.CommandLine.Programs可能有几个这个类型的变量。调用Var方法将一个*celsiusFlag参数赋值给一个flag.Value参数,导致编译器去检查*celsiusFlag是否有必须的方法。

+
// CelsiusFlag defines a Celsius flag with the specified name,
+// default value, and usage, and returns the address of the flag variable.
+// The flag argument must have a quantity and a unit, e.g., "100C".
+func CelsiusFlag(name string, value Celsius, usage string) *Celsius {
+	f := celsiusFlag{value}
+	flag.CommandLine.Var(&f, name, usage)
+	return &f.Celsius
+}
+
+

现在我们可以开始在我们的程序中使用新的标记:

+

gopl.io/ch7/tempflag

+
var temp = tempconv.CelsiusFlag("temp", 20.0, "the temperature")
+
+func main() {
+	flag.Parse()
+	fmt.Println(*temp)
+}
+
+

下面是典型的场景:

+
$ go build gopl.io/ch7/tempflag
+$ ./tempflag
+20°C
+$ ./tempflag -temp -18C
+-18°C
+$ ./tempflag -temp 212°F
+100°C
+$ ./tempflag -temp 273.15K
+invalid value "273.15K" for flag -temp: invalid temperature "273.15K"
+Usage of ./tempflag:
+  -temp value
+        the temperature (default 20°C)
+$ ./tempflag -help
+Usage of ./tempflag:
+  -temp value
+        the temperature (default 20°C)
+
+

练习 7.6: 对tempFlag加入支持开尔文温度。

+

练习 7.7: 解释为什么帮助信息在它的默认值是20.0没有包含°C的情况下输出了°C。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch7/ch7-05.html b/ch7/ch7-05.html new file mode 100644 index 0000000..55c280f --- /dev/null +++ b/ch7/ch7-05.html @@ -0,0 +1,337 @@ + + + + + + 接口值 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

7.5. 接口值

+

概念上讲一个接口的值,接口值,由两个部分组成,一个具体的类型和那个类型的值。它们被称为接口的动态类型和动态值。对于像Go语言这种静态类型的语言,类型是编译期的概念;因此一个类型不是一个值。在我们的概念模型中,一些提供每个类型信息的值被称为类型描述符,比如类型的名称和方法。在一个接口值中,类型部分代表与之相关类型的描述符。

+

下面4个语句中,变量w得到了3个不同的值。(开始和最后的值是相同的)

+
var w io.Writer
+w = os.Stdout
+w = new(bytes.Buffer)
+w = nil
+
+

让我们进一步观察在每一个语句后的w变量的值和动态行为。第一个语句定义了变量w:

+
var w io.Writer
+
+

在Go语言中,变量总是被一个定义明确的值初始化,即使接口类型也不例外。对于一个接口的零值就是它的类型和值的部分都是nil(图7.1)。

+

+

一个接口值基于它的动态类型被描述为空或非空,所以这是一个空的接口值。你可以通过使用w==nil或者w!=nil来判断接口值是否为空。调用一个空接口值上的任意方法都会产生panic:

+
w.Write([]byte("hello")) // panic: nil pointer dereference
+
+

第二个语句将一个*os.File类型的值赋给变量w:

+
w = os.Stdout
+
+

这个赋值过程调用了一个具体类型到接口类型的隐式转换,这和显式的使用io.Writer(os.Stdout)是等价的。这类转换不管是显式的还是隐式的,都会刻画出操作到的类型和值。这个接口值的动态类型被设为*os.File指针的类型描述符,它的动态值持有os.Stdout的拷贝;这是一个代表处理标准输出的os.File类型变量的指针(图7.2)。

+

+

调用一个包含*os.File类型指针的接口值的Write方法,使得(*os.File).Write方法被调用。这个调用输出“hello”。

+
w.Write([]byte("hello")) // "hello"
+
+

通常在编译期,我们不知道接口值的动态类型是什么,所以一个接口上的调用必须使用动态分配。因为不是直接进行调用,所以编译器必须把代码生成在类型描述符的方法Write上,然后间接调用那个地址。这个调用的接收者是一个接口动态值的拷贝,os.Stdout。效果和下面这个直接调用一样:

+
os.Stdout.Write([]byte("hello")) // "hello"
+
+

第三个语句给接口值赋了一个*bytes.Buffer类型的值

+
w = new(bytes.Buffer)
+
+

现在动态类型是*bytes.Buffer并且动态值是一个指向新分配的缓冲区的指针(图7.3)。

+

+

Write方法的调用也使用了和之前一样的机制:

+
w.Write([]byte("hello")) // writes "hello" to the bytes.Buffers
+
+

这次类型描述符是*bytes.Buffer,所以调用了(*bytes.Buffer).Write方法,并且接收者是该缓冲区的地址。这个调用把字符串“hello”添加到缓冲区中。

+

最后,第四个语句将nil赋给了接口值:

+
w = nil
+
+

这个重置将它所有的部分都设为nil值,把变量w恢复到和它之前定义时相同的状态,在图7.1中可以看到。

+

一个接口值可以持有任意大的动态值。例如,表示时间实例的time.Time类型,这个类型有几个对外不公开的字段。我们从它上面创建一个接口值:

+
var x interface{} = time.Now()
+
+

结果可能和图7.4相似。从概念上讲,不论接口值多大,动态值总是可以容下它。(这只是一个概念上的模型;具体的实现可能会非常不同)

+

+

接口值可以使用==和!=来进行比较。两个接口值相等仅当它们都是nil值,或者它们的动态类型相同并且动态值也根据这个动态类型的==操作相等。因为接口值是可比较的,所以它们可以用在map的键或者作为switch语句的操作数。

+

然而,如果两个接口值的动态类型相同,但是这个动态类型是不可比较的(比如切片),将它们进行比较就会失败并且panic:

+
var x interface{} = []int{1, 2, 3}
+fmt.Println(x == x) // panic: comparing uncomparable type []int
+
+

考虑到这点,接口类型是非常与众不同的。其它类型要么是安全的可比较类型(如基本类型和指针)要么是完全不可比较的类型(如切片,映射类型,和函数),但是在比较接口值或者包含了接口值的聚合类型时,我们必须要意识到潜在的panic。同样的风险也存在于使用接口作为map的键或者switch的操作数。只能比较你非常确定它们的动态值是可比较类型的接口值。

+

当我们处理错误或者调试的过程中,得知接口值的动态类型是非常有帮助的。所以我们使用fmt包的%T动作:

+
var w io.Writer
+fmt.Printf("%T\n", w) // "<nil>"
+w = os.Stdout
+fmt.Printf("%T\n", w) // "*os.File"
+w = new(bytes.Buffer)
+fmt.Printf("%T\n", w) // "*bytes.Buffer"
+
+

在fmt包内部,使用反射来获取接口动态类型的名称。我们会在第12章中学到反射相关的知识。

+

7.5.1. 警告:一个包含nil指针的接口不是nil接口

+

一个不包含任何值的nil接口值和一个刚好包含nil指针的接口值是不同的。这个细微区别产生了一个容易绊倒每个Go程序员的陷阱。

+

思考下面的程序。当debug变量设置为true时,main函数会将f函数的输出收集到一个bytes.Buffer类型中。

+
const debug = true
+
+func main() {
+	var buf *bytes.Buffer
+	if debug {
+		buf = new(bytes.Buffer) // enable collection of output
+	}
+	f(buf) // NOTE: subtly incorrect!
+	if debug {
+		// ...use buf...
+	}
+}
+
+// If out is non-nil, output will be written to it.
+func f(out io.Writer) {
+	// ...do something...
+	if out != nil {
+		out.Write([]byte("done!\n"))
+	}
+}
+
+

我们可能会预计当把变量debug设置为false时可以禁止对输出的收集,但是实际上在out.Write方法调用时程序发生了panic:

+
if out != nil {
+	out.Write([]byte("done!\n")) // panic: nil pointer dereference
+}
+
+

当main函数调用函数f时,它给f函数的out参数赋了一个*bytes.Buffer的空指针,所以out的动态值是nil。然而,它的动态类型是*bytes.Buffer,意思就是out变量是一个包含空指针值的非空接口(如图7.5),所以防御性检查out!=nil的结果依然是true。

+

+

动态分配机制依然决定(*bytes.Buffer).Write的方法会被调用,但是这次的接收者的值是nil。对于一些如*os.File的类型,nil是一个有效的接收者(§6.2.1),但是*bytes.Buffer类型不在这些种类中。这个方法会被调用,但是当它尝试去获取缓冲区时会发生panic。

+

问题在于尽管一个nil的*bytes.Buffer指针有实现这个接口的方法,它也不满足这个接口具体的行为上的要求。特别是这个调用违反了(*bytes.Buffer).Write方法的接收者非空的隐含先觉条件,所以将nil指针赋给这个接口是错误的。解决方案就是将main函数中的变量buf的类型改为io.Writer,因此可以避免一开始就将一个不完整的值赋值给这个接口:

+
var buf io.Writer
+if debug {
+	buf = new(bytes.Buffer) // enable collection of output
+}
+f(buf) // OK
+
+

现在我们已经把接口值的技巧都讲完了,让我们来看更多的一些在Go标准库中的重要接口类型。在下面的三章中,我们会看到接口类型是怎样用在排序,web服务,错误处理中的。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch7/ch7-06.html b/ch7/ch7-06.html new file mode 100644 index 0000000..3f4465c --- /dev/null +++ b/ch7/ch7-06.html @@ -0,0 +1,397 @@ + + + + + + sort.Interface接口 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

7.6. sort.Interface接口

+

排序操作和字符串格式化一样是很多程序经常使用的操作。尽管一个最短的快排程序只要15行就可以搞定,但是一个健壮的实现需要更多的代码,并且我们不希望每次我们需要的时候都重写或者拷贝这些代码。

+

幸运的是,sort包内置的提供了根据一些排序函数来对任何序列排序的功能。它的设计非常独到。在很多语言中,排序算法都是和序列数据类型关联,同时排序函数和具体类型元素关联。相比之下,Go语言的sort.Sort函数不会对具体的序列和它的元素做任何假设。相反,它使用了一个接口类型sort.Interface来指定通用的排序算法和可能被排序到的序列类型之间的约定。这个接口的实现由序列的具体表示和它希望排序的元素决定,序列的表示经常是一个切片。

+

一个内置的排序算法需要知道三个东西:序列的长度,表示两个元素比较的结果,一种交换两个元素的方式;这就是sort.Interface的三个方法:

+
package sort
+
+type Interface interface {
+	Len() int
+	Less(i, j int) bool // i, j are indices of sequence elements
+	Swap(i, j int)
+}
+
+

为了对序列进行排序,我们需要定义一个实现了这三个方法的类型,然后对这个类型的一个实例应用sort.Sort函数。思考对一个字符串切片进行排序,这可能是最简单的例子了。下面是这个新的类型StringSlice和它的Len,Less和Swap方法

+
type StringSlice []string
+func (p StringSlice) Len() int           { return len(p) }
+func (p StringSlice) Less(i, j int) bool { return p[i] < p[j] }
+func (p StringSlice) Swap(i, j int)      { p[i], p[j] = p[j], p[i] }
+
+

现在我们可以通过像下面这样将一个切片转换为一个StringSlice类型来进行排序:

+
sort.Sort(StringSlice(names))
+
+

这个转换得到一个相同长度,容量,和基于names数组的切片值;并且这个切片值的类型有三个排序需要的方法。

+

对字符串切片的排序是很常用的需要,所以sort包提供了StringSlice类型,也提供了Strings函数能让上面这些调用简化成sort.Strings(names)。

+

这里用到的技术很容易适用到其它排序序列中,例如我们可以忽略大小写或者含有的特殊字符。(本书使用Go程序对索引词和页码进行排序也用到了这个技术,对罗马数字做了额外逻辑处理。)对于更复杂的排序,我们使用相同的方法,但是会用更复杂的数据结构和更复杂地实现sort.Interface的方法。

+

我们会运行上面的例子来对一个表格中的音乐播放列表进行排序。每个track都是单独的一行,每一列都是这个track的属性像艺术家,标题,和运行时间。想象一个图形用户界面来呈现这个表格,并且点击一个属性的顶部会使这个列表按照这个属性进行排序;再一次点击相同属性的顶部会进行逆向排序。让我们看下每个点击会发生什么响应。

+

下面的变量tracks包含了一个播放列表。(One of the authors apologizes for the other author’s musical tastes.)每个元素都不是Track本身而是指向它的指针。尽管我们在下面的代码中直接存储Tracks也可以工作,sort函数会交换很多对元素,所以如果每个元素都是指针而不是Track类型会更快,指针是一个机器字码长度而Track类型可能是八个或更多。

+

gopl.io/ch7/sorting

+
type Track struct {
+	Title  string
+	Artist string
+	Album  string
+	Year   int
+	Length time.Duration
+}
+
+var tracks = []*Track{
+	{"Go", "Delilah", "From the Roots Up", 2012, length("3m38s")},
+	{"Go", "Moby", "Moby", 1992, length("3m37s")},
+	{"Go Ahead", "Alicia Keys", "As I Am", 2007, length("4m36s")},
+	{"Ready 2 Go", "Martin Solveig", "Smash", 2011, length("4m24s")},
+}
+
+func length(s string) time.Duration {
+	d, err := time.ParseDuration(s)
+	if err != nil {
+		panic(s)
+	}
+	return d
+}
+
+

printTracks函数将播放列表打印成一个表格。一个图形化的展示可能会更好点,但是这个小程序使用text/tabwriter包来生成一个列整齐对齐和隔开的表格,像下面展示的这样。注意到*tabwriter.Writer是满足io.Writer接口的。它会收集每一片写向它的数据;它的Flush方法会格式化整个表格并且将它写向os.Stdout(标准输出)。

+
func printTracks(tracks []*Track) {
+	const format = "%v\t%v\t%v\t%v\t%v\t\n"
+	tw := new(tabwriter.Writer).Init(os.Stdout, 0, 8, 2, ' ', 0)
+	fmt.Fprintf(tw, format, "Title", "Artist", "Album", "Year", "Length")
+	fmt.Fprintf(tw, format, "-----", "------", "-----", "----", "------")
+	for _, t := range tracks {
+		fmt.Fprintf(tw, format, t.Title, t.Artist, t.Album, t.Year, t.Length)
+	}
+	tw.Flush() // calculate column widths and print table
+}
+
+

为了能按照Artist字段对播放列表进行排序,我们会像对StringSlice那样定义一个新的带有必须的Len,Less和Swap方法的切片类型。

+
type byArtist []*Track
+func (x byArtist) Len() int           { return len(x) }
+func (x byArtist) Less(i, j int) bool { return x[i].Artist < x[j].Artist }
+func (x byArtist) Swap(i, j int)      { x[i], x[j] = x[j], x[i] }
+
+

为了调用通用的排序程序,我们必须先将tracks转换为新的byArtist类型,它定义了具体的排序:

+
sort.Sort(byArtist(tracks))
+
+

在按照artist对这个切片进行排序后,printTrack的输出如下

+
Title       Artist          Album               Year Length
+-----       ------          -----               ---- ------
+Go Ahead    Alicia Keys     As I Am             2007 4m36s
+Go          Delilah         From the Roots Up   2012 3m38s
+Ready 2 Go  Martin Solveig  Smash               2011 4m24s
+Go          Moby            Moby                1992 3m37s
+
+

如果用户第二次请求“按照artist排序”,我们会对tracks进行逆向排序。然而我们不需要定义一个有颠倒Less方法的新类型byReverseArtist,因为sort包中提供了Reverse函数将排序顺序转换成逆序。

+
sort.Sort(sort.Reverse(byArtist(tracks)))
+
+

在按照artist对这个切片进行逆向排序后,printTrack的输出如下

+
Title       Artist          Album               Year Length
+-----       ------          -----               ---- ------
+Go          Moby            Moby                1992 3m37s
+Ready 2 Go  Martin Solveig  Smash               2011 4m24s
+Go          Delilah         From the Roots Up   2012 3m38s
+Go Ahead    Alicia Keys     As I Am             2007 4m36s
+
+

sort.Reverse函数值得进行更近一步的学习,因为它使用了(§6.3)章中的组合,这是一个重要的思路。sort包定义了一个不公开的struct类型reverse,它嵌入了一个sort.Interface。reverse的Less方法调用了内嵌的sort.Interface值的Less方法,但是通过交换索引的方式使排序结果变成逆序。

+
package sort
+
+type reverse struct{ Interface } // that is, sort.Interface
+
+func (r reverse) Less(i, j int) bool { return r.Interface.Less(j, i) }
+
+func Reverse(data Interface) Interface { return reverse{data} }
+
+

reverse的另外两个方法Len和Swap隐式地由原有内嵌的sort.Interface提供。因为reverse是一个不公开的类型,所以导出函数Reverse返回一个包含原有sort.Interface值的reverse类型实例。

+

为了可以按照不同的列进行排序,我们必须定义一个新的类型例如byYear:

+
type byYear []*Track
+func (x byYear) Len() int           { return len(x) }
+func (x byYear) Less(i, j int) bool { return x[i].Year < x[j].Year }
+func (x byYear) Swap(i, j int)      { x[i], x[j] = x[j], x[i] }
+
+

在使用sort.Sort(byYear(tracks))按照年对tracks进行排序后,printTrack展示了一个按时间先后顺序的列表:

+
Title       Artist          Album               Year Length
+-----       ------          -----               ---- ------
+Go          Moby            Moby                1992 3m37s
+Go Ahead    Alicia Keys     As I Am             2007 4m36s
+Ready 2 Go  Martin Solveig  Smash               2011 4m24s
+Go          Delilah         From the Roots Up   2012 3m38s
+
+

对于我们需要的每个切片元素类型和每个排序函数,我们需要定义一个新的sort.Interface实现。如你所见,Len和Swap方法对于所有的切片类型都有相同的定义。下个例子,具体的类型customSort会将一个切片和函数结合,使我们只需要写比较函数就可以定义一个新的排序。顺便说下,实现了sort.Interface的具体类型不一定是切片类型;customSort是一个结构体类型。

+
type customSort struct {
+	t    []*Track
+	less func(x, y *Track) bool
+}
+
+func (x customSort) Len() int           { return len(x.t) }
+func (x customSort) Less(i, j int) bool { return x.less(x.t[i], x.t[j]) }
+func (x customSort) Swap(i, j int)	{ x.t[i], x.t[j] = x.t[j], x.t[i] }
+
+

让我们定义一个多层的排序函数,它主要的排序键是标题,第二个键是年,第三个键是运行时间Length。下面是该排序的调用,其中这个排序使用了匿名排序函数:

+
sort.Sort(customSort{tracks, func(x, y *Track) bool {
+	if x.Title != y.Title {
+		return x.Title < y.Title
+	}
+	if x.Year != y.Year {
+		return x.Year < y.Year
+	}
+	if x.Length != y.Length {
+		return x.Length < y.Length
+	}
+	return false
+}})
+
+

这下面是排序的结果。注意到两个标题是“Go”的track按照标题排序是相同的顺序,但是在按照year排序上更久的那个track优先。

+
Title       Artist          Album               Year Length
+-----       ------          -----               ---- ------
+Go          Moby            Moby                1992 3m37s
+Go          Delilah         From the Roots Up   2012 3m38s
+Go Ahead    Alicia Keys     As I Am             2007 4m36s
+Ready 2 Go  Martin Solveig  Smash               2011 4m24s
+
+

尽管对长度为n的序列排序需要 O(n log n)次比较操作,检查一个序列是否已经有序至少需要n-1次比较。sort包中的IsSorted函数帮我们做这样的检查。像sort.Sort一样,它也使用sort.Interface对这个序列和它的排序函数进行抽象,但是它从不会调用Swap方法:这段代码示范了IntsAreSorted和Ints函数在IntSlice类型上的使用:

+
values := []int{3, 1, 4, 1}
+fmt.Println(sort.IntsAreSorted(values)) // "false"
+sort.Ints(values)
+fmt.Println(values)                     // "[1 1 3 4]"
+fmt.Println(sort.IntsAreSorted(values)) // "true"
+sort.Sort(sort.Reverse(sort.IntSlice(values)))
+fmt.Println(values)                     // "[4 3 1 1]"
+fmt.Println(sort.IntsAreSorted(values)) // "false"
+
+

为了使用方便,sort包为[]int、[]string和[]float64的正常排序提供了特定版本的函数和类型。对于其他类型,例如[]int64或者[]uint,尽管路径也很简单,还是依赖我们自己实现。

+

练习 7.8: 很多图形界面提供了一个有状态的多重排序表格插件:主要的排序键是最近一次点击过列头的列,第二个排序键是第二最近点击过列头的列,等等。定义一个sort.Interface的实现用在这样的表格中。比较这个实现方式和重复使用sort.Stable来排序的方式。

+

练习 7.9: 使用html/template包(§4.6)替代printTracks将tracks展示成一个HTML表格。将这个解决方案用在前一个练习中,让每次点击一个列的头部产生一个HTTP请求来排序这个表格。

+

练习 7.10: sort.Interface类型也可以适用在其它地方。编写一个IsPalindrome(s sort.Interface) bool函数表明序列s是否是回文序列,换句话说反向排序不会改变这个序列。假设如果!s.Less(i, j) && !s.Less(j, i)则索引i和j上的元素相等。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch7/ch7-07.html b/ch7/ch7-07.html new file mode 100644 index 0000000..8aa1ec6 --- /dev/null +++ b/ch7/ch7-07.html @@ -0,0 +1,388 @@ + + + + + + http.Handler接口 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

7.7. http.Handler接口

+

在第一章中,我们粗略的了解了怎么用net/http包去实现网络客户端(§1.5)和服务器(§1.7)。在这个小节中,我们会对那些基于http.Handler接口的服务器API做更进一步的学习:

+

net/http

+
package http
+
+type Handler interface {
+	ServeHTTP(w ResponseWriter, r *Request)
+}
+
+func ListenAndServe(address string, h Handler) error
+
+

ListenAndServe函数需要一个例如“localhost:8000”的服务器地址,和一个所有请求都可以分派的Handler接口实例。它会一直运行,直到这个服务因为一个错误而失败(或者启动失败),它的返回值一定是一个非空的错误。

+

想象一个电子商务网站,为了销售,将数据库中物品的价格映射成美元。下面这个程序可能是能想到的最简单的实现了。它将库存清单模型化为一个命名为database的map类型,我们给这个类型一个ServeHttp方法,这样它可以满足http.Handler接口。这个handler会遍历整个map并输出物品信息。

+

gopl.io/ch7/http1

+
func main() {
+	db := database{"shoes": 50, "socks": 5}
+	log.Fatal(http.ListenAndServe("localhost:8000", db))
+}
+
+type dollars float32
+
+func (d dollars) String() string { return fmt.Sprintf("$%.2f", d) }
+
+type database map[string]dollars
+
+func (db database) ServeHTTP(w http.ResponseWriter, req *http.Request) {
+	for item, price := range db {
+		fmt.Fprintf(w, "%s: %s\n", item, price)
+	}
+}
+
+

如果我们启动这个服务,

+
$ go build gopl.io/ch7/http1
+$ ./http1 &
+
+

然后用1.5节中的获取程序(如果你更喜欢可以使用web浏览器)来连接服务器,我们得到下面的输出:

+
$ go build gopl.io/ch1/fetch
+$ ./fetch http://localhost:8000
+shoes: $50.00
+socks: $5.00
+
+

目前为止,这个服务器不考虑URL,只能为每个请求列出它全部的库存清单。更真实的服务器会定义多个不同的URL,每一个都会触发一个不同的行为。让我们使用/list来调用已经存在的这个行为并且增加另一个/price调用表明单个货品的价格,像这样/price?item=socks来指定一个请求参数。

+

gopl.io/ch7/http2

+
func (db database) ServeHTTP(w http.ResponseWriter, req *http.Request) {
+	switch req.URL.Path {
+	case "/list":
+		for item, price := range db {
+			fmt.Fprintf(w, "%s: %s\n", item, price)
+		}
+	case "/price":
+		item := req.URL.Query().Get("item")
+		price, ok := db[item]
+		if !ok {
+			w.WriteHeader(http.StatusNotFound) // 404
+			fmt.Fprintf(w, "no such item: %q\n", item)
+			return
+		}
+		fmt.Fprintf(w, "%s\n", price)
+	default:
+		w.WriteHeader(http.StatusNotFound) // 404
+		fmt.Fprintf(w, "no such page: %s\n", req.URL)
+	}
+}
+
+

现在handler基于URL的路径部分(req.URL.Path)来决定执行什么逻辑。如果这个handler不能识别这个路径,它会通过调用w.WriteHeader(http.StatusNotFound)返回客户端一个HTTP错误;这个检查应该在向w写入任何值前完成。(顺便提一下,http.ResponseWriter是另一个接口。它在io.Writer上增加了发送HTTP相应头的方法。)等效地,我们可以使用实用的http.Error函数:

+
msg := fmt.Sprintf("no such page: %s\n", req.URL)
+http.Error(w, msg, http.StatusNotFound) // 404
+
+

/price的case会调用URL的Query方法来将HTTP请求参数解析为一个map,或者更准确地说一个net/url包中url.Values(§6.2.1)类型的多重映射。然后找到第一个item参数并查找它的价格。如果这个货品没有找到会返回一个错误。

+

这里是一个和新服务器会话的例子:

+
$ go build gopl.io/ch7/http2
+$ go build gopl.io/ch1/fetch
+$ ./http2 &
+$ ./fetch http://localhost:8000/list
+shoes: $50.00
+socks: $5.00
+$ ./fetch http://localhost:8000/price?item=socks
+$5.00
+$ ./fetch http://localhost:8000/price?item=shoes
+$50.00
+$ ./fetch http://localhost:8000/price?item=hat
+no such item: "hat"
+$ ./fetch http://localhost:8000/help
+no such page: /help
+
+

显然我们可以继续向ServeHTTP方法中添加case,但在一个实际的应用中,将每个case中的逻辑定义到一个分开的方法或函数中会很实用。此外,相近的URL可能需要相似的逻辑;例如几个图片文件可能有形如/images/*.png的URL。因为这些原因,net/http包提供了一个请求多路器ServeMux来简化URL和handlers的联系。一个ServeMux将一批http.Handler聚集到一个单一的http.Handler中。再一次,我们可以看到满足同一接口的不同类型是可替换的:web服务器将请求指派给任意的http.Handler +而不需要考虑它后面的具体类型。

+

对于更复杂的应用,一些ServeMux可以通过组合来处理更加错综复杂的路由需求。Go语言目前没有一个权威的web框架,就像Ruby语言有Rails和python有Django。这并不是说这样的框架不存在,而是Go语言标准库中的构建模块就已经非常灵活以至于这些框架都是不必要的。此外,尽管在一个项目早期使用框架是非常方便的,但是它们带来额外的复杂度会使长期的维护更加困难。

+

在下面的程序中,我们创建一个ServeMux并且使用它将URL和相应处理/list和/price操作的handler联系起来,这些操作逻辑都已经被分到不同的方法中。然后我们在调用ListenAndServe函数中使用ServeMux为主要的handler。

+

gopl.io/ch7/http3

+
func main() {
+	db := database{"shoes": 50, "socks": 5}
+	mux := http.NewServeMux()
+	mux.Handle("/list", http.HandlerFunc(db.list))
+	mux.Handle("/price", http.HandlerFunc(db.price))
+	log.Fatal(http.ListenAndServe("localhost:8000", mux))
+}
+
+type database map[string]dollars
+
+func (db database) list(w http.ResponseWriter, req *http.Request) {
+	for item, price := range db {
+		fmt.Fprintf(w, "%s: %s\n", item, price)
+	}
+}
+
+func (db database) price(w http.ResponseWriter, req *http.Request) {
+	item := req.URL.Query().Get("item")
+	price, ok := db[item]
+	if !ok {
+		w.WriteHeader(http.StatusNotFound) // 404
+		fmt.Fprintf(w, "no such item: %q\n", item)
+		return
+	}
+	fmt.Fprintf(w, "%s\n", price)
+}
+
+

让我们关注这两个注册到handlers上的调用。第一个db.list是一个方法值(§6.4),它是下面这个类型的值。

+
func(w http.ResponseWriter, req *http.Request)
+
+

也就是说db.list的调用会援引一个接收者是db的database.list方法。所以db.list是一个实现了handler类似行为的函数,但是因为它没有方法(理解:该方法没有它自己的方法),所以它不满足http.Handler接口并且不能直接传给mux.Handle。

+

语句http.HandlerFunc(db.list)是一个转换而非一个函数调用,因为http.HandlerFunc是一个类型。它有如下的定义:

+

net/http

+
package http
+
+type HandlerFunc func(w ResponseWriter, r *Request)
+
+func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
+	f(w, r)
+}
+
+

HandlerFunc显示了在Go语言接口机制中一些不同寻常的特点。这是一个实现了接口http.Handler的方法的函数类型。ServeHTTP方法的行为是调用了它的函数本身。因此HandlerFunc是一个让函数值满足一个接口的适配器,这里函数和这个接口仅有的方法有相同的函数签名。实际上,这个技巧让一个单一的类型例如database以多种方式满足http.Handler接口:一种通过它的list方法,一种通过它的price方法等等。

+

因为handler通过这种方式注册非常普遍,ServeMux有一个方便的HandleFunc方法,它帮我们简化handler注册代码成这样:

+

gopl.io/ch7/http3a

+
mux.HandleFunc("/list", db.list)
+mux.HandleFunc("/price", db.price)
+
+

从上面的代码很容易看出应该怎么构建一个程序:由两个不同的web服务器监听不同的端口,并且定义不同的URL将它们指派到不同的handler。我们只要构建另外一个ServeMux并且再调用一次ListenAndServe(可能并行的)。但是在大多数程序中,一个web服务器就足够了。此外,在一个应用程序的多个文件中定义HTTP handler也是非常典型的,如果它们必须全部都显式地注册到这个应用的ServeMux实例上会比较麻烦。

+

所以为了方便,net/http包提供了一个全局的ServeMux实例DefaultServerMux和包级别的http.Handle和http.HandleFunc函数。现在,为了使用DefaultServeMux作为服务器的主handler,我们不需要将它传给ListenAndServe函数;nil值就可以工作。

+

然后服务器的主函数可以简化成:

+

gopl.io/ch7/http4

+
func main() {
+	db := database{"shoes": 50, "socks": 5}
+	http.HandleFunc("/list", db.list)
+	http.HandleFunc("/price", db.price)
+	log.Fatal(http.ListenAndServe("localhost:8000", nil))
+}
+
+

最后,一个重要的提示:就像我们在1.7节中提到的,web服务器在一个新的协程中调用每一个handler,所以当handler获取其它协程或者这个handler本身的其它请求也可以访问到变量时,一定要使用预防措施,比如锁机制。我们后面的两章中将讲到并发相关的知识。

+

练习 7.11: 增加额外的handler让客户端可以创建,读取,更新和删除数据库记录。例如,一个形如 /update?item=socks&price=6 的请求会更新库存清单里一个货品的价格并且当这个货品不存在或价格无效时返回一个错误值。(注意:这个修改会引入变量同时更新的问题)

+

练习 7.12: 修改/list的handler让它把输出打印成一个HTML的表格而不是文本。html/template包(§4.6)可能会对你有帮助。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch7/ch7-08.html b/ch7/ch7-08.html new file mode 100644 index 0000000..ef63900 --- /dev/null +++ b/ch7/ch7-08.html @@ -0,0 +1,291 @@ + + + + + + error接口 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

7.8. error接口

+

从本书的开始,我们就已经创建和使用过神秘的预定义error类型,而且没有解释它究竟是什么。实际上它就是interface类型,这个类型有一个返回错误信息的单一方法:

+
type error interface {
+	Error() string
+}
+
+

创建一个error最简单的方法就是调用errors.New函数,它会根据传入的错误信息返回一个新的error。整个errors包仅只有4行:

+
package errors
+
+func New(text string) error { return &errorString{text} }
+
+type errorString struct { text string }
+
+func (e *errorString) Error() string { return e.text }
+
+

承载errorString的类型是一个结构体而非一个字符串,这是为了保护它表示的错误避免粗心(或有意)的更新。并且因为是指针类型*errorString满足error接口而非errorString类型,所以每个New函数的调用都分配了一个独特的和其他错误不相同的实例。我们也不想要重要的error例如io.EOF和一个刚好有相同错误消息的error比较后相等。

+
fmt.Println(errors.New("EOF") == errors.New("EOF")) // "false"
+
+

调用errors.New函数是非常稀少的,因为有一个方便的封装函数fmt.Errorf,它还会处理字符串格式化。我们曾多次在第5章中用到它。

+
package fmt
+
+import "errors"
+
+func Errorf(format string, args ...interface{}) error {
+	return errors.New(Sprintf(format, args...))
+}
+
+

虽然*errorString可能是最简单的错误类型,但远非只有它一个。例如,syscall包提供了Go语言底层系统调用API。在多个平台上,它定义一个实现error接口的数字类型Errno,并且在Unix平台上,Errno的Error方法会从一个字符串表中查找错误消息,如下面展示的这样:

+
package syscall
+
+type Errno uintptr // operating system error code
+
+var errors = [...]string{
+	1:   "operation not permitted",   // EPERM
+	2:   "no such file or directory", // ENOENT
+	3:   "no such process",           // ESRCH
+	// ...
+}
+
+func (e Errno) Error() string {
+	if 0 <= int(e) && int(e) < len(errors) {
+		return errors[e]
+	}
+	return fmt.Sprintf("errno %d", e)
+}
+
+

下面的语句创建了一个持有Errno值为2的接口值,表示POSIX ENOENT状况:

+
var err error = syscall.Errno(2)
+fmt.Println(err.Error()) // "no such file or directory"
+fmt.Println(err)         // "no such file or directory"
+
+

err的值图形化的呈现在图7.6中。

+

+

Errno是一个系统调用错误的高效表示方式,它通过一个有限的集合进行描述,并且它满足标准的错误接口。我们会在第7.11节了解到其它满足这个接口的类型。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch7/ch7-09.html b/ch7/ch7-09.html new file mode 100644 index 0000000..8cb2d12 --- /dev/null +++ b/ch7/ch7-09.html @@ -0,0 +1,491 @@ + + + + + + 示例: 表达式求值 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

7.9. 示例: 表达式求值

+

在本节中,我们会构建一个简单算术表达式的求值器。我们将使用一个接口Expr来表示Go语言中任意的表达式。现在这个接口不需要有方法,但是我们后面会为它增加一些。

+
// An Expr is an arithmetic expression.
+type Expr interface{}
+
+

我们的表达式语言包括浮点数符号(小数点);二元操作符+,-,*, 和/;一元操作符-x和+x;调用pow(x,y),sin(x),和sqrt(x)的函数;例如x和pi的变量;当然也有括号和标准的优先级运算符。所有的值都是float64类型。这下面是一些表达式的例子:

+
sqrt(A / pi)
+pow(x, 3) + pow(y, 3)
+(F - 32) * 5 / 9
+
+

下面的五个具体类型表示了具体的表达式类型。Var类型表示对一个变量的引用。(我们很快会知道为什么它可以被输出。)literal类型表示一个浮点型常量。unary和binary类型表示有一到两个运算对象的运算符表达式,这些操作数可以是任意的Expr类型。call类型表示对一个函数的调用;我们限制它的fn字段只能是pow,sin或者sqrt。

+

gopl.io/ch7/eval

+
// A Var identifies a variable, e.g., x.
+type Var string
+
+// A literal is a numeric constant, e.g., 3.141.
+type literal float64
+
+// A unary represents a unary operator expression, e.g., -x.
+type unary struct {
+	op rune // one of '+', '-'
+	x  Expr
+}
+
+// A binary represents a binary operator expression, e.g., x+y.
+type binary struct {
+	op   rune // one of '+', '-', '*', '/'
+	x, y Expr
+}
+
+// A call represents a function call expression, e.g., sin(x).
+type call struct {
+	fn   string // one of "pow", "sin", "sqrt"
+	args []Expr
+}
+
+

为了计算一个包含变量的表达式,我们需要一个environment变量将变量的名字映射成对应的值:

+
type Env map[Var]float64
+
+

我们也需要每个表达式去定义一个Eval方法,这个方法会根据给定的environment变量返回表达式的值。因为每个表达式都必须提供这个方法,我们将它加入到Expr接口中。这个包只会对外公开Expr,Env,和Var类型。调用方不需要获取其它的表达式类型就可以使用这个求值器。

+
type Expr interface {
+	// Eval returns the value of this Expr in the environment env.
+	Eval(env Env) float64
+}
+
+

下面给大家展示一个具体的Eval方法。Var类型的这个方法对一个environment变量进行查找,如果这个变量没有在environment中定义过这个方法会返回一个零值,literal类型的这个方法简单的返回它真实的值。

+
func (v Var) Eval(env Env) float64 {
+	return env[v]
+}
+
+func (l literal) Eval(_ Env) float64 {
+	return float64(l)
+}
+
+

unary和binary的Eval方法会递归的计算它的运算对象,然后将运算符op作用到它们上。我们不将被零或无穷数除作为一个错误,因为它们都会产生一个固定的结果——无限。最后,call的这个方法会计算对于pow,sin,或者sqrt函数的参数值,然后调用对应在math包中的函数。

+
func (u unary) Eval(env Env) float64 {
+	switch u.op {
+	case '+':
+		return +u.x.Eval(env)
+	case '-':
+		return -u.x.Eval(env)
+	}
+	panic(fmt.Sprintf("unsupported unary operator: %q", u.op))
+}
+
+func (b binary) Eval(env Env) float64 {
+	switch b.op {
+	case '+':
+		return b.x.Eval(env) + b.y.Eval(env)
+	case '-':
+		return b.x.Eval(env) - b.y.Eval(env)
+	case '*':
+		return b.x.Eval(env) * b.y.Eval(env)
+	case '/':
+		return b.x.Eval(env) / b.y.Eval(env)
+	}
+	panic(fmt.Sprintf("unsupported binary operator: %q", b.op))
+}
+
+func (c call) Eval(env Env) float64 {
+	switch c.fn {
+	case "pow":
+		return math.Pow(c.args[0].Eval(env), c.args[1].Eval(env))
+	case "sin":
+		return math.Sin(c.args[0].Eval(env))
+	case "sqrt":
+		return math.Sqrt(c.args[0].Eval(env))
+	}
+	panic(fmt.Sprintf("unsupported function call: %s", c.fn))
+}
+
+

一些方法会失败。例如,一个call表达式可能有未知的函数或者错误的参数个数。用一个无效的运算符如!或者<去构建一个unary或者binary表达式也是可能会发生的(尽管下面提到的Parse函数不会这样做)。这些错误会让Eval方法panic。其它的错误,像计算一个没有在environment变量中出现过的Var,只会让Eval方法返回一个错误的结果。所有的这些错误都可以通过在计算前检查Expr来发现。这是我们接下来要讲的Check方法的工作,但是让我们先测试Eval方法。

+

下面的TestEval函数是对evaluator的一个测试。它使用了我们会在第11章讲解的testing包,但是现在知道调用t.Errof会报告一个错误就足够了。这个函数循环遍历一个表格中的输入,这个表格中定义了三个表达式和针对每个表达式不同的环境变量。第一个表达式根据给定圆的面积A计算它的半径,第二个表达式通过两个变量x和y计算两个立方体的体积之和,第三个表达式将华氏温度F转换成摄氏度。

+
func TestEval(t *testing.T) {
+	tests := []struct {
+		expr string
+		env  Env
+		want string
+	}{
+		{"sqrt(A / pi)", Env{"A": 87616, "pi": math.Pi}, "167"},
+		{"pow(x, 3) + pow(y, 3)", Env{"x": 12, "y": 1}, "1729"},
+		{"pow(x, 3) + pow(y, 3)", Env{"x": 9, "y": 10}, "1729"},
+		{"5 / 9 * (F - 32)", Env{"F": -40}, "-40"},
+		{"5 / 9 * (F - 32)", Env{"F": 32}, "0"},
+		{"5 / 9 * (F - 32)", Env{"F": 212}, "100"},
+	}
+	var prevExpr string
+	for _, test := range tests {
+		// Print expr only when it changes.
+		if test.expr != prevExpr {
+			fmt.Printf("\n%s\n", test.expr)
+			prevExpr = test.expr
+		}
+		expr, err := Parse(test.expr)
+		if err != nil {
+			t.Error(err) // parse error
+			continue
+		}
+		got := fmt.Sprintf("%.6g", expr.Eval(test.env))
+		fmt.Printf("\t%v => %s\n", test.env, got)
+		if got != test.want {
+			t.Errorf("%s.Eval() in %v = %q, want %q\n",
+			test.expr, test.env, got, test.want)
+		}
+	}
+}
+
+

对于表格中的每一条记录,这个测试会解析它的表达式然后在环境变量中计算它,输出结果。这里我们没有空间来展示Parse函数,但是如果你使用go get下载这个包你就可以看到这个函数。

+

go test(§11.1) 命令会运行一个包的测试用例:

+
$ go test -v gopl.io/ch7/eval
+
+

这个-v标识可以让我们看到测试用例打印的输出;正常情况下像这样一个成功的测试用例会阻止打印结果的输出。这里是测试用例里fmt.Printf语句的输出:

+
sqrt(A / pi)
+    map[A:87616 pi:3.141592653589793] => 167
+
+pow(x, 3) + pow(y, 3)
+    map[x:12 y:1] => 1729
+    map[x:9 y:10] => 1729
+
+5 / 9 * (F - 32)
+    map[F:-40] => -40
+    map[F:32] => 0
+    map[F:212] => 100
+
+

幸运的是目前为止所有的输入都是适合的格式,但是我们的运气不可能一直都有。甚至在解释型语言中,为了静态错误检查语法是非常常见的;静态错误就是不用运行程序就可以检测出来的错误。通过将静态检查和动态的部分分开,我们可以快速的检查错误并且对于多次检查只执行一次而不是每次表达式计算的时候都进行检查。

+

让我们往Expr接口中增加另一个方法。Check方法对一个表达式语义树检查出静态错误。我们马上会说明它的vars参数。

+
type Expr interface {
+	Eval(env Env) float64
+	// Check reports errors in this Expr and adds its Vars to the set.
+	Check(vars map[Var]bool) error
+}
+
+

具体的Check方法展示在下面。literal和Var类型的计算不可能失败,所以这些类型的Check方法会返回一个nil值。对于unary和binary的Check方法会首先检查操作符是否有效,然后递归的检查运算单元。相似地对于call的这个方法首先检查调用的函数是否已知并且有没有正确个数的参数,然后递归的检查每一个参数。

+
func (v Var) Check(vars map[Var]bool) error {
+	vars[v] = true
+	return nil
+}
+
+func (literal) Check(vars map[Var]bool) error {
+	return nil
+}
+
+func (u unary) Check(vars map[Var]bool) error {
+	if !strings.ContainsRune("+-", u.op) {
+		return fmt.Errorf("unexpected unary op %q", u.op)
+	}
+	return u.x.Check(vars)
+}
+
+func (b binary) Check(vars map[Var]bool) error {
+	if !strings.ContainsRune("+-*/", b.op) {
+		return fmt.Errorf("unexpected binary op %q", b.op)
+	}
+	if err := b.x.Check(vars); err != nil {
+		return err
+	}
+	return b.y.Check(vars)
+}
+
+func (c call) Check(vars map[Var]bool) error {
+	arity, ok := numParams[c.fn]
+	if !ok {
+		return fmt.Errorf("unknown function %q", c.fn)
+	}
+	if len(c.args) != arity {
+		return fmt.Errorf("call to %s has %d args, want %d",
+			c.fn, len(c.args), arity)
+	}
+	for _, arg := range c.args {
+		if err := arg.Check(vars); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+var numParams = map[string]int{"pow": 2, "sin": 1, "sqrt": 1}
+
+

我们在两个组中有选择地列出有问题的输入和它们得出的错误。Parse函数(这里没有出现)会报出一个语法错误和Check函数会报出语义错误。

+
x % 2               unexpected '%'
+math.Pi             unexpected '.'
+!true               unexpected '!'
+"hello"             unexpected '"'
+
+log(10)             unknown function "log"
+sqrt(1, 2)          call to sqrt has 2 args, want 1
+
+

Check方法的参数是一个Var类型的集合,这个集合聚集从表达式中找到的变量名。为了保证成功的计算,这些变量中的每一个都必须出现在环境变量中。从逻辑上讲,这个集合就是调用Check方法返回的结果,但是因为这个方法是递归调用的,所以对于Check方法,填充结果到一个作为参数传入的集合中会更加的方便。调用方在初始调用时必须提供一个空的集合。

+

在第3.2节中,我们绘制了一个在编译期才确定的函数f(x,y)。现在我们可以解析,检查和计算在字符串中的表达式,我们可以构建一个在运行时从客户端接收表达式的web应用并且它会绘制这个函数的表示的曲面。我们可以使用集合vars来检查表达式是否是一个只有两个变量x和y的函数——实际上是3个,因为我们为了方便会提供半径大小r。并且我们会在计算前使用Check方法拒绝有格式问题的表达式,这样我们就不会在下面函数的40000个计算过程(100x100个栅格,每一个有4个角)重复这些检查。

+

这个ParseAndCheck函数混合了解析和检查步骤的过程:

+

gopl.io/ch7/surface

+
import "gopl.io/ch7/eval"
+
+func parseAndCheck(s string) (eval.Expr, error) {
+	if s == "" {
+		return nil, fmt.Errorf("empty expression")
+	}
+	expr, err := eval.Parse(s)
+	if err != nil {
+		return nil, err
+	}
+	vars := make(map[eval.Var]bool)
+	if err := expr.Check(vars); err != nil {
+		return nil, err
+	}
+	for v := range vars {
+		if v != "x" && v != "y" && v != "r" {
+			return nil, fmt.Errorf("undefined variable: %s", v)
+		}
+	}
+	return expr, nil
+}
+
+

为了编写这个web应用,所有我们需要做的就是下面这个plot函数,这个函数有和http.HandlerFunc相似的签名:

+
func plot(w http.ResponseWriter, r *http.Request) {
+	r.ParseForm()
+	expr, err := parseAndCheck(r.Form.Get("expr"))
+	if err != nil {
+		http.Error(w, "bad expr: "+err.Error(), http.StatusBadRequest)
+		return
+	}
+	w.Header().Set("Content-Type", "image/svg+xml")
+	surface(w, func(x, y float64) float64 {
+		r := math.Hypot(x, y) // distance from (0,0)
+		return expr.Eval(eval.Env{"x": x, "y": y, "r": r})
+	})
+}
+
+

+

这个plot函数解析和检查在HTTP请求中指定的表达式并且用它来创建一个两个变量的匿名函数。这个匿名函数和来自原来surface-plotting程序中的固定函数f有相同的签名,但是它计算一个用户提供的表达式。环境变量中定义了x,y和半径r。最后plot调用surface函数,它就是gopl.io/ch3/surface中的主要函数,修改后它可以接受plot中的函数和输出io.Writer作为参数,而不是使用固定的函数f和os.Stdout。图7.7中显示了通过程序产生的3个曲面。

+

练习 7.13: 为Expr增加一个String方法来打印美观的语法树。当再一次解析的时候,检查它的结果是否生成相同的语法树。

+

练习 7.14: 定义一个新的满足Expr接口的具体类型并且提供一个新的操作例如对它运算单元中的最小值的计算。因为Parse函数不会创建这个新类型的实例,为了使用它你可能需要直接构造一个语法树(或者继承parser接口)。

+

练习 7.15: 编写一个从标准输入中读取一个单一表达式的程序,用户及时地提供对于任意变量的值,然后在结果环境变量中计算表达式的值。优雅的处理所有遇到的错误。

+

练习 7.16: 编写一个基于web的计算器程序。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch7/ch7-10.html b/ch7/ch7-10.html new file mode 100644 index 0000000..260d163 --- /dev/null +++ b/ch7/ch7-10.html @@ -0,0 +1,273 @@ + + + + + + 类型断言 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

7.10. 类型断言

+

类型断言是一个使用在接口值上的操作。语法上它看起来像x.(T)被称为断言类型,这里x表示一个接口的类型和T表示一个类型。一个类型断言检查它操作对象的动态类型是否和断言的类型匹配。

+

这里有两种可能。第一种,如果断言的类型T是一个具体类型,然后类型断言检查x的动态类型是否和T相同。如果这个检查成功了,类型断言的结果是x的动态值,当然它的类型是T。换句话说,具体类型的类型断言从它的操作对象中获得具体的值。如果检查失败,接下来这个操作会抛出panic。例如:

+
var w io.Writer
+w = os.Stdout
+f := w.(*os.File)      // success: f == os.Stdout
+c := w.(*bytes.Buffer) // panic: interface holds *os.File, not *bytes.Buffer
+
+

第二种,如果相反地断言的类型T是一个接口类型,然后类型断言检查是否x的动态类型满足T。如果这个检查成功了,动态值没有获取到;这个结果仍然是一个有相同动态类型和值部分的接口值,但是结果为类型T。换句话说,对一个接口类型的类型断言改变了类型的表述方式,改变了可以获取的方法集合(通常更大),但是它保留了接口值内部的动态类型和值的部分。

+

在下面的第一个类型断言后,w和rw都持有os.Stdout,因此它们都有一个动态类型*os.File,但是变量w是一个io.Writer类型,只对外公开了文件的Write方法,而rw变量还公开了它的Read方法。

+
var w io.Writer
+w = os.Stdout
+rw := w.(io.ReadWriter) // success: *os.File has both Read and Write
+w = new(ByteCounter)
+rw = w.(io.ReadWriter) // panic: *ByteCounter has no Read method
+
+

如果断言操作的对象是一个nil接口值,那么不论被断言的类型是什么这个类型断言都会失败。我们几乎不需要对一个更少限制性的接口类型(更少的方法集合)做断言,因为它表现的就像是赋值操作一样,除了对于nil接口值的情况。

+
w = rw             // io.ReadWriter is assignable to io.Writer
+w = rw.(io.Writer) // fails only if rw == nil
+
+

经常地,对一个接口值的动态类型我们是不确定的,并且我们更愿意去检验它是否是一些特定的类型。如果类型断言出现在一个预期有两个结果的赋值操作中,例如如下的定义,这个操作不会在失败的时候发生panic,但是替代地返回一个额外的第二个结果,这个结果是一个标识成功与否的布尔值:

+
var w io.Writer = os.Stdout
+f, ok := w.(*os.File)      // success:  ok, f == os.Stdout
+b, ok := w.(*bytes.Buffer) // failure: !ok, b == nil
+
+

第二个结果通常赋值给一个命名为ok的变量。如果这个操作失败了,那么ok就是false值,第一个结果等于被断言类型的零值,在这个例子中就是一个nil的*bytes.Buffer类型。

+

这个ok结果经常立即用于决定程序下面做什么。if语句的扩展格式让这个变的很简洁:

+
if f, ok := w.(*os.File); ok {
+	// ...use f...
+}
+
+

当类型断言的操作对象是一个变量,你有时会看见原来的变量名重用而不是声明一个新的本地变量名,这个重用的变量原来的值会被覆盖(理解:其实是声明了一个同名的新的本地变量,外层原来的w不会被改变),如下面这样:

+
if w, ok := w.(*os.File); ok {
+	// ...use w...
+}
+
+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch7/ch7-11.html b/ch7/ch7-11.html new file mode 100644 index 0000000..434491a --- /dev/null +++ b/ch7/ch7-11.html @@ -0,0 +1,296 @@ + + + + + + 基于类型断言识别错误类型 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

7.11. 基于类型断言区别错误类型

+

思考在os包中文件操作返回的错误集合。I/O可以因为任何数量的原因失败,但是有三种经常的错误必须进行不同的处理:文件已经存在(对于创建操作),找不到文件(对于读取操作),和权限拒绝。os包中提供了三个帮助函数来对给定的错误值表示的失败进行分类:

+
package os
+
+func IsExist(err error) bool
+func IsNotExist(err error) bool
+func IsPermission(err error) bool
+
+

对这些判断的一个缺乏经验的实现可能会去检查错误消息是否包含了特定的子字符串,

+
func IsNotExist(err error) bool {
+	// NOTE: not robust!
+	return strings.Contains(err.Error(), "file does not exist")
+}
+
+

但是处理I/O错误的逻辑可能一个和另一个平台非常的不同,所以这种方案并不健壮,并且对相同的失败可能会报出各种不同的错误消息。在测试的过程中,通过检查错误消息的子字符串来保证特定的函数以期望的方式失败是非常有用的,但对于线上的代码是不够的。

+

一个更可靠的方式是使用一个专门的类型来描述结构化的错误。os包中定义了一个PathError类型来描述在文件路径操作中涉及到的失败,像Open或者Delete操作;并且定义了一个叫LinkError的变体来描述涉及到两个文件路径的操作,像Symlink和Rename。这下面是os.PathError:

+
package os
+
+// PathError records an error and the operation and file path that caused it.
+type PathError struct {
+	Op   string
+	Path string
+	Err  error
+}
+
+func (e *PathError) Error() string {
+	return e.Op + " " + e.Path + ": " + e.Err.Error()
+}
+
+

大多数调用方都不知道PathError并且通过调用错误本身的Error方法来统一处理所有的错误。尽管PathError的Error方法简单地把这些字段连接起来生成错误消息,PathError的结构保护了内部的错误组件。调用方需要使用类型断言来检测错误的具体类型以便将一种失败和另一种区分开;具体的类型可以比字符串提供更多的细节。

+
_, err := os.Open("/no/such/file")
+fmt.Println(err) // "open /no/such/file: No such file or directory"
+fmt.Printf("%#v\n", err)
+// Output:
+// &os.PathError{Op:"open", Path:"/no/such/file", Err:0x2}
+
+

这就是三个帮助函数是怎么工作的。例如下面展示的IsNotExist,它会报出是否一个错误和syscall.ENOENT(§7.8)或者和有名的错误os.ErrNotExist相等(可以在§5.4.2中找到io.EOF);或者是一个*PathError,它内部的错误是syscall.ENOENT和os.ErrNotExist其中之一。

+
import (
+	"errors"
+	"syscall"
+)
+
+var ErrNotExist = errors.New("file does not exist")
+
+// IsNotExist returns a boolean indicating whether the error is known to
+// report that a file or directory does not exist. It is satisfied by
+// ErrNotExist as well as some syscall errors.
+func IsNotExist(err error) bool {
+	if pe, ok := err.(*PathError); ok {
+		err = pe.Err
+	}
+	return err == syscall.ENOENT || err == ErrNotExist
+}
+
+

下面这里是它的实际使用:

+
_, err := os.Open("/no/such/file")
+fmt.Println(os.IsNotExist(err)) // "true"
+
+

如果错误消息结合成一个更大的字符串,当然PathError的结构就不再为人所知,例如通过一个对fmt.Errorf函数的调用。区别错误通常必须在失败操作后,错误传回调用者前进行。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch7/ch7-12.html b/ch7/ch7-12.html new file mode 100644 index 0000000..907f0ee --- /dev/null +++ b/ch7/ch7-12.html @@ -0,0 +1,298 @@ + + + + + + 通过类型断言查询接口 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

7.12. 通过类型断言询问行为

+

下面这段逻辑和net/http包中web服务器负责写入HTTP头字段(例如:"Content-type:text/html")的部分相似。io.Writer接口类型的变量w代表HTTP响应;写入它的字节最终被发送到某个人的web浏览器上。

+
func writeHeader(w io.Writer, contentType string) error {
+	if _, err := w.Write([]byte("Content-Type: ")); err != nil {
+		return err
+	}
+	if _, err := w.Write([]byte(contentType)); err != nil {
+		return err
+	}
+	// ...
+}
+
+

因为Write方法需要传入一个byte切片而我们希望写入的值是一个字符串,所以我们需要使用[]byte(...)进行转换。这个转换分配内存并且做一个拷贝,但是这个拷贝在转换后几乎立马就被丢弃掉。让我们假装这是一个web服务器的核心部分并且我们的性能分析表示这个内存分配使服务器的速度变慢。这里我们可以避免掉内存分配么?

+

这个io.Writer接口告诉我们关于w持有的具体类型的唯一东西:就是可以向它写入字节切片。如果我们回顾net/http包中的内幕,我们知道在这个程序中的w变量持有的动态类型也有一个允许字符串高效写入的WriteString方法;这个方法会避免去分配一个临时的拷贝。(这可能像在黑夜中射击一样,但是许多满足io.Writer接口的重要类型同时也有WriteString方法,包括*bytes.Buffer*os.File*bufio.Writer。)

+

我们不能对任意io.Writer类型的变量w,假设它也拥有WriteString方法。但是我们可以定义一个只有这个方法的新接口并且使用类型断言来检测是否w的动态类型满足这个新接口。

+
// writeString writes s to w.
+// If w has a WriteString method, it is invoked instead of w.Write.
+func writeString(w io.Writer, s string) (n int, err error) {
+	type stringWriter interface {
+		WriteString(string) (n int, err error)
+	}
+	if sw, ok := w.(stringWriter); ok {
+		return sw.WriteString(s) // avoid a copy
+	}
+	return w.Write([]byte(s)) // allocate temporary copy
+}
+
+func writeHeader(w io.Writer, contentType string) error {
+	if _, err := writeString(w, "Content-Type: "); err != nil {
+		return err
+	}
+	if _, err := writeString(w, contentType); err != nil {
+		return err
+	}
+	// ...
+}
+
+

为了避免重复定义,我们将这个检查移入到一个实用工具函数writeString中,但是它太有用了以致于标准库将它作为io.WriteString函数提供。这是向一个io.Writer接口写入字符串的推荐方法。

+

这个例子的神奇之处在于,没有定义了WriteString方法的标准接口,也没有指定它是一个所需行为的标准接口。一个具体类型只会通过它的方法决定它是否满足stringWriter接口,而不是任何它和这个接口类型所表达的关系。它的意思就是上面的技术依赖于一个假设,这个假设就是:如果一个类型满足下面的这个接口,然后WriteString(s)方法就必须和Write([]byte(s))有相同的效果。

+
interface {
+	io.Writer
+	WriteString(s string) (n int, err error)
+}
+
+

尽管io.WriteString实施了这个假设,但是调用它的函数极少可能会去实施类似的假设。定义一个特定类型的方法隐式地获取了对特定行为的协约。对于Go语言的新手,特别是那些来自有强类型语言使用背景的新手,可能会发现它缺乏显式的意图令人感到混乱,但是在实战的过程中这几乎不是一个问题。除了空接口interface{},接口类型很少意外巧合地被实现。

+

上面的writeString函数使用一个类型断言来获知一个普遍接口类型的值是否满足一个更加具体的接口类型;并且如果满足,它会使用这个更具体接口的行为。这个技术可以被很好的使用,不论这个被询问的接口是一个标准如io.ReadWriter,或者用户定义的如stringWriter接口。

+

这也是fmt.Fprintf函数怎么从其它所有值中区分满足error或者fmt.Stringer接口的值。在fmt.Fprintf内部,有一个将单个操作对象转换成一个字符串的步骤,像下面这样:

+
package fmt
+
+func formatOneValue(x interface{}) string {
+	if err, ok := x.(error); ok {
+		return err.Error()
+	}
+	if str, ok := x.(Stringer); ok {
+		return str.String()
+	}
+	// ...all other types...
+}
+
+

如果x满足这两个接口类型中的一个,具体满足的接口决定对值的格式化方式。如果都不满足,默认的case或多或少会统一地使用反射来处理所有的其它类型;我们可以在第12章知道具体是怎么实现的。

+

再一次的,它假设任何有String方法的类型都满足fmt.Stringer中约定的行为,这个行为会返回一个适合打印的字符串。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch7/ch7-13.html b/ch7/ch7-13.html new file mode 100644 index 0000000..5714afc --- /dev/null +++ b/ch7/ch7-13.html @@ -0,0 +1,307 @@ + + + + + + 类型分支 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

7.13. 类型分支

+

接口被以两种不同的方式使用。在第一个方式中,以io.Reader,io.Writer,fmt.Stringer,sort.Interface,http.Handler和error为典型,一个接口的方法表达了实现这个接口的具体类型间的相似性,但是隐藏了代码的细节和这些具体类型本身的操作。重点在于方法上,而不是具体的类型上。

+

第二个方式是利用一个接口值可以持有各种具体类型值的能力,将这个接口认为是这些类型的联合。类型断言用来动态地区别这些类型,使得对每一种情况都不一样。在这个方式中,重点在于具体的类型满足这个接口,而不在于接口的方法(如果它确实有一些的话),并且没有任何的信息隐藏。我们将以这种方式使用的接口描述为discriminated unions(可辨识联合)。

+

如果你熟悉面向对象编程,你可能会将这两种方式当作是subtype polymorphism(子类型多态)和 ad hoc polymorphism(非参数多态),但是你不需要去记住这些术语。对于本章剩下的部分,我们将会呈现一些第二种方式的例子。

+

和其它那些语言一样,Go语言查询一个SQL数据库的API会干净地将查询中固定的部分和变化的部分分开。一个调用的例子可能看起来像这样:

+
import "database/sql"
+
+func listTracks(db sql.DB, artist string, minYear, maxYear int) {
+	result, err := db.Exec(
+		"SELECT * FROM tracks WHERE artist = ? AND ? <= year AND year <= ?",
+		artist, minYear, maxYear)
+	// ...
+}
+
+

Exec方法使用SQL字面量替换在查询字符串中的每个'?';SQL字面量表示相应参数的值,它有可能是一个布尔值,一个数字,一个字符串,或者nil空值。用这种方式构造查询可以帮助避免SQL注入攻击;这种攻击就是对手可以通过利用输入内容中不正确的引号来控制查询语句。在Exec函数内部,我们可能会找到像下面这样的一个函数,它会将每一个参数值转换成它的SQL字面量符号。

+
func sqlQuote(x interface{}) string {
+	if x == nil {
+		return "NULL"
+	} else if _, ok := x.(int); ok {
+		return fmt.Sprintf("%d", x)
+	} else if _, ok := x.(uint); ok {
+		return fmt.Sprintf("%d", x)
+	} else if b, ok := x.(bool); ok {
+		if b {
+			return "TRUE"
+		}
+		return "FALSE"
+	} else if s, ok := x.(string); ok {
+		return sqlQuoteString(s) // (not shown)
+	} else {
+		panic(fmt.Sprintf("unexpected type %T: %v", x, x))
+	}
+}
+
+

switch语句可以简化if-else链,如果这个if-else链对一连串值做相等测试。一个相似的type switch(类型分支)可以简化类型断言的if-else链。

+

在最简单的形式中,一个类型分支像普通的switch语句一样,它的运算对象是x.(type)——它使用了关键词字面量type——并且每个case有一到多个类型。一个类型分支基于这个接口值的动态类型使一个多路分支有效。这个nil的case和if x == nil匹配,并且这个default的case和如果其它case都不匹配的情况匹配。一个对sqlQuote的类型分支可能会有这些case:

+
switch x.(type) {
+case nil:       // ...
+case int, uint: // ...
+case bool:      // ...
+case string:    // ...
+default:        // ...
+}
+
+

和(§1.8)中的普通switch语句一样,每一个case会被顺序的进行考虑,并且当一个匹配找到时,这个case中的内容会被执行。当一个或多个case类型是接口时,case的顺序就会变得很重要,因为可能会有两个case同时匹配的情况。default case相对其它case的位置是无所谓的。它不会允许落空发生。

+

注意到在原来的函数中,对于bool和string情况的逻辑需要通过类型断言访问提取的值。因为这个做法很典型,类型分支语句有一个扩展的形式,它可以将提取的值绑定到一个在每个case范围内都有效的新变量。

+
switch x := x.(type) { /* ... */ }
+
+

这里我们已经将新的变量也命名为x;和类型断言一样,重用变量名是很常见的。和一个switch语句相似地,一个类型分支隐式的创建了一个词法块,因此新变量x的定义不会和外面块中的x变量冲突。每一个case也会隐式的创建一个单独的词法块。

+

使用类型分支的扩展形式来重写sqlQuote函数会让这个函数更加的清晰:

+
func sqlQuote(x interface{}) string {
+	switch x := x.(type) {
+	case nil:
+		return "NULL"
+	case int, uint:
+		return fmt.Sprintf("%d", x) // x has type interface{} here.
+	case bool:
+		if x {
+			return "TRUE"
+		}
+		return "FALSE"
+	case string:
+		return sqlQuoteString(x) // (not shown)
+	default:
+		panic(fmt.Sprintf("unexpected type %T: %v", x, x))
+	}
+}
+
+

在这个版本的函数中,在每个单一类型的case内部,变量x和这个case的类型相同。例如,变量x在bool的case中是bool类型和string的case中是string类型。在所有其它的情况中,变量x是switch运算对象的类型(接口);在这个例子中运算对象是一个interface{}。当多个case需要相同的操作时,比如int和uint的情况,类型分支可以很容易的合并这些情况。

+

尽管sqlQuote接受一个任意类型的参数,但是这个函数只会在它的参数匹配类型分支中的一个case时运行到结束;其它情况的它会panic出“unexpected type”消息。虽然x的类型是interface{},但是我们把它认为是一个int,uint,bool,string,和nil值的discriminated union(可识别联合)

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch7/ch7-14.html b/ch7/ch7-14.html new file mode 100644 index 0000000..6e76e71 --- /dev/null +++ b/ch7/ch7-14.html @@ -0,0 +1,352 @@ + + + + + + 示例: 基于标记的XML解码 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

7.14. 示例: 基于标记的XML解码

+

第4.5章节展示了如何使用encoding/json包中的Marshal和Unmarshal函数来将JSON文档转换成Go语言的数据结构。encoding/xml包提供了一个相似的API。当我们想构造一个文档树的表示时使用encoding/xml包会很方便,但是对于很多程序并不是必须的。encoding/xml包也提供了一个更低层的基于标记的API用于XML解码。在基于标记的样式中,解析器消费输入并产生一个标记流;四个主要的标记类型-StartElement,EndElement,CharData,和Comment-每一个都是encoding/xml包中的具体类型。每一个对(*xml.Decoder).Token的调用都返回一个标记。

+

这里显示的是和这个API相关的部分:

+

encoding/xml

+
package xml
+
+type Name struct {
+	Local string // e.g., "Title" or "id"
+}
+
+type Attr struct { // e.g., name="value"
+	Name  Name
+	Value string
+}
+
+// A Token includes StartElement, EndElement, CharData,
+// and Comment, plus a few esoteric types (not shown).
+type Token interface{}
+type StartElement struct { // e.g., <name>
+    Name Name
+    Attr []Attr
+}
+type EndElement struct { Name Name } // e.g., </name>
+type CharData []byte                 // e.g., <p>CharData</p>
+type Comment []byte                  // e.g., <!-- Comment -->
+
+type Decoder struct{ /* ... */ }
+func NewDecoder(io.Reader) *Decoder
+func (*Decoder) Token() (Token, error) // returns next Token in sequence
+
+

这个没有方法的Token接口也是一个可识别联合的例子。传统的接口如io.Reader的目的是隐藏满足它的具体类型的细节,这样就可以创造出新的实现:在这个实现中每个具体类型都被统一地对待。相反,满足可识别联合的具体类型的集合被设计为确定和暴露,而不是隐藏。可识别联合的类型几乎没有方法,操作它们的函数使用一个类型分支的case集合来进行表述,这个case集合中每一个case都有不同的逻辑。

+

下面的xmlselect程序获取和打印在一个XML文档树中确定的元素下找到的文本。使用上面的API,它可以在输入上一次完成它的工作而从来不要实例化这个文档树。

+

gopl.io/ch7/xmlselect

+
// Xmlselect prints the text of selected elements of an XML document.
+package main
+
+import (
+	"encoding/xml"
+	"fmt"
+	"io"
+	"os"
+	"strings"
+)
+
+func main() {
+	dec := xml.NewDecoder(os.Stdin)
+	var stack []string // stack of element names
+	for {
+		tok, err := dec.Token()
+		if err == io.EOF {
+			break
+		} else if err != nil {
+			fmt.Fprintf(os.Stderr, "xmlselect: %v\n", err)
+			os.Exit(1)
+		}
+		switch tok := tok.(type) {
+		case xml.StartElement:
+			stack = append(stack, tok.Name.Local) // push
+		case xml.EndElement:
+			stack = stack[:len(stack)-1] // pop
+		case xml.CharData:
+			if containsAll(stack, os.Args[1:]) {
+				fmt.Printf("%s: %s\n", strings.Join(stack, " "), tok)
+			}
+		}
+	}
+}
+
+// containsAll reports whether x contains the elements of y, in order.
+func containsAll(x, y []string) bool {
+	for len(y) <= len(x) {
+		if len(y) == 0 {
+			return true
+		}
+		if x[0] == y[0] {
+			y = y[1:]
+		}
+		x = x[1:]
+	}
+	return false
+}
+
+

main函数中的循环每遇到一个StartElement时,它把这个元素的名称压到一个栈里,并且每次遇到EndElement时,它将名称从这个栈中推出。这个API保证了StartElement和EndElement的序列可以被完全的匹配,甚至在一个糟糕的文档格式中。注释会被忽略。当xmlselect遇到一个CharData时,只有当栈中有序地包含所有通过命令行参数传入的元素名称时,它才会输出相应的文本。

+

下面的命令打印出任意出现在两层div元素下的h2元素的文本。它的输入是XML的说明文档,并且它自己就是XML文档格式的。

+
$ go build gopl.io/ch1/fetch
+$ ./fetch http://www.w3.org/TR/2006/REC-xml11-20060816 |
+    ./xmlselect div div h2
+html body div div h2: 1 Introduction
+html body div div h2: 2 Documents
+html body div div h2: 3 Logical Structures
+html body div div h2: 4 Physical Structures
+html body div div h2: 5 Conformance
+html body div div h2: 6 Notation
+html body div div h2: A References
+html body div div h2: B Definitions for Character Normalization
+...
+
+

练习 7.17: 扩展xmlselect程序以便让元素不仅可以通过名称选择,也可以通过它们CSS风格的属性进行选择。例如一个像这样

+
<div id="page" class="wide">
+
+

的元素可以通过匹配id或者class,同时还有它的名称来进行选择。

+

练习 7.18: 使用基于标记的解码API,编写一个可以读取任意XML文档并构造这个文档所代表的通用节点树的程序。节点有两种类型:CharData节点表示文本字符串,和 Element节点表示被命名的元素和它们的属性。每一个元素节点有一个子节点的切片。

+

你可能发现下面的定义会对你有帮助。

+
import "encoding/xml"
+
+type Node interface{} // CharData or *Element
+
+type CharData string
+
+type Element struct {
+	Type     xml.Name
+	Attr     []xml.Attr
+	Children []Node
+}
+
+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch7/ch7-15.html b/ch7/ch7-15.html new file mode 100644 index 0000000..8f4904f --- /dev/null +++ b/ch7/ch7-15.html @@ -0,0 +1,242 @@ + + + + + + 补充几点 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

7.15. 一些建议

+

当设计一个新的包时,新手Go程序员总是先创建一套接口,然后再定义一些满足它们的具体类型。这种方式的结果就是有很多的接口,它们中的每一个仅只有一个实现。不要再这么做了。这种接口是不必要的抽象;它们也有一个运行时损耗。你可以使用导出机制(§6.6)来限制一个类型的方法或一个结构体的字段是否在包外可见。接口只有当有两个或两个以上的具体类型必须以相同的方式进行处理时才需要。

+

当一个接口只被一个单一的具体类型实现时有一个例外,就是由于它的依赖,这个具体类型不能和这个接口存在在一个相同的包中。这种情况下,一个接口是解耦这两个包的一个好方式。

+

因为在Go语言中只有当两个或更多的类型实现一个接口时才使用接口,它们必定会从任意特定的实现细节中抽象出来。结果就是有更少和更简单方法的更小的接口(经常和io.Writer或 fmt.Stringer一样只有一个)。当新的类型出现时,小的接口更容易满足。对于接口设计的一个好的标准就是 ask only for what you need(只考虑你需要的东西)

+

我们完成了对方法和接口的学习过程。Go语言对面向对象风格的编程支持良好,但这并不意味着你只能使用这一风格。不是任何事物都需要被当做一个对象;独立的函数有它们自己的用处,未封装的数据类型也是这样。观察一下,在本书前五章的例子中像input.Scan这样的方法被调用不超过二十次,与之相反的是普遍调用的函数如fmt.Printf。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch7/ch7.html b/ch7/ch7.html new file mode 100644 index 0000000..e07e869 --- /dev/null +++ b/ch7/ch7.html @@ -0,0 +1,241 @@ + + + + + + 接口 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

第7章 接口

+

接口类型是对其它类型行为的抽象和概括;因为接口类型不会和特定的实现细节绑定在一起,通过这种抽象的方式我们可以让我们的函数更加灵活和更具有适应能力。

+

很多面向对象的语言都有相似的接口概念,但Go语言中接口类型的独特之处在于它是满足隐式实现的。也就是说,我们没有必要对于给定的具体类型定义所有满足的接口类型;简单地拥有一些必需的方法就足够了。这种设计可以让你创建一个新的接口类型满足已经存在的具体类型却不会去改变这些类型的定义;当我们使用的类型来自于不受我们控制的包时这种设计尤其有用。

+

在本章,我们会开始看到接口类型和值的一些基本技巧。顺着这种方式我们将学习几个来自标准库的重要接口。很多Go程序中都尽可能多的去使用标准库中的接口。最后,我们会在(§7.10)看到类型断言的知识,在(§7.13)看到类型开关的使用并且学到他们是怎样让不同的类型的概括成为可能。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch8/ch8-01.html b/ch8/ch8-01.html new file mode 100644 index 0000000..98ae734 --- /dev/null +++ b/ch8/ch8-01.html @@ -0,0 +1,274 @@ + + + + + + Goroutines - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

8.1. Goroutines

+

在Go语言中,每一个并发的执行单元叫作一个goroutine。设想这里的一个程序有两个函数,一个函数做计算,另一个输出结果,假设两个函数没有相互之间的调用关系。一个线性的程序会先调用其中的一个函数,然后再调用另一个。如果程序中包含多个goroutine,对两个函数的调用则可能发生在同一时刻。马上就会看到这样的一个程序。

+

如果你使用过操作系统或者其它语言提供的线程,那么你可以简单地把goroutine类比作一个线程,这样你就可以写出一些正确的程序了。goroutine和线程的本质区别会在9.8节中讲。

+

当一个程序启动时,其主函数即在一个单独的goroutine中运行,我们叫它main goroutine。新的goroutine会用go语句来创建。在语法上,go语句是一个普通的函数或方法调用前加上关键字go。go语句会使其语句中的函数在一个新创建的goroutine中运行。而go语句本身会迅速地完成。

+
f()    // call f(); wait for it to return
+go f() // create a new goroutine that calls f(); don't wait
+
+

下面的例子,main goroutine将计算菲波那契数列的第45个元素值。由于计算函数使用低效的递归,所以会运行相当长时间,在此期间我们想让用户看到一个可见的标识来表明程序依然在正常运行,所以来做一个动画的小图标:

+

gopl.io/ch8/spinner

+
func main() {
+	go spinner(100 * time.Millisecond)
+	const n = 45
+	fibN := fib(n) // slow
+	fmt.Printf("\rFibonacci(%d) = %d\n", n, fibN)
+}
+
+func spinner(delay time.Duration) {
+	for {
+		for _, r := range `-\|/` {
+			fmt.Printf("\r%c", r)
+			time.Sleep(delay)
+		}
+	}
+}
+
+func fib(x int) int {
+	if x < 2 {
+		return x
+	}
+	return fib(x-1) + fib(x-2)
+}
+
+

动画显示了几秒之后,fib(45)的调用成功地返回,并且打印结果:

+
Fibonacci(45) = 1134903170
+
+

然后主函数返回。主函数返回时,所有的goroutine都会被直接打断,程序退出。除了从主函数退出或者直接终止程序之外,没有其它的编程方法能够让一个goroutine来打断另一个的执行,但是之后可以看到一种方式来实现这个目的,通过goroutine之间的通信来让一个goroutine请求其它的goroutine,并让被请求的goroutine自行结束执行。

+

留意一下这里的两个独立的单元是如何进行组合的,spinning和菲波那契的计算。分别在独立的函数中,但两个函数会同时执行。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch8/ch8-02.html b/ch8/ch8-02.html new file mode 100644 index 0000000..f79dec3 --- /dev/null +++ b/ch8/ch8-02.html @@ -0,0 +1,369 @@ + + + + + + 示例: 并发的Clock服务 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

8.2. 示例: 并发的Clock服务

+

网络编程是并发大显身手的一个领域,由于服务器是最典型的需要同时处理很多连接的程序,这些连接一般来自于彼此独立的客户端。在本小节中,我们会讲解go语言的net包,这个包提供编写一个网络客户端或者服务器程序的基本组件,无论两者间通信是使用TCP、UDP或者Unix domain sockets。在第一章中我们使用过的net/http包里的方法,也算是net包的一部分。

+

我们的第一个例子是一个顺序执行的时钟服务器,它会每隔一秒钟将当前时间写到客户端:

+

gopl.io/ch8/clock1

+
// Clock1 is a TCP server that periodically writes the time.
+package main
+
+import (
+	"io"
+	"log"
+	"net"
+	"time"
+)
+
+func main() {
+	listener, err := net.Listen("tcp", "localhost:8000")
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	for {
+		conn, err := listener.Accept()
+		if err != nil {
+			log.Print(err) // e.g., connection aborted
+			continue
+		}
+		handleConn(conn) // handle one connection at a time
+	}
+}
+
+func handleConn(c net.Conn) {
+	defer c.Close()
+	for {
+		_, err := io.WriteString(c, time.Now().Format("15:04:05\n"))
+		if err != nil {
+			return // e.g., client disconnected
+		}
+		time.Sleep(1 * time.Second)
+	}
+}
+
+
+

Listen函数创建了一个net.Listener的对象,这个对象会监听一个网络端口上到来的连接,在这个例子里我们用的是TCP的localhost:8000端口。listener对象的Accept方法会直接阻塞,直到一个新的连接被创建,然后会返回一个net.Conn对象来表示这个连接。

+

handleConn函数会处理一个完整的客户端连接。在一个for死循环中,用time.Now()获取当前时刻,然后写到客户端。由于net.Conn实现了io.Writer接口,我们可以直接向其写入内容。这个死循环会一直执行,直到写入失败。最可能的原因是客户端主动断开连接。这种情况下handleConn函数会用defer调用关闭服务器侧的连接,然后返回到主函数,继续等待下一个连接请求。

+

time.Time.Format方法提供了一种格式化日期和时间信息的方式。它的参数是一个格式化模板,标识如何来格式化时间,而这个格式化模板限定为Mon Jan 2 03:04:05PM 2006 UTC-0700。有8个部分(周几、月份、一个月的第几天……)。可以以任意的形式来组合前面这个模板;出现在模板中的部分会作为参考来对时间格式进行输出。在上面的例子中我们只用到了小时、分钟和秒。time包里定义了很多标准时间格式,比如time.RFC1123。在进行格式化的逆向操作time.Parse时,也会用到同样的策略。(译注:这是go语言和其它语言相比比较奇葩的一个地方。你需要记住格式化字符串是1月2日下午3点4分5秒零六年UTC-0700,而不像其它语言那样Y-m-d H:i:s一样,当然了这里可以用1234567的方式来记忆,倒是也不麻烦。)

+

为了连接例子里的服务器,我们需要一个客户端程序,比如netcat这个工具(nc命令),这个工具可以用来执行网络连接操作。

+
$ go build gopl.io/ch8/clock1
+$ ./clock1 &
+$ nc localhost 8000
+13:58:54
+13:58:55
+13:58:56
+13:58:57
+^C
+
+

客户端将服务器发来的时间显示了出来,我们用Control+C来中断客户端的执行,在Unix系统上,你会看到^C这样的响应。如果你的系统没有装nc这个工具,你可以用telnet来实现同样的效果,或者也可以用我们下面的这个用go写的简单的telnet程序,用net.Dial就可以简单地创建一个TCP连接:

+

gopl.io/ch8/netcat1

+
// Netcat1 is a read-only TCP client.
+package main
+
+import (
+	"io"
+	"log"
+	"net"
+	"os"
+)
+
+func main() {
+	conn, err := net.Dial("tcp", "localhost:8000")
+	if err != nil {
+		log.Fatal(err)
+	}
+	defer conn.Close()
+	mustCopy(os.Stdout, conn)
+}
+
+func mustCopy(dst io.Writer, src io.Reader) {
+	if _, err := io.Copy(dst, src); err != nil {
+		log.Fatal(err)
+	}
+}
+
+

这个程序会从连接中读取数据,并将读到的内容写到标准输出中,直到遇到end of file的条件或者发生错误。mustCopy这个函数我们在本节的几个例子中都会用到。让我们同时运行两个客户端来进行一个测试,这里可以开两个终端窗口,下面左边的是其中的一个的输出,右边的是另一个的输出:

+
$ go build gopl.io/ch8/netcat1
+$ ./netcat1
+13:58:54                               $ ./netcat1
+13:58:55
+13:58:56
+^C
+                                       13:58:57
+                                       13:58:58
+                                       13:58:59
+                                       ^C
+$ killall clock1
+
+

killall命令是一个Unix命令行工具,可以用给定的进程名来杀掉所有名字匹配的进程。

+

第二个客户端必须等待第一个客户端完成工作,这样服务端才能继续向后执行;因为我们这里的服务器程序同一时间只能处理一个客户端连接。我们这里对服务端程序做一点小改动,使其支持并发:在handleConn函数调用的地方增加go关键字,让每一次handleConn的调用都进入一个独立的goroutine。

+

gopl.io/ch8/clock2

+
for {
+	conn, err := listener.Accept()
+	if err != nil {
+		log.Print(err) // e.g., connection aborted
+		continue
+	}
+	go handleConn(conn) // handle connections concurrently
+}
+
+
+

现在多个客户端可以同时接收到时间了:

+
$ go build gopl.io/ch8/clock2
+$ ./clock2 &
+$ go build gopl.io/ch8/netcat1
+$ ./netcat1
+14:02:54                               $ ./netcat1
+14:02:55                               14:02:55
+14:02:56                               14:02:56
+14:02:57                               ^C
+14:02:58
+14:02:59                               $ ./netcat1
+14:03:00                               14:03:00
+14:03:01                               14:03:01
+^C                                     14:03:02
+                                       ^C
+$ killall clock2
+
+

练习 8.1: 修改clock2来支持传入参数作为端口号,然后写一个clockwall的程序,这个程序可以同时与多个clock服务器通信,从多个服务器中读取时间,并且在一个表格中一次显示所有服务器传回的结果,类似于你在某些办公室里看到的时钟墙。如果你有地理学上分布式的服务器可以用的话,让这些服务器跑在不同的机器上面;或者在同一台机器上跑多个不同的实例,这些实例监听不同的端口,假装自己在不同的时区。像下面这样:

+
$ TZ=US/Eastern    ./clock2 -port 8010 &
+$ TZ=Asia/Tokyo    ./clock2 -port 8020 &
+$ TZ=Europe/London ./clock2 -port 8030 &
+$ clockwall NewYork=localhost:8010 Tokyo=localhost:8020 London=localhost:8030
+
+

练习 8.2: 实现一个并发FTP服务器。服务器应该解析客户端发来的一些命令,比如cd命令来切换目录,ls来列出目录内文件,get和send来传输文件,close来关闭连接。你可以用标准的ftp命令来作为客户端,或者也可以自己实现一个。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch8/ch8-03.html b/ch8/ch8-03.html new file mode 100644 index 0000000..30df3f1 --- /dev/null +++ b/ch8/ch8-03.html @@ -0,0 +1,325 @@ + + + + + + 示例: 并发的Echo服务 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

8.3. 示例: 并发的Echo服务

+

clock服务器每一个连接都会起一个goroutine。在本节中我们会创建一个echo服务器,这个服务在每个连接中会有多个goroutine。大多数echo服务仅仅会返回他们读取到的内容,就像下面这个简单的handleConn函数所做的一样:

+
func handleConn(c net.Conn) {
+	io.Copy(c, c) // NOTE: ignoring errors
+	c.Close()
+}
+
+

一个更有意思的echo服务应该模拟一个实际的echo的“回响”,并且一开始要用大写HELLO来表示“声音很大”,之后经过一小段延迟返回一个有所缓和的Hello,然后一个全小写字母的hello表示声音渐渐变小直至消失,像下面这个版本的handleConn(译注:笑看作者脑洞大开):

+

gopl.io/ch8/reverb1

+
func echo(c net.Conn, shout string, delay time.Duration) {
+	fmt.Fprintln(c, "\t", strings.ToUpper(shout))
+	time.Sleep(delay)
+	fmt.Fprintln(c, "\t", shout)
+	time.Sleep(delay)
+	fmt.Fprintln(c, "\t", strings.ToLower(shout))
+}
+
+func handleConn(c net.Conn) {
+	input := bufio.NewScanner(c)
+	for input.Scan() {
+		echo(c, input.Text(), 1*time.Second)
+	}
+	// NOTE: ignoring potential errors from input.Err()
+	c.Close()
+}
+
+

我们需要升级我们的客户端程序,这样它就可以发送终端的输入到服务器,并把服务端的返回输出到终端上,这使我们有了使用并发的另一个好机会:

+

gopl.io/ch8/netcat2

+
func main() {
+	conn, err := net.Dial("tcp", "localhost:8000")
+	if err != nil {
+		log.Fatal(err)
+	}
+	defer conn.Close()
+	go mustCopy(os.Stdout, conn)
+	mustCopy(conn, os.Stdin)
+}
+
+

当main goroutine从标准输入流中读取内容并将其发送给服务器时,另一个goroutine会读取并打印服务端的响应。当main goroutine碰到输入终止时,例如,用户在终端中按了Control-D(^D),在windows上是Control-Z,这时程序就会被终止,尽管其它goroutine中还有进行中的任务。(在8.4.1中引入了channels后我们会明白如何让程序等待两边都结束。)

+

下面这个会话中,客户端的输入是左对齐的,服务端的响应会用缩进来区别显示。 +客户端会向服务器“喊三次话”:

+
$ go build gopl.io/ch8/reverb1
+$ ./reverb1 &
+$ go build gopl.io/ch8/netcat2
+$ ./netcat2
+Hello?
+    HELLO?
+    Hello?
+    hello?
+Is there anybody there?
+    IS THERE ANYBODY THERE?
+Yooo-hooo!
+    Is there anybody there?
+    is there anybody there?
+    YOOO-HOOO!
+    Yooo-hooo!
+    yooo-hooo!
+^D
+$ killall reverb1
+
+

注意客户端的第三次shout在前一个shout处理完成之前一直没有被处理,这貌似看起来不是特别“现实”。真实世界里的回响应该是会由三次shout的回声组合而成的。为了模拟真实世界的回响,我们需要更多的goroutine来做这件事情。这样我们就再一次地需要go这个关键词了,这次我们用它来调用echo:

+

gopl.io/ch8/reverb2

+
func handleConn(c net.Conn) {
+	input := bufio.NewScanner(c)
+	for input.Scan() {
+		go echo(c, input.Text(), 1*time.Second)
+	}
+	// NOTE: ignoring potential errors from input.Err()
+	c.Close()
+}
+
+

go后跟的函数的参数会在go语句自身执行时被求值;因此input.Text()会在main goroutine中被求值。 +现在回响是并发并且会按时间来覆盖掉其它响应了:

+
$ go build gopl.io/ch8/reverb2
+$ ./reverb2 &
+$ ./netcat2
+Is there anybody there?
+    IS THERE ANYBODY THERE?
+Yooo-hooo!
+    Is there anybody there?
+    YOOO-HOOO!
+    is there anybody there?
+    Yooo-hooo!
+    yooo-hooo!
+^D
+$ killall reverb2
+
+

让服务使用并发不只是处理多个客户端的请求,甚至在处理单个连接时也可能会用到,就像我们上面的两个go关键词的用法。然而在我们使用go关键词的同时,需要慎重地考虑net.Conn中的方法在并发地调用时是否安全,事实上对于大多数类型来说也确实不安全。我们会在下一章中详细地探讨并发安全性。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch8/ch8-04.html b/ch8/ch8-04.html new file mode 100644 index 0000000..e0c12b9 --- /dev/null +++ b/ch8/ch8-04.html @@ -0,0 +1,453 @@ + + + + + + Channels - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

8.4. Channels

+

如果说goroutine是Go语言程序的并发体的话,那么channels则是它们之间的通信机制。一个channel是一个通信机制,它可以让一个goroutine通过它给另一个goroutine发送值信息。每个channel都有一个特殊的类型,也就是channels可发送数据的类型。一个可以发送int类型数据的channel一般写为chan int。

+

使用内置的make函数,我们可以创建一个channel:

+
ch := make(chan int) // ch has type 'chan int'
+
+

和map类似,channel也对应一个make创建的底层数据结构的引用。当我们复制一个channel或用于函数参数传递时,我们只是拷贝了一个channel引用,因此调用者和被调用者将引用同一个channel对象。和其它的引用类型一样,channel的零值也是nil。

+

两个相同类型的channel可以使用==运算符比较。如果两个channel引用的是相同的对象,那么比较的结果为真。一个channel也可以和nil进行比较。

+

一个channel有发送和接受两个主要操作,都是通信行为。一个发送语句将一个值从一个goroutine通过channel发送到另一个执行接收操作的goroutine。发送和接收两个操作都使用<-运算符。在发送语句中,<-运算符分割channel和要发送的值。在接收语句中,<-运算符写在channel对象之前。一个不使用接收结果的接收操作也是合法的。

+
ch <- x  // a send statement
+x = <-ch // a receive expression in an assignment statement
+<-ch     // a receive statement; result is discarded
+
+

Channel还支持close操作,用于关闭channel,随后对基于该channel的任何发送操作都将导致panic异常。对一个已经被close过的channel进行接收操作依然可以接受到之前已经成功发送的数据;如果channel中已经没有数据的话将产生一个零值的数据。

+

使用内置的close函数就可以关闭一个channel:

+
close(ch)
+
+

以最简单方式调用make函数创建的是一个无缓存的channel,但是我们也可以指定第二个整型参数,对应channel的容量。如果channel的容量大于零,那么该channel就是带缓存的channel。

+
ch = make(chan int)    // unbuffered channel
+ch = make(chan int, 0) // unbuffered channel
+ch = make(chan int, 3) // buffered channel with capacity 3
+
+

我们将先讨论无缓存的channel,然后在8.4.4节讨论带缓存的channel。

+

8.4.1. 不带缓存的Channels

+

一个基于无缓存Channels的发送操作将导致发送者goroutine阻塞,直到另一个goroutine在相同的Channels上执行接收操作,当发送的值通过Channels成功传输之后,两个goroutine可以继续执行后面的语句。反之,如果接收操作先发生,那么接收者goroutine也将阻塞,直到有另一个goroutine在相同的Channels上执行发送操作。

+

基于无缓存Channels的发送和接收操作将导致两个goroutine做一次同步操作。因为这个原因,无缓存Channels有时候也被称为同步Channels。当通过一个无缓存Channels发送数据时,接收者收到数据发生在再次唤醒发送者goroutine之前(译注:happens before,这是Go语言并发内存模型的一个关键术语!)。

+

在讨论并发编程时,当我们说x事件在y事件之前发生(happens before),我们并不是说x事件在时间上比y时间更早;我们要表达的意思是要保证在此之前的事件都已经完成了,例如在此之前的更新某些变量的操作已经完成,你可以放心依赖这些已完成的事件了。

+

当我们说x事件既不是在y事件之前发生也不是在y事件之后发生,我们就说x事件和y事件是并发的。这并不是意味着x事件和y事件就一定是同时发生的,我们只是不能确定这两个事件发生的先后顺序。在下一章中我们将看到,当两个goroutine并发访问了相同的变量时,我们有必要保证某些事件的执行顺序,以避免出现某些并发问题。

+

在8.3节的客户端程序,它在主goroutine中(译注:就是执行main函数的goroutine)将标准输入复制到server,因此当客户端程序关闭标准输入时,后台goroutine可能依然在工作。我们需要让主goroutine等待后台goroutine完成工作后再退出,我们使用了一个channel来同步两个goroutine:

+

gopl.io/ch8/netcat3

+
func main() {
+	conn, err := net.Dial("tcp", "localhost:8000")
+	if err != nil {
+		log.Fatal(err)
+	}
+	done := make(chan struct{})
+	go func() {
+		io.Copy(os.Stdout, conn) // NOTE: ignoring errors
+		log.Println("done")
+		done <- struct{}{} // signal the main goroutine
+	}()
+	mustCopy(conn, os.Stdin)
+	conn.Close()
+	<-done // wait for background goroutine to finish
+}
+
+

当用户关闭了标准输入,主goroutine中的mustCopy函数调用将返回,然后调用conn.Close()关闭读和写方向的网络连接。关闭网络连接中的写方向的连接将导致server程序收到一个文件(end-of-file)结束的信号。关闭网络连接中读方向的连接将导致后台goroutine的io.Copy函数调用返回一个“read from closed connection”(“从关闭的连接读”)类似的错误,因此我们临时移除了错误日志语句;在练习8.3将会提供一个更好的解决方案。(需要注意的是go语句调用了一个函数字面量,这是Go语言中启动goroutine常用的形式。)

+

在后台goroutine返回之前,它先打印一个日志信息,然后向done对应的channel发送一个值。主goroutine在退出前先等待从done对应的channel接收一个值。因此,总是可以在程序退出前正确输出“done”消息。

+

基于channels发送消息有两个重要方面。首先每个消息都有一个值,但是有时候通讯的事实和发生的时刻也同样重要。当我们更希望强调通讯发生的时刻时,我们将它称为消息事件。有些消息事件并不携带额外的信息,它仅仅是用作两个goroutine之间的同步,这时候我们可以用struct{}空结构体作为channels元素的类型,虽然也可以使用bool或int类型实现同样的功能,done <- 1语句也比done <- struct{}{}更短。

+

练习 8.3: 在netcat3例子中,conn虽然是一个interface类型的值,但是其底层真实类型是*net.TCPConn,代表一个TCP连接。一个TCP连接有读和写两个部分,可以使用CloseRead和CloseWrite方法分别关闭它们。修改netcat3的主goroutine代码,只关闭网络连接中写的部分,这样的话后台goroutine可以在标准输入被关闭后继续打印从reverb1服务器传回的数据。(要在reverb2服务器也完成同样的功能是比较困难的;参考练习 8.4。)

+

8.4.2. 串联的Channels(Pipeline)

+

Channels也可以用于将多个goroutine连接在一起,一个Channel的输出作为下一个Channel的输入。这种串联的Channels就是所谓的管道(pipeline)。下面的程序用两个channels将三个goroutine串联起来,如图8.1所示。

+

+

第一个goroutine是一个计数器,用于生成0、1、2、……形式的整数序列,然后通过channel将该整数序列发送给第二个goroutine;第二个goroutine是一个求平方的程序,对收到的每个整数求平方,然后将平方后的结果通过第二个channel发送给第三个goroutine;第三个goroutine是一个打印程序,打印收到的每个整数。为了保持例子清晰,我们有意选择了非常简单的函数,当然三个goroutine的计算很简单,在现实中确实没有必要为如此简单的运算构建三个goroutine。

+

gopl.io/ch8/pipeline1

+
func main() {
+	naturals := make(chan int)
+	squares := make(chan int)
+
+	// Counter
+	go func() {
+		for x := 0; ; x++ {
+			naturals <- x
+		}
+	}()
+
+	// Squarer
+	go func() {
+		for {
+			x := <-naturals
+			squares <- x * x
+		}
+	}()
+
+	// Printer (in main goroutine)
+	for {
+		fmt.Println(<-squares)
+	}
+}
+
+

如您所料,上面的程序将生成0、1、4、9、……形式的无穷数列。像这样的串联Channels的管道(Pipelines)可以用在需要长时间运行的服务中,每个长时间运行的goroutine可能会包含一个死循环,在不同goroutine的死循环内部使用串联的Channels来通信。但是,如果我们希望通过Channels只发送有限的数列该如何处理呢?

+

如果发送者知道,没有更多的值需要发送到channel的话,那么让接收者也能及时知道没有多余的值可接收将是有用的,因为接收者可以停止不必要的接收等待。这可以通过内置的close函数来关闭channel实现:

+
close(naturals)
+
+

当一个channel被关闭后,再向该channel发送数据将导致panic异常。当一个被关闭的channel中已经发送的数据都被成功接收后,后续的接收操作将不再阻塞,它们会立即返回一个零值。关闭上面例子中的naturals变量对应的channel并不能终止循环,它依然会收到一个永无休止的零值序列,然后将它们发送给打印者goroutine。

+

没有办法直接测试一个channel是否被关闭,但是接收操作有一个变体形式:它多接收一个结果,多接收的第二个结果是一个布尔值ok,ture表示成功从channels接收到值,false表示channels已经被关闭并且里面没有值可接收。使用这个特性,我们可以修改squarer函数中的循环代码,当naturals对应的channel被关闭并没有值可接收时跳出循环,并且也关闭squares对应的channel.

+
// Squarer
+go func() {
+	for {
+		x, ok := <-naturals
+		if !ok {
+			break // channel was closed and drained
+		}
+		squares <- x * x
+	}
+	close(squares)
+}()
+
+

因为上面的语法是笨拙的,而且这种处理模式很常见,因此Go语言的range循环可直接在channels上面迭代。使用range循环是上面处理模式的简洁语法,它依次从channel接收数据,当channel被关闭并且没有值可接收时跳出循环。

+

在下面的改进中,我们的计数器goroutine只生成100个含数字的序列,然后关闭naturals对应的channel,这将导致计算平方数的squarer对应的goroutine可以正常终止循环并关闭squares对应的channel。(在一个更复杂的程序中,可以通过defer语句关闭对应的channel。)最后,主goroutine也可以正常终止循环并退出程序。

+

gopl.io/ch8/pipeline2

+
func main() {
+	naturals := make(chan int)
+	squares := make(chan int)
+
+	// Counter
+	go func() {
+		for x := 0; x < 100; x++ {
+			naturals <- x
+		}
+		close(naturals)
+	}()
+
+	// Squarer
+	go func() {
+		for x := range naturals {
+			squares <- x * x
+		}
+		close(squares)
+	}()
+
+	// Printer (in main goroutine)
+	for x := range squares {
+		fmt.Println(x)
+	}
+}
+
+

其实你并不需要关闭每一个channel。只有当需要告诉接收者goroutine,所有的数据已经全部发送时才需要关闭channel。不管一个channel是否被关闭,当它没有被引用时将会被Go语言的垃圾自动回收器回收。(不要将关闭一个打开文件的操作和关闭一个channel操作混淆。对于每个打开的文件,都需要在不使用的时候调用对应的Close方法来关闭文件。)

+

试图重复关闭一个channel将导致panic异常,试图关闭一个nil值的channel也将导致panic异常。关闭一个channels还会触发一个广播机制,我们将在8.9节讨论。

+

8.4.3. 单方向的Channel

+

随着程序的增长,人们习惯于将大的函数拆分为小的函数。我们前面的例子中使用了三个goroutine,然后用两个channels来连接它们,它们都是main函数的局部变量。将三个goroutine拆分为以下三个函数是自然的想法:

+
func counter(out chan int)
+func squarer(out, in chan int)
+func printer(in chan int)
+
+

其中计算平方的squarer函数在两个串联Channels的中间,因此拥有两个channel类型的参数,一个用于输入一个用于输出。两个channel都拥有相同的类型,但是它们的使用方式相反:一个只用于接收,另一个只用于发送。参数的名字in和out已经明确表示了这个意图,但是并无法保证squarer函数向一个in参数对应的channel发送数据或者从一个out参数对应的channel接收数据。

+

这种场景是典型的。当一个channel作为一个函数参数时,它一般总是被专门用于只发送或者只接收。

+

为了表明这种意图并防止被滥用,Go语言的类型系统提供了单方向的channel类型,分别用于只发送或只接收的channel。类型chan<- int表示一个只发送int的channel,只能发送不能接收。相反,类型<-chan int表示一个只接收int的channel,只能接收不能发送。(箭头<-和关键字chan的相对位置表明了channel的方向。)这种限制将在编译期检测。

+

因为关闭操作只用于断言不再向channel发送新的数据,所以只有在发送者所在的goroutine才会调用close函数,因此对一个只接收的channel调用close将是一个编译错误。

+

这是改进的版本,这一次参数使用了单方向channel类型:

+

gopl.io/ch8/pipeline3

+
func counter(out chan<- int) {
+	for x := 0; x < 100; x++ {
+		out <- x
+	}
+	close(out)
+}
+
+func squarer(out chan<- int, in <-chan int) {
+	for v := range in {
+		out <- v * v
+	}
+	close(out)
+}
+
+func printer(in <-chan int) {
+	for v := range in {
+		fmt.Println(v)
+	}
+}
+
+func main() {
+	naturals := make(chan int)
+	squares := make(chan int)
+	go counter(naturals)
+	go squarer(squares, naturals)
+	printer(squares)
+}
+
+

调用counter(naturals)时,naturals的类型将隐式地从chan int转换成chan<- int。调用printer(squares)也会导致相似的隐式转换,这一次是转换为<-chan int类型只接收型的channel。任何双向channel向单向channel变量的赋值操作都将导致该隐式转换。这里并没有反向转换的语法:也就是不能将一个类似chan<- int类型的单向型的channel转换为chan int类型的双向型的channel。

+

8.4.4. 带缓存的Channels

+

带缓存的Channel内部持有一个元素队列。队列的最大容量是在调用make函数创建channel时通过第二个参数指定的。下面的语句创建了一个可以持有三个字符串元素的带缓存Channel。图8.2是ch变量对应的channel的图形表示形式。

+
ch = make(chan string, 3)
+
+

+

向缓存Channel的发送操作就是向内部缓存队列的尾部插入元素,接收操作则是从队列的头部删除元素。如果内部缓存队列是满的,那么发送操作将阻塞直到因另一个goroutine执行接收操作而释放了新的队列空间。相反,如果channel是空的,接收操作将阻塞直到有另一个goroutine执行发送操作而向队列插入元素。

+

我们可以在无阻塞的情况下连续向新创建的channel发送三个值:

+
ch <- "A"
+ch <- "B"
+ch <- "C"
+
+

此刻,channel的内部缓存队列将是满的(图8.3),如果有第四个发送操作将发生阻塞。

+

+

如果我们接收一个值,

+
fmt.Println(<-ch) // "A"
+
+

那么channel的缓存队列将不是满的也不是空的(图8.4),因此对该channel执行的发送或接收操作都不会发生阻塞。通过这种方式,channel的缓存队列解耦了接收和发送的goroutine。

+

+

在某些特殊情况下,程序可能需要知道channel内部缓存的容量,可以用内置的cap函数获取:

+
fmt.Println(cap(ch)) // "3"
+
+

同样,对于内置的len函数,如果传入的是channel,那么将返回channel内部缓存队列中有效元素的个数。因为在并发程序中该信息会随着接收操作而失效,但是它对某些故障诊断和性能优化会有帮助。

+
fmt.Println(len(ch)) // "2"
+
+

在继续执行两次接收操作后channel内部的缓存队列将又成为空的,如果有第四个接收操作将发生阻塞:

+
fmt.Println(<-ch) // "B"
+fmt.Println(<-ch) // "C"
+
+

在这个例子中,发送和接收操作都发生在同一个goroutine中,但是在真实的程序中它们一般由不同的goroutine执行。Go语言新手有时候会将一个带缓存的channel当作同一个goroutine中的队列使用,虽然语法看似简单,但实际上这是一个错误。Channel和goroutine的调度器机制是紧密相连的,如果没有其他goroutine从channel接收,发送者——或许是整个程序——将会面临永远阻塞的风险。如果你只是需要一个简单的队列,使用slice就可以了。

+

下面的例子展示了一个使用了带缓存channel的应用。它并发地向三个镜像站点发出请求,三个镜像站点分散在不同的地理位置。它们分别将收到的响应发送到带缓存channel,最后接收者只接收第一个收到的响应,也就是最快的那个响应。因此mirroredQuery函数可能在另外两个响应慢的镜像站点响应之前就返回了结果。(顺便说一下,多个goroutines并发地向同一个channel发送数据,或从同一个channel接收数据都是常见的用法。)

+
func mirroredQuery() string {
+	responses := make(chan string, 3)
+	go func() { responses <- request("asia.gopl.io") }()
+	go func() { responses <- request("europe.gopl.io") }()
+	go func() { responses <- request("americas.gopl.io") }()
+	return <-responses // return the quickest response
+}
+
+func request(hostname string) (response string) { /* ... */ }
+
+

如果我们使用了无缓存的channel,那么两个慢的goroutines将会因为没有人接收而被永远卡住。这种情况,称为goroutines泄漏,这将是一个BUG。和垃圾变量不同,泄漏的goroutines并不会被自动回收,因此确保每个不再需要的goroutine能正常退出是重要的。

+

关于无缓存或带缓存channels之间的选择,或者是带缓存channels的容量大小的选择,都可能影响程序的正确性。无缓存channel更强地保证了每个发送操作与相应的同步接收操作;但是对于带缓存channel,这些操作是解耦的。同样,即使我们知道将要发送到一个channel的信息的数量上限,创建一个对应容量大小的带缓存channel也是不现实的,因为这要求在执行任何接收操作之前缓存所有已经发送的值。如果未能分配足够的缓存将导致程序死锁。

+

Channel的缓存也可能影响程序的性能。想象一家蛋糕店有三个厨师,一个烘焙,一个上糖衣,还有一个将每个蛋糕传递到它下一个厨师的生产线。在狭小的厨房空间环境,每个厨师在完成蛋糕后必须等待下一个厨师已经准备好接受它;这类似于在一个无缓存的channel上进行沟通。

+

如果在每个厨师之间有一个放置一个蛋糕的额外空间,那么每个厨师就可以将一个完成的蛋糕临时放在那里而马上进入下一个蛋糕的制作中;这类似于将channel的缓存队列的容量设置为1。只要每个厨师的平均工作效率相近,那么其中大部分的传输工作将是迅速的,个体之间细小的效率差异将在交接过程中弥补。如果厨师之间有更大的额外空间——也是就更大容量的缓存队列——将可以在不停止生产线的前提下消除更大的效率波动,例如一个厨师可以短暂地休息,然后再加快赶上进度而不影响其他人。

+

另一方面,如果生产线的前期阶段一直快于后续阶段,那么它们之间的缓存在大部分时间都将是满的。相反,如果后续阶段比前期阶段更快,那么它们之间的缓存在大部分时间都将是空的。对于这类场景,额外的缓存并没有带来任何好处。

+

生产线的隐喻对于理解channels和goroutines的工作机制是很有帮助的。例如,如果第二阶段是需要精心制作的复杂操作,一个厨师可能无法跟上第一个厨师的进度,或者是无法满足第三阶段厨师的需求。要解决这个问题,我们可以再雇佣另一个厨师来帮助完成第二阶段的工作,他执行相同的任务但是独立工作。这类似于基于相同的channels创建另一个独立的goroutine。

+

我们没有太多的空间展示全部细节,但是gopl.io/ch8/cake包模拟了这个蛋糕店,可以通过不同的参数调整。它还对上面提到的几种场景提供对应的基准测试(§11.4) 。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch8/ch8-05.html b/ch8/ch8-05.html new file mode 100644 index 0000000..01d481d --- /dev/null +++ b/ch8/ch8-05.html @@ -0,0 +1,388 @@ + + + + + + 并发的循环 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

8.5. 并发的循环

+

本节中,我们会探索一些用来在并行时循环迭代的常见并发模型。我们会探究从全尺寸图片生成一些缩略图的问题。gopl.io/ch8/thumbnail包提供了ImageFile函数来帮我们拉伸图片。我们不会说明这个函数的实现,只需要从gopl.io下载它。

+

gopl.io/ch8/thumbnail

+
package thumbnail
+
+// ImageFile reads an image from infile and writes
+// a thumbnail-size version of it in the same directory.
+// It returns the generated file name, e.g., "foo.thumb.jpg".
+func ImageFile(infile string) (string, error)
+
+

下面的程序会循环迭代一些图片文件名,并为每一张图片生成一个缩略图:

+

gopl.io/ch8/thumbnail

+
// makeThumbnails makes thumbnails of the specified files.
+func makeThumbnails(filenames []string) {
+	for _, f := range filenames {
+		if _, err := thumbnail.ImageFile(f); err != nil {
+			log.Println(err)
+		}
+	}
+}
+
+

显然我们处理文件的顺序无关紧要,因为每一个图片的拉伸操作和其它图片的处理操作都是彼此独立的。像这种子问题都是完全彼此独立的问题被叫做易并行问题(译注:embarrassingly parallel,直译的话更像是尴尬并行)。易并行问题是最容易被实现成并行的一类问题(废话),并且最能够享受到并发带来的好处,能够随着并行的规模线性地扩展。

+

下面让我们并行地执行这些操作,从而将文件IO的延迟隐藏掉,并用上多核cpu的计算能力来拉伸图像。我们的第一个并发程序只是使用了一个go关键字。这里我们先忽略掉错误,之后再进行处理。

+
// NOTE: incorrect!
+func makeThumbnails2(filenames []string) {
+	for _, f := range filenames {
+		go thumbnail.ImageFile(f) // NOTE: ignoring errors
+	}
+}
+
+

这个版本运行的实在有点太快,实际上,由于它比最早的版本使用的时间要短得多,即使当文件名的slice中只包含有一个元素。这就有点奇怪了,如果程序没有并发执行的话,那为什么一个并发的版本还是要快呢?答案其实是makeThumbnails在它还没有完成工作之前就已经返回了。它启动了所有的goroutine,每一个文件名对应一个,但没有等待它们一直到执行完毕。

+

没有什么直接的办法能够等待goroutine完成,但是我们可以改变goroutine里的代码让其能够将完成情况报告给外部的goroutine知晓,使用的方式是向一个共享的channel中发送事件。因为我们已经确切地知道有len(filenames)个内部goroutine,所以外部的goroutine只需要在返回之前对这些事件计数。

+
// makeThumbnails3 makes thumbnails of the specified files in parallel.
+func makeThumbnails3(filenames []string) {
+	ch := make(chan struct{})
+	for _, f := range filenames {
+		go func(f string) {
+			thumbnail.ImageFile(f) // NOTE: ignoring errors
+			ch <- struct{}{}
+		}(f)
+	}
+	// Wait for goroutines to complete.
+	for range filenames {
+		<-ch
+	}
+}
+
+

注意我们将f的值作为一个显式的变量传给了函数,而不是在循环的闭包中声明:

+
for _, f := range filenames {
+	go func() {
+		thumbnail.ImageFile(f) // NOTE: incorrect!
+		// ...
+	}()
+}
+
+

回忆一下之前在5.6.1节中,匿名函数中的循环变量快照问题。上面这个单独的变量f是被所有的匿名函数值所共享,且会被连续的循环迭代所更新的。当新的goroutine开始执行字面函数时,for循环可能已经更新了f并且开始了另一轮的迭代或者(更有可能的)已经结束了整个循环,所以当这些goroutine开始读取f的值时,它们所看到的值已经是slice的最后一个元素了。显式地添加这个参数,我们能够确保使用的f是当go语句执行时的“当前”那个f。

+

如果我们想要从每一个worker goroutine往主goroutine中返回值时该怎么办呢?当我们调用thumbnail.ImageFile创建文件失败的时候,它会返回一个错误。下一个版本的makeThumbnails会返回其在做拉伸操作时接收到的第一个错误:

+
// makeThumbnails4 makes thumbnails for the specified files in parallel.
+// It returns an error if any step failed.
+func makeThumbnails4(filenames []string) error {
+	errors := make(chan error)
+
+	for _, f := range filenames {
+		go func(f string) {
+			_, err := thumbnail.ImageFile(f)
+			errors <- err
+		}(f)
+	}
+
+	for range filenames {
+		if err := <-errors; err != nil {
+			return err // NOTE: incorrect: goroutine leak!
+		}
+	}
+
+	return nil
+}
+
+

这个程序有一个微妙的bug。当它遇到第一个非nil的error时会直接将error返回到调用方,使得没有一个goroutine去排空errors channel。这样剩下的worker goroutine在向这个channel中发送值时,都会永远地阻塞下去,并且永远都不会退出。这种情况叫做goroutine泄露(§8.4.4),可能会导致整个程序卡住或者跑出out of memory的错误。

+

最简单的解决办法就是用一个具有合适大小的buffered channel,这样这些worker goroutine向channel中发送错误时就不会被阻塞。(一个可选的解决办法是创建一个另外的goroutine,当main goroutine返回第一个错误的同时去排空channel。)

+

下一个版本的makeThumbnails使用了一个buffered channel来返回生成的图片文件的名字,附带生成时的错误。

+
// makeThumbnails5 makes thumbnails for the specified files in parallel.
+// It returns the generated file names in an arbitrary order,
+// or an error if any step failed.
+func makeThumbnails5(filenames []string) (thumbfiles []string, err error) {
+	type item struct {
+		thumbfile string
+		err       error
+	}
+
+	ch := make(chan item, len(filenames))
+	for _, f := range filenames {
+		go func(f string) {
+			var it item
+			it.thumbfile, it.err = thumbnail.ImageFile(f)
+			ch <- it
+		}(f)
+	}
+
+	for range filenames {
+		it := <-ch
+		if it.err != nil {
+			return nil, it.err
+		}
+		thumbfiles = append(thumbfiles, it.thumbfile)
+	}
+
+	return thumbfiles, nil
+}
+
+

我们最后一个版本的makeThumbnails返回了新文件们的大小总计数(bytes)。和前面的版本都不一样的一点是我们在这个版本里没有把文件名放在slice里,而是通过一个string的channel传过来,所以我们无法对循环的次数进行预测。

+

为了知道最后一个goroutine什么时候结束(最后一个结束并不一定是最后一个开始),我们需要一个递增的计数器,在每一个goroutine启动时加一,在goroutine退出时减一。这需要一种特殊的计数器,这个计数器需要在多个goroutine操作时做到安全并且提供在其减为零之前一直等待的一种方法。这种计数类型被称为sync.WaitGroup,下面的代码就用到了这种方法:

+
// makeThumbnails6 makes thumbnails for each file received from the channel.
+// It returns the number of bytes occupied by the files it creates.
+func makeThumbnails6(filenames <-chan string) int64 {
+	sizes := make(chan int64)
+	var wg sync.WaitGroup // number of working goroutines
+	for f := range filenames {
+		wg.Add(1)
+		// worker
+		go func(f string) {
+			defer wg.Done()
+			thumb, err := thumbnail.ImageFile(f)
+			if err != nil {
+				log.Println(err)
+				return
+			}
+			info, _ := os.Stat(thumb) // OK to ignore error
+			sizes <- info.Size()
+		}(f)
+	}
+
+	// closer
+	go func() {
+		wg.Wait()
+		close(sizes)
+	}()
+
+	var total int64
+	for size := range sizes {
+		total += size
+	}
+	return total
+}
+
+

注意Add和Done方法的不对称。Add是为计数器加一,必须在worker goroutine开始之前调用,而不是在goroutine中;否则的话我们没办法确定Add是在"closer" goroutine调用Wait之前被调用。并且Add还有一个参数,但Done却没有任何参数;其实它和Add(-1)是等价的。我们使用defer来确保计数器即使是在出错的情况下依然能够正确地被减掉。上面的程序代码结构是当我们使用并发循环,但又不知道迭代次数时很通常而且很地道的写法。

+

sizes channel携带了每一个文件的大小到main goroutine,在main goroutine中使用了range loop来计算总和。观察一下我们是怎样创建一个closer goroutine,并让其在所有worker goroutine们结束之后再关闭sizes channel的。两步操作:wait和close,必须是基于sizes的循环的并发。考虑一下另一种方案:如果等待操作被放在了main goroutine中,在循环之前,这样的话就永远都不会结束了,如果在循环之后,那么又变成了不可达的部分,因为没有任何东西去关闭这个channel,这个循环就永远都不会终止。

+

图8.5 表明了makethumbnails6函数中事件的序列。纵列表示goroutine。窄线段代表sleep,粗线段代表活动。斜线箭头代表用来同步两个goroutine的事件。时间向下流动。注意main goroutine是如何大部分的时间被唤醒执行其range循环,等待worker发送值或者closer来关闭channel的。

+

+

练习 8.4: 修改reverb2服务器,在每一个连接中使用sync.WaitGroup来计数活跃的echo goroutine。当计数减为零时,关闭TCP连接的写入,像练习8.3中一样。验证一下你的修改版netcat3客户端会一直等待所有的并发“喊叫”完成,即使是在标准输入流已经关闭的情况下。

+

练习 8.5: 使用一个已有的CPU绑定的顺序程序,比如在3.3节中我们写的Mandelbrot程序或者3.2节中的3-D surface计算程序,并将他们的主循环改为并发形式,使用channel来进行通信。在多核计算机上这个程序得到了多少速度上的改进?使用多少个goroutine是最合适的呢?

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch8/ch8-06.html b/ch8/ch8-06.html new file mode 100644 index 0000000..cdc08e5 --- /dev/null +++ b/ch8/ch8-06.html @@ -0,0 +1,369 @@ + + + + + + 示例: 并发的Web爬虫 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

8.6. 示例: 并发的Web爬虫

+

在5.6节中,我们做了一个简单的web爬虫,用bfs(广度优先)算法来抓取整个网站。在本节中,我们会让这个爬虫并行化,这样每一个彼此独立的抓取命令可以并行进行IO,最大化利用网络资源。crawl函数和gopl.io/ch5/findlinks3中的是一样的。

+

gopl.io/ch8/crawl1

+
func crawl(url string) []string {
+	fmt.Println(url)
+	list, err := links.Extract(url)
+	if err != nil {
+		log.Print(err)
+	}
+	return list
+}
+
+

主函数和5.6节中的breadthFirst(广度优先)类似。像之前一样,一个worklist是一个记录了需要处理的元素的队列,每一个元素都是一个需要抓取的URL列表,不过这一次我们用channel代替slice来做这个队列。每一个对crawl的调用都会在他们自己的goroutine中进行并且会把他们抓到的链接发送回worklist。

+
func main() {
+	worklist := make(chan []string)
+
+	// Start with the command-line arguments.
+	go func() { worklist <- os.Args[1:] }()
+
+	// Crawl the web concurrently.
+	seen := make(map[string]bool)
+	for list := range worklist {
+		for _, link := range list {
+			if !seen[link] {
+				seen[link] = true
+				go func(link string) {
+					worklist <- crawl(link)
+				}(link)
+			}
+		}
+	}
+}
+
+

注意这里的crawl所在的goroutine会将link作为一个显式的参数传入,来避免“循环变量快照”的问题(在5.6.1中有讲解)。另外注意这里将命令行参数传入worklist也是在一个另外的goroutine中进行的,这是为了避免channel两端的main goroutine与crawler goroutine都尝试向对方发送内容,却没有一端接收内容时发生死锁。当然,这里我们也可以用buffered channel来解决问题,这里不再赘述。

+

现在爬虫可以高并发地运行起来,并且可以产生一大坨的URL了,不过还是会有俩问题。一个问题是在运行一段时间后可能会出现在log的错误信息里的:

+
$ go build gopl.io/ch8/crawl1
+$ ./crawl1 http://gopl.io/
+http://gopl.io/
+https://golang.org/help/
+https://golang.org/doc/
+https://golang.org/blog/
+...
+2015/07/15 18:22:12 Get ...: dial tcp: lookup blog.golang.org: no such host
+2015/07/15 18:22:12 Get ...: dial tcp 23.21.222.120:443: socket: too many open files
+...
+
+

最初的错误信息是一个让人莫名的DNS查找失败,即使这个域名是完全可靠的。而随后的错误信息揭示了原因:这个程序一次性创建了太多网络连接,超过了每一个进程的打开文件数限制,既而导致了在调用net.Dial像DNS查找失败这样的问题。

+

这个程序实在是太他妈并行了。无穷无尽地并行化并不是什么好事情,因为不管怎么说,你的系统总是会有一些个限制因素,比如CPU核心数会限制你的计算负载,比如你的硬盘转轴和磁头数限制了你的本地磁盘IO操作频率,比如你的网络带宽限制了你的下载速度上限,或者是你的一个web服务的服务容量上限等等。为了解决这个问题,我们可以限制并发程序所使用的资源来使之适应自己的运行环境。对于我们的例子来说,最简单的方法就是限制对links.Extract在同一时间最多不会有超过n次调用,这里的n一般小于文件描述符的上限值,比如20。这和一个夜店里限制客人数目是一个道理,只有当有客人离开时,才会允许新的客人进入店内。

+

我们可以用一个有容量限制的buffered channel来控制并发,这类似于操作系统里的计数信号量概念。从概念上讲,channel里的n个空槽代表n个可以处理内容的token(通行证),从channel里接收一个值会释放其中的一个token,并且生成一个新的空槽位。这样保证了在没有接收介入时最多有n个发送操作。(这里可能我们拿channel里填充的槽来做token更直观一些,不过还是这样吧。)由于channel里的元素类型并不重要,我们用一个零值的struct{}来作为其元素。

+

让我们重写crawl函数,将对links.Extract的调用操作用获取、释放token的操作包裹起来,来确保同一时间对其只有20个调用。信号量数量和其能操作的IO资源数量应保持接近。

+

gopl.io/ch8/crawl2

+
// tokens is a counting semaphore used to
+// enforce a limit of 20 concurrent requests.
+var tokens = make(chan struct{}, 20)
+
+func crawl(url string) []string {
+	fmt.Println(url)
+	tokens <- struct{}{} // acquire a token
+	list, err := links.Extract(url)
+	<-tokens // release the token
+	if err != nil {
+		log.Print(err)
+	}
+	return list
+}
+
+

第二个问题是这个程序永远都不会终止,即使它已经爬到了所有初始链接衍生出的链接。(当然,除非你慎重地选择了合适的初始化URL或者已经实现了练习8.6中的深度限制,你应该还没有意识到这个问题。)为了使这个程序能够终止,我们需要在worklist为空或者没有crawl的goroutine在运行时退出主循环。

+
func main() {
+	worklist := make(chan []string)
+	var n int // number of pending sends to worklist
+
+	// Start with the command-line arguments.
+	n++
+	go func() { worklist <- os.Args[1:] }()
+
+	// Crawl the web concurrently.
+	seen := make(map[string]bool)
+
+	for ; n > 0; n-- {
+		list := <-worklist
+		for _, link := range list {
+			if !seen[link] {
+				seen[link] = true
+				n++
+				go func(link string) {
+					worklist <- crawl(link)
+				}(link)
+			}
+		}
+	}
+}
+
+

这个版本中,计数器n对worklist的发送操作数量进行了限制。每一次我们发现有元素需要被发送到worklist时,我们都会对n进行++操作,在向worklist中发送初始的命令行参数之前,我们也进行过一次++操作。这里的操作++是在每启动一个crawler的goroutine之前。主循环会在n减为0时终止,这时候说明没活可干了。

+

现在这个并发爬虫会比5.6节中的深度优先搜索版快上20倍,而且不会出什么错,并且在其完成任务时也会正确地终止。

+

下面的程序是避免过度并发的另一种思路。这个版本使用了原来的crawl函数,但没有使用计数信号量,取而代之用了20个常驻的crawler goroutine,这样来保证最多20个HTTP请求在并发。

+
func main() {
+	worklist := make(chan []string)  // lists of URLs, may have duplicates
+	unseenLinks := make(chan string) // de-duplicated URLs
+
+	// Add command-line arguments to worklist.
+	go func() { worklist <- os.Args[1:] }()
+
+	// Create 20 crawler goroutines to fetch each unseen link.
+	for i := 0; i < 20; i++ {
+		go func() {
+			for link := range unseenLinks {
+				foundLinks := crawl(link)
+				go func() { worklist <- foundLinks }()
+			}
+		}()
+	}
+
+	// The main goroutine de-duplicates worklist items
+	// and sends the unseen ones to the crawlers.
+	seen := make(map[string]bool)
+	for list := range worklist {
+		for _, link := range list {
+			if !seen[link] {
+				seen[link] = true
+				unseenLinks <- link
+			}
+		}
+	}
+}
+
+

所有的爬虫goroutine现在都是被同一个channel - unseenLinks喂饱的了。主goroutine负责拆分它从worklist里拿到的元素,然后把没有抓过的经由unseenLinks channel发送给一个爬虫的goroutine。

+

seen这个map被限定在main goroutine中;也就是说这个map只能在main goroutine中进行访问。类似于其它的信息隐藏方式,这样的约束可以让我们从一定程度上保证程序的正确性。例如,内部变量不能够在函数外部被访问到;变量(§2.3.4)在没有发生变量逃逸(译注:局部变量被全局变量引用地址导致变量被分配在堆上)的情况下是无法在函数外部访问的;一个对象的封装字段无法被该对象的方法以外的方法访问到。在所有的情况下,信息隐藏都可以帮助我们约束我们的程序,使其不发生意料之外的情况。

+

crawl函数爬到的链接在一个专有的goroutine中被发送到worklist中来避免死锁。为了节省篇幅,这个例子的终止问题我们先不进行详细阐述了。

+

练习 8.6: 为并发爬虫增加深度限制。也就是说,如果用户设置了depth=3,那么只有从首页跳转三次以内能够跳到的页面才能被抓取到。

+

练习 8.7: 完成一个并发程序来创建一个线上网站的本地镜像,把该站点的所有可达的页面都抓取到本地硬盘。为了省事,我们这里可以只取出现在该域下的所有页面(比如golang.org开头,译注:外链的应该就不算了。)当然了,出现在页面里的链接你也需要进行一些处理,使其能够在你的镜像站点上进行跳转,而不是指向原始的链接。

+

译注: +拓展阅读 Handling 1 Million Requests per Minute with Go

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch8/ch8-07.html b/ch8/ch8-07.html new file mode 100644 index 0000000..57d2131 --- /dev/null +++ b/ch8/ch8-07.html @@ -0,0 +1,337 @@ + + + + + + 基于select的多路复用 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

8.7. 基于select的多路复用

+

下面的程序会进行火箭发射的倒计时。time.Tick函数返回一个channel,程序会周期性地像一个节拍器一样向这个channel发送事件。每一个事件的值是一个时间戳,不过更有意思的是其传送方式。

+

gopl.io/ch8/countdown1

+
func main() {
+	fmt.Println("Commencing countdown.")
+	tick := time.Tick(1 * time.Second)
+	for countdown := 10; countdown > 0; countdown-- {
+		fmt.Println(countdown)
+		<-tick
+	}
+	launch()
+}
+
+

现在我们让这个程序支持在倒计时中,用户按下return键时直接中断发射流程。首先,我们启动一个goroutine,这个goroutine会尝试从标准输入中读入一个单独的byte并且,如果成功了,会向名为abort的channel发送一个值。

+

gopl.io/ch8/countdown2

+
abort := make(chan struct{})
+go func() {
+	os.Stdin.Read(make([]byte, 1)) // read a single byte
+	abort <- struct{}{}
+}()
+
+

现在每一次计数循环的迭代都需要等待两个channel中的其中一个返回事件了:当一切正常时的ticker channel(就像NASA jorgon的"nominal",译注:这梗估计我们是不懂了)或者异常时返回的abort事件。我们无法做到从每一个channel中接收信息,如果我们这么做的话,如果第一个channel中没有事件发过来那么程序就会立刻被阻塞,这样我们就无法收到第二个channel中发过来的事件。这时候我们需要多路复用(multiplex)这些操作了,为了能够多路复用,我们使用了select语句。

+
select {
+case <-ch1:
+	// ...
+case x := <-ch2:
+	// ...use x...
+case ch3 <- y:
+	// ...
+default:
+	// ...
+}
+
+

上面是select语句的一般形式。和switch语句稍微有点相似,也会有几个case和最后的default选择分支。每一个case代表一个通信操作(在某个channel上进行发送或者接收),并且会包含一些语句组成的一个语句块。一个接收表达式可能只包含接收表达式自身(译注:不把接收到的值赋值给变量什么的),就像上面的第一个case,或者包含在一个简短的变量声明中,像第二个case里一样;第二种形式让你能够引用接收到的值。

+

select会等待case中有能够执行的case时去执行。当条件满足时,select才会去通信并执行case之后的语句;这时候其它通信是不会执行的。一个没有任何case的select语句写作select{},会永远地等待下去。

+

让我们回到我们的火箭发射程序。time.After函数会立即返回一个channel,并起一个新的goroutine在经过特定的时间后向该channel发送一个独立的值。下面的select语句会一直等待直到两个事件中的一个到达,无论是abort事件或者一个10秒经过的事件。如果10秒经过了还没有abort事件进入,那么火箭就会发射。

+
func main() {
+	// ...create abort channel...
+
+	fmt.Println("Commencing countdown.  Press return to abort.")
+	select {
+	case <-time.After(10 * time.Second):
+		// Do nothing.
+	case <-abort:
+		fmt.Println("Launch aborted!")
+		return
+	}
+	launch()
+}
+
+

下面这个例子更微妙。ch这个channel的buffer大小是1,所以会交替的为空或为满,所以只有一个case可以进行下去,无论i是奇数或者偶数,它都会打印0 2 4 6 8。

+
ch := make(chan int, 1)
+for i := 0; i < 10; i++ {
+	select {
+	case x := <-ch:
+		fmt.Println(x) // "0" "2" "4" "6" "8"
+	case ch <- i:
+	}
+}
+
+

如果多个case同时就绪时,select会随机地选择一个执行,这样来保证每一个channel都有平等的被select的机会。增加前一个例子的buffer大小会使其输出变得不确定,因为当buffer既不为满也不为空时,select语句的执行情况就像是抛硬币的行为一样是随机的。

+

下面让我们的发射程序打印倒计时。这里的select语句会使每次循环迭代等待一秒来执行退出操作。

+

gopl.io/ch8/countdown3

+
func main() {
+	// ...create abort channel...
+
+	fmt.Println("Commencing countdown.  Press return to abort.")
+	tick := time.Tick(1 * time.Second)
+	for countdown := 10; countdown > 0; countdown-- {
+		fmt.Println(countdown)
+		select {
+		case <-tick:
+			// Do nothing.
+		case <-abort:
+			fmt.Println("Launch aborted!")
+			return
+		}
+	}
+	launch()
+}
+
+

time.Tick函数表现得好像它创建了一个在循环中调用time.Sleep的goroutine,每次被唤醒时发送一个事件。当countdown函数返回时,它会停止从tick中接收事件,但是ticker这个goroutine还依然存活,继续徒劳地尝试向channel中发送值,然而这时候已经没有其它的goroutine会从该channel中接收值了——这被称为goroutine泄露(§8.4.4)。

+

Tick函数挺方便,但是只有当程序整个生命周期都需要这个时间时我们使用它才比较合适。否则的话,我们应该使用下面的这种模式:

+
ticker := time.NewTicker(1 * time.Second)
+<-ticker.C    // receive from the ticker's channel
+ticker.Stop() // cause the ticker's goroutine to terminate
+
+

有时候我们希望能够从channel中发送或者接收值,并避免因为发送或者接收导致的阻塞,尤其是当channel没有准备好写或者读时。select语句就可以实现这样的功能。select会有一个default来设置当其它的操作都不能够马上被处理时程序需要执行哪些逻辑。

+

下面的select语句会在abort channel中有值时,从其中接收值;无值时什么都不做。这是一个非阻塞的接收操作;反复地做这样的操作叫做“轮询channel”。

+
select {
+case <-abort:
+	fmt.Printf("Launch aborted!\n")
+	return
+default:
+	// do nothing
+}
+
+

channel的零值是nil。也许会让你觉得比较奇怪,nil的channel有时候也是有一些用处的。因为对一个nil的channel发送和接收操作会永远阻塞,在select语句中操作nil的channel永远都不会被select到。

+

这使得我们可以用nil来激活或者禁用case,来达成处理其它输入或输出事件时超时和取消的逻辑。我们会在下一节中看到一个例子。

+

练习 8.8: 使用select来改造8.3节中的echo服务器,为其增加超时,这样服务器可以在客户端10秒中没有任何喊话时自动断开连接。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch8/ch8-09.html b/ch8/ch8-09.html new file mode 100644 index 0000000..5ce43d3 --- /dev/null +++ b/ch8/ch8-09.html @@ -0,0 +1,302 @@ + + + + + + 并发的退出 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

8.9. 并发的退出

+

有时候我们需要通知goroutine停止它正在干的事情,比如一个正在执行计算的web服务,然而它的客户端已经断开了和服务端的连接。

+

Go语言并没有提供在一个goroutine中终止另一个goroutine的方法,由于这样会导致goroutine之间的共享变量落在未定义的状态上。在8.7节中的rocket launch程序中,我们往名字叫abort的channel里发送了一个简单的值,在countdown的goroutine中会把这个值理解为自己的退出信号。但是如果我们想要退出两个或者任意多个goroutine怎么办呢?

+

一种可能的手段是向abort的channel里发送和goroutine数目一样多的事件来退出它们。如果这些goroutine中已经有一些自己退出了,那么会导致我们的channel里的事件数比goroutine还多,这样导致我们的发送直接被阻塞。另一方面,如果这些goroutine又生成了其它的goroutine,我们的channel里的数目又太少了,所以有些goroutine可能会无法接收到退出消息。一般情况下我们是很难知道在某一个时刻具体有多少个goroutine在运行着的。另外,当一个goroutine从abort channel中接收到一个值的时候,他会消费掉这个值,这样其它的goroutine就没法看到这条信息。为了能够达到我们退出goroutine的目的,我们需要更靠谱的策略,来通过一个channel把消息广播出去,这样goroutine们能够看到这条事件消息,并且在事件完成之后,可以知道这件事已经发生过了。

+

回忆一下我们关闭了一个channel并且被消费掉了所有已发送的值,操作channel之后的代码可以立即被执行,并且会产生零值。我们可以将这个机制扩展一下,来作为我们的广播机制:不要向channel发送值,而是用关闭一个channel来进行广播。

+

只要一些小修改,我们就可以把退出逻辑加入到前一节的du程序。首先,我们创建一个退出的channel,不需要向这个channel发送任何值,但其所在的闭包内要写明程序需要退出。我们同时还定义了一个工具函数,cancelled,这个函数在被调用的时候会轮询退出状态。

+

gopl.io/ch8/du4

+
var done = make(chan struct{})
+
+func cancelled() bool {
+	select {
+	case <-done:
+		return true
+	default:
+		return false
+	}
+}
+
+

下面我们创建一个从标准输入流中读取内容的goroutine,这是一个比较典型的连接到终端的程序。每当有输入被读到(比如用户按了回车键),这个goroutine就会把取消消息通过关闭done的channel广播出去。

+
// Cancel traversal when input is detected.
+go func() {
+	os.Stdin.Read(make([]byte, 1)) // read a single byte
+	close(done)
+}()
+
+

现在我们需要使我们的goroutine来对取消进行响应。在main goroutine中,我们添加了select的第三个case语句,尝试从done channel中接收内容。如果这个case被满足的话,在select到的时候即会返回,但在结束之前我们需要把fileSizes channel中的内容“排”空,在channel被关闭之前,舍弃掉所有值。这样可以保证对walkDir的调用不要被向fileSizes发送信息阻塞住,可以正确地完成。

+
for {
+	select {
+	case <-done:
+		// Drain fileSizes to allow existing goroutines to finish.
+		for range fileSizes {
+			// Do nothing.
+		}
+		return
+	case size, ok := <-fileSizes:
+		// ...
+	}
+}
+
+

walkDir这个goroutine一启动就会轮询取消状态,如果取消状态被设置的话会直接返回,并且不做额外的事情。这样我们将所有在取消事件之后创建的goroutine改变为无操作。

+
func walkDir(dir string, n *sync.WaitGroup, fileSizes chan<- int64) {
+	defer n.Done()
+	if cancelled() {
+		return
+	}
+	for _, entry := range dirents(dir) {
+		// ...
+	}
+}
+
+

在walkDir函数的循环中我们对取消状态进行轮询可以带来明显的益处,可以避免在取消事件发生时还去创建goroutine。取消本身是有一些代价的;想要快速的响应需要对程序逻辑进行侵入式的修改。确保在取消发生之后不要有代价太大的操作可能会需要修改你代码里的很多地方,但是在一些重要的地方去检查取消事件也确实能带来很大的好处。

+

对这个程序的一个简单的性能分析可以揭示瓶颈在dirents函数中获取一个信号量。下面的select可以让这种操作可以被取消,并且可以将取消时的延迟从几百毫秒降低到几十毫秒。

+
func dirents(dir string) []os.FileInfo {
+	select {
+	case sema <- struct{}{}: // acquire token
+	case <-done:
+		return nil // cancelled
+	}
+	defer func() { <-sema }() // release token
+	// ...read directory...
+}
+
+

现在当取消发生时,所有后台的goroutine都会迅速停止并且主函数会返回。当然,当主函数返回时,一个程序会退出,而我们又无法在主函数退出的时候确认其已经释放了所有的资源(译注:因为程序都退出了,你的代码都没法执行了)。这里有一个方便的窍门我们可以一用:取代掉直接从主函数返回,我们调用一个panic,然后runtime会把每一个goroutine的栈dump下来。如果main goroutine是唯一一个剩下的goroutine的话,他会清理掉自己的一切资源。但是如果还有其它的goroutine没有退出,他们可能没办法被正确地取消掉,也有可能被取消但是取消操作会很花时间;所以这里的一个调研还是很有必要的。我们用panic来获取到足够的信息来验证我们上面的判断,看看最终到底是什么样的情况。

+

练习 8.10: HTTP请求可能会因http.Request结构体中Cancel channel的关闭而取消。修改8.6节中的web crawler来支持取消http请求。(提示:http.Get并没有提供方便地定制一个请求的方法。你可以用http.NewRequest来取而代之,设置它的Cancel字段,然后用http.DefaultClient.Do(req)来进行这个http请求。)

+

练习 8.11: 紧接着8.4.4中的mirroredQuery流程,实现一个并发请求url的fetch的变种。当第一个请求返回时,直接取消其它的请求。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch8/ch8-10.html b/ch8/ch8-10.html new file mode 100644 index 0000000..6d5a39c --- /dev/null +++ b/ch8/ch8-10.html @@ -0,0 +1,340 @@ + + + + + + 示例: 聊天服务 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

8.10. 示例: 聊天服务

+

我们用一个聊天服务器来终结本章节的内容,这个程序可以让一些用户通过服务器向其它所有用户广播文本消息。这个程序中有四种goroutine。main和broadcaster各自是一个goroutine实例,每一个客户端的连接都会有一个handleConn和clientWriter的goroutine。broadcaster是select用法的不错的样例,因为它需要处理三种不同类型的消息。

+

下面演示的main goroutine的工作,是listen和accept(译注:网络编程里的概念)从客户端过来的连接。对每一个连接,程序都会建立一个新的handleConn的goroutine,就像我们在本章开头的并发的echo服务器里所做的那样。

+

gopl.io/ch8/chat

+
func main() {
+	listener, err := net.Listen("tcp", "localhost:8000")
+	if err != nil {
+		log.Fatal(err)
+	}
+	go broadcaster()
+	for {
+		conn, err := listener.Accept()
+		if err != nil {
+			log.Print(err)
+			continue
+		}
+		go handleConn(conn)
+	}
+}
+
+

然后是broadcaster的goroutine。他的内部变量clients会记录当前建立连接的客户端集合。其记录的内容是每一个客户端的消息发出channel的“资格”信息。

+
type client chan<- string // an outgoing message channel
+
+var (
+	entering = make(chan client)
+	leaving  = make(chan client)
+	messages = make(chan string) // all incoming client messages
+)
+
+func broadcaster() {
+	clients := make(map[client]bool) // all connected clients
+	for {
+		select {
+		case msg := <-messages:
+			// Broadcast incoming message to all
+			// clients' outgoing message channels.
+			for cli := range clients {
+				cli <- msg
+			}
+		case cli := <-entering:
+			clients[cli] = true
+
+		case cli := <-leaving:
+			delete(clients, cli)
+			close(cli)
+		}
+	}
+}
+
+

broadcaster监听来自全局的entering和leaving的channel来获知客户端的到来和离开事件。当其接收到其中的一个事件时,会更新clients集合,当该事件是离开行为时,它会关闭客户端的消息发送channel。broadcaster也会监听全局的消息channel,所有的客户端都会向这个channel中发送消息。当broadcaster接收到什么消息时,就会将其广播至所有连接到服务端的客户端。

+

现在让我们看看每一个客户端的goroutine。handleConn函数会为它的客户端创建一个消息发送channel并通过entering channel来通知客户端的到来。然后它会读取客户端发来的每一行文本,并通过全局的消息channel来将这些文本发送出去,并为每条消息带上发送者的前缀来标明消息身份。当客户端发送完毕后,handleConn会通过leaving这个channel来通知客户端的离开并关闭连接。

+
func handleConn(conn net.Conn) {
+	ch := make(chan string) // outgoing client messages
+	go clientWriter(conn, ch)
+
+	who := conn.RemoteAddr().String()
+	ch <- "You are " + who
+	messages <- who + " has arrived"
+	entering <- ch
+
+	input := bufio.NewScanner(conn)
+	for input.Scan() {
+		messages <- who + ": " + input.Text()
+	}
+	// NOTE: ignoring potential errors from input.Err()
+
+	leaving <- ch
+	messages <- who + " has left"
+	conn.Close()
+}
+
+func clientWriter(conn net.Conn, ch <-chan string) {
+	for msg := range ch {
+		fmt.Fprintln(conn, msg) // NOTE: ignoring network errors
+	}
+}
+
+

另外,handleConn为每一个客户端创建了一个clientWriter的goroutine,用来接收向客户端发送消息的channel中的广播消息,并将它们写入到客户端的网络连接。客户端的读取循环会在broadcaster接收到leaving通知并关闭了channel后终止。

+

下面演示的是当服务器有两个活动的客户端连接,并且在两个窗口中运行的情况,使用netcat来聊天:

+
$ go build gopl.io/ch8/chat
+$ go build gopl.io/ch8/netcat3
+$ ./chat &
+$ ./netcat3
+You are 127.0.0.1:64208               $ ./netcat3
+127.0.0.1:64211 has arrived           You are 127.0.0.1:64211
+Hi!
+127.0.0.1:64208: Hi!                  127.0.0.1:64208: Hi!
+                                      Hi yourself.
+127.0.0.1:64211: Hi yourself.         127.0.0.1:64211: Hi yourself.
+^C
+                                      127.0.0.1:64208 has left
+$ ./netcat3
+You are 127.0.0.1:64216               127.0.0.1:64216 has arrived
+                                      Welcome.
+127.0.0.1:64211: Welcome.             127.0.0.1:64211: Welcome.
+                                      ^C
+127.0.0.1:64211 has left”
+
+

当与n个客户端保持聊天session时,这个程序会有2n+2个并发的goroutine,然而这个程序却并不需要显式的锁(§9.2)。clients这个map被限制在了一个独立的goroutine中,broadcaster,所以它不能被并发地访问。多个goroutine共享的变量只有这些channel和net.Conn的实例,两个东西都是并发安全的。我们会在下一章中更多地讲解约束,并发安全以及goroutine中共享变量的含义。

+

练习 8.12: 使broadcaster能够在每个新的客户端到来时通知它当前的客户端集合。这需要你在clients集合中,以及entering和leaving的channel中记录客户端的名字。

+

练习 8.13: 使聊天服务器能够断开空闲的客户端连接,比如最近五分钟之后没有发送任何消息的那些客户端。提示:可以在其它goroutine中调用conn.Close()来解除Read调用,就像input.Scanner()所做的那样。

+

练习 8.14: 修改聊天服务器的网络协议,这样每一个客户端就可以在entering时提供他们的名字。将消息前缀由之前的网络地址改为这个名字。

+

练习 8.15: 如果一个客户端没有及时地读取数据可能会导致所有的客户端被阻塞。修改broadcaster来跳过一条消息,而不是等待这个客户端一直到其准备好读写。或者为每一个客户端的消息发送channel建立缓冲区,这样大部分的消息便不会被丢掉;broadcaster应该用一个非阻塞的send向这个channel中发消息。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch8/ch8.html b/ch8/ch8.html new file mode 100644 index 0000000..3006218 --- /dev/null +++ b/ch8/ch8.html @@ -0,0 +1,241 @@ + + + + + + Goroutines和Channels - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

第8章 Goroutines和Channels

+

并发程序指同时进行多个任务的程序,随着硬件的发展,并发程序变得越来越重要。Web服务器会一次处理成千上万的请求。平板电脑和手机app在渲染用户画面同时还会后台执行各种计算任务和网络请求。即使是传统的批处理问题——读取数据、计算、写输出,现在也会用并发来隐藏掉I/O的操作延迟以充分利用现代计算机设备的多个核心。计算机的性能每年都在以非线性的速度增长。

+

Go语言中的并发程序可以用两种手段来实现。本章讲解goroutine和channel,其支持“顺序通信进程”(communicating sequential processes)或被简称为CSP。CSP是一种现代的并发编程模型,在这种编程模型中值会在不同的运行实例(goroutine)中传递,尽管大多数情况下仍然是被限制在单一实例中。第9章覆盖更为传统的并发模型:多线程共享内存,如果你在其它的主流语言中写过并发程序的话可能会更熟悉一些。第9章也会深入介绍一些并发程序带来的风险和陷阱。

+

尽管Go对并发的支持是众多强力特性之一,但跟踪调试并发程序还是很困难,在线性程序中形成的直觉往往还会使我们误入歧途。如果这是读者第一次接触并发,推荐稍微多花一些时间来思考这两个章节中的样例。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch9/ch9-01.html b/ch9/ch9-01.html new file mode 100644 index 0000000..ce64f2d --- /dev/null +++ b/ch9/ch9-01.html @@ -0,0 +1,364 @@ + + + + + + 竞争条件 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

9.1. 竞争条件

+

在一个线性(就是说只有一个goroutine的)的程序中,程序的执行顺序只由程序的逻辑来决定。例如,我们有一段语句序列,第一个在第二个之前(废话),以此类推。在有两个或更多goroutine的程序中,每一个goroutine内的语句也是按照既定的顺序去执行的,但是一般情况下我们没法去知道分别位于两个goroutine的事件x和y的执行顺序,x是在y之前还是之后还是同时发生是没法判断的。当我们没有办法自信地确认一个事件是在另一个事件的前面或者后面发生的话,就说明x和y这两个事件是并发的。

+

考虑一下,一个函数在线性程序中可以正确地工作。如果在并发的情况下,这个函数依然可以正确地工作的话,那么我们就说这个函数是并发安全的,并发安全的函数不需要额外的同步工作。我们可以把这个概念概括为一个特定类型的一些方法和操作函数,对于某个类型来说,如果其所有可访问的方法和操作都是并发安全的话,那么该类型便是并发安全的。

+

在一个程序中有非并发安全的类型的情况下,我们依然可以使这个程序并发安全。确实,并发安全的类型是例外,而不是规则,所以只有当文档中明确地说明了其是并发安全的情况下,你才可以并发地去访问它。我们会避免并发访问大多数的类型,无论是将变量局限在单一的一个goroutine内,还是用互斥条件维持更高级别的不变性,都是为了这个目的。我们会在本章中说明这些术语。

+

相反,包级别的导出函数一般情况下都是并发安全的。由于package级的变量没法被限制在单一的gorouine,所以修改这些变量“必须”使用互斥条件。

+

一个函数在并发调用时没法工作的原因太多了,比如死锁(deadlock)、活锁(livelock)和饿死(resource starvation)。我们没有空去讨论所有的问题,这里我们只聚焦在竞争条件上。

+

竞争条件指的是程序在多个goroutine交叉执行操作时,没有给出正确的结果。竞争条件是很恶劣的一种场景,因为这种问题会一直潜伏在你的程序里,然后在非常少见的时候蹦出来,或许只是会在很大的负载时才会发生,又或许是会在使用了某一个编译器、某一种平台或者某一种架构的时候才会出现。这些使得竞争条件带来的问题非常难以复现而且难以分析诊断。

+

传统上经常用经济损失来为竞争条件做比喻,所以我们来看一个简单的银行账户程序。

+
// Package bank implements a bank with only one account.
+package bank
+var balance int
+func Deposit(amount int) { balance = balance + amount }
+func Balance() int { return balance }
+
+

(当然我们也可以把Deposit存款函数写成balance += amount,这种形式也是等价的,不过长一些的形式解释起来更方便一些。)

+

对于这个简单的程序而言,我们一眼就能看出,以任意顺序调用函数Deposit和Balance都会得到正确的结果。也就是说,Balance函数会给出之前的所有存入的额度之和。然而,当我们并发地而不是顺序地调用这些函数的话,Balance就再也没办法保证结果正确了。考虑一下下面的两个goroutine,其代表了一个银行联合账户的两笔交易:

+
// Alice:
+go func() {
+	bank.Deposit(200)                // A1
+	fmt.Println("=", bank.Balance()) // A2
+}()
+
+// Bob:
+go bank.Deposit(100)                 // B
+
+

Alice存了$200,然后检查她的余额,同时Bob存了$100。因为A1和A2是和B并发执行的,我们没法预测他们发生的先后顺序。直观地来看的话,我们会认为其执行顺序只有三种可能性:“Alice先”,“Bob先”以及“Alice/Bob/Alice”交错执行。下面的表格会展示经过每一步骤后balance变量的值。引号里的字符串表示余额单。

+
Alice first        Bob first        Alice/Bob/Alice
+          0                0                      0
+  A1    200        B     100             A1     200
+  A2 "= 200"       A1    300             B      300
+  B     300        A2 "= 300"            A2  "= 300"
+
+

所有情况下最终的余额都是$300。唯一的变数是Alice的余额单是否包含了Bob交易,不过无论怎么着客户都不会在意。

+

但是事实是上面的直觉推断是错误的。第四种可能的结果是事实存在的,这种情况下Bob的存款会在Alice存款操作中间,在余额被读到(balance + amount)之后,在余额被更新之前(balance = ...),这样会导致Bob的交易丢失。而这是因为Alice的存款操作A1实际上是两个操作的一个序列,读取然后写;可以称之为A1r和A1w。下面是交叉时产生的问题:

+
Data race
+0
+A1r      0     ... = balance + amount
+B      100
+A1w    200     balance = ...
+A2  "= 200"
+
+

在A1r之后,balance + amount会被计算为200,所以这是A1w会写入的值,并不受其它存款操作的干预。最终的余额是$200。银行的账户上的资产比Bob实际的资产多了$100。(译注:因为丢失了Bob的存款操作,所以其实是说Bob的钱丢了。)

+

这个程序包含了一个特定的竞争条件,叫作数据竞争。无论任何时候,只要有两个goroutine并发访问同一变量,且至少其中的一个是写操作的时候就会发生数据竞争。

+

如果数据竞争的对象是一个比一个机器字(译注:32位机器上一个字=4个字节)更大的类型时,事情就变得更麻烦了,比如interface,string或者slice类型都是如此。下面的代码会并发地更新两个不同长度的slice:

+
var x []int
+go func() { x = make([]int, 10) }()
+go func() { x = make([]int, 1000000) }()
+x[999999] = 1 // NOTE: undefined behavior; memory corruption possible!
+
+

最后一个语句中的x的值是未定义的;其可能是nil,或者也可能是一个长度为10的slice,也可能是一个长度为1,000,000的slice。但是回忆一下slice的三个组成部分:指针(pointer)、长度(length)和容量(capacity)。如果指针是从第一个make调用来,而长度从第二个make来,x就变成了一个混合体,一个自称长度为1,000,000但实际上内部只有10个元素的slice。这样导致的结果是存储999,999元素的位置会碰撞一个遥远的内存位置,这种情况下难以对值进行预测,而且debug也会变成噩梦。这种语义雷区被称为未定义行为,对C程序员来说应该很熟悉;幸运的是在Go语言里造成的麻烦要比C里小得多。

+

尽管并发程序的概念让我们知道并发并不是简单的语句交叉执行。我们将会在9.4节中看到,数据竞争可能会有奇怪的结果。许多程序员,甚至一些非常聪明的人也还是会偶尔提出一些理由来允许数据竞争,比如:“互斥条件代价太高”,“这个逻辑只是用来做logging”,“我不介意丢失一些消息”等等。因为在他们的编译器或者平台上很少遇到问题,可能给了他们错误的信心。一个好的经验法则是根本就没有什么所谓的良性数据竞争。所以我们一定要避免数据竞争,那么在我们的程序中要如何做到呢?

+

我们来重复一下数据竞争的定义,因为实在太重要了:数据竞争会在两个以上的goroutine并发访问相同的变量且至少其中一个为写操作时发生。根据上述定义,有三种方式可以避免数据竞争:

+

第一种方法是不要去写变量。考虑一下下面的map,会被“懒”填充,也就是说在每个key被第一次请求到的时候才会去填值。如果Icon是被顺序调用的话,这个程序会工作很正常,但如果Icon被并发调用,那么对于这个map来说就会存在数据竞争。

+
var icons = make(map[string]image.Image)
+func loadIcon(name string) image.Image
+
+// NOTE: not concurrency-safe!
+func Icon(name string) image.Image {
+	icon, ok := icons[name]
+	if !ok {
+		icon = loadIcon(name)
+		icons[name] = icon
+	}
+	return icon
+}
+
+

反之,如果我们在创建goroutine之前的初始化阶段,就初始化了map中的所有条目并且再也不去修改它们,那么任意数量的goroutine并发访问Icon都是安全的,因为每一个goroutine都只是去读取而已。

+
var icons = map[string]image.Image{
+	"spades.png":   loadIcon("spades.png"),
+	"hearts.png":   loadIcon("hearts.png"),
+	"diamonds.png": loadIcon("diamonds.png"),
+	"clubs.png":    loadIcon("clubs.png"),
+}
+
+// Concurrency-safe.
+func Icon(name string) image.Image { return icons[name] }
+
+

上面的例子里icons变量在包初始化阶段就已经被赋值了,包的初始化是在程序main函数开始执行之前就完成了的。只要初始化完成了,icons就再也不会被修改。数据结构如果从不被修改或是不变量则是并发安全的,无需进行同步。不过显然,如果update操作是必要的,我们就没法用这种方法,比如说银行账户。

+

第二种避免数据竞争的方法是,避免从多个goroutine访问变量。这也是前一章中大多数程序所采用的方法。例如前面的并发web爬虫(§8.6)的main goroutine是唯一一个能够访问seen map的goroutine,而聊天服务器(§8.10)中的broadcaster goroutine是唯一一个能够访问clients map的goroutine。这些变量都被限定在了一个单独的goroutine中。

+

由于其它的goroutine不能够直接访问变量,它们只能使用一个channel来发送请求给指定的goroutine来查询更新变量。这也就是Go的口头禅“不要使用共享数据来通信;使用通信来共享数据”。一个提供对一个指定的变量通过channel来请求的goroutine叫做这个变量的monitor(监控)goroutine。例如broadcaster goroutine会监控clients map的全部访问。

+

下面是一个重写了的银行的例子,这个例子中balance变量被限制在了monitor goroutine中,名为teller:

+

gopl.io/ch9/bank1

+
// Package bank provides a concurrency-safe bank with one account.
+package bank
+
+var deposits = make(chan int) // send amount to deposit
+var balances = make(chan int) // receive balance
+
+func Deposit(amount int) { deposits <- amount }
+func Balance() int       { return <-balances }
+
+func teller() {
+	var balance int // balance is confined to teller goroutine
+	for {
+		select {
+		case amount := <-deposits:
+			balance += amount
+		case balances <- balance:
+		}
+	}
+}
+
+func init() {
+	go teller() // start the monitor goroutine
+}
+
+

即使当一个变量无法在其整个生命周期内被绑定到一个独立的goroutine,绑定依然是并发问题的一个解决方案。例如在一条流水线上的goroutine之间共享变量是很普遍的行为,在这两者间会通过channel来传输地址信息。如果流水线的每一个阶段都能够避免在将变量传送到下一阶段后再去访问它,那么对这个变量的所有访问就是线性的。其效果是变量会被绑定到流水线的一个阶段,传送完之后被绑定到下一个,以此类推。这种规则有时被称为串行绑定。

+

下面的例子中,Cakes会被严格地顺序访问,先是baker gorouine,然后是icer gorouine:

+
type Cake struct{ state string }
+
+func baker(cooked chan<- *Cake) {
+	for {
+		cake := new(Cake)
+		cake.state = "cooked"
+		cooked <- cake // baker never touches this cake again
+	}
+}
+
+func icer(iced chan<- *Cake, cooked <-chan *Cake) {
+	for cake := range cooked {
+		cake.state = "iced"
+		iced <- cake // icer never touches this cake again
+	}
+}
+
+

第三种避免数据竞争的方法是允许很多goroutine去访问变量,但是在同一个时刻最多只有一个goroutine在访问。这种方式被称为“互斥”,在下一节来讨论这个主题。

+

练习 9.1: 给gopl.io/ch9/bank1程序添加一个Withdraw(amount int)取款函数。其返回结果应该要表明事务是成功了还是因为没有足够资金失败了。这条消息会被发送给monitor的goroutine,且消息需要包含取款的额度和一个新的channel,这个新channel会被monitor goroutine来把boolean结果发回给Withdraw。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch9/ch9-02.html b/ch9/ch9-02.html new file mode 100644 index 0000000..8b0e783 --- /dev/null +++ b/ch9/ch9-02.html @@ -0,0 +1,348 @@ + + + + + + sync.Mutex互斥锁 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

9.2. sync.Mutex互斥锁

+

在8.6节中,我们使用了一个buffered channel作为一个计数信号量,来保证最多只有20个goroutine会同时执行HTTP请求。同理,我们可以用一个容量只有1的channel来保证最多只有一个goroutine在同一时刻访问一个共享变量。一个只能为1和0的信号量叫做二元信号量(binary semaphore)。

+

gopl.io/ch9/bank2

+
var (
+	sema    = make(chan struct{}, 1) // a binary semaphore guarding balance
+	balance int
+)
+
+func Deposit(amount int) {
+	sema <- struct{}{} // acquire token
+	balance = balance + amount
+	<-sema // release token
+}
+
+func Balance() int {
+	sema <- struct{}{} // acquire token
+	b := balance
+	<-sema // release token
+	return b
+}
+
+

这种互斥很实用,而且被sync包里的Mutex类型直接支持。它的Lock方法能够获取到token(这里叫锁),并且Unlock方法会释放这个token:

+

gopl.io/ch9/bank3

+
import "sync"
+
+var (
+	mu      sync.Mutex // guards balance
+	balance int
+)
+
+func Deposit(amount int) {
+	mu.Lock()
+	balance = balance + amount
+	mu.Unlock()
+}
+
+func Balance() int {
+	mu.Lock()
+	b := balance
+	mu.Unlock()
+	return b
+}
+
+

每次一个goroutine访问bank变量时(这里只有balance余额变量),它都会调用mutex的Lock方法来获取一个互斥锁。如果其它的goroutine已经获得了这个锁的话,这个操作会被阻塞直到其它goroutine调用了Unlock使该锁变回可用状态。mutex会保护共享变量。惯例来说,被mutex所保护的变量是在mutex变量声明之后立刻声明的。如果你的做法和惯例不符,确保在文档里对你的做法进行说明。

+

在Lock和Unlock之间的代码段中的内容goroutine可以随便读取或者修改,这个代码段叫做临界区。锁的持有者在其他goroutine获取该锁之前需要调用Unlock。goroutine在结束后释放锁是必要的,无论以哪条路径通过函数都需要释放,即使是在错误路径中,也要记得释放。

+

上面的bank程序例证了一种通用的并发模式。一系列的导出函数封装了一个或多个变量,那么访问这些变量唯一的方式就是通过这些函数来做(或者方法,对于一个对象的变量来说)。每一个函数在一开始就获取互斥锁并在最后释放锁,从而保证共享变量不会被并发访问。这种函数、互斥锁和变量的编排叫作监控monitor(这种老式单词的monitor是受“monitor goroutine”的术语启发而来的。两种用法都是一个代理人保证变量被顺序访问)。

+

由于在存款和查询余额函数中的临界区代码这么短——只有一行,没有分支调用——在代码最后去调用Unlock就显得更为直截了当。在更复杂的临界区的应用中,尤其是必须要尽早处理错误并返回的情况下,就很难去(靠人)判断对Lock和Unlock的调用是在所有路径中都能够严格配对的了。Go语言里的defer简直就是这种情况下的救星:我们用defer来调用Unlock,临界区会隐式地延伸到函数作用域的最后,这样我们就从“总要记得在函数返回之后或者发生错误返回时要记得调用一次Unlock”这种状态中获得了解放。Go会自动帮我们完成这些事情。

+
func Balance() int {
+	mu.Lock()
+	defer mu.Unlock()
+	return balance
+}
+
+

上面的例子里Unlock会在return语句读取完balance的值之后执行,所以Balance函数是并发安全的。这带来的另一点好处是,我们再也不需要一个本地变量b了。

+

此外,一个deferred Unlock即使在临界区发生panic时依然会执行,这对于用recover(§5.10)来恢复的程序来说是很重要的。defer调用只会比显式地调用Unlock成本高那么一点点,不过却在很大程度上保证了代码的整洁性。大多数情况下对于并发程序来说,代码的整洁性比过度的优化更重要。如果可能的话尽量使用defer来将临界区扩展到函数的结束。

+

考虑一下下面的Withdraw函数。成功的时候,它会正确地减掉余额并返回true。但如果银行记录资金对交易来说不足,那么取款就会恢复余额,并返回false。

+
// NOTE: not atomic!
+func Withdraw(amount int) bool {
+	Deposit(-amount)
+	if Balance() < 0 {
+		Deposit(amount)
+		return false // insufficient funds
+	}
+	return true
+}
+
+

函数终于给出了正确的结果,但是还有一点讨厌的副作用。当过多的取款操作同时执行时,balance可能会瞬时被减到0以下。这可能会引起一个并发的取款被不合逻辑地拒绝。所以如果Bob尝试买一辆sports car时,Alice可能就没办法为她的早咖啡付款了。这里的问题是取款不是一个原子操作:它包含了三个步骤,每一步都需要去获取并释放互斥锁,但任何一次锁都不会锁上整个取款流程。

+

理想情况下,取款应该只在整个操作中获得一次互斥锁。下面这样的尝试是错误的:

+
// NOTE: incorrect!
+func Withdraw(amount int) bool {
+	mu.Lock()
+	defer mu.Unlock()
+	Deposit(-amount)
+	if Balance() < 0 {
+		Deposit(amount)
+		return false // insufficient funds
+	}
+	return true
+}
+
+

上面这个例子中,Deposit会调用mu.Lock()第二次去获取互斥锁,但因为mutex已经锁上了,而无法被重入(译注:go里没有重入锁,关于重入锁的概念,请参考java)——也就是说没法对一个已经锁上的mutex来再次上锁——这会导致程序死锁,没法继续执行下去,Withdraw会永远阻塞下去。

+

关于Go的mutex不能重入这一点我们有很充分的理由。mutex的目的是确保共享变量在程序执行时的关键点上能够保证不变性。不变性的一层含义是“没有goroutine访问共享变量”,但实际上这里对于mutex保护的变量来说,不变性还包含更深层含义:当一个goroutine获得了一个互斥锁时,它能断定被互斥锁保护的变量正处于不变状态(译注:即没有其他代码块正在读写共享变量),在其获取并保持锁期间,可能会去更新共享变量,这样不变性只是短暂地被破坏,然而当其释放锁之后,锁必须保证共享变量重获不变性并且多个goroutine按顺序访问共享变量。尽管一个可以重入的mutex也可以保证没有其它的goroutine在访问共享变量,但它不具备不变性更深层含义。(译注:更详细的解释,Russ Cox认为可重入锁是bug的温床,是一个失败的设计)

+

一个通用的解决方案是将一个函数分离为多个函数,比如我们把Deposit分离成两个:一个不导出的函数deposit,这个函数假设锁总是会被保持并去做实际的操作,另一个是导出的函数Deposit,这个函数会调用deposit,但在调用前会先去获取锁。同理我们可以将Withdraw也表示成这种形式:

+
func Withdraw(amount int) bool {
+	mu.Lock()
+	defer mu.Unlock()
+	deposit(-amount)
+	if balance < 0 {
+		deposit(amount)
+		return false // insufficient funds
+	}
+	return true
+}
+
+func Deposit(amount int) {
+	mu.Lock()
+	defer mu.Unlock()
+	deposit(amount)
+}
+
+func Balance() int {
+	mu.Lock()
+	defer mu.Unlock()
+	return balance
+}
+
+// This function requires that the lock be held.
+func deposit(amount int) { balance += amount }
+
+

当然,这里的存款deposit函数很小,实际上取款Withdraw函数不需要理会对它的调用,尽管如此,这里的表达还是表明了规则。

+

封装(§6.6),用限制一个程序中的意外交互的方式,可以使我们获得数据结构的不变性。因为某种原因,封装还帮我们获得了并发的不变性。当你使用mutex时,确保mutex和其保护的变量没有被导出(在go里也就是小写,且不要被大写字母开头的函数访问啦),无论这些变量是包级的变量还是一个struct的字段。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch9/ch9-03.html b/ch9/ch9-03.html new file mode 100644 index 0000000..fb2a531 --- /dev/null +++ b/ch9/ch9-03.html @@ -0,0 +1,252 @@ + + + + + + sync.RWMutex读写锁 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

9.3. sync.RWMutex读写锁

+

在100刀的存款消失时不做记录多少还是会让我们有一些恐慌,Bob写了一个程序,每秒运行几百次来检查他的银行余额。他会在家,在工作中,甚至会在他的手机上来运行这个程序。银行注意到这些陡增的流量使得存款和取款有了延时,因为所有的余额查询请求是顺序执行的,这样会互斥地获得锁,并且会暂时阻止其它的goroutine运行。

+

由于Balance函数只需要读取变量的状态,所以我们同时让多个Balance调用并发运行事实上是安全的,只要在运行的时候没有存款或者取款操作就行。在这种场景下我们需要一种特殊类型的锁,其允许多个只读操作并行执行,但写操作会完全互斥。这种锁叫作“多读单写”锁(multiple readers, single writer lock),Go语言提供的这样的锁是sync.RWMutex:

+
var mu sync.RWMutex
+var balance int
+func Balance() int {
+	mu.RLock() // readers lock
+	defer mu.RUnlock()
+	return balance
+}
+
+

Balance函数现在调用了RLock和RUnlock方法来获取和释放一个读取或者共享锁。Deposit函数没有变化,会调用mu.Lock和mu.Unlock方法来获取和释放一个写或互斥锁。

+

在这次修改后,Bob的余额查询请求就可以彼此并行地执行并且会很快地完成了。锁在更多的时间范围可用,并且存款请求也能够及时地被响应了。

+

RLock只能在临界区共享变量没有任何写入操作时可用。一般来说,我们不应该假设逻辑上的只读函数/方法也不会去更新某一些变量。比如一个方法功能是访问一个变量,但它也有可能会同时去给一个内部的计数器+1(译注:可能是记录这个方法的访问次数啥的),或者去更新缓存——使即时的调用能够更快。如果有疑惑的话,请使用互斥锁。

+

RWMutex只有当获得锁的大部分goroutine都是读操作,而锁在竞争条件下,也就是说,goroutine们必须等待才能获取到锁的时候,RWMutex才是最能带来好处的。RWMutex需要更复杂的内部记录,所以会让它比一般的无竞争锁的mutex慢一些。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch9/ch9-04.html b/ch9/ch9-04.html new file mode 100644 index 0000000..3cce156 --- /dev/null +++ b/ch9/ch9-04.html @@ -0,0 +1,265 @@ + + + + + + 内存同步 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

9.4. 内存同步

+

你可能比较纠结为什么Balance方法需要用到互斥条件,无论是基于channel还是基于互斥量。毕竟和存款不一样,它只由一个简单的操作组成,所以不会碰到其它goroutine在其执行“期间”执行其它逻辑的风险。这里使用mutex有两方面考虑。第一Balance不会在其它操作比如Withdraw“中间”执行。第二(更重要的)是“同步”不仅仅是一堆goroutine执行顺序的问题,同样也会涉及到内存的问题。

+

在现代计算机中可能会有一堆处理器,每一个都会有其本地缓存(local cache)。为了效率,对内存的写入一般会在每一个处理器中缓冲,并在必要时一起flush到主存。这种情况下这些数据可能会以与当初goroutine写入顺序不同的顺序被提交到主存。像channel通信或者互斥量操作这样的原语会使处理器将其聚集的写入flush并commit,这样goroutine在某个时间点上的执行结果才能被其它处理器上运行的goroutine得到。

+

考虑一下下面代码片段的可能输出:

+
var x, y int
+go func() {
+	x = 1 // A1
+	fmt.Print("y:", y, " ") // A2
+}()
+go func() {
+	y = 1                   // B1
+	fmt.Print("x:", x, " ") // B2
+}()
+
+

因为两个goroutine是并发执行,并且访问共享变量时也没有互斥,会有数据竞争,所以程序的运行结果没法预测的话也请不要惊讶。我们可能希望它能够打印出下面这四种结果中的一种,相当于几种不同的交错执行时的情况:

+
y:0 x:1
+x:0 y:1
+x:1 y:1
+y:1 x:1
+
+

第四行可以被解释为执行顺序A1,B1,A2,B2或者B1,A1,A2,B2的执行结果。然而实际运行时还是有些情况让我们有点惊讶:

+
x:0 y:0
+y:0 x:0
+
+

根据所使用的编译器,CPU,或者其它很多影响因子,这两种情况也是有可能发生的。那么这两种情况要怎么解释呢?

+

在一个独立的goroutine中,每一个语句的执行顺序是可以被保证的,也就是说goroutine内顺序是连贯的。但是在不使用channel且不使用mutex这样的显式同步操作时,我们就没法保证事件在不同的goroutine中看到的执行顺序是一致的了。尽管goroutine A中一定需要观察到x=1执行成功之后才会去读取y,但它没法确保自己观察得到goroutine B中对y的写入,所以A还可能会打印出y的一个旧版的值。

+

尽管去理解并发的一种尝试是去将其运行理解为不同goroutine语句的交错执行,但看看上面的例子,这已经不是现代的编译器和cpu的工作方式了。因为赋值和打印指向不同的变量,编译器可能会断定两条语句的顺序不会影响执行结果,并且会交换两个语句的执行顺序。如果两个goroutine在不同的CPU上执行,每一个核心有自己的缓存,这样一个goroutine的写入对于其它goroutine的Print,在主存同步之前就是不可见的了。

+

所有并发的问题都可以用一致的、简单的既定的模式来规避。所以可能的话,将变量限定在goroutine内部;如果是多个goroutine都需要访问的变量,使用互斥条件来访问。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch9/ch9-06.html b/ch9/ch9-06.html new file mode 100644 index 0000000..9c727f6 --- /dev/null +++ b/ch9/ch9-06.html @@ -0,0 +1,243 @@ + + + + + + 竞争条件检测 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

9.6. 竞争条件检测

+

即使我们小心到不能再小心,但在并发程序中犯错还是太容易了。幸运的是,Go的runtime和工具链为我们装备了一个复杂但好用的动态分析工具,竞争检查器(the race detector)。

+

只要在go build,go run或者go test命令后面加上-race的flag,就会使编译器创建一个你的应用的“修改”版或者一个附带了能够记录所有运行期对共享变量访问工具的test,并且会记录下每一个读或者写共享变量的goroutine的身份信息。另外,修改版的程序会记录下所有的同步事件,比如go语句,channel操作,以及对(*sync.Mutex).Lock(*sync.WaitGroup).Wait等等的调用。(完整的同步事件集合是在The Go Memory Model文档中有说明,该文档是和语言文档放在一起的。译注:https://golang.org/ref/mem )

+

竞争检查器会检查这些事件,会寻找在哪一个goroutine中出现了这样的case,例如其读或者写了一个共享变量,这个共享变量是被另一个goroutine在没有进行干预同步操作便直接写入的。这种情况也就表明了是对一个共享变量的并发访问,即数据竞争。这个工具会打印一份报告,内容包含变量身份,读取和写入的goroutine中活跃的函数的调用栈。这些信息在定位问题时通常很有用。9.7节中会有一个竞争检查器的实战样例。

+

竞争检查器会报告所有的已经发生的数据竞争。然而,它只能检测到运行时的竞争条件;并不能证明之后不会发生数据竞争。所以为了使结果尽量正确,请保证你的测试并发地覆盖到了你的包。

+

由于需要额外的记录,因此构建时加了竞争检测的程序跑起来会慢一些,且需要更大的内存,即使是这样,这些代价对于很多生产环境的程序(工作)来说还是可以接受的。对于一些偶发的竞争条件来说,让竞争检查器来干活可以节省无数日夜的debugging。(译注:多少服务端C和C++程序员为此竞折腰。)

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch9/ch9-07.html b/ch9/ch9-07.html new file mode 100644 index 0000000..514828b --- /dev/null +++ b/ch9/ch9-07.html @@ -0,0 +1,522 @@ + + + + + + 示例: 并发的非阻塞缓存 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

9.7. 示例: 并发的非阻塞缓存

+

本节中我们会做一个无阻塞的缓存,这种工具可以帮助我们来解决现实世界中并发程序出现但没有现成的库可以解决的问题。这个问题叫作缓存(memoizing)函数(译注:Memoization的定义: memoization 一词是Donald Michie 根据拉丁语memorandum杜撰的一个词。相应的动词、过去分词、ing形式有memoiz、memoized、memoizing),也就是说,我们需要缓存函数的返回结果,这样在对函数进行调用的时候,我们就只需要一次计算,之后只要返回计算的结果就可以了。我们的解决方案会是并发安全且会避免对整个缓存加锁而导致所有操作都去争一个锁的设计。

+

我们将使用下面的httpGetBody函数作为我们需要缓存的函数的一个样例。这个函数会去进行HTTP GET请求并且获取http响应body。对这个函数的调用本身开销是比较大的,所以我们尽量避免在不必要的时候反复调用。

+
func httpGetBody(url string) (interface{}, error) {
+	resp, err := http.Get(url)
+	if err != nil {
+		return nil, err
+	}
+	defer resp.Body.Close()
+	return ioutil.ReadAll(resp.Body)
+}
+
+

最后一行稍微隐藏了一些细节。ReadAll会返回两个结果,一个[]byte数组和一个错误,不过这两个对象可以被赋值给httpGetBody的返回声明里的interface{}和error类型,所以我们也就可以这样返回结果并且不需要额外的工作了。我们在httpGetBody中选用这种返回类型是为了使其可以与缓存匹配。

+

下面是我们要设计的cache的第一个“草稿”:

+

gopl.io/ch9/memo1

+
// Package memo provides a concurrency-unsafe
+// memoization of a function of type Func.
+package memo
+
+// A Memo caches the results of calling a Func.
+type Memo struct {
+	f     Func
+	cache map[string]result
+}
+
+// Func is the type of the function to memoize.
+type Func func(key string) (interface{}, error)
+
+type result struct {
+	value interface{}
+	err   error
+}
+
+func New(f Func) *Memo {
+	return &Memo{f: f, cache: make(map[string]result)}
+}
+
+// NOTE: not concurrency-safe!
+func (memo *Memo) Get(key string) (interface{}, error) {
+	res, ok := memo.cache[key]
+	if !ok {
+		res.value, res.err = memo.f(key)
+		memo.cache[key] = res
+	}
+	return res.value, res.err
+}
+
+

Memo实例会记录需要缓存的函数f(类型为Func),以及缓存内容(里面是一个string到result映射的map)。每一个result都是简单的函数返回的值对儿——一个值和一个错误值。继续下去我们会展示一些Memo的变种,不过所有的例子都会遵循上面的这些方面。

+

下面是一个使用Memo的例子。对于流入的URL的每一个元素我们都会调用Get,并打印调用延时以及其返回的数据大小的log:

+
m := memo.New(httpGetBody)
+for url := range incomingURLs() {
+	start := time.Now()
+	value, err := m.Get(url)
+	if err != nil {
+		log.Print(err)
+	}
+	fmt.Printf("%s, %s, %d bytes\n",
+	url, time.Since(start), len(value.([]byte)))
+}
+
+

我们可以使用测试包(第11章的主题)来系统地鉴定缓存的效果。从下面的测试输出,我们可以看到URL流包含了一些重复的情况,尽管我们第一次对每一个URL的(*Memo).Get的调用都会花上几百毫秒,但第二次就只需要花1毫秒就可以返回完整的数据了。

+
$ go test -v gopl.io/ch9/memo1
+=== RUN   Test
+https://golang.org, 175.026418ms, 7537 bytes
+https://godoc.org, 172.686825ms, 6878 bytes
+https://play.golang.org, 115.762377ms, 5767 bytes
+http://gopl.io, 749.887242ms, 2856 bytes
+https://golang.org, 721ns, 7537 bytes
+https://godoc.org, 152ns, 6878 bytes
+https://play.golang.org, 205ns, 5767 bytes
+http://gopl.io, 326ns, 2856 bytes
+--- PASS: Test (1.21s)
+PASS
+ok  gopl.io/ch9/memo1   1.257s
+
+

这个测试是顺序地去做所有的调用的。

+

由于这种彼此独立的HTTP请求可以很好地并发,我们可以把这个测试改成并发形式。可以使用sync.WaitGroup来等待所有的请求都完成之后再返回。

+
m := memo.New(httpGetBody)
+var n sync.WaitGroup
+for url := range incomingURLs() {
+	n.Add(1)
+	go func(url string) {
+		start := time.Now()
+		value, err := m.Get(url)
+		if err != nil {
+			log.Print(err)
+		}
+		fmt.Printf("%s, %s, %d bytes\n",
+		url, time.Since(start), len(value.([]byte)))
+		n.Done()
+	}(url)
+}
+n.Wait()
+
+

这次测试跑起来更快了,然而不幸的是貌似这个测试不是每次都能够正常工作。我们注意到有一些意料之外的cache miss(缓存未命中),或者命中了缓存但却返回了错误的值,或者甚至会直接崩溃。

+

但更糟糕的是,有时候这个程序还是能正确的运行(译:也就是最让人崩溃的偶发bug),所以我们甚至可能都不会意识到这个程序有bug。但是我们可以使用-race这个flag来运行程序,竞争检测器(§9.6)会打印像下面这样的报告:

+
$ go test -run=TestConcurrent -race -v gopl.io/ch9/memo1
+=== RUN   TestConcurrent
+...
+WARNING: DATA RACE
+Write by goroutine 36:
+  runtime.mapassign1()
+      ~/go/src/runtime/hashmap.go:411 +0x0
+  gopl.io/ch9/memo1.(*Memo).Get()
+      ~/gobook2/src/gopl.io/ch9/memo1/memo.go:32 +0x205
+  ...
+Previous write by goroutine 35:
+  runtime.mapassign1()
+      ~/go/src/runtime/hashmap.go:411 +0x0
+  gopl.io/ch9/memo1.(*Memo).Get()
+      ~/gobook2/src/gopl.io/ch9/memo1/memo.go:32 +0x205
+...
+Found 1 data race(s)
+FAIL    gopl.io/ch9/memo1   2.393s
+
+

memo.go的32行出现了两次,说明有两个goroutine在没有同步干预的情况下更新了cache map。这表明Get不是并发安全的,存在数据竞争。

+
28  func (memo *Memo) Get(key string) (interface{}, error) {
+29      res, ok := memo.cache(key)
+30      if !ok {
+31          res.value, res.err = memo.f(key)
+32          memo.cache[key] = res
+33      }
+34      return res.value, res.err
+35  }
+
+

最简单的使cache并发安全的方式是使用基于监控的同步。只要给Memo加上一个mutex,在Get的一开始获取互斥锁,return的时候释放锁,就可以让cache的操作发生在临界区内了:

+

gopl.io/ch9/memo2

+
type Memo struct {
+	f     Func
+	mu    sync.Mutex // guards cache
+	cache map[string]result
+}
+
+// Get is concurrency-safe.
+func (memo *Memo) Get(key string) (value interface{}, err error) {
+	memo.mu.Lock()
+	res, ok := memo.cache[key]
+    if !ok {
+		res.value, res.err = memo.f(key)
+		memo.cache[key] = res
+	}
+	memo.mu.Unlock()
+	return res.value, res.err
+}
+
+

测试依然并发进行,但这回竞争检查器“沉默”了。不幸的是对于Memo的这一点改变使我们完全丧失了并发的性能优点。每次对f的调用期间都会持有锁,Get将本来可以并行运行的I/O操作串行化了。我们本章的目的是完成一个无锁缓存,而不是现在这样的将所有请求串行化的函数的缓存。

+

下一个Get的实现,调用Get的goroutine会两次获取锁:查找阶段获取一次,如果查找没有返回任何内容,那么进入更新阶段会再次获取。在这两次获取锁的中间阶段,其它goroutine可以随意使用cache。

+

gopl.io/ch9/memo3

+
func (memo *Memo) Get(key string) (value interface{}, err error) {
+	memo.mu.Lock()
+	res, ok := memo.cache[key]
+	memo.mu.Unlock()
+	if !ok {
+		res.value, res.err = memo.f(key)
+
+		// Between the two critical sections, several goroutines
+		// may race to compute f(key) and update the map.
+		memo.mu.Lock()
+		memo.cache[key] = res
+		memo.mu.Unlock()
+	}
+	return res.value, res.err
+}
+
+

这些修改使性能再次得到了提升,但有一些URL被获取了两次。这种情况在两个以上的goroutine同一时刻调用Get来请求同样的URL时会发生。多个goroutine一起查询cache,发现没有值,然后一起调用f这个慢不拉叽的函数。在得到结果后,也都会去更新map。其中一个获得的结果会覆盖掉另一个的结果。

+

理想情况下是应该避免掉多余的工作的。而这种“避免”工作一般被称为duplicate suppression(重复抑制/避免)。下面版本的Memo每一个map元素都是指向一个条目的指针。每一个条目包含对函数f调用结果的内容缓存。与之前不同的是这次entry还包含了一个叫ready的channel。在条目的结果被设置之后,这个channel就会被关闭,以向其它goroutine广播(§8.9)去读取该条目内的结果是安全的了。

+

gopl.io/ch9/memo4

+
type entry struct {
+	res   result
+	ready chan struct{} // closed when res is ready
+}
+
+func New(f Func) *Memo {
+	return &Memo{f: f, cache: make(map[string]*entry)}
+}
+
+type Memo struct {
+	f     Func
+	mu    sync.Mutex // guards cache
+	cache map[string]*entry
+}
+
+func (memo *Memo) Get(key string) (value interface{}, err error) {
+	memo.mu.Lock()
+	e := memo.cache[key]
+	if e == nil {
+		// This is the first request for this key.
+		// This goroutine becomes responsible for computing
+		// the value and broadcasting the ready condition.
+		e = &entry{ready: make(chan struct{})}
+		memo.cache[key] = e
+		memo.mu.Unlock()
+
+		e.res.value, e.res.err = memo.f(key)
+
+		close(e.ready) // broadcast ready condition
+	} else {
+		// This is a repeat request for this key.
+		memo.mu.Unlock()
+
+		<-e.ready // wait for ready condition
+	}
+	return e.res.value, e.res.err
+}
+
+

现在Get函数包括下面这些步骤了:获取互斥锁来保护共享变量cache map,查询map中是否存在指定条目,如果没有找到那么分配空间插入一个新条目,释放互斥锁。如果存在条目的话且其值没有写入完成(也就是有其它的goroutine在调用f这个慢函数)时,goroutine必须等待值ready之后才能读到条目的结果。而想知道是否ready的话,可以直接从ready channel中读取,由于这个读取操作在channel关闭之前一直是阻塞。

+

如果没有条目的话,需要向map中插入一个没有准备好的条目,当前正在调用的goroutine就需要负责调用慢函数、更新条目以及向其它所有goroutine广播条目已经ready可读的消息了。

+

条目中的e.res.value和e.res.err变量是在多个goroutine之间共享的。创建条目的goroutine同时也会设置条目的值,其它goroutine在收到"ready"的广播消息之后立刻会去读取条目的值。尽管会被多个goroutine同时访问,但却并不需要互斥锁。ready channel的关闭一定会发生在其它goroutine接收到广播事件之前,因此第一个goroutine对这些变量的写操作是一定发生在这些读操作之前的。不会发生数据竞争。

+

这样并发、不重复、无阻塞的cache就完成了。

+

上面这样Memo的实现使用了一个互斥量来保护多个goroutine调用Get时的共享map变量。不妨把这种设计和前面提到的把map变量限制在一个单独的monitor goroutine的方案做一些对比,后者在调用Get时需要发消息。

+

Func、result和entry的声明和之前保持一致:

+
// Func is the type of the function to memoize.
+type Func func(key string) (interface{}, error)
+
+// A result is the result of calling a Func.
+type result struct {
+	value interface{}
+	err   error
+}
+
+type entry struct {
+	res   result
+	ready chan struct{} // closed when res is ready
+}
+
+

然而Memo类型现在包含了一个叫做requests的channel,Get的调用方用这个channel来和monitor goroutine来通信。requests channel中的元素类型是request。Get的调用方会把这个结构中的两组key都填充好,实际上用这两个变量来对函数进行缓存的。另一个叫response的channel会被拿来发送响应结果。这个channel只会传回一个单独的值。

+

gopl.io/ch9/memo5

+
// A request is a message requesting that the Func be applied to key.
+type request struct {
+	key      string
+	response chan<- result // the client wants a single result
+}
+
+type Memo struct{ requests chan request }
+// New returns a memoization of f.  Clients must subsequently call Close.
+func New(f Func) *Memo {
+	memo := &Memo{requests: make(chan request)}
+	go memo.server(f)
+	return memo
+}
+
+func (memo *Memo) Get(key string) (interface{}, error) {
+	response := make(chan result)
+	memo.requests <- request{key, response}
+	res := <-response
+	return res.value, res.err
+}
+
+func (memo *Memo) Close() { close(memo.requests) }
+
+

上面的Get方法,会创建一个response channel,把它放进request结构中,然后发送给monitor goroutine,然后马上又会接收它。

+

cache变量被限制在了monitor goroutine ``(*Memo).server`中,下面会看到。monitor会在循环中一直读取请求,直到request channel被Close方法关闭。每一个请求都会去查询cache,如果没有找到条目的话,那么就会创建/插入一个新的条目。

+
func (memo *Memo) server(f Func) {
+	cache := make(map[string]*entry)
+	for req := range memo.requests {
+		e := cache[req.key]
+		if e == nil {
+			// This is the first request for this key.
+			e = &entry{ready: make(chan struct{})}
+			cache[req.key] = e
+			go e.call(f, req.key) // call f(key)
+		}
+		go e.deliver(req.response)
+	}
+}
+
+func (e *entry) call(f Func, key string) {
+	// Evaluate the function.
+	e.res.value, e.res.err = f(key)
+	// Broadcast the ready condition.
+	close(e.ready)
+}
+
+func (e *entry) deliver(response chan<- result) {
+	// Wait for the ready condition.
+	<-e.ready
+	// Send the result to the client.
+	response <- e.res
+}
+
+

和基于互斥量的版本类似,第一个对某个key的请求需要负责去调用函数f并传入这个key,将结果存在条目里,并关闭ready channel来广播条目的ready消息。使用(*entry).call来完成上述工作。

+

紧接着对同一个key的请求会发现map中已经有了存在的条目,然后会等待结果变为ready,并将结果从response发送给客户端的goroutien。上述工作是用(*entry).deliver来完成的。对call和deliver方法的调用必须让它们在自己的goroutine中进行以确保monitor goroutines不会因此而被阻塞住而没法处理新的请求。

+

这个例子说明我们无论用上锁,还是通信来建立并发程序都是可行的。

+

上面的两种方案并不好说特定情境下哪种更好,不过了解他们还是有价值的。有时候从一种方式切换到另一种可以使你的代码更为简洁。(译注:不是说好的golang推崇通信并发么。)

+

练习 9.3: 扩展Func类型和(*Memo).Get方法,支持调用方提供一个可选的done channel,使其具备通过该channel来取消整个操作的能力(§8.9)。一个被取消了的Func的调用结果不应该被缓存。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch9/ch9-08.html b/ch9/ch9-08.html new file mode 100644 index 0000000..681b7cf --- /dev/null +++ b/ch9/ch9-08.html @@ -0,0 +1,269 @@ + + + + + + Goroutines和线程 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

9.8. Goroutines和线程

+

在上一章中我们说goroutine和操作系统的线程区别可以先忽略。尽管两者的区别实际上只是一个量的区别,但量变会引起质变的道理同样适用于goroutine和线程。现在正是我们来区分开两者的最佳时机。

+

9.8.1. 动态栈

+

每一个OS线程都有一个固定大小的内存块(一般会是2MB)来做栈,这个栈会用来存储当前正在被调用或挂起(指在调用其它函数时)的函数的内部变量。这个固定大小的栈同时很大又很小。因为2MB的栈对于一个小小的goroutine来说是很大的内存浪费,比如对于我们用到的,一个只是用来WaitGroup之后关闭channel的goroutine来说。而对于go程序来说,同时创建成百上千个goroutine是非常普遍的,如果每一个goroutine都需要这么大的栈的话,那这么多的goroutine就不太可能了。除去大小的问题之外,固定大小的栈对于更复杂或者更深层次的递归函数调用来说显然是不够的。修改固定的大小可以提升空间的利用率,允许创建更多的线程,并且可以允许更深的递归调用,不过这两者是没法同时兼备的。

+

相反,一个goroutine会以一个很小的栈开始其生命周期,一般只需要2KB。一个goroutine的栈,和操作系统线程一样,会保存其活跃或挂起的函数调用的本地变量,但是和OS线程不太一样的是,一个goroutine的栈大小并不是固定的;栈的大小会根据需要动态地伸缩。而goroutine的栈的最大值有1GB,比传统的固定大小的线程栈要大得多,尽管一般情况下,大多goroutine都不需要这么大的栈。

+

** 练习 9.4:** 创建一个流水线程序,支持用channel连接任意数量的goroutine,在跑爆内存之前,可以创建多少流水线阶段?一个变量通过整个流水线需要用多久?(这个练习题翻译不是很确定)

+

9.8.2. Goroutine调度

+

OS线程会被操作系统内核调度。每几毫秒,一个硬件计时器会中断处理器,这会调用一个叫作scheduler的内核函数。这个函数会挂起当前执行的线程并将它的寄存器内容保存到内存中,检查线程列表并决定下一次哪个线程可以被运行,并从内存中恢复该线程的寄存器信息,然后恢复执行该线程的现场并开始执行线程。因为操作系统线程是被内核所调度,所以从一个线程向另一个“移动”需要完整的上下文切换,也就是说,保存一个用户线程的状态到内存,恢复另一个线程的到寄存器,然后更新调度器的数据结构。这几步操作很慢,因为其局部性很差需要几次内存访问,并且会增加运行的cpu周期。

+

Go的运行时包含了其自己的调度器,这个调度器使用了一些技术手段,比如m:n调度,因为其会在n个操作系统线程上多工(调度)m个goroutine。Go调度器的工作和内核的调度是相似的,但是这个调度器只关注单独的Go程序中的goroutine(译注:按程序独立)。

+

和操作系统的线程调度不同的是,Go调度器并不是用一个硬件定时器,而是被Go语言“建筑”本身进行调度的。例如当一个goroutine调用了time.Sleep,或者被channel调用或者mutex操作阻塞时,调度器会使其进入休眠并开始执行另一个goroutine,直到时机到了再去唤醒第一个goroutine。因为这种调度方式不需要进入内核的上下文,所以重新调度一个goroutine比调度一个线程代价要低得多。

+

** 练习 9.5: ** 写一个有两个goroutine的程序,两个goroutine会向两个无buffer channel反复地发送ping-pong消息。这样的程序每秒可以支持多少次通信?

+

9.8.3. GOMAXPROCS

+

Go的调度器使用了一个叫做GOMAXPROCS的变量来决定会有多少个操作系统的线程同时执行Go的代码。其默认的值是运行机器上的CPU的核心数,所以在一个有8个核心的机器上时,调度器一次会在8个OS线程上去调度GO代码。(GOMAXPROCS是前面说的m:n调度中的n)。在休眠中的或者在通信中被阻塞的goroutine是不需要一个对应的线程来做调度的。在I/O中或系统调用中或调用非Go语言函数时,是需要一个对应的操作系统线程的,但是GOMAXPROCS并不需要将这几种情况计算在内。

+

你可以用GOMAXPROCS的环境变量来显式地控制这个参数,或者也可以在运行时用runtime.GOMAXPROCS函数来修改它。我们在下面的小程序中会看到GOMAXPROCS的效果,这个程序会无限打印0和1。

+
for {
+	go fmt.Print(0)
+	fmt.Print(1)
+}
+
+$ GOMAXPROCS=1 go run hacker-cliché.go
+111111111111111111110000000000000000000011111...
+
+$ GOMAXPROCS=2 go run hacker-cliché.go
+010101010101010101011001100101011010010100110...
+
+

在第一次执行时,最多同时只能有一个goroutine被执行。初始情况下只有main goroutine被执行,所以会打印很多1。过了一段时间后,GO调度器会将其置为休眠,并唤醒另一个goroutine,这时候就开始打印很多0了,在打印的时候,goroutine是被调度到操作系统线程上的。在第二次执行时,我们使用了两个操作系统线程,所以两个goroutine可以一起被执行,以同样的频率交替打印0和1。我们必须强调的是goroutine的调度是受很多因子影响的,而runtime也是在不断地发展演进的,所以这里的你实际得到的结果可能会因为版本的不同而与我们运行的结果有所不同。

+

** 练习9.6:** 测试一下计算密集型的并发程序(练习8.5那样的)会被GOMAXPROCS怎样影响到。在你的电脑上最佳的值是多少?你的电脑CPU有多少个核心?

+

9.8.4. Goroutine没有ID号

+

在大多数支持多线程的操作系统和程序语言中,当前的线程都有一个独特的身份(id),并且这个身份信息可以以一个普通值的形式被很容易地获取到,典型的可以是一个integer或者指针值。这种情况下我们做一个抽象化的thread-local storage(线程本地存储,多线程编程中不希望其它线程访问的内容)就很容易,只需要以线程的id作为key的一个map就可以解决问题,每一个线程以其id就能从中获取到值,且和其它线程互不冲突。

+

goroutine没有可以被程序员获取到的身份(id)的概念。这一点是设计上故意而为之,由于thread-local storage总是会被滥用。比如说,一个web server是用一种支持tls的语言实现的,而非常普遍的是很多函数会去寻找HTTP请求的信息,这代表它们就是去其存储层(这个存储层有可能是tls)查找的。这就像是那些过分依赖全局变量的程序一样,会导致一种非健康的“距离外行为”,在这种行为下,一个函数的行为可能并不仅由自己的参数所决定,而是由其所运行在的线程所决定。因此,如果线程本身的身份会改变——比如一些worker线程之类的——那么函数的行为就会变得神秘莫测。

+

Go鼓励更为简单的模式,这种模式下参数(译注:外部显式参数和内部显式参数。tls 中的内容算是"外部"隐式参数)对函数的影响都是显式的。这样不仅使程序变得更易读,而且会让我们自由地向一些给定的函数分配子任务时不用担心其身份信息影响行为。

+

你现在应该已经明白了写一个Go程序所需要的所有语言特性信息。在后面两章节中,我们会回顾一些之前的实例和工具,支持我们写出更大规模的程序:如何将一个工程组织成一系列的包,如何获取,构建,测试,性能测试,剖析,写文档,并且将这些包分享出去。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch9/ch9.html b/ch9/ch9.html new file mode 100644 index 0000000..c7bc4b2 --- /dev/null +++ b/ch9/ch9.html @@ -0,0 +1,240 @@ + + + + + + 基于共享变量的并发 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

第9章 基于共享变量的并发

+

前一章我们介绍了一些使用goroutine和channel这样直接而自然的方式来实现并发的方法。然而这样做我们实际上回避了在写并发代码时必须处理的一些重要而且细微的问题。

+

在本章中,我们会细致地了解并发机制。尤其是在多goroutine之间的共享变量,并发问题的分析手段,以及解决这些问题的基本模式。最后我们会解释goroutine和操作系统线程之间的技术上的一些区别。

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/clipboard.min.js b/clipboard.min.js new file mode 100644 index 0000000..02c549e --- /dev/null +++ b/clipboard.min.js @@ -0,0 +1,7 @@ +/*! + * clipboard.js v2.0.4 + * https://zenorocha.github.io/clipboard.js + * + * Licensed MIT © Zeno Rocha + */ +!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.ClipboardJS=e():t.ClipboardJS=e()}(this,function(){return function(n){var o={};function r(t){if(o[t])return o[t].exports;var e=o[t]={i:t,l:!1,exports:{}};return n[t].call(e.exports,e,e.exports,r),e.l=!0,e.exports}return r.m=n,r.c=o,r.d=function(t,e,n){r.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:n})},r.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)r.d(n,o,function(t){return e[t]}.bind(null,o));return n},r.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return r.d(e,"a",e),e},r.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},r.p="",r(r.s=0)}([function(t,e,n){"use strict";var r="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},i=function(){function o(t,e){for(var n=0;n .hljs { + color: var(--links); +} + +/* Menu Bar */ + +#menu-bar, +#menu-bar-hover-placeholder { + z-index: 101; + margin: auto calc(0px - var(--page-padding)); +} +#menu-bar { + position: relative; + display: flex; + flex-wrap: wrap; + background-color: var(--bg); + border-bottom-color: var(--bg); + border-bottom-width: 1px; + border-bottom-style: solid; +} +#menu-bar.sticky, +.js #menu-bar-hover-placeholder:hover + #menu-bar, +.js #menu-bar:hover, +.js.sidebar-visible #menu-bar { + position: -webkit-sticky; + position: sticky; + top: 0 !important; +} +#menu-bar-hover-placeholder { + position: sticky; + position: -webkit-sticky; + top: 0; + height: var(--menu-bar-height); +} +#menu-bar.bordered { + border-bottom-color: var(--table-border-color); +} +#menu-bar i, #menu-bar .icon-button { + position: relative; + padding: 0 8px; + z-index: 10; + line-height: var(--menu-bar-height); + cursor: pointer; + transition: color 0.5s; +} +@media only screen and (max-width: 420px) { + #menu-bar i, #menu-bar .icon-button { + padding: 0 5px; + } +} + +.icon-button { + border: none; + background: none; + padding: 0; + color: inherit; +} +.icon-button i { + margin: 0; +} + +.right-buttons { + margin: 0 15px; +} +.right-buttons a { + text-decoration: none; +} + +.left-buttons { + display: flex; + margin: 0 5px; +} +.no-js .left-buttons { + display: none; +} + +.menu-title { + display: inline-block; + font-weight: 200; + font-size: 2.4rem; + line-height: var(--menu-bar-height); + text-align: center; + margin: 0; + flex: 1; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.js .menu-title { + cursor: pointer; +} + +.menu-bar, +.menu-bar:visited, +.nav-chapters, +.nav-chapters:visited, +.mobile-nav-chapters, +.mobile-nav-chapters:visited, +.menu-bar .icon-button, +.menu-bar a i { + color: var(--icons); +} + +.menu-bar i:hover, +.menu-bar .icon-button:hover, +.nav-chapters:hover, +.mobile-nav-chapters i:hover { + color: var(--icons-hover); +} + +/* Nav Icons */ + +.nav-chapters { + font-size: 2.5em; + text-align: center; + text-decoration: none; + + position: fixed; + top: 0; + bottom: 0; + margin: 0; + max-width: 150px; + min-width: 90px; + + display: flex; + justify-content: center; + align-content: center; + flex-direction: column; + + transition: color 0.5s, background-color 0.5s; +} + +.nav-chapters:hover { + text-decoration: none; + background-color: var(--theme-hover); + transition: background-color 0.15s, color 0.15s; +} + +.nav-wrapper { + margin-top: 50px; + display: none; +} + +.mobile-nav-chapters { + font-size: 2.5em; + text-align: center; + text-decoration: none; + width: 90px; + border-radius: 5px; + background-color: var(--sidebar-bg); +} + +.previous { + float: left; +} + +.next { + float: right; + right: var(--page-padding); +} + +@media only screen and (max-width: 1080px) { + .nav-wide-wrapper { display: none; } + .nav-wrapper { display: block; } +} + +@media only screen and (max-width: 1380px) { + .sidebar-visible .nav-wide-wrapper { display: none; } + .sidebar-visible .nav-wrapper { display: block; } +} + +/* Inline code */ + +:not(pre) > .hljs { + display: inline; + padding: 0.1em 0.3em; + border-radius: 3px; +} + +:not(pre):not(a) > .hljs { + color: var(--inline-code-color); + overflow-x: initial; +} + +a:hover > .hljs { + text-decoration: underline; +} + +pre { + position: relative; +} +pre > .buttons { + position: absolute; + z-index: 100; + right: 5px; + top: 5px; + + color: var(--sidebar-fg); + cursor: pointer; +} +pre > .buttons :hover { + color: var(--sidebar-active); +} +pre > .buttons i { + margin-left: 8px; +} +pre > .buttons button { + color: inherit; + background: transparent; + border: none; + cursor: inherit; +} +pre > .result { + margin-top: 10px; +} + +/* Search */ + +#searchresults a { + text-decoration: none; +} + +mark { + border-radius: 2px; + padding: 0 3px 1px 3px; + margin: 0 -3px -1px -3px; + background-color: var(--search-mark-bg); + transition: background-color 300ms linear; + cursor: pointer; +} + +mark.fade-out { + background-color: rgba(0,0,0,0) !important; + cursor: auto; +} + +.searchbar-outer { + margin-left: auto; + margin-right: auto; + max-width: var(--content-max-width); +} + +#searchbar { + width: 100%; + margin: 5px auto 0px auto; + padding: 10px 16px; + transition: box-shadow 300ms ease-in-out; + border: 1px solid var(--searchbar-border-color); + border-radius: 3px; + background-color: var(--searchbar-bg); + color: var(--searchbar-fg); +} +#searchbar:focus, +#searchbar.active { + box-shadow: 0 0 3px var(--searchbar-shadow-color); +} + +.searchresults-header { + font-weight: bold; + font-size: 1em; + padding: 18px 0 0 5px; + color: var(--searchresults-header-fg); +} + +.searchresults-outer { + margin-left: auto; + margin-right: auto; + max-width: var(--content-max-width); + border-bottom: 1px dashed var(--searchresults-border-color); +} + +ul#searchresults { + list-style: none; + padding-left: 20px; +} +ul#searchresults li { + margin: 10px 0px; + padding: 2px; + border-radius: 2px; +} +ul#searchresults li.focus { + background-color: var(--searchresults-li-bg); +} +ul#searchresults span.teaser { + display: block; + clear: both; + margin: 5px 0 0 20px; + font-size: 0.8em; +} +ul#searchresults span.teaser em { + font-weight: bold; + font-style: normal; +} + +/* Sidebar */ + +.sidebar { + position: fixed; + left: 0; + top: 0; + bottom: 0; + width: var(--sidebar-width); + font-size: 0.875em; + box-sizing: border-box; + -webkit-overflow-scrolling: touch; + overscroll-behavior-y: contain; + background-color: var(--sidebar-bg); + color: var(--sidebar-fg); +} +.sidebar-resizing { + -moz-user-select: none; + -webkit-user-select: none; + -ms-user-select: none; + user-select: none; +} +.js:not(.sidebar-resizing) .sidebar { + transition: transform 0.3s; /* Animation: slide away */ +} +.sidebar code { + line-height: 2em; +} +.sidebar .sidebar-scrollbox { + overflow-y: auto; + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + padding: 10px 10px; +} +.sidebar .sidebar-resize-handle { + position: absolute; + cursor: col-resize; + width: 0; + right: 0; + top: 0; + bottom: 0; +} +.js .sidebar .sidebar-resize-handle { + cursor: col-resize; + width: 5px; +} +.sidebar-hidden .sidebar { + transform: translateX(calc(0px - var(--sidebar-width))); +} +.sidebar::-webkit-scrollbar { + background: var(--sidebar-bg); +} +.sidebar::-webkit-scrollbar-thumb { + background: var(--scrollbar); +} + +.sidebar-visible .page-wrapper { + transform: translateX(var(--sidebar-width)); +} +@media only screen and (min-width: 620px) { + .sidebar-visible .page-wrapper { + transform: none; + margin-left: var(--sidebar-width); + } +} + +.chapter { + list-style: none outside none; + padding-left: 0; + line-height: 2.2em; +} + +.chapter ol { + width: 100%; +} + +.chapter li { + display: flex; + color: var(--sidebar-non-existant); +} +.chapter li a { + display: block; + padding: 0; + text-decoration: none; + color: var(--sidebar-fg); +} + +.chapter li a:hover { + color: var(--sidebar-active); +} + +.chapter li a.active { + color: var(--sidebar-active); +} + +.chapter li > a.toggle { + cursor: pointer; + display: block; + margin-left: auto; + padding: 0 10px; + user-select: none; + opacity: 0.68; +} + +.chapter li > a.toggle div { + transition: transform 0.5s; +} + +/* collapse the section */ +.chapter li:not(.expanded) + li > ol { + display: none; +} + +.chapter li.chapter-item { + line-height: 1.5em; + margin-top: 0.6em; +} + +.chapter li.expanded > a.toggle div { + transform: rotate(90deg); +} + +.spacer { + width: 100%; + height: 3px; + margin: 5px 0px; +} +.chapter .spacer { + background-color: var(--sidebar-spacer); +} + +@media (-moz-touch-enabled: 1), (pointer: coarse) { + .chapter li a { padding: 5px 0; } + .spacer { margin: 10px 0; } +} + +.section { + list-style: none outside none; + padding-left: 20px; + line-height: 1.9em; +} + +/* Theme Menu Popup */ + +.theme-popup { + position: absolute; + left: 10px; + top: var(--menu-bar-height); + z-index: 1000; + border-radius: 4px; + font-size: 0.7em; + color: var(--fg); + background: var(--theme-popup-bg); + border: 1px solid var(--theme-popup-border); + margin: 0; + padding: 0; + list-style: none; + display: none; +} +.theme-popup .default { + color: var(--icons); +} +.theme-popup .theme { + width: 100%; + border: 0; + margin: 0; + padding: 2px 10px; + line-height: 25px; + white-space: nowrap; + text-align: left; + cursor: pointer; + color: inherit; + background: inherit; + font-size: inherit; +} +.theme-popup .theme:hover { + background-color: var(--theme-hover); +} +.theme-popup .theme:hover:first-child, +.theme-popup .theme:hover:last-child { + border-top-left-radius: inherit; + border-top-right-radius: inherit; +} diff --git a/css/general.css b/css/general.css new file mode 100644 index 0000000..2cf347f --- /dev/null +++ b/css/general.css @@ -0,0 +1,177 @@ +/* Base styles and content styles */ + +@import 'variables.css'; + +:root { + /* Browser default font-size is 16px, this way 1 rem = 10px */ + font-size: 62.5%; +} + +html { + font-family: "Open Sans", sans-serif; + color: var(--fg); + background-color: var(--bg); + text-size-adjust: none; +} + +body { + margin: 0; + font-size: 1.6rem; + overflow-x: hidden; +} + +code { + font-family: "Source Code Pro", Consolas, "Ubuntu Mono", Menlo, "DejaVu Sans Mono", monospace, monospace !important; + font-size: 0.875em; /* please adjust the ace font size accordingly in editor.js */ +} + +/* Don't change font size in headers. */ +h1 code, h2 code, h3 code, h4 code, h5 code, h6 code { + font-size: unset; +} + +.left { float: left; } +.right { float: right; } +.boring { opacity: 0.6; } +.hide-boring .boring { display: none; } +.hidden { display: none !important; } + +h2, h3 { margin-top: 2.5em; } +h4, h5 { margin-top: 2em; } + +.header + .header h3, +.header + .header h4, +.header + .header h5 { + margin-top: 1em; +} + +h1:target::before, +h2:target::before, +h3:target::before, +h4:target::before, +h5:target::before, +h6:target::before { + display: inline-block; + content: "»"; + margin-left: -30px; + width: 30px; +} + +/* This is broken on Safari as of version 14, but is fixed + in Safari Technology Preview 117 which I think will be Safari 14.2. + https://bugs.webkit.org/show_bug.cgi?id=218076 +*/ +:target { + scroll-margin-top: calc(var(--menu-bar-height) + 0.5em); +} + +.page { + outline: 0; + padding: 0 var(--page-padding); + margin-top: calc(0px - var(--menu-bar-height)); /* Compensate for the #menu-bar-hover-placeholder */ +} +.page-wrapper { + box-sizing: border-box; +} +.js:not(.sidebar-resizing) .page-wrapper { + transition: margin-left 0.3s ease, transform 0.3s ease; /* Animation: slide away */ +} + +.content { + overflow-y: auto; + padding: 0 15px; + padding-bottom: 50px; +} +.content main { + margin-left: auto; + margin-right: auto; + max-width: var(--content-max-width); +} +.content p { line-height: 1.45em; } +.content ol { line-height: 1.45em; } +.content ul { line-height: 1.45em; } +.content a { text-decoration: none; } +.content a:hover { text-decoration: underline; } +.content img, .content video { max-width: 100%; } +.content .header:link, +.content .header:visited { + color: var(--fg); +} +.content .header:link, +.content .header:visited:hover { + text-decoration: none; +} + +table { + margin: 0 auto; + border-collapse: collapse; +} +table td { + padding: 3px 20px; + border: 1px var(--table-border-color) solid; +} +table thead { + background: var(--table-header-bg); +} +table thead td { + font-weight: 700; + border: none; +} +table thead th { + padding: 3px 20px; +} +table thead tr { + border: 1px var(--table-header-bg) solid; +} +/* Alternate background colors for rows */ +table tbody tr:nth-child(2n) { + background: var(--table-alternate-bg); +} + + +blockquote { + margin: 20px 0; + padding: 0 20px; + color: var(--fg); + background-color: var(--quote-bg); + border-top: .1em solid var(--quote-border); + border-bottom: .1em solid var(--quote-border); +} + + +:not(.footnote-definition) + .footnote-definition, +.footnote-definition + :not(.footnote-definition) { + margin-top: 2em; +} +.footnote-definition { + font-size: 0.9em; + margin: 0.5em 0; +} +.footnote-definition p { + display: inline; +} + +.tooltiptext { + position: absolute; + visibility: hidden; + color: #fff; + background-color: #333; + transform: translateX(-50%); /* Center by moving tooltip 50% of its width left */ + left: -8px; /* Half of the width of the icon */ + top: -35px; + font-size: 0.8em; + text-align: center; + border-radius: 6px; + padding: 5px 8px; + margin: 5px; + z-index: 1000; +} +.tooltipped .tooltiptext { + visibility: visible; +} + +.chapter li.part-title { + color: var(--sidebar-fg); + margin: 5px 0px; + font-weight: bold; +} diff --git a/css/print.css b/css/print.css new file mode 100644 index 0000000..5e690f7 --- /dev/null +++ b/css/print.css @@ -0,0 +1,54 @@ + +#sidebar, +#menu-bar, +.nav-chapters, +.mobile-nav-chapters { + display: none; +} + +#page-wrapper.page-wrapper { + transform: none; + margin-left: 0px; + overflow-y: initial; +} + +#content { + max-width: none; + margin: 0; + padding: 0; +} + +.page { + overflow-y: initial; +} + +code { + background-color: #666666; + border-radius: 5px; + + /* Force background to be printed in Chrome */ + -webkit-print-color-adjust: exact; +} + +pre > .buttons { + z-index: 2; +} + +a, a:visited, a:active, a:hover { + color: #4183c4; + text-decoration: none; +} + +h1, h2, h3, h4, h5, h6 { + page-break-inside: avoid; + page-break-after: avoid; +} + +pre, code { + page-break-inside: avoid; + white-space: pre-wrap; +} + +.fa { + display: none !important; +} diff --git a/css/variables.css b/css/variables.css new file mode 100644 index 0000000..9ff64d6 --- /dev/null +++ b/css/variables.css @@ -0,0 +1,253 @@ + +/* Globals */ + +:root { + --sidebar-width: 300px; + --page-padding: 15px; + --content-max-width: 750px; + --menu-bar-height: 50px; +} + +/* Themes */ + +.ayu { + --bg: hsl(210, 25%, 8%); + --fg: #c5c5c5; + + --sidebar-bg: #14191f; + --sidebar-fg: #c8c9db; + --sidebar-non-existant: #5c6773; + --sidebar-active: #ffb454; + --sidebar-spacer: #2d334f; + + --scrollbar: var(--sidebar-fg); + + --icons: #737480; + --icons-hover: #b7b9cc; + + --links: #0096cf; + + --inline-code-color: #ffb454; + + --theme-popup-bg: #14191f; + --theme-popup-border: #5c6773; + --theme-hover: #191f26; + + --quote-bg: hsl(226, 15%, 17%); + --quote-border: hsl(226, 15%, 22%); + + --table-border-color: hsl(210, 25%, 13%); + --table-header-bg: hsl(210, 25%, 28%); + --table-alternate-bg: hsl(210, 25%, 11%); + + --searchbar-border-color: #848484; + --searchbar-bg: #424242; + --searchbar-fg: #fff; + --searchbar-shadow-color: #d4c89f; + --searchresults-header-fg: #666; + --searchresults-border-color: #888; + --searchresults-li-bg: #252932; + --search-mark-bg: #e3b171; +} + +.coal { + --bg: hsl(200, 7%, 8%); + --fg: #98a3ad; + + --sidebar-bg: #292c2f; + --sidebar-fg: #a1adb8; + --sidebar-non-existant: #505254; + --sidebar-active: #3473ad; + --sidebar-spacer: #393939; + + --scrollbar: var(--sidebar-fg); + + --icons: #43484d; + --icons-hover: #b3c0cc; + + --links: #2b79a2; + + --inline-code-color: #c5c8c6;; + + --theme-popup-bg: #141617; + --theme-popup-border: #43484d; + --theme-hover: #1f2124; + + --quote-bg: hsl(234, 21%, 18%); + --quote-border: hsl(234, 21%, 23%); + + --table-border-color: hsl(200, 7%, 13%); + --table-header-bg: hsl(200, 7%, 28%); + --table-alternate-bg: hsl(200, 7%, 11%); + + --searchbar-border-color: #aaa; + --searchbar-bg: #b7b7b7; + --searchbar-fg: #000; + --searchbar-shadow-color: #aaa; + --searchresults-header-fg: #666; + --searchresults-border-color: #98a3ad; + --searchresults-li-bg: #2b2b2f; + --search-mark-bg: #355c7d; +} + +.light { + --bg: hsl(0, 0%, 100%); + --fg: hsl(0, 0%, 0%); + + --sidebar-bg: #fafafa; + --sidebar-fg: hsl(0, 0%, 0%); + --sidebar-non-existant: #aaaaaa; + --sidebar-active: #1f1fff; + --sidebar-spacer: #f4f4f4; + + --scrollbar: #8F8F8F; + + --icons: #747474; + --icons-hover: #000000; + + --links: #20609f; + + --inline-code-color: #301900; + + --theme-popup-bg: #fafafa; + --theme-popup-border: #cccccc; + --theme-hover: #e6e6e6; + + --quote-bg: hsl(197, 37%, 96%); + --quote-border: hsl(197, 37%, 91%); + + --table-border-color: hsl(0, 0%, 95%); + --table-header-bg: hsl(0, 0%, 80%); + --table-alternate-bg: hsl(0, 0%, 97%); + + --searchbar-border-color: #aaa; + --searchbar-bg: #fafafa; + --searchbar-fg: #000; + --searchbar-shadow-color: #aaa; + --searchresults-header-fg: #666; + --searchresults-border-color: #888; + --searchresults-li-bg: #e4f2fe; + --search-mark-bg: #a2cff5; +} + +.navy { + --bg: hsl(226, 23%, 11%); + --fg: #bcbdd0; + + --sidebar-bg: #282d3f; + --sidebar-fg: #c8c9db; + --sidebar-non-existant: #505274; + --sidebar-active: #2b79a2; + --sidebar-spacer: #2d334f; + + --scrollbar: var(--sidebar-fg); + + --icons: #737480; + --icons-hover: #b7b9cc; + + --links: #2b79a2; + + --inline-code-color: #c5c8c6;; + + --theme-popup-bg: #161923; + --theme-popup-border: #737480; + --theme-hover: #282e40; + + --quote-bg: hsl(226, 15%, 17%); + --quote-border: hsl(226, 15%, 22%); + + --table-border-color: hsl(226, 23%, 16%); + --table-header-bg: hsl(226, 23%, 31%); + --table-alternate-bg: hsl(226, 23%, 14%); + + --searchbar-border-color: #aaa; + --searchbar-bg: #aeaec6; + --searchbar-fg: #000; + --searchbar-shadow-color: #aaa; + --searchresults-header-fg: #5f5f71; + --searchresults-border-color: #5c5c68; + --searchresults-li-bg: #242430; + --search-mark-bg: #a2cff5; +} + +.rust { + --bg: hsl(60, 9%, 87%); + --fg: #262625; + + --sidebar-bg: #3b2e2a; + --sidebar-fg: #c8c9db; + --sidebar-non-existant: #505254; + --sidebar-active: #e69f67; + --sidebar-spacer: #45373a; + + --scrollbar: var(--sidebar-fg); + + --icons: #737480; + --icons-hover: #262625; + + --links: #2b79a2; + + --inline-code-color: #6e6b5e; + + --theme-popup-bg: #e1e1db; + --theme-popup-border: #b38f6b; + --theme-hover: #99908a; + + --quote-bg: hsl(60, 5%, 75%); + --quote-border: hsl(60, 5%, 70%); + + --table-border-color: hsl(60, 9%, 82%); + --table-header-bg: #b3a497; + --table-alternate-bg: hsl(60, 9%, 84%); + + --searchbar-border-color: #aaa; + --searchbar-bg: #fafafa; + --searchbar-fg: #000; + --searchbar-shadow-color: #aaa; + --searchresults-header-fg: #666; + --searchresults-border-color: #888; + --searchresults-li-bg: #dec2a2; + --search-mark-bg: #e69f67; +} + +@media (prefers-color-scheme: dark) { + .light.no-js { + --bg: hsl(200, 7%, 8%); + --fg: #98a3ad; + + --sidebar-bg: #292c2f; + --sidebar-fg: #a1adb8; + --sidebar-non-existant: #505254; + --sidebar-active: #3473ad; + --sidebar-spacer: #393939; + + --scrollbar: var(--sidebar-fg); + + --icons: #43484d; + --icons-hover: #b3c0cc; + + --links: #2b79a2; + + --inline-code-color: #c5c8c6;; + + --theme-popup-bg: #141617; + --theme-popup-border: #43484d; + --theme-hover: #1f2124; + + --quote-bg: hsl(234, 21%, 18%); + --quote-border: hsl(234, 21%, 23%); + + --table-border-color: hsl(200, 7%, 13%); + --table-header-bg: hsl(200, 7%, 28%); + --table-alternate-bg: hsl(200, 7%, 11%); + + --searchbar-border-color: #aaa; + --searchbar-bg: #b7b7b7; + --searchbar-fg: #000; + --searchbar-shadow-color: #aaa; + --searchresults-header-fg: #666; + --searchresults-border-color: #98a3ad; + --searchresults-li-bg: #2b2b2f; + --search-mark-bg: #355c7d; + } +} diff --git a/doc.go b/doc.go new file mode 100644 index 0000000..02cad64 --- /dev/null +++ b/doc.go @@ -0,0 +1,16 @@ +// Copyright 2015 Golang-China. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +// Go圣经中文版. +// +// 在线版本: http://golang-china.github.io/gopl-zh +// +// 离线版本: http://github.com/golang-china/gopl-zh/archive/gh-pages.zip +// +// 项目主页: http://github.com/golang-china/gopl-zh +// +// 原版官网: http://gopl.io +// +package gopl_zh diff --git a/elasticlunr.min.js b/elasticlunr.min.js new file mode 100644 index 0000000..94b20dd --- /dev/null +++ b/elasticlunr.min.js @@ -0,0 +1,10 @@ +/** + * elasticlunr - http://weixsong.github.io + * Lightweight full-text search engine in Javascript for browser search and offline search. - 0.9.5 + * + * Copyright (C) 2017 Oliver Nightingale + * Copyright (C) 2017 Wei Song + * MIT Licensed + * @license + */ +!function(){function e(e){if(null===e||"object"!=typeof e)return e;var t=e.constructor();for(var n in e)e.hasOwnProperty(n)&&(t[n]=e[n]);return t}var t=function(e){var n=new t.Index;return n.pipeline.add(t.trimmer,t.stopWordFilter,t.stemmer),e&&e.call(n,n),n};t.version="0.9.5",lunr=t,t.utils={},t.utils.warn=function(e){return function(t){e.console&&console.warn&&console.warn(t)}}(this),t.utils.toString=function(e){return void 0===e||null===e?"":e.toString()},t.EventEmitter=function(){this.events={}},t.EventEmitter.prototype.addListener=function(){var e=Array.prototype.slice.call(arguments),t=e.pop(),n=e;if("function"!=typeof t)throw new TypeError("last argument must be a function");n.forEach(function(e){this.hasHandler(e)||(this.events[e]=[]),this.events[e].push(t)},this)},t.EventEmitter.prototype.removeListener=function(e,t){if(this.hasHandler(e)){var n=this.events[e].indexOf(t);-1!==n&&(this.events[e].splice(n,1),0==this.events[e].length&&delete this.events[e])}},t.EventEmitter.prototype.emit=function(e){if(this.hasHandler(e)){var t=Array.prototype.slice.call(arguments,1);this.events[e].forEach(function(e){e.apply(void 0,t)},this)}},t.EventEmitter.prototype.hasHandler=function(e){return e in this.events},t.tokenizer=function(e){if(!arguments.length||null===e||void 0===e)return[];if(Array.isArray(e)){var n=e.filter(function(e){return null===e||void 0===e?!1:!0});n=n.map(function(e){return t.utils.toString(e).toLowerCase()});var i=[];return n.forEach(function(e){var n=e.split(t.tokenizer.seperator);i=i.concat(n)},this),i}return e.toString().trim().toLowerCase().split(t.tokenizer.seperator)},t.tokenizer.defaultSeperator=/[\s\-]+/,t.tokenizer.seperator=t.tokenizer.defaultSeperator,t.tokenizer.setSeperator=function(e){null!==e&&void 0!==e&&"object"==typeof e&&(t.tokenizer.seperator=e)},t.tokenizer.resetSeperator=function(){t.tokenizer.seperator=t.tokenizer.defaultSeperator},t.tokenizer.getSeperator=function(){return t.tokenizer.seperator},t.Pipeline=function(){this._queue=[]},t.Pipeline.registeredFunctions={},t.Pipeline.registerFunction=function(e,n){n in t.Pipeline.registeredFunctions&&t.utils.warn("Overwriting existing registered function: "+n),e.label=n,t.Pipeline.registeredFunctions[n]=e},t.Pipeline.getRegisteredFunction=function(e){return e in t.Pipeline.registeredFunctions!=!0?null:t.Pipeline.registeredFunctions[e]},t.Pipeline.warnIfFunctionNotRegistered=function(e){var n=e.label&&e.label in this.registeredFunctions;n||t.utils.warn("Function is not registered with pipeline. This may cause problems when serialising the index.\n",e)},t.Pipeline.load=function(e){var n=new t.Pipeline;return e.forEach(function(e){var i=t.Pipeline.getRegisteredFunction(e);if(!i)throw new Error("Cannot load un-registered function: "+e);n.add(i)}),n},t.Pipeline.prototype.add=function(){var e=Array.prototype.slice.call(arguments);e.forEach(function(e){t.Pipeline.warnIfFunctionNotRegistered(e),this._queue.push(e)},this)},t.Pipeline.prototype.after=function(e,n){t.Pipeline.warnIfFunctionNotRegistered(n);var i=this._queue.indexOf(e);if(-1===i)throw new Error("Cannot find existingFn");this._queue.splice(i+1,0,n)},t.Pipeline.prototype.before=function(e,n){t.Pipeline.warnIfFunctionNotRegistered(n);var i=this._queue.indexOf(e);if(-1===i)throw new Error("Cannot find existingFn");this._queue.splice(i,0,n)},t.Pipeline.prototype.remove=function(e){var t=this._queue.indexOf(e);-1!==t&&this._queue.splice(t,1)},t.Pipeline.prototype.run=function(e){for(var t=[],n=e.length,i=this._queue.length,o=0;n>o;o++){for(var r=e[o],s=0;i>s&&(r=this._queue[s](r,o,e),void 0!==r&&null!==r);s++);void 0!==r&&null!==r&&t.push(r)}return t},t.Pipeline.prototype.reset=function(){this._queue=[]},t.Pipeline.prototype.get=function(){return this._queue},t.Pipeline.prototype.toJSON=function(){return this._queue.map(function(e){return t.Pipeline.warnIfFunctionNotRegistered(e),e.label})},t.Index=function(){this._fields=[],this._ref="id",this.pipeline=new t.Pipeline,this.documentStore=new t.DocumentStore,this.index={},this.eventEmitter=new t.EventEmitter,this._idfCache={},this.on("add","remove","update",function(){this._idfCache={}}.bind(this))},t.Index.prototype.on=function(){var e=Array.prototype.slice.call(arguments);return this.eventEmitter.addListener.apply(this.eventEmitter,e)},t.Index.prototype.off=function(e,t){return this.eventEmitter.removeListener(e,t)},t.Index.load=function(e){e.version!==t.version&&t.utils.warn("version mismatch: current "+t.version+" importing "+e.version);var n=new this;n._fields=e.fields,n._ref=e.ref,n.documentStore=t.DocumentStore.load(e.documentStore),n.pipeline=t.Pipeline.load(e.pipeline),n.index={};for(var i in e.index)n.index[i]=t.InvertedIndex.load(e.index[i]);return n},t.Index.prototype.addField=function(e){return this._fields.push(e),this.index[e]=new t.InvertedIndex,this},t.Index.prototype.setRef=function(e){return this._ref=e,this},t.Index.prototype.saveDocument=function(e){return this.documentStore=new t.DocumentStore(e),this},t.Index.prototype.addDoc=function(e,n){if(e){var n=void 0===n?!0:n,i=e[this._ref];this.documentStore.addDoc(i,e),this._fields.forEach(function(n){var o=this.pipeline.run(t.tokenizer(e[n]));this.documentStore.addFieldLength(i,n,o.length);var r={};o.forEach(function(e){e in r?r[e]+=1:r[e]=1},this);for(var s in r){var u=r[s];u=Math.sqrt(u),this.index[n].addToken(s,{ref:i,tf:u})}},this),n&&this.eventEmitter.emit("add",e,this)}},t.Index.prototype.removeDocByRef=function(e){if(e&&this.documentStore.isDocStored()!==!1&&this.documentStore.hasDoc(e)){var t=this.documentStore.getDoc(e);this.removeDoc(t,!1)}},t.Index.prototype.removeDoc=function(e,n){if(e){var n=void 0===n?!0:n,i=e[this._ref];this.documentStore.hasDoc(i)&&(this.documentStore.removeDoc(i),this._fields.forEach(function(n){var o=this.pipeline.run(t.tokenizer(e[n]));o.forEach(function(e){this.index[n].removeToken(e,i)},this)},this),n&&this.eventEmitter.emit("remove",e,this))}},t.Index.prototype.updateDoc=function(e,t){var t=void 0===t?!0:t;this.removeDocByRef(e[this._ref],!1),this.addDoc(e,!1),t&&this.eventEmitter.emit("update",e,this)},t.Index.prototype.idf=function(e,t){var n="@"+t+"/"+e;if(Object.prototype.hasOwnProperty.call(this._idfCache,n))return this._idfCache[n];var i=this.index[t].getDocFreq(e),o=1+Math.log(this.documentStore.length/(i+1));return this._idfCache[n]=o,o},t.Index.prototype.getFields=function(){return this._fields.slice()},t.Index.prototype.search=function(e,n){if(!e)return[];e="string"==typeof e?{any:e}:JSON.parse(JSON.stringify(e));var i=null;null!=n&&(i=JSON.stringify(n));for(var o=new t.Configuration(i,this.getFields()).get(),r={},s=Object.keys(e),u=0;u0&&t.push(e);for(var i in n)"docs"!==i&&"df"!==i&&this.expandToken(e+i,t,n[i]);return t},t.InvertedIndex.prototype.toJSON=function(){return{root:this.root}},t.Configuration=function(e,n){var e=e||"";if(void 0==n||null==n)throw new Error("fields should not be null");this.config={};var i;try{i=JSON.parse(e),this.buildUserConfig(i,n)}catch(o){t.utils.warn("user configuration parse failed, will use default configuration"),this.buildDefaultConfig(n)}},t.Configuration.prototype.buildDefaultConfig=function(e){this.reset(),e.forEach(function(e){this.config[e]={boost:1,bool:"OR",expand:!1}},this)},t.Configuration.prototype.buildUserConfig=function(e,n){var i="OR",o=!1;if(this.reset(),"bool"in e&&(i=e.bool||i),"expand"in e&&(o=e.expand||o),"fields"in e)for(var r in e.fields)if(n.indexOf(r)>-1){var s=e.fields[r],u=o;void 0!=s.expand&&(u=s.expand),this.config[r]={boost:s.boost||0===s.boost?s.boost:1,bool:s.bool||i,expand:u}}else t.utils.warn("field name in user configuration not found in index instance fields");else this.addAllFields2UserConfig(i,o,n)},t.Configuration.prototype.addAllFields2UserConfig=function(e,t,n){n.forEach(function(n){this.config[n]={boost:1,bool:e,expand:t}},this)},t.Configuration.prototype.get=function(){return this.config},t.Configuration.prototype.reset=function(){this.config={}},lunr.SortedSet=function(){this.length=0,this.elements=[]},lunr.SortedSet.load=function(e){var t=new this;return t.elements=e,t.length=e.length,t},lunr.SortedSet.prototype.add=function(){var e,t;for(e=0;e1;){if(r===e)return o;e>r&&(t=o),r>e&&(n=o),i=n-t,o=t+Math.floor(i/2),r=this.elements[o]}return r===e?o:-1},lunr.SortedSet.prototype.locationFor=function(e){for(var t=0,n=this.elements.length,i=n-t,o=t+Math.floor(i/2),r=this.elements[o];i>1;)e>r&&(t=o),r>e&&(n=o),i=n-t,o=t+Math.floor(i/2),r=this.elements[o];return r>e?o:e>r?o+1:void 0},lunr.SortedSet.prototype.intersect=function(e){for(var t=new lunr.SortedSet,n=0,i=0,o=this.length,r=e.length,s=this.elements,u=e.elements;;){if(n>o-1||i>r-1)break;s[n]!==u[i]?s[n]u[i]&&i++:(t.add(s[n]),n++,i++)}return t},lunr.SortedSet.prototype.clone=function(){var e=new lunr.SortedSet;return e.elements=this.toArray(),e.length=e.elements.length,e},lunr.SortedSet.prototype.union=function(e){var t,n,i;this.length>=e.length?(t=this,n=e):(t=e,n=this),i=t.clone();for(var o=0,r=n.toArray();o + + + + diff --git a/fix-data-revision.go b/fix-data-revision.go new file mode 100644 index 0000000..088ea66 --- /dev/null +++ b/fix-data-revision.go @@ -0,0 +1,82 @@ +// Copyright 2015 . All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build ingore + +// 修复Gitbook生成html的时间戳. +package main + +import ( + "fmt" + "io/ioutil" + "log" + "os" + "path/filepath" + "regexp" + "strings" + "unicode/utf8" +) + +// 输出目录 +const dir = "_book" + +var ( + // data-revision="Mon Feb 01 2016 10:18:48 GMT+0800 (中国标准时间)" + reDataRevision = regexp.MustCompile(`data\-revision\=\"[^"]+\"`) + goldenDataRevision = `data-revision="Mon Jan 2 15:04:05 -0700 MST 2006"` +) + +func main() { + total := 0 + filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { + if err != nil { + log.Fatal("filepath.Walk: ", err) + return err + } + if info.IsDir() { + return nil + } + relpath, err := filepath.Rel(dir, path) + if err != nil { + log.Fatal("filepath.Rel: ", err) + return err + } + + if !strings.HasSuffix(relpath, ".html") { + return nil + } + if changed := convertFile(path); changed { + fmt.Printf("%s\n", relpath) + total++ + } + return nil + }) + fmt.Printf("total %d\n", total) +} + +func convertFile(path string) (changed bool) { + abspath, err := filepath.Abs(path) + if err != nil { + log.Fatal("convertFile: filepath.Abs:", err) + } + + oldData, err := ioutil.ReadFile(abspath) + if err != nil { + log.Fatal("convertFile: ioutil.ReadFile:", err) + } + if !utf8.Valid(oldData) { + return false + } + + newData := reDataRevision.ReplaceAll(oldData, []byte(goldenDataRevision)) + if string(newData) == string(oldData) { + return false + } + + err = ioutil.WriteFile(abspath, newData, 0666) + if err != nil { + log.Fatal("convertFile: ioutil.WriteFile:", err) + } + return true +} diff --git a/fonts/OPEN-SANS-LICENSE.txt b/fonts/OPEN-SANS-LICENSE.txt new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/fonts/OPEN-SANS-LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/fonts/SOURCE-CODE-PRO-LICENSE.txt b/fonts/SOURCE-CODE-PRO-LICENSE.txt new file mode 100644 index 0000000..366206f --- /dev/null +++ b/fonts/SOURCE-CODE-PRO-LICENSE.txt @@ -0,0 +1,93 @@ +Copyright 2010, 2012 Adobe Systems Incorporated (http://www.adobe.com/), with Reserved Font Name 'Source'. All Rights Reserved. Source is a trademark of Adobe Systems Incorporated in the United States and/or other countries. + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/fonts/fonts.css b/fonts/fonts.css new file mode 100644 index 0000000..858efa5 --- /dev/null +++ b/fonts/fonts.css @@ -0,0 +1,100 @@ +/* Open Sans is licensed under the Apache License, Version 2.0. See http://www.apache.org/licenses/LICENSE-2.0 */ +/* Source Code Pro is under the Open Font License. See https://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=OFL */ + +/* open-sans-300 - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 300; + src: local('Open Sans Light'), local('OpenSans-Light'), + url('open-sans-v17-all-charsets-300.woff2') format('woff2'); +} + +/* open-sans-300italic - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */ +@font-face { + font-family: 'Open Sans'; + font-style: italic; + font-weight: 300; + src: local('Open Sans Light Italic'), local('OpenSans-LightItalic'), + url('open-sans-v17-all-charsets-300italic.woff2') format('woff2'); +} + +/* open-sans-regular - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 400; + src: local('Open Sans Regular'), local('OpenSans-Regular'), + url('open-sans-v17-all-charsets-regular.woff2') format('woff2'); +} + +/* open-sans-italic - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */ +@font-face { + font-family: 'Open Sans'; + font-style: italic; + font-weight: 400; + src: local('Open Sans Italic'), local('OpenSans-Italic'), + url('open-sans-v17-all-charsets-italic.woff2') format('woff2'); +} + +/* open-sans-600 - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 600; + src: local('Open Sans SemiBold'), local('OpenSans-SemiBold'), + url('open-sans-v17-all-charsets-600.woff2') format('woff2'); +} + +/* open-sans-600italic - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */ +@font-face { + font-family: 'Open Sans'; + font-style: italic; + font-weight: 600; + src: local('Open Sans SemiBold Italic'), local('OpenSans-SemiBoldItalic'), + url('open-sans-v17-all-charsets-600italic.woff2') format('woff2'); +} + +/* open-sans-700 - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 700; + src: local('Open Sans Bold'), local('OpenSans-Bold'), + url('open-sans-v17-all-charsets-700.woff2') format('woff2'); +} + +/* open-sans-700italic - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */ +@font-face { + font-family: 'Open Sans'; + font-style: italic; + font-weight: 700; + src: local('Open Sans Bold Italic'), local('OpenSans-BoldItalic'), + url('open-sans-v17-all-charsets-700italic.woff2') format('woff2'); +} + +/* open-sans-800 - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 800; + src: local('Open Sans ExtraBold'), local('OpenSans-ExtraBold'), + url('open-sans-v17-all-charsets-800.woff2') format('woff2'); +} + +/* open-sans-800italic - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */ +@font-face { + font-family: 'Open Sans'; + font-style: italic; + font-weight: 800; + src: local('Open Sans ExtraBold Italic'), local('OpenSans-ExtraBoldItalic'), + url('open-sans-v17-all-charsets-800italic.woff2') format('woff2'); +} + +/* source-code-pro-500 - latin_vietnamese_latin-ext_greek_cyrillic-ext_cyrillic */ +@font-face { + font-family: 'Source Code Pro'; + font-style: normal; + font-weight: 500; + src: url('source-code-pro-v11-all-charsets-500.woff2') format('woff2'); +} diff --git a/fonts/open-sans-v17-all-charsets-300.woff2 b/fonts/open-sans-v17-all-charsets-300.woff2 new file mode 100644 index 0000000..9f51be3 Binary files /dev/null and b/fonts/open-sans-v17-all-charsets-300.woff2 differ diff --git a/fonts/open-sans-v17-all-charsets-300italic.woff2 b/fonts/open-sans-v17-all-charsets-300italic.woff2 new file mode 100644 index 0000000..2f54544 Binary files /dev/null and b/fonts/open-sans-v17-all-charsets-300italic.woff2 differ diff --git a/fonts/open-sans-v17-all-charsets-600.woff2 b/fonts/open-sans-v17-all-charsets-600.woff2 new file mode 100644 index 0000000..f503d55 Binary files /dev/null and b/fonts/open-sans-v17-all-charsets-600.woff2 differ diff --git a/fonts/open-sans-v17-all-charsets-600italic.woff2 b/fonts/open-sans-v17-all-charsets-600italic.woff2 new file mode 100644 index 0000000..c99aabe Binary files /dev/null and b/fonts/open-sans-v17-all-charsets-600italic.woff2 differ diff --git a/fonts/open-sans-v17-all-charsets-700.woff2 b/fonts/open-sans-v17-all-charsets-700.woff2 new file mode 100644 index 0000000..421a1ab Binary files /dev/null and b/fonts/open-sans-v17-all-charsets-700.woff2 differ diff --git a/fonts/open-sans-v17-all-charsets-700italic.woff2 b/fonts/open-sans-v17-all-charsets-700italic.woff2 new file mode 100644 index 0000000..12ce3d2 Binary files /dev/null and b/fonts/open-sans-v17-all-charsets-700italic.woff2 differ diff --git a/fonts/open-sans-v17-all-charsets-800.woff2 b/fonts/open-sans-v17-all-charsets-800.woff2 new file mode 100644 index 0000000..c94a223 Binary files /dev/null and b/fonts/open-sans-v17-all-charsets-800.woff2 differ diff --git a/fonts/open-sans-v17-all-charsets-800italic.woff2 b/fonts/open-sans-v17-all-charsets-800italic.woff2 new file mode 100644 index 0000000..eed7d3c Binary files /dev/null and b/fonts/open-sans-v17-all-charsets-800italic.woff2 differ diff --git a/fonts/open-sans-v17-all-charsets-italic.woff2 b/fonts/open-sans-v17-all-charsets-italic.woff2 new file mode 100644 index 0000000..398b68a Binary files /dev/null and b/fonts/open-sans-v17-all-charsets-italic.woff2 differ diff --git a/fonts/open-sans-v17-all-charsets-regular.woff2 b/fonts/open-sans-v17-all-charsets-regular.woff2 new file mode 100644 index 0000000..8383e94 Binary files /dev/null and b/fonts/open-sans-v17-all-charsets-regular.woff2 differ diff --git a/fonts/source-code-pro-v11-all-charsets-500.woff2 b/fonts/source-code-pro-v11-all-charsets-500.woff2 new file mode 100644 index 0000000..7222456 Binary files /dev/null and b/fonts/source-code-pro-v11-all-charsets-500.woff2 differ diff --git a/gopl-zh-qrcode.png b/gopl-zh-qrcode.png new file mode 100644 index 0000000..4912088 Binary files /dev/null and b/gopl-zh-qrcode.png differ diff --git a/highlight.css b/highlight.css new file mode 100644 index 0000000..c234322 --- /dev/null +++ b/highlight.css @@ -0,0 +1,83 @@ +/* + * An increased contrast highlighting scheme loosely based on the + * "Base16 Atelier Dune Light" theme by Bram de Haan + * (http://atelierbram.github.io/syntax-highlighting/atelier-schemes/dune) + * Original Base16 color scheme by Chris Kempson + * (https://github.com/chriskempson/base16) + */ + +/* Comment */ +.hljs-comment, +.hljs-quote { + color: #575757; +} + +/* Red */ +.hljs-variable, +.hljs-template-variable, +.hljs-attribute, +.hljs-tag, +.hljs-name, +.hljs-regexp, +.hljs-link, +.hljs-name, +.hljs-selector-id, +.hljs-selector-class { + color: #d70025; +} + +/* Orange */ +.hljs-number, +.hljs-meta, +.hljs-built_in, +.hljs-builtin-name, +.hljs-literal, +.hljs-type, +.hljs-params { + color: #b21e00; +} + +/* Green */ +.hljs-string, +.hljs-symbol, +.hljs-bullet { + color: #008200; +} + +/* Blue */ +.hljs-title, +.hljs-section { + color: #0030f2; +} + +/* Purple */ +.hljs-keyword, +.hljs-selector-tag { + color: #9d00ec; +} + +.hljs { + display: block; + overflow-x: auto; + background: #f6f7f6; + color: #000; + padding: 0.5em; +} + +.hljs-emphasis { + font-style: italic; +} + +.hljs-strong { + font-weight: bold; +} + +.hljs-addition { + color: #22863a; + background-color: #f0fff4; +} + +.hljs-deletion { + color: #b31d28; + background-color: #ffeef0; +} diff --git a/highlight.js b/highlight.js new file mode 100644 index 0000000..180385b --- /dev/null +++ b/highlight.js @@ -0,0 +1,6 @@ +/* + Highlight.js 10.1.1 (93fd0d73) + License: BSD-3-Clause + Copyright (c) 2006-2020, Ivan Sagalaev +*/ +var hljs=function(){"use strict";function e(n){Object.freeze(n);var t="function"==typeof n;return Object.getOwnPropertyNames(n).forEach((function(r){!Object.hasOwnProperty.call(n,r)||null===n[r]||"object"!=typeof n[r]&&"function"!=typeof n[r]||t&&("caller"===r||"callee"===r||"arguments"===r)||Object.isFrozen(n[r])||e(n[r])})),n}class n{constructor(e){void 0===e.data&&(e.data={}),this.data=e.data}ignoreMatch(){this.ignore=!0}}function t(e){return e.replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'")}function r(e,...n){var t={};for(const n in e)t[n]=e[n];return n.forEach((function(e){for(const n in e)t[n]=e[n]})),t}function a(e){return e.nodeName.toLowerCase()}var i=Object.freeze({__proto__:null,escapeHTML:t,inherit:r,nodeStream:function(e){var n=[];return function e(t,r){for(var i=t.firstChild;i;i=i.nextSibling)3===i.nodeType?r+=i.nodeValue.length:1===i.nodeType&&(n.push({event:"start",offset:r,node:i}),r=e(i,r),a(i).match(/br|hr|img|input/)||n.push({event:"stop",offset:r,node:i}));return r}(e,0),n},mergeStreams:function(e,n,r){var i=0,s="",o=[];function l(){return e.length&&n.length?e[0].offset!==n[0].offset?e[0].offset"}function u(e){s+=""}function d(e){("start"===e.event?c:u)(e.node)}for(;e.length||n.length;){var g=l();if(s+=t(r.substring(i,g[0].offset)),i=g[0].offset,g===e){o.reverse().forEach(u);do{d(g.splice(0,1)[0]),g=l()}while(g===e&&g.length&&g[0].offset===i);o.reverse().forEach(c)}else"start"===g[0].event?o.push(g[0].node):o.pop(),d(g.splice(0,1)[0])}return s+t(r.substr(i))}});const s="",o=e=>!!e.kind;class l{constructor(e,n){this.buffer="",this.classPrefix=n.classPrefix,e.walk(this)}addText(e){this.buffer+=t(e)}openNode(e){if(!o(e))return;let n=e.kind;e.sublanguage||(n=`${this.classPrefix}${n}`),this.span(n)}closeNode(e){o(e)&&(this.buffer+=s)}value(){return this.buffer}span(e){this.buffer+=``}}class c{constructor(){this.rootNode={children:[]},this.stack=[this.rootNode]}get top(){return this.stack[this.stack.length-1]}get root(){return this.rootNode}add(e){this.top.children.push(e)}openNode(e){const n={kind:e,children:[]};this.add(n),this.stack.push(n)}closeNode(){if(this.stack.length>1)return this.stack.pop()}closeAllNodes(){for(;this.closeNode(););}toJSON(){return JSON.stringify(this.rootNode,null,4)}walk(e){return this.constructor._walk(e,this.rootNode)}static _walk(e,n){return"string"==typeof n?e.addText(n):n.children&&(e.openNode(n),n.children.forEach(n=>this._walk(e,n)),e.closeNode(n)),e}static _collapse(e){"string"!=typeof e&&e.children&&(e.children.every(e=>"string"==typeof e)?e.children=[e.children.join("")]:e.children.forEach(e=>{c._collapse(e)}))}}class u extends c{constructor(e){super(),this.options=e}addKeyword(e,n){""!==e&&(this.openNode(n),this.addText(e),this.closeNode())}addText(e){""!==e&&this.add(e)}addSublanguage(e,n){const t=e.root;t.kind=n,t.sublanguage=!0,this.add(t)}toHTML(){return new l(this,this.options).value()}finalize(){return!0}}function d(e){return e?"string"==typeof e?e:e.source:null}const g="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",h={begin:"\\\\[\\s\\S]",relevance:0},f={className:"string",begin:"'",end:"'",illegal:"\\n",contains:[h]},p={className:"string",begin:'"',end:'"',illegal:"\\n",contains:[h]},b={begin:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/},m=function(e,n,t={}){var a=r({className:"comment",begin:e,end:n,contains:[]},t);return a.contains.push(b),a.contains.push({className:"doctag",begin:"(?:TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):",relevance:0}),a},v=m("//","$"),x=m("/\\*","\\*/"),E=m("#","$");var _=Object.freeze({__proto__:null,IDENT_RE:"[a-zA-Z]\\w*",UNDERSCORE_IDENT_RE:"[a-zA-Z_]\\w*",NUMBER_RE:"\\b\\d+(\\.\\d+)?",C_NUMBER_RE:g,BINARY_NUMBER_RE:"\\b(0b[01]+)",RE_STARTERS_RE:"!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~",SHEBANG:(e={})=>{const n=/^#![ ]*\//;return e.binary&&(e.begin=function(...e){return e.map(e=>d(e)).join("")}(n,/.*\b/,e.binary,/\b.*/)),r({className:"meta",begin:n,end:/$/,relevance:0,"on:begin":(e,n)=>{0!==e.index&&n.ignoreMatch()}},e)},BACKSLASH_ESCAPE:h,APOS_STRING_MODE:f,QUOTE_STRING_MODE:p,PHRASAL_WORDS_MODE:b,COMMENT:m,C_LINE_COMMENT_MODE:v,C_BLOCK_COMMENT_MODE:x,HASH_COMMENT_MODE:E,NUMBER_MODE:{className:"number",begin:"\\b\\d+(\\.\\d+)?",relevance:0},C_NUMBER_MODE:{className:"number",begin:g,relevance:0},BINARY_NUMBER_MODE:{className:"number",begin:"\\b(0b[01]+)",relevance:0},CSS_NUMBER_MODE:{className:"number",begin:"\\b\\d+(\\.\\d+)?(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?",relevance:0},REGEXP_MODE:{begin:/(?=\/[^/\n]*\/)/,contains:[{className:"regexp",begin:/\//,end:/\/[gimuy]*/,illegal:/\n/,contains:[h,{begin:/\[/,end:/\]/,relevance:0,contains:[h]}]}]},TITLE_MODE:{className:"title",begin:"[a-zA-Z]\\w*",relevance:0},UNDERSCORE_TITLE_MODE:{className:"title",begin:"[a-zA-Z_]\\w*",relevance:0},METHOD_GUARD:{begin:"\\.\\s*[a-zA-Z_]\\w*",relevance:0},END_SAME_AS_BEGIN:function(e){return Object.assign(e,{"on:begin":(e,n)=>{n.data._beginMatch=e[1]},"on:end":(e,n)=>{n.data._beginMatch!==e[1]&&n.ignoreMatch()}})}}),N="of and for in not or if then".split(" ");function w(e,n){return n?+n:function(e){return N.includes(e.toLowerCase())}(e)?0:1}const R=t,y=r,{nodeStream:k,mergeStreams:O}=i,M=Symbol("nomatch");return function(t){var a=[],i={},s={},o=[],l=!0,c=/(^(<[^>]+>|\t|)+|\n)/gm,g="Could not find the language '{}', did you forget to load/include a language module?";const h={disableAutodetect:!0,name:"Plain text",contains:[]};var f={noHighlightRe:/^(no-?highlight)$/i,languageDetectRe:/\blang(?:uage)?-([\w-]+)\b/i,classPrefix:"hljs-",tabReplace:null,useBR:!1,languages:null,__emitter:u};function p(e){return f.noHighlightRe.test(e)}function b(e,n,t,r){var a={code:n,language:e};S("before:highlight",a);var i=a.result?a.result:m(a.language,a.code,t,r);return i.code=a.code,S("after:highlight",i),i}function m(e,t,a,s){var o=t;function c(e,n){var t=E.case_insensitive?n[0].toLowerCase():n[0];return Object.prototype.hasOwnProperty.call(e.keywords,t)&&e.keywords[t]}function u(){null!=y.subLanguage?function(){if(""!==A){var e=null;if("string"==typeof y.subLanguage){if(!i[y.subLanguage])return void O.addText(A);e=m(y.subLanguage,A,!0,k[y.subLanguage]),k[y.subLanguage]=e.top}else e=v(A,y.subLanguage.length?y.subLanguage:null);y.relevance>0&&(I+=e.relevance),O.addSublanguage(e.emitter,e.language)}}():function(){if(!y.keywords)return void O.addText(A);let e=0;y.keywordPatternRe.lastIndex=0;let n=y.keywordPatternRe.exec(A),t="";for(;n;){t+=A.substring(e,n.index);const r=c(y,n);if(r){const[e,a]=r;O.addText(t),t="",I+=a,O.addKeyword(n[0],e)}else t+=n[0];e=y.keywordPatternRe.lastIndex,n=y.keywordPatternRe.exec(A)}t+=A.substr(e),O.addText(t)}(),A=""}function h(e){return e.className&&O.openNode(e.className),y=Object.create(e,{parent:{value:y}})}function p(e){return 0===y.matcher.regexIndex?(A+=e[0],1):(L=!0,0)}var b={};function x(t,r){var i=r&&r[0];if(A+=t,null==i)return u(),0;if("begin"===b.type&&"end"===r.type&&b.index===r.index&&""===i){if(A+=o.slice(r.index,r.index+1),!l){const n=Error("0 width match regex");throw n.languageName=e,n.badRule=b.rule,n}return 1}if(b=r,"begin"===r.type)return function(e){var t=e[0],r=e.rule;const a=new n(r),i=[r.__beforeBegin,r["on:begin"]];for(const n of i)if(n&&(n(e,a),a.ignore))return p(t);return r&&r.endSameAsBegin&&(r.endRe=RegExp(t.replace(/[-/\\^$*+?.()|[\]{}]/g,"\\$&"),"m")),r.skip?A+=t:(r.excludeBegin&&(A+=t),u(),r.returnBegin||r.excludeBegin||(A=t)),h(r),r.returnBegin?0:t.length}(r);if("illegal"===r.type&&!a){const e=Error('Illegal lexeme "'+i+'" for mode "'+(y.className||"")+'"');throw e.mode=y,e}if("end"===r.type){var s=function(e){var t=e[0],r=o.substr(e.index),a=function e(t,r,a){let i=function(e,n){var t=e&&e.exec(n);return t&&0===t.index}(t.endRe,a);if(i){if(t["on:end"]){const e=new n(t);t["on:end"](r,e),e.ignore&&(i=!1)}if(i){for(;t.endsParent&&t.parent;)t=t.parent;return t}}if(t.endsWithParent)return e(t.parent,r,a)}(y,e,r);if(!a)return M;var i=y;i.skip?A+=t:(i.returnEnd||i.excludeEnd||(A+=t),u(),i.excludeEnd&&(A=t));do{y.className&&O.closeNode(),y.skip||y.subLanguage||(I+=y.relevance),y=y.parent}while(y!==a.parent);return a.starts&&(a.endSameAsBegin&&(a.starts.endRe=a.endRe),h(a.starts)),i.returnEnd?0:t.length}(r);if(s!==M)return s}if("illegal"===r.type&&""===i)return 1;if(B>1e5&&B>3*r.index)throw Error("potential infinite loop, way more iterations than matches");return A+=i,i.length}var E=T(e);if(!E)throw console.error(g.replace("{}",e)),Error('Unknown language: "'+e+'"');var _=function(e){function n(n,t){return RegExp(d(n),"m"+(e.case_insensitive?"i":"")+(t?"g":""))}class t{constructor(){this.matchIndexes={},this.regexes=[],this.matchAt=1,this.position=0}addRule(e,n){n.position=this.position++,this.matchIndexes[this.matchAt]=n,this.regexes.push([n,e]),this.matchAt+=function(e){return RegExp(e.toString()+"|").exec("").length-1}(e)+1}compile(){0===this.regexes.length&&(this.exec=()=>null);const e=this.regexes.map(e=>e[1]);this.matcherRe=n(function(e,n="|"){for(var t=/\[(?:[^\\\]]|\\.)*\]|\(\??|\\([1-9][0-9]*)|\\./,r=0,a="",i=0;i0&&(a+=n),a+="(";o.length>0;){var l=t.exec(o);if(null==l){a+=o;break}a+=o.substring(0,l.index),o=o.substring(l.index+l[0].length),"\\"===l[0][0]&&l[1]?a+="\\"+(+l[1]+s):(a+=l[0],"("===l[0]&&r++)}a+=")"}return a}(e),!0),this.lastIndex=0}exec(e){this.matcherRe.lastIndex=this.lastIndex;const n=this.matcherRe.exec(e);if(!n)return null;const t=n.findIndex((e,n)=>n>0&&void 0!==e),r=this.matchIndexes[t];return n.splice(0,t),Object.assign(n,r)}}class a{constructor(){this.rules=[],this.multiRegexes=[],this.count=0,this.lastIndex=0,this.regexIndex=0}getMatcher(e){if(this.multiRegexes[e])return this.multiRegexes[e];const n=new t;return this.rules.slice(e).forEach(([e,t])=>n.addRule(e,t)),n.compile(),this.multiRegexes[e]=n,n}considerAll(){this.regexIndex=0}addRule(e,n){this.rules.push([e,n]),"begin"===n.type&&this.count++}exec(e){const n=this.getMatcher(this.regexIndex);n.lastIndex=this.lastIndex;const t=n.exec(e);return t&&(this.regexIndex+=t.position+1,this.regexIndex===this.count&&(this.regexIndex=0)),t}}function i(e,n){const t=e.input[e.index-1],r=e.input[e.index+e[0].length];"."!==t&&"."!==r||n.ignoreMatch()}if(e.contains&&e.contains.includes("self"))throw Error("ERR: contains `self` is not supported at the top-level of a language. See documentation.");return function t(s,o){const l=s;if(s.compiled)return l;s.compiled=!0,s.__beforeBegin=null,s.keywords=s.keywords||s.beginKeywords;let c=null;if("object"==typeof s.keywords&&(c=s.keywords.$pattern,delete s.keywords.$pattern),s.keywords&&(s.keywords=function(e,n){var t={};return"string"==typeof e?r("keyword",e):Object.keys(e).forEach((function(n){r(n,e[n])})),t;function r(e,r){n&&(r=r.toLowerCase()),r.split(" ").forEach((function(n){var r=n.split("|");t[r[0]]=[e,w(r[0],r[1])]}))}}(s.keywords,e.case_insensitive)),s.lexemes&&c)throw Error("ERR: Prefer `keywords.$pattern` to `mode.lexemes`, BOTH are not allowed. (see mode reference) ");return l.keywordPatternRe=n(s.lexemes||c||/\w+/,!0),o&&(s.beginKeywords&&(s.begin="\\b("+s.beginKeywords.split(" ").join("|")+")(?=\\b|\\s)",s.__beforeBegin=i),s.begin||(s.begin=/\B|\b/),l.beginRe=n(s.begin),s.endSameAsBegin&&(s.end=s.begin),s.end||s.endsWithParent||(s.end=/\B|\b/),s.end&&(l.endRe=n(s.end)),l.terminator_end=d(s.end)||"",s.endsWithParent&&o.terminator_end&&(l.terminator_end+=(s.end?"|":"")+o.terminator_end)),s.illegal&&(l.illegalRe=n(s.illegal)),void 0===s.relevance&&(s.relevance=1),s.contains||(s.contains=[]),s.contains=[].concat(...s.contains.map((function(e){return function(e){return e.variants&&!e.cached_variants&&(e.cached_variants=e.variants.map((function(n){return r(e,{variants:null},n)}))),e.cached_variants?e.cached_variants:function e(n){return!!n&&(n.endsWithParent||e(n.starts))}(e)?r(e,{starts:e.starts?r(e.starts):null}):Object.isFrozen(e)?r(e):e}("self"===e?s:e)}))),s.contains.forEach((function(e){t(e,l)})),s.starts&&t(s.starts,o),l.matcher=function(e){const n=new a;return e.contains.forEach(e=>n.addRule(e.begin,{rule:e,type:"begin"})),e.terminator_end&&n.addRule(e.terminator_end,{type:"end"}),e.illegal&&n.addRule(e.illegal,{type:"illegal"}),n}(l),l}(e)}(E),N="",y=s||_,k={},O=new f.__emitter(f);!function(){for(var e=[],n=y;n!==E;n=n.parent)n.className&&e.unshift(n.className);e.forEach(e=>O.openNode(e))}();var A="",I=0,S=0,B=0,L=!1;try{for(y.matcher.considerAll();;){B++,L?L=!1:(y.matcher.lastIndex=S,y.matcher.considerAll());const e=y.matcher.exec(o);if(!e)break;const n=x(o.substring(S,e.index),e);S=e.index+n}return x(o.substr(S)),O.closeAllNodes(),O.finalize(),N=O.toHTML(),{relevance:I,value:N,language:e,illegal:!1,emitter:O,top:y}}catch(n){if(n.message&&n.message.includes("Illegal"))return{illegal:!0,illegalBy:{msg:n.message,context:o.slice(S-100,S+100),mode:n.mode},sofar:N,relevance:0,value:R(o),emitter:O};if(l)return{illegal:!1,relevance:0,value:R(o),emitter:O,language:e,top:y,errorRaised:n};throw n}}function v(e,n){n=n||f.languages||Object.keys(i);var t=function(e){const n={relevance:0,emitter:new f.__emitter(f),value:R(e),illegal:!1,top:h};return n.emitter.addText(e),n}(e),r=t;return n.filter(T).filter(I).forEach((function(n){var a=m(n,e,!1);a.language=n,a.relevance>r.relevance&&(r=a),a.relevance>t.relevance&&(r=t,t=a)})),r.language&&(t.second_best=r),t}function x(e){return f.tabReplace||f.useBR?e.replace(c,e=>"\n"===e?f.useBR?"
":e:f.tabReplace?e.replace(/\t/g,f.tabReplace):e):e}function E(e){let n=null;const t=function(e){var n=e.className+" ";n+=e.parentNode?e.parentNode.className:"";const t=f.languageDetectRe.exec(n);if(t){var r=T(t[1]);return r||(console.warn(g.replace("{}",t[1])),console.warn("Falling back to no-highlight mode for this block.",e)),r?t[1]:"no-highlight"}return n.split(/\s+/).find(e=>p(e)||T(e))}(e);if(p(t))return;S("before:highlightBlock",{block:e,language:t}),f.useBR?(n=document.createElement("div")).innerHTML=e.innerHTML.replace(/\n/g,"").replace(//g,"\n"):n=e;const r=n.textContent,a=t?b(t,r,!0):v(r),i=k(n);if(i.length){const e=document.createElement("div");e.innerHTML=a.value,a.value=O(i,k(e),r)}a.value=x(a.value),S("after:highlightBlock",{block:e,result:a}),e.innerHTML=a.value,e.className=function(e,n,t){var r=n?s[n]:t,a=[e.trim()];return e.match(/\bhljs\b/)||a.push("hljs"),e.includes(r)||a.push(r),a.join(" ").trim()}(e.className,t,a.language),e.result={language:a.language,re:a.relevance,relavance:a.relevance},a.second_best&&(e.second_best={language:a.second_best.language,re:a.second_best.relevance,relavance:a.second_best.relevance})}const N=()=>{if(!N.called){N.called=!0;var e=document.querySelectorAll("pre code");a.forEach.call(e,E)}};function T(e){return e=(e||"").toLowerCase(),i[e]||i[s[e]]}function A(e,{languageName:n}){"string"==typeof e&&(e=[e]),e.forEach(e=>{s[e]=n})}function I(e){var n=T(e);return n&&!n.disableAutodetect}function S(e,n){var t=e;o.forEach((function(e){e[t]&&e[t](n)}))}Object.assign(t,{highlight:b,highlightAuto:v,fixMarkup:x,highlightBlock:E,configure:function(e){f=y(f,e)},initHighlighting:N,initHighlightingOnLoad:function(){window.addEventListener("DOMContentLoaded",N,!1)},registerLanguage:function(e,n){var r=null;try{r=n(t)}catch(n){if(console.error("Language definition for '{}' could not be registered.".replace("{}",e)),!l)throw n;console.error(n),r=h}r.name||(r.name=e),i[e]=r,r.rawDefinition=n.bind(null,t),r.aliases&&A(r.aliases,{languageName:e})},listLanguages:function(){return Object.keys(i)},getLanguage:T,registerAliases:A,requireLanguage:function(e){var n=T(e);if(n)return n;throw Error("The '{}' language is required, but not loaded.".replace("{}",e))},autoDetection:I,inherit:y,addPlugin:function(e){o.push(e)}}),t.debugMode=function(){l=!1},t.safeMode=function(){l=!0},t.versionString="10.1.1";for(const n in _)"object"==typeof _[n]&&e(_[n]);return Object.assign(t,_),t}({})}();"object"==typeof exports&&"undefined"!=typeof module&&(module.exports=hljs);hljs.registerLanguage("php",function(){"use strict";return function(e){var r={begin:"\\$+[a-zA-Z_-ÿ][a-zA-Z0-9_-ÿ]*"},t={className:"meta",variants:[{begin:/<\?php/,relevance:10},{begin:/<\?[=]?/},{begin:/\?>/}]},a={className:"string",contains:[e.BACKSLASH_ESCAPE,t],variants:[{begin:'b"',end:'"'},{begin:"b'",end:"'"},e.inherit(e.APOS_STRING_MODE,{illegal:null}),e.inherit(e.QUOTE_STRING_MODE,{illegal:null})]},n={variants:[e.BINARY_NUMBER_MODE,e.C_NUMBER_MODE]},i={keyword:"__CLASS__ __DIR__ __FILE__ __FUNCTION__ __LINE__ __METHOD__ __NAMESPACE__ __TRAIT__ die echo exit include include_once print require require_once array abstract and as binary bool boolean break callable case catch class clone const continue declare default do double else elseif empty enddeclare endfor endforeach endif endswitch endwhile eval extends final finally float for foreach from global goto if implements instanceof insteadof int integer interface isset iterable list new object or private protected public real return string switch throw trait try unset use var void while xor yield",literal:"false null true",built_in:"Error|0 AppendIterator ArgumentCountError ArithmeticError ArrayIterator ArrayObject AssertionError BadFunctionCallException BadMethodCallException CachingIterator CallbackFilterIterator CompileError Countable DirectoryIterator DivisionByZeroError DomainException EmptyIterator ErrorException Exception FilesystemIterator FilterIterator GlobIterator InfiniteIterator InvalidArgumentException IteratorIterator LengthException LimitIterator LogicException MultipleIterator NoRewindIterator OutOfBoundsException OutOfRangeException OuterIterator OverflowException ParentIterator ParseError RangeException RecursiveArrayIterator RecursiveCachingIterator RecursiveCallbackFilterIterator RecursiveDirectoryIterator RecursiveFilterIterator RecursiveIterator RecursiveIteratorIterator RecursiveRegexIterator RecursiveTreeIterator RegexIterator RuntimeException SeekableIterator SplDoublyLinkedList SplFileInfo SplFileObject SplFixedArray SplHeap SplMaxHeap SplMinHeap SplObjectStorage SplObserver SplObserver SplPriorityQueue SplQueue SplStack SplSubject SplSubject SplTempFileObject TypeError UnderflowException UnexpectedValueException ArrayAccess Closure Generator Iterator IteratorAggregate Serializable Throwable Traversable WeakReference Directory __PHP_Incomplete_Class parent php_user_filter self static stdClass"};return{aliases:["php","php3","php4","php5","php6","php7"],case_insensitive:!0,keywords:i,contains:[e.HASH_COMMENT_MODE,e.COMMENT("//","$",{contains:[t]}),e.COMMENT("/\\*","\\*/",{contains:[{className:"doctag",begin:"@[A-Za-z]+"}]}),e.COMMENT("__halt_compiler.+?;",!1,{endsWithParent:!0,keywords:"__halt_compiler"}),{className:"string",begin:/<<<['"]?\w+['"]?$/,end:/^\w+;?$/,contains:[e.BACKSLASH_ESCAPE,{className:"subst",variants:[{begin:/\$\w+/},{begin:/\{\$/,end:/\}/}]}]},t,{className:"keyword",begin:/\$this\b/},r,{begin:/(::|->)+[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/},{className:"function",beginKeywords:"fn function",end:/[;{]/,excludeEnd:!0,illegal:"[$%\\[]",contains:[e.UNDERSCORE_TITLE_MODE,{className:"params",begin:"\\(",end:"\\)",excludeBegin:!0,excludeEnd:!0,keywords:i,contains:["self",r,e.C_BLOCK_COMMENT_MODE,a,n]}]},{className:"class",beginKeywords:"class interface",end:"{",excludeEnd:!0,illegal:/[:\(\$"]/,contains:[{beginKeywords:"extends implements"},e.UNDERSCORE_TITLE_MODE]},{beginKeywords:"namespace",end:";",illegal:/[\.']/,contains:[e.UNDERSCORE_TITLE_MODE]},{beginKeywords:"use",end:";",contains:[e.UNDERSCORE_TITLE_MODE]},{begin:"=>"},a,n]}}}());hljs.registerLanguage("nginx",function(){"use strict";return function(e){var n={className:"variable",variants:[{begin:/\$\d+/},{begin:/\$\{/,end:/}/},{begin:"[\\$\\@]"+e.UNDERSCORE_IDENT_RE}]},a={endsWithParent:!0,keywords:{$pattern:"[a-z/_]+",literal:"on off yes no true false none blocked debug info notice warn error crit select break last permanent redirect kqueue rtsig epoll poll /dev/poll"},relevance:0,illegal:"=>",contains:[e.HASH_COMMENT_MODE,{className:"string",contains:[e.BACKSLASH_ESCAPE,n],variants:[{begin:/"/,end:/"/},{begin:/'/,end:/'/}]},{begin:"([a-z]+):/",end:"\\s",endsWithParent:!0,excludeEnd:!0,contains:[n]},{className:"regexp",contains:[e.BACKSLASH_ESCAPE,n],variants:[{begin:"\\s\\^",end:"\\s|{|;",returnEnd:!0},{begin:"~\\*?\\s+",end:"\\s|{|;",returnEnd:!0},{begin:"\\*(\\.[a-z\\-]+)+"},{begin:"([a-z\\-]+\\.)+\\*"}]},{className:"number",begin:"\\b\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}(:\\d{1,5})?\\b"},{className:"number",begin:"\\b\\d+[kKmMgGdshdwy]*\\b",relevance:0},n]};return{name:"Nginx config",aliases:["nginxconf"],contains:[e.HASH_COMMENT_MODE,{begin:e.UNDERSCORE_IDENT_RE+"\\s+{",returnBegin:!0,end:"{",contains:[{className:"section",begin:e.UNDERSCORE_IDENT_RE}],relevance:0},{begin:e.UNDERSCORE_IDENT_RE+"\\s",end:";|{",returnBegin:!0,contains:[{className:"attribute",begin:e.UNDERSCORE_IDENT_RE,starts:a}],relevance:0}],illegal:"[^\\s\\}]"}}}());hljs.registerLanguage("csharp",function(){"use strict";return function(e){var n={keyword:"abstract as base bool break byte case catch char checked const continue decimal default delegate do double enum event explicit extern finally fixed float for foreach goto if implicit in int interface internal is lock long object operator out override params private protected public readonly ref sbyte sealed short sizeof stackalloc static string struct switch this try typeof uint ulong unchecked unsafe ushort using virtual void volatile while add alias ascending async await by descending dynamic equals from get global group into join let nameof on orderby partial remove select set value var when where yield",literal:"null false true"},i=e.inherit(e.TITLE_MODE,{begin:"[a-zA-Z](\\.?\\w)*"}),a={className:"number",variants:[{begin:"\\b(0b[01']+)"},{begin:"(-?)\\b([\\d']+(\\.[\\d']*)?|\\.[\\d']+)(u|U|l|L|ul|UL|f|F|b|B)"},{begin:"(-?)(\\b0[xX][a-fA-F0-9']+|(\\b[\\d']+(\\.[\\d']*)?|\\.[\\d']+)([eE][-+]?[\\d']+)?)"}],relevance:0},s={className:"string",begin:'@"',end:'"',contains:[{begin:'""'}]},t=e.inherit(s,{illegal:/\n/}),l={className:"subst",begin:"{",end:"}",keywords:n},r=e.inherit(l,{illegal:/\n/}),c={className:"string",begin:/\$"/,end:'"',illegal:/\n/,contains:[{begin:"{{"},{begin:"}}"},e.BACKSLASH_ESCAPE,r]},o={className:"string",begin:/\$@"/,end:'"',contains:[{begin:"{{"},{begin:"}}"},{begin:'""'},l]},g=e.inherit(o,{illegal:/\n/,contains:[{begin:"{{"},{begin:"}}"},{begin:'""'},r]});l.contains=[o,c,s,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,a,e.C_BLOCK_COMMENT_MODE],r.contains=[g,c,t,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,a,e.inherit(e.C_BLOCK_COMMENT_MODE,{illegal:/\n/})];var d={variants:[o,c,s,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE]},E={begin:"<",end:">",contains:[{beginKeywords:"in out"},i]},_=e.IDENT_RE+"(<"+e.IDENT_RE+"(\\s*,\\s*"+e.IDENT_RE+")*>)?(\\[\\])?",b={begin:"@"+e.IDENT_RE,relevance:0};return{name:"C#",aliases:["cs","c#"],keywords:n,illegal:/::/,contains:[e.COMMENT("///","$",{returnBegin:!0,contains:[{className:"doctag",variants:[{begin:"///",relevance:0},{begin:"\x3c!--|--\x3e"},{begin:""}]}]}),e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,{className:"meta",begin:"#",end:"$",keywords:{"meta-keyword":"if else elif endif define undef warning error line region endregion pragma checksum"}},d,a,{beginKeywords:"class interface",end:/[{;=]/,illegal:/[^\s:,]/,contains:[{beginKeywords:"where class"},i,E,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{beginKeywords:"namespace",end:/[{;=]/,illegal:/[^\s:]/,contains:[i,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{className:"meta",begin:"^\\s*\\[",excludeBegin:!0,end:"\\]",excludeEnd:!0,contains:[{className:"meta-string",begin:/"/,end:/"/}]},{beginKeywords:"new return throw await else",relevance:0},{className:"function",begin:"("+_+"\\s+)+"+e.IDENT_RE+"\\s*(\\<.+\\>)?\\s*\\(",returnBegin:!0,end:/\s*[{;=]/,excludeEnd:!0,keywords:n,contains:[{begin:e.IDENT_RE+"\\s*(\\<.+\\>)?\\s*\\(",returnBegin:!0,contains:[e.TITLE_MODE,E],relevance:0},{className:"params",begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:n,relevance:0,contains:[d,a,e.C_BLOCK_COMMENT_MODE]},e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},b]}}}());hljs.registerLanguage("perl",function(){"use strict";return function(e){var n={$pattern:/[\w.]+/,keyword:"getpwent getservent quotemeta msgrcv scalar kill dbmclose undef lc ma syswrite tr send umask sysopen shmwrite vec qx utime local oct semctl localtime readpipe do return format read sprintf dbmopen pop getpgrp not getpwnam rewinddir qq fileno qw endprotoent wait sethostent bless s|0 opendir continue each sleep endgrent shutdown dump chomp connect getsockname die socketpair close flock exists index shmget sub for endpwent redo lstat msgctl setpgrp abs exit select print ref gethostbyaddr unshift fcntl syscall goto getnetbyaddr join gmtime symlink semget splice x|0 getpeername recv log setsockopt cos last reverse gethostbyname getgrnam study formline endhostent times chop length gethostent getnetent pack getprotoent getservbyname rand mkdir pos chmod y|0 substr endnetent printf next open msgsnd readdir use unlink getsockopt getpriority rindex wantarray hex system getservbyport endservent int chr untie rmdir prototype tell listen fork shmread ucfirst setprotoent else sysseek link getgrgid shmctl waitpid unpack getnetbyname reset chdir grep split require caller lcfirst until warn while values shift telldir getpwuid my getprotobynumber delete and sort uc defined srand accept package seekdir getprotobyname semop our rename seek if q|0 chroot sysread setpwent no crypt getc chown sqrt write setnetent setpriority foreach tie sin msgget map stat getlogin unless elsif truncate exec keys glob tied closedir ioctl socket readlink eval xor readline binmode setservent eof ord bind alarm pipe atan2 getgrent exp time push setgrent gt lt or ne m|0 break given say state when"},t={className:"subst",begin:"[$@]\\{",end:"\\}",keywords:n},s={begin:"->{",end:"}"},r={variants:[{begin:/\$\d/},{begin:/[\$%@](\^\w\b|#\w+(::\w+)*|{\w+}|\w+(::\w*)*)/},{begin:/[\$%@][^\s\w{]/,relevance:0}]},i=[e.BACKSLASH_ESCAPE,t,r],a=[r,e.HASH_COMMENT_MODE,e.COMMENT("^\\=\\w","\\=cut",{endsWithParent:!0}),s,{className:"string",contains:i,variants:[{begin:"q[qwxr]?\\s*\\(",end:"\\)",relevance:5},{begin:"q[qwxr]?\\s*\\[",end:"\\]",relevance:5},{begin:"q[qwxr]?\\s*\\{",end:"\\}",relevance:5},{begin:"q[qwxr]?\\s*\\|",end:"\\|",relevance:5},{begin:"q[qwxr]?\\s*\\<",end:"\\>",relevance:5},{begin:"qw\\s+q",end:"q",relevance:5},{begin:"'",end:"'",contains:[e.BACKSLASH_ESCAPE]},{begin:'"',end:'"'},{begin:"`",end:"`",contains:[e.BACKSLASH_ESCAPE]},{begin:"{\\w+}",contains:[],relevance:0},{begin:"-?\\w+\\s*\\=\\>",contains:[],relevance:0}]},{className:"number",begin:"(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b",relevance:0},{begin:"(\\/\\/|"+e.RE_STARTERS_RE+"|\\b(split|return|print|reverse|grep)\\b)\\s*",keywords:"split return print reverse grep",relevance:0,contains:[e.HASH_COMMENT_MODE,{className:"regexp",begin:"(s|tr|y)/(\\\\.|[^/])*/(\\\\.|[^/])*/[a-z]*",relevance:10},{className:"regexp",begin:"(m|qr)?/",end:"/[a-z]*",contains:[e.BACKSLASH_ESCAPE],relevance:0}]},{className:"function",beginKeywords:"sub",end:"(\\s*\\(.*?\\))?[;{]",excludeEnd:!0,relevance:5,contains:[e.TITLE_MODE]},{begin:"-\\w\\b",relevance:0},{begin:"^__DATA__$",end:"^__END__$",subLanguage:"mojolicious",contains:[{begin:"^@@.*",end:"$",className:"comment"}]}];return t.contains=a,s.contains=a,{name:"Perl",aliases:["pl","pm"],keywords:n,contains:a}}}());hljs.registerLanguage("swift",function(){"use strict";return function(e){var i={keyword:"#available #colorLiteral #column #else #elseif #endif #file #fileLiteral #function #if #imageLiteral #line #selector #sourceLocation _ __COLUMN__ __FILE__ __FUNCTION__ __LINE__ Any as as! as? associatedtype associativity break case catch class continue convenience default defer deinit didSet do dynamic dynamicType else enum extension fallthrough false fileprivate final for func get guard if import in indirect infix init inout internal is lazy left let mutating nil none nonmutating open operator optional override postfix precedence prefix private protocol Protocol public repeat required rethrows return right self Self set static struct subscript super switch throw throws true try try! try? Type typealias unowned var weak where while willSet",literal:"true false nil",built_in:"abs advance alignof alignofValue anyGenerator assert assertionFailure bridgeFromObjectiveC bridgeFromObjectiveCUnconditional bridgeToObjectiveC bridgeToObjectiveCUnconditional c compactMap contains count countElements countLeadingZeros debugPrint debugPrintln distance dropFirst dropLast dump encodeBitsAsWords enumerate equal fatalError filter find getBridgedObjectiveCType getVaList indices insertionSort isBridgedToObjectiveC isBridgedVerbatimToObjectiveC isUniquelyReferenced isUniquelyReferencedNonObjC join lazy lexicographicalCompare map max maxElement min minElement numericCast overlaps partition posix precondition preconditionFailure print println quickSort readLine reduce reflect reinterpretCast reverse roundUpToAlignment sizeof sizeofValue sort split startsWith stride strideof strideofValue swap toString transcode underestimateCount unsafeAddressOf unsafeBitCast unsafeDowncast unsafeUnwrap unsafeReflect withExtendedLifetime withObjectAtPlusZero withUnsafePointer withUnsafePointerToObject withUnsafeMutablePointer withUnsafeMutablePointers withUnsafePointer withUnsafePointers withVaList zip"},n=e.COMMENT("/\\*","\\*/",{contains:["self"]}),t={className:"subst",begin:/\\\(/,end:"\\)",keywords:i,contains:[]},a={className:"string",contains:[e.BACKSLASH_ESCAPE,t],variants:[{begin:/"""/,end:/"""/},{begin:/"/,end:/"/}]},r={className:"number",begin:"\\b([\\d_]+(\\.[\\deE_]+)?|0x[a-fA-F0-9_]+(\\.[a-fA-F0-9p_]+)?|0b[01_]+|0o[0-7_]+)\\b",relevance:0};return t.contains=[r],{name:"Swift",keywords:i,contains:[a,e.C_LINE_COMMENT_MODE,n,{className:"type",begin:"\\b[A-Z][\\wÀ-ʸ']*[!?]"},{className:"type",begin:"\\b[A-Z][\\wÀ-ʸ']*",relevance:0},r,{className:"function",beginKeywords:"func",end:"{",excludeEnd:!0,contains:[e.inherit(e.TITLE_MODE,{begin:/[A-Za-z$_][0-9A-Za-z$_]*/}),{begin://},{className:"params",begin:/\(/,end:/\)/,endsParent:!0,keywords:i,contains:["self",r,a,e.C_BLOCK_COMMENT_MODE,{begin:":"}],illegal:/["']/}],illegal:/\[|%/},{className:"class",beginKeywords:"struct protocol class extension enum",keywords:i,end:"\\{",excludeEnd:!0,contains:[e.inherit(e.TITLE_MODE,{begin:/[A-Za-z$_][\u00C0-\u02B80-9A-Za-z$_]*/})]},{className:"meta",begin:"(@discardableResult|@warn_unused_result|@exported|@lazy|@noescape|@NSCopying|@NSManaged|@objc|@objcMembers|@convention|@required|@noreturn|@IBAction|@IBDesignable|@IBInspectable|@IBOutlet|@infix|@prefix|@postfix|@autoclosure|@testable|@available|@nonobjc|@NSApplicationMain|@UIApplicationMain|@dynamicMemberLookup|@propertyWrapper)\\b"},{beginKeywords:"import",end:/$/,contains:[e.C_LINE_COMMENT_MODE,n]}]}}}());hljs.registerLanguage("makefile",function(){"use strict";return function(e){var i={className:"variable",variants:[{begin:"\\$\\("+e.UNDERSCORE_IDENT_RE+"\\)",contains:[e.BACKSLASH_ESCAPE]},{begin:/\$[@%`]+/}]}]}]};return{name:"HTML, XML",aliases:["html","xhtml","rss","atom","xjb","xsd","xsl","plist","wsf","svg"],case_insensitive:!0,contains:[{className:"meta",begin:"",relevance:10,contains:[a,i,t,s,{begin:"\\[",end:"\\]",contains:[{className:"meta",begin:"",contains:[a,s,i,t]}]}]},e.COMMENT("\x3c!--","--\x3e",{relevance:10}),{begin:"<\\!\\[CDATA\\[",end:"\\]\\]>",relevance:10},n,{className:"meta",begin:/<\?xml/,end:/\?>/,relevance:10},{className:"tag",begin:")",end:">",keywords:{name:"style"},contains:[c],starts:{end:"",returnEnd:!0,subLanguage:["css","xml"]}},{className:"tag",begin:")",end:">",keywords:{name:"script"},contains:[c],starts:{end:"<\/script>",returnEnd:!0,subLanguage:["javascript","handlebars","xml"]}},{className:"tag",begin:"",contains:[{className:"name",begin:/[^\/><\s]+/,relevance:0},c]}]}}}());hljs.registerLanguage("bash",function(){"use strict";return function(e){const s={};Object.assign(s,{className:"variable",variants:[{begin:/\$[\w\d#@][\w\d_]*/},{begin:/\$\{/,end:/\}/,contains:[{begin:/:-/,contains:[s]}]}]});const t={className:"subst",begin:/\$\(/,end:/\)/,contains:[e.BACKSLASH_ESCAPE]},n={className:"string",begin:/"/,end:/"/,contains:[e.BACKSLASH_ESCAPE,s,t]};t.contains.push(n);const a={begin:/\$\(\(/,end:/\)\)/,contains:[{begin:/\d+#[0-9a-f]+/,className:"number"},e.NUMBER_MODE,s]},i=e.SHEBANG({binary:"(fish|bash|zsh|sh|csh|ksh|tcsh|dash|scsh)",relevance:10}),c={className:"function",begin:/\w[\w\d_]*\s*\(\s*\)\s*\{/,returnBegin:!0,contains:[e.inherit(e.TITLE_MODE,{begin:/\w[\w\d_]*/})],relevance:0};return{name:"Bash",aliases:["sh","zsh"],keywords:{$pattern:/\b-?[a-z\._]+\b/,keyword:"if then else elif fi for while in do done case esac function",literal:"true false",built_in:"break cd continue eval exec exit export getopts hash pwd readonly return shift test times trap umask unset alias bind builtin caller command declare echo enable help let local logout mapfile printf read readarray source type typeset ulimit unalias set shopt autoload bg bindkey bye cap chdir clone comparguments compcall compctl compdescribe compfiles compgroups compquote comptags comptry compvalues dirs disable disown echotc echoti emulate fc fg float functions getcap getln history integer jobs kill limit log noglob popd print pushd pushln rehash sched setcap setopt stat suspend ttyctl unfunction unhash unlimit unsetopt vared wait whence where which zcompile zformat zftp zle zmodload zparseopts zprof zpty zregexparse zsocket zstyle ztcp",_:"-ne -eq -lt -gt -f -d -e -s -l -a"},contains:[i,e.SHEBANG(),c,a,e.HASH_COMMENT_MODE,n,{className:"",begin:/\\"/},{className:"string",begin:/'/,end:/'/},s]}}}());hljs.registerLanguage("c-like",function(){"use strict";return function(e){function t(e){return"(?:"+e+")?"}var n="(decltype\\(auto\\)|"+t("[a-zA-Z_]\\w*::")+"[a-zA-Z_]\\w*"+t("<.*?>")+")",r={className:"keyword",begin:"\\b[a-z\\d_]*_t\\b"},a={className:"string",variants:[{begin:'(u8?|U|L)?"',end:'"',illegal:"\\n",contains:[e.BACKSLASH_ESCAPE]},{begin:"(u8?|U|L)?'(\\\\(x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4,8}|[0-7]{3}|\\S)|.)",end:"'",illegal:"."},e.END_SAME_AS_BEGIN({begin:/(?:u8?|U|L)?R"([^()\\ ]{0,16})\(/,end:/\)([^()\\ ]{0,16})"/})]},i={className:"number",variants:[{begin:"\\b(0b[01']+)"},{begin:"(-?)\\b([\\d']+(\\.[\\d']*)?|\\.[\\d']+)(u|U|l|L|ul|UL|f|F|b|B)"},{begin:"(-?)(\\b0[xX][a-fA-F0-9']+|(\\b[\\d']+(\\.[\\d']*)?|\\.[\\d']+)([eE][-+]?[\\d']+)?)"}],relevance:0},s={className:"meta",begin:/#\s*[a-z]+\b/,end:/$/,keywords:{"meta-keyword":"if else elif endif define undef warning error line pragma _Pragma ifdef ifndef include"},contains:[{begin:/\\\n/,relevance:0},e.inherit(a,{className:"meta-string"}),{className:"meta-string",begin:/<.*?>/,end:/$/,illegal:"\\n"},e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},o={className:"title",begin:t("[a-zA-Z_]\\w*::")+e.IDENT_RE,relevance:0},c=t("[a-zA-Z_]\\w*::")+e.IDENT_RE+"\\s*\\(",l={keyword:"int float while private char char8_t char16_t char32_t catch import module export virtual operator sizeof dynamic_cast|10 typedef const_cast|10 const for static_cast|10 union namespace unsigned long volatile static protected bool template mutable if public friend do goto auto void enum else break extern using asm case typeid wchar_t short reinterpret_cast|10 default double register explicit signed typename try this switch continue inline delete alignas alignof constexpr consteval constinit decltype concept co_await co_return co_yield requires noexcept static_assert thread_local restrict final override atomic_bool atomic_char atomic_schar atomic_uchar atomic_short atomic_ushort atomic_int atomic_uint atomic_long atomic_ulong atomic_llong atomic_ullong new throw return and and_eq bitand bitor compl not not_eq or or_eq xor xor_eq",built_in:"std string wstring cin cout cerr clog stdin stdout stderr stringstream istringstream ostringstream auto_ptr deque list queue stack vector map set pair bitset multiset multimap unordered_set unordered_map unordered_multiset unordered_multimap priority_queue make_pair array shared_ptr abort terminate abs acos asin atan2 atan calloc ceil cosh cos exit exp fabs floor fmod fprintf fputs free frexp fscanf future isalnum isalpha iscntrl isdigit isgraph islower isprint ispunct isspace isupper isxdigit tolower toupper labs ldexp log10 log malloc realloc memchr memcmp memcpy memset modf pow printf putchar puts scanf sinh sin snprintf sprintf sqrt sscanf strcat strchr strcmp strcpy strcspn strlen strncat strncmp strncpy strpbrk strrchr strspn strstr tanh tan vfprintf vprintf vsprintf endl initializer_list unique_ptr _Bool complex _Complex imaginary _Imaginary",literal:"true false nullptr NULL"},d=[r,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,i,a],_={variants:[{begin:/=/,end:/;/},{begin:/\(/,end:/\)/},{beginKeywords:"new throw return else",end:/;/}],keywords:l,contains:d.concat([{begin:/\(/,end:/\)/,keywords:l,contains:d.concat(["self"]),relevance:0}]),relevance:0},u={className:"function",begin:"("+n+"[\\*&\\s]+)+"+c,returnBegin:!0,end:/[{;=]/,excludeEnd:!0,keywords:l,illegal:/[^\w\s\*&:<>]/,contains:[{begin:"decltype\\(auto\\)",keywords:l,relevance:0},{begin:c,returnBegin:!0,contains:[o],relevance:0},{className:"params",begin:/\(/,end:/\)/,keywords:l,relevance:0,contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,a,i,r,{begin:/\(/,end:/\)/,keywords:l,relevance:0,contains:["self",e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,a,i,r]}]},r,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,s]};return{aliases:["c","cc","h","c++","h++","hpp","hh","hxx","cxx"],keywords:l,disableAutodetect:!0,illegal:"",keywords:l,contains:["self",r]},{begin:e.IDENT_RE+"::",keywords:l},{className:"class",beginKeywords:"class struct",end:/[{;:]/,contains:[{begin://,contains:["self"]},e.TITLE_MODE]}]),exports:{preprocessor:s,strings:a,keywords:l}}}}());hljs.registerLanguage("coffeescript",function(){"use strict";const e=["as","in","of","if","for","while","finally","var","new","function","do","return","void","else","break","catch","instanceof","with","throw","case","default","try","switch","continue","typeof","delete","let","yield","const","class","debugger","async","await","static","import","from","export","extends"],n=["true","false","null","undefined","NaN","Infinity"],a=[].concat(["setInterval","setTimeout","clearInterval","clearTimeout","require","exports","eval","isFinite","isNaN","parseFloat","parseInt","decodeURI","decodeURIComponent","encodeURI","encodeURIComponent","escape","unescape"],["arguments","this","super","console","window","document","localStorage","module","global"],["Intl","DataView","Number","Math","Date","String","RegExp","Object","Function","Boolean","Error","Symbol","Set","Map","WeakSet","WeakMap","Proxy","Reflect","JSON","Promise","Float64Array","Int16Array","Int32Array","Int8Array","Uint16Array","Uint32Array","Float32Array","Array","Uint8Array","Uint8ClampedArray","ArrayBuffer"],["EvalError","InternalError","RangeError","ReferenceError","SyntaxError","TypeError","URIError"]);return function(r){var t={keyword:e.concat(["then","unless","until","loop","by","when","and","or","is","isnt","not"]).filter((e=>n=>!e.includes(n))(["var","const","let","function","static"])).join(" "),literal:n.concat(["yes","no","on","off"]).join(" "),built_in:a.concat(["npm","print"]).join(" ")},i="[A-Za-z$_][0-9A-Za-z$_]*",s={className:"subst",begin:/#\{/,end:/}/,keywords:t},o=[r.BINARY_NUMBER_MODE,r.inherit(r.C_NUMBER_MODE,{starts:{end:"(\\s*/)?",relevance:0}}),{className:"string",variants:[{begin:/'''/,end:/'''/,contains:[r.BACKSLASH_ESCAPE]},{begin:/'/,end:/'/,contains:[r.BACKSLASH_ESCAPE]},{begin:/"""/,end:/"""/,contains:[r.BACKSLASH_ESCAPE,s]},{begin:/"/,end:/"/,contains:[r.BACKSLASH_ESCAPE,s]}]},{className:"regexp",variants:[{begin:"///",end:"///",contains:[s,r.HASH_COMMENT_MODE]},{begin:"//[gim]{0,3}(?=\\W)",relevance:0},{begin:/\/(?![ *]).*?(?![\\]).\/[gim]{0,3}(?=\W)/}]},{begin:"@"+i},{subLanguage:"javascript",excludeBegin:!0,excludeEnd:!0,variants:[{begin:"```",end:"```"},{begin:"`",end:"`"}]}];s.contains=o;var c=r.inherit(r.TITLE_MODE,{begin:i}),l={className:"params",begin:"\\([^\\(]",returnBegin:!0,contains:[{begin:/\(/,end:/\)/,keywords:t,contains:["self"].concat(o)}]};return{name:"CoffeeScript",aliases:["coffee","cson","iced"],keywords:t,illegal:/\/\*/,contains:o.concat([r.COMMENT("###","###"),r.HASH_COMMENT_MODE,{className:"function",begin:"^\\s*"+i+"\\s*=\\s*(\\(.*\\))?\\s*\\B[-=]>",end:"[-=]>",returnBegin:!0,contains:[c,l]},{begin:/[:\(,=]\s*/,relevance:0,contains:[{className:"function",begin:"(\\(.*\\))?\\s*\\B[-=]>",end:"[-=]>",returnBegin:!0,contains:[l]}]},{className:"class",beginKeywords:"class",end:"$",illegal:/[:="\[\]]/,contains:[{beginKeywords:"extends",endsWithParent:!0,illegal:/[:="\[\]]/,contains:[c]},c]},{begin:i+":",end:":",returnBegin:!0,returnEnd:!0,relevance:0}])}}}());hljs.registerLanguage("ruby",function(){"use strict";return function(e){var n="[a-zA-Z_]\\w*[!?=]?|[-+~]\\@|<<|>>|=~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~`|]|\\[\\]=?",a={keyword:"and then defined module in return redo if BEGIN retry end for self when next until do begin unless END rescue else break undef not super class case require yield alias while ensure elsif or include attr_reader attr_writer attr_accessor",literal:"true false nil"},s={className:"doctag",begin:"@[A-Za-z]+"},i={begin:"#<",end:">"},r=[e.COMMENT("#","$",{contains:[s]}),e.COMMENT("^\\=begin","^\\=end",{contains:[s],relevance:10}),e.COMMENT("^__END__","\\n$")],c={className:"subst",begin:"#\\{",end:"}",keywords:a},t={className:"string",contains:[e.BACKSLASH_ESCAPE,c],variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/`/,end:/`/},{begin:"%[qQwWx]?\\(",end:"\\)"},{begin:"%[qQwWx]?\\[",end:"\\]"},{begin:"%[qQwWx]?{",end:"}"},{begin:"%[qQwWx]?<",end:">"},{begin:"%[qQwWx]?/",end:"/"},{begin:"%[qQwWx]?%",end:"%"},{begin:"%[qQwWx]?-",end:"-"},{begin:"%[qQwWx]?\\|",end:"\\|"},{begin:/\B\?(\\\d{1,3}|\\x[A-Fa-f0-9]{1,2}|\\u[A-Fa-f0-9]{4}|\\?\S)\b/},{begin:/<<[-~]?'?(\w+)(?:.|\n)*?\n\s*\1\b/,returnBegin:!0,contains:[{begin:/<<[-~]?'?/},e.END_SAME_AS_BEGIN({begin:/(\w+)/,end:/(\w+)/,contains:[e.BACKSLASH_ESCAPE,c]})]}]},b={className:"params",begin:"\\(",end:"\\)",endsParent:!0,keywords:a},d=[t,i,{className:"class",beginKeywords:"class module",end:"$|;",illegal:/=/,contains:[e.inherit(e.TITLE_MODE,{begin:"[A-Za-z_]\\w*(::\\w+)*(\\?|\\!)?"}),{begin:"<\\s*",contains:[{begin:"("+e.IDENT_RE+"::)?"+e.IDENT_RE}]}].concat(r)},{className:"function",beginKeywords:"def",end:"$|;",contains:[e.inherit(e.TITLE_MODE,{begin:n}),b].concat(r)},{begin:e.IDENT_RE+"::"},{className:"symbol",begin:e.UNDERSCORE_IDENT_RE+"(\\!|\\?)?:",relevance:0},{className:"symbol",begin:":(?!\\s)",contains:[t,{begin:n}],relevance:0},{className:"number",begin:"(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b",relevance:0},{begin:"(\\$\\W)|((\\$|\\@\\@?)(\\w+))"},{className:"params",begin:/\|/,end:/\|/,keywords:a},{begin:"("+e.RE_STARTERS_RE+"|unless)\\s*",keywords:"unless",contains:[i,{className:"regexp",contains:[e.BACKSLASH_ESCAPE,c],illegal:/\n/,variants:[{begin:"/",end:"/[a-z]*"},{begin:"%r{",end:"}[a-z]*"},{begin:"%r\\(",end:"\\)[a-z]*"},{begin:"%r!",end:"![a-z]*"},{begin:"%r\\[",end:"\\][a-z]*"}]}].concat(r),relevance:0}].concat(r);c.contains=d,b.contains=d;var g=[{begin:/^\s*=>/,starts:{end:"$",contains:d}},{className:"meta",begin:"^([>?]>|[\\w#]+\\(\\w+\\):\\d+:\\d+>|(\\w+-)?\\d+\\.\\d+\\.\\d(p\\d+)?[^>]+>)",starts:{end:"$",contains:d}}];return{name:"Ruby",aliases:["rb","gemspec","podspec","thor","irb"],keywords:a,illegal:/\/\*/,contains:r.concat(g).concat(d)}}}());hljs.registerLanguage("yaml",function(){"use strict";return function(e){var n="true false yes no null",a="[\\w#;/?:@&=+$,.~*\\'()[\\]]+",s={className:"string",relevance:0,variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/\S+/}],contains:[e.BACKSLASH_ESCAPE,{className:"template-variable",variants:[{begin:"{{",end:"}}"},{begin:"%{",end:"}"}]}]},i=e.inherit(s,{variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/[^\s,{}[\]]+/}]}),l={end:",",endsWithParent:!0,excludeEnd:!0,contains:[],keywords:n,relevance:0},t={begin:"{",end:"}",contains:[l],illegal:"\\n",relevance:0},g={begin:"\\[",end:"\\]",contains:[l],illegal:"\\n",relevance:0},b=[{className:"attr",variants:[{begin:"\\w[\\w :\\/.-]*:(?=[ \t]|$)"},{begin:'"\\w[\\w :\\/.-]*":(?=[ \t]|$)'},{begin:"'\\w[\\w :\\/.-]*':(?=[ \t]|$)"}]},{className:"meta",begin:"^---s*$",relevance:10},{className:"string",begin:"[\\|>]([0-9]?[+-])?[ ]*\\n( *)[\\S ]+\\n(\\2[\\S ]+\\n?)*"},{begin:"<%[%=-]?",end:"[%-]?%>",subLanguage:"ruby",excludeBegin:!0,excludeEnd:!0,relevance:0},{className:"type",begin:"!\\w+!"+a},{className:"type",begin:"!<"+a+">"},{className:"type",begin:"!"+a},{className:"type",begin:"!!"+a},{className:"meta",begin:"&"+e.UNDERSCORE_IDENT_RE+"$"},{className:"meta",begin:"\\*"+e.UNDERSCORE_IDENT_RE+"$"},{className:"bullet",begin:"\\-(?=[ ]|$)",relevance:0},e.HASH_COMMENT_MODE,{beginKeywords:n,keywords:{literal:n}},{className:"number",begin:"\\b[0-9]{4}(-[0-9][0-9]){0,2}([Tt \\t][0-9][0-9]?(:[0-9][0-9]){2})?(\\.[0-9]*)?([ \\t])*(Z|[-+][0-9][0-9]?(:[0-9][0-9])?)?\\b"},{className:"number",begin:e.C_NUMBER_RE+"\\b"},t,g,s],c=[...b];return c.pop(),c.push(i),l.contains=c,{name:"YAML",case_insensitive:!0,aliases:["yml","YAML"],contains:b}}}());hljs.registerLanguage("d",function(){"use strict";return function(e){var a={$pattern:e.UNDERSCORE_IDENT_RE,keyword:"abstract alias align asm assert auto body break byte case cast catch class const continue debug default delete deprecated do else enum export extern final finally for foreach foreach_reverse|10 goto if immutable import in inout int interface invariant is lazy macro mixin module new nothrow out override package pragma private protected public pure ref return scope shared static struct super switch synchronized template this throw try typedef typeid typeof union unittest version void volatile while with __FILE__ __LINE__ __gshared|10 __thread __traits __DATE__ __EOF__ __TIME__ __TIMESTAMP__ __VENDOR__ __VERSION__",built_in:"bool cdouble cent cfloat char creal dchar delegate double dstring float function idouble ifloat ireal long real short string ubyte ucent uint ulong ushort wchar wstring",literal:"false null true"},d="((0|[1-9][\\d_]*)|0[bB][01_]+|0[xX]([\\da-fA-F][\\da-fA-F_]*|_[\\da-fA-F][\\da-fA-F_]*))",n="\\\\(['\"\\?\\\\abfnrtv]|u[\\dA-Fa-f]{4}|[0-7]{1,3}|x[\\dA-Fa-f]{2}|U[\\dA-Fa-f]{8})|&[a-zA-Z\\d]{2,};",t={className:"number",begin:"\\b"+d+"(L|u|U|Lu|LU|uL|UL)?",relevance:0},_={className:"number",begin:"\\b(((0[xX](([\\da-fA-F][\\da-fA-F_]*|_[\\da-fA-F][\\da-fA-F_]*)\\.([\\da-fA-F][\\da-fA-F_]*|_[\\da-fA-F][\\da-fA-F_]*)|\\.?([\\da-fA-F][\\da-fA-F_]*|_[\\da-fA-F][\\da-fA-F_]*))[pP][+-]?(0|[1-9][\\d_]*|\\d[\\d_]*|[\\d_]+?\\d))|((0|[1-9][\\d_]*|\\d[\\d_]*|[\\d_]+?\\d)(\\.\\d*|([eE][+-]?(0|[1-9][\\d_]*|\\d[\\d_]*|[\\d_]+?\\d)))|\\d+\\.(0|[1-9][\\d_]*|\\d[\\d_]*|[\\d_]+?\\d)(0|[1-9][\\d_]*|\\d[\\d_]*|[\\d_]+?\\d)|\\.(0|[1-9][\\d_]*)([eE][+-]?(0|[1-9][\\d_]*|\\d[\\d_]*|[\\d_]+?\\d))?))([fF]|L|i|[fF]i|Li)?|"+d+"(i|[fF]i|Li))",relevance:0},r={className:"string",begin:"'("+n+"|.)",end:"'",illegal:"."},i={className:"string",begin:'"',contains:[{begin:n,relevance:0}],end:'"[cwd]?'},s=e.COMMENT("\\/\\+","\\+\\/",{contains:["self"],relevance:10});return{name:"D",keywords:a,contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,s,{className:"string",begin:'x"[\\da-fA-F\\s\\n\\r]*"[cwd]?',relevance:10},i,{className:"string",begin:'[rq]"',end:'"[cwd]?',relevance:5},{className:"string",begin:"`",end:"`[cwd]?"},{className:"string",begin:'q"\\{',end:'\\}"'},_,t,r,{className:"meta",begin:"^#!",end:"$",relevance:5},{className:"meta",begin:"#(line)",end:"$",relevance:5},{className:"keyword",begin:"@[a-zA-Z_][a-zA-Z_\\d]*"}]}}}());hljs.registerLanguage("properties",function(){"use strict";return function(e){var n="[ \\t\\f]*",t="("+n+"[:=]"+n+"|[ \\t\\f]+)",a="([^\\\\:= \\t\\f\\n]|\\\\.)+",s={end:t,relevance:0,starts:{className:"string",end:/$/,relevance:0,contains:[{begin:"\\\\\\n"}]}};return{name:".properties",case_insensitive:!0,illegal:/\S/,contains:[e.COMMENT("^\\s*[!#]","$"),{begin:"([^\\\\\\W:= \\t\\f\\n]|\\\\.)+"+t,returnBegin:!0,contains:[{className:"attr",begin:"([^\\\\\\W:= \\t\\f\\n]|\\\\.)+",endsParent:!0,relevance:0}],starts:s},{begin:a+t,returnBegin:!0,relevance:0,contains:[{className:"meta",begin:a,endsParent:!0,relevance:0}],starts:s},{className:"attr",relevance:0,begin:a+n+"$"}]}}}());hljs.registerLanguage("http",function(){"use strict";return function(e){var n="HTTP/[0-9\\.]+";return{name:"HTTP",aliases:["https"],illegal:"\\S",contains:[{begin:"^"+n,end:"$",contains:[{className:"number",begin:"\\b\\d{3}\\b"}]},{begin:"^[A-Z]+ (.*?) "+n+"$",returnBegin:!0,end:"$",contains:[{className:"string",begin:" ",end:" ",excludeBegin:!0,excludeEnd:!0},{begin:n},{className:"keyword",begin:"[A-Z]+"}]},{className:"attribute",begin:"^\\w",end:": ",excludeEnd:!0,illegal:"\\n|\\s|=",starts:{end:"$",relevance:0}},{begin:"\\n\\n",starts:{subLanguage:[],endsWithParent:!0}}]}}}());hljs.registerLanguage("haskell",function(){"use strict";return function(e){var n={variants:[e.COMMENT("--","$"),e.COMMENT("{-","-}",{contains:["self"]})]},i={className:"meta",begin:"{-#",end:"#-}"},a={className:"meta",begin:"^#",end:"$"},s={className:"type",begin:"\\b[A-Z][\\w']*",relevance:0},l={begin:"\\(",end:"\\)",illegal:'"',contains:[i,a,{className:"type",begin:"\\b[A-Z][\\w]*(\\((\\.\\.|,|\\w+)\\))?"},e.inherit(e.TITLE_MODE,{begin:"[_a-z][\\w']*"}),n]};return{name:"Haskell",aliases:["hs"],keywords:"let in if then else case of where do module import hiding qualified type data newtype deriving class instance as default infix infixl infixr foreign export ccall stdcall cplusplus jvm dotnet safe unsafe family forall mdo proc rec",contains:[{beginKeywords:"module",end:"where",keywords:"module where",contains:[l,n],illegal:"\\W\\.|;"},{begin:"\\bimport\\b",end:"$",keywords:"import qualified as hiding",contains:[l,n],illegal:"\\W\\.|;"},{className:"class",begin:"^(\\s*)?(class|instance)\\b",end:"where",keywords:"class family instance where",contains:[s,l,n]},{className:"class",begin:"\\b(data|(new)?type)\\b",end:"$",keywords:"data family type newtype deriving",contains:[i,s,l,{begin:"{",end:"}",contains:l.contains},n]},{beginKeywords:"default",end:"$",contains:[s,l,n]},{beginKeywords:"infix infixl infixr",end:"$",contains:[e.C_NUMBER_MODE,n]},{begin:"\\bforeign\\b",end:"$",keywords:"foreign import export ccall stdcall cplusplus jvm dotnet safe unsafe",contains:[s,e.QUOTE_STRING_MODE,n]},{className:"meta",begin:"#!\\/usr\\/bin\\/env runhaskell",end:"$"},i,a,e.QUOTE_STRING_MODE,e.C_NUMBER_MODE,s,e.inherit(e.TITLE_MODE,{begin:"^[_a-z][\\w']*"}),n,{begin:"->|<-"}]}}}());hljs.registerLanguage("handlebars",function(){"use strict";function e(...e){return e.map(e=>(function(e){return e?"string"==typeof e?e:e.source:null})(e)).join("")}return function(n){const a={"builtin-name":"action bindattr collection component concat debugger each each-in get hash if in input link-to loc log lookup mut outlet partial query-params render template textarea unbound unless view with yield"},t=/\[.*?\]/,s=/[^\s!"#%&'()*+,.\/;<=>@\[\\\]^`{|}~]+/,i=e("(",/'.*?'/,"|",/".*?"/,"|",t,"|",s,"|",/\.|\//,")+"),r=e("(",t,"|",s,")(?==)"),l={begin:i,lexemes:/[\w.\/]+/},c=n.inherit(l,{keywords:{literal:"true false undefined null"}}),o={begin:/\(/,end:/\)/},m={className:"attr",begin:r,relevance:0,starts:{begin:/=/,end:/=/,starts:{contains:[n.NUMBER_MODE,n.QUOTE_STRING_MODE,n.APOS_STRING_MODE,c,o]}}},d={contains:[n.NUMBER_MODE,n.QUOTE_STRING_MODE,n.APOS_STRING_MODE,{begin:/as\s+\|/,keywords:{keyword:"as"},end:/\|/,contains:[{begin:/\w+/}]},m,c,o],returnEnd:!0},g=n.inherit(l,{className:"name",keywords:a,starts:n.inherit(d,{end:/\)/})});o.contains=[g];const u=n.inherit(l,{keywords:a,className:"name",starts:n.inherit(d,{end:/}}/})}),b=n.inherit(l,{keywords:a,className:"name"}),h=n.inherit(l,{className:"name",keywords:a,starts:n.inherit(d,{end:/}}/})});return{name:"Handlebars",aliases:["hbs","html.hbs","html.handlebars","htmlbars"],case_insensitive:!0,subLanguage:"xml",contains:[{begin:/\\\{\{/,skip:!0},{begin:/\\\\(?=\{\{)/,skip:!0},n.COMMENT(/\{\{!--/,/--\}\}/),n.COMMENT(/\{\{!/,/\}\}/),{className:"template-tag",begin:/\{\{\{\{(?!\/)/,end:/\}\}\}\}/,contains:[u],starts:{end:/\{\{\{\{\//,returnEnd:!0,subLanguage:"xml"}},{className:"template-tag",begin:/\{\{\{\{\//,end:/\}\}\}\}/,contains:[b]},{className:"template-tag",begin:/\{\{#/,end:/\}\}/,contains:[u]},{className:"template-tag",begin:/\{\{(?=else\}\})/,end:/\}\}/,keywords:"else"},{className:"template-tag",begin:/\{\{\//,end:/\}\}/,contains:[b]},{className:"template-variable",begin:/\{\{\{/,end:/\}\}\}/,contains:[h]},{className:"template-variable",begin:/\{\{/,end:/\}\}/,contains:[h]}]}}}());hljs.registerLanguage("rust",function(){"use strict";return function(e){var n="([ui](8|16|32|64|128|size)|f(32|64))?",t="drop i8 i16 i32 i64 i128 isize u8 u16 u32 u64 u128 usize f32 f64 str char bool Box Option Result String Vec Copy Send Sized Sync Drop Fn FnMut FnOnce ToOwned Clone Debug PartialEq PartialOrd Eq Ord AsRef AsMut Into From Default Iterator Extend IntoIterator DoubleEndedIterator ExactSizeIterator SliceConcatExt ToString assert! assert_eq! bitflags! bytes! cfg! col! concat! concat_idents! debug_assert! debug_assert_eq! env! panic! file! format! format_args! include_bin! include_str! line! local_data_key! module_path! option_env! print! println! select! stringify! try! unimplemented! unreachable! vec! write! writeln! macro_rules! assert_ne! debug_assert_ne!";return{name:"Rust",aliases:["rs"],keywords:{$pattern:e.IDENT_RE+"!?",keyword:"abstract as async await become box break const continue crate do dyn else enum extern false final fn for if impl in let loop macro match mod move mut override priv pub ref return self Self static struct super trait true try type typeof unsafe unsized use virtual where while yield",literal:"true false Some None Ok Err",built_in:t},illegal:""}]}}}());hljs.registerLanguage("cpp",function(){"use strict";return function(e){var t=e.getLanguage("c-like").rawDefinition();return t.disableAutodetect=!1,t.name="C++",t.aliases=["cc","c++","h++","hpp","hh","hxx","cxx"],t}}());hljs.registerLanguage("ini",function(){"use strict";function e(e){return e?"string"==typeof e?e:e.source:null}function n(...n){return n.map(n=>e(n)).join("")}return function(a){var s={className:"number",relevance:0,variants:[{begin:/([\+\-]+)?[\d]+_[\d_]+/},{begin:a.NUMBER_RE}]},i=a.COMMENT();i.variants=[{begin:/;/,end:/$/},{begin:/#/,end:/$/}];var t={className:"variable",variants:[{begin:/\$[\w\d"][\w\d_]*/},{begin:/\$\{(.*?)}/}]},r={className:"literal",begin:/\bon|off|true|false|yes|no\b/},l={className:"string",contains:[a.BACKSLASH_ESCAPE],variants:[{begin:"'''",end:"'''",relevance:10},{begin:'"""',end:'"""',relevance:10},{begin:'"',end:'"'},{begin:"'",end:"'"}]},c={begin:/\[/,end:/\]/,contains:[i,r,t,l,s,"self"],relevance:0},g="("+[/[A-Za-z0-9_-]+/,/"(\\"|[^"])*"/,/'[^']*'/].map(n=>e(n)).join("|")+")";return{name:"TOML, also INI",aliases:["toml"],case_insensitive:!0,illegal:/\S/,contains:[i,{className:"section",begin:/\[+/,end:/\]+/},{begin:n(g,"(\\s*\\.\\s*",g,")*",n("(?=",/\s*=\s*[^#\s]/,")")),className:"attr",starts:{end:/$/,contains:[i,c,r,t,l,s]}}]}}}());hljs.registerLanguage("objectivec",function(){"use strict";return function(e){var n=/[a-zA-Z@][a-zA-Z0-9_]*/,_={$pattern:n,keyword:"@interface @class @protocol @implementation"};return{name:"Objective-C",aliases:["mm","objc","obj-c"],keywords:{$pattern:n,keyword:"int float while char export sizeof typedef const struct for union unsigned long volatile static bool mutable if do return goto void enum else break extern asm case short default double register explicit signed typename this switch continue wchar_t inline readonly assign readwrite self @synchronized id typeof nonatomic super unichar IBOutlet IBAction strong weak copy in out inout bycopy byref oneway __strong __weak __block __autoreleasing @private @protected @public @try @property @end @throw @catch @finally @autoreleasepool @synthesize @dynamic @selector @optional @required @encode @package @import @defs @compatibility_alias __bridge __bridge_transfer __bridge_retained __bridge_retain __covariant __contravariant __kindof _Nonnull _Nullable _Null_unspecified __FUNCTION__ __PRETTY_FUNCTION__ __attribute__ getter setter retain unsafe_unretained nonnull nullable null_unspecified null_resettable class instancetype NS_DESIGNATED_INITIALIZER NS_UNAVAILABLE NS_REQUIRES_SUPER NS_RETURNS_INNER_POINTER NS_INLINE NS_AVAILABLE NS_DEPRECATED NS_ENUM NS_OPTIONS NS_SWIFT_UNAVAILABLE NS_ASSUME_NONNULL_BEGIN NS_ASSUME_NONNULL_END NS_REFINED_FOR_SWIFT NS_SWIFT_NAME NS_SWIFT_NOTHROW NS_DURING NS_HANDLER NS_ENDHANDLER NS_VALUERETURN NS_VOIDRETURN",literal:"false true FALSE TRUE nil YES NO NULL",built_in:"BOOL dispatch_once_t dispatch_queue_t dispatch_sync dispatch_async dispatch_once"},illegal:"/,end:/$/,illegal:"\\n"},e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{className:"class",begin:"("+_.keyword.split(" ").join("|")+")\\b",end:"({|$)",excludeEnd:!0,keywords:_,contains:[e.UNDERSCORE_TITLE_MODE]},{begin:"\\."+e.UNDERSCORE_IDENT_RE,relevance:0}]}}}());hljs.registerLanguage("apache",function(){"use strict";return function(e){var n={className:"number",begin:"\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}(:\\d{1,5})?"};return{name:"Apache config",aliases:["apacheconf"],case_insensitive:!0,contains:[e.HASH_COMMENT_MODE,{className:"section",begin:"",contains:[n,{className:"number",begin:":\\d{1,5}"},e.inherit(e.QUOTE_STRING_MODE,{relevance:0})]},{className:"attribute",begin:/\w+/,relevance:0,keywords:{nomarkup:"order deny allow setenv rewriterule rewriteengine rewritecond documentroot sethandler errordocument loadmodule options header listen serverroot servername"},starts:{end:/$/,relevance:0,keywords:{literal:"on off all deny allow"},contains:[{className:"meta",begin:"\\s\\[",end:"\\]$"},{className:"variable",begin:"[\\$%]\\{",end:"\\}",contains:["self",{className:"number",begin:"[\\$%]\\d+"}]},n,{className:"number",begin:"\\d+"},e.QUOTE_STRING_MODE]}}],illegal:/\S/}}}());hljs.registerLanguage("java",function(){"use strict";function e(e){return e?"string"==typeof e?e:e.source:null}function n(e){return a("(",e,")?")}function a(...n){return n.map(n=>e(n)).join("")}function s(...n){return"("+n.map(n=>e(n)).join("|")+")"}return function(e){var t="false synchronized int abstract float private char boolean var static null if const for true while long strictfp finally protected import native final void enum else break transient catch instanceof byte super volatile case assert short package default double public try this switch continue throws protected public private module requires exports do",i={className:"meta",begin:"@[À-ʸa-zA-Z_$][À-ʸa-zA-Z_$0-9]*",contains:[{begin:/\(/,end:/\)/,contains:["self"]}]},r=e=>a("[",e,"]+([",e,"_]*[",e,"]+)?"),c={className:"number",variants:[{begin:`\\b(0[bB]${r("01")})[lL]?`},{begin:`\\b(0${r("0-7")})[dDfFlL]?`},{begin:a(/\b0[xX]/,s(a(r("a-fA-F0-9"),/\./,r("a-fA-F0-9")),a(r("a-fA-F0-9"),/\.?/),a(/\./,r("a-fA-F0-9"))),/([pP][+-]?(\d+))?/,/[fFdDlL]?/)},{begin:a(/\b/,s(a(/\d*\./,r("\\d")),r("\\d")),/[eE][+-]?[\d]+[dDfF]?/)},{begin:a(/\b/,r(/\d/),n(/\.?/),n(r(/\d/)),/[dDfFlL]?/)}],relevance:0};return{name:"Java",aliases:["jsp"],keywords:t,illegal:/<\/|#/,contains:[e.COMMENT("/\\*\\*","\\*/",{relevance:0,contains:[{begin:/\w+@/,relevance:0},{className:"doctag",begin:"@[A-Za-z]+"}]}),e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,{className:"class",beginKeywords:"class interface",end:/[{;=]/,excludeEnd:!0,keywords:"class interface",illegal:/[:"\[\]]/,contains:[{beginKeywords:"extends implements"},e.UNDERSCORE_TITLE_MODE]},{beginKeywords:"new throw return else",relevance:0},{className:"function",begin:"([À-ʸa-zA-Z_$][À-ʸa-zA-Z_$0-9]*(<[À-ʸa-zA-Z_$][À-ʸa-zA-Z_$0-9]*(\\s*,\\s*[À-ʸa-zA-Z_$][À-ʸa-zA-Z_$0-9]*)*>)?\\s+)+"+e.UNDERSCORE_IDENT_RE+"\\s*\\(",returnBegin:!0,end:/[{;=]/,excludeEnd:!0,keywords:t,contains:[{begin:e.UNDERSCORE_IDENT_RE+"\\s*\\(",returnBegin:!0,relevance:0,contains:[e.UNDERSCORE_TITLE_MODE]},{className:"params",begin:/\(/,end:/\)/,keywords:t,relevance:0,contains:[i,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,e.C_NUMBER_MODE,e.C_BLOCK_COMMENT_MODE]},e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},c,i]}}}());hljs.registerLanguage("x86asm",function(){"use strict";return function(s){return{name:"Intel x86 Assembly",case_insensitive:!0,keywords:{$pattern:"[.%]?"+s.IDENT_RE,keyword:"lock rep repe repz repne repnz xaquire xrelease bnd nobnd aaa aad aam aas adc add and arpl bb0_reset bb1_reset bound bsf bsr bswap bt btc btr bts call cbw cdq cdqe clc cld cli clts cmc cmp cmpsb cmpsd cmpsq cmpsw cmpxchg cmpxchg486 cmpxchg8b cmpxchg16b cpuid cpu_read cpu_write cqo cwd cwde daa das dec div dmint emms enter equ f2xm1 fabs fadd faddp fbld fbstp fchs fclex fcmovb fcmovbe fcmove fcmovnb fcmovnbe fcmovne fcmovnu fcmovu fcom fcomi fcomip fcomp fcompp fcos fdecstp fdisi fdiv fdivp fdivr fdivrp femms feni ffree ffreep fiadd ficom ficomp fidiv fidivr fild fimul fincstp finit fist fistp fisttp fisub fisubr fld fld1 fldcw fldenv fldl2e fldl2t fldlg2 fldln2 fldpi fldz fmul fmulp fnclex fndisi fneni fninit fnop fnsave fnstcw fnstenv fnstsw fpatan fprem fprem1 fptan frndint frstor fsave fscale fsetpm fsin fsincos fsqrt fst fstcw fstenv fstp fstsw fsub fsubp fsubr fsubrp ftst fucom fucomi fucomip fucomp fucompp fxam fxch fxtract fyl2x fyl2xp1 hlt ibts icebp idiv imul in inc incbin insb insd insw int int01 int1 int03 int3 into invd invpcid invlpg invlpga iret iretd iretq iretw jcxz jecxz jrcxz jmp jmpe lahf lar lds lea leave les lfence lfs lgdt lgs lidt lldt lmsw loadall loadall286 lodsb lodsd lodsq lodsw loop loope loopne loopnz loopz lsl lss ltr mfence monitor mov movd movq movsb movsd movsq movsw movsx movsxd movzx mul mwait neg nop not or out outsb outsd outsw packssdw packsswb packuswb paddb paddd paddsb paddsiw paddsw paddusb paddusw paddw pand pandn pause paveb pavgusb pcmpeqb pcmpeqd pcmpeqw pcmpgtb pcmpgtd pcmpgtw pdistib pf2id pfacc pfadd pfcmpeq pfcmpge pfcmpgt pfmax pfmin pfmul pfrcp pfrcpit1 pfrcpit2 pfrsqit1 pfrsqrt pfsub pfsubr pi2fd pmachriw pmaddwd pmagw pmulhriw pmulhrwa pmulhrwc pmulhw pmullw pmvgezb pmvlzb pmvnzb pmvzb pop popa popad popaw popf popfd popfq popfw por prefetch prefetchw pslld psllq psllw psrad psraw psrld psrlq psrlw psubb psubd psubsb psubsiw psubsw psubusb psubusw psubw punpckhbw punpckhdq punpckhwd punpcklbw punpckldq punpcklwd push pusha pushad pushaw pushf pushfd pushfq pushfw pxor rcl rcr rdshr rdmsr rdpmc rdtsc rdtscp ret retf retn rol ror rdm rsdc rsldt rsm rsts sahf sal salc sar sbb scasb scasd scasq scasw sfence sgdt shl shld shr shrd sidt sldt skinit smi smint smintold smsw stc std sti stosb stosd stosq stosw str sub svdc svldt svts swapgs syscall sysenter sysexit sysret test ud0 ud1 ud2b ud2 ud2a umov verr verw fwait wbinvd wrshr wrmsr xadd xbts xchg xlatb xlat xor cmove cmovz cmovne cmovnz cmova cmovnbe cmovae cmovnb cmovb cmovnae cmovbe cmovna cmovg cmovnle cmovge cmovnl cmovl cmovnge cmovle cmovng cmovc cmovnc cmovo cmovno cmovs cmovns cmovp cmovpe cmovnp cmovpo je jz jne jnz ja jnbe jae jnb jb jnae jbe jna jg jnle jge jnl jl jnge jle jng jc jnc jo jno js jns jpo jnp jpe jp sete setz setne setnz seta setnbe setae setnb setnc setb setnae setcset setbe setna setg setnle setge setnl setl setnge setle setng sets setns seto setno setpe setp setpo setnp addps addss andnps andps cmpeqps cmpeqss cmpleps cmpless cmpltps cmpltss cmpneqps cmpneqss cmpnleps cmpnless cmpnltps cmpnltss cmpordps cmpordss cmpunordps cmpunordss cmpps cmpss comiss cvtpi2ps cvtps2pi cvtsi2ss cvtss2si cvttps2pi cvttss2si divps divss ldmxcsr maxps maxss minps minss movaps movhps movlhps movlps movhlps movmskps movntps movss movups mulps mulss orps rcpps rcpss rsqrtps rsqrtss shufps sqrtps sqrtss stmxcsr subps subss ucomiss unpckhps unpcklps xorps fxrstor fxrstor64 fxsave fxsave64 xgetbv xsetbv xsave xsave64 xsaveopt xsaveopt64 xrstor xrstor64 prefetchnta prefetcht0 prefetcht1 prefetcht2 maskmovq movntq pavgb pavgw pextrw pinsrw pmaxsw pmaxub pminsw pminub pmovmskb pmulhuw psadbw pshufw pf2iw pfnacc pfpnacc pi2fw pswapd maskmovdqu clflush movntdq movnti movntpd movdqa movdqu movdq2q movq2dq paddq pmuludq pshufd pshufhw pshuflw pslldq psrldq psubq punpckhqdq punpcklqdq addpd addsd andnpd andpd cmpeqpd cmpeqsd cmplepd cmplesd cmpltpd cmpltsd cmpneqpd cmpneqsd cmpnlepd cmpnlesd cmpnltpd cmpnltsd cmpordpd cmpordsd cmpunordpd cmpunordsd cmppd comisd cvtdq2pd cvtdq2ps cvtpd2dq cvtpd2pi cvtpd2ps cvtpi2pd cvtps2dq cvtps2pd cvtsd2si cvtsd2ss cvtsi2sd cvtss2sd cvttpd2pi cvttpd2dq cvttps2dq cvttsd2si divpd divsd maxpd maxsd minpd minsd movapd movhpd movlpd movmskpd movupd mulpd mulsd orpd shufpd sqrtpd sqrtsd subpd subsd ucomisd unpckhpd unpcklpd xorpd addsubpd addsubps haddpd haddps hsubpd hsubps lddqu movddup movshdup movsldup clgi stgi vmcall vmclear vmfunc vmlaunch vmload vmmcall vmptrld vmptrst vmread vmresume vmrun vmsave vmwrite vmxoff vmxon invept invvpid pabsb pabsw pabsd palignr phaddw phaddd phaddsw phsubw phsubd phsubsw pmaddubsw pmulhrsw pshufb psignb psignw psignd extrq insertq movntsd movntss lzcnt blendpd blendps blendvpd blendvps dppd dpps extractps insertps movntdqa mpsadbw packusdw pblendvb pblendw pcmpeqq pextrb pextrd pextrq phminposuw pinsrb pinsrd pinsrq pmaxsb pmaxsd pmaxud pmaxuw pminsb pminsd pminud pminuw pmovsxbw pmovsxbd pmovsxbq pmovsxwd pmovsxwq pmovsxdq pmovzxbw pmovzxbd pmovzxbq pmovzxwd pmovzxwq pmovzxdq pmuldq pmulld ptest roundpd roundps roundsd roundss crc32 pcmpestri pcmpestrm pcmpistri pcmpistrm pcmpgtq popcnt getsec pfrcpv pfrsqrtv movbe aesenc aesenclast aesdec aesdeclast aesimc aeskeygenassist vaesenc vaesenclast vaesdec vaesdeclast vaesimc vaeskeygenassist vaddpd vaddps vaddsd vaddss vaddsubpd vaddsubps vandpd vandps vandnpd vandnps vblendpd vblendps vblendvpd vblendvps vbroadcastss vbroadcastsd vbroadcastf128 vcmpeq_ospd vcmpeqpd vcmplt_ospd vcmpltpd vcmple_ospd vcmplepd vcmpunord_qpd vcmpunordpd vcmpneq_uqpd vcmpneqpd vcmpnlt_uspd vcmpnltpd vcmpnle_uspd vcmpnlepd vcmpord_qpd vcmpordpd vcmpeq_uqpd vcmpnge_uspd vcmpngepd vcmpngt_uspd vcmpngtpd vcmpfalse_oqpd vcmpfalsepd vcmpneq_oqpd vcmpge_ospd vcmpgepd vcmpgt_ospd vcmpgtpd vcmptrue_uqpd vcmptruepd vcmplt_oqpd vcmple_oqpd vcmpunord_spd vcmpneq_uspd vcmpnlt_uqpd vcmpnle_uqpd vcmpord_spd vcmpeq_uspd vcmpnge_uqpd vcmpngt_uqpd vcmpfalse_ospd vcmpneq_ospd vcmpge_oqpd vcmpgt_oqpd vcmptrue_uspd vcmppd vcmpeq_osps vcmpeqps vcmplt_osps vcmpltps vcmple_osps vcmpleps vcmpunord_qps vcmpunordps vcmpneq_uqps vcmpneqps vcmpnlt_usps vcmpnltps vcmpnle_usps vcmpnleps vcmpord_qps vcmpordps vcmpeq_uqps vcmpnge_usps vcmpngeps vcmpngt_usps vcmpngtps vcmpfalse_oqps vcmpfalseps vcmpneq_oqps vcmpge_osps vcmpgeps vcmpgt_osps vcmpgtps vcmptrue_uqps vcmptrueps vcmplt_oqps vcmple_oqps vcmpunord_sps vcmpneq_usps vcmpnlt_uqps vcmpnle_uqps vcmpord_sps vcmpeq_usps vcmpnge_uqps vcmpngt_uqps vcmpfalse_osps vcmpneq_osps vcmpge_oqps vcmpgt_oqps vcmptrue_usps vcmpps vcmpeq_ossd vcmpeqsd vcmplt_ossd vcmpltsd vcmple_ossd vcmplesd vcmpunord_qsd vcmpunordsd vcmpneq_uqsd vcmpneqsd vcmpnlt_ussd vcmpnltsd vcmpnle_ussd vcmpnlesd vcmpord_qsd vcmpordsd vcmpeq_uqsd vcmpnge_ussd vcmpngesd vcmpngt_ussd vcmpngtsd vcmpfalse_oqsd vcmpfalsesd vcmpneq_oqsd vcmpge_ossd vcmpgesd vcmpgt_ossd vcmpgtsd vcmptrue_uqsd vcmptruesd vcmplt_oqsd vcmple_oqsd vcmpunord_ssd vcmpneq_ussd vcmpnlt_uqsd vcmpnle_uqsd vcmpord_ssd vcmpeq_ussd vcmpnge_uqsd vcmpngt_uqsd vcmpfalse_ossd vcmpneq_ossd vcmpge_oqsd vcmpgt_oqsd vcmptrue_ussd vcmpsd vcmpeq_osss vcmpeqss vcmplt_osss vcmpltss vcmple_osss vcmpless vcmpunord_qss vcmpunordss vcmpneq_uqss vcmpneqss vcmpnlt_usss vcmpnltss vcmpnle_usss vcmpnless vcmpord_qss vcmpordss vcmpeq_uqss vcmpnge_usss vcmpngess vcmpngt_usss vcmpngtss vcmpfalse_oqss vcmpfalsess vcmpneq_oqss vcmpge_osss vcmpgess vcmpgt_osss vcmpgtss vcmptrue_uqss vcmptruess vcmplt_oqss vcmple_oqss vcmpunord_sss vcmpneq_usss vcmpnlt_uqss vcmpnle_uqss vcmpord_sss vcmpeq_usss vcmpnge_uqss vcmpngt_uqss vcmpfalse_osss vcmpneq_osss vcmpge_oqss vcmpgt_oqss vcmptrue_usss vcmpss vcomisd vcomiss vcvtdq2pd vcvtdq2ps vcvtpd2dq vcvtpd2ps vcvtps2dq vcvtps2pd vcvtsd2si vcvtsd2ss vcvtsi2sd vcvtsi2ss vcvtss2sd vcvtss2si vcvttpd2dq vcvttps2dq vcvttsd2si vcvttss2si vdivpd vdivps vdivsd vdivss vdppd vdpps vextractf128 vextractps vhaddpd vhaddps vhsubpd vhsubps vinsertf128 vinsertps vlddqu vldqqu vldmxcsr vmaskmovdqu vmaskmovps vmaskmovpd vmaxpd vmaxps vmaxsd vmaxss vminpd vminps vminsd vminss vmovapd vmovaps vmovd vmovq vmovddup vmovdqa vmovqqa vmovdqu vmovqqu vmovhlps vmovhpd vmovhps vmovlhps vmovlpd vmovlps vmovmskpd vmovmskps vmovntdq vmovntqq vmovntdqa vmovntpd vmovntps vmovsd vmovshdup vmovsldup vmovss vmovupd vmovups vmpsadbw vmulpd vmulps vmulsd vmulss vorpd vorps vpabsb vpabsw vpabsd vpacksswb vpackssdw vpackuswb vpackusdw vpaddb vpaddw vpaddd vpaddq vpaddsb vpaddsw vpaddusb vpaddusw vpalignr vpand vpandn vpavgb vpavgw vpblendvb vpblendw vpcmpestri vpcmpestrm vpcmpistri vpcmpistrm vpcmpeqb vpcmpeqw vpcmpeqd vpcmpeqq vpcmpgtb vpcmpgtw vpcmpgtd vpcmpgtq vpermilpd vpermilps vperm2f128 vpextrb vpextrw vpextrd vpextrq vphaddw vphaddd vphaddsw vphminposuw vphsubw vphsubd vphsubsw vpinsrb vpinsrw vpinsrd vpinsrq vpmaddwd vpmaddubsw vpmaxsb vpmaxsw vpmaxsd vpmaxub vpmaxuw vpmaxud vpminsb vpminsw vpminsd vpminub vpminuw vpminud vpmovmskb vpmovsxbw vpmovsxbd vpmovsxbq vpmovsxwd vpmovsxwq vpmovsxdq vpmovzxbw vpmovzxbd vpmovzxbq vpmovzxwd vpmovzxwq vpmovzxdq vpmulhuw vpmulhrsw vpmulhw vpmullw vpmulld vpmuludq vpmuldq vpor vpsadbw vpshufb vpshufd vpshufhw vpshuflw vpsignb vpsignw vpsignd vpslldq vpsrldq vpsllw vpslld vpsllq vpsraw vpsrad vpsrlw vpsrld vpsrlq vptest vpsubb vpsubw vpsubd vpsubq vpsubsb vpsubsw vpsubusb vpsubusw vpunpckhbw vpunpckhwd vpunpckhdq vpunpckhqdq vpunpcklbw vpunpcklwd vpunpckldq vpunpcklqdq vpxor vrcpps vrcpss vrsqrtps vrsqrtss vroundpd vroundps vroundsd vroundss vshufpd vshufps vsqrtpd vsqrtps vsqrtsd vsqrtss vstmxcsr vsubpd vsubps vsubsd vsubss vtestps vtestpd vucomisd vucomiss vunpckhpd vunpckhps vunpcklpd vunpcklps vxorpd vxorps vzeroall vzeroupper pclmullqlqdq pclmulhqlqdq pclmullqhqdq pclmulhqhqdq pclmulqdq vpclmullqlqdq vpclmulhqlqdq vpclmullqhqdq vpclmulhqhqdq vpclmulqdq vfmadd132ps vfmadd132pd vfmadd312ps vfmadd312pd vfmadd213ps vfmadd213pd vfmadd123ps vfmadd123pd vfmadd231ps vfmadd231pd vfmadd321ps vfmadd321pd vfmaddsub132ps vfmaddsub132pd vfmaddsub312ps vfmaddsub312pd vfmaddsub213ps vfmaddsub213pd vfmaddsub123ps vfmaddsub123pd vfmaddsub231ps vfmaddsub231pd vfmaddsub321ps vfmaddsub321pd vfmsub132ps vfmsub132pd vfmsub312ps vfmsub312pd vfmsub213ps vfmsub213pd vfmsub123ps vfmsub123pd vfmsub231ps vfmsub231pd vfmsub321ps vfmsub321pd vfmsubadd132ps vfmsubadd132pd vfmsubadd312ps vfmsubadd312pd vfmsubadd213ps vfmsubadd213pd vfmsubadd123ps vfmsubadd123pd vfmsubadd231ps vfmsubadd231pd vfmsubadd321ps vfmsubadd321pd vfnmadd132ps vfnmadd132pd vfnmadd312ps vfnmadd312pd vfnmadd213ps vfnmadd213pd vfnmadd123ps vfnmadd123pd vfnmadd231ps vfnmadd231pd vfnmadd321ps vfnmadd321pd vfnmsub132ps vfnmsub132pd vfnmsub312ps vfnmsub312pd vfnmsub213ps vfnmsub213pd vfnmsub123ps vfnmsub123pd vfnmsub231ps vfnmsub231pd vfnmsub321ps vfnmsub321pd vfmadd132ss vfmadd132sd vfmadd312ss vfmadd312sd vfmadd213ss vfmadd213sd vfmadd123ss vfmadd123sd vfmadd231ss vfmadd231sd vfmadd321ss vfmadd321sd vfmsub132ss vfmsub132sd vfmsub312ss vfmsub312sd vfmsub213ss vfmsub213sd vfmsub123ss vfmsub123sd vfmsub231ss vfmsub231sd vfmsub321ss vfmsub321sd vfnmadd132ss vfnmadd132sd vfnmadd312ss vfnmadd312sd vfnmadd213ss vfnmadd213sd vfnmadd123ss vfnmadd123sd vfnmadd231ss vfnmadd231sd vfnmadd321ss vfnmadd321sd vfnmsub132ss vfnmsub132sd vfnmsub312ss vfnmsub312sd vfnmsub213ss vfnmsub213sd vfnmsub123ss vfnmsub123sd vfnmsub231ss vfnmsub231sd vfnmsub321ss vfnmsub321sd rdfsbase rdgsbase rdrand wrfsbase wrgsbase vcvtph2ps vcvtps2ph adcx adox rdseed clac stac xstore xcryptecb xcryptcbc xcryptctr xcryptcfb xcryptofb montmul xsha1 xsha256 llwpcb slwpcb lwpval lwpins vfmaddpd vfmaddps vfmaddsd vfmaddss vfmaddsubpd vfmaddsubps vfmsubaddpd vfmsubaddps vfmsubpd vfmsubps vfmsubsd vfmsubss vfnmaddpd vfnmaddps vfnmaddsd vfnmaddss vfnmsubpd vfnmsubps vfnmsubsd vfnmsubss vfrczpd vfrczps vfrczsd vfrczss vpcmov vpcomb vpcomd vpcomq vpcomub vpcomud vpcomuq vpcomuw vpcomw vphaddbd vphaddbq vphaddbw vphadddq vphaddubd vphaddubq vphaddubw vphaddudq vphadduwd vphadduwq vphaddwd vphaddwq vphsubbw vphsubdq vphsubwd vpmacsdd vpmacsdqh vpmacsdql vpmacssdd vpmacssdqh vpmacssdql vpmacsswd vpmacssww vpmacswd vpmacsww vpmadcsswd vpmadcswd vpperm vprotb vprotd vprotq vprotw vpshab vpshad vpshaq vpshaw vpshlb vpshld vpshlq vpshlw vbroadcasti128 vpblendd vpbroadcastb vpbroadcastw vpbroadcastd vpbroadcastq vpermd vpermpd vpermps vpermq vperm2i128 vextracti128 vinserti128 vpmaskmovd vpmaskmovq vpsllvd vpsllvq vpsravd vpsrlvd vpsrlvq vgatherdpd vgatherqpd vgatherdps vgatherqps vpgatherdd vpgatherqd vpgatherdq vpgatherqq xabort xbegin xend xtest andn bextr blci blcic blsi blsic blcfill blsfill blcmsk blsmsk blsr blcs bzhi mulx pdep pext rorx sarx shlx shrx tzcnt tzmsk t1mskc valignd valignq vblendmpd vblendmps vbroadcastf32x4 vbroadcastf64x4 vbroadcasti32x4 vbroadcasti64x4 vcompresspd vcompressps vcvtpd2udq vcvtps2udq vcvtsd2usi vcvtss2usi vcvttpd2udq vcvttps2udq vcvttsd2usi vcvttss2usi vcvtudq2pd vcvtudq2ps vcvtusi2sd vcvtusi2ss vexpandpd vexpandps vextractf32x4 vextractf64x4 vextracti32x4 vextracti64x4 vfixupimmpd vfixupimmps vfixupimmsd vfixupimmss vgetexppd vgetexpps vgetexpsd vgetexpss vgetmantpd vgetmantps vgetmantsd vgetmantss vinsertf32x4 vinsertf64x4 vinserti32x4 vinserti64x4 vmovdqa32 vmovdqa64 vmovdqu32 vmovdqu64 vpabsq vpandd vpandnd vpandnq vpandq vpblendmd vpblendmq vpcmpltd vpcmpled vpcmpneqd vpcmpnltd vpcmpnled vpcmpd vpcmpltq vpcmpleq vpcmpneqq vpcmpnltq vpcmpnleq vpcmpq vpcmpequd vpcmpltud vpcmpleud vpcmpnequd vpcmpnltud vpcmpnleud vpcmpud vpcmpequq vpcmpltuq vpcmpleuq vpcmpnequq vpcmpnltuq vpcmpnleuq vpcmpuq vpcompressd vpcompressq vpermi2d vpermi2pd vpermi2ps vpermi2q vpermt2d vpermt2pd vpermt2ps vpermt2q vpexpandd vpexpandq vpmaxsq vpmaxuq vpminsq vpminuq vpmovdb vpmovdw vpmovqb vpmovqd vpmovqw vpmovsdb vpmovsdw vpmovsqb vpmovsqd vpmovsqw vpmovusdb vpmovusdw vpmovusqb vpmovusqd vpmovusqw vpord vporq vprold vprolq vprolvd vprolvq vprord vprorq vprorvd vprorvq vpscatterdd vpscatterdq vpscatterqd vpscatterqq vpsraq vpsravq vpternlogd vpternlogq vptestmd vptestmq vptestnmd vptestnmq vpxord vpxorq vrcp14pd vrcp14ps vrcp14sd vrcp14ss vrndscalepd vrndscaleps vrndscalesd vrndscaless vrsqrt14pd vrsqrt14ps vrsqrt14sd vrsqrt14ss vscalefpd vscalefps vscalefsd vscalefss vscatterdpd vscatterdps vscatterqpd vscatterqps vshuff32x4 vshuff64x2 vshufi32x4 vshufi64x2 kandnw kandw kmovw knotw kortestw korw kshiftlw kshiftrw kunpckbw kxnorw kxorw vpbroadcastmb2q vpbroadcastmw2d vpconflictd vpconflictq vplzcntd vplzcntq vexp2pd vexp2ps vrcp28pd vrcp28ps vrcp28sd vrcp28ss vrsqrt28pd vrsqrt28ps vrsqrt28sd vrsqrt28ss vgatherpf0dpd vgatherpf0dps vgatherpf0qpd vgatherpf0qps vgatherpf1dpd vgatherpf1dps vgatherpf1qpd vgatherpf1qps vscatterpf0dpd vscatterpf0dps vscatterpf0qpd vscatterpf0qps vscatterpf1dpd vscatterpf1dps vscatterpf1qpd vscatterpf1qps prefetchwt1 bndmk bndcl bndcu bndcn bndmov bndldx bndstx sha1rnds4 sha1nexte sha1msg1 sha1msg2 sha256rnds2 sha256msg1 sha256msg2 hint_nop0 hint_nop1 hint_nop2 hint_nop3 hint_nop4 hint_nop5 hint_nop6 hint_nop7 hint_nop8 hint_nop9 hint_nop10 hint_nop11 hint_nop12 hint_nop13 hint_nop14 hint_nop15 hint_nop16 hint_nop17 hint_nop18 hint_nop19 hint_nop20 hint_nop21 hint_nop22 hint_nop23 hint_nop24 hint_nop25 hint_nop26 hint_nop27 hint_nop28 hint_nop29 hint_nop30 hint_nop31 hint_nop32 hint_nop33 hint_nop34 hint_nop35 hint_nop36 hint_nop37 hint_nop38 hint_nop39 hint_nop40 hint_nop41 hint_nop42 hint_nop43 hint_nop44 hint_nop45 hint_nop46 hint_nop47 hint_nop48 hint_nop49 hint_nop50 hint_nop51 hint_nop52 hint_nop53 hint_nop54 hint_nop55 hint_nop56 hint_nop57 hint_nop58 hint_nop59 hint_nop60 hint_nop61 hint_nop62 hint_nop63",built_in:"ip eip rip al ah bl bh cl ch dl dh sil dil bpl spl r8b r9b r10b r11b r12b r13b r14b r15b ax bx cx dx si di bp sp r8w r9w r10w r11w r12w r13w r14w r15w eax ebx ecx edx esi edi ebp esp eip r8d r9d r10d r11d r12d r13d r14d r15d rax rbx rcx rdx rsi rdi rbp rsp r8 r9 r10 r11 r12 r13 r14 r15 cs ds es fs gs ss st st0 st1 st2 st3 st4 st5 st6 st7 mm0 mm1 mm2 mm3 mm4 mm5 mm6 mm7 xmm0 xmm1 xmm2 xmm3 xmm4 xmm5 xmm6 xmm7 xmm8 xmm9 xmm10 xmm11 xmm12 xmm13 xmm14 xmm15 xmm16 xmm17 xmm18 xmm19 xmm20 xmm21 xmm22 xmm23 xmm24 xmm25 xmm26 xmm27 xmm28 xmm29 xmm30 xmm31 ymm0 ymm1 ymm2 ymm3 ymm4 ymm5 ymm6 ymm7 ymm8 ymm9 ymm10 ymm11 ymm12 ymm13 ymm14 ymm15 ymm16 ymm17 ymm18 ymm19 ymm20 ymm21 ymm22 ymm23 ymm24 ymm25 ymm26 ymm27 ymm28 ymm29 ymm30 ymm31 zmm0 zmm1 zmm2 zmm3 zmm4 zmm5 zmm6 zmm7 zmm8 zmm9 zmm10 zmm11 zmm12 zmm13 zmm14 zmm15 zmm16 zmm17 zmm18 zmm19 zmm20 zmm21 zmm22 zmm23 zmm24 zmm25 zmm26 zmm27 zmm28 zmm29 zmm30 zmm31 k0 k1 k2 k3 k4 k5 k6 k7 bnd0 bnd1 bnd2 bnd3 cr0 cr1 cr2 cr3 cr4 cr8 dr0 dr1 dr2 dr3 dr8 tr3 tr4 tr5 tr6 tr7 r0 r1 r2 r3 r4 r5 r6 r7 r0b r1b r2b r3b r4b r5b r6b r7b r0w r1w r2w r3w r4w r5w r6w r7w r0d r1d r2d r3d r4d r5d r6d r7d r0h r1h r2h r3h r0l r1l r2l r3l r4l r5l r6l r7l r8l r9l r10l r11l r12l r13l r14l r15l db dw dd dq dt ddq do dy dz resb resw resd resq rest resdq reso resy resz incbin equ times byte word dword qword nosplit rel abs seg wrt strict near far a32 ptr",meta:"%define %xdefine %+ %undef %defstr %deftok %assign %strcat %strlen %substr %rotate %elif %else %endif %if %ifmacro %ifctx %ifidn %ifidni %ifid %ifnum %ifstr %iftoken %ifempty %ifenv %error %warning %fatal %rep %endrep %include %push %pop %repl %pathsearch %depend %use %arg %stacksize %local %line %comment %endcomment .nolist __FILE__ __LINE__ __SECT__ __BITS__ __OUTPUT_FORMAT__ __DATE__ __TIME__ __DATE_NUM__ __TIME_NUM__ __UTC_DATE__ __UTC_TIME__ __UTC_DATE_NUM__ __UTC_TIME_NUM__ __PASS__ struc endstruc istruc at iend align alignb sectalign daz nodaz up down zero default option assume public bits use16 use32 use64 default section segment absolute extern global common cpu float __utf16__ __utf16le__ __utf16be__ __utf32__ __utf32le__ __utf32be__ __float8__ __float16__ __float32__ __float64__ __float80m__ __float80e__ __float128l__ __float128h__ __Infinity__ __QNaN__ __SNaN__ Inf NaN QNaN SNaN float8 float16 float32 float64 float80m float80e float128l float128h __FLOAT_DAZ__ __FLOAT_ROUND__ __FLOAT__"},contains:[s.COMMENT(";","$",{relevance:0}),{className:"number",variants:[{begin:"\\b(?:([0-9][0-9_]*)?\\.[0-9_]*(?:[eE][+-]?[0-9_]+)?|(0[Xx])?[0-9][0-9_]*\\.?[0-9_]*(?:[pP](?:[+-]?[0-9_]+)?)?)\\b",relevance:0},{begin:"\\$[0-9][0-9A-Fa-f]*",relevance:0},{begin:"\\b(?:[0-9A-Fa-f][0-9A-Fa-f_]*[Hh]|[0-9][0-9_]*[DdTt]?|[0-7][0-7_]*[QqOo]|[0-1][0-1_]*[BbYy])\\b"},{begin:"\\b(?:0[Xx][0-9A-Fa-f_]+|0[DdTt][0-9_]+|0[QqOo][0-7_]+|0[BbYy][0-1_]+)\\b"}]},s.QUOTE_STRING_MODE,{className:"string",variants:[{begin:"'",end:"[^\\\\]'"},{begin:"`",end:"[^\\\\]`"}],relevance:0},{className:"symbol",variants:[{begin:"^\\s*[A-Za-z._?][A-Za-z0-9_$#@~.?]*(:|\\s+label)"},{begin:"^\\s*%%[A-Za-z0-9_$#@~.?]*:"}],relevance:0},{className:"subst",begin:"%[0-9]+",relevance:0},{className:"subst",begin:"%!S+",relevance:0},{className:"meta",begin:/^\s*\.[\w_-]+/}]}}}());hljs.registerLanguage("kotlin",function(){"use strict";return function(e){var n={keyword:"abstract as val var vararg get set class object open private protected public noinline crossinline dynamic final enum if else do while for when throw try catch finally import package is in fun override companion reified inline lateinit init interface annotation data sealed internal infix operator out by constructor super tailrec where const inner suspend typealias external expect actual trait volatile transient native default",built_in:"Byte Short Char Int Long Boolean Float Double Void Unit Nothing",literal:"true false null"},a={className:"symbol",begin:e.UNDERSCORE_IDENT_RE+"@"},i={className:"subst",begin:"\\${",end:"}",contains:[e.C_NUMBER_MODE]},s={className:"variable",begin:"\\$"+e.UNDERSCORE_IDENT_RE},t={className:"string",variants:[{begin:'"""',end:'"""(?=[^"])',contains:[s,i]},{begin:"'",end:"'",illegal:/\n/,contains:[e.BACKSLASH_ESCAPE]},{begin:'"',end:'"',illegal:/\n/,contains:[e.BACKSLASH_ESCAPE,s,i]}]};i.contains.push(t);var r={className:"meta",begin:"@(?:file|property|field|get|set|receiver|param|setparam|delegate)\\s*:(?:\\s*"+e.UNDERSCORE_IDENT_RE+")?"},l={className:"meta",begin:"@"+e.UNDERSCORE_IDENT_RE,contains:[{begin:/\(/,end:/\)/,contains:[e.inherit(t,{className:"meta-string"})]}]},c=e.COMMENT("/\\*","\\*/",{contains:[e.C_BLOCK_COMMENT_MODE]}),o={variants:[{className:"type",begin:e.UNDERSCORE_IDENT_RE},{begin:/\(/,end:/\)/,contains:[]}]},d=o;return d.variants[1].contains=[o],o.variants[1].contains=[d],{name:"Kotlin",aliases:["kt"],keywords:n,contains:[e.COMMENT("/\\*\\*","\\*/",{relevance:0,contains:[{className:"doctag",begin:"@[A-Za-z]+"}]}),e.C_LINE_COMMENT_MODE,c,{className:"keyword",begin:/\b(break|continue|return|this)\b/,starts:{contains:[{className:"symbol",begin:/@\w+/}]}},a,r,l,{className:"function",beginKeywords:"fun",end:"[(]|$",returnBegin:!0,excludeEnd:!0,keywords:n,illegal:/fun\s+(<.*>)?[^\s\(]+(\s+[^\s\(]+)\s*=/,relevance:5,contains:[{begin:e.UNDERSCORE_IDENT_RE+"\\s*\\(",returnBegin:!0,relevance:0,contains:[e.UNDERSCORE_TITLE_MODE]},{className:"type",begin://,keywords:"reified",relevance:0},{className:"params",begin:/\(/,end:/\)/,endsParent:!0,keywords:n,relevance:0,contains:[{begin:/:/,end:/[=,\/]/,endsWithParent:!0,contains:[o,e.C_LINE_COMMENT_MODE,c],relevance:0},e.C_LINE_COMMENT_MODE,c,r,l,t,e.C_NUMBER_MODE]},c]},{className:"class",beginKeywords:"class interface trait",end:/[:\{(]|$/,excludeEnd:!0,illegal:"extends implements",contains:[{beginKeywords:"public protected internal private constructor"},e.UNDERSCORE_TITLE_MODE,{className:"type",begin://,excludeBegin:!0,excludeEnd:!0,relevance:0},{className:"type",begin:/[,:]\s*/,end:/[<\(,]|$/,excludeBegin:!0,returnEnd:!0},r,l]},t,{className:"meta",begin:"^#!/usr/bin/env",end:"$",illegal:"\n"},{className:"number",begin:"\\b(0[bB]([01]+[01_]+[01]+|[01]+)|0[xX]([a-fA-F0-9]+[a-fA-F0-9_]+[a-fA-F0-9]+|[a-fA-F0-9]+)|(([\\d]+[\\d_]+[\\d]+|[\\d]+)(\\.([\\d]+[\\d_]+[\\d]+|[\\d]+))?|\\.([\\d]+[\\d_]+[\\d]+|[\\d]+))([eE][-+]?\\d+)?)[lLfF]?",relevance:0}]}}}());hljs.registerLanguage("armasm",function(){"use strict";return function(s){const e={variants:[s.COMMENT("^[ \\t]*(?=#)","$",{relevance:0,excludeBegin:!0}),s.COMMENT("[;@]","$",{relevance:0}),s.C_LINE_COMMENT_MODE,s.C_BLOCK_COMMENT_MODE]};return{name:"ARM Assembly",case_insensitive:!0,aliases:["arm"],keywords:{$pattern:"\\.?"+s.IDENT_RE,meta:".2byte .4byte .align .ascii .asciz .balign .byte .code .data .else .end .endif .endm .endr .equ .err .exitm .extern .global .hword .if .ifdef .ifndef .include .irp .long .macro .rept .req .section .set .skip .space .text .word .arm .thumb .code16 .code32 .force_thumb .thumb_func .ltorg ALIAS ALIGN ARM AREA ASSERT ATTR CN CODE CODE16 CODE32 COMMON CP DATA DCB DCD DCDU DCDO DCFD DCFDU DCI DCQ DCQU DCW DCWU DN ELIF ELSE END ENDFUNC ENDIF ENDP ENTRY EQU EXPORT EXPORTAS EXTERN FIELD FILL FUNCTION GBLA GBLL GBLS GET GLOBAL IF IMPORT INCBIN INCLUDE INFO KEEP LCLA LCLL LCLS LTORG MACRO MAP MEND MEXIT NOFP OPT PRESERVE8 PROC QN READONLY RELOC REQUIRE REQUIRE8 RLIST FN ROUT SETA SETL SETS SN SPACE SUBT THUMB THUMBX TTL WHILE WEND ",built_in:"r0 r1 r2 r3 r4 r5 r6 r7 r8 r9 r10 r11 r12 r13 r14 r15 pc lr sp ip sl sb fp a1 a2 a3 a4 v1 v2 v3 v4 v5 v6 v7 v8 f0 f1 f2 f3 f4 f5 f6 f7 p0 p1 p2 p3 p4 p5 p6 p7 p8 p9 p10 p11 p12 p13 p14 p15 c0 c1 c2 c3 c4 c5 c6 c7 c8 c9 c10 c11 c12 c13 c14 c15 q0 q1 q2 q3 q4 q5 q6 q7 q8 q9 q10 q11 q12 q13 q14 q15 cpsr_c cpsr_x cpsr_s cpsr_f cpsr_cx cpsr_cxs cpsr_xs cpsr_xsf cpsr_sf cpsr_cxsf spsr_c spsr_x spsr_s spsr_f spsr_cx spsr_cxs spsr_xs spsr_xsf spsr_sf spsr_cxsf s0 s1 s2 s3 s4 s5 s6 s7 s8 s9 s10 s11 s12 s13 s14 s15 s16 s17 s18 s19 s20 s21 s22 s23 s24 s25 s26 s27 s28 s29 s30 s31 d0 d1 d2 d3 d4 d5 d6 d7 d8 d9 d10 d11 d12 d13 d14 d15 d16 d17 d18 d19 d20 d21 d22 d23 d24 d25 d26 d27 d28 d29 d30 d31 {PC} {VAR} {TRUE} {FALSE} {OPT} {CONFIG} {ENDIAN} {CODESIZE} {CPU} {FPU} {ARCHITECTURE} {PCSTOREOFFSET} {ARMASM_VERSION} {INTER} {ROPI} {RWPI} {SWST} {NOSWST} . @"},contains:[{className:"keyword",begin:"\\b(adc|(qd?|sh?|u[qh]?)?add(8|16)?|usada?8|(q|sh?|u[qh]?)?(as|sa)x|and|adrl?|sbc|rs[bc]|asr|b[lx]?|blx|bxj|cbn?z|tb[bh]|bic|bfc|bfi|[su]bfx|bkpt|cdp2?|clz|clrex|cmp|cmn|cpsi[ed]|cps|setend|dbg|dmb|dsb|eor|isb|it[te]{0,3}|lsl|lsr|ror|rrx|ldm(([id][ab])|f[ds])?|ldr((s|ex)?[bhd])?|movt?|mvn|mra|mar|mul|[us]mull|smul[bwt][bt]|smu[as]d|smmul|smmla|mla|umlaal|smlal?([wbt][bt]|d)|mls|smlsl?[ds]|smc|svc|sev|mia([bt]{2}|ph)?|mrr?c2?|mcrr2?|mrs|msr|orr|orn|pkh(tb|bt)|rbit|rev(16|sh)?|sel|[su]sat(16)?|nop|pop|push|rfe([id][ab])?|stm([id][ab])?|str(ex)?[bhd]?|(qd?)?sub|(sh?|q|u[qh]?)?sub(8|16)|[su]xt(a?h|a?b(16)?)|srs([id][ab])?|swpb?|swi|smi|tst|teq|wfe|wfi|yield)(eq|ne|cs|cc|mi|pl|vs|vc|hi|ls|ge|lt|gt|le|al|hs|lo)?[sptrx]?(?=\\s)"},e,s.QUOTE_STRING_MODE,{className:"string",begin:"'",end:"[^\\\\]'",relevance:0},{className:"title",begin:"\\|",end:"\\|",illegal:"\\n",relevance:0},{className:"number",variants:[{begin:"[#$=]?0x[0-9a-f]+"},{begin:"[#$=]?0b[01]+"},{begin:"[#$=]\\d+"},{begin:"\\b\\d+"}],relevance:0},{className:"symbol",variants:[{begin:"^[ \\t]*[a-z_\\.\\$][a-z0-9_\\.\\$]+:"},{begin:"^[a-z_\\.\\$][a-z0-9_\\.\\$]+"},{begin:"[=#]\\w+"}],relevance:0}]}}}());hljs.registerLanguage("go",function(){"use strict";return function(e){var n={keyword:"break default func interface select case map struct chan else goto package switch const fallthrough if range type continue for import return var go defer bool byte complex64 complex128 float32 float64 int8 int16 int32 int64 string uint8 uint16 uint32 uint64 int uint uintptr rune",literal:"true false iota nil",built_in:"append cap close complex copy imag len make new panic print println real recover delete"};return{name:"Go",aliases:["golang"],keywords:n,illegal:">>|\.\.\.) /},i={className:"subst",begin:/\{/,end:/\}/,keywords:n,illegal:/#/},s={begin:/\{\{/,relevance:0},r={className:"string",contains:[e.BACKSLASH_ESCAPE],variants:[{begin:/(u|b)?r?'''/,end:/'''/,contains:[e.BACKSLASH_ESCAPE,a],relevance:10},{begin:/(u|b)?r?"""/,end:/"""/,contains:[e.BACKSLASH_ESCAPE,a],relevance:10},{begin:/(fr|rf|f)'''/,end:/'''/,contains:[e.BACKSLASH_ESCAPE,a,s,i]},{begin:/(fr|rf|f)"""/,end:/"""/,contains:[e.BACKSLASH_ESCAPE,a,s,i]},{begin:/(u|r|ur)'/,end:/'/,relevance:10},{begin:/(u|r|ur)"/,end:/"/,relevance:10},{begin:/(b|br)'/,end:/'/},{begin:/(b|br)"/,end:/"/},{begin:/(fr|rf|f)'/,end:/'/,contains:[e.BACKSLASH_ESCAPE,s,i]},{begin:/(fr|rf|f)"/,end:/"/,contains:[e.BACKSLASH_ESCAPE,s,i]},e.APOS_STRING_MODE,e.QUOTE_STRING_MODE]},l={className:"number",relevance:0,variants:[{begin:e.BINARY_NUMBER_RE+"[lLjJ]?"},{begin:"\\b(0o[0-7]+)[lLjJ]?"},{begin:e.C_NUMBER_RE+"[lLjJ]?"}]},t={className:"params",variants:[{begin:/\(\s*\)/,skip:!0,className:null},{begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,contains:["self",a,l,r,e.HASH_COMMENT_MODE]}]};return i.contains=[r,l,a],{name:"Python",aliases:["py","gyp","ipython"],keywords:n,illegal:/(<\/|->|\?)|=>/,contains:[a,l,{beginKeywords:"if",relevance:0},r,e.HASH_COMMENT_MODE,{variants:[{className:"function",beginKeywords:"def"},{className:"class",beginKeywords:"class"}],end:/:/,illegal:/[${=;\n,]/,contains:[e.UNDERSCORE_TITLE_MODE,t,{begin:/->/,endsWithParent:!0,keywords:"None"}]},{className:"meta",begin:/^[\t ]*@/,end:/$/},{begin:/\b(print|exec)\(/}]}}}());hljs.registerLanguage("shell",function(){"use strict";return function(s){return{name:"Shell Session",aliases:["console"],contains:[{className:"meta",begin:"^\\s{0,3}[/\\w\\d\\[\\]()@-]*[>%$#]",starts:{end:"$",subLanguage:"bash"}}]}}}());hljs.registerLanguage("scala",function(){"use strict";return function(e){var n={className:"subst",variants:[{begin:"\\$[A-Za-z0-9_]+"},{begin:"\\${",end:"}"}]},a={className:"string",variants:[{begin:'"',end:'"',illegal:"\\n",contains:[e.BACKSLASH_ESCAPE]},{begin:'"""',end:'"""',relevance:10},{begin:'[a-z]+"',end:'"',illegal:"\\n",contains:[e.BACKSLASH_ESCAPE,n]},{className:"string",begin:'[a-z]+"""',end:'"""',contains:[n],relevance:10}]},s={className:"type",begin:"\\b[A-Z][A-Za-z0-9_]*",relevance:0},t={className:"title",begin:/[^0-9\n\t "'(),.`{}\[\]:;][^\n\t "'(),.`{}\[\]:;]+|[^0-9\n\t "'(),.`{}\[\]:;=]/,relevance:0},i={className:"class",beginKeywords:"class object trait type",end:/[:={\[\n;]/,excludeEnd:!0,contains:[{beginKeywords:"extends with",relevance:10},{begin:/\[/,end:/\]/,excludeBegin:!0,excludeEnd:!0,relevance:0,contains:[s]},{className:"params",begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,relevance:0,contains:[s]},t]},l={className:"function",beginKeywords:"def",end:/[:={\[(\n;]/,excludeEnd:!0,contains:[t]};return{name:"Scala",keywords:{literal:"true false null",keyword:"type yield lazy override def with val var sealed abstract private trait object if forSome for while throw finally protected extends import final return else break new catch super class case package default try this match continue throws implicit"},contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,a,{className:"symbol",begin:"'\\w[\\w\\d_]*(?!')"},s,l,i,e.C_NUMBER_MODE,{className:"meta",begin:"@[A-Za-z]+"}]}}}());hljs.registerLanguage("julia",function(){"use strict";return function(e){var r="[A-Za-z_\\u00A1-\\uFFFF][A-Za-z_0-9\\u00A1-\\uFFFF]*",t={$pattern:r,keyword:"in isa where baremodule begin break catch ccall const continue do else elseif end export false finally for function global if import importall let local macro module quote return true try using while type immutable abstract bitstype typealias ",literal:"true false ARGS C_NULL DevNull ENDIAN_BOM ENV I Inf Inf16 Inf32 Inf64 InsertionSort JULIA_HOME LOAD_PATH MergeSort NaN NaN16 NaN32 NaN64 PROGRAM_FILE QuickSort RoundDown RoundFromZero RoundNearest RoundNearestTiesAway RoundNearestTiesUp RoundToZero RoundUp STDERR STDIN STDOUT VERSION catalan e|0 eu|0 eulergamma golden im nothing pi γ π φ ",built_in:"ANY AbstractArray AbstractChannel AbstractFloat AbstractMatrix AbstractRNG AbstractSerializer AbstractSet AbstractSparseArray AbstractSparseMatrix AbstractSparseVector AbstractString AbstractUnitRange AbstractVecOrMat AbstractVector Any ArgumentError Array AssertionError Associative Base64DecodePipe Base64EncodePipe Bidiagonal BigFloat BigInt BitArray BitMatrix BitVector Bool BoundsError BufferStream CachingPool CapturedException CartesianIndex CartesianRange Cchar Cdouble Cfloat Channel Char Cint Cintmax_t Clong Clonglong ClusterManager Cmd CodeInfo Colon Complex Complex128 Complex32 Complex64 CompositeException Condition ConjArray ConjMatrix ConjVector Cptrdiff_t Cshort Csize_t Cssize_t Cstring Cuchar Cuint Cuintmax_t Culong Culonglong Cushort Cwchar_t Cwstring DataType Date DateFormat DateTime DenseArray DenseMatrix DenseVecOrMat DenseVector Diagonal Dict DimensionMismatch Dims DirectIndexString Display DivideError DomainError EOFError EachLine Enum Enumerate ErrorException Exception ExponentialBackOff Expr Factorization FileMonitor Float16 Float32 Float64 Function Future GlobalRef GotoNode HTML Hermitian IO IOBuffer IOContext IOStream IPAddr IPv4 IPv6 IndexCartesian IndexLinear IndexStyle InexactError InitError Int Int128 Int16 Int32 Int64 Int8 IntSet Integer InterruptException InvalidStateException Irrational KeyError LabelNode LinSpace LineNumberNode LoadError LowerTriangular MIME Matrix MersenneTwister Method MethodError MethodTable Module NTuple NewvarNode NullException Nullable Number ObjectIdDict OrdinalRange OutOfMemoryError OverflowError Pair ParseError PartialQuickSort PermutedDimsArray Pipe PollingFileWatcher ProcessExitedException Ptr QuoteNode RandomDevice Range RangeIndex Rational RawFD ReadOnlyMemoryError Real ReentrantLock Ref Regex RegexMatch RemoteChannel RemoteException RevString RoundingMode RowVector SSAValue SegmentationFault SerializationState Set SharedArray SharedMatrix SharedVector Signed SimpleVector Slot SlotNumber SparseMatrixCSC SparseVector StackFrame StackOverflowError StackTrace StepRange StepRangeLen StridedArray StridedMatrix StridedVecOrMat StridedVector String SubArray SubString SymTridiagonal Symbol Symmetric SystemError TCPSocket Task Text TextDisplay Timer Tridiagonal Tuple Type TypeError TypeMapEntry TypeMapLevel TypeName TypeVar TypedSlot UDPSocket UInt UInt128 UInt16 UInt32 UInt64 UInt8 UndefRefError UndefVarError UnicodeError UniformScaling Union UnionAll UnitRange Unsigned UpperTriangular Val Vararg VecElement VecOrMat Vector VersionNumber Void WeakKeyDict WeakRef WorkerConfig WorkerPool "},a={keywords:t,illegal:/<\//},n={className:"subst",begin:/\$\(/,end:/\)/,keywords:t},o={className:"variable",begin:"\\$"+r},i={className:"string",contains:[e.BACKSLASH_ESCAPE,n,o],variants:[{begin:/\w*"""/,end:/"""\w*/,relevance:10},{begin:/\w*"/,end:/"\w*/}]},l={className:"string",contains:[e.BACKSLASH_ESCAPE,n,o],begin:"`",end:"`"},s={className:"meta",begin:"@"+r};return a.name="Julia",a.contains=[{className:"number",begin:/(\b0x[\d_]*(\.[\d_]*)?|0x\.\d[\d_]*)p[-+]?\d+|\b0[box][a-fA-F0-9][a-fA-F0-9_]*|(\b\d[\d_]*(\.[\d_]*)?|\.\d[\d_]*)([eEfF][-+]?\d+)?/,relevance:0},{className:"string",begin:/'(.|\\[xXuU][a-zA-Z0-9]+)'/},i,l,s,{className:"comment",variants:[{begin:"#=",end:"=#",relevance:10},{begin:"#",end:"$"}]},e.HASH_COMMENT_MODE,{className:"keyword",begin:"\\b(((abstract|primitive)\\s+)type|(mutable\\s+)?struct)\\b"},{begin:/<:/}],n.contains=a.contains,a}}());hljs.registerLanguage("php-template",function(){"use strict";return function(n){return{name:"PHP template",subLanguage:"xml",contains:[{begin:/<\?(php|=)?/,end:/\?>/,subLanguage:"php",contains:[{begin:"/\\*",end:"\\*/",skip:!0},{begin:'b"',end:'"',skip:!0},{begin:"b'",end:"'",skip:!0},n.inherit(n.APOS_STRING_MODE,{illegal:null,className:null,contains:null,skip:!0}),n.inherit(n.QUOTE_STRING_MODE,{illegal:null,className:null,contains:null,skip:!0})]}]}}}());hljs.registerLanguage("scss",function(){"use strict";return function(e){var t={className:"variable",begin:"(\\$[a-zA-Z-][a-zA-Z0-9_-]*)\\b"},i={className:"number",begin:"#[0-9A-Fa-f]+"};return e.CSS_NUMBER_MODE,e.QUOTE_STRING_MODE,e.APOS_STRING_MODE,e.C_BLOCK_COMMENT_MODE,{name:"SCSS",case_insensitive:!0,illegal:"[=/|']",contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,{className:"selector-id",begin:"\\#[A-Za-z0-9_-]+",relevance:0},{className:"selector-class",begin:"\\.[A-Za-z0-9_-]+",relevance:0},{className:"selector-attr",begin:"\\[",end:"\\]",illegal:"$"},{className:"selector-tag",begin:"\\b(a|abbr|acronym|address|area|article|aside|audio|b|base|big|blockquote|body|br|button|canvas|caption|cite|code|col|colgroup|command|datalist|dd|del|details|dfn|div|dl|dt|em|embed|fieldset|figcaption|figure|footer|form|frame|frameset|(h[1-6])|head|header|hgroup|hr|html|i|iframe|img|input|ins|kbd|keygen|label|legend|li|link|map|mark|meta|meter|nav|noframes|noscript|object|ol|optgroup|option|output|p|param|pre|progress|q|rp|rt|ruby|samp|script|section|select|small|span|strike|strong|style|sub|sup|table|tbody|td|textarea|tfoot|th|thead|time|title|tr|tt|ul|var|video)\\b",relevance:0},{className:"selector-pseudo",begin:":(visited|valid|root|right|required|read-write|read-only|out-range|optional|only-of-type|only-child|nth-of-type|nth-last-of-type|nth-last-child|nth-child|not|link|left|last-of-type|last-child|lang|invalid|indeterminate|in-range|hover|focus|first-of-type|first-line|first-letter|first-child|first|enabled|empty|disabled|default|checked|before|after|active)"},{className:"selector-pseudo",begin:"::(after|before|choices|first-letter|first-line|repeat-index|repeat-item|selection|value)"},t,{className:"attribute",begin:"\\b(src|z-index|word-wrap|word-spacing|word-break|width|widows|white-space|visibility|vertical-align|unicode-bidi|transition-timing-function|transition-property|transition-duration|transition-delay|transition|transform-style|transform-origin|transform|top|text-underline-position|text-transform|text-shadow|text-rendering|text-overflow|text-indent|text-decoration-style|text-decoration-line|text-decoration-color|text-decoration|text-align-last|text-align|tab-size|table-layout|right|resize|quotes|position|pointer-events|perspective-origin|perspective|page-break-inside|page-break-before|page-break-after|padding-top|padding-right|padding-left|padding-bottom|padding|overflow-y|overflow-x|overflow-wrap|overflow|outline-width|outline-style|outline-offset|outline-color|outline|orphans|order|opacity|object-position|object-fit|normal|none|nav-up|nav-right|nav-left|nav-index|nav-down|min-width|min-height|max-width|max-height|mask|marks|margin-top|margin-right|margin-left|margin-bottom|margin|list-style-type|list-style-position|list-style-image|list-style|line-height|letter-spacing|left|justify-content|initial|inherit|ime-mode|image-orientation|image-resolution|image-rendering|icon|hyphens|height|font-weight|font-variant-ligatures|font-variant|font-style|font-stretch|font-size-adjust|font-size|font-language-override|font-kerning|font-feature-settings|font-family|font|float|flex-wrap|flex-shrink|flex-grow|flex-flow|flex-direction|flex-basis|flex|filter|empty-cells|display|direction|cursor|counter-reset|counter-increment|content|column-width|column-span|column-rule-width|column-rule-style|column-rule-color|column-rule|column-gap|column-fill|column-count|columns|color|clip-path|clip|clear|caption-side|break-inside|break-before|break-after|box-sizing|box-shadow|box-decoration-break|bottom|border-width|border-top-width|border-top-style|border-top-right-radius|border-top-left-radius|border-top-color|border-top|border-style|border-spacing|border-right-width|border-right-style|border-right-color|border-right|border-radius|border-left-width|border-left-style|border-left-color|border-left|border-image-width|border-image-source|border-image-slice|border-image-repeat|border-image-outset|border-image|border-color|border-collapse|border-bottom-width|border-bottom-style|border-bottom-right-radius|border-bottom-left-radius|border-bottom-color|border-bottom|border|background-size|background-repeat|background-position|background-origin|background-image|background-color|background-clip|background-attachment|background-blend-mode|background|backface-visibility|auto|animation-timing-function|animation-play-state|animation-name|animation-iteration-count|animation-fill-mode|animation-duration|animation-direction|animation-delay|animation|align-self|align-items|align-content)\\b",illegal:"[^\\s]"},{begin:"\\b(whitespace|wait|w-resize|visible|vertical-text|vertical-ideographic|uppercase|upper-roman|upper-alpha|underline|transparent|top|thin|thick|text|text-top|text-bottom|tb-rl|table-header-group|table-footer-group|sw-resize|super|strict|static|square|solid|small-caps|separate|se-resize|scroll|s-resize|rtl|row-resize|ridge|right|repeat|repeat-y|repeat-x|relative|progress|pointer|overline|outside|outset|oblique|nowrap|not-allowed|normal|none|nw-resize|no-repeat|no-drop|newspaper|ne-resize|n-resize|move|middle|medium|ltr|lr-tb|lowercase|lower-roman|lower-alpha|loose|list-item|line|line-through|line-edge|lighter|left|keep-all|justify|italic|inter-word|inter-ideograph|inside|inset|inline|inline-block|inherit|inactive|ideograph-space|ideograph-parenthesis|ideograph-numeric|ideograph-alpha|horizontal|hidden|help|hand|groove|fixed|ellipsis|e-resize|double|dotted|distribute|distribute-space|distribute-letter|distribute-all-lines|disc|disabled|default|decimal|dashed|crosshair|collapse|col-resize|circle|char|center|capitalize|break-word|break-all|bottom|both|bolder|bold|block|bidi-override|below|baseline|auto|always|all-scroll|absolute|table|table-cell)\\b"},{begin:":",end:";",contains:[t,i,e.CSS_NUMBER_MODE,e.QUOTE_STRING_MODE,e.APOS_STRING_MODE,{className:"meta",begin:"!important"}]},{begin:"@(page|font-face)",lexemes:"@[a-z-]+",keywords:"@page @font-face"},{begin:"@",end:"[{;]",returnBegin:!0,keywords:"and or not only",contains:[{begin:"@[a-z-]+",className:"keyword"},t,e.QUOTE_STRING_MODE,e.APOS_STRING_MODE,i,e.CSS_NUMBER_MODE]}]}}}());hljs.registerLanguage("r",function(){"use strict";return function(e){var n="([a-zA-Z]|\\.[a-zA-Z.])[a-zA-Z0-9._]*";return{name:"R",contains:[e.HASH_COMMENT_MODE,{begin:n,keywords:{$pattern:n,keyword:"function if in break next repeat else for return switch while try tryCatch stop warning require library attach detach source setMethod setGeneric setGroupGeneric setClass ...",literal:"NULL NA TRUE FALSE T F Inf NaN NA_integer_|10 NA_real_|10 NA_character_|10 NA_complex_|10"},relevance:0},{className:"number",begin:"0[xX][0-9a-fA-F]+[Li]?\\b",relevance:0},{className:"number",begin:"\\d+(?:[eE][+\\-]?\\d*)?L\\b",relevance:0},{className:"number",begin:"\\d+\\.(?!\\d)(?:i\\b)?",relevance:0},{className:"number",begin:"\\d+(?:\\.\\d*)?(?:[eE][+\\-]?\\d*)?i?\\b",relevance:0},{className:"number",begin:"\\.\\d+(?:[eE][+\\-]?\\d*)?i?\\b",relevance:0},{begin:"`",end:"`",relevance:0},{className:"string",contains:[e.BACKSLASH_ESCAPE],variants:[{begin:'"',end:'"'},{begin:"'",end:"'"}]}]}}}());hljs.registerLanguage("sql",function(){"use strict";return function(e){var t=e.COMMENT("--","$");return{name:"SQL",case_insensitive:!0,illegal:/[<>{}*]/,contains:[{beginKeywords:"begin end start commit rollback savepoint lock alter create drop rename call delete do handler insert load replace select truncate update set show pragma grant merge describe use explain help declare prepare execute deallocate release unlock purge reset change stop analyze cache flush optimize repair kill install uninstall checksum restore check backup revoke comment values with",end:/;/,endsWithParent:!0,keywords:{$pattern:/[\w\.]+/,keyword:"as abort abs absolute acc acce accep accept access accessed accessible account acos action activate add addtime admin administer advanced advise aes_decrypt aes_encrypt after agent aggregate ali alia alias all allocate allow alter always analyze ancillary and anti any anydata anydataset anyschema anytype apply archive archived archivelog are as asc ascii asin assembly assertion associate asynchronous at atan atn2 attr attri attrib attribu attribut attribute attributes audit authenticated authentication authid authors auto autoallocate autodblink autoextend automatic availability avg backup badfile basicfile before begin beginning benchmark between bfile bfile_base big bigfile bin binary_double binary_float binlog bit_and bit_count bit_length bit_or bit_xor bitmap blob_base block blocksize body both bound bucket buffer_cache buffer_pool build bulk by byte byteordermark bytes cache caching call calling cancel capacity cascade cascaded case cast catalog category ceil ceiling chain change changed char_base char_length character_length characters characterset charindex charset charsetform charsetid check checksum checksum_agg child choose chr chunk class cleanup clear client clob clob_base clone close cluster_id cluster_probability cluster_set clustering coalesce coercibility col collate collation collect colu colum column column_value columns columns_updated comment commit compact compatibility compiled complete composite_limit compound compress compute concat concat_ws concurrent confirm conn connec connect connect_by_iscycle connect_by_isleaf connect_by_root connect_time connection consider consistent constant constraint constraints constructor container content contents context contributors controlfile conv convert convert_tz corr corr_k corr_s corresponding corruption cos cost count count_big counted covar_pop covar_samp cpu_per_call cpu_per_session crc32 create creation critical cross cube cume_dist curdate current current_date current_time current_timestamp current_user cursor curtime customdatum cycle data database databases datafile datafiles datalength date_add date_cache date_format date_sub dateadd datediff datefromparts datename datepart datetime2fromparts day day_to_second dayname dayofmonth dayofweek dayofyear days db_role_change dbtimezone ddl deallocate declare decode decompose decrement decrypt deduplicate def defa defau defaul default defaults deferred defi defin define degrees delayed delegate delete delete_all delimited demand dense_rank depth dequeue des_decrypt des_encrypt des_key_file desc descr descri describ describe descriptor deterministic diagnostics difference dimension direct_load directory disable disable_all disallow disassociate discardfile disconnect diskgroup distinct distinctrow distribute distributed div do document domain dotnet double downgrade drop dumpfile duplicate duration each edition editionable editions element ellipsis else elsif elt empty enable enable_all enclosed encode encoding encrypt end end-exec endian enforced engine engines enqueue enterprise entityescaping eomonth error errors escaped evalname evaluate event eventdata events except exception exceptions exchange exclude excluding execu execut execute exempt exists exit exp expire explain explode export export_set extended extent external external_1 external_2 externally extract failed failed_login_attempts failover failure far fast feature_set feature_value fetch field fields file file_name_convert filesystem_like_logging final finish first first_value fixed flash_cache flashback floor flush following follows for forall force foreign form forma format found found_rows freelist freelists freepools fresh from from_base64 from_days ftp full function general generated get get_format get_lock getdate getutcdate global global_name globally go goto grant grants greatest group group_concat group_id grouping grouping_id groups gtid_subtract guarantee guard handler hash hashkeys having hea head headi headin heading heap help hex hierarchy high high_priority hosts hour hours http id ident_current ident_incr ident_seed identified identity idle_time if ifnull ignore iif ilike ilm immediate import in include including increment index indexes indexing indextype indicator indices inet6_aton inet6_ntoa inet_aton inet_ntoa infile initial initialized initially initrans inmemory inner innodb input insert install instance instantiable instr interface interleaved intersect into invalidate invisible is is_free_lock is_ipv4 is_ipv4_compat is_not is_not_null is_used_lock isdate isnull isolation iterate java join json json_exists keep keep_duplicates key keys kill language large last last_day last_insert_id last_value lateral lax lcase lead leading least leaves left len lenght length less level levels library like like2 like4 likec limit lines link list listagg little ln load load_file lob lobs local localtime localtimestamp locate locator lock locked log log10 log2 logfile logfiles logging logical logical_reads_per_call logoff logon logs long loop low low_priority lower lpad lrtrim ltrim main make_set makedate maketime managed management manual map mapping mask master master_pos_wait match matched materialized max maxextents maximize maxinstances maxlen maxlogfiles maxloghistory maxlogmembers maxsize maxtrans md5 measures median medium member memcompress memory merge microsecond mid migration min minextents minimum mining minus minute minutes minvalue missing mod mode model modification modify module monitoring month months mount move movement multiset mutex name name_const names nan national native natural nav nchar nclob nested never new newline next nextval no no_write_to_binlog noarchivelog noaudit nobadfile nocheck nocompress nocopy nocycle nodelay nodiscardfile noentityescaping noguarantee nokeep nologfile nomapping nomaxvalue nominimize nominvalue nomonitoring none noneditionable nonschema noorder nopr nopro noprom nopromp noprompt norely noresetlogs noreverse normal norowdependencies noschemacheck noswitch not nothing notice notnull notrim novalidate now nowait nth_value nullif nulls num numb numbe nvarchar nvarchar2 object ocicoll ocidate ocidatetime ociduration ociinterval ociloblocator ocinumber ociref ocirefcursor ocirowid ocistring ocitype oct octet_length of off offline offset oid oidindex old on online only opaque open operations operator optimal optimize option optionally or oracle oracle_date oradata ord ordaudio orddicom orddoc order ordimage ordinality ordvideo organization orlany orlvary out outer outfile outline output over overflow overriding package pad parallel parallel_enable parameters parent parse partial partition partitions pascal passing password password_grace_time password_lock_time password_reuse_max password_reuse_time password_verify_function patch path patindex pctincrease pctthreshold pctused pctversion percent percent_rank percentile_cont percentile_disc performance period period_add period_diff permanent physical pi pipe pipelined pivot pluggable plugin policy position post_transaction pow power pragma prebuilt precedes preceding precision prediction prediction_cost prediction_details prediction_probability prediction_set prepare present preserve prior priority private private_sga privileges procedural procedure procedure_analyze processlist profiles project prompt protection public publishingservername purge quarter query quick quiesce quota quotename radians raise rand range rank raw read reads readsize rebuild record records recover recovery recursive recycle redo reduced ref reference referenced references referencing refresh regexp_like register regr_avgx regr_avgy regr_count regr_intercept regr_r2 regr_slope regr_sxx regr_sxy reject rekey relational relative relaylog release release_lock relies_on relocate rely rem remainder rename repair repeat replace replicate replication required reset resetlogs resize resource respect restore restricted result result_cache resumable resume retention return returning returns reuse reverse revoke right rlike role roles rollback rolling rollup round row row_count rowdependencies rowid rownum rows rtrim rules safe salt sample save savepoint sb1 sb2 sb4 scan schema schemacheck scn scope scroll sdo_georaster sdo_topo_geometry search sec_to_time second seconds section securefile security seed segment select self semi sequence sequential serializable server servererror session session_user sessions_per_user set sets settings sha sha1 sha2 share shared shared_pool short show shrink shutdown si_averagecolor si_colorhistogram si_featurelist si_positionalcolor si_stillimage si_texture siblings sid sign sin size size_t sizes skip slave sleep smalldatetimefromparts smallfile snapshot some soname sort soundex source space sparse spfile split sql sql_big_result sql_buffer_result sql_cache sql_calc_found_rows sql_small_result sql_variant_property sqlcode sqldata sqlerror sqlname sqlstate sqrt square standalone standby start starting startup statement static statistics stats_binomial_test stats_crosstab stats_ks_test stats_mode stats_mw_test stats_one_way_anova stats_t_test_ stats_t_test_indep stats_t_test_one stats_t_test_paired stats_wsr_test status std stddev stddev_pop stddev_samp stdev stop storage store stored str str_to_date straight_join strcmp strict string struct stuff style subdate subpartition subpartitions substitutable substr substring subtime subtring_index subtype success sum suspend switch switchoffset switchover sync synchronous synonym sys sys_xmlagg sysasm sysaux sysdate sysdatetimeoffset sysdba sysoper system system_user sysutcdatetime table tables tablespace tablesample tan tdo template temporary terminated tertiary_weights test than then thread through tier ties time time_format time_zone timediff timefromparts timeout timestamp timestampadd timestampdiff timezone_abbr timezone_minute timezone_region to to_base64 to_date to_days to_seconds todatetimeoffset trace tracking transaction transactional translate translation treat trigger trigger_nestlevel triggers trim truncate try_cast try_convert try_parse type ub1 ub2 ub4 ucase unarchived unbounded uncompress under undo unhex unicode uniform uninstall union unique unix_timestamp unknown unlimited unlock unnest unpivot unrecoverable unsafe unsigned until untrusted unusable unused update updated upgrade upped upper upsert url urowid usable usage use use_stored_outlines user user_data user_resources users using utc_date utc_timestamp uuid uuid_short validate validate_password_strength validation valist value values var var_samp varcharc vari varia variab variabl variable variables variance varp varraw varrawc varray verify version versions view virtual visible void wait wallet warning warnings week weekday weekofyear wellformed when whene whenev wheneve whenever where while whitespace window with within without work wrapped xdb xml xmlagg xmlattributes xmlcast xmlcolattval xmlelement xmlexists xmlforest xmlindex xmlnamespaces xmlpi xmlquery xmlroot xmlschema xmlserialize xmltable xmltype xor year year_to_month years yearweek",literal:"true false null unknown",built_in:"array bigint binary bit blob bool boolean char character date dec decimal float int int8 integer interval number numeric real record serial serial8 smallint text time timestamp tinyint varchar varchar2 varying void"},contains:[{className:"string",begin:"'",end:"'",contains:[{begin:"''"}]},{className:"string",begin:'"',end:'"',contains:[{begin:'""'}]},{className:"string",begin:"`",end:"`"},e.C_NUMBER_MODE,e.C_BLOCK_COMMENT_MODE,t,e.HASH_COMMENT_MODE]},e.C_BLOCK_COMMENT_MODE,t,e.HASH_COMMENT_MODE]}}}());hljs.registerLanguage("c",function(){"use strict";return function(e){var n=e.getLanguage("c-like").rawDefinition();return n.name="C",n.aliases=["c","h"],n}}());hljs.registerLanguage("json",function(){"use strict";return function(n){var e={literal:"true false null"},i=[n.C_LINE_COMMENT_MODE,n.C_BLOCK_COMMENT_MODE],t=[n.QUOTE_STRING_MODE,n.C_NUMBER_MODE],a={end:",",endsWithParent:!0,excludeEnd:!0,contains:t,keywords:e},l={begin:"{",end:"}",contains:[{className:"attr",begin:/"/,end:/"/,contains:[n.BACKSLASH_ESCAPE],illegal:"\\n"},n.inherit(a,{begin:/:/})].concat(i),illegal:"\\S"},s={begin:"\\[",end:"\\]",contains:[n.inherit(a)],illegal:"\\S"};return t.push(l,s),i.forEach((function(n){t.push(n)})),{name:"JSON",contains:t,keywords:e,illegal:"\\S"}}}());hljs.registerLanguage("python-repl",function(){"use strict";return function(n){return{aliases:["pycon"],contains:[{className:"meta",starts:{end:/ |$/,starts:{end:"$",subLanguage:"python"}},variants:[{begin:/^>>>(?=[ ]|$)/},{begin:/^\.\.\.(?=[ ]|$)/}]}]}}}());hljs.registerLanguage("markdown",function(){"use strict";return function(n){const e={begin:"<",end:">",subLanguage:"xml",relevance:0},a={begin:"\\[.+?\\][\\(\\[].*?[\\)\\]]",returnBegin:!0,contains:[{className:"string",begin:"\\[",end:"\\]",excludeBegin:!0,returnEnd:!0,relevance:0},{className:"link",begin:"\\]\\(",end:"\\)",excludeBegin:!0,excludeEnd:!0},{className:"symbol",begin:"\\]\\[",end:"\\]",excludeBegin:!0,excludeEnd:!0}],relevance:10},i={className:"strong",contains:[],variants:[{begin:/_{2}/,end:/_{2}/},{begin:/\*{2}/,end:/\*{2}/}]},s={className:"emphasis",contains:[],variants:[{begin:/\*(?!\*)/,end:/\*/},{begin:/_(?!_)/,end:/_/,relevance:0}]};i.contains.push(s),s.contains.push(i);var c=[e,a];return i.contains=i.contains.concat(c),s.contains=s.contains.concat(c),{name:"Markdown",aliases:["md","mkdown","mkd"],contains:[{className:"section",variants:[{begin:"^#{1,6}",end:"$",contains:c=c.concat(i,s)},{begin:"(?=^.+?\\n[=-]{2,}$)",contains:[{begin:"^[=-]*$"},{begin:"^",end:"\\n",contains:c}]}]},e,{className:"bullet",begin:"^[ \t]*([*+-]|(\\d+\\.))(?=\\s+)",end:"\\s+",excludeEnd:!0},i,s,{className:"quote",begin:"^>\\s+",contains:c,end:"$"},{className:"code",variants:[{begin:"(`{3,})(.|\\n)*?\\1`*[ ]*"},{begin:"(~{3,})(.|\\n)*?\\1~*[ ]*"},{begin:"```",end:"```+[ ]*$"},{begin:"~~~",end:"~~~+[ ]*$"},{begin:"`.+?`"},{begin:"(?=^( {4}|\\t))",contains:[{begin:"^( {4}|\\t)",end:"(\\n)$"}],relevance:0}]},{begin:"^[-\\*]{3,}",end:"$"},a,{begin:/^\[[^\n]+\]:/,returnBegin:!0,contains:[{className:"symbol",begin:/\[/,end:/\]/,excludeBegin:!0,excludeEnd:!0},{className:"link",begin:/:\s*/,end:/$/,excludeBegin:!0}]}]}}}());hljs.registerLanguage("javascript",function(){"use strict";const e=["as","in","of","if","for","while","finally","var","new","function","do","return","void","else","break","catch","instanceof","with","throw","case","default","try","switch","continue","typeof","delete","let","yield","const","class","debugger","async","await","static","import","from","export","extends"],n=["true","false","null","undefined","NaN","Infinity"],a=[].concat(["setInterval","setTimeout","clearInterval","clearTimeout","require","exports","eval","isFinite","isNaN","parseFloat","parseInt","decodeURI","decodeURIComponent","encodeURI","encodeURIComponent","escape","unescape"],["arguments","this","super","console","window","document","localStorage","module","global"],["Intl","DataView","Number","Math","Date","String","RegExp","Object","Function","Boolean","Error","Symbol","Set","Map","WeakSet","WeakMap","Proxy","Reflect","JSON","Promise","Float64Array","Int16Array","Int32Array","Int8Array","Uint16Array","Uint32Array","Float32Array","Array","Uint8Array","Uint8ClampedArray","ArrayBuffer"],["EvalError","InternalError","RangeError","ReferenceError","SyntaxError","TypeError","URIError"]);function s(e){return r("(?=",e,")")}function r(...e){return e.map(e=>(function(e){return e?"string"==typeof e?e:e.source:null})(e)).join("")}return function(t){var i="[A-Za-z$_][0-9A-Za-z$_]*",c={begin:/<[A-Za-z0-9\\._:-]+/,end:/\/[A-Za-z0-9\\._:-]+>|\/>/},o={$pattern:"[A-Za-z$_][0-9A-Za-z$_]*",keyword:e.join(" "),literal:n.join(" "),built_in:a.join(" ")},l={className:"number",variants:[{begin:"\\b(0[bB][01]+)n?"},{begin:"\\b(0[oO][0-7]+)n?"},{begin:t.C_NUMBER_RE+"n?"}],relevance:0},E={className:"subst",begin:"\\$\\{",end:"\\}",keywords:o,contains:[]},d={begin:"html`",end:"",starts:{end:"`",returnEnd:!1,contains:[t.BACKSLASH_ESCAPE,E],subLanguage:"xml"}},g={begin:"css`",end:"",starts:{end:"`",returnEnd:!1,contains:[t.BACKSLASH_ESCAPE,E],subLanguage:"css"}},u={className:"string",begin:"`",end:"`",contains:[t.BACKSLASH_ESCAPE,E]};E.contains=[t.APOS_STRING_MODE,t.QUOTE_STRING_MODE,d,g,u,l,t.REGEXP_MODE];var b=E.contains.concat([{begin:/\(/,end:/\)/,contains:["self"].concat(E.contains,[t.C_BLOCK_COMMENT_MODE,t.C_LINE_COMMENT_MODE])},t.C_BLOCK_COMMENT_MODE,t.C_LINE_COMMENT_MODE]),_={className:"params",begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,contains:b};return{name:"JavaScript",aliases:["js","jsx","mjs","cjs"],keywords:o,contains:[t.SHEBANG({binary:"node",relevance:5}),{className:"meta",relevance:10,begin:/^\s*['"]use (strict|asm)['"]/},t.APOS_STRING_MODE,t.QUOTE_STRING_MODE,d,g,u,t.C_LINE_COMMENT_MODE,t.COMMENT("/\\*\\*","\\*/",{relevance:0,contains:[{className:"doctag",begin:"@[A-Za-z]+",contains:[{className:"type",begin:"\\{",end:"\\}",relevance:0},{className:"variable",begin:i+"(?=\\s*(-)|$)",endsParent:!0,relevance:0},{begin:/(?=[^\n])\s/,relevance:0}]}]}),t.C_BLOCK_COMMENT_MODE,l,{begin:r(/[{,\n]\s*/,s(r(/(((\/\/.*)|(\/\*(.|\n)*\*\/))\s*)*/,i+"\\s*:"))),relevance:0,contains:[{className:"attr",begin:i+s("\\s*:"),relevance:0}]},{begin:"("+t.RE_STARTERS_RE+"|\\b(case|return|throw)\\b)\\s*",keywords:"return throw case",contains:[t.C_LINE_COMMENT_MODE,t.C_BLOCK_COMMENT_MODE,t.REGEXP_MODE,{className:"function",begin:"(\\([^(]*(\\([^(]*(\\([^(]*\\))?\\))?\\)|"+t.UNDERSCORE_IDENT_RE+")\\s*=>",returnBegin:!0,end:"\\s*=>",contains:[{className:"params",variants:[{begin:t.UNDERSCORE_IDENT_RE},{className:null,begin:/\(\s*\)/,skip:!0},{begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:o,contains:b}]}]},{begin:/,/,relevance:0},{className:"",begin:/\s/,end:/\s*/,skip:!0},{variants:[{begin:"<>",end:""},{begin:c.begin,end:c.end}],subLanguage:"xml",contains:[{begin:c.begin,end:c.end,skip:!0,contains:["self"]}]}],relevance:0},{className:"function",beginKeywords:"function",end:/\{/,excludeEnd:!0,contains:[t.inherit(t.TITLE_MODE,{begin:i}),_],illegal:/\[|%/},{begin:/\$[(.]/},t.METHOD_GUARD,{className:"class",beginKeywords:"class",end:/[{;=]/,excludeEnd:!0,illegal:/[:"\[\]]/,contains:[{beginKeywords:"extends"},t.UNDERSCORE_TITLE_MODE]},{beginKeywords:"constructor",end:/\{/,excludeEnd:!0},{begin:"(get|set)\\s+(?="+i+"\\()",end:/{/,keywords:"get set",contains:[t.inherit(t.TITLE_MODE,{begin:i}),{begin:/\(\)/},_]}],illegal:/#(?!!)/}}}());hljs.registerLanguage("typescript",function(){"use strict";const e=["as","in","of","if","for","while","finally","var","new","function","do","return","void","else","break","catch","instanceof","with","throw","case","default","try","switch","continue","typeof","delete","let","yield","const","class","debugger","async","await","static","import","from","export","extends"],n=["true","false","null","undefined","NaN","Infinity"],a=[].concat(["setInterval","setTimeout","clearInterval","clearTimeout","require","exports","eval","isFinite","isNaN","parseFloat","parseInt","decodeURI","decodeURIComponent","encodeURI","encodeURIComponent","escape","unescape"],["arguments","this","super","console","window","document","localStorage","module","global"],["Intl","DataView","Number","Math","Date","String","RegExp","Object","Function","Boolean","Error","Symbol","Set","Map","WeakSet","WeakMap","Proxy","Reflect","JSON","Promise","Float64Array","Int16Array","Int32Array","Int8Array","Uint16Array","Uint32Array","Float32Array","Array","Uint8Array","Uint8ClampedArray","ArrayBuffer"],["EvalError","InternalError","RangeError","ReferenceError","SyntaxError","TypeError","URIError"]);return function(r){var t={$pattern:"[A-Za-z$_][0-9A-Za-z$_]*",keyword:e.concat(["type","namespace","typedef","interface","public","private","protected","implements","declare","abstract","readonly"]).join(" "),literal:n.join(" "),built_in:a.concat(["any","void","number","boolean","string","object","never","enum"]).join(" ")},s={className:"meta",begin:"@[A-Za-z$_][0-9A-Za-z$_]*"},i={className:"number",variants:[{begin:"\\b(0[bB][01]+)n?"},{begin:"\\b(0[oO][0-7]+)n?"},{begin:r.C_NUMBER_RE+"n?"}],relevance:0},o={className:"subst",begin:"\\$\\{",end:"\\}",keywords:t,contains:[]},c={begin:"html`",end:"",starts:{end:"`",returnEnd:!1,contains:[r.BACKSLASH_ESCAPE,o],subLanguage:"xml"}},l={begin:"css`",end:"",starts:{end:"`",returnEnd:!1,contains:[r.BACKSLASH_ESCAPE,o],subLanguage:"css"}},E={className:"string",begin:"`",end:"`",contains:[r.BACKSLASH_ESCAPE,o]};o.contains=[r.APOS_STRING_MODE,r.QUOTE_STRING_MODE,c,l,E,i,r.REGEXP_MODE];var d={begin:"\\(",end:/\)/,keywords:t,contains:["self",r.QUOTE_STRING_MODE,r.APOS_STRING_MODE,r.NUMBER_MODE]},u={className:"params",begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:t,contains:[r.C_LINE_COMMENT_MODE,r.C_BLOCK_COMMENT_MODE,s,d]};return{name:"TypeScript",aliases:["ts"],keywords:t,contains:[r.SHEBANG(),{className:"meta",begin:/^\s*['"]use strict['"]/},r.APOS_STRING_MODE,r.QUOTE_STRING_MODE,c,l,E,r.C_LINE_COMMENT_MODE,r.C_BLOCK_COMMENT_MODE,i,{begin:"("+r.RE_STARTERS_RE+"|\\b(case|return|throw)\\b)\\s*",keywords:"return throw case",contains:[r.C_LINE_COMMENT_MODE,r.C_BLOCK_COMMENT_MODE,r.REGEXP_MODE,{className:"function",begin:"(\\([^(]*(\\([^(]*(\\([^(]*\\))?\\))?\\)|"+r.UNDERSCORE_IDENT_RE+")\\s*=>",returnBegin:!0,end:"\\s*=>",contains:[{className:"params",variants:[{begin:r.UNDERSCORE_IDENT_RE},{className:null,begin:/\(\s*\)/,skip:!0},{begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:t,contains:d.contains}]}]}],relevance:0},{className:"function",beginKeywords:"function",end:/[\{;]/,excludeEnd:!0,keywords:t,contains:["self",r.inherit(r.TITLE_MODE,{begin:"[A-Za-z$_][0-9A-Za-z$_]*"}),u],illegal:/%/,relevance:0},{beginKeywords:"constructor",end:/[\{;]/,excludeEnd:!0,contains:["self",u]},{begin:/module\./,keywords:{built_in:"module"},relevance:0},{beginKeywords:"module",end:/\{/,excludeEnd:!0},{beginKeywords:"interface",end:/\{/,excludeEnd:!0,keywords:"interface extends"},{begin:/\$[(.]/},{begin:"\\."+r.IDENT_RE,relevance:0},s,d]}}}());hljs.registerLanguage("plaintext",function(){"use strict";return function(t){return{name:"Plain text",aliases:["text","txt"],disableAutodetect:!0}}}());hljs.registerLanguage("less",function(){"use strict";return function(e){var n="([\\w-]+|@{[\\w-]+})",a=[],s=[],t=function(e){return{className:"string",begin:"~?"+e+".*?"+e}},r=function(e,n,a){return{className:e,begin:n,relevance:a}},i={begin:"\\(",end:"\\)",contains:s,relevance:0};s.push(e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,t("'"),t('"'),e.CSS_NUMBER_MODE,{begin:"(url|data-uri)\\(",starts:{className:"string",end:"[\\)\\n]",excludeEnd:!0}},r("number","#[0-9A-Fa-f]+\\b"),i,r("variable","@@?[\\w-]+",10),r("variable","@{[\\w-]+}"),r("built_in","~?`[^`]*?`"),{className:"attribute",begin:"[\\w-]+\\s*:",end:":",returnBegin:!0,excludeEnd:!0},{className:"meta",begin:"!important"});var c=s.concat({begin:"{",end:"}",contains:a}),l={beginKeywords:"when",endsWithParent:!0,contains:[{beginKeywords:"and not"}].concat(s)},o={begin:n+"\\s*:",returnBegin:!0,end:"[;}]",relevance:0,contains:[{className:"attribute",begin:n,end:":",excludeEnd:!0,starts:{endsWithParent:!0,illegal:"[<=$]",relevance:0,contains:s}}]},g={className:"keyword",begin:"@(import|media|charset|font-face|(-[a-z]+-)?keyframes|supports|document|namespace|page|viewport|host)\\b",starts:{end:"[;{}]",returnEnd:!0,contains:s,relevance:0}},d={className:"variable",variants:[{begin:"@[\\w-]+\\s*:",relevance:15},{begin:"@[\\w-]+"}],starts:{end:"[;}]",returnEnd:!0,contains:c}},b={variants:[{begin:"[\\.#:&\\[>]",end:"[;{}]"},{begin:n,end:"{"}],returnBegin:!0,returnEnd:!0,illegal:"[<='$\"]",relevance:0,contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,l,r("keyword","all\\b"),r("variable","@{[\\w-]+}"),r("selector-tag",n+"%?",0),r("selector-id","#"+n),r("selector-class","\\."+n,0),r("selector-tag","&",0),{className:"selector-attr",begin:"\\[",end:"\\]"},{className:"selector-pseudo",begin:/:(:)?[a-zA-Z0-9\_\-\+\(\)"'.]+/},{begin:"\\(",end:"\\)",contains:c},{begin:"!important"}]};return a.push(e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,g,d,o,b),{name:"Less",case_insensitive:!0,illegal:"[=>'/<($\"]",contains:a}}}());hljs.registerLanguage("lua",function(){"use strict";return function(e){var t={begin:"\\[=*\\[",end:"\\]=*\\]",contains:["self"]},a=[e.COMMENT("--(?!\\[=*\\[)","$"),e.COMMENT("--\\[=*\\[","\\]=*\\]",{contains:[t],relevance:10})];return{name:"Lua",keywords:{$pattern:e.UNDERSCORE_IDENT_RE,literal:"true false nil",keyword:"and break do else elseif end for goto if in local not or repeat return then until while",built_in:"_G _ENV _VERSION __index __newindex __mode __call __metatable __tostring __len __gc __add __sub __mul __div __mod __pow __concat __unm __eq __lt __le assert collectgarbage dofile error getfenv getmetatable ipairs load loadfile loadstring module next pairs pcall print rawequal rawget rawset require select setfenv setmetatable tonumber tostring type unpack xpcall arg self coroutine resume yield status wrap create running debug getupvalue debug sethook getmetatable gethook setmetatable setlocal traceback setfenv getinfo setupvalue getlocal getregistry getfenv io lines write close flush open output type read stderr stdin input stdout popen tmpfile math log max acos huge ldexp pi cos tanh pow deg tan cosh sinh random randomseed frexp ceil floor rad abs sqrt modf asin min mod fmod log10 atan2 exp sin atan os exit setlocale date getenv difftime remove time clock tmpname rename execute package preload loadlib loaded loaders cpath config path seeall string sub upper len gfind rep find match char dump gmatch reverse byte format gsub lower table setn insert getn foreachi maxn foreach concat sort remove"},contains:a.concat([{className:"function",beginKeywords:"function",end:"\\)",contains:[e.inherit(e.TITLE_MODE,{begin:"([_a-zA-Z]\\w*\\.)*([_a-zA-Z]\\w*:)?[_a-zA-Z]\\w*"}),{className:"params",begin:"\\(",endsWithParent:!0,contains:a}].concat(a)},e.C_NUMBER_MODE,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,{className:"string",begin:"\\[=*\\[",end:"\\]=*\\]",contains:[t],relevance:5}])}}}()); diff --git a/images/Alan.Donovan.png b/images/Alan.Donovan.png new file mode 100644 index 0000000..3101c93 Binary files /dev/null and b/images/Alan.Donovan.png differ diff --git a/images/Brian.W.Kernighan.png b/images/Brian.W.Kernighan.png new file mode 100644 index 0000000..ffb5813 Binary files /dev/null and b/images/Brian.W.Kernighan.png differ diff --git a/images/by-nc-sa-4.0-88x31.png b/images/by-nc-sa-4.0-88x31.png new file mode 100644 index 0000000..79fd9d7 Binary files /dev/null and b/images/by-nc-sa-4.0-88x31.png differ diff --git a/images/ch0-01.png b/images/ch0-01.png new file mode 100644 index 0000000..d868a4e Binary files /dev/null and b/images/ch0-01.png differ diff --git a/images/ch1-01.gif b/images/ch1-01.gif new file mode 100644 index 0000000..866bef1 Binary files /dev/null and b/images/ch1-01.gif differ diff --git a/images/ch1-01.png b/images/ch1-01.png new file mode 100644 index 0000000..69752ec Binary files /dev/null and b/images/ch1-01.png differ diff --git a/images/ch1-02.png b/images/ch1-02.png new file mode 100644 index 0000000..29e7e70 Binary files /dev/null and b/images/ch1-02.png differ diff --git a/images/ch1-03.png b/images/ch1-03.png new file mode 100644 index 0000000..e592b12 Binary files /dev/null and b/images/ch1-03.png differ diff --git a/images/ch10-01.png b/images/ch10-01.png new file mode 100644 index 0000000..e615ba4 Binary files /dev/null and b/images/ch10-01.png differ diff --git a/images/ch11-01.png b/images/ch11-01.png new file mode 100644 index 0000000..25b3872 Binary files /dev/null and b/images/ch11-01.png differ diff --git a/images/ch11-02.png b/images/ch11-02.png new file mode 100644 index 0000000..3d09bab Binary files /dev/null and b/images/ch11-02.png differ diff --git a/images/ch11-03.png b/images/ch11-03.png new file mode 100644 index 0000000..dffde67 Binary files /dev/null and b/images/ch11-03.png differ diff --git a/images/ch11-04.png b/images/ch11-04.png new file mode 100644 index 0000000..4bab69b Binary files /dev/null and b/images/ch11-04.png differ diff --git a/images/ch13-01.png b/images/ch13-01.png new file mode 100644 index 0000000..7208c14 Binary files /dev/null and b/images/ch13-01.png differ diff --git a/images/ch3-01.png b/images/ch3-01.png new file mode 100644 index 0000000..40160c7 Binary files /dev/null and b/images/ch3-01.png differ diff --git a/images/ch3-02.png b/images/ch3-02.png new file mode 100644 index 0000000..c3a31b0 Binary files /dev/null and b/images/ch3-02.png differ diff --git a/images/ch3-03.png b/images/ch3-03.png new file mode 100644 index 0000000..f586d17 Binary files /dev/null and b/images/ch3-03.png differ diff --git a/images/ch3-04.png b/images/ch3-04.png new file mode 100644 index 0000000..48806c6 Binary files /dev/null and b/images/ch3-04.png differ diff --git a/images/ch3-05.png b/images/ch3-05.png new file mode 100644 index 0000000..3424b7e Binary files /dev/null and b/images/ch3-05.png differ diff --git a/images/ch4-01.png b/images/ch4-01.png new file mode 100644 index 0000000..a8d2956 Binary files /dev/null and b/images/ch4-01.png differ diff --git a/images/ch4-02.png b/images/ch4-02.png new file mode 100644 index 0000000..35fa39a Binary files /dev/null and b/images/ch4-02.png differ diff --git a/images/ch4-03.png b/images/ch4-03.png new file mode 100644 index 0000000..e498e21 Binary files /dev/null and b/images/ch4-03.png differ diff --git a/images/ch4-04.png b/images/ch4-04.png new file mode 100644 index 0000000..9410895 Binary files /dev/null and b/images/ch4-04.png differ diff --git a/images/ch4-05.png b/images/ch4-05.png new file mode 100644 index 0000000..b2ad360 Binary files /dev/null and b/images/ch4-05.png differ diff --git a/images/ch4-06.png b/images/ch4-06.png new file mode 100644 index 0000000..c198829 Binary files /dev/null and b/images/ch4-06.png differ diff --git a/images/ch4-xx-01.png b/images/ch4-xx-01.png new file mode 100644 index 0000000..b588fa2 Binary files /dev/null and b/images/ch4-xx-01.png differ diff --git a/images/ch6-xx-00.png b/images/ch6-xx-00.png new file mode 100644 index 0000000..22a0f3c Binary files /dev/null and b/images/ch6-xx-00.png differ diff --git a/images/ch7-01.png b/images/ch7-01.png new file mode 100644 index 0000000..e3d88d1 Binary files /dev/null and b/images/ch7-01.png differ diff --git a/images/ch7-02.png b/images/ch7-02.png new file mode 100644 index 0000000..b10c60a Binary files /dev/null and b/images/ch7-02.png differ diff --git a/images/ch7-03.png b/images/ch7-03.png new file mode 100644 index 0000000..4c1a68b Binary files /dev/null and b/images/ch7-03.png differ diff --git a/images/ch7-04.png b/images/ch7-04.png new file mode 100644 index 0000000..26320cc Binary files /dev/null and b/images/ch7-04.png differ diff --git a/images/ch7-05.png b/images/ch7-05.png new file mode 100644 index 0000000..3c9b827 Binary files /dev/null and b/images/ch7-05.png differ diff --git a/images/ch7-06.png b/images/ch7-06.png new file mode 100644 index 0000000..1b12e9a Binary files /dev/null and b/images/ch7-06.png differ diff --git a/images/ch7-07.png b/images/ch7-07.png new file mode 100644 index 0000000..2844dcc Binary files /dev/null and b/images/ch7-07.png differ diff --git a/images/ch8-01.png b/images/ch8-01.png new file mode 100644 index 0000000..e509831 Binary files /dev/null and b/images/ch8-01.png differ diff --git a/images/ch8-02.png b/images/ch8-02.png new file mode 100644 index 0000000..f19a1ab Binary files /dev/null and b/images/ch8-02.png differ diff --git a/images/ch8-03.png b/images/ch8-03.png new file mode 100644 index 0000000..4bdef9c Binary files /dev/null and b/images/ch8-03.png differ diff --git a/images/ch8-04.png b/images/ch8-04.png new file mode 100644 index 0000000..e650400 Binary files /dev/null and b/images/ch8-04.png differ diff --git a/images/ch8-05.png b/images/ch8-05.png new file mode 100644 index 0000000..fa68f81 Binary files /dev/null and b/images/ch8-05.png differ diff --git a/images/cover.png b/images/cover.png new file mode 100644 index 0000000..c18ebc4 Binary files /dev/null and b/images/cover.png differ diff --git a/images/favicon.ico b/images/favicon.ico new file mode 100644 index 0000000..d287722 Binary files /dev/null and b/images/favicon.ico differ diff --git a/images/go-log04.png b/images/go-log04.png new file mode 100644 index 0000000..31f1ee9 Binary files /dev/null and b/images/go-log04.png differ diff --git a/images/gopher/go_lang_mascot_by_kirael_art-d7kunhu.gif b/images/gopher/go_lang_mascot_by_kirael_art-d7kunhu.gif new file mode 100644 index 0000000..782df9b Binary files /dev/null and b/images/gopher/go_lang_mascot_by_kirael_art-d7kunhu.gif differ diff --git a/images/logo/bitbucket.png b/images/logo/bitbucket.png new file mode 100644 index 0000000..ed5aa05 Binary files /dev/null and b/images/logo/bitbucket.png differ diff --git a/images/logo/github.png b/images/logo/github.png new file mode 100644 index 0000000..be64b0b Binary files /dev/null and b/images/logo/github.png differ diff --git a/images/logo/gopher-china.png b/images/logo/gopher-china.png new file mode 100644 index 0000000..bf21cde Binary files /dev/null and b/images/logo/gopher-china.png differ diff --git a/index.html b/index.html new file mode 100644 index 0000000..9d374d6 --- /dev/null +++ b/index.html @@ -0,0 +1,246 @@ + + + + + + Go语言圣经 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

Go语言圣经(中文版)

+

Go语言圣经 《The Go Programming Language》 中文版本,仅供学习交流之用。对于希望学习CGO、Go汇编语言等高级用法的同学,我们推荐《Go语言高级编程》开源图书。如果希望深入学习Go语言语法树结构,可以参考《Go语法树入门——开启自制编程语言和编译器之旅》。如果想从头实现一个玩具Go语言可以参考《从头实现µGo语言》

+

+ +

译者信息:

+ + + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/js/bigPicture.js b/js/bigPicture.js new file mode 100644 index 0000000..41cdd1f --- /dev/null +++ b/js/bigPicture.js @@ -0,0 +1 @@ +var BigPicture=function(){var t,n,e,o,i,r,a,c,p,s,l,d,u,f,m,b,g,h,x,v,y,w,_,T,k,M,S,L,E,A,H,z,I,C=[],D={},O="appendChild",N="createElement",V="removeChild";function W(){var n=t.getBoundingClientRect();return"transform:translate3D("+(n.left-(e.clientWidth-n.width)/2)+"px, "+(n.top-(e.clientHeight-n.height)/2)+"px, 0) scale3D("+t.clientWidth/o.clientWidth+", "+t.clientHeight/o.clientHeight+", 0)"}function q(t){var n=A.length-1;if(!u){if(t>0&&E===n||t<0&&!E){if(!I.loop)return j(i,""),void setTimeout(j,9,i,"animation:"+(t>0?"bpl":"bpf")+" .3s;transition:transform .35s");E=t>0?-1:n+1}if([(E=Math.max(0,Math.min(E+t,n)))-1,E,E+1].forEach(function(t){if(t=Math.max(0,Math.min(t,n)),!D[t]){var e=A[t].src,o=document[N]("IMG");o.addEventListener("load",F.bind(null,e)),o.src=e,D[t]=o}}),D[E].complete)return B(t);u=1,j(m,"opacity:.4;"),e[O](m),D[E].onload=function(){y&&B(t)},D[E].onerror=function(){A[E]={error:"Error loading image"},y&&B(t)}}}function B(n){u&&(e[V](m),u=0);var r=A[E];if(r.error)alert(r.error);else{var a=e.querySelector("img:last-of-type");j(i=o=D[E],"animation:"+(n>0?"bpfl":"bpfr")+" .35s;transition:transform .35s"),j(a,"animation:"+(n>0?"bpfol":"bpfor")+" .35s both"),e[O](i),r.el&&(t=r.el)}H.innerHTML=E+1+"/"+A.length,X(A[E].caption),M&&M([i,A[E]])}function P(){var t,n,e=.95*window.innerHeight,o=.95*window.innerWidth,i=I.dimensions||[1920,1080],r=i[0],a=i[1],p=a/r;p>e/o?n=(t=Math.min(a,e))/p:t=(n=Math.min(r,o))*p,c.style.cssText+="width:"+n+"px;height:"+t+"px;"}function G(t){~[1,4].indexOf(o.readyState)?(U(),setTimeout(function(){o.play()},99)):o.error?U(t):f=setTimeout(G,35,t)}function R(n){I.noLoader||(n&&j(m,"top:"+t.offsetTop+"px;left:"+t.offsetLeft+"px;height:"+t.clientHeight+"px;width:"+t.clientWidth+"px"),t.parentElement[n?O:V](m),u=n)}function X(t){t&&(g.innerHTML=t),j(b,"opacity:"+(t?"1;pointer-events:auto":"0"))}function F(t){!~C.indexOf(t)&&C.push(t)}function U(t){if(u&&R(),T&&T(),"string"==typeof t)return $(),I.onError?I.onError():alert("Error: The requested "+t+" could not be loaded.");_&&F(s),o.style.cssText+=W(),j(e,"opacity:1;pointer-events:auto"),k=setTimeout(k,410),v=1,y=!!A,setTimeout(function(){o.style.cssText+="transition:transform .35s;transform:none",h&&setTimeout(X,250,h)},60)}function Y(t){var n=t?t.target:e,i=[b,x,r,a,g,L,S,m];n.blur(),w||~i.indexOf(n)||(o.style.cssText+=W(),j(e,"pointer-events:auto"),setTimeout($,350),clearTimeout(k),v=0,w=1)}function $(){if((o===c?p:o).removeAttribute("src"),document.body[V](e),e[V](o),j(e,""),j(o,""),X(0),y){for(var t=e.querySelectorAll("img"),n=0;n',n}function d(t,n){var e=document[N]("button");return e.className="bp-lr",e.innerHTML='',j(e,n),e.onclick=function(n){n.stopPropagation(),q(t)},e}var f=document[N]("STYLE");f.innerHTML="#bp_caption,#bp_container{bottom:0;left:0;right:0;position:fixed;opacity:0}#bp_container>*,#bp_loader{position:absolute;right:0;z-index:10}#bp_container,#bp_caption,#bp_container svg{pointer-events:none}#bp_container{top:0;z-index:9999;background:rgba(0,0,0,.7);opacity:0;transition:opacity .35s}#bp_loader{top:0;left:0;bottom:0;display:flex;align-items:center;cursor:wait;background:0;z-index:9}#bp_loader svg{width:50%;max-width:300px;max-height:50%;margin:auto;animation:bpturn 1s infinite linear}#bp_aud,#bp_container img,#bp_sv,#bp_vid{user-select:none;max-height:96%;max-width:96%;top:0;bottom:0;left:0;margin:auto;box-shadow:0 0 3em rgba(0,0,0,.4);z-index:-1}#bp_sv{background:#111}#bp_sv svg{width:66px}#bp_caption{font-size:.9em;padding:1.3em;background:rgba(15,15,15,.94);color:#fff;text-align:center;transition:opacity .3s}#bp_aud{width:650px;top:calc(50% - 20px);bottom:auto;box-shadow:none}#bp_count{left:0;right:auto;padding:14px;color:rgba(255,255,255,.7);font-size:22px;cursor:default}#bp_container button{position:absolute;border:0;outline:0;background:0;cursor:pointer;transition:all .1s}#bp_container>.bp-x{padding:0;height:41px;width:41px;border-radius:100%;top:8px;right:14px;opacity:.8;line-height:1}#bp_container>.bp-x:focus,#bp_container>.bp-x:hover{background:rgba(255,255,255,.2)}.bp-x svg,.bp-xc svg{height:21px;width:20px;fill:#fff;vertical-align:top;}.bp-xc svg{width:16px}#bp_container .bp-xc{left:2%;bottom:100%;padding:9px 20px 7px;background:#d04444;border-radius:2px 2px 0 0;opacity:.85}#bp_container .bp-xc:focus,#bp_container .bp-xc:hover{opacity:1}.bp-lr{top:50%;top:calc(50% - 130px);padding:99px 0;width:6%;background:0;border:0;opacity:.4;transition:opacity .1s}.bp-lr:focus,.bp-lr:hover{opacity:.8}@keyframes bpf{50%{transform:translatex(15px)}100%{transform:none}}@keyframes bpl{50%{transform:translatex(-15px)}100%{transform:none}}@keyframes bpfl{0%{opacity:0;transform:translatex(70px)}100%{opacity:1;transform:none}}@keyframes bpfr{0%{opacity:0;transform:translatex(-70px)}100%{opacity:1;transform:none}}@keyframes bpfol{0%{opacity:1;transform:none}100%{opacity:0;transform:translatex(-70px)}}@keyframes bpfor{0%{opacity:1;transform:none}100%{opacity:0;transform:translatex(70px)}}@keyframes bpturn{0%{transform:none}100%{transform:rotate(360deg)}}@media (max-width:600px){.bp-lr{font-size:15vw}}",document.head[O](f),(e=document[N]("DIV")).id="bp_container",e.onclick=Y,l=s("bp-x"),e[O](l),"ontouchstart"in window&&(z=1,e.ontouchstart=function(n){var e=n.changedTouches;t=e[0].pageX},e.ontouchmove=function(t){t.preventDefault()},e.ontouchend=function(n){var e=n.changedTouches;if(y){var o=e[0].pageX-t;o<-30&&q(1),o>30&&q(-1)}}),i=document[N]("IMG"),(r=document[N]("VIDEO")).id="bp_vid",r.setAttribute("playsinline",1),r.controls=1,r.loop=1,(a=document[N]("audio")).id="bp_aud",a.controls=1,a.loop=1,(H=document[N]("span")).id="bp_count",(b=document[N]("DIV")).id="bp_caption",(x=s("bp-xc")).onclick=X.bind(null,0),b[O](x),g=document[N]("SPAN"),b[O](g),e[O](b),S=d(1,"transform:scalex(-1)"),L=d(-1,"left:0;right:auto"),(m=document[N]("DIV")).id="bp_loader",m.innerHTML='',(c=document[N]("DIV")).id="bp_sv",(p=document[N]("IFRAME")).setAttribute("allowfullscreen",1),p.allow="autoplay; fullscreen",p.onload=function(){return c[V](m)},j(p,"border:0;position:absolute;height:100%;width:100%;left:0;top:0"),c[O](p),i.onload=U,i.onerror=U.bind(null,"image"),window.addEventListener("resize",function(){y||u&&R(1),o===c&&P()}),document.addEventListener("keyup",function(t){var n=t.keyCode;27===n&&v&&Y(),y&&(39===n&&q(1),37===n&&q(-1),38===n&&q(10),40===n&&q(-10))}),document.addEventListener("keydown",function(t){y&&~[37,38,39,40].indexOf(t.keyCode)&&t.preventDefault()}),document.addEventListener("focus",function(t){v&&!e.contains(t.target)&&(t.stopPropagation(),l.focus())},1),n=1}(),u&&(clearTimeout(f),$()),I=w,d=w.ytSrc||w.vimeoSrc,T=w.animationStart,k=w.animationEnd,M=w.onChangeImage,_=0,h=(t=w.el).getAttribute("data-caption"),w.gallery?function(n,r){var a=I.galleryAttribute||"data-bp";if(Array.isArray(n))A=n,h=n[E=r||0].caption;else{var c=(A=[].slice.call("string"==typeof n?document.querySelectorAll(n+" ["+a+"]"):n)).indexOf(t);E=0===r||r?r:-1!==c?c:0,A=A.map(function(t){return{el:t,src:t.getAttribute(a),caption:t.getAttribute("data-caption")}})}_=1,!~C.indexOf(s=A[E].src)&&R(1),A.length>1?(e[O](H),H.innerHTML=E+1+"/"+A.length,z||(e[O](S),e[O](L))):A=0,(o=i).src=s}(w.gallery,w.position):d||w.iframeSrc?(o=c,I.ytSrc?W="https://www.youtube.com/embed/"+d+"?html5=1&rel=0&playsinline=1&autoplay=1":I.vimeoSrc?W="https://player.vimeo.com/video/"+d+"?autoplay=1":I.iframeSrc&&(W=I.iframeSrc),j(m,""),c[O](m),p.src=W,P(),setTimeout(U,9)):w.imgSrc?(_=1,!~C.indexOf(s=w.imgSrc)&&R(1),(o=i).src=s):w.audio?(R(1),(o=a).src=w.audio,G("audio file")):w.vidSrc?(R(1),w.dimensions&&j(r,"width:"+w.dimensions[0]+"px"),D=w.vidSrc,Array.isArray(D)?(o=r.cloneNode(),D.forEach(function(t){var n=document[N]("SOURCE");n.src=t,n.type="video/"+t.match(/.(\w+)$/)[1],o[O](n)})):(o=r).src=D,G("video")):(o=i).src="IMG"===t.tagName?t.src:window.getComputedStyle(t).backgroundImage.replace(/^url|[(|)|'|"]/g,""),e[O](o),document.body[O](e),{close:Y,next:function(){return q(1)},prev:function(){return q(-1)}};var W}}(); diff --git a/js/custom.js b/js/custom.js new file mode 100644 index 0000000..3a5ca26 --- /dev/null +++ b/js/custom.js @@ -0,0 +1,146 @@ +// https://giscus.app + +const data_repo = "gopl-zh/gopl-zh.github.com"; +const data_repo_id = "MDEwOlJlcG9zaXRvcnk2MTUzMTQ2Mw=="; +const data_category = "General"; +const data_category_id = "DIC_kwDOA6rlR84CQnJW"; + +var initAll = function () { + var path = window.location.pathname; + if (path.endsWith("/print.html")) { + return; + } + + var images = document.querySelectorAll("main img") + Array.prototype.forEach.call(images, function (img) { + img.addEventListener("click", function () { + BigPicture({ + el: img, + }); + }); + }); + + // Un-active everything when you click it + Array.prototype.forEach.call(document.getElementsByClassName("pagetoc")[0].children, function (el) { + el.addEventHandler("click", function () { + Array.prototype.forEach.call(document.getElementsByClassName("pagetoc")[0].children, function (el) { + el.classList.remove("active"); + }); + el.classList.add("active"); + }); + }); + + var updateFunction = function () { + var id = null; + var elements = document.getElementsByClassName("header"); + Array.prototype.forEach.call(elements, function (el) { + if (window.pageYOffset >= el.offsetTop) { + id = el; + } + }); + + Array.prototype.forEach.call(document.getElementsByClassName("pagetoc")[0].children, function (el) { + el.classList.remove("active"); + }); + + Array.prototype.forEach.call(document.getElementsByClassName("pagetoc")[0].children, function (el) { + if (id == null) { + return; + } + if (id.href.localeCompare(el.href) == 0) { + el.classList.add("active"); + } + }); + }; + + var pagetoc = document.getElementsByClassName("pagetoc")[0]; + var elements = document.getElementsByClassName("header"); + Array.prototype.forEach.call(elements, function (el) { + var link = document.createElement("a"); + + // Indent shows hierarchy + var indent = ""; + switch (el.parentElement.tagName) { + case "H1": + return; + case "H3": + indent = "20px"; + break; + case "H4": + indent = "40px"; + break; + default: + break; + } + + link.appendChild(document.createTextNode(el.text)); + link.style.paddingLeft = indent; + link.href = el.href; + pagetoc.appendChild(link); + }); + updateFunction.call(); + + // Handle active elements on scroll + window.addEventListener("scroll", updateFunction); + + document.getElementById("theme-list").addEventListener("click", function (e) { + var iframe = document.querySelector('.giscus-frame'); + if (!iframe) return; + var theme; + if (e.target.className === "theme") { + theme = e.target.id; + } else { + return; + } + + // 若当前 mdbook 主题不是 Light 或 Rust ,则将 giscuz 主题设置为 transparent_dark + var giscusTheme = "light" + if (theme != "light" && theme != "rust") { + giscusTheme = "transparent_dark"; + } + + var msg = { + setConfig: { + theme: giscusTheme + } + }; + iframe.contentWindow.postMessage({ giscus: msg }, 'https://giscus.app'); + }); + + pagePath = pagePath.replace("index.md", ""); + pagePath = pagePath.replace(".md", ""); + if (pagePath.length > 0) { + if (pagePath.charAt(pagePath.length-1) == "/"){ + pagePath = pagePath.substring(0, pagePath.length-1); + } + }else { + pagePath = "index"; + } + + var giscusTheme = "light"; + const themeClass = document.getElementsByTagName("html")[0].className; + if (themeClass.indexOf("light") == -1 && themeClass.indexOf("rust") == -1) { + giscusTheme = "transparent_dark"; + } + + var script = document.createElement("script"); + script.type = "text/javascript"; + script.src = "https://giscus.app/client.js"; + script.async = true; + script.crossOrigin = "anonymous"; + script.setAttribute("data-repo", data_repo); + script.setAttribute("data-repo-id", data_repo_id); + script.setAttribute("data-category", data_category); + script.setAttribute("data-category-id", data_category_id); + script.setAttribute("data-mapping", "specific"); + script.setAttribute("data-term", pagePath); + script.setAttribute("data-reactions-enabled", "1"); + script.setAttribute("data-emit-metadata", "0"); + script.setAttribute("data-input-position", "top"); + script.setAttribute("data-theme", giscusTheme); + script.setAttribute("data-lang", "zh-CN"); + script.setAttribute("data-loading", "lazy"); + document.getElementById("giscus-container").appendChild(script); +}; + +window.addEventListener('load', initAll); diff --git a/make.bat b/make.bat new file mode 100644 index 0000000..61a62e8 --- /dev/null +++ b/make.bat @@ -0,0 +1,9 @@ +@echo off +cls +setlocal EnableDelayedExpansion +rem gitbook install +rem &^ 批处理运行gitbook会中断命令 所以用&链接,用^处理换行 +go run update_version.go +gitbook build &^ +go run fix-data-revision.go &^ +go run builder.go \ No newline at end of file diff --git a/mark.min.js b/mark.min.js new file mode 100644 index 0000000..1636231 --- /dev/null +++ b/mark.min.js @@ -0,0 +1,7 @@ +/*!*************************************************** +* mark.js v8.11.1 +* https://markjs.io/ +* Copyright (c) 2014–2018, Julian Kühnel +* Released under the MIT license https://git.io/vwTVl +*****************************************************/ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):e.Mark=t()}(this,function(){"use strict";var e="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},t=function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")},n=function(){function e(e,t){for(var n=0;n1&&void 0!==arguments[1])||arguments[1],i=arguments.length>2&&void 0!==arguments[2]?arguments[2]:[],o=arguments.length>3&&void 0!==arguments[3]?arguments[3]:5e3;t(this,e),this.ctx=n,this.iframes=r,this.exclude=i,this.iframesTimeout=o}return n(e,[{key:"getContexts",value:function(){var e=[];return(void 0!==this.ctx&&this.ctx?NodeList.prototype.isPrototypeOf(this.ctx)?Array.prototype.slice.call(this.ctx):Array.isArray(this.ctx)?this.ctx:"string"==typeof this.ctx?Array.prototype.slice.call(document.querySelectorAll(this.ctx)):[this.ctx]:[]).forEach(function(t){var n=e.filter(function(e){return e.contains(t)}).length>0;-1!==e.indexOf(t)||n||e.push(t)}),e}},{key:"getIframeContents",value:function(e,t){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:function(){},r=void 0;try{var i=e.contentWindow;if(r=i.document,!i||!r)throw new Error("iframe inaccessible")}catch(e){n()}r&&t(r)}},{key:"isIframeBlank",value:function(e){var t="about:blank",n=e.getAttribute("src").trim();return e.contentWindow.location.href===t&&n!==t&&n}},{key:"observeIframeLoad",value:function(e,t,n){var r=this,i=!1,o=null,a=function a(){if(!i){i=!0,clearTimeout(o);try{r.isIframeBlank(e)||(e.removeEventListener("load",a),r.getIframeContents(e,t,n))}catch(e){n()}}};e.addEventListener("load",a),o=setTimeout(a,this.iframesTimeout)}},{key:"onIframeReady",value:function(e,t,n){try{"complete"===e.contentWindow.document.readyState?this.isIframeBlank(e)?this.observeIframeLoad(e,t,n):this.getIframeContents(e,t,n):this.observeIframeLoad(e,t,n)}catch(e){n()}}},{key:"waitForIframes",value:function(e,t){var n=this,r=0;this.forEachIframe(e,function(){return!0},function(e){r++,n.waitForIframes(e.querySelector("html"),function(){--r||t()})},function(e){e||t()})}},{key:"forEachIframe",value:function(t,n,r){var i=this,o=arguments.length>3&&void 0!==arguments[3]?arguments[3]:function(){},a=t.querySelectorAll("iframe"),s=a.length,c=0;a=Array.prototype.slice.call(a);var u=function(){--s<=0&&o(c)};s||u(),a.forEach(function(t){e.matches(t,i.exclude)?u():i.onIframeReady(t,function(e){n(t)&&(c++,r(e)),u()},u)})}},{key:"createIterator",value:function(e,t,n){return document.createNodeIterator(e,t,n,!1)}},{key:"createInstanceOnIframe",value:function(t){return new e(t.querySelector("html"),this.iframes)}},{key:"compareNodeIframe",value:function(e,t,n){if(e.compareDocumentPosition(n)&Node.DOCUMENT_POSITION_PRECEDING){if(null===t)return!0;if(t.compareDocumentPosition(n)&Node.DOCUMENT_POSITION_FOLLOWING)return!0}return!1}},{key:"getIteratorNode",value:function(e){var t=e.previousNode();return{prevNode:t,node:null===t?e.nextNode():e.nextNode()&&e.nextNode()}}},{key:"checkIframeFilter",value:function(e,t,n,r){var i=!1,o=!1;return r.forEach(function(e,t){e.val===n&&(i=t,o=e.handled)}),this.compareNodeIframe(e,t,n)?(!1!==i||o?!1===i||o||(r[i].handled=!0):r.push({val:n,handled:!0}),!0):(!1===i&&r.push({val:n,handled:!1}),!1)}},{key:"handleOpenIframes",value:function(e,t,n,r){var i=this;e.forEach(function(e){e.handled||i.getIframeContents(e.val,function(e){i.createInstanceOnIframe(e).forEachNode(t,n,r)})})}},{key:"iterateThroughNodes",value:function(e,t,n,r,i){for(var o,a=this,s=this.createIterator(t,e,r),c=[],u=[],l=void 0,h=void 0;void 0,o=a.getIteratorNode(s),h=o.prevNode,l=o.node;)this.iframes&&this.forEachIframe(t,function(e){return a.checkIframeFilter(l,h,e,c)},function(t){a.createInstanceOnIframe(t).forEachNode(e,function(e){return u.push(e)},r)}),u.push(l);u.forEach(function(e){n(e)}),this.iframes&&this.handleOpenIframes(c,e,n,r),i()}},{key:"forEachNode",value:function(e,t,n){var r=this,i=arguments.length>3&&void 0!==arguments[3]?arguments[3]:function(){},o=this.getContexts(),a=o.length;a||i(),o.forEach(function(o){var s=function(){r.iterateThroughNodes(e,o,t,n,function(){--a<=0&&i()})};r.iframes?r.waitForIframes(o,s):s()})}}],[{key:"matches",value:function(e,t){var n="string"==typeof t?[t]:t,r=e.matches||e.matchesSelector||e.msMatchesSelector||e.mozMatchesSelector||e.oMatchesSelector||e.webkitMatchesSelector;if(r){var i=!1;return n.every(function(t){return!r.call(e,t)||(i=!0,!1)}),i}return!1}}]),e}(),o=function(){function e(n){t(this,e),this.opt=r({},{diacritics:!0,synonyms:{},accuracy:"partially",caseSensitive:!1,ignoreJoiners:!1,ignorePunctuation:[],wildcards:"disabled"},n)}return n(e,[{key:"create",value:function(e){return"disabled"!==this.opt.wildcards&&(e=this.setupWildcardsRegExp(e)),e=this.escapeStr(e),Object.keys(this.opt.synonyms).length&&(e=this.createSynonymsRegExp(e)),(this.opt.ignoreJoiners||this.opt.ignorePunctuation.length)&&(e=this.setupIgnoreJoinersRegExp(e)),this.opt.diacritics&&(e=this.createDiacriticsRegExp(e)),e=this.createMergedBlanksRegExp(e),(this.opt.ignoreJoiners||this.opt.ignorePunctuation.length)&&(e=this.createJoinersRegExp(e)),"disabled"!==this.opt.wildcards&&(e=this.createWildcardsRegExp(e)),e=this.createAccuracyRegExp(e),new RegExp(e,"gm"+(this.opt.caseSensitive?"":"i"))}},{key:"escapeStr",value:function(e){return e.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g,"\\$&")}},{key:"createSynonymsRegExp",value:function(e){var t=this.opt.synonyms,n=this.opt.caseSensitive?"":"i",r=this.opt.ignoreJoiners||this.opt.ignorePunctuation.length?"\0":"";for(var i in t)if(t.hasOwnProperty(i)){var o=t[i],a="disabled"!==this.opt.wildcards?this.setupWildcardsRegExp(i):this.escapeStr(i),s="disabled"!==this.opt.wildcards?this.setupWildcardsRegExp(o):this.escapeStr(o);""!==a&&""!==s&&(e=e.replace(new RegExp("("+this.escapeStr(a)+"|"+this.escapeStr(s)+")","gm"+n),r+"("+this.processSynonyms(a)+"|"+this.processSynonyms(s)+")"+r))}return e}},{key:"processSynonyms",value:function(e){return(this.opt.ignoreJoiners||this.opt.ignorePunctuation.length)&&(e=this.setupIgnoreJoinersRegExp(e)),e}},{key:"setupWildcardsRegExp",value:function(e){return(e=e.replace(/(?:\\)*\?/g,function(e){return"\\"===e.charAt(0)?"?":""})).replace(/(?:\\)*\*/g,function(e){return"\\"===e.charAt(0)?"*":""})}},{key:"createWildcardsRegExp",value:function(e){var t="withSpaces"===this.opt.wildcards;return e.replace(/\u0001/g,t?"[\\S\\s]?":"\\S?").replace(/\u0002/g,t?"[\\S\\s]*?":"\\S*")}},{key:"setupIgnoreJoinersRegExp",value:function(e){return e.replace(/[^(|)\\]/g,function(e,t,n){var r=n.charAt(t+1);return/[(|)\\]/.test(r)||""===r?e:e+"\0"})}},{key:"createJoinersRegExp",value:function(e){var t=[],n=this.opt.ignorePunctuation;return Array.isArray(n)&&n.length&&t.push(this.escapeStr(n.join(""))),this.opt.ignoreJoiners&&t.push("\\u00ad\\u200b\\u200c\\u200d"),t.length?e.split(/\u0000+/).join("["+t.join("")+"]*"):e}},{key:"createDiacriticsRegExp",value:function(e){var t=this.opt.caseSensitive?"":"i",n=this.opt.caseSensitive?["aàáảãạăằắẳẵặâầấẩẫậäåāą","AÀÁẢÃẠĂẰẮẲẴẶÂẦẤẨẪẬÄÅĀĄ","cçćč","CÇĆČ","dđď","DĐĎ","eèéẻẽẹêềếểễệëěēę","EÈÉẺẼẸÊỀẾỂỄỆËĚĒĘ","iìíỉĩịîïī","IÌÍỈĨỊÎÏĪ","lł","LŁ","nñňń","NÑŇŃ","oòóỏõọôồốổỗộơởỡớờợöøō","OÒÓỎÕỌÔỒỐỔỖỘƠỞỠỚỜỢÖØŌ","rř","RŘ","sšśșş","SŠŚȘŞ","tťțţ","TŤȚŢ","uùúủũụưừứửữựûüůū","UÙÚỦŨỤƯỪỨỬỮỰÛÜŮŪ","yýỳỷỹỵÿ","YÝỲỶỸỴŸ","zžżź","ZŽŻŹ"]:["aàáảãạăằắẳẵặâầấẩẫậäåāąAÀÁẢÃẠĂẰẮẲẴẶÂẦẤẨẪẬÄÅĀĄ","cçćčCÇĆČ","dđďDĐĎ","eèéẻẽẹêềếểễệëěēęEÈÉẺẼẸÊỀẾỂỄỆËĚĒĘ","iìíỉĩịîïīIÌÍỈĨỊÎÏĪ","lłLŁ","nñňńNÑŇŃ","oòóỏõọôồốổỗộơởỡớờợöøōOÒÓỎÕỌÔỒỐỔỖỘƠỞỠỚỜỢÖØŌ","rřRŘ","sšśșşSŠŚȘŞ","tťțţTŤȚŢ","uùúủũụưừứửữựûüůūUÙÚỦŨỤƯỪỨỬỮỰÛÜŮŪ","yýỳỷỹỵÿYÝỲỶỸỴŸ","zžżźZŽŻŹ"],r=[];return e.split("").forEach(function(i){n.every(function(n){if(-1!==n.indexOf(i)){if(r.indexOf(n)>-1)return!1;e=e.replace(new RegExp("["+n+"]","gm"+t),"["+n+"]"),r.push(n)}return!0})}),e}},{key:"createMergedBlanksRegExp",value:function(e){return e.replace(/[\s]+/gim,"[\\s]+")}},{key:"createAccuracyRegExp",value:function(e){var t=this,n=this.opt.accuracy,r="string"==typeof n?n:n.value,i="";switch(("string"==typeof n?[]:n.limiters).forEach(function(e){i+="|"+t.escapeStr(e)}),r){case"partially":default:return"()("+e+")";case"complementary":return"()([^"+(i="\\s"+(i||this.escapeStr("!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~¡¿")))+"]*"+e+"[^"+i+"]*)";case"exactly":return"(^|\\s"+i+")("+e+")(?=$|\\s"+i+")"}}}]),e}(),a=function(){function a(e){t(this,a),this.ctx=e,this.ie=!1;var n=window.navigator.userAgent;(n.indexOf("MSIE")>-1||n.indexOf("Trident")>-1)&&(this.ie=!0)}return n(a,[{key:"log",value:function(t){var n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"debug",r=this.opt.log;this.opt.debug&&"object"===(void 0===r?"undefined":e(r))&&"function"==typeof r[n]&&r[n]("mark.js: "+t)}},{key:"getSeparatedKeywords",value:function(e){var t=this,n=[];return e.forEach(function(e){t.opt.separateWordSearch?e.split(" ").forEach(function(e){e.trim()&&-1===n.indexOf(e)&&n.push(e)}):e.trim()&&-1===n.indexOf(e)&&n.push(e)}),{keywords:n.sort(function(e,t){return t.length-e.length}),length:n.length}}},{key:"isNumeric",value:function(e){return Number(parseFloat(e))==e}},{key:"checkRanges",value:function(e){var t=this;if(!Array.isArray(e)||"[object Object]"!==Object.prototype.toString.call(e[0]))return this.log("markRanges() will only accept an array of objects"),this.opt.noMatch(e),[];var n=[],r=0;return e.sort(function(e,t){return e.start-t.start}).forEach(function(e){var i=t.callNoMatchOnInvalidRanges(e,r),o=i.start,a=i.end;i.valid&&(e.start=o,e.length=a-o,n.push(e),r=a)}),n}},{key:"callNoMatchOnInvalidRanges",value:function(e,t){var n=void 0,r=void 0,i=!1;return e&&void 0!==e.start?(r=(n=parseInt(e.start,10))+parseInt(e.length,10),this.isNumeric(e.start)&&this.isNumeric(e.length)&&r-t>0&&r-n>0?i=!0:(this.log("Ignoring invalid or overlapping range: "+JSON.stringify(e)),this.opt.noMatch(e))):(this.log("Ignoring invalid range: "+JSON.stringify(e)),this.opt.noMatch(e)),{start:n,end:r,valid:i}}},{key:"checkWhitespaceRanges",value:function(e,t,n){var r=void 0,i=!0,o=n.length,a=t-o,s=parseInt(e.start,10)-a;return(r=(s=s>o?o:s)+parseInt(e.length,10))>o&&(r=o,this.log("End range automatically set to the max value of "+o)),s<0||r-s<0||s>o||r>o?(i=!1,this.log("Invalid range: "+JSON.stringify(e)),this.opt.noMatch(e)):""===n.substring(s,r).replace(/\s+/g,"")&&(i=!1,this.log("Skipping whitespace only range: "+JSON.stringify(e)),this.opt.noMatch(e)),{start:s,end:r,valid:i}}},{key:"getTextNodes",value:function(e){var t=this,n="",r=[];this.iterator.forEachNode(NodeFilter.SHOW_TEXT,function(e){r.push({start:n.length,end:(n+=e.textContent).length,node:e})},function(e){return t.matchesExclude(e.parentNode)?NodeFilter.FILTER_REJECT:NodeFilter.FILTER_ACCEPT},function(){e({value:n,nodes:r})})}},{key:"matchesExclude",value:function(e){return i.matches(e,this.opt.exclude.concat(["script","style","title","head","html"]))}},{key:"wrapRangeInTextNode",value:function(e,t,n){var r=this.opt.element?this.opt.element:"mark",i=e.splitText(t),o=i.splitText(n-t),a=document.createElement(r);return a.setAttribute("data-markjs","true"),this.opt.className&&a.setAttribute("class",this.opt.className),a.textContent=i.textContent,i.parentNode.replaceChild(a,i),o}},{key:"wrapRangeInMappedTextNode",value:function(e,t,n,r,i){var o=this;e.nodes.every(function(a,s){var c=e.nodes[s+1];if(void 0===c||c.start>t){if(!r(a.node))return!1;var u=t-a.start,l=(n>a.end?a.end:n)-a.start,h=e.value.substr(0,a.start),f=e.value.substr(l+a.start);if(a.node=o.wrapRangeInTextNode(a.node,u,l),e.value=h+f,e.nodes.forEach(function(t,n){n>=s&&(e.nodes[n].start>0&&n!==s&&(e.nodes[n].start-=l),e.nodes[n].end-=l)}),n-=l,i(a.node.previousSibling,a.start),!(n>a.end))return!1;t=a.end}return!0})}},{key:"wrapGroups",value:function(e,t,n,r){return r((e=this.wrapRangeInTextNode(e,t,t+n)).previousSibling),e}},{key:"separateGroups",value:function(e,t,n,r,i){for(var o=t.length,a=1;a-1&&r(t[a],e)&&(e=this.wrapGroups(e,s,t[a].length,i))}return e}},{key:"wrapMatches",value:function(e,t,n,r,i){var o=this,a=0===t?0:t+1;this.getTextNodes(function(t){t.nodes.forEach(function(t){t=t.node;for(var i=void 0;null!==(i=e.exec(t.textContent))&&""!==i[a];){if(o.opt.separateGroups)t=o.separateGroups(t,i,a,n,r);else{if(!n(i[a],t))continue;var s=i.index;if(0!==a)for(var c=1;c + + + + + 前言 - Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

前言

+

Go语言起源

+

编程语言的演化跟生物物种的演化类似,一个成功的编程语言的后代一般都会继承它们祖先的优点;当然有时多种语言杂合也可能会产生令人惊讶的特性;还有一些激进的新特性可能并没有先例。通过观察这些影响,我们可以学到为什么一门语言是这样子的,它已经适应了怎样的环境。

+

下图展示了有哪些早期的编程语言对Go语言的设计产生了重要影响。

+

+

Go语言有时候被描述为“C类似语言”,或者是“21世纪的C语言”。Go从C语言继承了相似的表达式语法、控制流结构、基础数据类型、调用参数传值、指针等很多思想,还有C语言一直所看中的编译后机器码的运行效率以及和现有操作系统的无缝适配。

+

但是在Go语言的家族树中还有其它的祖先。其中一个有影响力的分支来自Niklaus Wirth所设计的[Pascal][Pascal]语言。然后[Modula-2][Modula-2]语言激发了包的概念。然后[Oberon][Oberon]语言摒弃了模块接口文件和模块实现文件之间的区别。第二代的[Oberon-2][Oberon-2]语言直接影响了包的导入和声明的语法,还有[Oberon][Oberon]语言的面向对象特性所提供的方法的声明语法等。

+

Go语言的另一支祖先,带来了Go语言区别其他语言的重要特性,灵感来自于贝尔实验室的Tony Hoare于1978年发表的鲜为外界所知的关于并发研究的基础文献 顺序通信进程[communicating sequential processes][CSP] ,缩写为[CSP][CSP]。在[CSP][CSP]中,程序是一组中间没有共享状态的平行运行的处理过程,它们之间使用管道进行通信和控制同步。不过Tony Hoare的[CSP][CSP]只是一个用于描述并发性基本概念的描述语言,并不是一个可以编写可执行程序的通用编程语言。

+

接下来,Rob Pike和其他人开始不断尝试将CSP引入实际的编程语言中。他们第一次尝试引入CSP特性的编程语言叫Squeak(老鼠间交流的语言),是一个提供鼠标和键盘事件处理的编程语言,它的管道是静态创建的。然后是改进版的Newsqueak语言,提供了类似C语言语句和表达式的语法和类似[Pascal][Pascal]语言的推导语法。Newsqueak是一个带垃圾回收的纯函数式语言,它再次针对键盘、鼠标和窗口事件管理。但是在Newsqueak语言中管道是动态创建的,属于第一类值,可以保存到变量中。

+

在Plan9操作系统中,这些优秀的想法被吸收到了一个叫[Alef][Alef]的编程语言中。Alef试图将Newsqueak语言改造为系统编程语言,但是因为缺少垃圾回收机制而导致并发编程很痛苦。(译注:在Alef之后还有一个叫[Limbo][Limbo]的编程语言,Go语言从其中借鉴了很多特性。 具体请参考Pike的讲稿:http://talks.golang.org/2012/concurrency.slide#9 )

+

Go语言的其他的一些特性零散地来自于其他一些编程语言;比如iota语法是从[APL][APL]语言借鉴,词法作用域与嵌套函数来自于[Scheme][Scheme]语言(和其他很多语言)。当然,我们也可以从Go中发现很多创新的设计。比如Go语言的切片为动态数组提供了有效的随机存取的性能,这可能会让人联想到链表的底层的共享机制。还有Go语言新发明的defer语句。

+

Go语言项目

+

所有的编程语言都反映了语言设计者对编程哲学的反思,通常包括之前的语言所暴露的一些不足地方的改进。Go项目是在Google公司维护超级复杂的几个软件系统遇到的一些问题的反思(但是这类问题绝不是Google公司所特有的)。

+

正如Rob Pike所说,“软件的复杂性是乘法级相关的”,通过增加一个部分的复杂性来修复问题通常将慢慢地增加其他部分的复杂性。通过增加功能、选项和配置是修复问题的最快的途径,但是这很容易让人忘记简洁的内涵,即从长远来看,简洁依然是好软件的关键因素。

+

简洁的设计需要在工作开始的时候舍弃不必要的想法,并且在软件的生命周期内严格区别好的改变和坏的改变。通过足够的努力,一个好的改变可以在不破坏原有完整概念的前提下保持自适应,正如Fred Brooks所说的“概念完整性”;而一个坏的改变则不能达到这个效果,它们仅仅是通过肤浅的和简单的妥协来破坏原有设计的一致性。只有通过简洁的设计,才能让一个系统保持稳定、安全和持续的进化。

+

Go项目包括编程语言本身,附带了相关的工具和标准库,最后但并非代表不重要的是,关于简洁编程哲学的宣言。就事后诸葛的角度来看,Go语言的这些地方都做的还不错:拥有自动垃圾回收、一个包系统、函数作为一等公民、词法作用域、系统调用接口、只读的UTF8字符串等。但是Go语言本身只有很少的特性,也不太可能添加太多的特性。例如,它没有隐式的数值转换,没有构造函数和析构函数,没有运算符重载,没有默认参数,也没有继承,没有泛型,没有异常,没有宏,没有函数修饰,更没有线程局部存储。但是,语言本身是成熟和稳定的,而且承诺保证向后兼容:用之前的Go语言编写程序可以用新版本的Go语言编译器和标准库直接构建而不需要修改代码。

+

Go语言有足够的类型系统以避免动态语言中那些粗心的类型错误,但是,Go语言的类型系统相比传统的强类型语言又要简洁很多。虽然,有时候这会导致一个“无类型”的抽象类型概念,但是Go语言程序员并不需要像C++或Haskell程序员那样纠结于具体类型的安全属性。在实践中,Go语言简洁的类型系统给程序员带来了更多的安全性和更好的运行时性能。

+

Go语言鼓励当代计算机系统设计的原则,特别是局部的重要性。它的内置数据类型和大多数的准库数据结构都经过精心设计而避免显式的初始化或隐式的构造函数,因为很少的内存分配和内存初始化代码被隐藏在库代码中了。Go语言的聚合类型(结构体和数组)可以直接操作它们的元素,只需要更少的存储空间、更少的内存写操作,而且指针操作比其他间接操作的语言也更有效率。由于现代计算机是一个并行的机器,Go语言提供了基于CSP的并发特性支持。Go语言的动态栈使得轻量级线程goroutine的初始栈可以很小,因此,创建一个goroutine的代价很小,创建百万级的goroutine完全是可行的。

+

Go语言的标准库(通常被称为语言自带的电池),提供了清晰的构建模块和公共接口,包含I/O操作、文本处理、图像、密码学、网络和分布式应用程序等,并支持许多标准化的文件格式和编解码协议。库和工具使用了大量的约定来减少额外的配置和解释,从而最终简化程序的逻辑,而且,每个Go程序结构都是如此的相似,因此,Go程序也很容易学习。使用Go语言自带工具构建Go语言项目只需要使用文件名和标识符名称,一个偶尔的特殊注释来确定所有的库、可执行文件、测试、基准测试、例子、以及特定于平台的变量、项目的文档等;Go语言源代码本身就包含了构建规范。

+

本书的组织

+

我们假设你已经有一种或多种其他编程语言的使用经历,不管是类似C、C++或Java的编译型语言,还是类似Python、Ruby、JavaScript的脚本语言,因此我们不会像对完全的编程语言初学者那样解释所有的细节。因为,Go语言的变量、常量、表达式、控制流和函数等基本语法也是类似的。

+

第一章包含了本教程的基本结构,通过十几个程序介绍了用Go语言如何实现类似读写文件、文本格式化、创建图像、网络客户端和服务器通讯等日常工作。

+

第二章描述了Go语言程序的基本元素结构、变量、新类型定义、包和文件、以及作用域等概念。第三章讨论了数字、布尔值、字符串和常量,并演示了如何显示和处理Unicode字符。第四章描述了复合类型,从简单的数组、字典、切片到动态列表。第五章涵盖了函数,并讨论了错误处理、panic和recover,还有defer语句。

+

第一章到第五章是基础部分,主流命令式编程语言这部分都类似。个别之处,Go语言有自己特色的语法和风格,但是大多数程序员能很快适应。其余章节是Go语言特有的:方法、接口、并发、包、测试和反射等语言特性。

+

Go语言的面向对象机制与一般语言不同。它没有类层次结构,甚至可以说没有类;仅仅通过组合(而不是继承)简单的对象来构建复杂的对象。方法不仅可以定义在结构体上,而且,可以定义在任何用户自定义的类型上;并且,具体类型和抽象类型(接口)之间的关系是隐式的,所以很多类型的设计者可能并不知道该类型到底实现了哪些接口。方法在第六章讨论,接口在第七章讨论。

+

第八章讨论了基于顺序通信进程(CSP)概念的并发编程,使用goroutines和channels处理并发编程。第九章则讨论了传统的基于共享变量的并发编程。

+

第十章描述了包机制和包的组织结构。这一章还展示了如何有效地利用Go自带的工具,使用单个命令完成编译、测试、基准测试、代码格式化、文档以及其他诸多任务。

+

第十一章讨论了单元测试,Go语言的工具和标准库中集成了轻量级的测试功能,避免了强大但复杂的测试框架。测试库提供了一些基本构件,必要时可以用来构建复杂的测试构件。

+

第十二章讨论了反射,一种程序在运行期间审视自己的能力。反射是一个强大的编程工具,不过要谨慎地使用;这一章利用反射机制实现一些重要的Go语言库函数,展示了反射的强大用法。第十三章解释了底层编程的细节,在必要时,可以使用unsafe包绕过Go语言安全的类型系统。

+

每一章都有一些练习题,你可以用来测试你对Go的理解,你也可以探讨书中这些例子的扩展和替代。

+

书中所有的代码都可以从 http://gopl.io 上的Git仓库下载。go get命令根据每个例子的导入路径智能地获取、构建并安装。只需要选择一个目录作为工作空间,然后将GOPATH环境变量设置为该路径。

+

必要时,Go语言工具会创建目录。例如:

+
$ export GOPATH=$HOME/gobook    # 选择工作目录
+$ go get gopl.io/ch1/helloworld # 获取/编译/安装
+$ $GOPATH/bin/helloworld        # 运行程序
+Hello, 世界                     # 这是中文
+
+

运行这些例子需要安装Go1.5以上的版本。

+
$ go version
+go version go1.5 linux/amd64
+
+

如果使用其他的操作系统,请参考 https://golang.org/doc/install 提供的说明安装。

+

更多的信息

+

最佳的帮助信息来自Go语言的官方网站,https://golang.org ,它提供了完善的参考文档,包括编程语言规范和标准库等诸多权威的帮助信息。同时也包含了如何编写更地道的Go程序的基本教程,还有各种各样的在线文本资源和视频资源,它们是本书最有价值的补充。Go语言的官方博客 https://blog.golang.org 会不定期发布一些Go语言最好的实践文章,包括当前语言的发展状态、未来的计划、会议报告和Go语言相关的各种会议的主题等信息(译注: http://talks.golang.org/ 包含了官方收录的各种报告的讲稿)。

+

在线访问的一个有价值的地方是可以从web页面运行Go语言的程序(而纸质书则没有这么便利了)。这个功能由来自 https://play.golang.org 的 Go Playground 提供,并且可以方便地嵌入到其他页面中,例如 https://golang.org 的主页,或 godoc 提供的文档页面中。

+

Playground可以简单的通过执行一个小程序来测试对语法、语义和对程序库的理解,类似其他很多语言提供的REPL即时运行的工具。同时它可以生成对应的url,非常适合共享Go语言代码片段,汇报bug或提供反馈意见等。

+

基于 Playground 构建的 Go Tour,https://tour.golang.org ,是一个系列的Go语言入门教程,它包含了诸多基本概念和结构相关的并可在线运行的互动小程序。

+

当然,Playground 和 Tour 也有一些限制,它们只能导入标准库,而且因为安全的原因对一些网络库做了限制。如果要在编译和运行时需要访问互联网,对于一些更复杂的实验,你可能需要在自己的电脑上构建并运行程序。幸运的是下载Go语言的过程很简单,从 https://golang.org 下载安装包应该不超过几分钟(译注:感谢伟大的长城,让大陆的Gopher们都学会了自己打洞的基本生活技能,下载时间可能会因为洞的大小等因素从几分钟到几天或更久),然后就可以在自己电脑上编写和运行Go程序了。

+

Go语言是一个开源项目,你可以在 https://golang.org/pkg 阅读标准库中任意函数和类型的实现代码,和下载安装包的代码完全一致。这样,你可以知道很多函数是如何工作的, 通过挖掘找出一些答案的细节,或者仅仅是出于欣赏专业级Go代码。

+

致谢

+

Rob PikeRuss Cox,以及很多其他Go团队的核心成员多次仔细阅读了本书的手稿,他们对本书的组织结构和表述用词等给出了很多宝贵的建议。在准备日文版翻译的时候,Yoshiki Shibata更是仔细地审阅了本书的每个部分,及时发现了诸多英文和代码的错误。我们非常感谢本书的每一位审阅者,并感谢对本书给出了重要的建议的Brian Goetz、Corey Kosak、Arnold Robbins、Josh Bleecher Snyder和Peter Weinberger等人。

+

我们还感谢Sameer Ajmani、Ittai Balaban、David Crawshaw、Billy Donohue、Jonathan Feinberg、Andrew Gerrand、Robert Griesemer、John Linderman、Minux Ma(译注:中国人,Go团队成员。)、Bryan Mills、Bala Natarajan、Cosmos Nicolaou、Paul Staniforth、Nigel Tao(译注:好像是陶哲轩的兄弟)以及Howard Trickey给出的许多有价值的建议。我们还要感谢David Brailsford和Raph Levien关于类型设置的建议。

+

我们从来自Addison-Wesley的编辑Greg Doench收到了很多帮助,从最开始就得到了越来越多的帮助。来自AW生产团队的John Fuller、Dayna Isley、Julie Nahil、Chuti Prasertsith到Barbara Wood,感谢你们的热心帮助。

+

Alan Donovan特别感谢:Sameer Ajmani、Chris Demetriou、Walt Drummond和Google公司的Reid Tatge允许他有充裕的时间去写本书;感谢Stephen Donovan的建议和始终如一的鼓励,以及他的妻子Leila Kazemi并没有让他为了家庭琐事而分心,并热情坚定地支持这个项目。

+

Brian Kernighan特别感谢:朋友和同事对他的耐心和宽容,让他慢慢地梳理本书的写作思路。同时感谢他的妻子Meg和其他很多朋友对他写作事业的支持。

+

2015年 10月 于 纽约

+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/print.html b/print.html new file mode 100644 index 0000000..2dc528b --- /dev/null +++ b/print.html @@ -0,0 +1,10427 @@ + + + + + + Go语言圣经 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + +
+ +

Go语言圣经(中文版)

+

Go语言圣经 《The Go Programming Language》 中文版本,仅供学习交流之用。对于希望学习CGO、Go汇编语言等高级用法的同学,我们推荐《Go语言高级编程》开源图书。如果希望深入学习Go语言语法树结构,可以参考《Go语法树入门——开启自制编程语言和编译器之旅》。如果想从头实现一个玩具Go语言可以参考《从头实现µGo语言》

+

+ +

译者信息:

+ +

前言

+

Go语言起源

+

编程语言的演化跟生物物种的演化类似,一个成功的编程语言的后代一般都会继承它们祖先的优点;当然有时多种语言杂合也可能会产生令人惊讶的特性;还有一些激进的新特性可能并没有先例。通过观察这些影响,我们可以学到为什么一门语言是这样子的,它已经适应了怎样的环境。

+

下图展示了有哪些早期的编程语言对Go语言的设计产生了重要影响。

+

+

Go语言有时候被描述为“C类似语言”,或者是“21世纪的C语言”。Go从C语言继承了相似的表达式语法、控制流结构、基础数据类型、调用参数传值、指针等很多思想,还有C语言一直所看中的编译后机器码的运行效率以及和现有操作系统的无缝适配。

+

但是在Go语言的家族树中还有其它的祖先。其中一个有影响力的分支来自Niklaus Wirth所设计的[Pascal][Pascal]语言。然后[Modula-2][Modula-2]语言激发了包的概念。然后[Oberon][Oberon]语言摒弃了模块接口文件和模块实现文件之间的区别。第二代的[Oberon-2][Oberon-2]语言直接影响了包的导入和声明的语法,还有[Oberon][Oberon]语言的面向对象特性所提供的方法的声明语法等。

+

Go语言的另一支祖先,带来了Go语言区别其他语言的重要特性,灵感来自于贝尔实验室的Tony Hoare于1978年发表的鲜为外界所知的关于并发研究的基础文献 顺序通信进程[communicating sequential processes][CSP] ,缩写为[CSP][CSP]。在[CSP][CSP]中,程序是一组中间没有共享状态的平行运行的处理过程,它们之间使用管道进行通信和控制同步。不过Tony Hoare的[CSP][CSP]只是一个用于描述并发性基本概念的描述语言,并不是一个可以编写可执行程序的通用编程语言。

+

接下来,Rob Pike和其他人开始不断尝试将CSP引入实际的编程语言中。他们第一次尝试引入CSP特性的编程语言叫Squeak(老鼠间交流的语言),是一个提供鼠标和键盘事件处理的编程语言,它的管道是静态创建的。然后是改进版的Newsqueak语言,提供了类似C语言语句和表达式的语法和类似[Pascal][Pascal]语言的推导语法。Newsqueak是一个带垃圾回收的纯函数式语言,它再次针对键盘、鼠标和窗口事件管理。但是在Newsqueak语言中管道是动态创建的,属于第一类值,可以保存到变量中。

+

在Plan9操作系统中,这些优秀的想法被吸收到了一个叫[Alef][Alef]的编程语言中。Alef试图将Newsqueak语言改造为系统编程语言,但是因为缺少垃圾回收机制而导致并发编程很痛苦。(译注:在Alef之后还有一个叫[Limbo][Limbo]的编程语言,Go语言从其中借鉴了很多特性。 具体请参考Pike的讲稿:http://talks.golang.org/2012/concurrency.slide#9 )

+

Go语言的其他的一些特性零散地来自于其他一些编程语言;比如iota语法是从[APL][APL]语言借鉴,词法作用域与嵌套函数来自于[Scheme][Scheme]语言(和其他很多语言)。当然,我们也可以从Go中发现很多创新的设计。比如Go语言的切片为动态数组提供了有效的随机存取的性能,这可能会让人联想到链表的底层的共享机制。还有Go语言新发明的defer语句。

+

Go语言项目

+

所有的编程语言都反映了语言设计者对编程哲学的反思,通常包括之前的语言所暴露的一些不足地方的改进。Go项目是在Google公司维护超级复杂的几个软件系统遇到的一些问题的反思(但是这类问题绝不是Google公司所特有的)。

+

正如Rob Pike所说,“软件的复杂性是乘法级相关的”,通过增加一个部分的复杂性来修复问题通常将慢慢地增加其他部分的复杂性。通过增加功能、选项和配置是修复问题的最快的途径,但是这很容易让人忘记简洁的内涵,即从长远来看,简洁依然是好软件的关键因素。

+

简洁的设计需要在工作开始的时候舍弃不必要的想法,并且在软件的生命周期内严格区别好的改变和坏的改变。通过足够的努力,一个好的改变可以在不破坏原有完整概念的前提下保持自适应,正如Fred Brooks所说的“概念完整性”;而一个坏的改变则不能达到这个效果,它们仅仅是通过肤浅的和简单的妥协来破坏原有设计的一致性。只有通过简洁的设计,才能让一个系统保持稳定、安全和持续的进化。

+

Go项目包括编程语言本身,附带了相关的工具和标准库,最后但并非代表不重要的是,关于简洁编程哲学的宣言。就事后诸葛的角度来看,Go语言的这些地方都做的还不错:拥有自动垃圾回收、一个包系统、函数作为一等公民、词法作用域、系统调用接口、只读的UTF8字符串等。但是Go语言本身只有很少的特性,也不太可能添加太多的特性。例如,它没有隐式的数值转换,没有构造函数和析构函数,没有运算符重载,没有默认参数,也没有继承,没有泛型,没有异常,没有宏,没有函数修饰,更没有线程局部存储。但是,语言本身是成熟和稳定的,而且承诺保证向后兼容:用之前的Go语言编写程序可以用新版本的Go语言编译器和标准库直接构建而不需要修改代码。

+

Go语言有足够的类型系统以避免动态语言中那些粗心的类型错误,但是,Go语言的类型系统相比传统的强类型语言又要简洁很多。虽然,有时候这会导致一个“无类型”的抽象类型概念,但是Go语言程序员并不需要像C++或Haskell程序员那样纠结于具体类型的安全属性。在实践中,Go语言简洁的类型系统给程序员带来了更多的安全性和更好的运行时性能。

+

Go语言鼓励当代计算机系统设计的原则,特别是局部的重要性。它的内置数据类型和大多数的准库数据结构都经过精心设计而避免显式的初始化或隐式的构造函数,因为很少的内存分配和内存初始化代码被隐藏在库代码中了。Go语言的聚合类型(结构体和数组)可以直接操作它们的元素,只需要更少的存储空间、更少的内存写操作,而且指针操作比其他间接操作的语言也更有效率。由于现代计算机是一个并行的机器,Go语言提供了基于CSP的并发特性支持。Go语言的动态栈使得轻量级线程goroutine的初始栈可以很小,因此,创建一个goroutine的代价很小,创建百万级的goroutine完全是可行的。

+

Go语言的标准库(通常被称为语言自带的电池),提供了清晰的构建模块和公共接口,包含I/O操作、文本处理、图像、密码学、网络和分布式应用程序等,并支持许多标准化的文件格式和编解码协议。库和工具使用了大量的约定来减少额外的配置和解释,从而最终简化程序的逻辑,而且,每个Go程序结构都是如此的相似,因此,Go程序也很容易学习。使用Go语言自带工具构建Go语言项目只需要使用文件名和标识符名称,一个偶尔的特殊注释来确定所有的库、可执行文件、测试、基准测试、例子、以及特定于平台的变量、项目的文档等;Go语言源代码本身就包含了构建规范。

+

本书的组织

+

我们假设你已经有一种或多种其他编程语言的使用经历,不管是类似C、C++或Java的编译型语言,还是类似Python、Ruby、JavaScript的脚本语言,因此我们不会像对完全的编程语言初学者那样解释所有的细节。因为,Go语言的变量、常量、表达式、控制流和函数等基本语法也是类似的。

+

第一章包含了本教程的基本结构,通过十几个程序介绍了用Go语言如何实现类似读写文件、文本格式化、创建图像、网络客户端和服务器通讯等日常工作。

+

第二章描述了Go语言程序的基本元素结构、变量、新类型定义、包和文件、以及作用域等概念。第三章讨论了数字、布尔值、字符串和常量,并演示了如何显示和处理Unicode字符。第四章描述了复合类型,从简单的数组、字典、切片到动态列表。第五章涵盖了函数,并讨论了错误处理、panic和recover,还有defer语句。

+

第一章到第五章是基础部分,主流命令式编程语言这部分都类似。个别之处,Go语言有自己特色的语法和风格,但是大多数程序员能很快适应。其余章节是Go语言特有的:方法、接口、并发、包、测试和反射等语言特性。

+

Go语言的面向对象机制与一般语言不同。它没有类层次结构,甚至可以说没有类;仅仅通过组合(而不是继承)简单的对象来构建复杂的对象。方法不仅可以定义在结构体上,而且,可以定义在任何用户自定义的类型上;并且,具体类型和抽象类型(接口)之间的关系是隐式的,所以很多类型的设计者可能并不知道该类型到底实现了哪些接口。方法在第六章讨论,接口在第七章讨论。

+

第八章讨论了基于顺序通信进程(CSP)概念的并发编程,使用goroutines和channels处理并发编程。第九章则讨论了传统的基于共享变量的并发编程。

+

第十章描述了包机制和包的组织结构。这一章还展示了如何有效地利用Go自带的工具,使用单个命令完成编译、测试、基准测试、代码格式化、文档以及其他诸多任务。

+

第十一章讨论了单元测试,Go语言的工具和标准库中集成了轻量级的测试功能,避免了强大但复杂的测试框架。测试库提供了一些基本构件,必要时可以用来构建复杂的测试构件。

+

第十二章讨论了反射,一种程序在运行期间审视自己的能力。反射是一个强大的编程工具,不过要谨慎地使用;这一章利用反射机制实现一些重要的Go语言库函数,展示了反射的强大用法。第十三章解释了底层编程的细节,在必要时,可以使用unsafe包绕过Go语言安全的类型系统。

+

每一章都有一些练习题,你可以用来测试你对Go的理解,你也可以探讨书中这些例子的扩展和替代。

+

书中所有的代码都可以从 http://gopl.io 上的Git仓库下载。go get命令根据每个例子的导入路径智能地获取、构建并安装。只需要选择一个目录作为工作空间,然后将GOPATH环境变量设置为该路径。

+

必要时,Go语言工具会创建目录。例如:

+
$ export GOPATH=$HOME/gobook    # 选择工作目录
+$ go get gopl.io/ch1/helloworld # 获取/编译/安装
+$ $GOPATH/bin/helloworld        # 运行程序
+Hello, 世界                     # 这是中文
+
+

运行这些例子需要安装Go1.5以上的版本。

+
$ go version
+go version go1.5 linux/amd64
+
+

如果使用其他的操作系统,请参考 https://golang.org/doc/install 提供的说明安装。

+

更多的信息

+

最佳的帮助信息来自Go语言的官方网站,https://golang.org ,它提供了完善的参考文档,包括编程语言规范和标准库等诸多权威的帮助信息。同时也包含了如何编写更地道的Go程序的基本教程,还有各种各样的在线文本资源和视频资源,它们是本书最有价值的补充。Go语言的官方博客 https://blog.golang.org 会不定期发布一些Go语言最好的实践文章,包括当前语言的发展状态、未来的计划、会议报告和Go语言相关的各种会议的主题等信息(译注: http://talks.golang.org/ 包含了官方收录的各种报告的讲稿)。

+

在线访问的一个有价值的地方是可以从web页面运行Go语言的程序(而纸质书则没有这么便利了)。这个功能由来自 https://play.golang.org 的 Go Playground 提供,并且可以方便地嵌入到其他页面中,例如 https://golang.org 的主页,或 godoc 提供的文档页面中。

+

Playground可以简单的通过执行一个小程序来测试对语法、语义和对程序库的理解,类似其他很多语言提供的REPL即时运行的工具。同时它可以生成对应的url,非常适合共享Go语言代码片段,汇报bug或提供反馈意见等。

+

基于 Playground 构建的 Go Tour,https://tour.golang.org ,是一个系列的Go语言入门教程,它包含了诸多基本概念和结构相关的并可在线运行的互动小程序。

+

当然,Playground 和 Tour 也有一些限制,它们只能导入标准库,而且因为安全的原因对一些网络库做了限制。如果要在编译和运行时需要访问互联网,对于一些更复杂的实验,你可能需要在自己的电脑上构建并运行程序。幸运的是下载Go语言的过程很简单,从 https://golang.org 下载安装包应该不超过几分钟(译注:感谢伟大的长城,让大陆的Gopher们都学会了自己打洞的基本生活技能,下载时间可能会因为洞的大小等因素从几分钟到几天或更久),然后就可以在自己电脑上编写和运行Go程序了。

+

Go语言是一个开源项目,你可以在 https://golang.org/pkg 阅读标准库中任意函数和类型的实现代码,和下载安装包的代码完全一致。这样,你可以知道很多函数是如何工作的, 通过挖掘找出一些答案的细节,或者仅仅是出于欣赏专业级Go代码。

+

致谢

+

Rob PikeRuss Cox,以及很多其他Go团队的核心成员多次仔细阅读了本书的手稿,他们对本书的组织结构和表述用词等给出了很多宝贵的建议。在准备日文版翻译的时候,Yoshiki Shibata更是仔细地审阅了本书的每个部分,及时发现了诸多英文和代码的错误。我们非常感谢本书的每一位审阅者,并感谢对本书给出了重要的建议的Brian Goetz、Corey Kosak、Arnold Robbins、Josh Bleecher Snyder和Peter Weinberger等人。

+

我们还感谢Sameer Ajmani、Ittai Balaban、David Crawshaw、Billy Donohue、Jonathan Feinberg、Andrew Gerrand、Robert Griesemer、John Linderman、Minux Ma(译注:中国人,Go团队成员。)、Bryan Mills、Bala Natarajan、Cosmos Nicolaou、Paul Staniforth、Nigel Tao(译注:好像是陶哲轩的兄弟)以及Howard Trickey给出的许多有价值的建议。我们还要感谢David Brailsford和Raph Levien关于类型设置的建议。

+

我们从来自Addison-Wesley的编辑Greg Doench收到了很多帮助,从最开始就得到了越来越多的帮助。来自AW生产团队的John Fuller、Dayna Isley、Julie Nahil、Chuti Prasertsith到Barbara Wood,感谢你们的热心帮助。

+

Alan Donovan特别感谢:Sameer Ajmani、Chris Demetriou、Walt Drummond和Google公司的Reid Tatge允许他有充裕的时间去写本书;感谢Stephen Donovan的建议和始终如一的鼓励,以及他的妻子Leila Kazemi并没有让他为了家庭琐事而分心,并热情坚定地支持这个项目。

+

Brian Kernighan特别感谢:朋友和同事对他的耐心和宽容,让他慢慢地梳理本书的写作思路。同时感谢他的妻子Meg和其他很多朋友对他写作事业的支持。

+

2015年 10月 于 纽约

+

第1章 入门

+

本章介绍Go语言的基础组件。本章提供了足够的信息和示例程序,希望可以帮你尽快入门,写出有用的程序。本章和之后章节的示例程序都针对你可能遇到的现实案例。先了解几个Go程序,涉及的主题从简单的文件处理、图像处理到互联网客户端和服务端并发。当然,第一章不会解释细枝末节,但用这些程序来学习一门新语言还是很有效的。

+

学习一门新语言时,会有一种自然的倾向,按照自己熟悉的语言的套路写新语言程序。学习Go语言的过程中,请警惕这种想法,尽量别这么做。我们会演示怎么写好Go语言程序,所以,请使用本书的代码作为你自己写程序时的指南。

+

1.1. Hello, World

+

我们以现已成为传统的“hello world”案例来开始吧,这个例子首次出现于1978年出版的C语言圣经《The C Programming Language》(译注:本书作者之一Brian W. Kernighan也是《The C Programming Language》一书的作者)。C语言是直接影响Go语言设计的语言之一。这个例子体现了Go语言一些核心理念。

+

gopl.io/ch1/helloworld

+
package main
+
+import "fmt"
+
+func main() {
+	fmt.Println("Hello, 世界")
+}
+
+

Go是一门编译型语言,Go语言的工具链将源代码及其依赖转换成计算机的机器指令(译注:静态编译)。Go语言提供的工具都通过一个单独的命令go调用,go命令有一系列子命令。最简单的一个子命令就是run。这个命令编译一个或多个以.go结尾的源文件,链接库文件,并运行最终生成的可执行文件。(本书使用$表示命令行提示符。)

+
$ go run helloworld.go
+
+

毫无意外,这个命令会输出:

+
Hello, 世界
+
+

Go语言原生支持Unicode,它可以处理全世界任何语言的文本。

+

如果不只是一次性实验,你肯定希望能够编译这个程序,保存编译结果以备将来之用。可以用build子命令:

+
$ go build helloworld.go
+
+

这个命令生成一个名为helloworld的可执行的二进制文件(译注:Windows系统下生成的可执行文件是helloworld.exe,增加了.exe后缀名),之后你可以随时运行它(译注:在Windows系统下在命令行直接输入helloworld.exe命令运行),不需任何处理(译注:因为静态编译,所以不用担心在系统库更新的时候冲突,幸福感满满)。

+
$ ./helloworld
+Hello, 世界
+
+

本书中所有示例代码上都有一行标记,利用这些标记可以从gopl.io网站上本书源码仓库里获取代码:

+
gopl.io/ch1/helloworld
+
+

执行 go get gopl.io/ch1/helloworld 命令,就会从网上获取代码,并放到对应目录中(需要先安装Git或Hg之类的版本管理工具,并将对应的命令添加到PATH环境变量中。序言已经提及,需要先设置好GOPATH环境变量,下载的代码会放在$GOPATH/src/gopl.io/ch1/helloworld目录)。2.6和10.7节有这方面更详细的介绍。

+

来讨论下程序本身。Go语言的代码通过(package)组织,包类似于其它语言里的库(libraries)或者模块(modules)。一个包由位于单个目录下的一个或多个.go源代码文件组成,目录定义包的作用。每个源文件都以一条package声明语句开始,这个例子里就是package main,表示该文件属于哪个包,紧跟着一系列导入(import)的包,之后是存储在这个文件里的程序语句。

+

Go的标准库提供了100多个包,以支持常见功能,如输入、输出、排序以及文本处理。比如fmt包,就含有格式化输出、接收输入的函数。Println是其中一个基础函数,可以打印以空格间隔的一个或多个值,并在最后添加一个换行符,从而输出一整行。

+

main包比较特殊。它定义了一个独立可执行的程序,而不是一个库。在main里的main 函数 也很特殊,它是整个程序执行时的入口(译注:C系语言差不多都这样)。main函数所做的事情就是程序做的。当然了,main函数一般调用其它包里的函数完成很多工作(如:fmt.Println)。

+

必须告诉编译器源文件需要哪些包,这就是跟随在package声明后面的import声明扮演的角色。hello world例子只用到了一个包,大多数程序需要导入多个包。

+

必须恰当导入需要的包,缺少了必要的包或者导入了不需要的包,程序都无法编译通过。这项严格要求避免了程序开发过程中引入未使用的包(译注:Go语言编译过程没有警告信息,争议特性之一)。

+

import声明必须跟在文件的package声明之后。随后,则是组成程序的函数、变量、常量、类型的声明语句(分别由关键字funcvarconsttype定义)。这些内容的声明顺序并不重要(译注:最好还是定一下规范)。这个例子的程序已经尽可能短了,只声明了一个函数,其中只调用了一个其他函数。为了节省篇幅,有些时候示例程序会省略packageimport声明,但是,这些声明在源代码里有,并且必须得有才能编译。

+

一个函数的声明由func关键字、函数名、参数列表、返回值列表(这个例子里的main函数参数列表和返回值都是空的)以及包含在大括号里的函数体组成。第五章进一步考察函数。

+

Go语言不需要在语句或者声明的末尾添加分号,除非一行上有多条语句。实际上,编译器会主动把特定符号后的换行符转换为分号,因此换行符添加的位置会影响Go代码的正确解析(译注:比如行末是标识符、整数、浮点数、虚数、字符或字符串文字、关键字breakcontinuefallthroughreturn中的一个、运算符和分隔符++--)]}中的一个)。举个例子,函数的左括号{必须和func函数声明在同一行上,且位于末尾,不能独占一行,而在表达式x + y中,可在+后换行,不能在+前换行(译注:以+结尾的话不会被插入分号分隔符,但是以x结尾的话则会被分号分隔符,从而导致编译错误)。

+

Go语言在代码格式上采取了很强硬的态度。gofmt工具把代码格式化为标准格式(译注:这个格式化工具没有任何可以调整代码格式的参数,Go语言就是这么任性),并且go工具中的fmt子命令会对指定包,否则默认为当前目录中所有.go源文件应用gofmt命令。本书中的所有代码都被gofmt过。你也应该养成格式化自己的代码的习惯。以法令方式规定标准的代码格式可以避免无尽的无意义的琐碎争执(译注:也导致了Go语言的TIOBE排名较低,因为缺少撕逼的话题)。更重要的是,这样可以做多种自动源码转换,如果放任Go语言代码格式,这些转换就不大可能了。

+

很多文本编辑器都可以配置为保存文件时自动执行gofmt,这样你的源代码总会被恰当地格式化。还有个相关的工具,goimports,可以根据代码需要,自动地添加或删除import声明。这个工具并没有包含在标准的分发包中,可以用下面的命令安装:

+
$ go get golang.org/x/tools/cmd/goimports
+
+

对于大多数用户来说,下载、编译包、运行测试用例、察看Go语言的文档等等常用功能都可以用go的工具完成。10.7节详细介绍这些知识。

+

1.2. 命令行参数

+

大多数的程序都是处理输入,产生输出;这也正是“计算”的定义。但是,程序如何获取要处理的输入数据呢?一些程序生成自己的数据,但通常情况下,输入来自于程序外部:文件、网络连接、其它程序的输出、敲键盘的用户、命令行参数或其它类似输入源。下面几个例子会讨论其中几个输入源,首先是命令行参数。

+

os包以跨平台的方式,提供了一些与操作系统交互的函数和变量。程序的命令行参数可从os包的Args变量获取;os包外部使用os.Args访问该变量。

+

os.Args变量是一个字符串(string)的切片(slice)(译注:slice和Python语言中的切片类似,是一个简版的动态数组),切片是Go语言的基础概念,稍后详细介绍。现在先把切片s当作数组元素序列,序列的长度动态变化,用s[i]访问单个元素,用s[m:n]获取子序列(译注:和python里的语法差不多)。序列的元素数目为len(s)。和大多数编程语言类似,区间索引时,Go语言里也采用左闭右开形式,即,区间包括第一个索引元素,不包括最后一个,因为这样可以简化逻辑。(译注:比如a = [1, 2, 3, 4, 5], a[0:3] = [1, 2, 3],不包含最后一个元素)。比如s[m:n]这个切片,0 ≤ m ≤ n ≤ len(s),包含n-m个元素。

+

os.Args的第一个元素:os.Args[0],是命令本身的名字;其它的元素则是程序启动时传给它的参数。s[m:n]形式的切片表达式,产生从第m个元素到第n-1个元素的切片,下个例子用到的元素包含在os.Args[1:len(os.Args)]切片中。如果省略切片表达式的m或n,会默认传入0或len(s),因此前面的切片可以简写成os.Args[1:]。

+

下面是Unix里echo命令的一份实现,echo把它的命令行参数打印成一行。程序导入了两个包,用括号把它们括起来写成列表形式,而没有分开写成独立的import声明。两种形式都合法,列表形式习惯上用得多。包导入顺序并不重要;gofmt工具格式化时按照字母顺序对包名排序。(示例有多个版本时,我们会对示例编号,这样可以明确当前正在讨论的是哪个。)

+

gopl.io/ch1/echo1

+
// Echo1 prints its command-line arguments.
+package main
+
+import (
+	"fmt"
+	"os"
+)
+
+func main() {
+	var s, sep string
+	for i := 1; i < len(os.Args); i++ {
+		s += sep + os.Args[i]
+		sep = " "
+	}
+	fmt.Println(s)
+}
+
+

注释语句以//开头。对于程序员来说,//之后到行末之间所有的内容都是注释,被编译器忽略。按照惯例,我们在每个包的包声明前添加注释;对于main package,注释包含一句或几句话,从整体角度对程序做个描述。

+

var声明定义了两个string类型的变量s和sep。变量会在声明时直接初始化。如果变量没有显式初始化,则被隐式地赋予其类型的零值(zero value),数值类型是0,字符串类型是空字符串""。这个例子里,声明把s和sep隐式地初始化成空字符串。第2章再来详细地讲解变量和声明。

+

对数值类型,Go语言提供了常规的数值和逻辑运算符。而对string类型,+运算符连接字符串(译注:和C++或者js是一样的)。所以表达式:

+
sep + os.Args[i]
+
+

表示连接字符串sep和os.Args。程序中使用的语句:

+
s += sep + os.Args[i]
+
+

是一条赋值语句,将s的旧值跟sep与os.Args[i]连接后赋值回s,等价于:

+
s = s + sep + os.Args[i]
+
+

运算符+=是赋值运算符(assignment operator),每种数值运算符或逻辑运算符,如+*,都有对应的赋值运算符。

+

echo程序可以每循环一次输出一个参数,这个版本却是不断地把新文本追加到末尾来构造字符串。字符串s开始为空,即值为"",每次循环会添加一些文本;第一次迭代之后,还会再插入一个空格,因此循环结束时每个参数中间都有一个空格。这是一种二次加工(quadratic process),当参数数量庞大时,开销很大,但是对于echo,这种情形不大可能出现。本章会介绍echo的若干改进版,下一章解决低效问题。

+

循环索引变量i在for循环的第一部分中定义。符号:=短变量声明(short variable declaration)的一部分,这是定义一个或多个变量并根据它们的初始值为这些变量赋予适当类型的语句。下一章有这方面更多说明。

+

自增语句i++i加1;这和i += 1以及i = i + 1都是等价的。对应的还有i--i减1。它们是语句,而不像C系的其它语言那样是表达式。所以j = i++非法,而且++和--都只能放在变量名后面,因此--i也非法。

+

Go语言只有for循环这一种循环语句。for循环有多种形式,其中一种如下所示:

+
for initialization; condition; post {
+	// zero or more statements
+}
+
+

for循环三个部分不需括号包围。大括号强制要求,左大括号必须和post语句在同一行。

+

initialization语句是可选的,在循环开始前执行。initalization如果存在,必须是一条简单语句(simple statement),即,短变量声明、自增语句、赋值语句或函数调用。condition是一个布尔表达式(boolean expression),其值在每次循环迭代开始时计算。如果为true则执行循环体语句。post语句在循环体执行结束后执行,之后再次对condition求值。condition值为false时,循环结束。

+

for循环的这三个部分每个都可以省略,如果省略initializationpost,分号也可以省略:

+
// a traditional "while" loop
+for condition {
+	// ...
+}
+
+

如果连condition也省略了,像下面这样:

+
// a traditional infinite loop
+for {
+	// ...
+}
+
+

这就变成一个无限循环,尽管如此,还可以用其他方式终止循环,如一条breakreturn语句。

+

for循环的另一种形式,在某种数据类型的区间(range)上遍历,如字符串或切片。echo的第二版本展示了这种形式:

+

gopl.io/ch1/echo2

+
// Echo2 prints its command-line arguments.
+package main
+
+import (
+	"fmt"
+    "os"
+)
+
+func main() {
+	s, sep := "", ""
+	for _, arg := range os.Args[1:] {
+		s += sep + arg
+		sep = " "
+	}
+	fmt.Println(s)
+}
+
+

每次循环迭代,range产生一对值;索引以及在该索引处的元素值。这个例子不需要索引,但range的语法要求,要处理元素,必须处理索引。一种思路是把索引赋值给一个临时变量(如temp)然后忽略它的值,但Go语言不允许使用无用的局部变量(local variables),因为这会导致编译错误。

+

Go语言中这种情况的解决方法是用空标识符(blank identifier),即_(也就是下划线)。空标识符可用于在任何语法需要变量名但程序逻辑不需要的时候(如:在循环里)丢弃不需要的循环索引,并保留元素值。大多数的Go程序员都会像上面这样使用range_echo程序,因为隐式地而非显式地索引os.Args,容易写对。

+

echo的这个版本使用一条短变量声明来声明并初始化sseps,也可以将这两个变量分开声明,声明一个变量有好几种方式,下面这些都等价:

+
s := ""
+var s string
+var s = ""
+var s string = ""
+
+

用哪种不用哪种,为什么呢?第一种形式,是一条短变量声明,最简洁,但只能用在函数内部,而不能用于包变量。第二种形式依赖于字符串的默认初始化零值机制,被初始化为""。第三种形式用得很少,除非同时声明多个变量。第四种形式显式地标明变量的类型,当变量类型与初值类型相同时,类型冗余,但如果两者类型不同,变量类型就必须了。实践中一般使用前两种形式中的某个,初始值重要的话就显式地指定变量的类型,否则使用隐式初始化。

+

如前文所述,每次循环迭代字符串s的内容都会更新。+=连接原字符串、空格和下个参数,产生新字符串,并把它赋值给ss原来的内容已经不再使用,将在适当时机对它进行垃圾回收。

+

如果连接涉及的数据量很大,这种方式代价高昂。一种简单且高效的解决方案是使用strings包的Join函数:

+

gopl.io/ch1/echo3

+
func main() {
+	fmt.Println(strings.Join(os.Args[1:], " "))
+}
+
+

最后,如果不关心输出格式,只想看看输出值,或许只是为了调试,可以用Println为我们格式化输出。

+
fmt.Println(os.Args[1:])
+
+

这条语句的输出结果跟strings.Join得到的结果很像,只是被放到了一对方括号里。切片都会被打印成这种格式。

+

练习 1.1: 修改echo程序,使其能够打印os.Args[0],即被执行命令本身的名字。

+

练习 1.2: 修改echo程序,使其打印每个参数的索引和值,每个一行。

+

练习 1.3: 做实验测量潜在低效的版本和使用了strings.Join的版本的运行时间差异。(1.6节讲解了部分time包,11.4节展示了如何写标准测试程序,以得到系统性的性能评测。)

+

1.3. 查找重复的行

+

对文件做拷贝、打印、搜索、排序、统计或类似事情的程序都有一个差不多的程序结构:一个处理输入的循环,在每个元素上执行计算处理,在处理的同时或最后产生输出。我们会展示一个名为dup的程序的三个版本;灵感来自于Unix的uniq命令,其寻找相邻的重复行。该程序使用的结构和包是个参考范例,可以方便地修改。

+

dup的第一个版本打印标准输入中多次出现的行,以重复次数开头。该程序将引入if语句,map数据类型以及bufio包。

+

gopl.io/ch1/dup1

+
// Dup1 prints the text of each line that appears more than
+// once in the standard input, preceded by its count.
+package main
+
+import (
+	"bufio"
+	"fmt"
+	"os"
+)
+
+func main() {
+	counts := make(map[string]int)
+	input := bufio.NewScanner(os.Stdin)
+	for input.Scan() {
+		counts[input.Text()]++
+	}
+	// NOTE: ignoring potential errors from input.Err()
+	for line, n := range counts {
+		if n > 1 {
+			fmt.Printf("%d\t%s\n", n, line)
+		}
+	}
+}
+
+

正如for循环一样,if语句条件两边也不加括号,但是主体部分需要加。if语句的else部分是可选的,在if的条件为false时执行。

+

map存储了键/值(key/value)的集合,对集合元素,提供常数时间的存、取或测试操作。键可以是任意类型,只要其值能用==运算符比较,最常见的例子是字符串;值则可以是任意类型。这个例子中的键是字符串,值是整数。内置函数make创建空map,此外,它还有别的作用。4.3节讨论map

+

(译注:从功能和实现上说,Gomap类似于Java语言中的HashMap,Python语言中的dictLua语言中的table,通常使用hash实现。遗憾的是,对于该词的翻译并不统一,数学界术语为映射,而计算机界众说纷纭莫衷一是。为了防止对读者造成误解,保留不译。)

+

每次dup读取一行输入,该行被当做键存入map,其对应的值递增。counts[input.Text()]++语句等价下面两句:

+
line := input.Text()
+counts[line] = counts[line] + 1
+
+

map中不含某个键时不用担心,首次读到新行时,等号右边的表达式counts[line]的值将被计算为其类型的零值,对于int即0。

+

为了打印结果,我们使用了基于range的循环,并在counts这个map上迭代。跟之前类似,每次迭代得到两个结果,键和其在map中对应的值。map的迭代顺序并不确定,从实践来看,该顺序随机,每次运行都会变化。这种设计是有意为之的,因为能防止程序依赖特定遍历顺序,而这是无法保证的。(译注:具体可以参见这里http://stackoverflow.com/questions/11853396/google-go-lang-assignment-order)

+

继续来看bufio包,它使处理输入和输出方便又高效。Scanner类型是该包最有用的特性之一,它读取输入并将其拆成行或单词;通常是处理行形式的输入最简单的方法。

+

程序使用短变量声明创建bufio.Scanner类型的变量input

+
input := bufio.NewScanner(os.Stdin)
+
+

该变量从程序的标准输入中读取内容。每次调用input.Scan(),即读入下一行,并移除行末的换行符;读取的内容可以调用input.Text()得到。Scan函数在读到一行时返回true,不再有输入时返回false

+

类似于C或其它语言里的printf函数,fmt.Printf函数对一些表达式产生格式化输出。该函数的首个参数是个格式字符串,指定后续参数被如何格式化。各个参数的格式取决于“转换字符”(conversion character),形式为百分号后跟一个字母。举个例子,%d表示以十进制形式打印一个整型操作数,而%s则表示把字符串型操作数的值展开。

+

Printf有一大堆这种转换,Go程序员称之为动词(verb)。下面的表格虽然远不是完整的规范,但展示了可用的很多特性:

+
%d          十进制整数
+%x, %o, %b  十六进制,八进制,二进制整数。
+%f, %g, %e  浮点数: 3.141593 3.141592653589793 3.141593e+00
+%t          布尔:true或false
+%c          字符(rune) (Unicode码点)
+%s          字符串
+%q          带双引号的字符串"abc"或带单引号的字符'c'
+%v          变量的自然形式(natural format)
+%T          变量的类型
+%%          字面上的百分号标志(无操作数)
+
+

dup1的格式字符串中还含有制表符\t和换行符\n。字符串字面上可能含有这些代表不可见字符的转义字符(escape sequences)。默认情况下,Printf不会换行。按照惯例,以字母f结尾的格式化函数,如log.Printffmt.Errorf,都采用fmt.Printf的格式化准则。而以ln结尾的格式化函数,则遵循Println的方式,以跟%v差不多的方式格式化参数,并在最后添加一个换行符。(译注:后缀fformatlnline。)

+

很多程序要么从标准输入中读取数据,如上面的例子所示,要么从一系列具名文件中读取数据。dup程序的下个版本读取标准输入或是使用os.Open打开各个具名文件,并操作它们。

+

gopl.io/ch1/dup2

+
// Dup2 prints the count and text of lines that appear more than once
+// in the input.  It reads from stdin or from a list of named files.
+package main
+
+import (
+	"bufio"
+	"fmt"
+	"os"
+)
+
+func main() {
+	counts := make(map[string]int)
+	files := os.Args[1:]
+	if len(files) == 0 {
+		countLines(os.Stdin, counts)
+	} else {
+		for _, arg := range files {
+			f, err := os.Open(arg)
+			if err != nil {
+				fmt.Fprintf(os.Stderr, "dup2: %v\n", err)
+				continue
+			}
+			countLines(f, counts)
+			f.Close()
+		}
+	}
+	for line, n := range counts {
+		if n > 1 {
+			fmt.Printf("%d\t%s\n", n, line)
+		}
+	}
+}
+
+func countLines(f *os.File, counts map[string]int) {
+	input := bufio.NewScanner(f)
+	for input.Scan() {
+		counts[input.Text()]++
+	}
+	// NOTE: ignoring potential errors from input.Err()
+}
+
+

os.Open函数返回两个值。第一个值是被打开的文件(*os.File),其后被Scanner读取。

+

os.Open返回的第二个值是内置error类型的值。如果err等于内置值nil(译注:相当于其它语言里的NULL),那么文件被成功打开。读取文件,直到文件结束,然后调用Close关闭该文件,并释放占用的所有资源。相反的话,如果err的值不是nil,说明打开文件时出错了。这种情况下,错误值描述了所遇到的问题。我们的错误处理非常简单,只是使用Fprintf与表示任意类型默认格式值的动词%v,向标准错误流打印一条信息,然后dup继续处理下一个文件;continue语句直接跳到for循环的下个迭代开始执行。

+

为了使示例代码保持合理的大小,本书开始的一些示例有意简化了错误处理,显而易见的是,应该检查os.Open返回的错误值,然而,使用input.Scan读取文件过程中,不大可能出现错误,因此我们忽略了错误处理。我们会在跳过错误检查的地方做说明。5.4节中深入介绍错误处理。

+

注意countLines函数在其声明前被调用。函数和包级别的变量(package-level entities)可以任意顺序声明,并不影响其被调用。(译注:最好还是遵循一定的规范)

+

map是一个由make函数创建的数据结构的引用。map作为参数传递给某函数时,该函数接收这个引用的一份拷贝(copy,或译为副本),被调用函数对map底层数据结构的任何修改,调用者函数都可以通过持有的map引用看到。在我们的例子中,countLines函数向counts插入的值,也会被main函数看到。(译注:类似于C++里的引用传递,实际上指针是另一个指针了,但内部存的值指向同一块内存)

+

dup的前两个版本以"流”模式读取输入,并根据需要拆分成多个行。理论上,这些程序可以处理任意数量的输入数据。还有另一个方法,就是一口气把全部输入数据读到内存中,一次分割为多行,然后处理它们。下面这个版本,dup3,就是这么操作的。这个例子引入了ReadFile函数(来自于io/ioutil包),其读取指定文件的全部内容,strings.Split函数把字符串分割成子串的切片。(Split的作用与前文提到的strings.Join相反。)

+

我们略微简化了dup3。首先,由于ReadFile函数需要文件名作为参数,因此只读指定文件,不读标准输入。其次,由于行计数代码只在一处用到,故将其移回main函数。

+

gopl.io/ch1/dup3

+
package main
+
+import (
+	"fmt"
+	"io/ioutil"
+	"os"
+	"strings"
+)
+
+func main() {
+	counts := make(map[string]int)
+	for _, filename := range os.Args[1:] {
+		data, err := ioutil.ReadFile(filename)
+		if err != nil {
+			fmt.Fprintf(os.Stderr, "dup3: %v\n", err)
+			continue
+		}
+		for _, line := range strings.Split(string(data), "\n") {
+			counts[line]++
+		}
+	}
+	for line, n := range counts {
+		if n > 1 {
+			fmt.Printf("%d\t%s\n", n, line)
+		}
+	}
+}
+
+

ReadFile函数返回一个字节切片(byte slice),必须把它转换为string,才能用strings.Split分割。我们会在3.5.4节详细讲解字符串和字节切片。

+

实现上,bufio.Scannerioutil.ReadFileioutil.WriteFile都使用*os.FileReadWrite方法,但是,大多数程序员很少需要直接调用那些低级(lower-level)函数。高级(higher-level)函数,像bufioio/ioutil包中所提供的那些,用起来要容易点。

+

练习 1.4: 修改dup2,出现重复的行时打印文件名称。

+

1.4. GIF动画

+

下面的程序会演示Go语言标准库里的image这个package的用法,我们会用这个包来生成一系列的bit-mapped图,然后将这些图片编码为一个GIF动画。我们生成的图形名字叫利萨如图形(Lissajous figures),这种效果是在1960年代的老电影里出现的一种视觉特效。它们是协振子在两个纬度上振动所产生的曲线,比如两个sin正弦波分别在x轴和y轴输入会产生的曲线。图1.1是这样的一个例子:

+

+

译注:要看这个程序的结果,需要将标准输出重定向到一个GIF图像文件(使用 ./lissajous > output.gif 命令)。下面是GIF图像动画效果:

+

+

这段代码里我们用了一些新的结构,包括const声明,struct结构体类型,复合声明。和我们举的其它的例子不太一样,这一个例子包含了浮点数运算。这些概念我们只在这里简单地说明一下,之后的章节会更详细地讲解。

+

gopl.io/ch1/lissajous

+
// Lissajous generates GIF animations of random Lissajous figures.
+package main
+
+import (
+	"image"
+	"image/color"
+	"image/gif"
+	"io"
+	"math"
+	"math/rand"
+	"os"
+	"time"
+)
+
+var palette = []color.Color{color.White, color.Black}
+
+const (
+	whiteIndex = 0 // first color in palette
+	blackIndex = 1 // next color in palette
+)
+
+func main() {
+	// The sequence of images is deterministic unless we seed
+	// the pseudo-random number generator using the current time.
+	// Thanks to Randall McPherson for pointing out the omission.
+	rand.Seed(time.Now().UTC().UnixNano())
+	lissajous(os.Stdout)
+}
+
+func lissajous(out io.Writer) {
+	const (
+		cycles  = 5     // number of complete x oscillator revolutions
+		res     = 0.001 // angular resolution
+		size    = 100   // image canvas covers [-size..+size]
+		nframes = 64    // number of animation frames
+		delay   = 8     // delay between frames in 10ms units
+	)
+
+	freq := rand.Float64() * 3.0 // relative frequency of y oscillator
+	anim := gif.GIF{LoopCount: nframes}
+	phase := 0.0 // phase difference
+	for i := 0; i < nframes; i++ {
+		rect := image.Rect(0, 0, 2*size+1, 2*size+1)
+		img := image.NewPaletted(rect, palette)
+		for t := 0.0; t < cycles*2*math.Pi; t += res {
+			x := math.Sin(t)
+			y := math.Sin(t*freq + phase)
+			img.SetColorIndex(size+int(x*size+0.5), size+int(y*size+0.5),
+				blackIndex)
+		}
+		phase += 0.1
+		anim.Delay = append(anim.Delay, delay)
+		anim.Image = append(anim.Image, img)
+	}
+	gif.EncodeAll(out, &anim) // NOTE: ignoring encoding errors
+}
+
+
+

当我们import了一个包路径包含有多个单词的package时,比如image/color(image和color两个单词),通常我们只需要用最后那个单词表示这个包就可以。所以当我们写color.White时,这个变量指向的是image/color包里的变量,同理gif.GIF是属于image/gif包里的变量。

+

这个程序里的常量声明给出了一系列的常量值,常量是指在程序编译后运行时始终都不会变化的值,比如圈数、帧数、延迟值。常量声明和变量声明一般都会出现在包级别,所以这些常量在整个包中都是可以共享的,或者你也可以把常量声明定义在函数体内部,那么这种常量就只能在函数体内用。目前常量声明的值必须是一个数字值、字符串或者一个固定的boolean值。

+

[]color.Color{...}和gif.GIF{...}这两个表达式就是我们说的复合声明(4.2和4.4.1节有说明)。这是实例化Go语言里的复合类型的一种写法。这里的前者生成的是一个slice切片,后者生成的是一个struct结构体。

+

gif.GIF是一个struct类型(参考4.4节)。struct是一组值或者叫字段的集合,不同的类型集合在一个struct可以让我们以一个统一的单元进行处理。anim是一个gif.GIF类型的struct变量。这种写法会生成一个struct变量,并且其内部变量LoopCount字段会被设置为nframes;而其它的字段会被设置为各自类型默认的零值。struct内部的变量可以以一个点(.)来进行访问,就像在最后两个赋值语句中显式地更新了anim这个struct的Delay和Image字段。

+

lissajous函数内部有两层嵌套的for循环。外层循环会循环64次,每一次都会生成一个单独的动画帧。它生成了一个包含两种颜色的201*201大小的图片,白色和黑色。所有像素点都会被默认设置为其零值(也就是调色板palette里的第0个值),这里我们设置的是白色。每次外层循环都会生成一张新图片,并将一些像素设置为黑色。其结果会append到之前结果之后。这里我们用到了append(参考4.2.1)内置函数,将结果append到anim中的帧列表末尾,并设置一个默认的80ms的延迟值。循环结束后所有的延迟值被编码进了GIF图片中,并将结果写入到输出流。out这个变量是io.Writer类型,这个类型支持把输出结果写到很多目标,很快我们就可以看到例子。

+

内层循环设置两个偏振值。x轴偏振使用sin函数。y轴偏振也是正弦波,但其相对x轴的偏振是一个0-3的随机值,初始偏振值是一个零值,随着动画的每一帧逐渐增加。循环会一直跑到x轴完成五次完整的循环。每一步它都会调用SetColorIndex来为(x,y)点来染黑色。

+

main函数调用lissajous函数,用它来向标准输出流打印信息,所以下面这个命令会像图1.1中产生一个GIF动画。

+
$ go build gopl.io/ch1/lissajous
+$ ./lissajous >out.gif
+
+

练习 1.5: 修改前面的Lissajous程序里的调色板,由黑色改为绿色。我们可以用color.RGBA{0xRR, 0xGG, 0xBB, 0xff}来得到#RRGGBB这个色值,三个十六进制的字符串分别代表红、绿、蓝像素。

+

练习 1.6: 修改Lissajous程序,修改其调色板来生成更丰富的颜色,然后修改SetColorIndex的第三个参数,看看显示结果吧。

+

1.5. 获取URL

+

对于很多现代应用来说,访问互联网上的信息和访问本地文件系统一样重要。Go语言在net这个强大package的帮助下提供了一系列的package来做这件事情,使用这些包可以更简单地用网络收发信息,还可以建立更底层的网络连接,编写服务器程序。在这些情景下,Go语言原生的并发特性(在第八章中会介绍)显得尤其好用。

+

为了最简单地展示基于HTTP获取信息的方式,下面给出一个示例程序fetch,这个程序将获取对应的url,并将其源文本打印出来;这个例子的灵感来源于curl工具(译注:unix下的一个用来发http请求的工具,具体可以man curl)。当然,curl提供的功能更为复杂丰富,这里只编写最简单的样例。这个样例之后还会多次被用到。

+

gopl.io/ch1/fetch

+
// Fetch prints the content found at a URL.
+package main
+
+import (
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"os"
+)
+
+func main() {
+	for _, url := range os.Args[1:] {
+		resp, err := http.Get(url)
+		if err != nil {
+			fmt.Fprintf(os.Stderr, "fetch: %v\n", err)
+			os.Exit(1)
+		}
+		b, err := ioutil.ReadAll(resp.Body)
+		resp.Body.Close()
+		if err != nil {
+			fmt.Fprintf(os.Stderr, "fetch: reading %s: %v\n", url, err)
+			os.Exit(1)
+		}
+		fmt.Printf("%s", b)
+	}
+}
+
+

这个程序从两个package中导入了函数,net/http和io/ioutil包,http.Get函数是创建HTTP请求的函数,如果获取过程没有出错,那么会在resp这个结构体中得到访问的请求结果。resp的Body字段包括一个可读的服务器响应流。ioutil.ReadAll函数从response中读取到全部内容;将其结果保存在变量b中。resp.Body.Close关闭resp的Body流,防止资源泄露,Printf函数会将结果b写出到标准输出流中。

+
$ go build gopl.io/ch1/fetch
+$ ./fetch http://gopl.io
+<html>
+<head>
+<title>The Go Programming Language</title>title>
+...
+
+

HTTP请求如果失败了的话,会得到下面这样的结果:

+
$ ./fetch http://bad.gopl.io
+fetch: Get http://bad.gopl.io: dial tcp: lookup bad.gopl.io: no such host
+
+

译注:在大天朝的网络环境下很容易重现这种错误,下面是Windows下运行得到的错误信息:

+
$ go run main.go http://gopl.io
+fetch: Get http://gopl.io: dial tcp: lookup gopl.io: getaddrinfow: No such host is known.
+
+

无论哪种失败原因,我们的程序都用了os.Exit函数来终止进程,并且返回一个status错误码,其值为1。

+

练习 1.7: 函数调用io.Copy(dst, src)会从src中读取内容,并将读到的结果写入到dst中,使用这个函数替代掉例子中的ioutil.ReadAll来拷贝响应结构体到os.Stdout,避免申请一个缓冲区(例子中的b)来存储。记得处理io.Copy返回结果中的错误。

+

练习 1.8: 修改fetch这个范例,如果输入的url参数没有 http:// 前缀的话,为这个url加上该前缀。你可能会用到strings.HasPrefix这个函数。

+

练习 1.9: 修改fetch打印出HTTP协议的状态码,可以从resp.Status变量得到该状态码。

+

1.6. 并发获取多个URL

+

Go语言最有意思并且最新奇的特性就是对并发编程的支持。并发编程是一个大话题,在第八章和第九章中会专门讲到。这里我们只浅尝辄止地来体验一下Go语言里的goroutine和channel。

+

下面的例子fetchall,和前面小节的fetch程序所要做的工作基本一致,fetchall的特别之处在于它会同时去获取所有的URL,所以这个程序的总执行时间不会超过执行时间最长的那一个任务,前面的fetch程序执行时间则是所有任务执行时间之和。fetchall程序只会打印获取的内容大小和经过的时间,不会像之前那样打印获取的内容。

+

gopl.io/ch1/fetchall

+
// Fetchall fetches URLs in parallel and reports their times and sizes.
+package main
+
+import (
+	"fmt"
+	"io"
+	"io/ioutil"
+	"net/http"
+	"os"
+	"time"
+)
+
+func main() {
+	start := time.Now()
+	ch := make(chan string)
+	for _, url := range os.Args[1:] {
+		go fetch(url, ch) // start a goroutine
+	}
+	for range os.Args[1:] {
+		fmt.Println(<-ch) // receive from channel ch
+	}
+	fmt.Printf("%.2fs elapsed\n", time.Since(start).Seconds())
+}
+
+func fetch(url string, ch chan<- string) {
+	start := time.Now()
+	resp, err := http.Get(url)
+	if err != nil {
+		ch <- fmt.Sprint(err) // send to channel ch
+		return
+	}
+	nbytes, err := io.Copy(ioutil.Discard, resp.Body)
+	resp.Body.Close() // don't leak resources
+	if err != nil {
+		ch <- fmt.Sprintf("while reading %s: %v", url, err)
+		return
+	}
+	secs := time.Since(start).Seconds()
+	ch <- fmt.Sprintf("%.2fs  %7d  %s", secs, nbytes, url)
+}
+
+

下面使用fetchall来请求几个地址:

+
$ go build gopl.io/ch1/fetchall
+$ ./fetchall https://golang.org http://gopl.io https://godoc.org
+0.14s     6852  https://godoc.org
+0.16s     7261  https://golang.org
+0.48s     2475  http://gopl.io
+0.48s elapsed
+
+

goroutine是一种函数的并发执行方式,而channel是用来在goroutine之间进行参数传递。main函数本身也运行在一个goroutine中,而go function则表示创建一个新的goroutine,并在这个新的goroutine中执行这个函数。

+

main函数中用make函数创建了一个传递string类型参数的channel,对每一个命令行参数,我们都用go这个关键字来创建一个goroutine,并且让函数在这个goroutine异步执行http.Get方法。这个程序里的io.Copy会把响应的Body内容拷贝到ioutil.Discard输出流中(译注:可以把这个变量看作一个垃圾桶,可以向里面写一些不需要的数据),因为我们需要这个方法返回的字节数,但是又不想要其内容。每当请求返回内容时,fetch函数都会往ch这个channel里写入一个字符串,由main函数里的第二个for循环来处理并打印channel里的这个字符串。

+

当一个goroutine尝试在一个channel上做send或者receive操作时,这个goroutine会阻塞在调用处,直到另一个goroutine从这个channel里接收或者写入值,这样两个goroutine才会继续执行channel操作之后的逻辑。在这个例子中,每一个fetch函数在执行时都会往channel里发送一个值(ch <- expression),主函数负责接收这些值(<-ch)。这个程序中我们用main函数来完整地处理/接收所有fetch函数传回的字符串,可以避免因为有两个goroutine同时完成而使得其输出交错在一起的危险。

+

练习 1.10: 找一个数据量比较大的网站,用本小节中的程序调研网站的缓存策略,对每个URL执行两遍请求,查看两次时间是否有较大的差别,并且每次获取到的响应内容是否一致,修改本节中的程序,将响应结果输出到文件,以便于进行对比。

+

练习 1.11: 在fetchall中尝试使用长一些的参数列表,比如使用在alexa.com的上百万网站里排名靠前的。如果一个网站没有回应,程序将采取怎样的行为?(Section8.9 描述了在这种情况下的应对机制)。

+

1.7. Web服务

+

Go语言的内置库使得写一个类似fetch的web服务器变得异常地简单。在本节中,我们会展示一个微型服务器,这个服务器的功能是返回当前用户正在访问的URL。比如用户访问的是 http://localhost:8000/hello ,那么响应是URL.Path = "hello"。

+

gopl.io/ch1/server1

+
// Server1 is a minimal "echo" server.
+package main
+
+import (
+	"fmt"
+	"log"
+	"net/http"
+)
+
+func main() {
+	http.HandleFunc("/", handler) // each request calls handler
+	log.Fatal(http.ListenAndServe("localhost:8000", nil))
+}
+
+// handler echoes the Path component of the request URL r.
+func handler(w http.ResponseWriter, r *http.Request) {
+	fmt.Fprintf(w, "URL.Path = %q\n", r.URL.Path)
+}
+
+

我们只用了八九行代码就实现了一个Web服务程序,这都是多亏了标准库里的方法已经帮我们完成了大量工作。main函数将所有发送到/路径下的请求和handler函数关联起来,/开头的请求其实就是所有发送到当前站点上的请求,服务监听8000端口。发送到这个服务的“请求”是一个http.Request类型的对象,这个对象中包含了请求中的一系列相关字段,其中就包括我们需要的URL。当请求到达服务器时,这个请求会被传给handler函数来处理,这个函数会将/hello这个路径从请求的URL中解析出来,然后把其发送到响应中,这里我们用的是标准输出流的fmt.Fprintf。Web服务会在第7.7节中做更详细的阐述。

+

让我们在后台运行这个服务程序。如果你的操作系统是Mac OS X或者Linux,那么在运行命令的末尾加上一个&符号,即可让程序简单地跑在后台,windows下可以在另外一个命令行窗口去运行这个程序。

+
$ go run src/gopl.io/ch1/server1/main.go &
+
+

现在可以通过命令行来发送客户端请求了:

+
$ go build gopl.io/ch1/fetch
+$ ./fetch http://localhost:8000
+URL.Path = "/"
+$ ./fetch http://localhost:8000/help
+URL.Path = "/help"
+
+

还可以直接在浏览器里访问这个URL,然后得到返回结果,如图1.2:

+

+

在这个服务的基础上叠加特性是很容易的。一种比较实用的修改是为访问的url添加某种状态。比如,下面这个版本输出了同样的内容,但是会对请求的次数进行计算;对URL的请求结果会包含各种URL被访问的总次数,直接对/count这个URL的访问要除外。

+

gopl.io/ch1/server2

+
// Server2 is a minimal "echo" and counter server.
+package main
+
+import (
+	"fmt"
+	"log"
+	"net/http"
+	"sync"
+)
+
+var mu sync.Mutex
+var count int
+
+func main() {
+	http.HandleFunc("/", handler)
+	http.HandleFunc("/count", counter)
+	log.Fatal(http.ListenAndServe("localhost:8000", nil))
+}
+
+// handler echoes the Path component of the requested URL.
+func handler(w http.ResponseWriter, r *http.Request) {
+	mu.Lock()
+	count++
+	mu.Unlock()
+	fmt.Fprintf(w, "URL.Path = %q\n", r.URL.Path)
+}
+
+// counter echoes the number of calls so far.
+func counter(w http.ResponseWriter, r *http.Request) {
+	mu.Lock()
+	fmt.Fprintf(w, "Count %d\n", count)
+	mu.Unlock()
+}
+
+

这个服务器有两个请求处理函数,根据请求的url不同会调用不同的函数:对/count这个url的请求会调用到counter这个函数,其它的url都会调用默认的处理函数。如果你的请求pattern是以/结尾,那么所有以该url为前缀的url都会被这条规则匹配。在这些代码的背后,服务器每一次接收请求处理时都会另起一个goroutine,这样服务器就可以同一时间处理多个请求。然而在并发情况下,假如真的有两个请求同一时刻去更新count,那么这个值可能并不会被正确地增加;这个程序可能会引发一个严重的bug:竞态条件(参见9.1)。为了避免这个问题,我们必须保证每次修改变量的最多只能有一个goroutine,这也就是代码里的mu.Lock()和mu.Unlock()调用将修改count的所有行为包在中间的目的。第九章中我们会进一步讲解共享变量。

+

下面是一个更为丰富的例子,handler函数会把请求的http头和请求的form数据都打印出来,这样可以使检查和调试这个服务更为方便:

+

gopl.io/ch1/server3

+
// handler echoes the HTTP request.
+func handler(w http.ResponseWriter, r *http.Request) {
+	fmt.Fprintf(w, "%s %s %s\n", r.Method, r.URL, r.Proto)
+	for k, v := range r.Header {
+		fmt.Fprintf(w, "Header[%q] = %q\n", k, v)
+	}
+	fmt.Fprintf(w, "Host = %q\n", r.Host)
+	fmt.Fprintf(w, "RemoteAddr = %q\n", r.RemoteAddr)
+	if err := r.ParseForm(); err != nil {
+		log.Print(err)
+	}
+	for k, v := range r.Form {
+		fmt.Fprintf(w, "Form[%q] = %q\n", k, v)
+	}
+}
+
+

我们用http.Request这个struct里的字段来输出下面这样的内容:

+
GET /?q=query HTTP/1.1
+Header["Accept-Encoding"] = ["gzip, deflate, sdch"]
+Header["Accept-Language"] = ["en-US,en;q=0.8"]
+Header["Connection"] = ["keep-alive"]
+Header["Accept"] = ["text/html,application/xhtml+xml,application/xml;..."]
+Header["User-Agent"] = ["Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_5)..."]
+Host = "localhost:8000"
+RemoteAddr = "127.0.0.1:59911"
+Form["q"] = ["query"]
+
+

可以看到这里的ParseForm被嵌套在了if语句中。Go语言允许这样的一个简单的语句结果作为局部的变量声明出现在if语句的最前面,这一点对错误处理很有用处。我们还可以像下面这样写(当然看起来就长了一些):

+
err := r.ParseForm()
+if err != nil {
+	log.Print(err)
+}
+
+

用if和ParseForm结合可以让代码更加简单,并且可以限制err这个变量的作用域,这么做是很不错的。我们会在2.7节中讲解作用域。

+

在这些程序中,我们看到了很多不同的类型被输出到标准输出流中。比如前面的fetch程序,把HTTP的响应数据拷贝到了os.Stdout,lissajous程序里我们输出的是一个文件。fetchall程序则完全忽略到了HTTP的响应Body,只是计算了一下响应Body的大小,这个程序中把响应Body拷贝到了ioutil.Discard。在本节的web服务器程序中则是用fmt.Fprintf直接写到了http.ResponseWriter中。

+

尽管三种具体的实现流程并不太一样,他们都实现一个共同的接口,即当它们被调用需要一个标准流输出时都可以满足。这个接口叫作io.Writer,在7.1节中会详细讨论。

+

Go语言的接口机制会在第7章中讲解,为了在这里简单说明接口能做什么,让我们简单地将这里的web服务器和之前写的lissajous函数结合起来,这样GIF动画可以被写到HTTP的客户端,而不是之前的标准输出流。只要在web服务器的代码里加入下面这几行。

+
handler := func(w http.ResponseWriter, r *http.Request) {
+	lissajous(w)
+}
+http.HandleFunc("/", handler)
+
+

或者另一种等价形式:

+
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+	lissajous(w)
+})
+
+

HandleFunc函数的第二个参数是一个函数的字面值,也就是一个在使用时定义的匿名函数。这些内容我们会在5.6节中讲解。

+

做完这些修改之后,在浏览器里访问 http://localhost:8000 。每次你载入这个页面都可以看到一个像图1.3那样的动画。

+

+

练习 1.12: 修改Lissajour服务,从URL读取变量,比如你可以访问 http://localhost:8000/?cycles=20 这个URL,这样访问可以将程序里的cycles默认的5修改为20。字符串转换为数字可以调用strconv.Atoi函数。你可以在godoc里查看strconv.Atoi的详细说明。

+

1.8. 本章要点

+

本章对Go语言做了一些介绍,Go语言很多方面在有限的篇幅中无法覆盖到。本节会把没有讲到的内容也做一些简单的介绍,这样读者在读到完整的内容之前,可以有个简单的印象。

+

控制流: 在本章我们只介绍了if控制和for,但是没有提到switch多路选择。这里是一个简单的switch的例子:

+
switch coinflip() {
+case "heads":
+	heads++
+case "tails":
+	tails++
+default:
+	fmt.Println("landed on edge!")
+}
+
+

在翻转硬币的时候,例子里的coinflip函数返回几种不同的结果,每一个case都会对应一个返回结果,这里需要注意,Go语言并不需要显式地在每一个case后写break,语言默认执行完case后的逻辑语句会自动退出。当然了,如果你想要相邻的几个case都执行同一逻辑的话,需要自己显式地写上一个fallthrough语句来覆盖这种默认行为。不过fallthrough语句在一般的程序中很少用到。

+

Go语言里的switch还可以不带操作对象(译注:switch不带操作对象时默认用true值代替,然后将每个case的表达式和true值进行比较);可以直接罗列多种条件,像其它语言里面的多个if else一样,下面是一个例子:

+
func Signum(x int) int {
+	switch {
+	case x > 0:
+		return +1
+	default:
+		return 0
+	case x < 0:
+		return -1
+	}
+}
+
+

这种形式叫做无tag switch(tagless switch);这和switch true是等价的。

+

像for和if控制语句一样,switch也可以紧跟一个简短的变量声明,一个自增表达式、赋值语句,或者一个函数调用(译注:比其它语言丰富)。

+

break和continue语句会改变控制流。和其它语言中的break和continue一样,break会中断当前的循环,并开始执行循环之后的内容,而continue会跳过当前循环,并开始执行下一次循环。这两个语句除了可以控制for循环,还可以用来控制switch和select语句(之后会讲到),在1.3节中我们看到,continue会跳过内层的循环,如果我们想跳过的是更外层的循环的话,我们可以在相应的位置加上label,这样break和continue就可以根据我们的想法来continue和break任意循环。这看起来甚至有点像goto语句的作用了。当然,一般程序员也不会用到这种操作。这两种行为更多地被用到机器生成的代码中。

+

命名类型: 类型声明使得我们可以很方便地给一个特殊类型一个名字。因为struct类型声明通常非常地长,所以我们总要给这种struct取一个名字。本章中就有这样一个例子,二维点类型:

+
type Point struct {
+	X, Y int
+}
+var p Point
+
+

类型声明和命名类型会在第二章中介绍。

+

指针: Go语言提供了指针。指针是一种直接存储了变量的内存地址的数据类型。在其它语言中,比如C语言,指针操作是完全不受约束的。在另外一些语言中,指针一般被处理为“引用”,除了到处传递这些指针之外,并不能对这些指针做太多事情。Go语言在这两种范围中取了一种平衡。指针是可见的内存地址,&操作符可以返回一个变量的内存地址,并且*操作符可以获取指针指向的变量内容,但是在Go语言里没有指针运算,也就是不能像c语言里可以对指针进行加或减操作。我们会在2.3.2中进行详细介绍。

+

方法和接口: 方法是和命名类型关联的一类函数。Go语言里比较特殊的是方法可以被关联到任意一种命名类型。在第六章我们会详细地讲方法。接口是一种抽象类型,这种类型可以让我们以同样的方式来处理不同的固有类型,不用关心它们的具体实现,而只需要关注它们提供的方法。第七章中会详细说明这些内容。

+

包(packages): Go语言提供了一些很好用的package,并且这些package是可以扩展的。Go语言社区已经创造并且分享了很多很多。所以Go语言编程大多数情况下就是用已有的package来写我们自己的代码。通过这本书,我们会讲解一些重要的标准库内的package,但是还是有很多限于篇幅没有去说明,因为我们没法在这样的厚度的书里去做一部代码大全。

+

在你开始写一个新程序之前,最好先去检查一下是不是已经有了现成的库可以帮助你更高效地完成这件事情。你可以在 https://golang.org/pkg 和 https://godoc.org 中找到标准库和社区写的package。godoc这个工具可以让你直接在本地命令行阅读标准库的文档。比如下面这个例子。

+
$ go doc http.ListenAndServe
+package http // import "net/http"
+func ListenAndServe(addr string, handler Handler) error
+    ListenAndServe listens on the TCP network address addr and then
+    calls Serve with handler to handle requests on incoming connections.
+...
+
+

注释: 我们之前已经提到过了在源文件的开头写的注释是这个源文件的文档。在每一个函数之前写一个说明函数行为的注释也是一个好习惯。这些惯例很重要,因为这些内容会被像godoc这样的工具检测到,并且在执行命令时显示这些注释。具体可以参考10.7.4。

+

多行注释可以用 /* ... */ 来包裹,和其它大多数语言一样。在文件一开头的注释一般都是这种形式,或者一大段的解释性的注释文字也会被这符号包住,来避免每一行都需要加//。在注释中//和/*是没什么意义的,所以不要在注释中再嵌入注释。

+

第2章 程序结构

+

Go语言和其他编程语言一样,一个大的程序是由很多小的基础构件组成的。变量保存值,简单的加法和减法运算被组合成较复杂的表达式。基础类型被聚合为数组或结构体等更复杂的数据结构。然后使用if和for之类的控制语句来组织和控制表达式的执行流程。然后多个语句被组织到一个个函数中,以便代码的隔离和复用。函数以源文件和包的方式被组织。

+

我们已经在前面章节的例子中看到了很多例子。在本章中,我们将深入讨论Go程序基础结构方面的一些细节。每个示例程序都是刻意写的简单,这样我们可以减少复杂的算法或数据结构等不相关的问题带来的干扰,从而可以专注于Go语言本身的学习。

+

2.1. 命名

+

Go语言中的函数名、变量名、常量名、类型名、语句标号和包名等所有的命名,都遵循一个简单的命名规则:一个名字必须以一个字母(Unicode字母)或下划线开头,后面可以跟任意数量的字母、数字或下划线。大写字母和小写字母是不同的:heapSort和Heapsort是两个不同的名字。

+

Go语言中类似if和switch的关键字有25个;关键字不能用于自定义名字,只能在特定语法结构中使用。

+
break      default       func     interface   select
+case       defer         go       map         struct
+chan       else          goto     package     switch
+const      fallthrough   if       range       type
+continue   for           import   return      var
+
+

此外,还有大约30多个预定义的名字,比如int和true等,主要对应内建的常量、类型和函数。

+
内建常量: true false iota nil
+
+内建类型: int int8 int16 int32 int64
+          uint uint8 uint16 uint32 uint64 uintptr
+          float32 float64 complex128 complex64
+          bool byte rune string error
+
+内建函数: make len cap new append copy close delete
+          complex real imag
+          panic recover
+
+

这些内部预先定义的名字并不是关键字,你可以在定义中重新使用它们。在一些特殊的场景中重新定义它们也是有意义的,但是也要注意避免过度而引起语义混乱。

+

如果一个名字是在函数内部定义,那么它就只在函数内部有效。如果是在函数外部定义,那么将在当前包的所有文件中都可以访问。名字的开头字母的大小写决定了名字在包外的可见性。如果一个名字是大写字母开头的(译注:必须是在函数外部定义的包级名字;包级函数名本身也是包级名字),那么它将是导出的,也就是说可以被外部的包访问,例如fmt包的Printf函数就是导出的,可以在fmt包外部访问。包本身的名字一般总是用小写字母。

+

名字的长度没有逻辑限制,但是Go语言的风格是尽量使用短小的名字,对于局部变量尤其是这样;你会经常看到i之类的短名字,而不是冗长的theLoopIndex命名。通常来说,如果一个名字的作用域比较大,生命周期也比较长,那么用长的名字将会更有意义。

+

在习惯上,Go语言程序员推荐使用 驼峰式 命名,当名字由几个单词组成时优先使用大小写分隔,而不是优先用下划线分隔。因此,在标准库有QuoteRuneToASCII和parseRequestLine这样的函数命名,但是一般不会用quote_rune_to_ASCII和parse_request_line这样的命名。而像ASCII和HTML这样的缩略词则避免使用大小写混合的写法,它们可能被称为htmlEscape、HTMLEscape或escapeHTML,但不会是escapeHtml。

+

2.2. 声明

+

声明语句定义了程序的各种实体对象以及部分或全部的属性。Go语言主要有四种类型的声明语句:var、const、type和func,分别对应变量、常量、类型和函数实体对象的声明。这一章我们重点讨论变量和类型的声明,第三章将讨论常量的声明,第五章将讨论函数的声明。

+

一个Go语言编写的程序对应一个或多个以.go为文件后缀名的源文件。每个源文件中以包的声明语句开始,说明该源文件是属于哪个包。包声明语句之后是import语句导入依赖的其它包,然后是包一级的类型、变量、常量、函数的声明语句,包一级的各种类型的声明语句的顺序无关紧要(译注:函数内部的名字则必须先声明之后才能使用)。例如,下面的例子中声明了一个常量、一个函数和两个变量:

+

gopl.io/ch2/boiling

+
// Boiling prints the boiling point of water.
+package main
+
+import "fmt"
+
+const boilingF = 212.0
+
+func main() {
+	var f = boilingF
+	var c = (f - 32) * 5 / 9
+	fmt.Printf("boiling point = %g°F or %g°C\n", f, c)
+	// Output:
+	// boiling point = 212°F or 100°C
+}
+
+

其中常量boilingF是在包一级范围声明语句声明的,然后f和c两个变量是在main函数内部声明的声明语句声明的。在包一级声明语句声明的名字可在整个包对应的每个源文件中访问,而不是仅仅在其声明语句所在的源文件中访问。相比之下,局部声明的名字就只能在函数内部很小的范围被访问。

+

一个函数的声明由一个函数名字、参数列表(由函数的调用者提供参数变量的具体值)、一个可选的返回值列表和包含函数定义的函数体组成。如果函数没有返回值,那么返回值列表是省略的。执行函数从函数的第一个语句开始,依次顺序执行直到遇到return返回语句,如果没有返回语句则是执行到函数末尾,然后返回到函数调用者。

+

我们已经看到过很多函数声明和函数调用的例子了,在第五章将深入讨论函数的相关细节,这里只简单解释下。下面的fToC函数封装了温度转换的处理逻辑,这样它只需要被定义一次,就可以在多个地方多次被使用。在这个例子中,main函数就调用了两次fToC函数,分别使用在局部定义的两个常量作为调用函数的参数。

+

gopl.io/ch2/ftoc

+
// Ftoc prints two Fahrenheit-to-Celsius conversions.
+package main
+
+import "fmt"
+
+func main() {
+	const freezingF, boilingF = 32.0, 212.0
+	fmt.Printf("%g°F = %g°C\n", freezingF, fToC(freezingF)) // "32°F = 0°C"
+	fmt.Printf("%g°F = %g°C\n", boilingF, fToC(boilingF))   // "212°F = 100°C"
+}
+
+func fToC(f float64) float64 {
+	return (f - 32) * 5 / 9
+}
+
+

2.3. 变量

+

var声明语句可以创建一个特定类型的变量,然后给变量附加一个名字,并且设置变量的初始值。变量声明的一般语法如下:

+
var 变量名字 类型 = 表达式
+
+

其中“类型”或“= 表达式”两个部分可以省略其中的一个。如果省略的是类型信息,那么将根据初始化表达式来推导变量的类型信息。如果初始化表达式被省略,那么将用零值初始化该变量。 数值类型变量对应的零值是0,布尔类型变量对应的零值是false,字符串类型对应的零值是空字符串,接口或引用类型(包括slice、指针、map、chan和函数)变量对应的零值是nil。数组或结构体等聚合类型对应的零值是每个元素或字段都是对应该类型的零值。

+

零值初始化机制可以确保每个声明的变量总是有一个良好定义的值,因此在Go语言中不存在未初始化的变量。这个特性可以简化很多代码,而且可以在没有增加额外工作的前提下确保边界条件下的合理行为。例如:

+
var s string
+fmt.Println(s) // ""
+
+

这段代码将打印一个空字符串,而不是导致错误或产生不可预知的行为。Go语言程序员应该让一些聚合类型的零值也具有意义,这样可以保证不管任何类型的变量总是有一个合理有效的零值状态。

+

也可以在一个声明语句中同时声明一组变量,或用一组初始化表达式声明并初始化一组变量。如果省略每个变量的类型,将可以声明多个类型不同的变量(类型由初始化表达式推导):

+
var i, j, k int                 // int, int, int
+var b, f, s = true, 2.3, "four" // bool, float64, string
+
+

初始化表达式可以是字面量或任意的表达式。在包级别声明的变量会在main入口函数执行前完成初始化(§2.6.2),局部变量将在声明语句被执行到的时候完成初始化。

+

一组变量也可以通过调用一个函数,由函数返回的多个返回值初始化:

+
var f, err = os.Open(name) // os.Open returns a file and an error
+
+

2.3.1. 简短变量声明

+

在函数内部,有一种称为简短变量声明语句的形式可用于声明和初始化局部变量。它以“名字 := 表达式”形式声明变量,变量的类型根据表达式来自动推导。下面是lissajous函数中的三个简短变量声明语句(§1.4):

+
anim := gif.GIF{LoopCount: nframes}
+freq := rand.Float64() * 3.0
+t := 0.0
+
+

因为简洁和灵活的特点,简短变量声明被广泛用于大部分的局部变量的声明和初始化。var形式的声明语句往往是用于需要显式指定变量类型的地方,或者因为变量稍后会被重新赋值而初始值无关紧要的地方。

+
i := 100                  // an int
+var boiling float64 = 100 // a float64
+var names []string
+var err error
+var p Point
+
+

和var形式声明语句一样,简短变量声明语句也可以用来声明和初始化一组变量:

+
i, j := 0, 1
+
+

但是这种同时声明多个变量的方式应该限制只在可以提高代码可读性的地方使用,比如for语句的循环的初始化语句部分。

+

请记住“:=”是一个变量声明语句,而“=”是一个变量赋值操作。也不要混淆多个变量的声明和元组的多重赋值(§2.4.1),后者是将右边各个表达式的值赋值给左边对应位置的各个变量:

+
i, j = j, i // 交换 i 和 j 的值
+
+

和普通var形式的变量声明语句一样,简短变量声明语句也可以用函数的返回值来声明和初始化变量,像下面的os.Open函数调用将返回两个值:

+
f, err := os.Open(name)
+if err != nil {
+	return err
+}
+// ...use f...
+f.Close()
+
+

这里有一个比较微妙的地方:简短变量声明左边的变量可能并不是全部都是刚刚声明的。如果有一些已经在相同的词法域声明过了(§2.7),那么简短变量声明语句对这些已经声明过的变量就只有赋值行为了。

+

在下面的代码中,第一个语句声明了in和err两个变量。在第二个语句只声明了out一个变量,然后对已经声明的err进行了赋值操作。

+
in, err := os.Open(infile)
+// ...
+out, err := os.Create(outfile)
+
+

简短变量声明语句中必须至少要声明一个新的变量,下面的代码将不能编译通过:

+
f, err := os.Open(infile)
+// ...
+f, err := os.Create(outfile) // compile error: no new variables
+
+

解决的方法是第二个简短变量声明语句改用普通的多重赋值语句。

+

简短变量声明语句只有对已经在同级词法域声明过的变量才和赋值操作语句等价,如果变量是在外部词法域声明的,那么简短变量声明语句将会在当前词法域重新声明一个新的变量。我们在本章后面将会看到类似的例子。

+

2.3.2. 指针

+

一个变量对应一个保存了变量对应类型值的内存空间。普通变量在声明语句创建时被绑定到一个变量名,比如叫x的变量,但是还有很多变量始终以表达式方式引入,例如x[i]或x.f变量。所有这些表达式一般都是读取一个变量的值,除非它们是出现在赋值语句的左边,这种时候是给对应变量赋予一个新的值。

+

一个指针的值是另一个变量的地址。一个指针对应变量在内存中的存储位置。并不是每一个值都会有一个内存地址,但是对于每一个变量必然有对应的内存地址。通过指针,我们可以直接读或更新对应变量的值,而不需要知道该变量的名字(如果变量有名字的话)。

+

如果用“var x int”声明语句声明一个x变量,那么&x表达式(取x变量的内存地址)将产生一个指向该整数变量的指针,指针对应的数据类型是*int,指针被称之为“指向int类型的指针”。如果指针名字为p,那么可以说“p指针指向变量x”,或者说“p指针保存了x变量的内存地址”。同时*p表达式对应p指针指向的变量的值。一般*p表达式读取指针指向的变量的值,这里为int类型的值,同时因为*p对应一个变量,所以该表达式也可以出现在赋值语句的左边,表示更新指针所指向的变量的值。

+
x := 1
+p := &x         // p, of type *int, points to x
+fmt.Println(*p) // "1"
+*p = 2          // equivalent to x = 2
+fmt.Println(x)  // "2"
+
+

对于聚合类型每个成员——比如结构体的每个字段、或者是数组的每个元素——也都是对应一个变量,因此可以被取地址。

+

变量有时候被称为可寻址的值。即使变量由表达式临时生成,那么表达式也必须能接受&取地址操作。

+

任何类型的指针的零值都是nil。如果p指向某个有效变量,那么p != nil测试为真。指针之间也是可以进行相等测试的,只有当它们指向同一个变量或全部是nil时才相等。

+
var x, y int
+fmt.Println(&x == &x, &x == &y, &x == nil) // "true false false"
+
+

在Go语言中,返回函数中局部变量的地址也是安全的。例如下面的代码,调用f函数时创建局部变量v,在局部变量地址被返回之后依然有效,因为指针p依然引用这个变量。

+
var p = f()
+
+func f() *int {
+	v := 1
+	return &v
+}
+
+

每次调用f函数都将返回不同的结果:

+
fmt.Println(f() == f()) // "false"
+
+

因为指针包含了一个变量的地址,因此如果将指针作为参数调用函数,那将可以在函数中通过该指针来更新变量的值。例如下面这个例子就是通过指针来更新变量的值,然后返回更新后的值,可用在一个表达式中(译注:这是对C语言中++v操作的模拟,这里只是为了说明指针的用法,incr函数模拟的做法并不推荐):

+
func incr(p *int) int {
+	*p++ // 非常重要:只是增加p指向的变量的值,并不改变p指针!!!
+	return *p
+}
+
+v := 1
+incr(&v)              // side effect: v is now 2
+fmt.Println(incr(&v)) // "3" (and v is 3)
+
+

每次我们对一个变量取地址,或者复制指针,我们都是为原变量创建了新的别名。例如,*p就是变量v的别名。指针特别有价值的地方在于我们可以不用名字而访问一个变量,但是这是一把双刃剑:要找到一个变量的所有访问者并不容易,我们必须知道变量全部的别名(译注:这是Go语言的垃圾回收器所做的工作)。不仅仅是指针会创建别名,很多其他引用类型也会创建别名,例如slice、map和chan,甚至结构体、数组和接口都会创建所引用变量的别名。

+

指针是实现标准库中flag包的关键技术,它使用命令行参数来设置对应变量的值,而这些对应命令行标志参数的变量可能会零散分布在整个程序中。为了说明这一点,在早些的echo版本中,就包含了两个可选的命令行参数:-n用于忽略行尾的换行符,-s sep用于指定分隔字符(默认是空格)。下面这是第四个版本,对应包路径为gopl.io/ch2/echo4。

+

gopl.io/ch2/echo4

+
// Echo4 prints its command-line arguments.
+package main
+
+import (
+	"flag"
+	"fmt"
+	"strings"
+)
+
+var n = flag.Bool("n", false, "omit trailing newline")
+var sep = flag.String("s", " ", "separator")
+
+func main() {
+	flag.Parse()
+	fmt.Print(strings.Join(flag.Args(), *sep))
+	if !*n {
+		fmt.Println()
+	}
+}
+
+

调用flag.Bool函数会创建一个新的对应布尔型标志参数的变量。它有三个属性:第一个是命令行标志参数的名字“n”,然后是该标志参数的默认值(这里是false),最后是该标志参数对应的描述信息。如果用户在命令行输入了一个无效的标志参数,或者输入-h-help参数,那么将打印所有标志参数的名字、默认值和描述信息。类似的,调用flag.String函数将创建一个对应字符串类型的标志参数变量,同样包含命令行标志参数对应的参数名、默认值、和描述信息。程序中的sepn变量分别是指向对应命令行标志参数变量的指针,因此必须用*sep*n形式的指针语法间接引用它们。

+

当程序运行时,必须在使用标志参数对应的变量之前先调用flag.Parse函数,用于更新每个标志参数对应变量的值(之前是默认值)。对于非标志参数的普通命令行参数可以通过调用flag.Args()函数来访问,返回值对应一个字符串类型的slice。如果在flag.Parse函数解析命令行参数时遇到错误,默认将打印相关的提示信息,然后调用os.Exit(2)终止程序。

+

让我们运行一些echo测试用例:

+
$ go build gopl.io/ch2/echo4
+$ ./echo4 a bc def
+a bc def
+$ ./echo4 -s / a bc def
+a/bc/def
+$ ./echo4 -n a bc def
+a bc def$
+$ ./echo4 -help
+Usage of ./echo4:
+  -n    omit trailing newline
+  -s string
+        separator (default " ")
+
+

2.3.3. new函数

+

另一个创建变量的方法是调用内建的new函数。表达式new(T)将创建一个T类型的匿名变量,初始化为T类型的零值,然后返回变量地址,返回的指针类型为*T

+
p := new(int)   // p, *int 类型, 指向匿名的 int 变量
+fmt.Println(*p) // "0"
+*p = 2          // 设置 int 匿名变量的值为 2
+fmt.Println(*p) // "2"
+
+

用new创建变量和普通变量声明语句方式创建变量没有什么区别,除了不需要声明一个临时变量的名字外,我们还可以在表达式中使用new(T)。换言之,new函数类似是一种语法糖,而不是一个新的基础概念。

+

下面的两个newInt函数有着相同的行为:

+
func newInt() *int {
+	return new(int)
+}
+
+func newInt() *int {
+	var dummy int
+	return &dummy
+}
+
+

每次调用new函数都是返回一个新的变量的地址,因此下面两个地址是不同的:

+
p := new(int)
+q := new(int)
+fmt.Println(p == q) // "false"
+
+

当然也可能有特殊情况:如果两个类型都是空的,也就是说类型的大小是0,例如struct{}[0]int,有可能有相同的地址(依赖具体的语言实现)(译注:请谨慎使用大小为0的类型,因为如果类型的大小为0的话,可能导致Go语言的自动垃圾回收器有不同的行为,具体请查看runtime.SetFinalizer函数相关文档)。

+

new函数使用通常相对比较少,因为对于结构体来说,直接用字面量语法创建新变量的方法会更灵活(§4.4.1)。

+

由于new只是一个预定义的函数,它并不是一个关键字,因此我们可以将new名字重新定义为别的类型。例如下面的例子:

+
func delta(old, new int) int { return new - old }
+
+

由于new被定义为int类型的变量名,因此在delta函数内部是无法使用内置的new函数的。

+

2.3.4. 变量的生命周期

+

变量的生命周期指的是在程序运行期间变量有效存在的时间段。对于在包一级声明的变量来说,它们的生命周期和整个程序的运行周期是一致的。而相比之下,局部变量的生命周期则是动态的:每次从创建一个新变量的声明语句开始,直到该变量不再被引用为止,然后变量的存储空间可能被回收。函数的参数变量和返回值变量都是局部变量。它们在函数每次被调用的时候创建。

+

例如,下面是从1.4节的Lissajous程序摘录的代码片段:

+
for t := 0.0; t < cycles*2*math.Pi; t += res {
+	x := math.Sin(t)
+	y := math.Sin(t*freq + phase)
+	img.SetColorIndex(size+int(x*size+0.5), size+int(y*size+0.5),
+		blackIndex)
+}
+
+

译注:函数的右小括弧也可以另起一行缩进,同时为了防止编译器在行尾自动插入分号而导致的编译错误,可以在末尾的参数变量后面显式插入逗号。像下面这样:

+
for t := 0.0; t < cycles*2*math.Pi; t += res {
+	x := math.Sin(t)
+	y := math.Sin(t*freq + phase)
+	img.SetColorIndex(
+		size+int(x*size+0.5), size+int(y*size+0.5),
+		blackIndex, // 最后插入的逗号不会导致编译错误,这是Go编译器的一个特性
+	)               // 小括弧另起一行缩进,和大括弧的风格保存一致
+}
+
+

在每次循环的开始会创建临时变量t,然后在每次循环迭代中创建临时变量x和y。

+

那么Go语言的自动垃圾收集器是如何知道一个变量是何时可以被回收的呢?这里我们可以避开完整的技术细节,基本的实现思路是,从每个包级的变量和每个当前运行函数的每一个局部变量开始,通过指针或引用的访问路径遍历,是否可以找到该变量。如果不存在这样的访问路径,那么说明该变量是不可达的,也就是说它是否存在并不会影响程序后续的计算结果。

+

因为一个变量的有效周期只取决于是否可达,因此一个循环迭代内部的局部变量的生命周期可能超出其局部作用域。同时,局部变量可能在函数返回之后依然存在。

+

编译器会自动选择在栈上还是在堆上分配局部变量的存储空间,但可能令人惊讶的是,这个选择并不是由用var还是new声明变量的方式决定的。

+
var global *int
+
+func f() {
+	var x int
+	x = 1
+	global = &x
+}
+
+func g() {
+	y := new(int)
+	*y = 1
+}
+
+

f函数里的x变量必须在堆上分配,因为它在函数退出后依然可以通过包一级的global变量找到,虽然它是在函数内部定义的;用Go语言的术语说,这个x局部变量从函数f中逃逸了。相反,当g函数返回时,变量*y将是不可达的,也就是说可以马上被回收的。因此,*y并没有从函数g中逃逸,编译器可以选择在栈上分配*y的存储空间(译注:也可以选择在堆上分配,然后由Go语言的GC回收这个变量的内存空间),虽然这里用的是new方式。其实在任何时候,你并不需为了编写正确的代码而要考虑变量的逃逸行为,要记住的是,逃逸的变量需要额外分配内存,同时对性能的优化可能会产生细微的影响。

+

Go语言的自动垃圾收集器对编写正确的代码是一个巨大的帮助,但也并不是说你完全不用考虑内存了。你虽然不需要显式地分配和释放内存,但是要编写高效的程序你依然需要了解变量的生命周期。例如,如果将指向短生命周期对象的指针保存到具有长生命周期的对象中,特别是保存到全局变量时,会阻止对短生命周期对象的垃圾回收(从而可能影响程序的性能)。

+

2.4. 赋值

+

使用赋值语句可以更新一个变量的值,最简单的赋值语句是将要被赋值的变量放在=的左边,新值的表达式放在=的右边。

+
x = 1                       // 命名变量的赋值
+*p = true                   // 通过指针间接赋值
+person.name = "bob"         // 结构体字段赋值
+count[x] = count[x] * scale // 数组、slice或map的元素赋值
+
+

特定的二元算术运算符和赋值语句的复合操作有一个简洁形式,例如上面最后的语句可以重写为:

+
count[x] *= scale
+
+

这样可以省去对变量表达式的重复计算。

+

数值变量也可以支持++递增和--递减语句(译注:自增和自减是语句,而不是表达式,因此x = i++之类的表达式是错误的):

+
v := 1
+v++    // 等价方式 v = v + 1;v 变成 2
+v--    // 等价方式 v = v - 1;v 变成 1
+
+

2.4.1. 元组赋值

+

元组赋值是另一种形式的赋值语句,它允许同时更新多个变量的值。在赋值之前,赋值语句右边的所有表达式将会先进行求值,然后再统一更新左边对应变量的值。这对于处理有些同时出现在元组赋值语句左右两边的变量很有帮助,例如我们可以这样交换两个变量的值:

+
x, y = y, x
+
+a[i], a[j] = a[j], a[i]
+
+

或者是计算两个整数值的的最大公约数(GCD)(译注:GCD不是那个敏感字,而是greatest common divisor的缩写,欧几里德的GCD是最早的非平凡算法):

+
func gcd(x, y int) int {
+	for y != 0 {
+		x, y = y, x%y
+	}
+	return x
+}
+
+

或者是计算斐波纳契数列(Fibonacci)的第N个数:

+
func fib(n int) int {
+	x, y := 0, 1
+	for i := 0; i < n; i++ {
+		x, y = y, x+y
+	}
+	return x
+}
+
+

元组赋值也可以使一系列琐碎赋值更加紧凑(译注: 特别是在for循环的初始化部分),

+
i, j, k = 2, 3, 5
+
+

但如果表达式太复杂的话,应该尽量避免过度使用元组赋值;因为每个变量单独赋值语句的写法可读性会更好。

+

有些表达式会产生多个值,比如调用一个有多个返回值的函数。当这样一个函数调用出现在元组赋值右边的表达式中时(译注:右边不能再有其它表达式),左边变量的数目必须和右边一致。

+
f, err = os.Open("foo.txt") // function call returns two values
+
+

通常,这类函数会用额外的返回值来表达某种错误类型,例如os.Open是用额外的返回值返回一个error类型的错误,还有一些是用来返回布尔值,通常被称为ok。在稍后我们将看到的三个操作都是类似的用法。如果map查找(§4.3)、类型断言(§7.10)或通道接收(§8.4.2)出现在赋值语句的右边,它们都可能会产生两个结果,有一个额外的布尔结果表示操作是否成功:

+
v, ok = m[key]             // map lookup
+v, ok = x.(T)              // type assertion
+v, ok = <-ch               // channel receive
+
+

译注:map查找(§4.3)、类型断言(§7.10)或通道接收(§8.4.2)出现在赋值语句的右边时,并不一定是产生两个结果,也可能只产生一个结果。对于只产生一个结果的情形,map查找失败时会返回零值,类型断言失败时会发生运行时panic异常,通道接收失败时会返回零值(阻塞不算是失败)。例如下面的例子:

+
v = m[key]                // map查找,失败时返回零值
+v = x.(T)                 // type断言,失败时panic异常
+v = <-ch                  // 管道接收,失败时返回零值(阻塞不算是失败)
+
+_, ok = m[key]            // map返回2个值
+_, ok = mm[""], false     // map返回1个值
+_ = mm[""]                // map返回1个值
+
+

和变量声明一样,我们可以用下划线空白标识符_来丢弃不需要的值。

+
_, err = io.Copy(dst, src) // 丢弃字节数
+_, ok = x.(T)              // 只检测类型,忽略具体值
+
+

2.4.2. 可赋值性

+

赋值语句是显式的赋值形式,但是程序中还有很多地方会发生隐式的赋值行为:函数调用会隐式地将调用参数的值赋值给函数的参数变量,一个返回语句会隐式地将返回操作的值赋值给结果变量,一个复合类型的字面量(§4.2)也会产生赋值行为。例如下面的语句:

+
medals := []string{"gold", "silver", "bronze"}
+
+

隐式地对slice的每个元素进行赋值操作,类似这样写的行为:

+
medals[0] = "gold"
+medals[1] = "silver"
+medals[2] = "bronze"
+
+

map和chan的元素,虽然不是普通的变量,但是也有类似的隐式赋值行为。

+

不管是隐式还是显式地赋值,在赋值语句左边的变量和右边最终的求到的值必须有相同的数据类型。更直白地说,只有右边的值对于左边的变量是可赋值的,赋值语句才是允许的。

+

可赋值性的规则对于不同类型有着不同要求,对每个新类型特殊的地方我们会专门解释。对于目前我们已经讨论过的类型,它的规则是简单的:类型必须完全匹配,nil可以赋值给任何指针或引用类型的变量。常量(§3.6)则有更灵活的赋值规则,因为这样可以避免不必要的显式的类型转换。

+

对于两个值是否可以用==!=进行相等比较的能力也和可赋值能力有关系:对于任何类型的值的相等比较,第二个值必须是对第一个值类型对应的变量是可赋值的,反之亦然。和前面一样,我们会对每个新类型比较特殊的地方做专门的解释。

+

2.5. 类型

+

变量或表达式的类型定义了对应存储值的属性特征,例如数值在内存的存储大小(或者是元素的bit个数),它们在内部是如何表达的,是否支持一些操作符,以及它们自己关联的方法集等。

+

在任何程序中都会存在一些变量有着相同的内部结构,但是却表示完全不同的概念。例如,一个int类型的变量可以用来表示一个循环的迭代索引、或者一个时间戳、或者一个文件描述符、或者一个月份;一个float64类型的变量可以用来表示每秒移动几米的速度、或者是不同温度单位下的温度;一个字符串可以用来表示一个密码或者一个颜色的名称。

+

一个类型声明语句创建了一个新的类型名称,和现有类型具有相同的底层结构。新命名的类型提供了一个方法,用来分隔不同概念的类型,这样即使它们底层类型相同也是不兼容的。

+
type 类型名字 底层类型
+
+

类型声明语句一般出现在包一级,因此如果新创建的类型名字的首字符大写,则在包外部也可以使用。

+

译注:对于中文汉字,Unicode标志都作为小写字母处理,因此中文的命名默认不能导出;不过国内的用户针对该问题提出了不同的看法,根据RobPike的回复,在Go2中有可能会将中日韩等字符当作大写字母处理。下面是RobPik在 Issue763 的回复:

+
+

A solution that's been kicking around for a while:

+

For Go 2 (can't do it before then): Change the definition to “lower case letters and _ are package-local; all else is exported”. Then with non-cased languages, such as Japanese, we can write 日本语 for an exported name and _日本语 for a local name. This rule has no effect, relative to the Go 1 rule, with cased languages. They behave exactly the same.

+
+

为了说明类型声明,我们将不同温度单位分别定义为不同的类型:

+

gopl.io/ch2/tempconv0

+
// Package tempconv performs Celsius and Fahrenheit temperature computations.
+package tempconv
+
+import "fmt"
+
+type Celsius float64    // 摄氏温度
+type Fahrenheit float64 // 华氏温度
+
+const (
+	AbsoluteZeroC Celsius = -273.15 // 绝对零度
+	FreezingC     Celsius = 0       // 结冰点温度
+	BoilingC      Celsius = 100     // 沸水温度
+)
+
+func CToF(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) }
+
+func FToC(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) }
+
+

我们在这个包声明了两种类型:Celsius和Fahrenheit分别对应不同的温度单位。它们虽然有着相同的底层类型float64,但是它们是不同的数据类型,因此它们不可以被相互比较或混在一个表达式运算。刻意区分类型,可以避免一些像无意中使用不同单位的温度混合计算导致的错误;因此需要一个类似Celsius(t)或Fahrenheit(t)形式的显式转型操作才能将float64转为对应的类型。Celsius(t)和Fahrenheit(t)是类型转换操作,它们并不是函数调用。类型转换不会改变值本身,但是会使它们的语义发生变化。另一方面,CToF和FToC两个函数则是对不同温度单位下的温度进行换算,它们会返回不同的值。

+

对于每一个类型T,都有一个对应的类型转换操作T(x),用于将x转为T类型(译注:如果T是指针类型,可能会需要用小括弧包装T,比如(*int)(0))。只有当两个类型的底层基础类型相同时,才允许这种转型操作,或者是两者都是指向相同底层结构的指针类型,这些转换只改变类型而不会影响值本身。如果x是可以赋值给T类型的值,那么x必然也可以被转为T类型,但是一般没有这个必要。

+

数值类型之间的转型也是允许的,并且在字符串和一些特定类型的slice之间也是可以转换的,在下一章我们会看到这样的例子。这类转换可能改变值的表现。例如,将一个浮点数转为整数将丢弃小数部分,将一个字符串转为[]byte类型的slice将拷贝一个字符串数据的副本。在任何情况下,运行时不会发生转换失败的错误(译注: 错误只会发生在编译阶段)。

+

底层数据类型决定了内部结构和表达方式,也决定是否可以像底层类型一样对内置运算符的支持。这意味着,Celsius和Fahrenheit类型的算术运算行为和底层的float64类型是一样的,正如我们所期望的那样。

+
fmt.Printf("%g\n", BoilingC-FreezingC) // "100" °C
+boilingF := CToF(BoilingC)
+fmt.Printf("%g\n", boilingF-CToF(FreezingC)) // "180" °F
+fmt.Printf("%g\n", boilingF-FreezingC)       // compile error: type mismatch
+
+

比较运算符==<也可以用来比较一个命名类型的变量和另一个有相同类型的变量,或有着相同底层类型的未命名类型的值之间做比较。但是如果两个值有着不同的类型,则不能直接进行比较:

+
var c Celsius
+var f Fahrenheit
+fmt.Println(c == 0)          // "true"
+fmt.Println(f >= 0)          // "true"
+fmt.Println(c == f)          // compile error: type mismatch
+fmt.Println(c == Celsius(f)) // "true"!
+
+

注意最后那个语句。尽管看起来像函数调用,但是Celsius(f)是类型转换操作,它并不会改变值,仅仅是改变值的类型而已。测试为真的原因是因为c和f都是零值。

+

一个命名的类型可以提供书写方便,特别是可以避免一遍又一遍地书写复杂类型(译注:例如用匿名的结构体定义变量)。虽然对于像float64这种简单的底层类型没有简洁很多,但是如果是复杂的类型将会简洁很多,特别是我们即将讨论的结构体类型。

+

命名类型还可以为该类型的值定义新的行为。这些行为表示为一组关联到该类型的函数集合,我们称为类型的方法集。我们将在第六章中讨论方法的细节,这里只说些简单用法。

+

下面的声明语句,Celsius类型的参数c出现在了函数名的前面,表示声明的是Celsius类型的一个名叫String的方法,该方法返回该类型对象c带着°C温度单位的字符串:

+
func (c Celsius) String() string { return fmt.Sprintf("%g°C", c) }
+
+

许多类型都会定义一个String方法,因为当使用fmt包的打印方法时,将会优先使用该类型对应的String方法返回的结果打印,我们将在7.1节讲述。

+
c := FToC(212.0)
+fmt.Println(c.String()) // "100°C"
+fmt.Printf("%v\n", c)   // "100°C"; no need to call String explicitly
+fmt.Printf("%s\n", c)   // "100°C"
+fmt.Println(c)          // "100°C"
+fmt.Printf("%g\n", c)   // "100"; does not call String
+fmt.Println(float64(c)) // "100"; does not call String
+
+

2.6. 包和文件

+

Go语言中的包和其他语言的库或模块的概念类似,目的都是为了支持模块化、封装、单独编译和代码重用。一个包的源代码保存在一个或多个以.go为文件后缀名的源文件中,通常一个包所在目录路径的后缀是包的导入路径;例如包gopl.io/ch1/helloworld对应的目录路径是$GOPATH/src/gopl.io/ch1/helloworld。

+

每个包都对应一个独立的名字空间。例如,在image包中的Decode函数和在unicode/utf16包中的 Decode函数是不同的。要在外部引用该函数,必须显式使用image.Decode或utf16.Decode形式访问。

+

包还可以让我们通过控制哪些名字是外部可见的来隐藏内部实现信息。在Go语言中,一个简单的规则是:如果一个名字是大写字母开头的,那么该名字是导出的(译注:因为汉字不区分大小写,因此汉字开头的名字是没有导出的)。

+

为了演示包基本的用法,先假设我们的温度转换软件已经很流行,我们希望到Go语言社区也能使用这个包。我们该如何做呢?

+

让我们创建一个名为gopl.io/ch2/tempconv的包,这是前面例子的一个改进版本。(这里我们没有按照惯例按顺序对例子进行编号,因此包路径看起来更像一个真实的包)包代码存储在两个源文件中,用来演示如何在一个源文件声明然后在其他的源文件访问;虽然在现实中,这样小的包一般只需要一个文件。

+

我们把变量的声明、对应的常量,还有方法都放到tempconv.go源文件中:

+

gopl.io/ch2/tempconv

+
// Package tempconv performs Celsius and Fahrenheit conversions.
+package tempconv
+
+import "fmt"
+
+type Celsius float64
+type Fahrenheit float64
+
+const (
+	AbsoluteZeroC Celsius = -273.15
+	FreezingC     Celsius = 0
+	BoilingC      Celsius = 100
+)
+
+func (c Celsius) String() string    { return fmt.Sprintf("%g°C", c) }
+func (f Fahrenheit) String() string { return fmt.Sprintf("%g°F", f) }
+
+

转换函数则放在另一个conv.go源文件中:

+
package tempconv
+
+// CToF converts a Celsius temperature to Fahrenheit.
+func CToF(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) }
+
+// FToC converts a Fahrenheit temperature to Celsius.
+func FToC(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) }
+
+

每个源文件都是以包的声明语句开始,用来指明包的名字。当包被导入的时候,包内的成员将通过类似tempconv.CToF的形式访问。而包级别的名字,例如在一个文件声明的类型和常量,在同一个包的其他源文件也是可以直接访问的,就好像所有代码都在一个文件一样。要注意的是tempconv.go源文件导入了fmt包,但是conv.go源文件并没有,因为这个源文件中的代码并没有用到fmt包。

+

因为包级别的常量名都是以大写字母开头,它们可以像tempconv.AbsoluteZeroC这样被外部代码访问:

+
fmt.Printf("Brrrr! %v\n", tempconv.AbsoluteZeroC) // "Brrrr! -273.15°C"
+
+

要将摄氏温度转换为华氏温度,需要先用import语句导入gopl.io/ch2/tempconv包,然后就可以使用下面的代码进行转换了:

+
fmt.Println(tempconv.CToF(tempconv.BoilingC)) // "212°F"
+
+

在每个源文件的包声明前紧跟着的注释是包注释(§10.7.4)。通常,包注释的第一句应该先是包的功能概要说明。一个包通常只有一个源文件有包注释(译注:如果有多个包注释,目前的文档工具会根据源文件名的先后顺序将它们链接为一个包注释)。如果包注释很大,通常会放到一个独立的doc.go文件中。

+

练习 2.1: 向tempconv包添加类型、常量和函数用来处理Kelvin绝对温度的转换,Kelvin 绝对零度是−273.15°C,Kelvin绝对温度1K和摄氏度1°C的单位间隔是一样的。

+

2.6.1. 导入包

+

在Go语言程序中,每个包都有一个全局唯一的导入路径。导入语句中类似"gopl.io/ch2/tempconv"的字符串对应包的导入路径。Go语言的规范并没有定义这些字符串的具体含义或包来自哪里,它们是由构建工具来解释的。当使用Go语言自带的go工具箱时(第十章),一个导入路径代表一个目录中的一个或多个Go源文件。

+

除了包的导入路径,每个包还有一个包名,包名一般是短小的名字(并不要求包名是唯一的),包名在包的声明处指定。按照惯例,一个包的名字和包的导入路径的最后一个字段相同,例如gopl.io/ch2/tempconv包的名字一般是tempconv。

+

要使用gopl.io/ch2/tempconv包,需要先导入:

+

gopl.io/ch2/cf

+
// Cf converts its numeric argument to Celsius and Fahrenheit.
+package main
+
+import (
+	"fmt"
+	"os"
+	"strconv"
+
+	"gopl.io/ch2/tempconv"
+)
+
+func main() {
+	for _, arg := range os.Args[1:] {
+		t, err := strconv.ParseFloat(arg, 64)
+		if err != nil {
+			fmt.Fprintf(os.Stderr, "cf: %v\n", err)
+			os.Exit(1)
+		}
+		f := tempconv.Fahrenheit(t)
+		c := tempconv.Celsius(t)
+		fmt.Printf("%s = %s, %s = %s\n",
+			f, tempconv.FToC(f), c, tempconv.CToF(c))
+	}
+}
+
+

导入语句将导入的包绑定到一个短小的名字,然后通过该短小的名字就可以引用包中导出的全部内容。上面的导入声明将允许我们以tempconv.CToF的形式来访问gopl.io/ch2/tempconv包中的内容。在默认情况下,导入的包绑定到tempconv名字(译注:指包声明语句指定的名字),但是我们也可以绑定到另一个名称,以避免名字冲突(§10.4)。

+

cf程序将命令行输入的一个温度在Celsius和Fahrenheit温度单位之间转换:

+
$ go build gopl.io/ch2/cf
+$ ./cf 32
+32°F = 0°C, 32°C = 89.6°F
+$ ./cf 212
+212°F = 100°C, 212°C = 413.6°F
+$ ./cf -40
+-40°F = -40°C, -40°C = -40°F
+
+

如果导入了一个包,但是又没有使用该包将被当作一个编译错误处理。这种强制规则可以有效减少不必要的依赖,虽然在调试期间可能会让人讨厌,因为删除一个类似log.Print("got here!")的打印语句可能导致需要同时删除log包导入声明,否则,编译器将会发出一个错误。在这种情况下,我们需要将不必要的导入删除或注释掉。

+

不过有更好的解决方案,我们可以使用golang.org/x/tools/cmd/goimports导入工具,它可以根据需要自动添加或删除导入的包;许多编辑器都可以集成goimports工具,然后在保存文件的时候自动运行。类似的还有gofmt工具,可以用来格式化Go源文件。

+

练习 2.2: 写一个通用的单位转换程序,用类似cf程序的方式从命令行读取参数,如果缺省的话则是从标准输入读取参数,然后做类似Celsius和Fahrenheit的单位转换,长度单位可以对应英尺和米,重量单位可以对应磅和公斤等。

+

2.6.2. 包的初始化

+

包的初始化首先是解决包级变量的依赖顺序,然后按照包级变量声明出现的顺序依次初始化:

+
var a = b + c // a 第三个初始化, 为 3
+var b = f()   // b 第二个初始化, 为 2, 通过调用 f (依赖c)
+var c = 1     // c 第一个初始化, 为 1
+
+func f() int { return c + 1 }
+
+

如果包中含有多个.go源文件,它们将按照发给编译器的顺序进行初始化,Go语言的构建工具首先会将.go文件根据文件名排序,然后依次调用编译器编译。

+

对于在包级别声明的变量,如果有初始化表达式则用表达式初始化,还有一些没有初始化表达式的,例如某些表格数据初始化并不是一个简单的赋值过程。在这种情况下,我们可以用一个特殊的init初始化函数来简化初始化工作。每个文件都可以包含多个init初始化函数

+
func init() { /* ... */ }
+
+

这样的init初始化函数除了不能被调用或引用外,其他行为和普通函数类似。在每个文件中的init初始化函数,在程序开始执行时按照它们声明的顺序被自动调用。

+

每个包在解决依赖的前提下,以导入声明的顺序初始化,每个包只会被初始化一次。因此,如果一个p包导入了q包,那么在p包初始化的时候可以认为q包必然已经初始化过了。初始化工作是自下而上进行的,main包最后被初始化。以这种方式,可以确保在main函数执行之前,所有依赖的包都已经完成初始化工作了。

+

下面的代码定义了一个PopCount函数,用于返回一个数字中含二进制1bit的个数。它使用init初始化函数来生成辅助表格pc,pc表格用于处理每个8bit宽度的数字含二进制的1bit的bit个数,这样的话在处理64bit宽度的数字时就没有必要循环64次,只需要8次查表就可以了。(这并不是最快的统计1bit数目的算法,但是它可以方便演示init函数的用法,并且演示了如何预生成辅助表格,这是编程中常用的技术)。

+

gopl.io/ch2/popcount

+
package popcount
+
+// pc[i] is the population count of i.
+var pc [256]byte
+
+func init() {
+	for i := range pc {
+		pc[i] = pc[i/2] + byte(i&1)
+	}
+}
+
+// PopCount returns the population count (number of set bits) of x.
+func PopCount(x uint64) int {
+	return int(pc[byte(x>>(0*8))] +
+		pc[byte(x>>(1*8))] +
+		pc[byte(x>>(2*8))] +
+		pc[byte(x>>(3*8))] +
+		pc[byte(x>>(4*8))] +
+		pc[byte(x>>(5*8))] +
+		pc[byte(x>>(6*8))] +
+		pc[byte(x>>(7*8))])
+}
+
+

译注:对于pc这类需要复杂处理的初始化,可以通过将初始化逻辑包装为一个匿名函数处理,像下面这样:

+
// pc[i] is the population count of i.
+var pc [256]byte = func() (pc [256]byte) {
+	for i := range pc {
+		pc[i] = pc[i/2] + byte(i&1)
+	}
+	return
+}()
+
+

要注意的是在init函数中,range循环只使用了索引,省略了没有用到的值部分。循环也可以这样写:

+
for i, _ := range pc {
+
+

我们在下一节和10.5节还将看到其它使用init函数的地方。

+

练习 2.3: 重写PopCount函数,用一个循环代替单一的表达式。比较两个版本的性能。(11.4节将展示如何系统地比较两个不同实现的性能。)

+

练习 2.4: 用移位算法重写PopCount函数,每次测试最右边的1bit,然后统计总数。比较和查表算法的性能差异。

+

练习 2.5: 表达式x&(x-1)用于将x的最低的一个非零的bit位清零。使用这个算法重写PopCount函数,然后比较性能。

+

2.7. 作用域

+

一个声明语句将程序中的实体和一个名字关联,比如一个函数或一个变量。声明语句的作用域是指源代码中可以有效使用这个名字的范围。

+

不要将作用域和生命周期混为一谈。声明语句的作用域对应的是一个源代码的文本区域;它是一个编译时的属性。一个变量的生命周期是指程序运行时变量存在的有效时间段,在此时间区域内它可以被程序的其他部分引用;是一个运行时的概念。

+

句法块是由花括弧所包含的一系列语句,就像函数体或循环体花括弧包裹的内容一样。句法块内部声明的名字是无法被外部块访问的。这个块决定了内部声明的名字的作用域范围。我们可以把块(block)的概念推广到包括其他声明的群组,这些声明在代码中并未显式地使用花括号包裹起来,我们称之为词法块。对全局的源代码来说,存在一个整体的词法块,称为全局词法块;对于每个包;每个for、if和switch语句,也都有对应词法块;每个switch或select的分支也有独立的词法块;当然也包括显式书写的词法块(花括弧包含的语句)。

+

声明语句对应的词法域决定了作用域范围的大小。对于内置的类型、函数和常量,比如int、len和true等是在全局作用域的,因此可以在整个程序中直接使用。任何在函数外部(也就是包级语法域)声明的名字可以在同一个包的任何源文件中访问的。对于导入的包,例如tempconv导入的fmt包,则是对应源文件级的作用域,因此只能在当前的文件中访问导入的fmt包,当前包的其它源文件无法访问在当前源文件导入的包。还有许多声明语句,比如tempconv.CToF函数中的变量c,则是局部作用域的,它只能在函数内部(甚至只能是局部的某些部分)访问。

+

控制流标号,就是break、continue或goto语句后面跟着的那种标号,则是函数级的作用域。

+

一个程序可能包含多个同名的声明,只要它们在不同的词法域就没有关系。例如,你可以声明一个局部变量,和包级的变量同名。或者是像2.3.3节的例子那样,你可以将一个函数参数的名字声明为new,虽然内置的new是全局作用域的。但是物极必反,如果滥用不同词法域可重名的特性的话,可能导致程序很难阅读。

+

当编译器遇到一个名字引用时,它会对其定义进行查找,查找过程从最内层的词法域向全局的作用域进行。如果查找失败,则报告“未声明的名字”这样的错误。如果该名字在内部和外部的块分别声明过,则内部块的声明首先被找到。在这种情况下,内部声明屏蔽了外部同名的声明,让外部的声明的名字无法被访问:

+
func f() {}
+
+var g = "g"
+
+func main() {
+	f := "f"
+	fmt.Println(f) // "f"; local var f shadows package-level func f
+	fmt.Println(g) // "g"; package-level var
+	fmt.Println(h) // compile error: undefined: h
+}
+
+

在函数中词法域可以深度嵌套,因此内部的一个声明可能屏蔽外部的声明。还有许多语法块是if或for等控制流语句构造的。下面的代码有三个不同的变量x,因为它们是定义在不同的词法域(这个例子只是为了演示作用域规则,但不是好的编程风格)。

+
func main() {
+	x := "hello!"
+	for i := 0; i < len(x); i++ {
+		x := x[i]
+		if x != '!' {
+			x := x + 'A' - 'a'
+			fmt.Printf("%c", x) // "HELLO" (one letter per iteration)
+		}
+	}
+}
+
+

x[i]x + 'A' - 'a'声明语句的初始化的表达式中都引用了外部作用域声明的x变量,稍后我们会解释这个。(注意,后面的表达式与unicode.ToUpper并不等价。)

+

正如上面例子所示,并不是所有的词法域都显式地对应到由花括弧包含的语句;还有一些隐含的规则。上面的for语句创建了两个词法域:花括弧包含的是显式的部分,是for的循环体部分词法域,另外一个隐式的部分则是循环的初始化部分,比如用于迭代变量i的初始化。隐式的词法域部分的作用域还包含条件测试部分和循环后的迭代部分(i++),当然也包含循环体词法域。

+

下面的例子同样有三个不同的x变量,每个声明在不同的词法域,一个在函数体词法域,一个在for隐式的初始化词法域,一个在for循环体词法域;只有两个块是显式创建的:

+
func main() {
+	x := "hello"
+	for _, x := range x {
+		x := x + 'A' - 'a'
+		fmt.Printf("%c", x) // "HELLO" (one letter per iteration)
+	}
+}
+
+

和for循环类似,if和switch语句也会在条件部分创建隐式词法域,还有它们对应的执行体词法域。下面的if-else测试链演示了x和y的有效作用域范围:

+
if x := f(); x == 0 {
+	fmt.Println(x)
+} else if y := g(x); x == y {
+	fmt.Println(x, y)
+} else {
+	fmt.Println(x, y)
+}
+fmt.Println(x, y) // compile error: x and y are not visible here
+
+

第二个if语句嵌套在第一个内部,因此第一个if语句条件初始化词法域声明的变量在第二个if中也可以访问。switch语句的每个分支也有类似的词法域规则:条件部分为一个隐式词法域,然后是每个分支的词法域。

+

在包级别,声明的顺序并不会影响作用域范围,因此一个先声明的可以引用它自身或者是引用后面的一个声明,这可以让我们定义一些相互嵌套或递归的类型或函数。但是如果一个变量或常量递归引用了自身,则会产生编译错误。

+

在这个程序中:

+
if f, err := os.Open(fname); err != nil { // compile error: unused: f
+	return err
+}
+f.ReadByte() // compile error: undefined f
+f.Close()    // compile error: undefined f
+
+

变量f的作用域只在if语句内,因此后面的语句将无法引入它,这将导致编译错误。你可能会收到一个局部变量f没有声明的错误提示,具体错误信息依赖编译器的实现。

+

通常需要在if之前声明变量,这样可以确保后面的语句依然可以访问变量:

+
f, err := os.Open(fname)
+if err != nil {
+	return err
+}
+f.ReadByte()
+f.Close()
+
+

你可能会考虑通过将ReadByte和Close移动到if的else块来解决这个问题:

+
if f, err := os.Open(fname); err != nil {
+	return err
+} else {
+	// f and err are visible here too
+	f.ReadByte()
+	f.Close()
+}
+
+

但这不是Go语言推荐的做法,Go语言的习惯是在if中处理错误然后直接返回,这样可以确保正常执行的语句不需要代码缩进。

+

要特别注意短变量声明语句的作用域范围,考虑下面的程序,它的目的是获取当前的工作目录然后保存到一个包级的变量中。这本来可以通过直接调用os.Getwd完成,但是将这个从主逻辑中分离出来可能会更好,特别是在需要处理错误的时候。函数log.Fatalf用于打印日志信息,然后调用os.Exit(1)终止程序。

+
var cwd string
+
+func init() {
+	cwd, err := os.Getwd() // compile error: unused: cwd
+	if err != nil {
+		log.Fatalf("os.Getwd failed: %v", err)
+	}
+}
+
+

虽然cwd在外部已经声明过,但是:=语句还是将cwd和err重新声明为新的局部变量。因为内部声明的cwd将屏蔽外部的声明,因此上面的代码并不会正确更新包级声明的cwd变量。

+

由于当前的编译器会检测到局部声明的cwd并没有使用,然后报告这可能是一个错误,但是这种检测并不可靠。因为一些小的代码变更,例如增加一个局部cwd的打印语句,就可能导致这种检测失效。

+
var cwd string
+
+func init() {
+	cwd, err := os.Getwd() // NOTE: wrong!
+	if err != nil {
+		log.Fatalf("os.Getwd failed: %v", err)
+	}
+	log.Printf("Working directory = %s", cwd)
+}
+
+

全局的cwd变量依然是没有被正确初始化的,而且看似正常的日志输出更是让这个BUG更加隐晦。

+

有许多方式可以避免出现类似潜在的问题。最直接的方法是通过单独声明err变量,来避免使用:=的简短声明方式:

+
var cwd string
+
+func init() {
+	var err error
+	cwd, err = os.Getwd()
+	if err != nil {
+		log.Fatalf("os.Getwd failed: %v", err)
+	}
+}
+
+

我们已经看到包、文件、声明和语句如何来表达一个程序结构。在下面的两个章节,我们将探讨数据的结构。

+

第3章 基础数据类型

+

虽然从底层而言,所有的数据都是由比特组成,但计算机一般操作的是固定大小的数,如整数、浮点数、比特数组、内存地址等。进一步将这些数组织在一起,就可表达更多的对象,例如数据包、像素点、诗歌,甚至其他任何对象。Go语言提供了丰富的数据组织形式,这依赖于Go语言内置的数据类型。这些内置的数据类型,兼顾了硬件的特性和表达复杂数据结构的便捷性。

+

Go语言将数据类型分为四类:基础类型、复合类型、引用类型和接口类型。本章介绍基础类型,包括:数字、字符串和布尔型。复合数据类型——数组(§4.1)和结构体(§4.2)——是通过组合简单类型,来表达更加复杂的数据结构。引用类型包括指针(§2.3.2)、切片(§4.2))、字典(§4.3)、函数(§5)、通道(§8),虽然数据种类很多,但它们都是对程序中一个变量或状态的间接引用。这意味着对任一引用类型数据的修改都会影响所有该引用的拷贝。我们将在第7章介绍接口类型。

+

3.1. 整型

+

Go语言的数值类型包括几种不同大小的整数、浮点数和复数。每种数值类型都决定了对应的大小范围和是否支持正负符号。让我们先从整数类型开始介绍。

+

Go语言同时提供了有符号和无符号类型的整数运算。这里有int8、int16、int32和int64四种截然不同大小的有符号整数类型,分别对应8、16、32、64bit大小的有符号整数,与此对应的是uint8、uint16、uint32和uint64四种无符号整数类型。

+

这里还有两种一般对应特定CPU平台机器字大小的有符号和无符号整数int和uint;其中int是应用最广泛的数值类型。这两种类型都有同样的大小,32或64bit,但是我们不能对此做任何的假设;因为不同的编译器即使在相同的硬件平台上可能产生不同的大小。

+

Unicode字符rune类型是和int32等价的类型,通常用于表示一个Unicode码点。这两个名称可以互换使用。同样byte也是uint8类型的等价类型,byte类型一般用于强调数值是一个原始的数据而不是一个小的整数。

+

最后,还有一种无符号的整数类型uintptr,没有指定具体的bit大小但是足以容纳指针。uintptr类型只有在底层编程时才需要,特别是Go语言和C语言函数库或操作系统接口相交互的地方。我们将在第十三章的unsafe包相关部分看到类似的例子。

+

不管它们的具体大小,int、uint和uintptr是不同类型的兄弟类型。其中int和int32也是不同的类型,即使int的大小也是32bit,在需要将int当作int32类型的地方需要一个显式的类型转换操作,反之亦然。

+

其中有符号整数采用2的补码形式表示,也就是最高bit位用来表示符号位,一个n-bit的有符号数的值域是从$-2^{n-1}$到$2^{n-1}-1$。无符号整数的所有bit位都用于表示非负数,值域是0到$2^n-1$。例如,int8类型整数的值域是从-128到127,而uint8类型整数的值域是从0到255。

+

下面是Go语言中关于算术运算、逻辑运算和比较运算的二元运算符,它们按照优先级递减的顺序排列:

+
*      /      %      <<       >>     &       &^
++      -      |      ^
+==     !=     <      <=       >      >=
+&&
+||
+
+

二元运算符有五种优先级。在同一个优先级,使用左优先结合规则,但是使用括号可以明确优先顺序,使用括号也可以用于提升优先级,例如mask & (1 << 28)

+

对于上表中前两行的运算符,例如+运算符还有一个与赋值相结合的对应运算符+=,可以用于简化赋值语句。

+

算术运算符+-*/可以适用于整数、浮点数和复数,但是取模运算符%仅用于整数间的运算。对于不同编程语言,%取模运算的行为可能并不相同。在Go语言中,%取模运算符的符号和被取模数的符号总是一致的,因此-5%3-5%-3结果都是-2。除法运算符/的行为则依赖于操作数是否全为整数,比如5.0/4.0的结果是1.25,但是5/4的结果是1,因为整数除法会向着0方向截断余数。

+

一个算术运算的结果,不管是有符号或者是无符号的,如果需要更多的bit位才能正确表示的话,就说明计算结果是溢出了。超出的高位的bit位部分将被丢弃。如果原始的数值是有符号类型,而且最左边的bit位是1的话,那么最终结果可能是负的,例如int8的例子:

+
var u uint8 = 255
+fmt.Println(u, u+1, u*u) // "255 0 1"
+
+var i int8 = 127
+fmt.Println(i, i+1, i*i) // "127 -128 1"
+
+

两个相同的整数类型可以使用下面的二元比较运算符进行比较;比较表达式的结果是布尔类型。

+
==    等于
+!=    不等于
+<     小于
+<=    小于等于
+>     大于
+>=    大于等于
+
+

事实上,布尔型、数字类型和字符串等基本类型都是可比较的,也就是说两个相同类型的值可以用==和!=进行比较。此外,整数、浮点数和字符串可以根据比较结果排序。许多其它类型的值可能是不可比较的,因此也就可能是不可排序的。对于我们遇到的每种类型,我们需要保证规则的一致性。

+

这里是一元的加法和减法运算符:

+
+      一元加法(无效果)
+-      负数
+
+

对于整数,+x是0+x的简写,-x则是0-x的简写;对于浮点数和复数,+x就是x,-x则是x 的负数。

+

Go语言还提供了以下的bit位操作运算符,前面4个操作运算符并不区分是有符号还是无符号数:

+
&      位运算 AND
+|      位运算 OR
+^      位运算 XOR
+&^     位清空(AND NOT)
+<<     左移
+>>     右移
+
+

位操作运算符^作为二元运算符时是按位异或(XOR),当用作一元运算符时表示按位取反;也就是说,它返回一个每个bit位都取反的数。位操作运算符&^用于按位置零(AND NOT):如果对应y中bit位为1的话,表达式z = x &^ y结果z的对应的bit位为0,否则z对应的bit位等于x相应的bit位的值。

+

下面的代码演示了如何使用位操作解释uint8类型值的8个独立的bit位。它使用了Printf函数的%b参数打印二进制格式的数字;其中%08b中08表示打印至少8个字符宽度,不足的前缀部分用0填充。

+
var x uint8 = 1<<1 | 1<<5
+var y uint8 = 1<<1 | 1<<2
+
+fmt.Printf("%08b\n", x) // "00100010", the set {1, 5}
+fmt.Printf("%08b\n", y) // "00000110", the set {1, 2}
+
+fmt.Printf("%08b\n", x&y)  // "00000010", the intersection {1}
+fmt.Printf("%08b\n", x|y)  // "00100110", the union {1, 2, 5}
+fmt.Printf("%08b\n", x^y)  // "00100100", the symmetric difference {2, 5}
+fmt.Printf("%08b\n", x&^y) // "00100000", the difference {5}
+
+for i := uint(0); i < 8; i++ {
+	if x&(1<<i) != 0 { // membership test
+		fmt.Println(i) // "1", "5"
+	}
+}
+
+fmt.Printf("%08b\n", x<<1) // "01000100", the set {2, 6}
+fmt.Printf("%08b\n", x>>1) // "00010001", the set {0, 4}
+
+

(6.5节给出了一个可以远大于一个字节的整数集的实现。)

+

x<<nx>>n移位运算中,决定了移位操作的bit数部分必须是无符号数;被操作的x可以是有符号数或无符号数。算术上,一个x<<n左移运算等价于乘以$2^n$,一个x>>n右移运算等价于除以$2^n$。

+

左移运算用零填充右边空缺的bit位,无符号数的右移运算也是用0填充左边空缺的bit位,但是有符号数的右移运算会用符号位的值填充左边空缺的bit位。因为这个原因,最好用无符号运算,这样你可以将整数完全当作一个bit位模式处理。

+

尽管Go语言提供了无符号数的运算,但即使数值本身不可能出现负数,我们还是倾向于使用有符号的int类型,就像数组的长度那样,虽然使用uint无符号类型似乎是一个更合理的选择。事实上,内置的len函数返回一个有符号的int,我们可以像下面例子那样处理逆序循环。

+
medals := []string{"gold", "silver", "bronze"}
+for i := len(medals) - 1; i >= 0; i-- {
+	fmt.Println(medals[i]) // "bronze", "silver", "gold"
+}
+
+

另一个选择对于上面的例子来说将是灾难性的。如果len函数返回一个无符号数,那么i也将是无符号的uint类型,然后条件i >= 0则永远为真。在三次迭代之后,也就是i == 0时,i--语句将不会产生-1,而是变成一个uint类型的最大值(可能是$2^64-1$),然后medals[i]表达式运行时将发生panic异常(§5.9),也就是试图访问一个slice范围以外的元素。

+

出于这个原因,无符号数往往只有在位运算或其它特殊的运算场景才会使用,就像bit集合、分析二进制文件格式或者是哈希和加密操作等。它们通常并不用于仅仅是表达非负数量的场合。

+

一般来说,需要一个显式的转换将一个值从一种类型转化为另一种类型,并且算术和逻辑运算的二元操作中必须是相同的类型。虽然这偶尔会导致需要很长的表达式,但是它消除了所有和类型相关的问题,而且也使得程序容易理解。

+

在很多场景,会遇到类似下面代码的常见的错误:

+
var apples int32 = 1
+var oranges int16 = 2
+var compote int = apples + oranges // compile error
+
+

当尝试编译这三个语句时,将产生一个错误信息:

+
invalid operation: apples + oranges (mismatched types int32 and int16)
+
+

这种类型不匹配的问题可以有几种不同的方法修复,最常见方法是将它们都显式转型为一个常见类型:

+
var compote = int(apples) + int(oranges)
+
+

如2.5节所述,对于每种类型T,如果转换允许的话,类型转换操作T(x)将x转换为T类型。许多整数之间的相互转换并不会改变数值;它们只是告诉编译器如何解释这个值。但是对于将一个大尺寸的整数类型转为一个小尺寸的整数类型,或者是将一个浮点数转为整数,可能会改变数值或丢失精度:

+
f := 3.141 // a float64
+i := int(f)
+fmt.Println(f, i) // "3.141 3"
+f = 1.99
+fmt.Println(int(f)) // "1"
+
+

浮点数到整数的转换将丢失任何小数部分,然后向数轴零方向截断。你应该避免对可能会超出目标类型表示范围的数值做类型转换,因为截断的行为可能依赖于具体的实现:

+
f := 1e100  // a float64
+i := int(f) // 结果依赖于具体实现
+
+

任何大小的整数字面值都可以用以0开始的八进制格式书写,例如0666;或用以0x或0X开头的十六进制格式书写,例如0xdeadbeef。十六进制数字可以用大写或小写字母。如今八进制数据通常用于POSIX操作系统上的文件访问权限标志,十六进制数字则更强调数字值的bit位模式。

+

当使用fmt包打印一个数值时,我们可以用%d、%o或%x参数控制输出的进制格式,就像下面的例子:

+
o := 0666
+fmt.Printf("%d %[1]o %#[1]o\n", o) // "438 666 0666"
+x := int64(0xdeadbeef)
+fmt.Printf("%d %[1]x %#[1]x %#[1]X\n", x)
+// Output:
+// 3735928559 deadbeef 0xdeadbeef 0XDEADBEEF
+
+

请注意fmt的两个使用技巧。通常Printf格式化字符串包含多个%参数时将会包含对应相同数量的额外操作数,但是%之后的[1]副词告诉Printf函数再次使用第一个操作数。第二,%后的#副词告诉Printf在用%o、%x或%X输出时生成0、0x或0X前缀。

+

字符面值通过一对单引号直接包含对应字符。最简单的例子是ASCII中类似'a'写法的字符面值,但是我们也可以通过转义的数值来表示任意的Unicode码点对应的字符,马上将会看到这样的例子。

+

字符使用%c参数打印,或者是用%q参数打印带单引号的字符:

+
ascii := 'a'
+unicode := '国'
+newline := '\n'
+fmt.Printf("%d %[1]c %[1]q\n", ascii)   // "97 a 'a'"
+fmt.Printf("%d %[1]c %[1]q\n", unicode) // "22269 国 '国'"
+fmt.Printf("%d %[1]q\n", newline)       // "10 '\n'"
+
+

3.2. 浮点数

+

Go语言提供了两种精度的浮点数,float32和float64。它们的算术规范由IEEE754浮点数国际标准定义,该浮点数规范被所有现代的CPU支持。

+

这些浮点数类型的取值范围可以从很微小到很巨大。浮点数的范围极限值可以在math包找到。常量math.MaxFloat32表示float32能表示的最大数值,大约是 3.4e38;对应的math.MaxFloat64常量大约是1.8e308。它们分别能表示的最小值近似为1.4e-45和4.9e-324。

+

一个float32类型的浮点数可以提供大约6个十进制数的精度,而float64则可以提供约15个十进制数的精度;通常应该优先使用float64类型,因为float32类型的累计计算误差很容易扩散,并且float32能精确表示的正整数并不是很大(译注:因为float32的有效bit位只有23个,其它的bit位用于指数和符号;当整数大于23bit能表达的范围时,float32的表示将出现误差):

+
var f float32 = 16777216 // 1 << 24
+fmt.Println(f == f+1)    // "true"!
+
+

浮点数的字面值可以直接写小数部分,像这样:

+
const e = 2.71828 // (approximately)
+
+

小数点前面或后面的数字都可能被省略(例如.707或1.)。很小或很大的数最好用科学计数法书写,通过e或E来指定指数部分:

+
const Avogadro = 6.02214129e23  // 阿伏伽德罗常数
+const Planck   = 6.62606957e-34 // 普朗克常数
+
+

用Printf函数的%g参数打印浮点数,将采用更紧凑的表示形式打印,并提供足够的精度,但是对应表格的数据,使用%e(带指数)或%f的形式打印可能更合适。所有的这三个打印形式都可以指定打印的宽度和控制打印精度。

+
for x := 0; x < 8; x++ {
+	fmt.Printf("x = %d e^x = %8.3f\n", x, math.Exp(float64(x)))
+}
+
+

上面代码打印e的幂,打印精度是小数点后三个小数精度和8个字符宽度:

+
x = 0       e^x =    1.000
+x = 1       e^x =    2.718
+x = 2       e^x =    7.389
+x = 3       e^x =   20.086
+x = 4       e^x =   54.598
+x = 5       e^x =  148.413
+x = 6       e^x =  403.429
+x = 7       e^x = 1096.633
+
+

math包中除了提供大量常用的数学函数外,还提供了IEEE754浮点数标准中定义的特殊值的创建和测试:正无穷大和负无穷大,分别用于表示太大溢出的数字和除零的结果;还有NaN非数,一般用于表示无效的除法操作结果0/0或Sqrt(-1).

+
var z float64
+fmt.Println(z, -z, 1/z, -1/z, z/z) // "0 -0 +Inf -Inf NaN"
+
+

函数math.IsNaN用于测试一个数是否是非数NaN,math.NaN则返回非数对应的值。虽然可以用math.NaN来表示一个非法的结果,但是测试一个结果是否是非数NaN则是充满风险的,因为NaN和任何数都是不相等的(译注:在浮点数中,NaN、正无穷大和负无穷大都不是唯一的,每个都有非常多种的bit模式表示):

+
nan := math.NaN()
+fmt.Println(nan == nan, nan < nan, nan > nan) // "false false false"
+
+

如果一个函数返回的浮点数结果可能失败,最好的做法是用单独的标志报告失败,像这样:

+
func compute() (value float64, ok bool) {
+	// ...
+	if failed {
+		return 0, false
+	}
+	return result, true
+}
+
+

接下来的程序演示了通过浮点计算生成的图形。它是带有两个参数的z = f(x, y)函数的三维形式,使用了可缩放矢量图形(SVG)格式输出,SVG是一个用于矢量线绘制的XML标准。图3.1显示了sin(r)/r函数的输出图形,其中r是sqrt(x*x+y*y)

+

+

gopl.io/ch3/surface

+
// Surface computes an SVG rendering of a 3-D surface function.
+package main
+
+import (
+	"fmt"
+	"math"
+)
+
+const (
+	width, height = 600, 320            // canvas size in pixels
+	cells         = 100                 // number of grid cells
+	xyrange       = 30.0                // axis ranges (-xyrange..+xyrange)
+	xyscale       = width / 2 / xyrange // pixels per x or y unit
+	zscale        = height * 0.4        // pixels per z unit
+	angle         = math.Pi / 6         // angle of x, y axes (=30°)
+)
+
+var sin30, cos30 = math.Sin(angle), math.Cos(angle) // sin(30°), cos(30°)
+
+func main() {
+	fmt.Printf("<svg xmlns='http://www.w3.org/2000/svg' "+
+		"style='stroke: grey; fill: white; stroke-width: 0.7' "+
+		"width='%d' height='%d'>", width, height)
+	for i := 0; i < cells; i++ {
+		for j := 0; j < cells; j++ {
+			ax, ay := corner(i+1, j)
+			bx, by := corner(i, j)
+			cx, cy := corner(i, j+1)
+			dx, dy := corner(i+1, j+1)
+			fmt.Printf("<polygon points='%g,%g %g,%g %g,%g %g,%g'/>\n",
+				ax, ay, bx, by, cx, cy, dx, dy)
+		}
+	}
+	fmt.Println("</svg>")
+}
+
+func corner(i, j int) (float64, float64) {
+	// Find point (x,y) at corner of cell (i,j).
+	x := xyrange * (float64(i)/cells - 0.5)
+	y := xyrange * (float64(j)/cells - 0.5)
+
+	// Compute surface height z.
+	z := f(x, y)
+
+	// Project (x,y,z) isometrically onto 2-D SVG canvas (sx,sy).
+	sx := width/2 + (x-y)*cos30*xyscale
+	sy := height/2 + (x+y)*sin30*xyscale - z*zscale
+	return sx, sy
+}
+
+func f(x, y float64) float64 {
+	r := math.Hypot(x, y) // distance from (0,0)
+	return math.Sin(r) / r
+}
+
+

要注意的是corner函数返回了两个结果,分别对应每个网格顶点的坐标参数。

+

要解释这个程序是如何工作的需要一些基本的几何学知识,但是我们可以跳过几何学原理,因为程序的重点是演示浮点数运算。程序的本质是三个不同的坐标系中映射关系,如图3.2所示。第一个是100x100的二维网格,对应整数坐标(i,j),从远处的(0,0)位置开始。我们从远处向前面绘制,因此远处先绘制的多边形有可能被前面后绘制的多边形覆盖。

+

第二个坐标系是一个三维的网格浮点坐标(x,y,z),其中x和y是i和j的线性函数,通过平移转换为网格单元的中心,然后用xyrange系数缩放。高度z是函数f(x,y)的值。

+

第三个坐标系是一个二维的画布,起点(0,0)在左上角。画布中点的坐标用(sx,sy)表示。我们使用等角投影将三维点(x,y,z)投影到二维的画布中。

+

+

画布中从远处到右边的点对应较大的x值和较大的y值。并且画布中x和y值越大,则对应的z值越小。x和y的垂直和水平缩放系数来自30度角的正弦和余弦值。z的缩放系数0.4,是一个任意选择的参数。

+

对于二维网格中的每一个网格单元,main函数计算单元的四个顶点在画布中对应多边形ABCD的顶点,其中B对应(i,j)顶点位置,A、C和D是其它相邻的顶点,然后输出SVG的绘制指令。

+

练习 3.1: 如果f函数返回的是无限制的float64值,那么SVG文件可能输出无效的多边形元素(虽然许多SVG渲染器会妥善处理这类问题)。修改程序跳过无效的多边形。

+

练习 3.2: 试验math包中其他函数的渲染图形。你是否能输出一个egg box、moguls或a saddle图案?

+

练习 3.3: 根据高度给每个多边形上色,那样峰值部将是红色(#ff0000),谷部将是蓝色(#0000ff)。

+

练习 3.4: 参考1.7节Lissajous例子的函数,构造一个web服务器,用于计算函数曲面然后返回SVG数据给客户端。服务器必须设置Content-Type头部:

+
w.Header().Set("Content-Type", "image/svg+xml")
+
+

(这一步在Lissajous例子中不是必须的,因为服务器使用标准的PNG图像格式,可以根据前面的512个字节自动输出对应的头部。)允许客户端通过HTTP请求参数设置高度、宽度和颜色等参数。

+

3.3. 复数

+

Go语言提供了两种精度的复数类型:complex64和complex128,分别对应float32和float64两种浮点数精度。内置的complex函数用于构建复数,内建的real和imag函数分别返回复数的实部和虚部:

+
var x complex128 = complex(1, 2) // 1+2i
+var y complex128 = complex(3, 4) // 3+4i
+fmt.Println(x*y)                 // "(-5+10i)"
+fmt.Println(real(x*y))           // "-5"
+fmt.Println(imag(x*y))           // "10"
+
+

如果一个浮点数面值或一个十进制整数面值后面跟着一个i,例如3.141592i或2i,它将构成一个复数的虚部,复数的实部是0:

+
fmt.Println(1i * 1i) // "(-1+0i)", i^2 = -1
+
+

在常量算术规则下,一个复数常量可以加到另一个普通数值常量(整数或浮点数、实部或虚部),我们可以用自然的方式书写复数,就像1+2i或与之等价的写法2i+1。上面x和y的声明语句还可以简化:

+
x := 1 + 2i
+y := 3 + 4i
+
+

复数也可以用==和!=进行相等比较。只有两个复数的实部和虚部都相等的时候它们才是相等的(译注:浮点数的相等比较是危险的,需要特别小心处理精度问题)。

+

math/cmplx包提供了复数处理的许多函数,例如求复数的平方根函数和求幂函数。

+
fmt.Println(cmplx.Sqrt(-1)) // "(0+1i)"
+
+

下面的程序使用complex128复数算法来生成一个Mandelbrot图像。

+

gopl.io/ch3/mandelbrot

+
// Mandelbrot emits a PNG image of the Mandelbrot fractal.
+package main
+
+import (
+	"image"
+	"image/color"
+	"image/png"
+	"math/cmplx"
+	"os"
+)
+
+
+func main() {
+	const (
+		xmin, ymin, xmax, ymax = -2, -2, +2, +2
+		width, height          = 1024, 1024
+	)
+
+	img := image.NewRGBA(image.Rect(0, 0, width, height))
+	for py := 0; py < height; py++ {
+		y := float64(py)/height*(ymax-ymin) + ymin
+		for px := 0; px < width; px++ {
+			x := float64(px)/width*(xmax-xmin) + xmin
+			z := complex(x, y)
+			// Image point (px, py) represents complex value z.
+			img.Set(px, py, mandelbrot(z))
+		}
+	}
+	png.Encode(os.Stdout, img) // NOTE: ignoring errors
+}
+
+func mandelbrot(z complex128) color.Color {
+	const iterations = 200
+	const contrast = 15
+
+	var v complex128
+	for n := uint8(0); n < iterations; n++ {
+		v = v*v + z
+		if cmplx.Abs(v) > 2 {
+			return color.Gray{255 - contrast*n}
+		}
+	}
+	return color.Black
+}
+
+

用于遍历1024x1024图像每个点的两个嵌套的循环对应-2到+2区间的复数平面。程序反复测试每个点对应复数值平方值加一个增量值对应的点是否超出半径为2的圆。如果超过了,通过根据预设置的逃逸迭代次数对应的灰度颜色来代替。如果不是,那么该点属于Mandelbrot集合,使用黑色颜色标记。最终程序将生成的PNG格式分形图像输出到标准输出,如图3.3所示。

+

+

练习 3.5: 实现一个彩色的Mandelbrot图像,使用image.NewRGBA创建图像,使用color.RGBA或color.YCbCr生成颜色。

+

练习 3.6: 升采样技术可以降低每个像素对计算颜色值和平均值的影响。简单的方法是将每个像素分成四个子像素,实现它。

+

练习 3.7: 另一个生成分形图像的方式是使用牛顿法来求解一个复数方程,例如$z^4-1=0$。每个起点到四个根的迭代次数对应阴影的灰度。方程根对应的点用颜色表示。

+

练习 3.8: 通过提高精度来生成更多级别的分形。使用四种不同精度类型的数字实现相同的分形:complex64、complex128、big.Float和big.Rat。(后面两种类型在math/big包声明。Float是有指定限精度的浮点数;Rat是无限精度的有理数。)它们间的性能和内存使用对比如何?当渲染图可见时缩放的级别是多少?

+

练习 3.9: 编写一个web服务器,用于给客户端生成分形的图像。运行客户端通过HTTP参数指定x、y和zoom参数。

+

3.4. 布尔型

+

一个布尔类型的值只有两种:true和false。if和for语句的条件部分都是布尔类型的值,并且==和<等比较操作也会产生布尔型的值。一元操作符!对应逻辑非操作,因此!true的值为false,更罗嗦的说法是(!true==false)==true,虽然表达方式不一样,不过我们一般会采用简洁的布尔表达式,就像用x来表示x==true

+

布尔值可以和&&(AND)和||(OR)操作符结合,并且有短路行为:如果运算符左边值已经可以确定整个布尔表达式的值,那么运算符右边的值将不再被求值,因此下面的表达式总是安全的:

+
s != "" && s[0] == 'x'
+
+

其中s[0]操作如果应用于空字符串将会导致panic异常。

+

因为&&的优先级比||高(助记:&&对应逻辑乘法,||对应逻辑加法,乘法比加法优先级要高),下面形式的布尔表达式是不需要加小括弧的:

+
if 'a' <= c && c <= 'z' ||
+	'A' <= c && c <= 'Z' ||
+	'0' <= c && c <= '9' {
+	// ...ASCII letter or digit...
+}
+
+

布尔值并不会隐式转换为数字值0或1,反之亦然。必须使用一个显式的if语句辅助转换:

+
i := 0
+if b {
+	i = 1
+}
+
+

如果需要经常做类似的转换,包装成一个函数会更方便:

+
// btoi returns 1 if b is true and 0 if false.
+func btoi(b bool) int {
+	if b {
+		return 1
+	}
+	return 0
+}
+
+

数字到布尔型的逆转换则非常简单,不过为了保持对称,我们也可以包装一个函数:

+
// itob reports whether i is non-zero.
+func itob(i int) bool { return i != 0 }
+
+

3.5. 字符串

+

一个字符串是一个不可改变的字节序列。字符串可以包含任意的数据,包括byte值0,但是通常是用来包含人类可读的文本。文本字符串通常被解释为采用UTF8编码的Unicode码点(rune)序列,我们稍后会详细讨论这个问题。

+

内置的len函数可以返回一个字符串中的字节数目(不是rune字符数目),索引操作s[i]返回第i个字节的字节值,i必须满足0 ≤ i< len(s)条件约束。

+
s := "hello, world"
+fmt.Println(len(s))     // "12"
+fmt.Println(s[0], s[7]) // "104 119" ('h' and 'w')
+
+

如果试图访问超出字符串索引范围的字节将会导致panic异常:

+
c := s[len(s)] // panic: index out of range
+
+

第i个字节并不一定是字符串的第i个字符,因为对于非ASCII字符的UTF8编码会要两个或多个字节。我们先简单说下字符的工作方式。

+

子字符串操作s[i:j]基于原始的s字符串的第i个字节开始到第j个字节(并不包含j本身)生成一个新字符串。生成的新字符串将包含j-i个字节。

+
fmt.Println(s[0:5]) // "hello"
+
+

同样,如果索引超出字符串范围或者j小于i的话将导致panic异常。

+

不管i还是j都可能被忽略,当它们被忽略时将采用0作为开始位置,采用len(s)作为结束的位置。

+
fmt.Println(s[:5]) // "hello"
+fmt.Println(s[7:]) // "world"
+fmt.Println(s[:])  // "hello, world"
+
+

其中+操作符将两个字符串连接构造一个新字符串:

+
fmt.Println("goodbye" + s[5:]) // "goodbye, world"
+
+

字符串可以用==和<进行比较;比较通过逐个字节比较完成的,因此比较的结果是字符串自然编码的顺序。

+

字符串的值是不可变的:一个字符串包含的字节序列永远不会被改变,当然我们也可以给一个字符串变量分配一个新字符串值。可以像下面这样将一个字符串追加到另一个字符串:

+
s := "left foot"
+t := s
+s += ", right foot"
+
+

这并不会导致原始的字符串值被改变,但是变量s将因为+=语句持有一个新的字符串值,但是t依然是包含原先的字符串值。

+
fmt.Println(s) // "left foot, right foot"
+fmt.Println(t) // "left foot"
+
+

因为字符串是不可修改的,因此尝试修改字符串内部数据的操作也是被禁止的:

+
s[0] = 'L' // compile error: cannot assign to s[0]
+
+

不变性意味着如果两个字符串共享相同的底层数据的话也是安全的,这使得复制任何长度的字符串代价是低廉的。同样,一个字符串s和对应的子字符串切片s[7:]的操作也可以安全地共享相同的内存,因此字符串切片操作代价也是低廉的。在这两种情况下都没有必要分配新的内存。 图3.4演示了一个字符串和两个子串共享相同的底层数据。

+

3.5.1. 字符串面值

+

字符串值也可以用字符串面值方式编写,只要将一系列字节序列包含在双引号内即可:

+
"Hello, 世界"
+
+

+

因为Go语言源文件总是用UTF8编码,并且Go语言的文本字符串也以UTF8编码的方式处理,因此我们可以将Unicode码点也写到字符串面值中。

+

在一个双引号包含的字符串面值中,可以用以反斜杠\开头的转义序列插入任意的数据。下面的换行、回车和制表符等是常见的ASCII控制代码的转义方式:

+
\a      响铃
+\b      退格
+\f      换页
+\n      换行
+\r      回车
+\t      制表符
+\v      垂直制表符
+\'      单引号(只用在 '\'' 形式的rune符号面值中)
+\"      双引号(只用在 "..." 形式的字符串面值中)
+\\      反斜杠
+
+

可以通过十六进制或八进制转义在字符串面值中包含任意的字节。一个十六进制的转义形式是\xhh,其中两个h表示十六进制数字(大写或小写都可以)。一个八进制转义形式是\ooo,包含三个八进制的o数字(0到7),但是不能超过\377(译注:对应一个字节的范围,十进制为255)。每一个单一的字节表达一个特定的值。稍后我们将看到如何将一个Unicode码点写到字符串面值中。

+

一个原生的字符串面值形式是`...`,使用反引号代替双引号。在原生的字符串面值中,没有转义操作;全部的内容都是字面的意思,包含退格和换行,因此一个程序中的原生字符串面值可能跨越多行(译注:在原生字符串面值内部是无法直接写`字符的,可以用八进制或十六进制转义或+"`"连接字符串常量完成)。唯一的特殊处理是会删除回车以保证在所有平台上的值都是一样的,包括那些把回车也放入文本文件的系统(译注:Windows系统会把回车和换行一起放入文本文件中)。

+

原生字符串面值用于编写正则表达式会很方便,因为正则表达式往往会包含很多反斜杠。原生字符串面值同时被广泛应用于HTML模板、JSON面值、命令行提示信息以及那些需要扩展到多行的场景。

+
const GoUsage = `Go is a tool for managing Go source code.
+
+Usage:
+	go command [arguments]
+...`
+
+

3.5.2. Unicode

+

在很久以前,世界还是比较简单的,起码计算机世界就只有一个ASCII字符集:美国信息交换标准代码。ASCII,更准确地说是美国的ASCII,使用7bit来表示128个字符:包含英文字母的大小写、数字、各种标点符号和设备控制符。对于早期的计算机程序来说,这些就足够了,但是这也导致了世界上很多其他地区的用户无法直接使用自己的符号系统。随着互联网的发展,混合多种语言的数据变得很常见(译注:比如本身的英文原文或中文翻译都包含了ASCII、中文、日文等多种语言字符)。如何有效处理这些包含了各种语言的丰富多样的文本数据呢?

+

答案就是使用Unicode( http://unicode.org ),它收集了这个世界上所有的符号系统,包括重音符号和其它变音符号,制表符和回车符,还有很多神秘的符号,每个符号都分配一个唯一的Unicode码点,Unicode码点对应Go语言中的rune整数类型(译注:rune是int32等价类型)。

+

在第八版本的Unicode标准里收集了超过120,000个字符,涵盖超过100多种语言。这些在计算机程序和数据中是如何体现的呢?通用的表示一个Unicode码点的数据类型是int32,也就是Go语言中rune对应的类型;它的同义词rune符文正是这个意思。

+

我们可以将一个符文序列表示为一个int32序列。这种编码方式叫UTF-32或UCS-4,每个Unicode码点都使用同样大小的32bit来表示。这种方式比较简单统一,但是它会浪费很多存储空间,因为大多数计算机可读的文本是ASCII字符,本来每个ASCII字符只需要8bit或1字节就能表示。而且即使是常用的字符也远少于65,536个,也就是说用16bit编码方式就能表达常用字符。但是,还有其它更好的编码方法吗?

+

3.5.3. UTF-8

+

UTF8是一个将Unicode码点编码为字节序列的变长编码。UTF8编码是由Go语言之父Ken Thompson和Rob Pike共同发明的,现在已经是Unicode的标准。UTF8编码使用1到4个字节来表示每个Unicode码点,ASCII部分字符只使用1个字节,常用字符部分使用2或3个字节表示。每个符号编码后第一个字节的高端bit位用于表示编码总共有多少个字节。如果第一个字节的高端bit为0,则表示对应7bit的ASCII字符,ASCII字符每个字符依然是一个字节,和传统的ASCII编码兼容。如果第一个字节的高端bit是110,则说明需要2个字节;后续的每个高端bit都以10开头。更大的Unicode码点也是采用类似的策略处理。

+
0xxxxxxx                             runes 0-127    (ASCII)
+110xxxxx 10xxxxxx                    128-2047       (values <128 unused)
+1110xxxx 10xxxxxx 10xxxxxx           2048-65535     (values <2048 unused)
+11110xxx 10xxxxxx 10xxxxxx 10xxxxxx  65536-0x10ffff (other values unused)
+
+

变长的编码无法直接通过索引来访问第n个字符,但是UTF8编码获得了很多额外的优点。首先UTF8编码比较紧凑,完全兼容ASCII码,并且可以自动同步:它可以通过向前回朔最多3个字节就能确定当前字符编码的开始字节的位置。它也是一个前缀编码,所以当从左向右解码时不会有任何歧义也并不需要向前查看(译注:像GBK之类的编码,如果不知道起点位置则可能会出现歧义)。没有任何字符的编码是其它字符编码的子串,或是其它编码序列的字串,因此搜索一个字符时只要搜索它的字节编码序列即可,不用担心前后的上下文会对搜索结果产生干扰。同时UTF8编码的顺序和Unicode码点的顺序一致,因此可以直接排序UTF8编码序列。同时因为没有嵌入的NUL(0)字节,可以很好地兼容那些使用NUL作为字符串结尾的编程语言。

+

Go语言的源文件采用UTF8编码,并且Go语言处理UTF8编码的文本也很出色。unicode包提供了诸多处理rune字符相关功能的函数(比如区分字母和数字,或者是字母的大写和小写转换等),unicode/utf8包则提供了用于rune字符序列的UTF8编码和解码的功能。

+

有很多Unicode字符很难直接从键盘输入,并且还有很多字符有着相似的结构;有一些甚至是不可见的字符(译注:中文和日文就有很多相似但不同的字)。Go语言字符串面值中的Unicode转义字符让我们可以通过Unicode码点输入特殊的字符。有两种形式:\uhhhh对应16bit的码点值,\Uhhhhhhhh对应32bit的码点值,其中h是一个十六进制数字;一般很少需要使用32bit的形式。每一个对应码点的UTF8编码。例如:下面的字母串面值都表示相同的值:

+
"世界"
+"\xe4\xb8\x96\xe7\x95\x8c"
+"\u4e16\u754c"
+"\U00004e16\U0000754c"
+
+

上面三个转义序列都为第一个字符串提供替代写法,但是它们的值都是相同的。

+

Unicode转义也可以使用在rune字符中。下面三个字符是等价的:

+
'世' '\u4e16' '\U00004e16'
+
+

对于小于256的码点值可以写在一个十六进制转义字节中,例如\x41对应字符'A',但是对于更大的码点则必须使用\u\U转义形式。因此,\xe4\xb8\x96并不是一个合法的rune字符,虽然这三个字节对应一个有效的UTF8编码的码点。

+

得益于UTF8编码优良的设计,诸多字符串操作都不需要解码操作。我们可以不用解码直接测试一个字符串是否是另一个字符串的前缀:

+
func HasPrefix(s, prefix string) bool {
+	return len(s) >= len(prefix) && s[:len(prefix)] == prefix
+}
+
+

或者是后缀测试:

+
func HasSuffix(s, suffix string) bool {
+	return len(s) >= len(suffix) && s[len(s)-len(suffix):] == suffix
+}
+
+

或者是包含子串测试:

+
func Contains(s, substr string) bool {
+	for i := 0; i < len(s); i++ {
+		if HasPrefix(s[i:], substr) {
+			return true
+		}
+	}
+	return false
+}
+
+

对于UTF8编码后文本的处理和原始的字节处理逻辑是一样的。但是对应很多其它编码则并不是这样的。(上面的函数都来自strings字符串处理包,真实的代码包含了一个用哈希技术优化的Contains 实现。)

+

另一方面,如果我们真的关心每个Unicode字符,我们可以使用其它处理方式。考虑前面的第一个例子中的字符串,它混合了中西两种字符。图3.5展示了它的内存表示形式。字符串包含13个字节,以UTF8形式编码,但是只对应9个Unicode字符:

+
import "unicode/utf8"
+
+s := "Hello, 世界"
+fmt.Println(len(s))                    // "13"
+fmt.Println(utf8.RuneCountInString(s)) // "9"
+
+

为了处理这些真实的字符,我们需要一个UTF8解码器。unicode/utf8包提供了该功能,我们可以这样使用:

+
for i := 0; i < len(s); {
+	r, size := utf8.DecodeRuneInString(s[i:])
+	fmt.Printf("%d\t%c\n", i, r)
+	i += size
+}
+
+

每一次调用DecodeRuneInString函数都返回一个r和长度,r对应字符本身,长度对应r采用UTF8编码后的编码字节数目。长度可以用于更新第i个字符在字符串中的字节索引位置。但是这种编码方式是笨拙的,我们需要更简洁的语法。幸运的是,Go语言的range循环在处理字符串的时候,会自动隐式解码UTF8字符串。下面的循环运行如图3.5所示;需要注意的是对于非ASCII,索引更新的步长将超过1个字节。

+

+
for i, r := range "Hello, 世界" {
+	fmt.Printf("%d\t%q\t%d\n", i, r, r)
+}
+
+

我们可以使用一个简单的循环来统计字符串中字符的数目,像这样:

+
n := 0
+for _, _ = range s {
+	n++
+}
+
+

像其它形式的循环那样,我们也可以忽略不需要的变量:

+
n := 0
+for range s {
+	n++
+}
+
+

或者我们可以直接调用utf8.RuneCountInString(s)函数。

+

正如我们前面提到的,文本字符串采用UTF8编码只是一种惯例,但是对于循环的真正字符串并不是一个惯例,这是正确的。如果用于循环的字符串只是一个普通的二进制数据,或者是含有错误编码的UTF8数据,将会发生什么呢?

+

每一个UTF8字符解码,不管是显式地调用utf8.DecodeRuneInString解码或是在range循环中隐式地解码,如果遇到一个错误的UTF8编码输入,将生成一个特别的Unicode字符\uFFFD,在印刷中这个符号通常是一个黑色六角或钻石形状,里面包含一个白色的问号"?"。当程序遇到这样的一个字符,通常是一个危险信号,说明输入并不是一个完美没有错误的UTF8字符串。

+

UTF8字符串作为交换格式是非常方便的,但是在程序内部采用rune序列可能更方便,因为rune大小一致,支持数组索引和方便切割。

+

将[]rune类型转换应用到UTF8编码的字符串,将返回字符串编码的Unicode码点序列:

+
// "program" in Japanese katakana
+s := "プログラム"
+fmt.Printf("% x\n", s) // "e3 83 97 e3 83 ad e3 82 b0 e3 83 a9 e3 83 a0"
+r := []rune(s)
+fmt.Printf("%x\n", r)  // "[30d7 30ed 30b0 30e9 30e0]"
+
+

(在第一个Printf中的% x参数用于在每个十六进制数字前插入一个空格。)

+

如果是将一个[]rune类型的Unicode字符slice或数组转为string,则对它们进行UTF8编码:

+
fmt.Println(string(r)) // "プログラム"
+
+

将一个整数转型为字符串意思是生成以只包含对应Unicode码点字符的UTF8字符串:

+
fmt.Println(string(65))     // "A", not "65"
+fmt.Println(string(0x4eac)) // "京"
+
+

如果对应码点的字符是无效的,则用\uFFFD无效字符作为替换:

+
fmt.Println(string(1234567)) // "?"
+
+

3.5.4. 字符串和Byte切片

+

标准库中有四个包对字符串处理尤为重要:bytes、strings、strconv和unicode包。strings包提供了许多如字符串的查询、替换、比较、截断、拆分和合并等功能。

+

bytes包也提供了很多类似功能的函数,但是针对和字符串有着相同结构的[]byte类型。因为字符串是只读的,因此逐步构建字符串会导致很多分配和复制。在这种情况下,使用bytes.Buffer类型将会更有效,稍后我们将展示。

+

strconv包提供了布尔型、整型数、浮点数和对应字符串的相互转换,还提供了双引号转义相关的转换。

+

unicode包提供了IsDigit、IsLetter、IsUpper和IsLower等类似功能,它们用于给字符分类。每个函数有一个单一的rune类型的参数,然后返回一个布尔值。而像ToUpper和ToLower之类的转换函数将用于rune字符的大小写转换。所有的这些函数都是遵循Unicode标准定义的字母、数字等分类规范。strings包也有类似的函数,它们是ToUpper和ToLower,将原始字符串的每个字符都做相应的转换,然后返回新的字符串。

+

下面例子的basename函数灵感源于Unix shell的同名工具。在我们实现的版本中,basename(s)将看起来像是系统路径的前缀删除,同时将看似文件类型的后缀名部分删除:

+
fmt.Println(basename("a/b/c.go")) // "c"
+fmt.Println(basename("c.d.go"))   // "c.d"
+fmt.Println(basename("abc"))      // "abc"
+
+

第一个版本并没有使用任何库,全部手工硬编码实现:

+

gopl.io/ch3/basename1

+
// basename removes directory components and a .suffix.
+// e.g., a => a, a.go => a, a/b/c.go => c, a/b.c.go => b.c
+func basename(s string) string {
+	// Discard last '/' and everything before.
+	for i := len(s) - 1; i >= 0; i-- {
+		if s[i] == '/' {
+			s = s[i+1:]
+			break
+		}
+	}
+	// Preserve everything before last '.'.
+	for i := len(s) - 1; i >= 0; i-- {
+		if s[i] == '.' {
+			s = s[:i]
+			break
+		}
+	}
+	return s
+}
+
+

这个简化版本使用了strings.LastIndex库函数:

+

gopl.io/ch3/basename2

+
func basename(s string) string {
+	slash := strings.LastIndex(s, "/") // -1 if "/" not found
+	s = s[slash+1:]
+	if dot := strings.LastIndex(s, "."); dot >= 0 {
+		s = s[:dot]
+	}
+	return s
+}
+
+

path和path/filepath包提供了关于文件路径名更一般的函数操作。使用斜杠分隔路径可以在任何操作系统上工作。斜杠本身不应该用于文件名,但是在其他一些领域可能会用于文件名,例如URL路径组件。相比之下,path/filepath包则使用操作系统本身的路径规则,例如POSIX系统使用/foo/bar,而Microsoft Windows使用c:\foo\bar等。

+

让我们继续另一个字符串的例子。函数的功能是将一个表示整数值的字符串,每隔三个字符插入一个逗号分隔符,例如“12345”处理后成为“12,345”。这个版本只适用于整数类型;支持浮点数类型的留作练习。

+

gopl.io/ch3/comma

+
// comma inserts commas in a non-negative decimal integer string.
+func comma(s string) string {
+	n := len(s)
+	if n <= 3 {
+		return s
+	}
+	return comma(s[:n-3]) + "," + s[n-3:]
+}
+
+

输入comma函数的参数是一个字符串。如果输入字符串的长度小于或等于3的话,则不需要插入逗号分隔符。否则,comma函数将在最后三个字符前的位置将字符串切割为两个子串并插入逗号分隔符,然后通过递归调用自身来得出前面的子串。

+

一个字符串是包含只读字节的数组,一旦创建,是不可变的。相比之下,一个字节slice的元素则可以自由地修改。

+

字符串和字节slice之间可以相互转换:

+
s := "abc"
+b := []byte(s)
+s2 := string(b)
+
+

从概念上讲,一个[]byte(s)转换是分配了一个新的字节数组用于保存字符串数据的拷贝,然后引用这个底层的字节数组。编译器的优化可以避免在一些场景下分配和复制字符串数据,但总的来说需要确保在变量b被修改的情况下,原始的s字符串也不会改变。将一个字节slice转换到字符串的string(b)操作则是构造一个字符串拷贝,以确保s2字符串是只读的。

+

为了避免转换中不必要的内存分配,bytes包和strings同时提供了许多实用函数。下面是strings包中的六个函数:

+
func Contains(s, substr string) bool
+func Count(s, sep string) int
+func Fields(s string) []string
+func HasPrefix(s, prefix string) bool
+func Index(s, sep string) int
+func Join(a []string, sep string) string
+
+

bytes包中也对应的六个函数:

+
func Contains(b, subslice []byte) bool
+func Count(s, sep []byte) int
+func Fields(s []byte) [][]byte
+func HasPrefix(s, prefix []byte) bool
+func Index(s, sep []byte) int
+func Join(s [][]byte, sep []byte) []byte
+
+

它们之间唯一的区别是字符串类型参数被替换成了字节slice类型的参数。

+

bytes包还提供了Buffer类型用于字节slice的缓存。一个Buffer开始是空的,但是随着string、byte或[]byte等类型数据的写入可以动态增长,一个bytes.Buffer变量并不需要初始化,因为零值也是有效的:

+

gopl.io/ch3/printints

+
// intsToString is like fmt.Sprint(values) but adds commas.
+func intsToString(values []int) string {
+	var buf bytes.Buffer
+	buf.WriteByte('[')
+	for i, v := range values {
+		if i > 0 {
+			buf.WriteString(", ")
+		}
+		fmt.Fprintf(&buf, "%d", v)
+	}
+	buf.WriteByte(']')
+	return buf.String()
+}
+
+func main() {
+	fmt.Println(intsToString([]int{1, 2, 3})) // "[1, 2, 3]"
+}
+
+

当向bytes.Buffer添加任意字符的UTF8编码时,最好使用bytes.Buffer的WriteRune方法,但是WriteByte方法对于写入类似'['和']'等ASCII字符则会更加有效。

+

bytes.Buffer类型有着很多实用的功能,我们在第七章讨论接口时将会涉及到,我们将看看如何将它用作一个I/O的输入和输出对象,例如当做Fprintf的io.Writer输出对象,或者当作io.Reader类型的输入源对象。

+

练习 3.10: 编写一个非递归版本的comma函数,使用bytes.Buffer代替字符串链接操作。

+

练习 3.11: 完善comma函数,以支持浮点数处理和一个可选的正负号的处理。

+

练习 3.12: 编写一个函数,判断两个字符串是否是相互打乱的,也就是说它们有着相同的字符,但是对应不同的顺序。

+

3.5.5. 字符串和数字的转换

+

除了字符串、字符、字节之间的转换,字符串和数值之间的转换也比较常见。由strconv包提供这类转换功能。

+

将一个整数转为字符串,一种方法是用fmt.Sprintf返回一个格式化的字符串;另一个方法是用strconv.Itoa(“整数到ASCII”):

+
x := 123
+y := fmt.Sprintf("%d", x)
+fmt.Println(y, strconv.Itoa(x)) // "123 123"
+
+

FormatInt和FormatUint函数可以用不同的进制来格式化数字:

+
fmt.Println(strconv.FormatInt(int64(x), 2)) // "1111011"
+
+

fmt.Printf函数的%b、%d、%o和%x等参数提供功能往往比strconv包的Format函数方便很多,特别是在需要包含有附加额外信息的时候:

+
s := fmt.Sprintf("x=%b", x) // "x=1111011"
+
+

如果要将一个字符串解析为整数,可以使用strconv包的Atoi或ParseInt函数,还有用于解析无符号整数的ParseUint函数:

+
x, err := strconv.Atoi("123")             // x is an int
+y, err := strconv.ParseInt("123", 10, 64) // base 10, up to 64 bits
+
+

ParseInt函数的第三个参数是用于指定整型数的大小;例如16表示int16,0则表示int。在任何情况下,返回的结果y总是int64类型,你可以通过强制类型转换将它转为更小的整数类型。

+

有时候也会使用fmt.Scanf来解析输入的字符串和数字,特别是当字符串和数字混合在一行的时候,它可以灵活处理不完整或不规则的输入。

+

3.6. 常量

+

常量表达式的值在编译期计算,而不是在运行期。每种常量的潜在类型都是基础类型:boolean、string或数字。

+

一个常量的声明语句定义了常量的名字,和变量的声明语法类似,常量的值不可修改,这样可以防止在运行期被意外或恶意的修改。例如,常量比变量更适合用于表达像π之类的数学常数,因为它们的值不会发生变化:

+
const pi = 3.14159 // approximately; math.Pi is a better approximation
+
+

和变量声明一样,可以批量声明多个常量;这比较适合声明一组相关的常量:

+
const (
+	e  = 2.71828182845904523536028747135266249775724709369995957496696763
+	pi = 3.14159265358979323846264338327950288419716939937510582097494459
+)
+
+

所有常量的运算都可以在编译期完成,这样可以减少运行时的工作,也方便其他编译优化。当操作数是常量时,一些运行时的错误也可以在编译时被发现,例如整数除零、字符串索引越界、任何导致无效浮点数的操作等。

+

常量间的所有算术运算、逻辑运算和比较运算的结果也是常量,对常量的类型转换操作或以下函数调用都是返回常量结果:len、cap、real、imag、complex和unsafe.Sizeof(§13.1)。

+

因为它们的值是在编译期就确定的,因此常量可以是构成类型的一部分,例如用于指定数组类型的长度:

+
const IPv4Len = 4
+
+// parseIPv4 parses an IPv4 address (d.d.d.d).
+func parseIPv4(s string) IP {
+	var p [IPv4Len]byte
+	// ...
+}
+
+

一个常量的声明也可以包含一个类型和一个值,但是如果没有显式指明类型,那么将从右边的表达式推断类型。在下面的代码中,time.Duration是一个命名类型,底层类型是int64,time.Minute是对应类型的常量。下面声明的两个常量都是time.Duration类型,可以通过%T参数打印类型信息:

+
const noDelay time.Duration = 0
+const timeout = 5 * time.Minute
+fmt.Printf("%T %[1]v\n", noDelay)     // "time.Duration 0"
+fmt.Printf("%T %[1]v\n", timeout)     // "time.Duration 5m0s"
+fmt.Printf("%T %[1]v\n", time.Minute) // "time.Duration 1m0s"
+
+

如果是批量声明的常量,除了第一个外其它的常量右边的初始化表达式都可以省略,如果省略初始化表达式则表示使用前面常量的初始化表达式写法,对应的常量类型也一样的。例如:

+
const (
+	a = 1
+	b
+	c = 2
+	d
+)
+
+fmt.Println(a, b, c, d) // "1 1 2 2"
+
+

如果只是简单地复制右边的常量表达式,其实并没有太实用的价值。但是它可以带来其它的特性,那就是iota常量生成器语法。

+

3.6.1. iota 常量生成器

+

常量声明可以使用iota常量生成器初始化,它用于生成一组以相似规则初始化的常量,但是不用每行都写一遍初始化表达式。在一个const声明语句中,在第一个声明的常量所在的行,iota将会被置为0,然后在每一个有常量声明的行加一。

+

下面是来自time包的例子,它首先定义了一个Weekday命名类型,然后为一周的每天定义了一个常量,从周日0开始。在其它编程语言中,这种类型一般被称为枚举类型。

+
type Weekday int
+
+const (
+	Sunday Weekday = iota
+	Monday
+	Tuesday
+	Wednesday
+	Thursday
+	Friday
+	Saturday
+)
+
+

周日将对应0,周一为1,如此等等。

+

我们也可以在复杂的常量表达式中使用iota,下面是来自net包的例子,用于给一个无符号整数的最低5bit的每个bit指定一个名字:

+
type Flags uint
+
+const (
+	FlagUp Flags = 1 << iota // is up
+	FlagBroadcast            // supports broadcast access capability
+	FlagLoopback             // is a loopback interface
+	FlagPointToPoint         // belongs to a point-to-point link
+	FlagMulticast            // supports multicast access capability
+)
+
+

随着iota的递增,每个常量对应表达式1 << iota,是连续的2的幂,分别对应一个bit位置。使用这些常量可以用于测试、设置或清除对应的bit位的值:

+

gopl.io/ch3/netflag

+
func IsUp(v Flags) bool     { return v&FlagUp == FlagUp }
+func TurnDown(v *Flags)     { *v &^= FlagUp }
+func SetBroadcast(v *Flags) { *v |= FlagBroadcast }
+func IsCast(v Flags) bool   { return v&(FlagBroadcast|FlagMulticast) != 0 }
+
+func main() {
+	var v Flags = FlagMulticast | FlagUp
+	fmt.Printf("%b %t\n", v, IsUp(v)) // "10001 true"
+	TurnDown(&v)
+	fmt.Printf("%b %t\n", v, IsUp(v)) // "10000 false"
+	SetBroadcast(&v)
+	fmt.Printf("%b %t\n", v, IsUp(v))   // "10010 false"
+	fmt.Printf("%b %t\n", v, IsCast(v)) // "10010 true"
+}
+
+

下面是一个更复杂的例子,每个常量都是1024的幂:

+
const (
+	_ = 1 << (10 * iota)
+	KiB // 1024
+	MiB // 1048576
+	GiB // 1073741824
+	TiB // 1099511627776             (exceeds 1 << 32)
+	PiB // 1125899906842624
+	EiB // 1152921504606846976
+	ZiB // 1180591620717411303424    (exceeds 1 << 64)
+	YiB // 1208925819614629174706176
+)
+
+

不过iota常量生成规则也有其局限性。例如,它并不能用于产生1000的幂(KB、MB等),因为Go语言并没有计算幂的运算符。

+

练习 3.13: 编写KB、MB的常量声明,然后扩展到YB。

+

3.6.2. 无类型常量

+

Go语言的常量有个不同寻常之处。虽然一个常量可以有任意一个确定的基础类型,例如int或float64,或者是类似time.Duration这样命名的基础类型,但是许多常量并没有一个明确的基础类型。编译器为这些没有明确基础类型的数字常量提供比基础类型更高精度的算术运算;你可以认为至少有256bit的运算精度。这里有六种未明确类型的常量类型,分别是无类型的布尔型、无类型的整数、无类型的字符、无类型的浮点数、无类型的复数、无类型的字符串。

+

通过延迟明确常量的具体类型,无类型的常量不仅可以提供更高的运算精度,而且可以直接用于更多的表达式而不需要显式的类型转换。例如,例子中的ZiB和YiB的值已经超出任何Go语言中整数类型能表达的范围,但是它们依然是合法的常量,而且像下面的常量表达式依然有效(译注:YiB/ZiB是在编译期计算出来的,并且结果常量是1024,是Go语言int变量能有效表示的):

+
fmt.Println(YiB/ZiB) // "1024"
+
+

另一个例子,math.Pi无类型的浮点数常量,可以直接用于任意需要浮点数或复数的地方:

+
var x float32 = math.Pi
+var y float64 = math.Pi
+var z complex128 = math.Pi
+
+

如果math.Pi被确定为特定类型,比如float64,那么结果精度可能会不一样,同时对于需要float32或complex128类型值的地方则会强制需要一个明确的类型转换:

+
const Pi64 float64 = math.Pi
+
+var x float32 = float32(Pi64)
+var y float64 = Pi64
+var z complex128 = complex128(Pi64)
+
+

对于常量面值,不同的写法可能会对应不同的类型。例如0、0.0、0i和\u0000虽然有着相同的常量值,但是它们分别对应无类型的整数、无类型的浮点数、无类型的复数和无类型的字符等不同的常量类型。同样,true和false也是无类型的布尔类型,字符串面值常量是无类型的字符串类型。

+

前面说过除法运算符/会根据操作数的类型生成对应类型的结果。因此,不同写法的常量除法表达式可能对应不同的结果:

+
var f float64 = 212
+fmt.Println((f - 32) * 5 / 9)     // "100"; (f - 32) * 5 is a float64
+fmt.Println(5 / 9 * (f - 32))     // "0";   5/9 is an untyped integer, 0
+fmt.Println(5.0 / 9.0 * (f - 32)) // "100"; 5.0/9.0 is an untyped float
+
+

只有常量可以是无类型的。当一个无类型的常量被赋值给一个变量的时候,就像下面的第一行语句,或者出现在有明确类型的变量声明的右边,如下面的其余三行语句,无类型的常量将会被隐式转换为对应的类型,如果转换合法的话。

+
var f float64 = 3 + 0i // untyped complex -> float64
+f = 2                  // untyped integer -> float64
+f = 1e123              // untyped floating-point -> float64
+f = 'a'                // untyped rune -> float64
+
+

上面的语句相当于:

+
var f float64 = float64(3 + 0i)
+f = float64(2)
+f = float64(1e123)
+f = float64('a')
+
+

无论是隐式或显式转换,将一种类型转换为另一种类型都要求目标可以表示原始值。对于浮点数和复数,可能会有舍入处理:

+
const (
+	deadbeef = 0xdeadbeef // untyped int with value 3735928559
+	a = uint32(deadbeef)  // uint32 with value 3735928559
+	b = float32(deadbeef) // float32 with value 3735928576 (rounded up)
+	c = float64(deadbeef) // float64 with value 3735928559 (exact)
+	d = int32(deadbeef)   // compile error: constant overflows int32
+	e = float64(1e309)    // compile error: constant overflows float64
+	f = uint(-1)          // compile error: constant underflows uint
+)
+
+

对于一个没有显式类型的变量声明(包括简短变量声明),常量的形式将隐式决定变量的默认类型,就像下面的例子:

+
i := 0      // untyped integer;        implicit int(0)
+r := '\000' // untyped rune;           implicit rune('\000')
+f := 0.0    // untyped floating-point; implicit float64(0.0)
+c := 0i     // untyped complex;        implicit complex128(0i)
+
+

注意有一点不同:无类型整数常量转换为int,它的内存大小是不确定的,但是无类型浮点数和复数常量则转换为内存大小明确的float64和complex128。 +如果不知道浮点数类型的内存大小是很难写出正确的数值算法的,因此Go语言不存在整型类似的不确定内存大小的浮点数和复数类型。

+

如果要给变量一个不同的类型,我们必须显式地将无类型的常量转化为所需的类型,或给声明的变量指定明确的类型,像下面例子这样:

+
var i = int8(0)
+var i int8 = 0
+
+

当尝试将这些无类型的常量转为一个接口值时(见第7章),这些默认类型将显得尤为重要,因为要靠它们明确接口对应的动态类型。

+
fmt.Printf("%T\n", 0)      // "int"
+fmt.Printf("%T\n", 0.0)    // "float64"
+fmt.Printf("%T\n", 0i)     // "complex128"
+fmt.Printf("%T\n", '\000') // "int32" (rune)
+
+

现在我们已经讲述了Go语言中全部的基础数据类型。下一步将演示如何用基础数据类型组合成数组或结构体等复杂数据类型,然后构建用于解决实际编程问题的数据结构,这将是第四章的讨论主题。

+

第4章 复合数据类型

+

在第三章我们讨论了基本数据类型,它们可以用于构建程序中数据的结构,是Go语言世界的原子。在本章,我们将讨论复合数据类型,它是以不同的方式组合基本类型而构造出来的复合数据类型。我们主要讨论四种类型——数组、slice、map和结构体——同时在本章的最后,我们将演示如何使用结构体来解码和编码到对应JSON格式的数据,并且通过结合使用模板来生成HTML页面。

+

数组和结构体是聚合类型;它们的值由许多元素或成员字段的值组成。数组是由同构的元素组成——每个数组元素都是完全相同的类型——结构体则是由异构的元素组成的。数组和结构体都是有固定内存大小的数据结构。相比之下,slice和map则是动态的数据结构,它们将根据需要动态增长。

+

4.1. 数组

+

数组是一个由固定长度的特定类型元素组成的序列,一个数组可以由零个或多个元素组成。因为数组的长度是固定的,因此在Go语言中很少直接使用数组。和数组对应的类型是Slice(切片),它是可以增长和收缩的动态序列,slice功能也更灵活,但是要理解slice工作原理的话需要先理解数组。

+

数组的每个元素可以通过索引下标来访问,索引下标的范围是从0开始到数组长度减1的位置。内置的len函数将返回数组中元素的个数。

+
var a [3]int             // array of 3 integers
+fmt.Println(a[0])        // print the first element
+fmt.Println(a[len(a)-1]) // print the last element, a[2]
+
+// Print the indices and elements.
+for i, v := range a {
+	fmt.Printf("%d %d\n", i, v)
+}
+
+// Print the elements only.
+for _, v := range a {
+	fmt.Printf("%d\n", v)
+}
+
+

默认情况下,数组的每个元素都被初始化为元素类型对应的零值,对于数字类型来说就是0。我们也可以使用数组字面值语法用一组值来初始化数组:

+
var q [3]int = [3]int{1, 2, 3}
+var r [3]int = [3]int{1, 2}
+fmt.Println(r[2]) // "0"
+
+

在数组字面值中,如果在数组的长度位置出现的是“...”省略号,则表示数组的长度是根据初始化值的个数来计算。因此,上面q数组的定义可以简化为

+
q := [...]int{1, 2, 3}
+fmt.Printf("%T\n", q) // "[3]int"
+
+

数组的长度是数组类型的一个组成部分,因此[3]int和[4]int是两种不同的数组类型。数组的长度必须是常量表达式,因为数组的长度需要在编译阶段确定。

+
q := [3]int{1, 2, 3}
+q = [4]int{1, 2, 3, 4} // compile error: cannot assign [4]int to [3]int
+
+

我们将会发现,数组、slice、map和结构体字面值的写法都很相似。上面的形式是直接提供顺序初始化值序列,但是也可以指定一个索引和对应值列表的方式初始化,就像下面这样:

+
type Currency int
+
+const (
+	USD Currency = iota // 美元
+	EUR                 // 欧元
+	GBP                 // 英镑
+	RMB                 // 人民币
+)
+
+symbol := [...]string{USD: "$", EUR: "€", GBP: "£", RMB: "¥"}
+
+fmt.Println(RMB, symbol[RMB]) // "3 ¥"
+
+

在这种形式的数组字面值形式中,初始化索引的顺序是无关紧要的,而且没用到的索引可以省略,和前面提到的规则一样,未指定初始值的元素将用零值初始化。例如,

+
r := [...]int{99: -1}
+
+

定义了一个含有100个元素的数组r,最后一个元素被初始化为-1,其它元素都是用0初始化。

+

如果一个数组的元素类型是可以相互比较的,那么数组类型也是可以相互比较的,这时候我们可以直接通过==比较运算符来比较两个数组,只有当两个数组的所有元素都是相等的时候数组才是相等的。不相等比较运算符!=遵循同样的规则。

+
a := [2]int{1, 2}
+b := [...]int{1, 2}
+c := [2]int{1, 3}
+fmt.Println(a == b, a == c, b == c) // "true false false"
+d := [3]int{1, 2}
+fmt.Println(a == d) // compile error: cannot compare [2]int == [3]int
+
+

作为一个真实的例子,crypto/sha256包的Sum256函数对一个任意的字节slice类型的数据生成一个对应的消息摘要。消息摘要有256bit大小,因此对应[32]byte数组类型。如果两个消息摘要是相同的,那么可以认为两个消息本身也是相同(译注:理论上有HASH码碰撞的情况,但是实际应用可以基本忽略);如果消息摘要不同,那么消息本身必然也是不同的。下面的例子用SHA256算法分别生成“x”和“X”两个信息的摘要:

+

gopl.io/ch4/sha256

+
import "crypto/sha256"
+
+func main() {
+	c1 := sha256.Sum256([]byte("x"))
+	c2 := sha256.Sum256([]byte("X"))
+	fmt.Printf("%x\n%x\n%t\n%T\n", c1, c2, c1 == c2, c1)
+	// Output:
+	// 2d711642b726b04401627ca9fbac32f5c8530fb1903cc4db02258717921a4881
+	// 4b68ab3847feda7d6c62c1fbcbeebfa35eab7351ed5e78f4ddadea5df64b8015
+	// false
+	// [32]uint8
+}
+
+

上面例子中,两个消息虽然只有一个字符的差异,但是生成的消息摘要则几乎有一半的bit位是不相同的。需要注意Printf函数的%x副词参数,它用于指定以十六进制的格式打印数组或slice全部的元素,%t副词参数是用于打印布尔型数据,%T副词参数是用于显示一个值对应的数据类型。

+

当调用一个函数的时候,函数的每个调用参数将会被赋值给函数内部的参数变量,所以函数参数变量接收的是一个复制的副本,并不是原始调用的变量。因为函数参数传递的机制导致传递大的数组类型将是低效的,并且对数组参数的任何的修改都是发生在复制的数组上,并不能直接修改调用时原始的数组变量。在这个方面,Go语言对待数组的方式和其它很多编程语言不同,其它编程语言可能会隐式地将数组作为引用或指针对象传入被调用的函数。

+

当然,我们可以显式地传入一个数组指针,那样的话函数通过指针对数组的任何修改都可以直接反馈到调用者。下面的函数用于给[32]byte类型的数组清零:

+
func zero(ptr *[32]byte) {
+	for i := range ptr {
+		ptr[i] = 0
+	}
+}
+
+

其实数组字面值[32]byte{}就可以生成一个32字节的数组。而且每个数组的元素都是零值初始化,也就是0。因此,我们可以将上面的zero函数写的更简洁一点:

+
func zero(ptr *[32]byte) {
+	*ptr = [32]byte{}
+}
+
+

虽然通过指针来传递数组参数是高效的,而且也允许在函数内部修改数组的值,但是数组依然是僵化的类型,因为数组的类型包含了僵化的长度信息。上面的zero函数并不能接收指向[16]byte类型数组的指针,而且也没有任何添加或删除数组元素的方法。由于这些原因,除了像SHA256这类需要处理特定大小数组的特例外,数组依然很少用作函数参数;相反,我们一般使用slice来替代数组。

+

练习 4.1: 编写一个函数,计算两个SHA256哈希码中不同bit的数目。(参考2.6.2节的PopCount函数。)

+

练习 4.2: 编写一个程序,默认情况下打印标准输入的SHA256编码,并支持通过命令行flag定制,输出SHA384或SHA512哈希算法。

+

4.2. Slice

+

Slice(切片)代表变长的序列,序列中每个元素都有相同的类型。一个slice类型一般写作[]T,其中T代表slice中元素的类型;slice的语法和数组很像,只是没有固定长度而已。

+

数组和slice之间有着紧密的联系。一个slice是一个轻量级的数据结构,提供了访问数组子序列(或者全部)元素的功能,而且slice的底层确实引用一个数组对象。一个slice由三个部分构成:指针、长度和容量。指针指向第一个slice元素对应的底层数组元素的地址,要注意的是slice的第一个元素并不一定就是数组的第一个元素。长度对应slice中元素的数目;长度不能超过容量,容量一般是从slice的开始位置到底层数据的结尾位置。内置的len和cap函数分别返回slice的长度和容量。

+

多个slice之间可以共享底层的数据,并且引用的数组部分区间可能重叠。图4.1显示了表示一年中每个月份名字的字符串数组,还有重叠引用了该数组的两个slice。数组这样定义

+
months := [...]string{1: "January", /* ... */, 12: "December"}
+
+

因此一月份是months[1],十二月份是months[12]。通常,数组的第一个元素从索引0开始,但是月份一般是从1开始的,因此我们声明数组时直接跳过第0个元素,第0个元素会被自动初始化为空字符串。

+

slice的切片操作s[i:j],其中0 ≤ i≤ j≤ cap(s),用于创建一个新的slice,引用s的从第i个元素开始到第j-1个元素的子序列。新的slice将只有j-i个元素。如果i位置的索引被省略的话将使用0代替,如果j位置的索引被省略的话将使用len(s)代替。因此,months[1:13]切片操作将引用全部有效的月份,和months[1:]操作等价;months[:]切片操作则是引用整个数组。让我们分别定义表示第二季度和北方夏天月份的slice,它们有重叠部分:

+

+
Q2 := months[4:7]
+summer := months[6:9]
+fmt.Println(Q2)     // ["April" "May" "June"]
+fmt.Println(summer) // ["June" "July" "August"]
+
+

两个slice都包含了六月份,下面的代码是一个包含相同月份的测试(性能较低):

+
for _, s := range summer {
+	for _, q := range Q2 {
+		if s == q {
+			fmt.Printf("%s appears in both\n", s)
+		}
+	}
+}
+
+

如果切片操作超出cap(s)的上限将导致一个panic异常,但是超出len(s)则是意味着扩展了slice,因为新slice的长度会变大:

+
fmt.Println(summer[:20]) // panic: out of range
+
+endlessSummer := summer[:5] // extend a slice (within capacity)
+fmt.Println(endlessSummer)  // "[June July August September October]"
+
+

另外,字符串的切片操作和[]byte字节类型切片的切片操作是类似的。都写作x[m:n],并且都是返回一个原始字节序列的子序列,底层都是共享之前的底层数组,因此这种操作都是常量时间复杂度。x[m:n]切片操作对于字符串则生成一个新字符串,如果x是[]byte的话则生成一个新的[]byte。

+

因为slice值包含指向第一个slice元素的指针,因此向函数传递slice将允许在函数内部修改底层数组的元素。换句话说,复制一个slice只是对底层的数组创建了一个新的slice别名(§2.3.2)。下面的reverse函数在原内存空间将[]int类型的slice反转,而且它可以用于任意长度的slice。

+

gopl.io/ch4/rev

+
// reverse reverses a slice of ints in place.
+func reverse(s []int) {
+	for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
+		s[i], s[j] = s[j], s[i]
+	}
+}
+
+

这里我们反转数组的应用:

+
a := [...]int{0, 1, 2, 3, 4, 5}
+reverse(a[:])
+fmt.Println(a) // "[5 4 3 2 1 0]"
+
+

一种将slice元素循环向左旋转n个元素的方法是三次调用reverse反转函数,第一次是反转开头的n个元素,然后是反转剩下的元素,最后是反转整个slice的元素。(如果是向右循环旋转,则将第三个函数调用移到第一个调用位置就可以了。)

+
s := []int{0, 1, 2, 3, 4, 5}
+// Rotate s left by two positions.
+reverse(s[:2])
+reverse(s[2:])
+reverse(s)
+fmt.Println(s) // "[2 3 4 5 0 1]"
+
+

要注意的是slice类型的变量s和数组类型的变量a的初始化语法的差异。slice和数组的字面值语法很类似,它们都是用花括弧包含一系列的初始化元素,但是对于slice并没有指明序列的长度。这会隐式地创建一个合适大小的数组,然后slice的指针指向底层的数组。就像数组字面值一样,slice的字面值也可以按顺序指定初始化值序列,或者是通过索引和元素值指定,或者用两种风格的混合语法初始化。

+

和数组不同的是,slice之间不能比较,因此我们不能使用==操作符来判断两个slice是否含有全部相等元素。不过标准库提供了高度优化的bytes.Equal函数来判断两个字节型slice是否相等([]byte),但是对于其他类型的slice,我们必须自己展开每个元素进行比较:

+
func equal(x, y []string) bool {
+	if len(x) != len(y) {
+		return false
+	}
+	for i := range x {
+		if x[i] != y[i] {
+			return false
+		}
+	}
+	return true
+}
+
+

上面关于两个slice的深度相等测试,运行的时间并不比支持==操作的数组或字符串更多,但是为何slice不直接支持比较运算符呢?这方面有两个原因。第一个原因,一个slice的元素是间接引用的,一个slice甚至可以包含自身(译注:当slice声明为[]interface{}时,slice的元素可以是自身)。虽然有很多办法处理这种情形,但是没有一个是简单有效的。

+

第二个原因,因为slice的元素是间接引用的,一个固定的slice值(译注:指slice本身的值,不是元素的值)在不同的时刻可能包含不同的元素,因为底层数组的元素可能会被修改。而例如Go语言中map的key只做简单的浅拷贝,它要求key在整个生命周期内保持不变性(译注:例如slice扩容,就会导致其本身的值/地址变化)。而用深度相等判断的话,显然在map的key这种场合不合适。对于像指针或chan之类的引用类型,==相等测试可以判断两个是否是引用相同的对象。一个针对slice的浅相等测试的==操作符可能是有一定用处的,也能临时解决map类型的key问题,但是slice和数组不同的相等测试行为会让人困惑。因此,安全的做法是直接禁止slice之间的比较操作。

+

slice唯一合法的比较操作是和nil比较,例如:

+
if summer == nil { /* ... */ }
+
+

一个零值的slice等于nil。一个nil值的slice并没有底层数组。一个nil值的slice的长度和容量都是0,但是也有非nil值的slice的长度和容量也是0的,例如[]int{}或make([]int, 3)[3:]。与任意类型的nil值一样,我们可以用[]int(nil)类型转换表达式来生成一个对应类型slice的nil值。

+
var s []int    // len(s) == 0, s == nil
+s = nil        // len(s) == 0, s == nil
+s = []int(nil) // len(s) == 0, s == nil
+s = []int{}    // len(s) == 0, s != nil
+
+

如果你需要测试一个slice是否是空的,使用len(s) == 0来判断,而不应该用s == nil来判断。除了和nil相等比较外,一个nil值的slice的行为和其它任意0长度的slice一样;例如reverse(nil)也是安全的。除了文档已经明确说明的地方,所有的Go语言函数应该以相同的方式对待nil值的slice和0长度的slice。

+

内置的make函数创建一个指定元素类型、长度和容量的slice。容量部分可以省略,在这种情况下,容量将等于长度。

+
make([]T, len)
+make([]T, len, cap) // same as make([]T, cap)[:len]
+
+

在底层,make创建了一个匿名的数组变量,然后返回一个slice;只有通过返回的slice才能引用底层匿名的数组变量。在第一种语句中,slice是整个数组的view。在第二个语句中,slice只引用了底层数组的前len个元素,但是容量将包含整个的数组。额外的元素是留给未来的增长用的。

+

4.2.1. append函数

+

内置的append函数用于向slice追加元素:

+
var runes []rune
+for _, r := range "Hello, 世界" {
+	runes = append(runes, r)
+}
+fmt.Printf("%q\n", runes) // "['H' 'e' 'l' 'l' 'o' ',' ' ' '世' '界']"
+
+

在循环中使用append函数构建一个由九个rune字符构成的slice,当然对应这个特殊的问题我们可以通过Go语言内置的[]rune("Hello, 世界")转换操作完成。

+

append函数对于理解slice底层是如何工作的非常重要,所以让我们仔细查看究竟是发生了什么。下面是第一个版本的appendInt函数,专门用于处理[]int类型的slice:

+

gopl.io/ch4/append

+
func appendInt(x []int, y int) []int {
+	var z []int
+	zlen := len(x) + 1
+	if zlen <= cap(x) {
+		// There is room to grow.  Extend the slice.
+		z = x[:zlen]
+	} else {
+		// There is insufficient space.  Allocate a new array.
+		// Grow by doubling, for amortized linear complexity.
+		zcap := zlen
+		if zcap < 2*len(x) {
+			zcap = 2 * len(x)
+		}
+		z = make([]int, zlen, zcap)
+		copy(z, x) // a built-in function; see text
+	}
+	z[len(x)] = y
+	return z
+}
+
+

每次调用appendInt函数,必须先检测slice底层数组是否有足够的容量来保存新添加的元素。如果有足够空间的话,直接扩展slice(依然在原有的底层数组之上),将新添加的y元素复制到新扩展的空间,并返回slice。因此,输入的x和输出的z共享相同的底层数组。

+

如果没有足够的增长空间的话,appendInt函数则会先分配一个足够大的slice用于保存新的结果,先将输入的x复制到新的空间,然后添加y元素。结果z和输入的x引用的将是不同的底层数组。

+

虽然通过循环复制元素更直接,不过内置的copy函数可以方便地将一个slice复制另一个相同类型的slice。copy函数的第一个参数是要复制的目标slice,第二个参数是源slice,目标和源的位置顺序和dst = src赋值语句是一致的。两个slice可以共享同一个底层数组,甚至有重叠也没有问题。copy函数将返回成功复制的元素的个数(我们这里没有用到),等于两个slice中较小的长度,所以我们不用担心覆盖会超出目标slice的范围。

+

为了提高内存使用效率,新分配的数组一般略大于保存x和y所需要的最低大小。通过在每次扩展数组时直接将长度翻倍从而避免了多次内存分配,也确保了添加单个元素操作的平均时间是一个常数时间。这个程序演示了效果:

+
func main() {
+	var x, y []int
+	for i := 0; i < 10; i++ {
+		y = appendInt(x, i)
+		fmt.Printf("%d cap=%d\t%v\n", i, cap(y), y)
+		x = y
+	}
+}
+
+

每一次容量的变化都会导致重新分配内存和copy操作:

+
0  cap=1    [0]
+1  cap=2    [0 1]
+2  cap=4    [0 1 2]
+3  cap=4    [0 1 2 3]
+4  cap=8    [0 1 2 3 4]
+5  cap=8    [0 1 2 3 4 5]
+6  cap=8    [0 1 2 3 4 5 6]
+7  cap=8    [0 1 2 3 4 5 6 7]
+8  cap=16   [0 1 2 3 4 5 6 7 8]
+9  cap=16   [0 1 2 3 4 5 6 7 8 9]
+
+

让我们仔细查看i=3次的迭代。当时x包含了[0 1 2]三个元素,但是容量是4,因此可以简单将新的元素添加到末尾,不需要新的内存分配。然后新的y的长度和容量都是4,并且和x引用着相同的底层数组,如图4.2所示。

+

+

在下一次迭代时i=4,现在没有新的空余的空间了,因此appendInt函数分配一个容量为8的底层数组,将x的4个元素[0 1 2 3]复制到新空间的开头,然后添加新的元素i,新元素的值是4。新的y的长度是5,容量是8;后面有3个空闲的位置,三次迭代都不需要分配新的空间。当前迭代中,y和x是对应不同底层数组的view。这次操作如图4.3所示。

+

+

内置的append函数可能使用比appendInt更复杂的内存扩展策略。因此,通常我们并不知道append调用是否导致了内存的重新分配,因此我们也不能确认新的slice和原始的slice是否引用的是相同的底层数组空间。同样,我们不能确认在原先的slice上的操作是否会影响到新的slice。因此,通常是将append返回的结果直接赋值给输入的slice变量:

+
runes = append(runes, r)
+
+

更新slice变量不仅对调用append函数是必要的,实际上对应任何可能导致长度、容量或底层数组变化的操作都是必要的。要正确地使用slice,需要记住尽管底层数组的元素是间接访问的,但是slice对应结构体本身的指针、长度和容量部分是直接访问的。要更新这些信息需要像上面例子那样一个显式的赋值操作。从这个角度看,slice并不是一个纯粹的引用类型,它实际上是一个类似下面结构体的聚合类型:

+
type IntSlice struct {
+	ptr      *int
+	len, cap int
+}
+
+

我们的appendInt函数每次只能向slice追加一个元素,但是内置的append函数则可以追加多个元素,甚至追加一个slice。

+
var x []int
+x = append(x, 1)
+x = append(x, 2, 3)
+x = append(x, 4, 5, 6)
+x = append(x, x...) // append the slice x
+fmt.Println(x)      // "[1 2 3 4 5 6 1 2 3 4 5 6]"
+
+

通过下面的小修改,我们可以达到append函数类似的功能。其中在appendInt函数参数中的最后的“...”省略号表示接收变长的参数为slice。我们将在5.7节详细解释这个特性。

+
func appendInt(x []int, y ...int) []int {
+	var z []int
+	zlen := len(x) + len(y)
+	// ...expand z to at least zlen...
+	copy(z[len(x):], y)
+	return z
+}
+
+

为了避免重复,和前面相同的代码并没有显示。

+

4.2.2. Slice内存技巧

+

让我们看看更多的例子,比如旋转slice、反转slice或在slice原有内存空间修改元素。给定一个字符串列表,下面的nonempty函数将在原有slice内存空间之上返回不包含空字符串的列表:

+

gopl.io/ch4/nonempty

+
// Nonempty is an example of an in-place slice algorithm.
+package main
+
+import "fmt"
+
+// nonempty returns a slice holding only the non-empty strings.
+// The underlying array is modified during the call.
+func nonempty(strings []string) []string {
+	i := 0
+	for _, s := range strings {
+		if s != "" {
+			strings[i] = s
+			i++
+		}
+	}
+	return strings[:i]
+}
+
+

比较微妙的地方是,输入的slice和输出的slice共享一个底层数组。这可以避免分配另一个数组,不过原来的数据将可能会被覆盖,正如下面两个打印语句看到的那样:

+
data := []string{"one", "", "three"}
+fmt.Printf("%q\n", nonempty(data)) // `["one" "three"]`
+fmt.Printf("%q\n", data)           // `["one" "three" "three"]`
+
+

因此我们通常会这样使用nonempty函数:data = nonempty(data)

+

nonempty函数也可以使用append函数实现:

+
func nonempty2(strings []string) []string {
+	out := strings[:0] // zero-length slice of original
+	for _, s := range strings {
+		if s != "" {
+			out = append(out, s)
+		}
+	}
+	return out
+}
+
+

无论如何实现,以这种方式重用一个slice一般都要求最多为每个输入值产生一个输出值,事实上很多这类算法都是用来过滤或合并序列中相邻的元素。这种slice用法是比较复杂的技巧,虽然使用到了slice的一些技巧,但是对于某些场合是比较清晰和有效的。

+

一个slice可以用来模拟一个stack。最初给定的空slice对应一个空的stack,然后可以使用append函数将新的值压入stack:

+
stack = append(stack, v) // push v
+
+

stack的顶部位置对应slice的最后一个元素:

+
top := stack[len(stack)-1] // top of stack
+
+

通过收缩stack可以弹出栈顶的元素

+
stack = stack[:len(stack)-1] // pop
+
+

要删除slice中间的某个元素并保存原有的元素顺序,可以通过内置的copy函数将后面的子slice向前依次移动一位完成:

+
func remove(slice []int, i int) []int {
+	copy(slice[i:], slice[i+1:])
+	return slice[:len(slice)-1]
+}
+
+func main() {
+	s := []int{5, 6, 7, 8, 9}
+	fmt.Println(remove(s, 2)) // "[5 6 8 9]"
+}
+
+

如果删除元素后不用保持原来顺序的话,我们可以简单的用最后一个元素覆盖被删除的元素:

+
func remove(slice []int, i int) []int {
+	slice[i] = slice[len(slice)-1]
+	return slice[:len(slice)-1]
+}
+
+func main() {
+	s := []int{5, 6, 7, 8, 9}
+	fmt.Println(remove(s, 2)) // "[5 6 9 8]
+}
+
+

练习 4.3: 重写reverse函数,使用数组指针代替slice。

+

练习 4.4: 编写一个rotate函数,通过一次循环完成旋转。

+

练习 4.5: 写一个函数在原地完成消除[]string中相邻重复的字符串的操作。

+

练习 4.6: 编写一个函数,原地将一个UTF-8编码的[]byte类型的slice中相邻的空格(参考unicode.IsSpace)替换成一个空格返回

+

练习 4.7: 修改reverse函数用于原地反转UTF-8编码的[]byte。是否可以不用分配额外的内存?

+

4.3. Map

+

哈希表是一种巧妙并且实用的数据结构。它是一个无序的key/value对的集合,其中所有的key都是不同的,然后通过给定的key可以在常数时间复杂度内检索、更新或删除对应的value。

+

在Go语言中,一个map就是一个哈希表的引用,map类型可以写为map[K]V,其中K和V分别对应key和value。map中所有的key都有相同的类型,所有的value也有着相同的类型,但是key和value之间可以是不同的数据类型。其中K对应的key必须是支持==比较运算符的数据类型,所以map可以通过测试key是否相等来判断是否已经存在。虽然浮点数类型也是支持相等运算符比较的,但是将浮点数用做key类型则是一个坏的想法,正如第三章提到的,最坏的情况是可能出现的NaN和任何浮点数都不相等。对于V对应的value数据类型则没有任何的限制。

+

内置的make函数可以创建一个map:

+
ages := make(map[string]int) // mapping from strings to ints
+
+

我们也可以用map字面值的语法创建map,同时还可以指定一些最初的key/value:

+
ages := map[string]int{
+	"alice":   31,
+	"charlie": 34,
+}
+
+

这相当于

+
ages := make(map[string]int)
+ages["alice"] = 31
+ages["charlie"] = 34
+
+

因此,另一种创建空的map的表达式是map[string]int{}

+

Map中的元素通过key对应的下标语法访问:

+
ages["alice"] = 32
+fmt.Println(ages["alice"]) // "32"
+
+

使用内置的delete函数可以删除元素:

+
delete(ages, "alice") // remove element ages["alice"]
+
+

所有这些操作是安全的,即使这些元素不在map中也没有关系;如果一个查找失败将返回value类型对应的零值,例如,即使map中不存在“bob”下面的代码也可以正常工作,因为ages["bob"]失败时将返回0。

+
ages["bob"] = ages["bob"] + 1 // happy birthday!
+
+

而且x += yx++等简短赋值语法也可以用在map上,所以上面的代码可以改写成

+
ages["bob"] += 1
+
+

更简单的写法

+
ages["bob"]++
+
+

但是map中的元素并不是一个变量,因此我们不能对map的元素进行取址操作:

+
_ = &ages["bob"] // compile error: cannot take address of map element
+
+

禁止对map元素取址的原因是map可能随着元素数量的增长而重新分配更大的内存空间,从而可能导致之前的地址无效。

+

要想遍历map中全部的key/value对的话,可以使用range风格的for循环实现,和之前的slice遍历语法类似。下面的迭代语句将在每次迭代时设置name和age变量,它们对应下一个键/值对:

+
for name, age := range ages {
+	fmt.Printf("%s\t%d\n", name, age)
+}
+
+

Map的迭代顺序是不确定的,并且不同的哈希函数实现可能导致不同的遍历顺序。在实践中,遍历的顺序是随机的,每一次遍历的顺序都不相同。这是故意的,每次都使用随机的遍历顺序可以强制要求程序不会依赖具体的哈希函数实现。如果要按顺序遍历key/value对,我们必须显式地对key进行排序,可以使用sort包的Strings函数对字符串slice进行排序。下面是常见的处理方式:

+
import "sort"
+
+var names []string
+for name := range ages {
+	names = append(names, name)
+}
+sort.Strings(names)
+for _, name := range names {
+	fmt.Printf("%s\t%d\n", name, ages[name])
+}
+
+

因为我们一开始就知道names的最终大小,因此给slice分配一个合适的大小将会更有效。下面的代码创建了一个空的slice,但是slice的容量刚好可以放下map中全部的key:

+
names := make([]string, 0, len(ages))
+
+

在上面的第一个range循环中,我们只关心map中的key,所以我们忽略了第二个循环变量。在第二个循环中,我们只关心names中的名字,所以我们使用“_”空白标识符来忽略第一个循环变量,也就是迭代slice时的索引。

+

map类型的零值是nil,也就是没有引用任何哈希表。

+
var ages map[string]int
+fmt.Println(ages == nil)    // "true"
+fmt.Println(len(ages) == 0) // "true"
+
+

map上的大部分操作,包括查找、删除、len和range循环都可以安全工作在nil值的map上,它们的行为和一个空的map类似。但是向一个nil值的map存入元素将导致一个panic异常:

+
ages["carol"] = 21 // panic: assignment to entry in nil map
+
+

在向map存数据前必须先创建map。

+

通过key作为索引下标来访问map将产生一个value。如果key在map中是存在的,那么将得到与key对应的value;如果key不存在,那么将得到value对应类型的零值,正如我们前面看到的ages["bob"]那样。这个规则很实用,但是有时候可能需要知道对应的元素是否真的是在map之中。例如,如果元素类型是一个数字,你可能需要区分一个已经存在的0,和不存在而返回零值的0,可以像下面这样测试:

+
age, ok := ages["bob"]
+if !ok { /* "bob" is not a key in this map; age == 0. */ }
+
+

你会经常看到将这两个结合起来使用,像这样:

+
if age, ok := ages["bob"]; !ok { /* ... */ }
+
+

在这种场景下,map的下标语法将产生两个值;第二个是一个布尔值,用于报告元素是否真的存在。布尔变量一般命名为ok,特别适合马上用于if条件判断部分。

+

和slice一样,map之间也不能进行相等比较;唯一的例外是和nil进行比较。要判断两个map是否包含相同的key和value,我们必须通过一个循环实现:

+
func equal(x, y map[string]int) bool {
+	if len(x) != len(y) {
+		return false
+	}
+	for k, xv := range x {
+		if yv, ok := y[k]; !ok || yv != xv {
+			return false
+		}
+	}
+	return true
+}
+
+

从例子中可以看到如何用!ok来区分元素不存在,与元素存在但为0的。我们不能简单地用xv != y[k]判断,那样会导致在判断下面两个map时产生错误的结果:

+
// True if equal is written incorrectly.
+equal(map[string]int{"A": 0}, map[string]int{"B": 42})
+
+

Go语言中并没有提供一个set类型,但是map中的key也是不相同的,可以用map实现类似set的功能。为了说明这一点,下面的dedup程序读取多行输入,但是只打印第一次出现的行。(它是1.3节中出现的dup程序的变体。)dedup程序通过map来表示所有的输入行所对应的set集合,以确保已经在集合存在的行不会被重复打印。

+

gopl.io/ch4/dedup

+
func main() {
+	seen := make(map[string]bool) // a set of strings
+	input := bufio.NewScanner(os.Stdin)
+	for input.Scan() {
+		line := input.Text()
+		if !seen[line] {
+			seen[line] = true
+			fmt.Println(line)
+		}
+	}
+
+	if err := input.Err(); err != nil {
+		fmt.Fprintf(os.Stderr, "dedup: %v\n", err)
+		os.Exit(1)
+	}
+}
+
+

Go程序员将这种忽略value的map当作一个字符串集合,并非所有map[string]bool类型value都是无关紧要的;有一些则可能会同时包含true和false的值。

+

有时候我们需要一个map或set的key是slice类型,但是map的key必须是可比较的类型,但是slice并不满足这个条件。不过,我们可以通过两个步骤绕过这个限制。第一步,定义一个辅助函数k,将slice转为map对应的string类型的key,确保只有x和y相等时k(x) == k(y)才成立。然后创建一个key为string类型的map,在每次对map操作时先用k辅助函数将slice转化为string类型。

+

下面的例子演示了如何使用map来记录提交相同的字符串列表的次数。它使用了fmt.Sprintf函数将字符串列表转换为一个字符串以用于map的key,通过%q参数忠实地记录每个字符串元素的信息:

+
var m = make(map[string]int)
+
+func k(list []string) string { return fmt.Sprintf("%q", list) }
+
+func Add(list []string)       { m[k(list)]++ }
+func Count(list []string) int { return m[k(list)] }
+
+

使用同样的技术可以处理任何不可比较的key类型,而不仅仅是slice类型。这种技术对于想使用自定义key比较函数的时候也很有用,例如在比较字符串的时候忽略大小写。同时,辅助函数k(x)也不一定是字符串类型,它可以返回任何可比较的类型,例如整数、数组或结构体等。

+

这是map的另一个例子,下面的程序用于统计输入中每个Unicode码点出现的次数。虽然Unicode全部码点的数量巨大,但是出现在特定文档中的字符种类并没有多少,使用map可以用比较自然的方式来跟踪那些出现过的字符的次数。

+

gopl.io/ch4/charcount

+
// Charcount computes counts of Unicode characters.
+package main
+
+import (
+	"bufio"
+	"fmt"
+	"io"
+	"os"
+	"unicode"
+	"unicode/utf8"
+)
+
+func main() {
+	counts := make(map[rune]int)    // counts of Unicode characters
+	var utflen [utf8.UTFMax + 1]int // count of lengths of UTF-8 encodings
+	invalid := 0                    // count of invalid UTF-8 characters
+
+	in := bufio.NewReader(os.Stdin)
+	for {
+		r, n, err := in.ReadRune() // returns rune, nbytes, error
+		if err == io.EOF {
+			break
+		}
+		if err != nil {
+			fmt.Fprintf(os.Stderr, "charcount: %v\n", err)
+			os.Exit(1)
+		}
+		if r == unicode.ReplacementChar && n == 1 {
+			invalid++
+			continue
+		}
+		counts[r]++
+		utflen[n]++
+	}
+	fmt.Printf("rune\tcount\n")
+	for c, n := range counts {
+		fmt.Printf("%q\t%d\n", c, n)
+	}
+	fmt.Print("\nlen\tcount\n")
+	for i, n := range utflen {
+		if i > 0 {
+			fmt.Printf("%d\t%d\n", i, n)
+		}
+	}
+	if invalid > 0 {
+		fmt.Printf("\n%d invalid UTF-8 characters\n", invalid)
+	}
+}
+
+

ReadRune方法执行UTF-8解码并返回三个值:解码的rune字符的值,字符UTF-8编码后的长度,和一个错误值。我们可预期的错误值只有对应文件结尾的io.EOF。如果输入的是无效的UTF-8编码的字符,返回的将是unicode.ReplacementChar表示无效字符,并且编码长度是1。

+

charcount程序同时打印不同UTF-8编码长度的字符数目。对此,map并不是一个合适的数据结构;因为UTF-8编码的长度总是从1到utf8.UTFMax(最大是4个字节),使用数组将更有效。

+

作为一个实验,我们用charcount程序对英文版原稿的字符进行了统计。虽然大部分是英语,但是也有一些非ASCII字符。下面是排名前10的非ASCII字符:

+

+

下面是不同UTF-8编码长度的字符的数目:

+
len count
+1   765391
+2   60
+3   70
+4   0
+
+

Map的value类型也可以是一个聚合类型,比如是一个map或slice。在下面的代码中,图graph的key类型是一个字符串,value类型map[string]bool代表一个字符串集合。从概念上讲,graph将一个字符串类型的key映射到一组相关的字符串集合,它们指向新的graph的key。

+

gopl.io/ch4/graph

+
var graph = make(map[string]map[string]bool)
+
+func addEdge(from, to string) {
+	edges := graph[from]
+	if edges == nil {
+		edges = make(map[string]bool)
+		graph[from] = edges
+	}
+	edges[to] = true
+}
+
+func hasEdge(from, to string) bool {
+	return graph[from][to]
+}
+
+

其中addEdge函数惰性初始化map是一个惯用方式,也就是说在每个值首次作为key时才初始化。hasEdge函数显示了如何让map的零值也能正常工作;即使from到to的边不存在,graph[from][to]依然可以返回一个有意义的结果。

+

练习 4.8: 修改charcount程序,使用unicode.IsLetter等相关的函数,统计字母、数字等Unicode中不同的字符类别。

+

练习 4.9: 编写一个程序wordfreq程序,报告输入文本中每个单词出现的频率。在第一次调用Scan前先调用input.Split(bufio.ScanWords)函数,这样可以按单词而不是按行输入。

+

4.4. 结构体

+

结构体是一种聚合的数据类型,是由零个或多个任意类型的值聚合成的实体。每个值称为结构体的成员。用结构体的经典案例是处理公司的员工信息,每个员工信息包含一个唯一的员工编号、员工的名字、家庭住址、出生日期、工作岗位、薪资、上级领导等等。所有的这些信息都需要绑定到一个实体中,可以作为一个整体单元被复制,作为函数的参数或返回值,或者是被存储到数组中,等等。

+

下面两个语句声明了一个叫Employee的命名的结构体类型,并且声明了一个Employee类型的变量dilbert:

+
type Employee struct {
+	ID        int
+	Name      string
+	Address   string
+	DoB       time.Time
+	Position  string
+	Salary    int
+	ManagerID int
+}
+
+var dilbert Employee
+
+

dilbert结构体变量的成员可以通过点操作符访问,比如dilbert.Name和dilbert.DoB。因为dilbert是一个变量,它所有的成员也同样是变量,我们可以直接对每个成员赋值:

+
dilbert.Salary -= 5000 // demoted, for writing too few lines of code
+
+

或者是对成员取地址,然后通过指针访问:

+
position := &dilbert.Position
+*position = "Senior " + *position // promoted, for outsourcing to Elbonia
+
+

点操作符也可以和指向结构体的指针一起工作:

+
var employeeOfTheMonth *Employee = &dilbert
+employeeOfTheMonth.Position += " (proactive team player)"
+
+

相当于下面语句

+
(*employeeOfTheMonth).Position += " (proactive team player)"
+
+

下面的EmployeeByID函数将根据给定的员工ID返回对应的员工信息结构体的指针。我们可以使用点操作符来访问它里面的成员:

+
func EmployeeByID(id int) *Employee { /* ... */ }
+
+fmt.Println(EmployeeByID(dilbert.ManagerID).Position) // "Pointy-haired boss"
+
+id := dilbert.ID
+EmployeeByID(id).Salary = 0 // fired for... no real reason
+
+

后面的语句通过EmployeeByID返回的结构体指针更新了Employee结构体的成员。如果将EmployeeByID函数的返回值从*Employee指针类型改为Employee值类型,那么更新语句将不能编译通过,因为在赋值语句的左边并不确定是一个变量(译注:调用函数返回的是值,并不是一个可取地址的变量)。

+

通常一行对应一个结构体成员,成员的名字在前类型在后,不过如果相邻的成员类型如果相同的话可以被合并到一行,就像下面的Name和Address成员那样:

+
type Employee struct {
+	ID            int
+	Name, Address string
+	DoB           time.Time
+	Position      string
+	Salary        int
+	ManagerID     int
+}
+
+

结构体成员的输入顺序也有重要的意义。我们也可以将Position成员合并(因为也是字符串类型),或者是交换Name和Address出现的先后顺序,那样的话就是定义了不同的结构体类型。通常,我们只是将相关的成员写到一起。

+

如果结构体成员名字是以大写字母开头的,那么该成员就是导出的;这是Go语言导出规则决定的。一个结构体可能同时包含导出和未导出的成员。

+

结构体类型往往是冗长的,因为它的每个成员可能都会占一行。虽然我们每次都可以重写整个结构体成员,但是重复会令人厌烦。因此,完整的结构体写法通常只在类型声明语句的地方出现,就像Employee类型声明语句那样。

+

一个命名为S的结构体类型将不能再包含S类型的成员:因为一个聚合的值不能包含它自身。(该限制同样适用于数组。)但是S类型的结构体可以包含*S指针类型的成员,这可以让我们创建递归的数据结构,比如链表和树结构等。在下面的代码中,我们使用一个二叉树来实现一个插入排序:

+

gopl.io/ch4/treesort

+
type tree struct {
+	value       int
+	left, right *tree
+}
+
+// Sort sorts values in place.
+func Sort(values []int) {
+	var root *tree
+	for _, v := range values {
+		root = add(root, v)
+	}
+	appendValues(values[:0], root)
+}
+
+// appendValues appends the elements of t to values in order
+// and returns the resulting slice.
+func appendValues(values []int, t *tree) []int {
+	if t != nil {
+		values = appendValues(values, t.left)
+		values = append(values, t.value)
+		values = appendValues(values, t.right)
+	}
+	return values
+}
+
+func add(t *tree, value int) *tree {
+	if t == nil {
+		// Equivalent to return &tree{value: value}.
+		t = new(tree)
+		t.value = value
+		return t
+	}
+	if value < t.value {
+		t.left = add(t.left, value)
+	} else {
+		t.right = add(t.right, value)
+	}
+	return t
+}
+
+

结构体类型的零值是每个成员都是零值。通常会将零值作为最合理的默认值。例如,对于bytes.Buffer类型,结构体初始值就是一个随时可用的空缓存,还有在第9章将会讲到的sync.Mutex的零值也是有效的未锁定状态。有时候这种零值可用的特性是自然获得的,但是也有些类型需要一些额外的工作。

+

如果结构体没有任何成员的话就是空结构体,写作struct{}。它的大小为0,也不包含任何信息,但是有时候依然是有价值的。有些Go语言程序员用map来模拟set数据结构时,用它来代替map中布尔类型的value,只是强调key的重要性,但是因为节约的空间有限,而且语法比较复杂,所以我们通常会避免这样的用法。

+
seen := make(map[string]struct{}) // set of strings
+// ...
+if _, ok := seen[s]; !ok {
+	seen[s] = struct{}{}
+	// ...first time seeing s...
+}
+
+

4.4.1. 结构体字面值

+

结构体值也可以用结构体字面值表示,结构体字面值可以指定每个成员的值。

+
type Point struct{ X, Y int }
+
+p := Point{1, 2}
+
+

这里有两种形式的结构体字面值语法,上面的是第一种写法,要求以结构体成员定义的顺序为每个结构体成员指定一个字面值。它要求写代码和读代码的人要记住结构体的每个成员的类型和顺序,不过结构体成员有细微的调整就可能导致上述代码不能编译。因此,上述的语法一般只在定义结构体的包内部使用,或者是在较小的结构体中使用,这些结构体的成员排列比较规则,比如image.Point{x, y}或color.RGBA{red, green, blue, alpha}。

+

其实更常用的是第二种写法,以成员名字和相应的值来初始化,可以包含部分或全部的成员,如1.4节的Lissajous程序的写法:

+
anim := gif.GIF{LoopCount: nframes}
+
+

在这种形式的结构体字面值写法中,如果成员被忽略的话将默认用零值。因为提供了成员的名字,所以成员出现的顺序并不重要。

+

两种不同形式的写法不能混合使用。而且,你不能企图在外部包中用第一种顺序赋值的技巧来偷偷地初始化结构体中未导出的成员。

+
package p
+type T struct{ a, b int } // a and b are not exported
+
+package q
+import "p"
+var _ = p.T{a: 1, b: 2} // compile error: can't reference a, b
+var _ = p.T{1, 2}       // compile error: can't reference a, b
+
+

虽然上面最后一行代码的编译错误信息中并没有显式提到未导出的成员,但是这样企图隐式使用未导出成员的行为也是不允许的。

+

结构体可以作为函数的参数和返回值。例如,这个Scale函数将Point类型的值缩放后返回:

+
func Scale(p Point, factor int) Point {
+	return Point{p.X * factor, p.Y * factor}
+}
+
+fmt.Println(Scale(Point{1, 2}, 5)) // "{5 10}"
+
+

如果考虑效率的话,较大的结构体通常会用指针的方式传入和返回,

+
func Bonus(e *Employee, percent int) int {
+	return e.Salary * percent / 100
+}
+
+

如果要在函数内部修改结构体成员的话,用指针传入是必须的;因为在Go语言中,所有的函数参数都是值拷贝传入的,函数参数将不再是函数调用时的原始变量。

+
func AwardAnnualRaise(e *Employee) {
+	e.Salary = e.Salary * 105 / 100
+}
+
+

因为结构体通常通过指针处理,可以用下面的写法来创建并初始化一个结构体变量,并返回结构体的地址:

+
pp := &Point{1, 2}
+
+

它和下面的语句是等价的

+
pp := new(Point)
+*pp = Point{1, 2}
+
+

不过&Point{1, 2}写法可以直接在表达式中使用,比如一个函数调用。

+

4.4.2. 结构体比较

+

如果结构体的全部成员都是可以比较的,那么结构体也是可以比较的,那样的话两个结构体将可以使用==或!=运算符进行比较。相等比较运算符==将比较两个结构体的每个成员,因此下面两个比较的表达式是等价的:

+
type Point struct{ X, Y int }
+
+p := Point{1, 2}
+q := Point{2, 1}
+fmt.Println(p.X == q.X && p.Y == q.Y) // "false"
+fmt.Println(p == q)                   // "false"
+
+

可比较的结构体类型和其他可比较的类型一样,可以用于map的key类型。

+
type address struct {
+	hostname string
+	port     int
+}
+
+hits := make(map[address]int)
+hits[address{"golang.org", 443}]++
+
+

4.4.3. 结构体嵌入和匿名成员

+

在本节中,我们将看到如何使用Go语言提供的不同寻常的结构体嵌入机制让一个命名的结构体包含另一个结构体类型的匿名成员,这样就可以通过简单的点运算符x.f来访问匿名成员链中嵌套的x.d.e.f成员。

+

考虑一个二维的绘图程序,提供了一个各种图形的库,例如矩形、椭圆形、星形和轮形等几何形状。这里是其中两个的定义:

+
type Circle struct {
+	X, Y, Radius int
+}
+
+type Wheel struct {
+	X, Y, Radius, Spokes int
+}
+
+

一个Circle代表的圆形类型包含了标准圆心的X和Y坐标信息,和一个Radius表示的半径信息。一个Wheel轮形除了包含Circle类型所有的全部成员外,还增加了Spokes表示径向辐条的数量。我们可以这样创建一个wheel变量:

+
var w Wheel
+w.X = 8
+w.Y = 8
+w.Radius = 5
+w.Spokes = 20
+
+

随着库中几何形状数量的增多,我们一定会注意到它们之间的相似和重复之处,所以我们可能为了便于维护而将相同的属性独立出来:

+
type Point struct {
+	X, Y int
+}
+
+type Circle struct {
+	Center Point
+	Radius int
+}
+
+type Wheel struct {
+	Circle Circle
+	Spokes int
+}
+
+

这样改动之后结构体类型变的清晰了,但是这种修改同时也导致了访问每个成员变得繁琐:

+
var w Wheel
+w.Circle.Center.X = 8
+w.Circle.Center.Y = 8
+w.Circle.Radius = 5
+w.Spokes = 20
+
+

Go语言有一个特性让我们只声明一个成员对应的数据类型而不指名成员的名字;这类成员就叫匿名成员。匿名成员的数据类型必须是命名的类型或指向一个命名的类型的指针。下面的代码中,Circle和Wheel各自都有一个匿名成员。我们可以说Point类型被嵌入到了Circle结构体,同时Circle类型被嵌入到了Wheel结构体。

+
type Circle struct {
+	Point
+	Radius int
+}
+
+type Wheel struct {
+	Circle
+	Spokes int
+}
+
+

得益于匿名嵌入的特性,我们可以直接访问叶子属性而不需要给出完整的路径:

+
var w Wheel
+w.X = 8            // equivalent to w.Circle.Point.X = 8
+w.Y = 8            // equivalent to w.Circle.Point.Y = 8
+w.Radius = 5       // equivalent to w.Circle.Radius = 5
+w.Spokes = 20
+
+

在右边的注释中给出的显式形式访问这些叶子成员的语法依然有效,因此匿名成员并不是真的无法访问了。其中匿名成员Circle和Point都有自己的名字——就是命名的类型名字——但是这些名字在点操作符中是可选的。我们在访问子成员的时候可以忽略任何匿名成员部分。

+

不幸的是,结构体字面值并没有简短表示匿名成员的语法, 因此下面的语句都不能编译通过:

+
w = Wheel{8, 8, 5, 20}                       // compile error: unknown fields
+w = Wheel{X: 8, Y: 8, Radius: 5, Spokes: 20} // compile error: unknown fields
+
+

结构体字面值必须遵循形状类型声明时的结构,所以我们只能用下面的两种语法,它们彼此是等价的:

+

gopl.io/ch4/embed

+
w = Wheel{Circle{Point{8, 8}, 5}, 20}
+
+w = Wheel{
+	Circle: Circle{
+		Point:  Point{X: 8, Y: 8},
+		Radius: 5,
+	},
+	Spokes: 20, // NOTE: trailing comma necessary here (and at Radius)
+}
+
+fmt.Printf("%#v\n", w)
+// Output:
+// Wheel{Circle:Circle{Point:Point{X:8, Y:8}, Radius:5}, Spokes:20}
+
+w.X = 42
+
+fmt.Printf("%#v\n", w)
+// Output:
+// Wheel{Circle:Circle{Point:Point{X:42, Y:8}, Radius:5}, Spokes:20}
+
+

需要注意的是Printf函数中%v参数包含的#副词,它表示用和Go语言类似的语法打印值。对于结构体类型来说,将包含每个成员的名字。

+

因为匿名成员也有一个隐式的名字,因此不能同时包含两个类型相同的匿名成员,这会导致名字冲突。同时,因为成员的名字是由其类型隐式地决定的,所以匿名成员也有可见性的规则约束。在上面的例子中,Point和Circle匿名成员都是导出的。即使它们不导出(比如改成小写字母开头的point和circle),我们依然可以用简短形式访问匿名成员嵌套的成员

+
w.X = 8 // equivalent to w.circle.point.X = 8
+
+

但是在包外部,因为circle和point没有导出,不能访问它们的成员,因此简短的匿名成员访问语法也是禁止的。

+

到目前为止,我们看到匿名成员特性只是对访问嵌套成员的点运算符提供了简短的语法糖。稍后,我们将会看到匿名成员并不要求是结构体类型;其实任何命名的类型都可以作为结构体的匿名成员。但是为什么要嵌入一个没有任何子成员类型的匿名成员类型呢?

+

答案是匿名类型的方法集。简短的点运算符语法可以用于选择匿名成员嵌套的成员,也可以用于访问它们的方法。实际上,外层的结构体不仅仅是获得了匿名成员类型的所有成员,而且也获得了该类型导出的全部的方法。这个机制可以用于将一些有简单行为的对象组合成有复杂行为的对象。组合是Go语言中面向对象编程的核心,我们将在6.3节中专门讨论。

+

4.5. JSON

+

JavaScript对象表示法(JSON)是一种用于发送和接收结构化信息的标准协议。在类似的协议中,JSON并不是唯一的一个标准协议。 XML(§7.14)、ASN.1和Google的Protocol Buffers都是类似的协议,并且有各自的特色,但是由于简洁性、可读性和流行程度等原因,JSON是应用最广泛的一个。

+

Go语言对于这些标准格式的编码和解码都有良好的支持,由标准库中的encoding/json、encoding/xml、encoding/asn1等包提供支持(译注:Protocol Buffers的支持由 github.com/golang/protobuf 包提供),并且这类包都有着相似的API接口。本节,我们将对重要的encoding/json包的用法做个概述。

+

JSON是对JavaScript中各种类型的值——字符串、数字、布尔值和对象——Unicode本文编码。它可以用有效可读的方式表示第三章的基础数据类型和本章的数组、slice、结构体和map等聚合数据类型。

+

基本的JSON类型有数字(十进制或科学记数法)、布尔值(true或false)、字符串,其中字符串是以双引号包含的Unicode字符序列,支持和Go语言类似的反斜杠转义特性,不过JSON使用的是\Uhhhh转义数字来表示一个UTF-16编码(译注:UTF-16和UTF-8一样是一种变长的编码,有些Unicode码点较大的字符需要用4个字节表示;而且UTF-16还有大端和小端的问题),而不是Go语言的rune类型。

+

这些基础类型可以通过JSON的数组和对象类型进行递归组合。一个JSON数组是一个有序的值序列,写在一个方括号中并以逗号分隔;一个JSON数组可以用于编码Go语言的数组和slice。一个JSON对象是一个字符串到值的映射,写成一系列的name:value对形式,用花括号包含并以逗号分隔;JSON的对象类型可以用于编码Go语言的map类型(key类型是字符串)和结构体。例如:

+
boolean         true
+number          -273.15
+string          "She said \"Hello, BF\""
+array           ["gold", "silver", "bronze"]
+object          {"year": 1980,
+                 "event": "archery",
+                 "medals": ["gold", "silver", "bronze"]}
+
+

考虑一个应用程序,该程序负责收集各种电影评论并提供反馈功能。它的Movie数据类型和一个典型的表示电影的值列表如下所示。(在结构体声明中,Year和Color成员后面的字符串面值是结构体成员Tag;我们稍后会解释它的作用。)

+

gopl.io/ch4/movie

+
type Movie struct {
+	Title  string
+	Year   int  `json:"released"`
+	Color  bool `json:"color,omitempty"`
+	Actors []string
+}
+
+var movies = []Movie{
+	{Title: "Casablanca", Year: 1942, Color: false,
+		Actors: []string{"Humphrey Bogart", "Ingrid Bergman"}},
+	{Title: "Cool Hand Luke", Year: 1967, Color: true,
+		Actors: []string{"Paul Newman"}},
+	{Title: "Bullitt", Year: 1968, Color: true,
+		Actors: []string{"Steve McQueen", "Jacqueline Bisset"}},
+	// ...
+}
+
+

这样的数据结构特别适合JSON格式,并且在两者之间相互转换也很容易。将一个Go语言中类似movies的结构体slice转为JSON的过程叫编组(marshaling)。编组通过调用json.Marshal函数完成:

+
data, err := json.Marshal(movies)
+if err != nil {
+	log.Fatalf("JSON marshaling failed: %s", err)
+}
+fmt.Printf("%s\n", data)
+
+

Marshal函数返回一个编码后的字节slice,包含很长的字符串,并且没有空白缩进;我们将它折行以便于显示:

+
[{"Title":"Casablanca","released":1942,"Actors":["Humphrey Bogart","Ingr
+id Bergman"]},{"Title":"Cool Hand Luke","released":1967,"color":true,"Ac
+tors":["Paul Newman"]},{"Title":"Bullitt","released":1968,"color":true,"
+Actors":["Steve McQueen","Jacqueline Bisset"]}]
+
+

这种紧凑的表示形式虽然包含了全部的信息,但是很难阅读。为了生成便于阅读的格式,另一个json.MarshalIndent函数将产生整齐缩进的输出。该函数有两个额外的字符串参数用于表示每一行输出的前缀和每一个层级的缩进:

+
data, err := json.MarshalIndent(movies, "", "    ")
+if err != nil {
+	log.Fatalf("JSON marshaling failed: %s", err)
+}
+fmt.Printf("%s\n", data)
+
+

上面的代码将产生这样的输出(译注:在最后一个成员或元素后面并没有逗号分隔符):

+
[
+	{
+		"Title": "Casablanca",
+		"released": 1942,
+		"Actors": [
+			"Humphrey Bogart",
+			"Ingrid Bergman"
+		]
+	},
+	{
+		"Title": "Cool Hand Luke",
+		"released": 1967,
+		"color": true,
+		"Actors": [
+			"Paul Newman"
+		]
+	},
+	{
+		"Title": "Bullitt",
+		"released": 1968,
+		"color": true,
+		"Actors": [
+			"Steve McQueen",
+			"Jacqueline Bisset"
+		]
+	}
+]
+
+

在编码时,默认使用Go语言结构体的成员名字作为JSON的对象(通过reflect反射技术,我们将在12.6节讨论)。只有导出的结构体成员才会被编码,这也就是我们为什么选择用大写字母开头的成员名称。

+

细心的读者可能已经注意到,其中Year名字的成员在编码后变成了released,还有Color成员编码后变成了小写字母开头的color。这是因为结构体成员Tag所导致的。一个结构体成员Tag是和在编译阶段关联到该成员的元信息字符串:

+
Year  int  `json:"released"`
+Color bool `json:"color,omitempty"`
+
+

结构体的成员Tag可以是任意的字符串面值,但是通常是一系列用空格分隔的key:"value"键值对序列;因为值中含有双引号字符,因此成员Tag一般用原生字符串面值的形式书写。json开头键名对应的值用于控制encoding/json包的编码和解码的行为,并且encoding/...下面其它的包也遵循这个约定。成员Tag中json对应值的第一部分用于指定JSON对象的名字,比如将Go语言中的TotalCount成员对应到JSON中的total_count对象。Color成员的Tag还带了一个额外的omitempty选项,表示当Go语言结构体成员为空或零值时不生成该JSON对象(这里false为零值)。果然,Casablanca是一个黑白电影,并没有输出Color成员。

+

编码的逆操作是解码,对应将JSON数据解码为Go语言的数据结构,Go语言中一般叫unmarshaling,通过json.Unmarshal函数完成。下面的代码将JSON格式的电影数据解码为一个结构体slice,结构体中只有Title成员。通过定义合适的Go语言数据结构,我们可以选择性地解码JSON中感兴趣的成员。当Unmarshal函数调用返回,slice将被只含有Title信息的值填充,其它JSON成员将被忽略。

+
var titles []struct{ Title string }
+if err := json.Unmarshal(data, &titles); err != nil {
+	log.Fatalf("JSON unmarshaling failed: %s", err)
+}
+fmt.Println(titles) // "[{Casablanca} {Cool Hand Luke} {Bullitt}]"
+
+

许多web服务都提供JSON接口,通过HTTP接口发送JSON格式请求并返回JSON格式的信息。为了说明这一点,我们通过Github的issue查询服务来演示类似的用法。首先,我们要定义合适的类型和常量:

+

gopl.io/ch4/github

+
// Package github provides a Go API for the GitHub issue tracker.
+// See https://developer.github.com/v3/search/#search-issues.
+package github
+
+import "time"
+
+const IssuesURL = "https://api.github.com/search/issues"
+
+type IssuesSearchResult struct {
+	TotalCount int `json:"total_count"`
+	Items          []*Issue
+}
+
+type Issue struct {
+	Number    int
+	HTMLURL   string `json:"html_url"`
+	Title     string
+	State     string
+	User      *User
+	CreatedAt time.Time `json:"created_at"`
+	Body      string    // in Markdown format
+}
+
+type User struct {
+	Login   string
+	HTMLURL string `json:"html_url"`
+}
+
+

和前面一样,即使对应的JSON对象名是小写字母,每个结构体的成员名也是声明为大写字母开头的。因为有些JSON成员名字和Go结构体成员名字并不相同,因此需要Go语言结构体成员Tag来指定对应的JSON名字。同样,在解码的时候也需要做同样的处理,GitHub服务返回的信息比我们定义的要多很多。

+

SearchIssues函数发出一个HTTP请求,然后解码返回的JSON格式的结果。因为用户提供的查询条件可能包含类似?&之类的特殊字符,为了避免对URL造成冲突,我们用url.QueryEscape来对查询中的特殊字符进行转义操作。

+

gopl.io/ch4/github

+
package github
+
+import (
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"net/url"
+	"strings"
+)
+
+// SearchIssues queries the GitHub issue tracker.
+func SearchIssues(terms []string) (*IssuesSearchResult, error) {
+	q := url.QueryEscape(strings.Join(terms, " "))
+	resp, err := http.Get(IssuesURL + "?q=" + q)
+	if err != nil {
+		return nil, err
+	}
+
+	// We must close resp.Body on all execution paths.
+	// (Chapter 5 presents 'defer', which makes this simpler.)
+	if resp.StatusCode != http.StatusOK {
+		resp.Body.Close()
+		return nil, fmt.Errorf("search query failed: %s", resp.Status)
+	}
+
+	var result IssuesSearchResult
+	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
+		resp.Body.Close()
+		return nil, err
+	}
+	resp.Body.Close()
+	return &result, nil
+}
+
+

在早些的例子中,我们使用了json.Unmarshal函数来将JSON格式的字符串解码为字节slice。但是这个例子中,我们使用了基于流式的解码器json.Decoder,它可以从一个输入流解码JSON数据,尽管这不是必须的。如您所料,还有一个针对输出流的json.Encoder编码对象。

+

我们调用Decode方法来填充变量。这里有多种方法可以格式化结构。下面是最简单的一种,以一个固定宽度打印每个issue,但是在下一节我们将看到如何利用模板来输出复杂的格式。

+

gopl.io/ch4/issues

+
// Issues prints a table of GitHub issues matching the search terms.
+package main
+
+import (
+	"fmt"
+	"log"
+	"os"
+
+	"gopl.io/ch4/github"
+)
+
+func main() {
+	result, err := github.SearchIssues(os.Args[1:])
+	if err != nil {
+		log.Fatal(err)
+	}
+	fmt.Printf("%d issues:\n", result.TotalCount)
+	for _, item := range result.Items {
+		fmt.Printf("#%-5d %9.9s %.55s\n",
+			item.Number, item.User.Login, item.Title)
+	}
+}
+
+

通过命令行参数指定检索条件。下面的命令是查询Go语言项目中和JSON解码相关的问题,还有查询返回的结果:

+
$ go build gopl.io/ch4/issues
+$ ./issues repo:golang/go is:open json decoder
+13 issues:
+#5680    eaigner encoding/json: set key converter on en/decoder
+#6050  gopherbot encoding/json: provide tokenizer
+#8658  gopherbot encoding/json: use bufio
+#8462  kortschak encoding/json: UnmarshalText confuses json.Unmarshal
+#5901        rsc encoding/json: allow override type marshaling
+#9812  klauspost encoding/json: string tag not symmetric
+#7872  extempora encoding/json: Encoder internally buffers full output
+#9650    cespare encoding/json: Decoding gives errPhase when unmarshalin
+#6716  gopherbot encoding/json: include field name in unmarshal error me
+#6901  lukescott encoding/json, encoding/xml: option to treat unknown fi
+#6384    joeshaw encoding/json: encode precise floating point integers u
+#6647    btracey x/tools/cmd/godoc: display type kind of each named type
+#4237  gjemiller encoding/base64: URLEncoding padding is optional
+
+

GitHub的Web服务接口 https://developer.github.com/v3/ 包含了更多的特性。

+

练习 4.10: 修改issues程序,根据问题的时间进行分类,比如不到一个月的、不到一年的、超过一年。

+

练习 4.11: 编写一个工具,允许用户在命令行创建、读取、更新和关闭GitHub上的issue,当必要的时候自动打开用户默认的编辑器用于输入文本信息。

+

练习 4.12: 流行的web漫画服务xkcd也提供了JSON接口。例如,一个 https://xkcd.com/571/info.0.json 请求将返回一个很多人喜爱的571编号的详细描述。下载每个链接(只下载一次)然后创建一个离线索引。编写一个xkcd工具,使用这些离线索引,打印和命令行输入的检索词相匹配的漫画的URL。

+

练习 4.13: 使用开放电影数据库的JSON服务接口,允许你检索和下载 https://omdbapi.com/ 上电影的名字和对应的海报图像。编写一个poster工具,通过命令行输入的电影名字,下载对应的海报。

+

4.6. 文本和HTML模板

+

前面的例子,只是最简单的格式化,使用Printf是完全足够的。但是有时候会需要复杂的打印格式,这时候一般需要将格式化代码分离出来以便更安全地修改。这些功能是由text/template和html/template等模板包提供的,它们提供了一个将变量值填充到一个文本或HTML格式的模板的机制。

+

一个模板是一个字符串或一个文件,里面包含了一个或多个由双花括号包含的{{action}}对象。大部分的字符串只是按字面值打印,但是对于actions部分将触发其它的行为。每个actions都包含了一个用模板语言书写的表达式,一个action虽然简短但是可以输出复杂的打印值,模板语言包含通过选择结构体的成员、调用函数或方法、表达式控制流if-else语句和range循环语句,还有其它实例化模板等诸多特性。下面是一个简单的模板字符串:

+

gopl.io/ch4/issuesreport

+
const templ = `{{.TotalCount}} issues:
+{{range .Items}}----------------------------------------
+Number: {{.Number}}
+User:   {{.User.Login}}
+Title:  {{.Title | printf "%.64s"}}
+Age:    {{.CreatedAt | daysAgo}} days
+{{end}}`
+
+

{% endraw %}

+

这个模板先打印匹配到的issue总数,然后打印每个issue的编号、创建用户、标题还有存在的时间。对于每一个action,都有一个当前值的概念,对应点操作符,写作“.”。当前值“.”最初被初始化为调用模板时的参数,在当前例子中对应github.IssuesSearchResult类型的变量。模板中{{.TotalCount}}对应action将展开为结构体中TotalCount成员以默认的方式打印的值。模板中{{range .Items}}{{end}}对应一个循环action,因此它们之间的内容可能会被展开多次,循环每次迭代的当前值对应当前的Items元素的值。

+

{% endraw %}

+

在一个action中,|操作符表示将前一个表达式的结果作为后一个函数的输入,类似于UNIX中管道的概念。在Title这一行的action中,第二个操作是一个printf函数,是一个基于fmt.Sprintf实现的内置函数,所有模板都可以直接使用。对于Age部分,第二个动作是一个叫daysAgo的函数,通过time.Since函数将CreatedAt成员转换为过去的时间长度:

+
func daysAgo(t time.Time) int {
+	return int(time.Since(t).Hours() / 24)
+}
+
+

需要注意的是CreatedAt的参数类型是time.Time,并不是字符串。以同样的方式,我们可以通过定义一些方法来控制字符串的格式化(§2.5),一个类型同样可以定制自己的JSON编码和解码行为。time.Time类型对应的JSON值是一个标准时间格式的字符串。

+

生成模板的输出需要两个处理步骤。第一步是要分析模板并转为内部表示,然后基于指定的输入执行模板。分析模板部分一般只需要执行一次。下面的代码创建并分析上面定义的模板templ。注意方法调用链的顺序:template.New先创建并返回一个模板;Funcs方法将daysAgo等自定义函数注册到模板中,并返回模板;最后调用Parse函数分析模板。

+
report, err := template.New("report").
+	Funcs(template.FuncMap{"daysAgo": daysAgo}).
+	Parse(templ)
+if err != nil {
+	log.Fatal(err)
+}
+
+

因为模板通常在编译时就测试好了,如果模板解析失败将是一个致命的错误。template.Must辅助函数可以简化这个致命错误的处理:它接受一个模板和一个error类型的参数,检测error是否为nil(如果不是nil则发出panic异常),然后返回传入的模板。我们将在5.9节再讨论这个话题。

+

一旦模板已经创建、注册了daysAgo函数、并通过分析和检测,我们就可以使用github.IssuesSearchResult作为输入源、os.Stdout作为输出源来执行模板:

+
var report = template.Must(template.New("issuelist").
+	Funcs(template.FuncMap{"daysAgo": daysAgo}).
+	Parse(templ))
+
+func main() {
+	result, err := github.SearchIssues(os.Args[1:])
+	if err != nil {
+		log.Fatal(err)
+	}
+	if err := report.Execute(os.Stdout, result); err != nil {
+		log.Fatal(err)
+	}
+}
+
+

程序输出一个纯文本报告:

+
$ go build gopl.io/ch4/issuesreport
+$ ./issuesreport repo:golang/go is:open json decoder
+13 issues:
+----------------------------------------
+Number: 5680
+User:      eaigner
+Title:     encoding/json: set key converter on en/decoder
+Age:       750 days
+----------------------------------------
+Number: 6050
+User:      gopherbot
+Title:     encoding/json: provide tokenizer
+Age:       695 days
+----------------------------------------
+...
+
+

现在让我们转到html/template模板包。它使用和text/template包相同的API和模板语言,但是增加了一个将字符串自动转义特性,这可以避免输入字符串和HTML、JavaScript、CSS或URL语法产生冲突的问题。这个特性还可以避免一些长期存在的安全问题,比如通过生成HTML注入攻击,通过构造一个含有恶意代码的问题标题,这些都可能让模板输出错误的输出,从而让他们控制页面。

+

下面的模板以HTML格式输出issue列表。注意import语句的不同:

+

gopl.io/ch4/issueshtml

+
import "html/template"
+
+var issueList = template.Must(template.New("issuelist").Parse(`
+<h1>{{.TotalCount}} issues</h1>
+<table>
+<tr style='text-align: left'>
+  <th>#</th>
+  <th>State</th>
+  <th>User</th>
+  <th>Title</th>
+</tr>
+{{range .Items}}
+<tr>
+  <td><a href='{{.HTMLURL}}'>{{.Number}}</a></td>
+  <td>{{.State}}</td>
+  <td><a href='{{.User.HTMLURL}}'>{{.User.Login}}</a></td>
+  <td><a href='{{.HTMLURL}}'>{{.Title}}</a></td>
+</tr>
+{{end}}
+</table>
+`))
+
+

{% endraw %}

+

下面的命令将在新的模板上执行一个稍微不同的查询:

+
$ go build gopl.io/ch4/issueshtml
+$ ./issueshtml repo:golang/go commenter:gopherbot json encoder >issues.html
+
+

图4.4显示了在web浏览器中的效果图。每个issue包含到Github对应页面的链接。

+

+

图4.4中issue没有包含会对HTML格式产生冲突的特殊字符,但是我们马上将看到标题中含有&<字符的issue。下面的命令选择了两个这样的issue:

+
$ ./issueshtml repo:golang/go 3133 10535 >issues2.html
+
+

图4.5显示了该查询的结果。注意,html/template包已经自动将特殊字符转义,因此我们依然可以看到正确的字面值。如果我们使用text/template包的话,这2个issue将会产生错误,其中“&lt;”四个字符将会被当作小于字符“<”处理,同时“<link>”字符串将会被当作一个链接元素处理,它们都会导致HTML文档结构的改变,从而导致有未知的风险。

+

我们也可以通过对信任的HTML字符串使用template.HTML类型来抑制这种自动转义的行为。还有很多采用类型命名的字符串类型分别对应信任的JavaScript、CSS和URL。下面的程序演示了两个使用不同类型的相同字符串产生的不同结果:A是一个普通字符串,B是一个信任的template.HTML字符串类型。

+

+

gopl.io/ch4/autoescape

+
func main() {
+	const templ = `<p>A: {{.A}}</p><p>B: {{.B}}</p>`
+	t := template.Must(template.New("escape").Parse(templ))
+	var data struct {
+		A string        // untrusted plain text
+		B template.HTML // trusted HTML
+	}
+	data.A = "<b>Hello!</b>"
+	data.B = "<b>Hello!</b>"
+	if err := t.Execute(os.Stdout, data); err != nil {
+		log.Fatal(err)
+	}
+}
+
+

{% endraw %}

+

图4.6显示了出现在浏览器中的模板输出。我们看到A的黑体标记被转义失效了,但是B没有。

+

+

我们这里只讲述了模板系统中最基本的特性。一如既往,如果想了解更多的信息,请自己查看包文档:

+
$ go doc text/template
+$ go doc html/template
+
+

练习 4.14: 创建一个web服务器,查询一次GitHub,然后生成BUG报告、里程碑和对应的用户信息。

+

第5章 函数

+

函数可以让我们将一个语句序列打包为一个单元,然后可以从程序中其它地方多次调用。函数的机制可以让我们将一个大的工作分解为小的任务,这样的小任务可以让不同程序员在不同时间、不同地方独立完成。一个函数同时对用户隐藏了其实现细节。由于这些因素,对于任何编程语言来说,函数都是一个至关重要的部分。

+

我们已经见过许多函数了。现在,让我们多花一点时间来彻底地讨论函数特性。本章的运行示例是一个网络蜘蛛,也就是web搜索引擎中负责抓取网页部分的组件,它们根据抓取网页中的链接继续抓取链接指向的页面。一个网络蜘蛛的例子给我们足够的机会去探索递归函数、匿名函数、错误处理和函数其它的很多特性。

+

5.1. 函数声明

+

函数声明包括函数名、形式参数列表、返回值列表(可省略)以及函数体。

+
func name(parameter-list) (result-list) {
+	body
+}
+
+

形式参数列表描述了函数的参数名以及参数类型。这些参数作为局部变量,其值由参数调用者提供。返回值列表描述了函数返回值的变量名以及类型。如果函数返回一个无名变量或者没有返回值,返回值列表的括号是可以省略的。如果一个函数声明不包括返回值列表,那么函数体执行完毕后,不会返回任何值。在hypot函数中:

+
func hypot(x, y float64) float64 {
+	return math.Sqrt(x*x + y*y)
+}
+fmt.Println(hypot(3,4)) // "5"
+
+

x和y是形参名,3和4是调用时的传入的实参,函数返回了一个float64类型的值。 +返回值也可以像形式参数一样被命名。在这种情况下,每个返回值被声明成一个局部变量,并根据该返回值的类型,将其初始化为该类型的零值。 +如果一个函数在声明时,包含返回值列表,该函数必须以 return语句结尾,除非函数明显无法运行到结尾处。例如函数在结尾时调用了panic异常或函数中存在无限循环。

+

正如hypot一样,如果一组形参或返回值有相同的类型,我们不必为每个形参都写出参数类型。下面2个声明是等价的:

+
func f(i, j, k int, s, t string)                 { /* ... */ }
+func f(i int, j int, k int,  s string, t string) { /* ... */ }
+
+

下面,我们给出4种方法声明拥有2个int型参数和1个int型返回值的函数.blank identifier(译者注:即下文的_符号)可以强调某个参数未被使用。

+
func add(x int, y int) int   {return x + y}
+func sub(x, y int) (z int)   { z = x - y; return}
+func first(x int, _ int) int { return x }
+func zero(int, int) int      { return 0 }
+
+fmt.Printf("%T\n", add)   // "func(int, int) int"
+fmt.Printf("%T\n", sub)   // "func(int, int) int"
+fmt.Printf("%T\n", first) // "func(int, int) int"
+fmt.Printf("%T\n", zero)  // "func(int, int) int"
+
+

函数的类型被称为函数的签名。如果两个函数形式参数列表和返回值列表中的变量类型一一对应,那么这两个函数被认为有相同的类型或签名。形参和返回值的变量名不影响函数签名,也不影响它们是否可以以省略参数类型的形式表示。

+

每一次函数调用都必须按照声明顺序为所有参数提供实参(参数值)。在函数调用时,Go语言没有默认参数值,也没有任何方法可以通过参数名指定形参,因此形参和返回值的变量名对于函数调用者而言没有意义。

+

在函数体中,函数的形参作为局部变量,被初始化为调用者提供的值。函数的形参和有名返回值作为函数最外层的局部变量,被存储在相同的词法块中。

+

实参通过值的方式传递,因此函数的形参是实参的拷贝。对形参进行修改不会影响实参。但是,如果实参包括引用类型,如指针,slice(切片)、map、function、channel等类型,实参可能会由于函数的间接引用被修改。

+

你可能会偶尔遇到没有函数体的函数声明,这表示该函数不是以Go实现的。这样的声明定义了函数签名。

+
package math
+
+func Sin(x float64) float //implemented in assembly language
+
+

5.2. 递归

+

函数可以是递归的,这意味着函数可以直接或间接的调用自身。对许多问题而言,递归是一种强有力的技术,例如处理递归的数据结构。在4.4节,我们通过遍历二叉树来实现简单的插入排序,在本章节,我们再次使用它来处理HTML文件。

+

下文的示例代码使用了非标准包 golang.org/x/net/html ,解析HTML。golang.org/x/... 目录下存储了一些由Go团队设计、维护,对网络编程、国际化文件处理、移动平台、图像处理、加密解密、开发者工具提供支持的扩展包。未将这些扩展包加入到标准库原因有二,一是部分包仍在开发中,二是对大多数Go语言的开发者而言,扩展包提供的功能很少被使用。

+

例子中调用golang.org/x/net/html的部分api如下所示。html.Parse函数读入一组bytes解析后,返回html.Node类型的HTML页面树状结构根节点。HTML拥有很多类型的结点如text(文本)、commnets(注释)类型,在下面的例子中,我们 只关注< name key='value' >形式的结点。

+

golang.org/x/net/html

+
package html
+
+type Node struct {
+	Type                    NodeType
+	Data                    string
+	Attr                    []Attribute
+	FirstChild, NextSibling *Node
+}
+
+type NodeType int32
+
+const (
+	ErrorNode NodeType = iota
+	TextNode
+	DocumentNode
+	ElementNode
+	CommentNode
+	DoctypeNode
+)
+
+type Attribute struct {
+	Key, Val string
+}
+
+func Parse(r io.Reader) (*Node, error)
+
+

main函数解析HTML标准输入,通过递归函数visit获得links(链接),并打印出这些links:

+

gopl.io/ch5/findlinks1

+
// Findlinks1 prints the links in an HTML document read from standard input.
+package main
+
+import (
+	"fmt"
+	"os"
+
+	"golang.org/x/net/html"
+)
+
+func main() {
+	doc, err := html.Parse(os.Stdin)
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "findlinks1: %v\n", err)
+		os.Exit(1)
+	}
+	for _, link := range visit(nil, doc) {
+		fmt.Println(link)
+	}
+}
+
+

visit函数遍历HTML的节点树,从每一个anchor元素的href属性获得link,将这些links存入字符串数组中,并返回这个字符串数组。

+
// visit appends to links each link found in n and returns the result.
+func visit(links []string, n *html.Node) []string {
+	if n.Type == html.ElementNode && n.Data == "a" {
+		for _, a := range n.Attr {
+			if a.Key == "href" {
+				links = append(links, a.Val)
+			}
+		}
+	}
+	for c := n.FirstChild; c != nil; c = c.NextSibling {
+		links = visit(links, c)
+	}
+	return links
+}
+
+

为了遍历结点n的所有后代结点,每次遇到n的孩子结点时,visit递归的调用自身。这些孩子结点存放在FirstChild链表中。

+

让我们以Go的主页(golang.org)作为目标,运行findlinks。我们以fetch(1.5章)的输出作为findlinks的输入。下面的输出做了简化处理。

+
$ go build gopl.io/ch1/fetch
+$ go build gopl.io/ch5/findlinks1
+$ ./fetch https://golang.org | ./findlinks1
+#
+/doc/
+/pkg/
+/help/
+/blog/
+http://play.golang.org/
+//tour.golang.org/
+https://golang.org/dl/
+//blog.golang.org/
+/LICENSE
+/doc/tos.html
+http://www.google.com/intl/en/policies/privacy/
+
+

注意在页面中出现的链接格式,在之后我们会介绍如何将这些链接,根据根路径( https://golang.org )生成可以直接访问的url。

+

在函数outline中,我们通过递归的方式遍历整个HTML结点树,并输出树的结构。在outline内部,每遇到一个HTML元素标签,就将其入栈,并输出。

+

gopl.io/ch5/outline

+
func main() {
+	doc, err := html.Parse(os.Stdin)
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "outline: %v\n", err)
+		os.Exit(1)
+	}
+	outline(nil, doc)
+}
+func outline(stack []string, n *html.Node) {
+	if n.Type == html.ElementNode {
+		stack = append(stack, n.Data) // push tag
+		fmt.Println(stack)
+	}
+	for c := n.FirstChild; c != nil; c = c.NextSibling {
+		outline(stack, c)
+	}
+}
+
+

有一点值得注意:outline有入栈操作,但没有相对应的出栈操作。当outline调用自身时,被调用者接收的是stack的拷贝。被调用者对stack的元素追加操作,修改的是stack的拷贝,其可能会修改slice底层的数组甚至是申请一块新的内存空间进行扩容;但这个过程并不会修改调用方的stack。因此当函数返回时,调用方的stack与其调用自身之前完全一致。

+

下面是 https://golang.org 页面的简要结构:

+
$ go build gopl.io/ch5/outline
+$ ./fetch https://golang.org | ./outline
+[html]
+[html head]
+[html head meta]
+[html head title]
+[html head link]
+[html body]
+[html body div]
+[html body div]
+[html body div div]
+[html body div div form]
+[html body div div form div]
+[html body div div form div a]
+...
+
+

正如你在上面实验中所见,大部分HTML页面只需几层递归就能被处理,但仍然有些页面需要深层次的递归。

+

大部分编程语言使用固定大小的函数调用栈,常见的大小从64KB到2MB不等。固定大小栈会限制递归的深度,当你用递归处理大量数据时,需要避免栈溢出;除此之外,还会导致安全性问题。与此相反,Go语言使用可变栈,栈的大小按需增加(初始时很小)。这使得我们使用递归时不必考虑溢出和安全问题。

+

练习 5.1: 修改findlinks代码中遍历n.FirstChild链表的部分,将循环调用visit,改成递归调用。

+

练习 5.2: 编写函数,记录在HTML树中出现的同名元素的次数。

+

练习 5.3: 编写函数输出所有text结点的内容。注意不要访问<script><style>元素,因为这些元素对浏览者是不可见的。

+

练习 5.4: 扩展visit函数,使其能够处理其他类型的结点,如images、scripts和style sheets。

+

5.3. 多返回值

+

在Go中,一个函数可以返回多个值。我们已经在之前例子中看到,许多标准库中的函数返回2个值,一个是期望得到的返回值,另一个是函数出错时的错误信息。下面的例子会展示如何编写多返回值的函数。

+

下面的程序是findlinks的改进版本。修改后的findlinks可以自己发起HTTP请求,这样我们就不必再运行fetch。因为HTTP请求和解析操作可能会失败,因此findlinks声明了2个返回值:链接列表和错误信息。一般而言,HTML的解析器可以处理HTML页面的错误结点,构造出HTML页面结构,所以解析HTML很少失败。这意味着如果findlinks函数失败了,很可能是由于I/O的错误导致的。

+

gopl.io/ch5/findlinks2

+
func main() {
+	for _, url := range os.Args[1:] {
+		links, err := findLinks(url)
+		if err != nil {
+			fmt.Fprintf(os.Stderr, "findlinks2: %v\n", err)
+			continue
+		}
+		for _, link := range links {
+			fmt.Println(link)
+		}
+	}
+}
+
+// findLinks performs an HTTP GET request for url, parses the
+// response as HTML, and extracts and returns the links.
+func findLinks(url string) ([]string, error) {
+	resp, err := http.Get(url)
+	if err != nil {
+		return nil, err
+	}
+	if resp.StatusCode != http.StatusOK {
+		resp.Body.Close()
+		return nil, fmt.Errorf("getting %s: %s", url, resp.Status)
+	}
+	doc, err := html.Parse(resp.Body)
+	resp.Body.Close()
+	if err != nil {
+		return nil, fmt.Errorf("parsing %s as HTML: %v", url, err)
+	}
+	return visit(nil, doc), nil
+}
+
+

在findlinks中,有4处return语句,每一处return都返回了一组值。前三处return,将http和html包中的错误信息传递给findlinks的调用者。第一处return直接返回错误信息,其他两处通过fmt.Errorf(§7.8)输出详细的错误信息。如果findlinks成功结束,最后的return语句将一组解析获得的连接返回给用户。

+

在findlinks中,我们必须确保resp.Body被关闭,释放网络资源。虽然Go的垃圾回收机制会回收不被使用的内存,但是这不包括操作系统层面的资源,比如打开的文件、网络连接。因此我们必须显式的释放这些资源。

+

调用多返回值函数时,返回给调用者的是一组值,调用者必须显式的将这些值分配给变量:

+
links, err := findLinks(url)
+
+

如果某个值不被使用,可以将其分配给blank identifier:

+
links, _ := findLinks(url) // errors ignored
+
+

一个函数内部可以将另一个有多返回值的函数调用作为返回值,下面的例子展示了与findLinks有相同功能的函数,两者的区别在于下面的例子先输出参数:

+
func findLinksLog(url string) ([]string, error) {
+	log.Printf("findLinks %s", url)
+	return findLinks(url)
+}
+
+

当你调用接受多参数的函数时,可以将一个返回多参数的函数调用作为该函数的参数。虽然这很少出现在实际生产代码中,但这个特性在debug时很方便,我们只需要一条语句就可以输出所有的返回值。下面的代码是等价的:

+
log.Println(findLinks(url))
+links, err := findLinks(url)
+log.Println(links, err)
+
+

准确的变量名可以传达函数返回值的含义。尤其在返回值的类型都相同时,就像下面这样:

+
func Size(rect image.Rectangle) (width, height int)
+func Split(path string) (dir, file string)
+func HourMinSec(t time.Time) (hour, minute, second int)
+
+

虽然良好的命名很重要,但你也不必为每一个返回值都取一个适当的名字。比如,按照惯例,函数的最后一个bool类型的返回值表示函数是否运行成功,error类型的返回值代表函数的错误信息,对于这些类似的惯例,我们不必思考合适的命名,它们都无需解释。

+

如果一个函数所有的返回值都有显式的变量名,那么该函数的return语句可以省略操作数。这称之为bare return。

+
// CountWordsAndImages does an HTTP GET request for the HTML
+// document url and returns the number of words and images in it.
+func CountWordsAndImages(url string) (words, images int, err error) {
+	resp, err := http.Get(url)
+	if err != nil {
+		return
+	}
+	doc, err := html.Parse(resp.Body)
+	resp.Body.Close()
+	if err != nil {
+		err = fmt.Errorf("parsing HTML: %s", err)
+		return
+	}
+	words, images = countWordsAndImages(doc)
+	return
+}
+func countWordsAndImages(n *html.Node) (words, images int) { /* ... */ }
+
+

按照返回值列表的次序,返回所有的返回值,在上面的例子中,每一个return语句等价于:

+
return words, images, err
+
+

当一个函数有多处return语句以及许多返回值时,bare return 可以减少代码的重复,但是使得代码难以被理解。举个例子,如果你没有仔细的审查代码,很难发现前2处return等价于 return 0,0,err(Go会将返回值 words和images在函数体的开始处,根据它们的类型,将其初始化为0),最后一处return等价于 return words, image, nil。基于以上原因,不宜过度使用bare return。

+

练习 5.5: 实现countWordsAndImages。(参考练习4.9如何分词)

+

练习 5.6: 修改gopl.io/ch3/surface(§3.2)中的corner函数,将返回值命名,并使用bare return。

+

5.4. 错误

+

在Go中有一部分函数总是能成功的运行。比如strings.Contains和strconv.FormatBool函数,对各种可能的输入都做了良好的处理,使得运行时几乎不会失败,除非遇到灾难性的、不可预料的情况,比如运行时的内存溢出。导致这种错误的原因很复杂,难以处理,从错误中恢复的可能性也很低。

+

还有一部分函数只要输入的参数满足一定条件,也能保证运行成功。比如time.Date函数,该函数将年月日等参数构造成time.Time对象,除非最后一个参数(时区)是nil。这种情况下会引发panic异常。panic是来自被调用函数的信号,表示发生了某个已知的bug。一个良好的程序永远不应该发生panic异常。

+

对于大部分函数而言,永远无法确保能否成功运行。这是因为错误的原因超出了程序员的控制。举个例子,任何进行I/O操作的函数都会面临出现错误的可能,只有没有经验的程序员才会相信读写操作不会失败,即使是简单的读写。因此,当本该可信的操作出乎意料的失败后,我们必须弄清楚导致失败的原因。

+

在Go的错误处理中,错误是软件包API和应用程序用户界面的一个重要组成部分,程序运行失败仅被认为是几个预期的结果之一。

+

对于那些将运行失败看作是预期结果的函数,它们会返回一个额外的返回值,通常是最后一个,来传递错误信息。如果导致失败的原因只有一个,额外的返回值可以是一个布尔值,通常被命名为ok。比如,cache.Lookup失败的唯一原因是key不存在,那么代码可以按照下面的方式组织:

+
value, ok := cache.Lookup(key)
+if !ok {
+	// ...cache[key] does not exist…
+}
+
+

通常,导致失败的原因不止一种,尤其是对I/O操作而言,用户需要了解更多的错误信息。因此,额外的返回值不再是简单的布尔类型,而是error类型。

+

内置的error是接口类型。我们将在第七章了解接口类型的含义,以及它对错误处理的影响。现在我们只需要明白error类型可能是nil或者non-nil。nil意味着函数运行成功,non-nil表示失败。对于non-nil的error类型,我们可以通过调用error的Error函数或者输出函数获得字符串类型的错误信息。

+
fmt.Println(err)
+fmt.Printf("%v", err)
+
+

通常,当函数返回non-nil的error时,其他的返回值是未定义的(undefined),这些未定义的返回值应该被忽略。然而,有少部分函数在发生错误时,仍然会返回一些有用的返回值。比如,当读取文件发生错误时,Read函数会返回可以读取的字节数以及错误信息。对于这种情况,正确的处理方式应该是先处理这些不完整的数据,再处理错误。因此对函数的返回值要有清晰的说明,以便于其他人使用。

+

在Go中,函数运行失败时会返回错误信息,这些错误信息被认为是一种预期的值而非异常(exception),这使得Go有别于那些将函数运行失败看作是异常的语言。虽然Go有各种异常机制,但这些机制仅被使用在处理那些未被预料到的错误,即bug,而不是那些在健壮程序中应该被避免的程序错误。对于Go的异常机制我们将在5.9介绍。

+

Go这样设计的原因是由于对于某个应该在控制流程中处理的错误而言,将这个错误以异常的形式抛出会混乱对错误的描述,这通常会导致一些糟糕的后果。当某个程序错误被当作异常处理后,这个错误会将堆栈跟踪信息返回给终端用户,这些信息复杂且无用,无法帮助定位错误。

+

正因此,Go使用控制流机制(如if和return)处理错误,这使得编码人员能更多的关注错误处理。

+

5.4.1. 错误处理策略

+

当一次函数调用返回错误时,调用者应该选择合适的方式处理错误。根据情况的不同,有很多处理方式,让我们来看看常用的五种方式。

+

首先,也是最常用的方式是传播错误。这意味着函数中某个子程序的失败,会变成该函数的失败。下面,我们以5.3节的findLinks函数作为例子。如果findLinks对http.Get的调用失败,findLinks会直接将这个HTTP错误返回给调用者:

+
resp, err := http.Get(url)
+if err != nil{
+	return nil, err
+}
+
+

当对html.Parse的调用失败时,findLinks不会直接返回html.Parse的错误,因为缺少两条重要信息:1、发生错误时的解析器(html parser);2、发生错误的url。因此,findLinks构造了一个新的错误信息,既包含了这两项,也包括了底层的解析出错的信息。

+
doc, err := html.Parse(resp.Body)
+resp.Body.Close()
+if err != nil {
+	return nil, fmt.Errorf("parsing %s as HTML: %v", url,err)
+}
+
+

fmt.Errorf函数使用fmt.Sprintf格式化错误信息并返回。我们使用该函数添加额外的前缀上下文信息到原始错误信息。当错误最终由main函数处理时,错误信息应提供清晰的从原因到后果的因果链,就像美国宇航局事故调查时做的那样:

+
genesis: crashed: no parachute: G-switch failed: bad relay orientation
+
+

由于错误信息经常是以链式组合在一起的,所以错误信息中应避免大写和换行符。最终的错误信息可能很长,我们可以通过类似grep的工具处理错误信息(译者注:grep是一种文本搜索工具)。

+

编写错误信息时,我们要确保错误信息对问题细节的描述是详尽的。尤其是要注意错误信息表达的一致性,即相同的函数或同包内的同一组函数返回的错误在构成和处理方式上是相似的。

+

以os包为例,os包确保文件操作(如os.Open、Read、Write、Close)返回的每个错误的描述不仅仅包含错误的原因(如无权限,文件目录不存在)也包含文件名,这样调用者在构造新的错误信息时无需再添加这些信息。

+

一般而言,被调用函数f(x)会将调用信息和参数信息作为发生错误时的上下文放在错误信息中并返回给调用者,调用者需要添加一些错误信息中不包含的信息,比如添加url到html.Parse返回的错误中。

+

让我们来看看处理错误的第二种策略。如果错误的发生是偶然性的,或由不可预知的问题导致的。一个明智的选择是重新尝试失败的操作。在重试时,我们需要限制重试的时间间隔或重试的次数,防止无限制的重试。

+

gopl.io/ch5/wait

+
// WaitForServer attempts to contact the server of a URL.
+// It tries for one minute using exponential back-off.
+// It reports an error if all attempts fail.
+func WaitForServer(url string) error {
+	const timeout = 1 * time.Minute
+	deadline := time.Now().Add(timeout)
+	for tries := 0; time.Now().Before(deadline); tries++ {
+		_, err := http.Head(url)
+		if err == nil {
+			return nil // success
+		}
+		log.Printf("server not responding (%s);retrying…", err)
+		time.Sleep(time.Second << uint(tries)) // exponential back-off
+	}
+	return fmt.Errorf("server %s failed to respond after %s", url, timeout)
+}
+
+

如果错误发生后,程序无法继续运行,我们就可以采用第三种策略:输出错误信息并结束程序。需要注意的是,这种策略只应在main中执行。对库函数而言,应仅向上传播错误,除非该错误意味着程序内部包含不一致性,即遇到了bug,才能在库函数中结束程序。

+
// (In function main.)
+if err := WaitForServer(url); err != nil {
+	fmt.Fprintf(os.Stderr, "Site is down: %v\n", err)
+	os.Exit(1)
+}
+
+

调用log.Fatalf可以更简洁的代码达到与上文相同的效果。log中的所有函数,都默认会在错误信息之前输出时间信息。

+
if err := WaitForServer(url); err != nil {
+	log.Fatalf("Site is down: %v\n", err)
+}
+
+

长时间运行的服务器常采用默认的时间格式,而交互式工具很少采用包含如此多信息的格式。

+
2006/01/02 15:04:05 Site is down: no such domain:
+bad.gopl.io
+
+

我们可以设置log的前缀信息屏蔽时间信息,一般而言,前缀信息会被设置成命令名。

+
log.SetPrefix("wait: ")
+log.SetFlags(0)
+
+

第四种策略:有时,我们只需要输出错误信息就足够了,不需要中断程序的运行。我们可以通过log包提供函数

+
if err := Ping(); err != nil {
+	log.Printf("ping failed: %v; networking disabled",err)
+}
+
+

或者标准错误流输出错误信息。

+
if err := Ping(); err != nil {
+	fmt.Fprintf(os.Stderr, "ping failed: %v; networking disabled\n", err)
+}
+
+

log包中的所有函数会为没有换行符的字符串增加换行符。

+

第五种,也是最后一种策略:我们可以直接忽略掉错误。

+
dir, err := ioutil.TempDir("", "scratch")
+if err != nil {
+	return fmt.Errorf("failed to create temp dir: %v",err)
+}
+// ...use temp dir…
+os.RemoveAll(dir) // ignore errors; $TMPDIR is cleaned periodically
+
+

尽管os.RemoveAll会失败,但上面的例子并没有做错误处理。这是因为操作系统会定期的清理临时目录。正因如此,虽然程序没有处理错误,但程序的逻辑不会因此受到影响。我们应该在每次函数调用后,都养成考虑错误处理的习惯,当你决定忽略某个错误时,你应该清晰地写下你的意图。

+

在Go中,错误处理有一套独特的编码风格。检查某个子函数是否失败后,我们通常将处理失败的逻辑代码放在处理成功的代码之前。如果某个错误会导致函数返回,那么成功时的逻辑代码不应放在else语句块中,而应直接放在函数体中。Go中大部分函数的代码结构几乎相同,首先是一系列的初始检查,防止错误发生,之后是函数的实际逻辑。

+

5.4.2. 文件结尾错误(EOF)

+

函数经常会返回多种错误,这对终端用户来说可能会很有趣,但对程序而言,这使得情况变得复杂。很多时候,程序必须根据错误类型,作出不同的响应。让我们考虑这样一个例子:从文件中读取n个字节。如果n等于文件的长度,读取过程的任何错误都表示失败。如果n小于文件的长度,调用者会重复的读取固定大小的数据直到文件结束。这会导致调用者必须分别处理由文件结束引起的各种错误。基于这样的原因,io包保证任何由文件结束引起的读取失败都返回同一个错误——io.EOF,该错误在io包中定义:

+
package io
+
+import "errors"
+
+// EOF is the error returned by Read when no more input is available.
+var EOF = errors.New("EOF")
+
+

调用者只需通过简单的比较,就可以检测出这个错误。下面的例子展示了如何从标准输入中读取字符,以及判断文件结束。(4.3的chartcount程序展示了更加复杂的代码)

+
in := bufio.NewReader(os.Stdin)
+for {
+	r, _, err := in.ReadRune()
+	if err == io.EOF {
+		break // finished reading
+	}
+	if err != nil {
+		return fmt.Errorf("read failed:%v", err)
+	}
+	// ...use r…
+}
+
+

因为文件结束这种错误不需要更多的描述,所以io.EOF有固定的错误信息——“EOF”。对于其他错误,我们可能需要在错误信息中描述错误的类型和数量,这使得我们不能像io.EOF一样采用固定的错误信息。在7.11节中,我们会提出更系统的方法区分某些固定的错误值。

+

5.5. 函数值

+

在Go中,函数被看作第一类值(first-class values):函数像其他值一样,拥有类型,可以被赋值给其他变量,传递给函数,从函数返回。对函数值(function value)的调用类似函数调用。例子如下:

+
	func square(n int) int { return n * n }
+	func negative(n int) int { return -n }
+	func product(m, n int) int { return m * n }
+
+	f := square
+	fmt.Println(f(3)) // "9"
+
+	f = negative
+	fmt.Println(f(3))     // "-3"
+	fmt.Printf("%T\n", f) // "func(int) int"
+
+	f = product // compile error: can't assign func(int, int) int to func(int) int
+
+

函数类型的零值是nil。调用值为nil的函数值会引起panic错误:

+
	var f func(int) int
+	f(3) // 此处f的值为nil, 会引起panic错误
+
+

函数值可以与nil比较:

+
	var f func(int) int
+	if f != nil {
+		f(3)
+	}
+
+

但是函数值之间是不可比较的,也不能用函数值作为map的key。

+

函数值使得我们不仅仅可以通过数据来参数化函数,亦可通过行为。标准库中包含许多这样的例子。下面的代码展示了如何使用这个技巧。strings.Map对字符串中的每个字符调用add1函数,并将每个add1函数的返回值组成一个新的字符串返回给调用者。

+
	func add1(r rune) rune { return r + 1 }
+
+	fmt.Println(strings.Map(add1, "HAL-9000")) // "IBM.:111"
+	fmt.Println(strings.Map(add1, "VMS"))      // "WNT"
+	fmt.Println(strings.Map(add1, "Admix"))    // "Benjy"
+
+

5.2节的findLinks函数使用了辅助函数visit,遍历和操作了HTML页面的所有结点。使用函数值,我们可以将遍历结点的逻辑和操作结点的逻辑分离,使得我们可以复用遍历的逻辑,从而对结点进行不同的操作。

+

gopl.io/ch5/outline2

+
// forEachNode针对每个结点x,都会调用pre(x)和post(x)。
+// pre和post都是可选的。
+// 遍历孩子结点之前,pre被调用
+// 遍历孩子结点之后,post被调用
+func forEachNode(n *html.Node, pre, post func(n *html.Node)) {
+	if pre != nil {
+		pre(n)
+	}
+	for c := n.FirstChild; c != nil; c = c.NextSibling {
+		forEachNode(c, pre, post)
+	}
+	if post != nil {
+		post(n)
+	}
+}
+
+

该函数接收2个函数作为参数,分别在结点的孩子被访问前和访问后调用。这样的设计给调用者更大的灵活性。举个例子,现在我们有startElement和endElement两个函数用于输出HTML元素的开始标签和结束标签<b>...</b>

+
var depth int
+func startElement(n *html.Node) {
+	if n.Type == html.ElementNode {
+		fmt.Printf("%*s<%s>\n", depth*2, "", n.Data)
+		depth++
+	}
+}
+func endElement(n *html.Node) {
+	if n.Type == html.ElementNode {
+		depth--
+		fmt.Printf("%*s</%s>\n", depth*2, "", n.Data)
+	}
+}
+
+

上面的代码利用fmt.Printf的一个小技巧控制输出的缩进。%*s中的*会在字符串之前填充一些空格。在例子中,每次输出会先填充depth*2数量的空格,再输出"",最后再输出HTML标签。

+

如果我们像下面这样调用forEachNode:

+
forEachNode(doc, startElement, endElement)
+
+

与之前的outline程序相比,我们得到了更加详细的页面结构:

+
$ go build gopl.io/ch5/outline2
+$ ./outline2 http://gopl.io
+<html>
+  <head>
+    <meta>
+    </meta>
+    <title>
+	</title>
+	<style>
+	</style>
+  </head>
+  <body>
+    <table>
+      <tbody>
+        <tr>
+          <td>
+            <a>
+              <img>
+              </img>
+...
+
+

练习 5.7: 完善startElement和endElement函数,使其成为通用的HTML输出器。要求:输出注释结点,文本结点以及每个元素的属性(< a href='...'>)。使用简略格式输出没有孩子结点的元素(即用<img/>代替<img></img>)。编写测试,验证程序输出的格式正确。(详见11章)

+

练习 5.8: 修改pre和post函数,使其返回布尔类型的返回值。返回false时,中止forEachNoded的遍历。使用修改后的代码编写ElementByID函数,根据用户输入的id查找第一个拥有该id元素的HTML元素,查找成功后,停止遍历。

+
func ElementByID(doc *html.Node, id string) *html.Node
+
+

练习 5.9: 编写函数expand,将s中的"foo"替换为f("foo")的返回值。

+
func expand(s string, f func(string) string) string
+
+

5.6. 匿名函数

+

拥有函数名的函数只能在包级语法块中被声明,通过函数字面量(function literal),我们可绕过这一限制,在任何表达式中表示一个函数值。函数字面量的语法和函数声明相似,区别在于func关键字后没有函数名。函数值字面量是一种表达式,它的值被称为匿名函数(anonymous function)。

+

函数字面量允许我们在使用函数时,再定义它。通过这种技巧,我们可以改写之前对strings.Map的调用:

+
strings.Map(func(r rune) rune { return r + 1 }, "HAL-9000")
+
+

更为重要的是,通过这种方式定义的函数可以访问完整的词法环境(lexical environment),这意味着在函数中定义的内部函数可以引用该函数的变量,如下例所示:

+

gopl.io/ch5/squares

+
// squares返回一个匿名函数。
+// 该匿名函数每次被调用时都会返回下一个数的平方。
+func squares() func() int {
+	var x int
+	return func() int {
+		x++
+		return x * x
+	}
+}
+func main() {
+	f := squares()
+	fmt.Println(f()) // "1"
+	fmt.Println(f()) // "4"
+	fmt.Println(f()) // "9"
+	fmt.Println(f()) // "16"
+}
+
+

函数squares返回另一个类型为 func() int 的函数。对squares的一次调用会生成一个局部变量x并返回一个匿名函数。每次调用匿名函数时,该函数都会先使x的值加1,再返回x的平方。第二次调用squares时,会生成第二个x变量,并返回一个新的匿名函数。新匿名函数操作的是第二个x变量。

+

squares的例子证明,函数值不仅仅是一串代码,还记录了状态。在squares中定义的匿名内部函数可以访问和更新squares中的局部变量,这意味着匿名函数和squares中,存在变量引用。这就是函数值属于引用类型和函数值不可比较的原因。Go使用闭包(closures)技术实现函数值,Go程序员也把函数值叫做闭包。

+

通过这个例子,我们看到变量的生命周期不由它的作用域决定:squares返回后,变量x仍然隐式的存在于f中。

+

接下来,我们讨论一个有点学术性的例子,考虑这样一个问题:给定一些计算机课程,每个课程都有前置课程,只有完成了前置课程才可以开始当前课程的学习;我们的目标是选择出一组课程,这组课程必须确保按顺序学习时,能全部被完成。每个课程的前置课程如下:

+

gopl.io/ch5/toposort

+
// prereqs记录了每个课程的前置课程
+var prereqs = map[string][]string{
+	"algorithms": {"data structures"},
+	"calculus": {"linear algebra"},
+	"compilers": {
+		"data structures",
+		"formal languages",
+		"computer organization",
+	},
+	"data structures":       {"discrete math"},
+	"databases":             {"data structures"},
+	"discrete math":         {"intro to programming"},
+	"formal languages":      {"discrete math"},
+	"networks":              {"operating systems"},
+	"operating systems":     {"data structures", "computer organization"},
+	"programming languages": {"data structures", "computer organization"},
+}
+
+

这类问题被称作拓扑排序。从概念上说,前置条件可以构成有向图。图中的顶点表示课程,边表示课程间的依赖关系。显然,图中应该无环,这也就是说从某点出发的边,最终不会回到该点。下面的代码用深度优先搜索了整张图,获得了符合要求的课程序列。

+
func main() {
+	for i, course := range topoSort(prereqs) {
+		fmt.Printf("%d:\t%s\n", i+1, course)
+	}
+}
+
+func topoSort(m map[string][]string) []string {
+	var order []string
+	seen := make(map[string]bool)
+	var visitAll func(items []string)
+	visitAll = func(items []string) {
+		for _, item := range items {
+			if !seen[item] {
+				seen[item] = true
+				visitAll(m[item])
+				order = append(order, item)
+			}
+		}
+	}
+	var keys []string
+	for key := range m {
+		keys = append(keys, key)
+	}
+	sort.Strings(keys)
+	visitAll(keys)
+	return order
+}
+
+

当匿名函数需要被递归调用时,我们必须首先声明一个变量(在上面的例子中,我们首先声明了 visitAll),再将匿名函数赋值给这个变量。如果不分成两步,函数字面量无法与visitAll绑定,我们也无法递归调用该匿名函数。

+
visitAll := func(items []string) {
+	// ...
+	visitAll(m[item]) // compile error: undefined: visitAll
+	// ...
+}
+
+

在toposort程序的输出如下所示,它的输出顺序是大多人想看到的固定顺序输出,但是这需要我们多花点心思才能做到。哈希表prepreqs的value是遍历顺序固定的切片,而不再试遍历顺序随机的map,所以我们对prereqs的key值进行排序,保证每次运行toposort程序,都以相同的遍历顺序遍历prereqs。

+
1: intro to programming
+2: discrete math
+3: data structures
+4: algorithms
+5: linear algebra
+6: calculus
+7: formal languages
+8: computer organization
+9: compilers
+10: databases
+11: operating systems
+12: networks
+13: programming languages
+
+

让我们回到findLinks这个例子。我们将代码移动到了links包下,将函数重命名为Extract,在第八章我们会再次用到这个函数。新的匿名函数被引入,用于替换原来的visit函数。该匿名函数负责将新连接添加到切片中。在Extract中,使用forEachNode遍历HTML页面,由于Extract只需要在遍历结点前操作结点,所以forEachNode的post参数被传入nil。

+

gopl.io/ch5/links

+
// Package links provides a link-extraction function.
+package links
+import (
+	"fmt"
+	"net/http"
+	"golang.org/x/net/html"
+)
+// Extract makes an HTTP GET request to the specified URL, parses
+// the response as HTML, and returns the links in the HTML document.
+func Extract(url string) ([]string, error) {
+	resp, err := http.Get(url)
+	if err != nil {
+		return nil, err
+	}
+	if resp.StatusCode != http.StatusOK {
+	resp.Body.Close()
+		return nil, fmt.Errorf("getting %s: %s", url, resp.Status)
+	}
+	doc, err := html.Parse(resp.Body)
+	resp.Body.Close()
+	if err != nil {
+		return nil, fmt.Errorf("parsing %s as HTML: %v", url, err)
+	}
+	var links []string
+	visitNode := func(n *html.Node) {
+		if n.Type == html.ElementNode && n.Data == "a" {
+			for _, a := range n.Attr {
+				if a.Key != "href" {
+					continue
+				}
+				link, err := resp.Request.URL.Parse(a.Val)
+				if err != nil {
+					continue // ignore bad URLs
+				}
+				links = append(links, link.String())
+			}
+		}
+	}
+	forEachNode(doc, visitNode, nil)
+	return links, nil
+}
+
+

上面的代码对之前的版本做了改进,现在links中存储的不是href属性的原始值,而是通过resp.Request.URL解析后的值。解析后,这些连接以绝对路径的形式存在,可以直接被http.Get访问。

+

网页抓取的核心问题就是如何遍历图。在topoSort的例子中,已经展示了深度优先遍历,在网页抓取中,我们会展示如何用广度优先遍历图。在第8章,我们会介绍如何将深度优先和广度优先结合使用。

+

下面的函数实现了广度优先算法。调用者需要输入一个初始的待访问列表和一个函数f。待访问列表中的每个元素被定义为string类型。广度优先算法会为每个元素调用一次f。每次f执行完毕后,会返回一组待访问元素。这些元素会被加入到待访问列表中。当待访问列表中的所有元素都被访问后,breadthFirst函数运行结束。为了避免同一个元素被访问两次,代码中维护了一个map。

+

gopl.io/ch5/findlinks3

+
// breadthFirst calls f for each item in the worklist.
+// Any items returned by f are added to the worklist.
+// f is called at most once for each item.
+func breadthFirst(f func(item string) []string, worklist []string) {
+	seen := make(map[string]bool)
+	for len(worklist) > 0 {
+		items := worklist
+		worklist = nil
+		for _, item := range items {
+			if !seen[item] {
+				seen[item] = true
+				worklist = append(worklist, f(item)...)
+			}
+		}
+	}
+}
+
+

就像我们在章节3解释的那样,append的参数“f(item)...”,会将f返回的一组元素一个个添加到worklist中。

+

在我们网页抓取器中,元素的类型是url。crawl函数会将URL输出,提取其中的新链接,并将这些新链接返回。我们会将crawl作为参数传递给breadthFirst。

+
func crawl(url string) []string {
+	fmt.Println(url)
+	list, err := links.Extract(url)
+	if err != nil {
+		log.Print(err)
+	}
+	return list
+}
+
+

为了使抓取器开始运行,我们用命令行输入的参数作为初始的待访问url。

+
func main() {
+	// Crawl the web breadth-first,
+	// starting from the command-line arguments.
+	breadthFirst(crawl, os.Args[1:])
+}
+
+

让我们从 https://golang.org 开始,下面是程序的输出结果:

+
$ go build gopl.io/ch5/findlinks3
+$ ./findlinks3 https://golang.org
+https://golang.org/
+https://golang.org/doc/
+https://golang.org/pkg/
+https://golang.org/project/
+https://code.google.com/p/go-tour/
+https://golang.org/doc/code.html
+https://www.youtube.com/watch?v=XCsL89YtqCs
+http://research.swtch.com/gotour
+
+

当所有发现的链接都已经被访问或电脑的内存耗尽时,程序运行结束。

+

练习5.10: 重写topoSort函数,用map代替切片并移除对key的排序代码。验证结果的正确性(结果不唯一)。

+

练习5.11: 现在线性代数的老师把微积分设为了前置课程。完善topSort,使其能检测有向图中的环。

+

练习5.12: gopl.io/ch5/outline2(5.5节)的startElement和endElement共用了全局变量depth,将它们修改为匿名函数,使其共享outline中的局部变量。

+

练习5.13: 修改crawl,使其能保存发现的页面,必要时,可以创建目录来保存这些页面。只保存来自原始域名下的页面。假设初始页面在golang.org下,就不要保存vimeo.com下的页面。

+

练习5.14: 使用breadthFirst遍历其他数据结构。比如,topoSort例子中的课程依赖关系(有向图)、个人计算机的文件层次结构(树);你所在城市的公交或地铁线路(无向图)。

+

5.6.1. 警告:捕获迭代变量

+

本节,将介绍Go词法作用域的一个陷阱。请务必仔细的阅读,弄清楚发生问题的原因。即使是经验丰富的程序员也会在这个问题上犯错误。

+

考虑这样一个问题:你被要求首先创建一些目录,再将目录删除。在下面的例子中我们用函数值来完成删除操作。下面的示例代码需要引入os包。为了使代码简单,我们忽略了所有的异常处理。

+
var rmdirs []func()
+for _, d := range tempDirs() {
+	dir := d // NOTE: necessary!
+	os.MkdirAll(dir, 0755) // creates parent directories too
+	rmdirs = append(rmdirs, func() {
+		os.RemoveAll(dir)
+	})
+}
+// ...do some work…
+for _, rmdir := range rmdirs {
+	rmdir() // clean up
+}
+
+

你可能会感到困惑,为什么要在循环体中用循环变量d赋值一个新的局部变量,而不是像下面的代码一样直接使用循环变量dir。需要注意,下面的代码是错误的。

+
var rmdirs []func()
+for _, dir := range tempDirs() {
+	os.MkdirAll(dir, 0755)
+	rmdirs = append(rmdirs, func() {
+		os.RemoveAll(dir) // NOTE: incorrect!
+	})
+}
+
+

问题的原因在于循环变量的作用域。在上面的程序中,for循环语句引入了新的词法块,循环变量dir在这个词法块中被声明。在该循环中生成的所有函数值都共享相同的循环变量。需要注意,函数值中记录的是循环变量的内存地址,而不是循环变量某一时刻的值。以dir为例,后续的迭代会不断更新dir的值,当删除操作执行时,for循环已完成,dir中存储的值等于最后一次迭代的值。这意味着,每次对os.RemoveAll的调用删除的都是相同的目录。

+

通常,为了解决这个问题,我们会引入一个与循环变量同名的局部变量,作为循环变量的副本。比如下面的变量dir,虽然这看起来很奇怪,但却很有用。

+
for _, dir := range tempDirs() {
+	dir := dir // declares inner dir, initialized to outer dir
+	// ...
+}
+
+

这个问题不仅存在基于range的循环,在下面的例子中,对循环变量i的使用也存在同样的问题:

+
var rmdirs []func()
+dirs := tempDirs()
+for i := 0; i < len(dirs); i++ {
+	os.MkdirAll(dirs[i], 0755) // OK
+	rmdirs = append(rmdirs, func() {
+		os.RemoveAll(dirs[i]) // NOTE: incorrect!
+	})
+}
+
+

如果你使用go语句(第八章)或者defer语句(5.8节)会经常遇到此类问题。这不是go或defer本身导致的,而是因为它们都会等待循环结束后,再执行函数值。

+

5.7. 可变参数

+

参数数量可变的函数称为可变参数函数。典型的例子就是fmt.Printf和类似函数。Printf首先接收一个必备的参数,之后接收任意个数的后续参数。

+

在声明可变参数函数时,需要在参数列表的最后一个参数类型之前加上省略符号“...”,这表示该函数会接收任意数量的该类型参数。

+

gopl.io/ch5/sum

+
func sum(vals ...int) int {
+	total := 0
+	for _, val := range vals {
+		total += val
+	}
+	return total
+}
+
+

sum函数返回任意个int型参数的和。在函数体中,vals被看作是类型为[] int的切片。sum可以接收任意数量的int型参数:

+
fmt.Println(sum())           // "0"
+fmt.Println(sum(3))          // "3"
+fmt.Println(sum(1, 2, 3, 4)) // "10"
+
+

在上面的代码中,调用者隐式的创建一个数组,并将原始参数复制到数组中,再把数组的一个切片作为参数传给被调用函数。如果原始参数已经是切片类型,我们该如何传递给sum?只需在最后一个参数后加上省略符。下面的代码功能与上个例子中最后一条语句相同。

+
values := []int{1, 2, 3, 4}
+fmt.Println(sum(values...)) // "10"
+
+

虽然在可变参数函数内部,...int 型参数的行为看起来很像切片类型,但实际上,可变参数函数和以切片作为参数的函数是不同的。

+
func f(...int) {}
+func g([]int) {}
+fmt.Printf("%T\n", f) // "func(...int)"
+fmt.Printf("%T\n", g) // "func([]int)"
+
+

可变参数函数经常被用于格式化字符串。下面的errorf函数构造了一个以行号开头的,经过格式化的错误信息。函数名的后缀f是一种通用的命名规范,代表该可变参数函数可以接收Printf风格的格式化字符串。

+
func errorf(linenum int, format string, args ...interface{}) {
+	fmt.Fprintf(os.Stderr, "Line %d: ", linenum)
+	fmt.Fprintf(os.Stderr, format, args...)
+	fmt.Fprintln(os.Stderr)
+}
+linenum, name := 12, "count"
+errorf(linenum, "undefined: %s", name) // "Line 12: undefined: count"
+
+

interface{}表示函数的最后一个参数可以接收任意类型,我们会在第7章详细介绍。

+

练习5.15: 编写类似sum的可变参数函数max和min。考虑不传参时,max和min该如何处理,再编写至少接收1个参数的版本。

+

**练习5.16:**编写多参数版本的strings.Join。

+

**练习5.17:**编写多参数版本的ElementsByTagName,函数接收一个HTML结点树以及任意数量的标签名,返回与这些标签名匹配的所有元素。下面给出了2个例子:

+
func ElementsByTagName(doc *html.Node, name...string) []*html.Node
+images := ElementsByTagName(doc, "img")
+headings := ElementsByTagName(doc, "h1", "h2", "h3", "h4")
+
+

5.8. Deferred函数

+

在findLinks的例子中,我们用http.Get的输出作为html.Parse的输入。只有url的内容的确是HTML格式的,html.Parse才可以正常工作,但实际上,url指向的内容很丰富,可能是图片,纯文本或是其他。将这些格式的内容传递给html.parse,会产生不良后果。

+

下面的例子获取HTML页面并输出页面的标题。title函数会检查服务器返回的Content-Type字段,如果发现页面不是HTML,将终止函数运行,返回错误。

+

gopl.io/ch5/title1

+
func title(url string) error {
+	resp, err := http.Get(url)
+	if err != nil {
+		return err
+	}
+	// Check Content-Type is HTML (e.g., "text/html;charset=utf-8").
+	ct := resp.Header.Get("Content-Type")
+	if ct != "text/html" && !strings.HasPrefix(ct,"text/html;") {
+		resp.Body.Close()
+		return fmt.Errorf("%s has type %s, not text/html",url, ct)
+	}
+	doc, err := html.Parse(resp.Body)
+	resp.Body.Close()
+	if err != nil {
+		return fmt.Errorf("parsing %s as HTML: %v", url,err)
+	}
+	visitNode := func(n *html.Node) {
+		if n.Type == html.ElementNode && n.Data == "title"&&n.FirstChild != nil {
+			fmt.Println(n.FirstChild.Data)
+		}
+	}
+	forEachNode(doc, visitNode, nil)
+	return nil
+}
+
+

下面展示了运行效果:

+
$ go build gopl.io/ch5/title1
+$ ./title1 http://gopl.io
+The Go Programming Language
+$ ./title1 https://golang.org/doc/effective_go.html
+Effective Go - The Go Programming Language
+$ ./title1 https://golang.org/doc/gopher/frontpage.png
+title1: https://golang.org/doc/gopher/frontpage.png has type image/png, not text/html
+
+

resp.Body.close调用了多次,这是为了确保title在所有执行路径下(即使函数运行失败)都关闭了网络连接。随着函数变得复杂,需要处理的错误也变多,维护清理逻辑变得越来越困难。而Go语言独有的defer机制可以让事情变得简单。

+

你只需要在调用普通函数或方法前加上关键字defer,就完成了defer所需要的语法。当执行到该条语句时,函数和参数表达式得到计算,但直到包含该defer语句的函数执行完毕时,defer后的函数才会被执行,不论包含defer语句的函数是通过return正常结束,还是由于panic导致的异常结束。你可以在一个函数中执行多条defer语句,它们的执行顺序与声明顺序相反。

+

defer语句经常被用于处理成对的操作,如打开、关闭、连接、断开连接、加锁、释放锁。通过defer机制,不论函数逻辑多复杂,都能保证在任何执行路径下,资源被释放。释放资源的defer应该直接跟在请求资源的语句后。在下面的代码中,一条defer语句替代了之前的所有resp.Body.Close

+

gopl.io/ch5/title2

+
func title(url string) error {
+	resp, err := http.Get(url)
+	if err != nil {
+		return err
+	}
+	defer resp.Body.Close()
+	ct := resp.Header.Get("Content-Type")
+	if ct != "text/html" && !strings.HasPrefix(ct,"text/html;") {
+		return fmt.Errorf("%s has type %s, not text/html",url, ct)
+	}
+	doc, err := html.Parse(resp.Body)
+	if err != nil {
+		return fmt.Errorf("parsing %s as HTML: %v", url,err)
+	}
+	// ...print doc's title element…
+	return nil
+}
+
+

在处理其他资源时,也可以采用defer机制,比如对文件的操作:

+

io/ioutil

+
package ioutil
+func ReadFile(filename string) ([]byte, error) {
+	f, err := os.Open(filename)
+	if err != nil {
+		return nil, err
+	}
+	defer f.Close()
+	return ReadAll(f)
+}
+
+

或是处理互斥锁(9.2章)

+
var mu sync.Mutex
+var m = make(map[string]int)
+func lookup(key string) int {
+	mu.Lock()
+	defer mu.Unlock()
+	return m[key]
+}
+
+

调试复杂程序时,defer机制也常被用于记录何时进入和退出函数。下例中的bigSlowOperation函数,直接调用trace记录函数的被调情况。bigSlowOperation被调时,trace会返回一个函数值,该函数值会在bigSlowOperation退出时被调用。通过这种方式, 我们可以只通过一条语句控制函数的入口和所有的出口,甚至可以记录函数的运行时间,如例子中的start。需要注意一点:不要忘记defer语句后的圆括号,否则本该在进入时执行的操作会在退出时执行,而本该在退出时执行的,永远不会被执行。

+

gopl.io/ch5/trace

+
func bigSlowOperation() {
+	defer trace("bigSlowOperation")() // don't forget the extra parentheses
+	// ...lots of work…
+	time.Sleep(10 * time.Second) // simulate slow operation by sleeping
+}
+func trace(msg string) func() {
+	start := time.Now()
+	log.Printf("enter %s", msg)
+	return func() { 
+		log.Printf("exit %s (%s)", msg,time.Since(start)) 
+	}
+}
+
+

每一次bigSlowOperation被调用,程序都会记录函数的进入,退出,持续时间。(我们用time.Sleep模拟一个耗时的操作)

+
$ go build gopl.io/ch5/trace
+$ ./trace
+2015/11/18 09:53:26 enter bigSlowOperation
+2015/11/18 09:53:36 exit bigSlowOperation (10.000589217s)
+
+

我们知道,defer语句中的函数会在return语句更新返回值变量后再执行,又因为在函数中定义的匿名函数可以访问该函数包括返回值变量在内的所有变量,所以,对匿名函数采用defer机制,可以使其观察函数的返回值。

+

以double函数为例:

+
func double(x int) int {
+	return x + x
+}
+
+

我们只需要首先命名double的返回值,再增加defer语句,我们就可以在double每次被调用时,输出参数以及返回值。

+
func double(x int) (result int) {
+	defer func() { fmt.Printf("double(%d) = %d\n", x,result) }()
+	return x + x
+}
+_ = double(4)
+// Output:
+// "double(4) = 8"
+
+

可能double函数过于简单,看不出这个小技巧的作用,但对于有许多return语句的函数而言,这个技巧很有用。

+

被延迟执行的匿名函数甚至可以修改函数返回给调用者的返回值:

+
func triple(x int) (result int) {
+	defer func() { result += x }()
+	return double(x)
+}
+fmt.Println(triple(4)) // "12"
+
+

在循环体中的defer语句需要特别注意,因为只有在函数执行完毕后,这些被延迟的函数才会执行。下面的代码会导致系统的文件描述符耗尽,因为在所有文件都被处理之前,没有文件会被关闭。

+
for _, filename := range filenames {
+	f, err := os.Open(filename)
+	if err != nil {
+		return err
+	}
+	defer f.Close() // NOTE: risky; could run out of file descriptors
+	// ...process f…
+}
+
+

一种解决方法是将循环体中的defer语句移至另外一个函数。在每次循环时,调用这个函数。

+
for _, filename := range filenames {
+	if err := doFile(filename); err != nil {
+		return err
+	}
+}
+func doFile(filename string) error {
+	f, err := os.Open(filename)
+	if err != nil {
+		return err
+	}
+	defer f.Close()
+	// ...process f…
+}
+
+

下面的代码是fetch(1.5节)的改进版,我们将http响应信息写入本地文件而不是从标准输出流输出。我们通过path.Base提出url路径的最后一段作为文件名。

+

gopl.io/ch5/fetch

+
// Fetch downloads the URL and returns the
+// name and length of the local file.
+func fetch(url string) (filename string, n int64, err error) {
+	resp, err := http.Get(url)
+	if err != nil {
+		return "", 0, err
+	}
+	defer resp.Body.Close()
+	local := path.Base(resp.Request.URL.Path)
+	if local == "/" {
+		local = "index.html"
+	}
+	f, err := os.Create(local)
+	if err != nil {
+		return "", 0, err
+	}
+	n, err = io.Copy(f, resp.Body)
+	// Close file, but prefer error from Copy, if any.
+	if closeErr := f.Close(); err == nil {
+		err = closeErr
+	}
+	return local, n, err
+}
+
+

对resp.Body.Close延迟调用我们已经见过了,在此不做解释。上例中,通过os.Create打开文件进行写入,在关闭文件时,我们没有对f.close采用defer机制,因为这会产生一些微妙的错误。许多文件系统,尤其是NFS,写入文件时发生的错误会被延迟到文件关闭时反馈。如果没有检查文件关闭时的反馈信息,可能会导致数据丢失,而我们还误以为写入操作成功。如果io.Copy和f.close都失败了,我们倾向于将io.Copy的错误信息反馈给调用者,因为它先于f.close发生,更有可能接近问题的本质。

+

**练习5.18:**不修改fetch的行为,重写fetch函数,要求使用defer机制关闭文件。

+

5.9. Panic异常

+

Go的类型系统会在编译时捕获很多错误,但有些错误只能在运行时检查,如数组访问越界、空指针引用等。这些运行时错误会引起panic异常。

+

一般而言,当panic异常发生时,程序会中断运行,并立即执行在该goroutine(可以先理解成线程,在第8章会详细介绍)中被延迟的函数(defer 机制)。随后,程序崩溃并输出日志信息。日志信息包括panic value和函数调用的堆栈跟踪信息。panic value通常是某种错误信息。对于每个goroutine,日志信息中都会有与之相对的,发生panic时的函数调用堆栈跟踪信息。通常,我们不需要再次运行程序去定位问题,日志信息已经提供了足够的诊断依据。因此,在我们填写问题报告时,一般会将panic异常和日志信息一并记录。

+

不是所有的panic异常都来自运行时,直接调用内置的panic函数也会引发panic异常;panic函数接受任何值作为参数。当某些不应该发生的场景发生时,我们就应该调用panic。比如,当程序到达了某条逻辑上不可能到达的路径:

+
switch s := suit(drawCard()); s {
+case "Spades":                                // ...
+case "Hearts":                                // ...
+case "Diamonds":                              // ...
+case "Clubs":                                 // ...
+default:
+	panic(fmt.Sprintf("invalid suit %q", s)) // Joker?
+}
+
+

断言函数必须满足的前置条件是明智的做法,但这很容易被滥用。除非你能提供更多的错误信息,或者能更快速的发现错误,否则不需要使用断言,编译器在运行时会帮你检查代码。

+
func Reset(x *Buffer) {
+	if x == nil {
+		panic("x is nil") // unnecessary!
+	}
+	x.elements = nil
+}
+
+

虽然Go的panic机制类似于其他语言的异常,但panic的适用场景有一些不同。由于panic会引起程序的崩溃,因此panic一般用于严重错误,如程序内部的逻辑不一致。勤奋的程序员认为任何崩溃都表明代码中存在漏洞,所以对于大部分漏洞,我们应该使用Go提供的错误机制,而不是panic,尽量避免程序的崩溃。在健壮的程序中,任何可以预料到的错误,如不正确的输入、错误的配置或是失败的I/O操作都应该被优雅的处理,最好的处理方式,就是使用Go的错误机制。

+

考虑regexp.Compile函数,该函数将正则表达式编译成有效的可匹配格式。当输入的正则表达式不合法时,该函数会返回一个错误。当调用者明确的知道正确的输入不会引起函数错误时,要求调用者检查这个错误是不必要和累赘的。我们应该假设函数的输入一直合法,就如前面的断言一样:当调用者输入了不应该出现的输入时,触发panic异常。

+

在程序源码中,大多数正则表达式是字符串字面值(string literals),因此regexp包提供了包装函数regexp.MustCompile检查输入的合法性。

+
package regexp
+func Compile(expr string) (*Regexp, error) { /* ... */ }
+func MustCompile(expr string) *Regexp {
+	re, err := Compile(expr)
+	if err != nil {
+		panic(err)
+	}
+	return re
+}
+
+

包装函数使得调用者可以便捷的用一个编译后的正则表达式为包级别的变量赋值:

+
var httpSchemeRE = regexp.MustCompile(`^https?:`) //"http:" or "https:"
+
+

显然,MustCompile不能接收不合法的输入。函数名中的Must前缀是一种针对此类函数的命名约定,比如template.Must(4.6节)

+
func main() {
+	f(3)
+}
+func f(x int) {
+	fmt.Printf("f(%d)\n", x+0/x) // panics if x == 0
+	defer fmt.Printf("defer %d\n", x)
+	f(x - 1)
+}
+
+

上例中的运行输出如下:

+
f(3)
+f(2)
+f(1)
+defer 1
+defer 2
+defer 3
+
+

当f(0)被调用时,发生panic异常,之前被延迟执行的3个fmt.Printf被调用。程序中断执行后,panic信息和堆栈信息会被输出(下面是简化的输出):

+
panic: runtime error: integer divide by zero
+main.f(0)
+src/gopl.io/ch5/defer1/defer.go:14
+main.f(1)
+src/gopl.io/ch5/defer1/defer.go:16
+main.f(2)
+src/gopl.io/ch5/defer1/defer.go:16
+main.f(3)
+src/gopl.io/ch5/defer1/defer.go:16
+main.main()
+src/gopl.io/ch5/defer1/defer.go:10
+
+

我们在下一节将看到,如何使程序从panic异常中恢复,阻止程序的崩溃。

+

为了方便诊断问题,runtime包允许程序员输出堆栈信息。在下面的例子中,我们通过在main函数中延迟调用printStack输出堆栈信息。

+

gopl.io/ch5/defer2

+
func main() {
+	defer printStack()
+	f(3)
+}
+func printStack() {
+	var buf [4096]byte
+	n := runtime.Stack(buf[:], false)
+	os.Stdout.Write(buf[:n])
+}
+
+

printStack的简化输出如下(下面只是printStack的输出,不包括panic的日志信息):

+
goroutine 1 [running]:
+main.printStack()
+src/gopl.io/ch5/defer2/defer.go:20
+main.f(0)
+src/gopl.io/ch5/defer2/defer.go:27
+main.f(1)
+src/gopl.io/ch5/defer2/defer.go:29
+main.f(2)
+src/gopl.io/ch5/defer2/defer.go:29
+main.f(3)
+src/gopl.io/ch5/defer2/defer.go:29
+main.main()
+src/gopl.io/ch5/defer2/defer.go:15
+
+

将panic机制类比其他语言异常机制的读者可能会惊讶,runtime.Stack为何能输出已经被释放函数的信息?在Go的panic机制中,延迟函数的调用在释放堆栈信息之前。

+

5.10. Recover捕获异常

+

通常来说,不应该对panic异常做任何处理,但有时,也许我们可以从异常中恢复,至少我们可以在程序崩溃前,做一些操作。举个例子,当web服务器遇到不可预料的严重问题时,在崩溃前应该将所有的连接关闭;如果不做任何处理,会使得客户端一直处于等待状态。如果web服务器还在开发阶段,服务器甚至可以将异常信息反馈到客户端,帮助调试。

+

如果在deferred函数中调用了内置函数recover,并且定义该defer语句的函数发生了panic异常,recover会使程序从panic中恢复,并返回panic value。导致panic异常的函数不会继续运行,但能正常返回。在未发生panic时调用recover,recover会返回nil。

+

让我们以语言解析器为例,说明recover的使用场景。考虑到语言解析器的复杂性,即使某个语言解析器目前工作正常,也无法肯定它没有漏洞。因此,当某个异常出现时,我们不会选择让解析器崩溃,而是会将panic异常当作普通的解析错误,并附加额外信息提醒用户报告此错误。

+
func Parse(input string) (s *Syntax, err error) {
+	defer func() {
+		if p := recover(); p != nil {
+			err = fmt.Errorf("internal error: %v", p)
+		}
+	}()
+	// ...parser...
+}
+
+

deferred函数帮助Parse从panic中恢复。在deferred函数内部,panic value被附加到错误信息中;并用err变量接收错误信息,返回给调用者。我们也可以通过调用runtime.Stack往错误信息中添加完整的堆栈调用信息。

+

不加区分的恢复所有的panic异常,不是可取的做法;因为在panic之后,无法保证包级变量的状态仍然和我们预期一致。比如,对数据结构的一次重要更新没有被完整完成、文件或者网络连接没有被关闭、获得的锁没有被释放。此外,如果写日志时产生的panic被不加区分的恢复,可能会导致漏洞被忽略。

+

虽然把对panic的处理都集中在一个包下,有助于简化对复杂和不可以预料问题的处理,但作为被广泛遵守的规范,你不应该试图去恢复其他包引起的panic。公有的API应该将函数的运行失败作为error返回,而不是panic。同样的,你也不应该恢复一个由他人开发的函数引起的panic,比如说调用者传入的回调函数,因为你无法确保这样做是安全的。

+

有时我们很难完全遵循规范,举个例子,net/http包中提供了一个web服务器,将收到的请求分发给用户提供的处理函数。很显然,我们不能因为某个处理函数引发的panic异常,杀掉整个进程;web服务器遇到处理函数导致的panic时会调用recover,输出堆栈信息,继续运行。这样的做法在实践中很便捷,但也会引起资源泄漏,或是因为recover操作,导致其他问题。

+

基于以上原因,安全的做法是有选择性的recover。换句话说,只恢复应该被恢复的panic异常,此外,这些异常所占的比例应该尽可能的低。为了标识某个panic是否应该被恢复,我们可以将panic value设置成特殊类型。在recover时对panic value进行检查,如果发现panic value是特殊类型,就将这个panic作为error处理,如果不是,则按照正常的panic进行处理(在下面的例子中,我们会看到这种方式)。

+

下面的例子是title函数的变形,如果HTML页面包含多个<title>,该函数会给调用者返回一个错误(error)。在soleTitle内部处理时,如果检测到有多个<title>,会调用panic,阻止函数继续递归,并将特殊类型bailout作为panic的参数。

+
// soleTitle returns the text of the first non-empty title element
+// in doc, and an error if there was not exactly one.
+func soleTitle(doc *html.Node) (title string, err error) {
+	type bailout struct{}
+	defer func() {
+		switch p := recover(); p {
+		case nil:       // no panic
+		case bailout{}: // "expected" panic
+			err = fmt.Errorf("multiple title elements")
+		default:
+			panic(p) // unexpected panic; carry on panicking
+		}
+	}()
+	// Bail out of recursion if we find more than one nonempty title.
+	forEachNode(doc, func(n *html.Node) {
+		if n.Type == html.ElementNode && n.Data == "title" &&
+			n.FirstChild != nil {
+			if title != "" {
+				panic(bailout{}) // multiple titleelements
+			}
+			title = n.FirstChild.Data
+		}
+	}, nil)
+	if title == "" {
+		return "", fmt.Errorf("no title element")
+	}
+	return title, nil
+}
+
+

在上例中,deferred函数调用recover,并检查panic value。当panic value是bailout{}类型时,deferred函数生成一个error返回给调用者。当panic value是其他non-nil值时,表示发生了未知的panic异常,deferred函数将调用panic函数并将当前的panic value作为参数传入;此时,等同于recover没有做任何操作。(请注意:在例子中,对可预期的错误采用了panic,这违反了之前的建议,我们在此只是想向读者演示这种机制。)

+

有些情况下,我们无法恢复。某些致命错误会导致Go在运行时终止程序,如内存不足。

+

练习5.19: 使用panic和recover编写一个不包含return语句但能返回一个非零值的函数。

+

第6章 方法

+

从90年代早期开始,面向对象编程(OOP)就成为了称霸工程界和教育界的编程范式,所以之后几乎所有大规模被应用的语言都包含了对OOP的支持,go语言也不例外。

+

尽管没有被大众所接受的明确的OOP的定义,从我们的理解来讲,一个对象其实也就是一个简单的值或者一个变量,在这个对象中会包含一些方法,而一个方法则是一个一个和特殊类型关联的函数。一个面向对象的程序会用方法来表达其属性和对应的操作,这样使用这个对象的用户就不需要直接去操作对象,而是借助方法来做这些事情。

+

在早些的章节中,我们已经使用了标准库提供的一些方法,比如time.Duration这个类型的Seconds方法:

+
const day = 24 * time.Hour
+fmt.Println(day.Seconds()) // "86400"
+
+

并且在2.5节中,我们定义了一个自己的方法,Celsius类型的String方法:

+
func (c Celsius) String() string { return fmt.Sprintf("%g°C", c) }
+
+

在本章中,OOP编程的第一方面,我们会向你展示如何有效地定义和使用方法。我们会覆盖到OOP编程的两个关键点,封装和组合。

+

6.1. 方法声明

+

在函数声明时,在其名字之前放上一个变量,即是一个方法。这个附加的参数会将该函数附加到这种类型上,即相当于为这种类型定义了一个独占的方法。

+

下面来写我们第一个方法的例子,这个例子在package geometry下:

+

gopl.io/ch6/geometry

+
package geometry
+
+import "math"
+
+type Point struct{ X, Y float64 }
+
+// traditional function
+func Distance(p, q Point) float64 {
+	return math.Hypot(q.X-p.X, q.Y-p.Y)
+}
+
+// same thing, but as a method of the Point type
+func (p Point) Distance(q Point) float64 {
+	return math.Hypot(q.X-p.X, q.Y-p.Y)
+}
+
+

上面的代码里那个附加的参数p,叫做方法的接收器(receiver),早期的面向对象语言留下的遗产将调用一个方法称为“向一个对象发送消息”。

+

在Go语言中,我们并不会像其它语言那样用this或者self作为接收器;我们可以任意的选择接收器的名字。由于接收器的名字经常会被使用到,所以保持其在方法间传递时的一致性和简短性是不错的主意。这里的建议是可以使用其类型的第一个字母,比如这里使用了Point的首字母p。

+

在方法调用过程中,接收器参数一般会在方法名之前出现。这和方法声明是一样的,都是接收器参数在方法名字之前。下面是例子:

+
p := Point{1, 2}
+q := Point{4, 6}
+fmt.Println(Distance(p, q)) // "5", function call
+fmt.Println(p.Distance(q))  // "5", method call
+
+

可以看到,上面的两个函数调用都是Distance,但是却没有发生冲突。第一个Distance的调用实际上用的是包级别的函数geometry.Distance,而第二个则是使用刚刚声明的Point,调用的是Point类下声明的Point.Distance方法。

+

这种p.Distance的表达式叫做选择器,因为他会选择合适的对应p这个对象的Distance方法来执行。选择器也会被用来选择一个struct类型的字段,比如p.X。由于方法和字段都是在同一命名空间,所以如果我们在这里声明一个X方法的话,编译器会报错,因为在调用p.X时会有歧义(译注:这里确实挺奇怪的)。

+

因为每种类型都有其方法的命名空间,我们在用Distance这个名字的时候,不同的Distance调用指向了不同类型里的Distance方法。让我们来定义一个Path类型,这个Path代表一个线段的集合,并且也给这个Path定义一个叫Distance的方法。

+
// A Path is a journey connecting the points with straight lines.
+type Path []Point
+// Distance returns the distance traveled along the path.
+func (path Path) Distance() float64 {
+	sum := 0.0
+	for i := range path {
+		if i > 0 {
+			sum += path[i-1].Distance(path[i])
+		}
+	}
+	return sum
+}
+
+

Path是一个命名的slice类型,而不是Point那样的struct类型,然而我们依然可以为它定义方法。在能够给任意类型定义方法这一点上,Go和很多其它的面向对象的语言不太一样。因此在Go语言里,我们为一些简单的数值、字符串、slice、map来定义一些附加行为很方便。我们可以给同一个包内的任意命名类型定义方法,只要这个命名类型的底层类型(译注:这个例子里,底层类型是指[]Point这个slice,Path就是命名类型)不是指针或者interface。

+

两个Distance方法有不同的类型。他们两个方法之间没有任何关系,尽管Path的Distance方法会在内部调用Point.Distance方法来计算每个连接邻接点的线段的长度。

+

让我们来调用一个新方法,计算三角形的周长:

+
perim := Path{
+	{1, 1},
+	{5, 1},
+	{5, 4},
+	{1, 1},
+}
+fmt.Println(perim.Distance()) // "12"
+
+

在上面两个对Distance名字的方法的调用中,编译器会根据方法的名字以及接收器来决定具体调用的是哪一个函数。第一个例子中path[i-1]数组中的类型是Point,因此Point.Distance这个方法被调用;在第二个例子中perim的类型是Path,因此Distance调用的是Path.Distance。

+

对于一个给定的类型,其内部的方法都必须有唯一的方法名,但是不同的类型却可以有同样的方法名,比如我们这里Point和Path就都有Distance这个名字的方法;所以我们没有必要非在方法名之前加类型名来消除歧义,比如PathDistance。这里我们已经看到了方法比之函数的一些好处:方法名可以简短。当我们在包外调用的时候这种好处就会被放大,因为我们可以使用这个短名字,而可以省略掉包的名字,下面是例子:

+
import "gopl.io/ch6/geometry"
+
+perim := geometry.Path{{1, 1}, {5, 1}, {5, 4}, {1, 1}}
+fmt.Println(geometry.PathDistance(perim)) // "12", standalone function
+fmt.Println(perim.Distance())             // "12", method of geometry.Path
+
+

译注: 如果我们要用方法去计算perim的distance,还需要去写全geometry的包名,和其函数名,但是因为Path这个类型定义了一个可以直接用的Distance方法,所以我们可以直接写perim.Distance()。相当于可以少打很多字,作者应该是这个意思。因为在Go里包外调用函数需要带上包名,还是挺麻烦的。

+

6.2. 基于指针对象的方法

+

当调用一个函数时,会对其每一个参数值进行拷贝,如果一个函数需要更新一个变量,或者函数的其中一个参数实在太大我们希望能够避免进行这种默认的拷贝,这种情况下我们就需要用到指针了。对应到我们这里用来更新接收器的对象的方法,当这个接受者变量本身比较大时,我们就可以用其指针而不是对象来声明方法,如下:

+
func (p *Point) ScaleBy(factor float64) {
+	p.X *= factor
+	p.Y *= factor
+}
+
+

这个方法的名字是(*Point).ScaleBy。这里的括号是必须的;没有括号的话这个表达式可能会被理解为*(Point.ScaleBy)

+

在现实的程序里,一般会约定如果Point这个类有一个指针作为接收器的方法,那么所有Point的方法都必须有一个指针接收器,即使是那些并不需要这个指针接收器的函数。我们在这里打破了这个约定只是为了展示一下两种方法的异同而已。

+

只有类型(Point)和指向他们的指针(*Point),才可能是出现在接收器声明里的两种接收器。此外,为了避免歧义,在声明方法时,如果一个类型名本身是一个指针的话,是不允许其出现在接收器中的,比如下面这个例子:

+
type P *int
+func (P) f() { /* ... */ } // compile error: invalid receiver type
+
+

想要调用指针类型方法(*Point).ScaleBy,只要提供一个Point类型的指针即可,像下面这样。

+
r := &Point{1, 2}
+r.ScaleBy(2)
+fmt.Println(*r) // "{2, 4}"
+
+

或者这样:

+
p := Point{1, 2}
+pptr := &p
+pptr.ScaleBy(2)
+fmt.Println(p) // "{2, 4}"
+
+

或者这样:

+
p := Point{1, 2}
+(&p).ScaleBy(2)
+fmt.Println(p) // "{2, 4}"
+
+

不过后面两种方法有些笨拙。幸运的是,go语言本身在这种地方会帮到我们。如果接收器p是一个Point类型的变量,并且其方法需要一个Point指针作为接收器,我们可以用下面这种简短的写法:

+
p.ScaleBy(2)
+
+

编译器会隐式地帮我们用&p去调用ScaleBy这个方法。这种简写方法只适用于“变量”,包括struct里的字段比如p.X,以及array和slice内的元素比如perim[0]。我们不能通过一个无法取到地址的接收器来调用指针方法,比如临时变量的内存地址就无法获取得到:

+
Point{1, 2}.ScaleBy(2) // compile error: can't take address of Point literal
+
+

但是我们可以用一个*Point这样的接收器来调用Point的方法,因为我们可以通过地址来找到这个变量,只要用解引用符号*来取到该变量即可。编译器在这里也会给我们隐式地插入*这个操作符,所以下面这两种写法等价的:

+
pptr.Distance(q)
+(*pptr).Distance(q)
+
+

这里的几个例子可能让你有些困惑,所以我们总结一下:在每一个合法的方法调用表达式中,也就是下面三种情况里的任意一种情况都是可以的:

+

要么接收器的实际参数和其形式参数是相同的类型,比如两者都是类型T或者都是类型*T

+
Point{1, 2}.Distance(q) //  Point
+pptr.ScaleBy(2)         // *Point
+
+

或者接收器实参是类型T,但接收器形参是类型*T,这种情况下编译器会隐式地为我们取变量的地址:

+
p.ScaleBy(2) // implicit (&p)
+
+

或者接收器实参是类型*T,形参是类型T。编译器会隐式地为我们解引用,取到指针指向的实际变量:

+
pptr.Distance(q) // implicit (*pptr)
+
+

如果命名类型T(译注:用type xxx定义的类型)的所有方法都是用T类型自己来做接收器(而不是*T),那么拷贝这种类型的实例就是安全的;调用他的任何一个方法也就会产生一个值的拷贝。比如time.Duration的这个类型,在调用其方法时就会被全部拷贝一份,包括在作为参数传入函数的时候。但是如果一个方法使用指针作为接收器,你需要避免对其进行拷贝,因为这样可能会破坏掉该类型内部的不变性。比如你对bytes.Buffer对象进行了拷贝,那么可能会引起原始对象和拷贝对象只是别名而已,实际上它们指向的对象是一样的。紧接着对拷贝后的变量进行修改可能会有让你有意外的结果。

+

译注: 作者这里说的比较绕,其实有两点:

+
    +
  1. 不管你的method的receiver是指针类型还是非指针类型,都是可以通过指针/非指针类型进行调用的,编译器会帮你做类型转换。
  2. +
  3. 在声明一个method的receiver该是指针还是非指针类型时,你需要考虑两方面的因素,第一方面是这个对象本身是不是特别大,如果声明为非指针变量时,调用会产生一次拷贝;第二方面是如果你用指针类型作为receiver,那么你一定要注意,这种指针类型指向的始终是一块内存地址,就算你对其进行了拷贝。熟悉C或者C++的人这里应该很快能明白。
  4. +
+

6.2.1. Nil也是一个合法的接收器类型

+

就像一些函数允许nil指针作为参数一样,方法理论上也可以用nil指针作为其接收器,尤其当nil对于对象来说是合法的零值时,比如map或者slice。在下面的简单int链表的例子里,nil代表的是空链表:

+
// An IntList is a linked list of integers.
+// A nil *IntList represents the empty list.
+type IntList struct {
+	Value int
+	Tail  *IntList
+}
+// Sum returns the sum of the list elements.
+func (list *IntList) Sum() int {
+	if list == nil {
+		return 0
+	}
+	return list.Value + list.Tail.Sum()
+}
+
+

当你定义一个允许nil作为接收器值的方法的类型时,在类型前面的注释中指出nil变量代表的意义是很有必要的,就像我们上面例子里做的这样。

+

下面是net/url包里Values类型定义的一部分。

+

net/url

+
package url
+
+// Values maps a string key to a list of values.
+type Values map[string][]string
+// Get returns the first value associated with the given key,
+// or "" if there are none.
+func (v Values) Get(key string) string {
+	if vs := v[key]; len(vs) > 0 {
+		return vs[0]
+	}
+	return ""
+}
+// Add adds the value to key.
+// It appends to any existing values associated with key.
+func (v Values) Add(key, value string) {
+	v[key] = append(v[key], value)
+}
+
+

这个定义向外部暴露了一个map的命名类型,并且提供了一些能够简单操作这个map的方法。这个map的value字段是一个string的slice,所以这个Values是一个多维map。客户端使用这个变量的时候可以使用map固有的一些操作(make,切片,m[key]等等),也可以使用这里提供的操作方法,或者两者并用,都是可以的:

+

gopl.io/ch6/urlvalues

+
m := url.Values{"lang": {"en"}} // direct construction
+m.Add("item", "1")
+m.Add("item", "2")
+
+fmt.Println(m.Get("lang")) // "en"
+fmt.Println(m.Get("q"))    // ""
+fmt.Println(m.Get("item")) // "1"      (first value)
+fmt.Println(m["item"])     // "[1 2]"  (direct map access)
+
+m = nil
+fmt.Println(m.Get("item")) // ""
+m.Add("item", "3")         // panic: assignment to entry in nil map
+
+

对Get的最后一次调用中,nil接收器的行为即是一个空map的行为。我们可以等价地将这个操作写成Value(nil).Get("item"),但是如果你直接写nil.Get("item")的话是无法通过编译的,因为nil的字面量编译器无法判断其准确类型。所以相比之下,最后的那行m.Add的调用就会产生一个panic,因为他尝试更新一个空map。

+

由于url.Values是一个map类型,并且间接引用了其key/value对,因此url.Values.Add对这个map里的元素做任何的更新、删除操作对调用方都是可见的。实际上,就像在普通函数中一样,虽然可以通过引用来操作内部值,但在方法想要修改引用本身时是不会影响原始值的,比如把他置换为nil,或者让这个引用指向了其它的对象,调用方都不会受影响。(译注:因为传入的是存储了内存地址的变量,你改变这个变量本身是影响不了原始的变量的,想想C语言,是差不多的)

+

6.3. 通过嵌入结构体来扩展类型

+

来看看ColoredPoint这个类型:

+

gopl.io/ch6/coloredpoint

+
import "image/color"
+
+type Point struct{ X, Y float64 }
+
+type ColoredPoint struct {
+	Point
+	Color color.RGBA
+}
+
+

我们完全可以将ColoredPoint定义为一个有三个字段的struct,但是我们却将Point这个类型嵌入到ColoredPoint来提供X和Y这两个字段。像我们在4.4节中看到的那样,内嵌可以使我们在定义ColoredPoint时得到一种句法上的简写形式,并使其包含Point类型所具有的一切字段,然后再定义一些自己的。如果我们想要的话,我们可以直接认为通过嵌入的字段就是ColoredPoint自身的字段,而完全不需要在调用时指出Point,比如下面这样。

+
var cp ColoredPoint
+cp.X = 1
+fmt.Println(cp.Point.X) // "1"
+cp.Point.Y = 2
+fmt.Println(cp.Y) // "2"
+
+

对于Point中的方法我们也有类似的用法,我们可以把ColoredPoint类型当作接收器来调用Point里的方法,即使ColoredPoint里没有声明这些方法:

+
red := color.RGBA{255, 0, 0, 255}
+blue := color.RGBA{0, 0, 255, 255}
+var p = ColoredPoint{Point{1, 1}, red}
+var q = ColoredPoint{Point{5, 4}, blue}
+fmt.Println(p.Distance(q.Point)) // "5"
+p.ScaleBy(2)
+q.ScaleBy(2)
+fmt.Println(p.Distance(q.Point)) // "10"
+
+

Point类的方法也被引入了ColoredPoint。用这种方式,内嵌可以使我们定义字段特别多的复杂类型,我们可以将字段先按小类型分组,然后定义小类型的方法,之后再把它们组合起来。

+

读者如果对基于类来实现面向对象的语言比较熟悉的话,可能会倾向于将Point看作一个基类,而ColoredPoint看作其子类或者继承类,或者将ColoredPoint看作"is a" Point类型。但这是错误的理解。请注意上面例子中对Distance方法的调用。Distance有一个参数是Point类型,但q并不是一个Point类,所以尽管q有着Point这个内嵌类型,我们也必须要显式地选择它。尝试直接传q的话你会看到下面这样的错误:

+
p.Distance(q) // compile error: cannot use q (ColoredPoint) as Point
+
+

一个ColoredPoint并不是一个Point,但他"has a"Point,并且它有从Point类里引入的Distance和ScaleBy方法。如果你喜欢从实现的角度来考虑问题,内嵌字段会指导编译器去生成额外的包装方法来委托已经声明好的方法,和下面的形式是等价的:

+
func (p ColoredPoint) Distance(q Point) float64 {
+	return p.Point.Distance(q)
+}
+
+func (p *ColoredPoint) ScaleBy(factor float64) {
+	p.Point.ScaleBy(factor)
+}
+
+

当Point.Distance被第一个包装方法调用时,它的接收器值是p.Point,而不是p,当然了,在Point类的方法里,你是访问不到ColoredPoint的任何字段的。

+

在类型中内嵌的匿名字段也可能是一个命名类型的指针,这种情况下字段和方法会被间接地引入到当前的类型中(译注:访问需要通过该指针指向的对象去取)。添加这一层间接关系让我们可以共享通用的结构并动态地改变对象之间的关系。下面这个ColoredPoint的声明内嵌了一个*Point的指针。

+
type ColoredPoint struct {
+	*Point
+	Color color.RGBA
+}
+
+p := ColoredPoint{&Point{1, 1}, red}
+q := ColoredPoint{&Point{5, 4}, blue}
+fmt.Println(p.Distance(*q.Point)) // "5"
+q.Point = p.Point                 // p and q now share the same Point
+p.ScaleBy(2)
+fmt.Println(*p.Point, *q.Point) // "{2 2} {2 2}"
+
+

一个struct类型也可能会有多个匿名字段。我们将ColoredPoint定义为下面这样:

+
type ColoredPoint struct {
+	Point
+	color.RGBA
+}
+
+

然后这种类型的值便会拥有Point和RGBA类型的所有方法,以及直接定义在ColoredPoint中的方法。当编译器解析一个选择器到方法时,比如p.ScaleBy,它会首先去找直接定义在这个类型里的ScaleBy方法,然后找被ColoredPoint的内嵌字段们引入的方法,然后去找Point和RGBA的内嵌字段引入的方法,然后一直递归向下找。如果选择器有二义性的话编译器会报错,比如你在同一级里有两个同名的方法。

+

方法只能在命名类型(像Point)或者指向类型的指针上定义,但是多亏了内嵌,有些时候我们给匿名struct类型来定义方法也有了手段。

+

下面是一个小trick。这个例子展示了简单的cache,其使用两个包级别的变量来实现,一个mutex互斥量(§9.2)和它所操作的cache:

+
var (
+	mu sync.Mutex // guards mapping
+	mapping = make(map[string]string)
+)
+
+func Lookup(key string) string {
+	mu.Lock()
+	v := mapping[key]
+	mu.Unlock()
+	return v
+}
+
+

下面这个版本在功能上是一致的,但将两个包级别的变量放在了cache这个struct一组内:

+
var cache = struct {
+	sync.Mutex
+	mapping map[string]string
+}{
+	mapping: make(map[string]string),
+}
+
+
+func Lookup(key string) string {
+	cache.Lock()
+	v := cache.mapping[key]
+	cache.Unlock()
+	return v
+}
+
+

我们给新的变量起了一个更具表达性的名字:cache。因为sync.Mutex字段也被嵌入到了这个struct里,其Lock和Unlock方法也就都被引入到了这个匿名结构中了,这让我们能够以一个简单明了的语法来对其进行加锁解锁操作。

+

6.4. 方法值和方法表达式

+

我们经常选择一个方法,并且在同一个表达式里执行,比如常见的p.Distance()形式,实际上将其分成两步来执行也是可能的。p.Distance叫作“选择器”,选择器会返回一个方法“值”->一个将方法(Point.Distance)绑定到特定接收器变量的函数。这个函数可以不通过指定其接收器即可被调用;即调用时不需要指定接收器(译注:因为已经在前文中指定过了),只要传入函数的参数即可:

+
p := Point{1, 2}
+q := Point{4, 6}
+
+distanceFromP := p.Distance        // method value
+fmt.Println(distanceFromP(q))      // "5"
+var origin Point                   // {0, 0}
+fmt.Println(distanceFromP(origin)) // "2.23606797749979", sqrt(5)
+
+scaleP := p.ScaleBy // method value
+scaleP(2)           // p becomes (2, 4)
+scaleP(3)           //      then (6, 12)
+scaleP(10)          //      then (60, 120)
+
+

在一个包的API需要一个函数值、且调用方希望操作的是某一个绑定了对象的方法的话,方法“值”会非常实用(``=_=`真是绕)。举例来说,下面例子中的time.AfterFunc这个函数的功能是在指定的延迟时间之后来执行一个(译注:另外的)函数。且这个函数操作的是一个Rocket对象r

+
type Rocket struct { /* ... */ }
+func (r *Rocket) Launch() { /* ... */ }
+r := new(Rocket)
+time.AfterFunc(10 * time.Second, func() { r.Launch() })
+
+

直接用方法“值”传入AfterFunc的话可以更为简短:

+
time.AfterFunc(10 * time.Second, r.Launch)
+
+

译注:省掉了上面那个例子里的匿名函数。

+

和方法“值”相关的还有方法表达式。当调用一个方法时,与调用一个普通的函数相比,我们必须要用选择器(p.Distance)语法来指定方法的接收器。

+

当T是一个类型时,方法表达式可能会写作T.f或者(*T).f,会返回一个函数“值”,这种函数会将其第一个参数用作接收器,所以可以用通常(译注:不写选择器)的方式来对其进行调用:

+
p := Point{1, 2}
+q := Point{4, 6}
+
+distance := Point.Distance   // method expression
+fmt.Println(distance(p, q))  // "5"
+fmt.Printf("%T\n", distance) // "func(Point, Point) float64"
+
+scale := (*Point).ScaleBy
+scale(&p, 2)
+fmt.Println(p)            // "{2 4}"
+fmt.Printf("%T\n", scale) // "func(*Point, float64)"
+
+// 译注:这个Distance实际上是指定了Point对象为接收器的一个方法func (p Point) Distance(),
+// 但通过Point.Distance得到的函数需要比实际的Distance方法多一个参数,
+// 即其需要用第一个额外参数指定接收器,后面排列Distance方法的参数。
+// 看起来本书中函数和方法的区别是指有没有接收器,而不像其他语言那样是指有没有返回值。
+
+

当你根据一个变量来决定调用同一个类型的哪个函数时,方法表达式就显得很有用了。你可以根据选择来调用接收器各不相同的方法。下面的例子,变量op代表Point类型的addition或者subtraction方法,Path.TranslateBy方法会为其Path数组中的每一个Point来调用对应的方法:

+
type Point struct{ X, Y float64 }
+
+func (p Point) Add(q Point) Point { return Point{p.X + q.X, p.Y + q.Y} }
+func (p Point) Sub(q Point) Point { return Point{p.X - q.X, p.Y - q.Y} }
+
+type Path []Point
+
+func (path Path) TranslateBy(offset Point, add bool) {
+	var op func(p, q Point) Point
+	if add {
+		op = Point.Add
+	} else {
+		op = Point.Sub
+	}
+	for i := range path {
+		// Call either path[i].Add(offset) or path[i].Sub(offset).
+		path[i] = op(path[i], offset)
+	}
+}
+
+

6.5. 示例: Bit数组

+

Go语言里的集合一般会用map[T]bool这种形式来表示,T代表元素类型。集合用map类型来表示虽然非常灵活,但我们可以以一种更好的形式来表示它。例如在数据流分析领域,集合元素通常是一个非负整数,集合会包含很多元素,并且集合会经常进行并集、交集操作,这种情况下,bit数组会比map表现更加理想。(译注:这里再补充一个例子,比如我们执行一个http下载任务,把文件按照16kb一块划分为很多块,需要有一个全局变量来标识哪些块下载完成了,这种时候也需要用到bit数组。)

+

一个bit数组通常会用一个无符号数或者称之为“字”的slice来表示,每一个元素的每一位都表示集合里的一个值。当集合的第i位被设置时,我们才说这个集合包含元素i。下面的这个程序展示了一个简单的bit数组类型,并且实现了三个函数来对这个bit数组来进行操作:

+

gopl.io/ch6/intset

+
// An IntSet is a set of small non-negative integers.
+// Its zero value represents the empty set.
+type IntSet struct {
+	words []uint64
+}
+
+// Has reports whether the set contains the non-negative value x.
+func (s *IntSet) Has(x int) bool {
+	word, bit := x/64, uint(x%64)
+	return word < len(s.words) && s.words[word]&(1<<bit) != 0
+}
+
+// Add adds the non-negative value x to the set.
+func (s *IntSet) Add(x int) {
+	word, bit := x/64, uint(x%64)
+	for word >= len(s.words) {
+		s.words = append(s.words, 0)
+	}
+	s.words[word] |= 1 << bit
+}
+
+// UnionWith sets s to the union of s and t.
+func (s *IntSet) UnionWith(t *IntSet) {
+	for i, tword := range t.words {
+		if i < len(s.words) {
+			s.words[i] |= tword
+		} else {
+			s.words = append(s.words, tword)
+		}
+	}
+}
+
+
+

因为每一个字都有64个二进制位,所以为了定位x的bit位,我们用了x/64的商作为字的下标,并且用x%64得到的值作为这个字内的bit的所在位置。UnionWith这个方法里用到了bit位的“或”逻辑操作符号|来一次完成64个元素的或计算。(在练习6.5中我们还会有程序用到这个64位字的例子。)

+

当前这个实现还缺少了很多必要的特性,我们把其中一些作为练习题列在本小节之后。但是有一个方法如果缺失的话我们的bit数组可能会比较难混:将IntSet作为一个字符串来打印。这里我们来实现它,让我们来给上面的例子添加一个String方法,类似2.5节中做的那样:

+
// String returns the set as a string of the form "{1 2 3}".
+func (s *IntSet) String() string {
+	var buf bytes.Buffer
+	buf.WriteByte('{')
+	for i, word := range s.words {
+		if word == 0 {
+			continue
+		}
+		for j := 0; j < 64; j++ {
+			if word&(1<<uint(j)) != 0 {
+				if buf.Len() > len("{") {
+					buf.WriteByte(' ')
+				}
+				fmt.Fprintf(&buf, "%d", 64*i+j)
+			}
+		}
+	}
+	buf.WriteByte('}')
+	return buf.String()
+}
+
+

这里留意一下String方法,是不是和3.5.4节中的intsToString方法很相似;bytes.Buffer在String方法里经常这么用。当你为一个复杂的类型定义了一个String方法时,fmt包就会特殊对待这种类型的值,这样可以让这些类型在打印的时候看起来更加友好,而不是直接打印其原始的值。fmt会直接调用用户定义的String方法。这种机制依赖于接口和类型断言,在第7章中我们会详细介绍。

+

现在我们就可以在实战中直接用上面定义好的IntSet了:

+
var x, y IntSet
+x.Add(1)
+x.Add(144)
+x.Add(9)
+fmt.Println(x.String()) // "{1 9 144}"
+
+y.Add(9)
+y.Add(42)
+fmt.Println(y.String()) // "{9 42}"
+
+x.UnionWith(&y)
+fmt.Println(x.String()) // "{1 9 42 144}"
+fmt.Println(x.Has(9), x.Has(123)) // "true false"
+
+

这里要注意:我们声明的String和Has两个方法都是以指针类型*IntSet来作为接收器的,但实际上对于这两个类型来说,把接收器声明为指针类型也没什么必要。不过另外两个函数就不是这样了,因为另外两个函数操作的是s.words对象,如果你不把接收器声明为指针对象,那么实际操作的是拷贝对象,而不是原来的那个对象。因此,因为我们的String方法定义在IntSet指针上,所以当我们的变量是IntSet类型而不是IntSet指针时,可能会有下面这样让人意外的情况:

+
fmt.Println(&x)         // "{1 9 42 144}"
+fmt.Println(x.String()) // "{1 9 42 144}"
+fmt.Println(x)          // "{[4398046511618 0 65536]}"
+
+

在第一个Println中,我们打印一个*IntSet的指针,这个类型的指针确实有自定义的String方法。第二Println,我们直接调用了x变量的String()方法;这种情况下编译器会隐式地在x前插入&操作符,这样相当于我们还是调用的IntSet指针的String方法。在第三个Println中,因为IntSet类型没有String方法,所以Println方法会直接以原始的方式理解并打印。所以在这种情况下&符号是不能忘的。在我们这种场景下,你把String方法绑定到IntSet对象上,而不是IntSet指针上可能会更合适一些,不过这也需要具体问题具体分析。

+

练习6.1: 为bit数组实现下面这些方法

+
func (*IntSet) Len() int      // return the number of elements
+func (*IntSet) Remove(x int)  // remove x from the set
+func (*IntSet) Clear()        // remove all elements from the set
+func (*IntSet) Copy() *IntSet // return a copy of the set
+
+

练习 6.2: 定义一个变参方法(*IntSet).AddAll(...int),这个方法可以添加一组IntSet,比如s.AddAll(1,2,3)。

+

练习 6.3: (*IntSet).UnionWith会用|操作符计算两个集合的并集,我们再为IntSet实现另外的几个函数IntersectWith(交集:元素在A集合B集合均出现),DifferenceWith(差集:元素出现在A集合,未出现在B集合),SymmetricDifference(并差集:元素出现在A但没有出现在B,或者出现在B没有出现在A)。

+

***练习6.4: ** 实现一个Elems方法,返回集合中的所有元素,用于做一些range之类的遍历操作。

+

练习 6.5: 我们这章定义的IntSet里的每个字都是用的uint64类型,但是64位的数值可能在32位的平台上不高效。修改程序,使其使用uint类型,这种类型对于32位平台来说更合适。当然了,这里我们可以不用简单粗暴地除64,可以定义一个常量来决定是用32还是64,这里你可能会用到平台的自动判断的一个智能表达式:32 << (^uint(0) >> 63)

+

6.6. 封装

+

一个对象的变量或者方法如果对调用方是不可见的话,一般就被定义为“封装”。封装有时候也被叫做信息隐藏,同时也是面向对象编程最关键的一个方面。

+

Go语言只有一种控制可见性的手段:大写首字母的标识符会从定义它们的包中被导出,小写字母的则不会。这种限制包内成员的方式同样适用于struct或者一个类型的方法。因而如果我们想要封装一个对象,我们必须将其定义为一个struct。

+

这也就是前面的小节中IntSet被定义为struct类型的原因,尽管它只有一个字段:

+
type IntSet struct {
+    words []uint64
+}
+
+

当然,我们也可以把IntSet定义为一个slice类型,但这样我们就需要把代码中所有方法里用到的s.words用*s替换掉了:

+
type IntSet []uint64
+
+

尽管这个版本的IntSet在本质上是一样的,但它也允许其它包中可以直接读取并编辑这个slice。换句话说,相对于*s这个表达式会出现在所有的包中,s.words只需要在定义IntSet的包中出现(译注:所以还是推荐后者吧的意思)。

+

这种基于名字的手段使得在语言中最小的封装单元是package,而不是像其它语言一样的类型。一个struct类型的字段对同一个包的所有代码都有可见性,无论你的代码是写在一个函数还是一个方法里。

+

封装提供了三方面的优点。首先,因为调用方不能直接修改对象的变量值,其只需要关注少量的语句并且只要弄懂少量变量的可能的值即可。

+

第二,隐藏实现的细节,可以防止调用方依赖那些可能变化的具体实现,这样使设计包的程序员在不破坏对外的api情况下能得到更大的自由。

+

把bytes.Buffer这个类型作为例子来考虑。这个类型在做短字符串叠加的时候很常用,所以在设计的时候可以做一些预先的优化,比如提前预留一部分空间,来避免反复的内存分配。又因为Buffer是一个struct类型,这些额外的空间可以用附加的字节数组来保存,且放在一个小写字母开头的字段中。这样在外部的调用方只能看到性能的提升,但并不会得到这个附加变量。Buffer和其增长算法我们列在这里,为了简洁性稍微做了一些精简:

+
type Buffer struct {
+    buf     []byte
+    initial [64]byte
+    /* ... */
+}
+
+// Grow expands the buffer's capacity, if necessary,
+// to guarantee space for another n bytes. [...]
+func (b *Buffer) Grow(n int) {
+    if b.buf == nil {
+        b.buf = b.initial[:0] // use preallocated space initially
+    }
+    if len(b.buf)+n > cap(b.buf) {
+        buf := make([]byte, b.Len(), 2*cap(b.buf) + n)
+        copy(buf, b.buf)
+        b.buf = buf
+    }
+}
+
+

封装的第三个优点也是最重要的优点,是阻止了外部调用方对对象内部的值任意地进行修改。因为对象内部变量只可以被同一个包内的函数修改,所以包的作者可以让这些函数确保对象内部的一些值的不变性。比如下面的Counter类型允许调用方来增加counter变量的值,并且允许将这个值reset为0,但是不允许随便设置这个值(译注:因为压根就访问不到):

+
type Counter struct { n int }
+func (c *Counter) N() int     { return c.n }
+func (c *Counter) Increment() { c.n++ }
+func (c *Counter) Reset()     { c.n = 0 }
+
+

只用来访问或修改内部变量的函数被称为setter或者getter,例子如下,比如log包里的Logger类型对应的一些函数。在命名一个getter方法时,我们通常会省略掉前面的Get前缀。这种简洁上的偏好也可以推广到各种类型的前缀比如Fetch,Find或者Lookup。

+
package log
+type Logger struct {
+	flags  int
+	prefix string
+	// ...
+}
+func (l *Logger) Flags() int
+func (l *Logger) SetFlags(flag int)
+func (l *Logger) Prefix() string
+func (l *Logger) SetPrefix(prefix string)
+
+

Go的编码风格不禁止直接导出字段。当然,一旦进行了导出,就没有办法在保证API兼容的情况下去除对其的导出,所以在一开始的选择一定要经过深思熟虑并且要考虑到包内部的一些不变量的保证,未来可能的变化,以及调用方的代码质量是否会因为包的一点修改而变差。

+

封装并不总是理想的。 +虽然封装在有些情况是必要的,但有时候我们也需要暴露一些内部内容,比如:time.Duration将其表现暴露为一个int64数字的纳秒,使得我们可以用一般的数值操作来对时间进行对比,甚至可以定义这种类型的常量:

+
const day = 24 * time.Hour
+fmt.Println(day.Seconds()) // "86400"
+
+

另一个例子,将IntSet和本章开头的geometry.Path进行对比。Path被定义为一个slice类型,这允许其调用slice的字面方法来对其内部的points用range进行迭代遍历;在这一点上,IntSet是没有办法让你这么做的。

+

这两种类型决定性的不同:geometry.Path的本质是一个坐标点的序列,不多也不少,我们可以预见到之后也并不会给他增加额外的字段,所以在geometry包中将Path暴露为一个slice。相比之下,IntSet仅仅是在这里用了一个[]uint64的slice。这个类型还可以用[]uint类型来表示,或者我们甚至可以用其它完全不同的占用更小内存空间的东西来表示这个集合,所以我们可能还会需要额外的字段来在这个类型中记录元素的个数。也正是因为这些原因,我们让IntSet对调用方不透明。

+

在这章中,我们学到了如何将方法与命名类型进行组合,并且知道了如何调用这些方法。尽管方法对于OOP编程来说至关重要,但他们只是OOP编程里的半边天。为了完成OOP,我们还需要接口。Go里的接口会在下一章中介绍。

+

第7章 接口

+

接口类型是对其它类型行为的抽象和概括;因为接口类型不会和特定的实现细节绑定在一起,通过这种抽象的方式我们可以让我们的函数更加灵活和更具有适应能力。

+

很多面向对象的语言都有相似的接口概念,但Go语言中接口类型的独特之处在于它是满足隐式实现的。也就是说,我们没有必要对于给定的具体类型定义所有满足的接口类型;简单地拥有一些必需的方法就足够了。这种设计可以让你创建一个新的接口类型满足已经存在的具体类型却不会去改变这些类型的定义;当我们使用的类型来自于不受我们控制的包时这种设计尤其有用。

+

在本章,我们会开始看到接口类型和值的一些基本技巧。顺着这种方式我们将学习几个来自标准库的重要接口。很多Go程序中都尽可能多的去使用标准库中的接口。最后,我们会在(§7.10)看到类型断言的知识,在(§7.13)看到类型开关的使用并且学到他们是怎样让不同的类型的概括成为可能。

+

7.1. 接口约定

+

目前为止,我们看到的类型都是具体的类型。一个具体的类型可以准确的描述它所代表的值,并且展示出对类型本身的一些操作方式:就像数字类型的算术操作,切片类型的取下标、添加元素和范围获取操作。具体的类型还可以通过它的内置方法提供额外的行为操作。总的来说,当你拿到一个具体的类型时你就知道它的本身是什么和你可以用它来做什么。

+

在Go语言中还存在着另外一种类型:接口类型。接口类型是一种抽象的类型。它不会暴露出它所代表的对象的内部值的结构和这个对象支持的基础操作的集合;它们只会表现出它们自己的方法。也就是说当你有看到一个接口类型的值时,你不知道它是什么,唯一知道的就是可以通过它的方法来做什么。

+

在本书中,我们一直使用两个相似的函数来进行字符串的格式化:fmt.Printf,它会把结果写到标准输出,和fmt.Sprintf,它会把结果以字符串的形式返回。得益于使用接口,我们不必可悲的因为返回结果在使用方式上的一些浅显不同就必需把格式化这个最困难的过程复制一份。实际上,这两个函数都使用了另一个函数fmt.Fprintf来进行封装。fmt.Fprintf这个函数对它的计算结果会被怎么使用是完全不知道的。

+
package fmt
+
+func Fprintf(w io.Writer, format string, args ...interface{}) (int, error)
+func Printf(format string, args ...interface{}) (int, error) {
+	return Fprintf(os.Stdout, format, args...)
+}
+func Sprintf(format string, args ...interface{}) string {
+	var buf bytes.Buffer
+	Fprintf(&buf, format, args...)
+	return buf.String()
+}
+
+

Fprintf的前缀F表示文件(File)也表明格式化输出结果应该被写入第一个参数提供的文件中。在Printf函数中的第一个参数os.Stdout是*os.File类型;在Sprintf函数中的第一个参数&buf是一个指向可以写入字节的内存缓冲区,然而它 +并不是一个文件类型尽管它在某种意义上和文件类型相似。

+

即使Fprintf函数中的第一个参数也不是一个文件类型。它是io.Writer类型,这是一个接口类型定义如下:

+
package io
+
+// Writer is the interface that wraps the basic Write method.
+type Writer interface {
+	// Write writes len(p) bytes from p to the underlying data stream.
+	// It returns the number of bytes written from p (0 <= n <= len(p))
+	// and any error encountered that caused the write to stop early.
+	// Write must return a non-nil error if it returns n < len(p).
+	// Write must not modify the slice data, even temporarily.
+	//
+	// Implementations must not retain p.
+	Write(p []byte) (n int, err error)
+}
+
+

io.Writer类型定义了函数Fprintf和这个函数调用者之间的约定。一方面这个约定需要调用者提供具体类型的值就像*os.File*bytes.Buffer,这些类型都有一个特定签名和行为的Write的函数。另一方面这个约定保证了Fprintf接受任何满足io.Writer接口的值都可以工作。Fprintf函数可能没有假定写入的是一个文件或是一段内存,而是写入一个可以调用Write函数的值。

+

因为fmt.Fprintf函数没有对具体操作的值做任何假设,而是仅仅通过io.Writer接口的约定来保证行为,所以第一个参数可以安全地传入一个只需要满足io.Writer接口的任意具体类型的值。一个类型可以自由地被另一个满足相同接口的类型替换,被称作可替换性(LSP里氏替换)。这是一个面向对象的特征。

+

让我们通过一个新的类型来进行校验,下面*ByteCounter类型里的Write方法,仅仅在丢弃写向它的字节前统计它们的长度。(在这个+=赋值语句中,让len(p)的类型和*c的类型匹配的转换是必须的。)

+

gopl.io/ch7/bytecounter

+
type ByteCounter int
+
+func (c *ByteCounter) Write(p []byte) (int, error) {
+	*c += ByteCounter(len(p)) // convert int to ByteCounter
+	return len(p), nil
+}
+
+

因为*ByteCounter满足io.Writer的约定,我们可以把它传入Fprintf函数中;Fprintf函数执行字符串格式化的过程不会去关注ByteCounter正确的累加结果的长度。

+
var c ByteCounter
+c.Write([]byte("hello"))
+fmt.Println(c) // "5", = len("hello")
+c = 0          // reset the counter
+var name = "Dolly"
+fmt.Fprintf(&c, "hello, %s", name)
+fmt.Println(c) // "12", = len("hello, Dolly")
+
+

除了io.Writer这个接口类型,还有另一个对fmt包很重要的接口类型。Fprintf和Fprintln函数向类型提供了一种控制它们值输出的途径。在2.5节中,我们为Celsius类型提供了一个String方法以便于可以打印成这样"100°C" ,在6.5节中我们给*IntSet添加一个String方法,这样集合可以用传统的符号来进行表示就像"{1 2 3}"。给一个类型定义String方法,可以让它满足最广泛使用之一的接口类型fmt.Stringer:

+
package fmt
+
+// The String method is used to print values passed
+// as an operand to any format that accepts a string
+// or to an unformatted printer such as Print.
+type Stringer interface {
+	String() string
+}
+
+

我们会在7.10节解释fmt包怎么发现哪些值是满足这个接口类型的。

+

练习 7.1: 使用来自ByteCounter的思路,实现一个针对单词和行数的计数器。你会发现bufio.ScanWords非常的有用。

+

练习 7.2: 写一个带有如下函数签名的函数CountingWriter,传入一个io.Writer接口类型,返回一个把原来的Writer封装在里面的新的Writer类型和一个表示新的写入字节数的int64类型指针。

+
func CountingWriter(w io.Writer) (io.Writer, *int64)
+
+

练习 7.3: 为在gopl.io/ch4/treesort(§4.4)中的*tree类型实现一个String方法去展示tree类型的值序列。

+

7.2. 接口类型

+

接口类型具体描述了一系列方法的集合,一个实现了这些方法的具体类型是这个接口类型的实例。

+

io.Writer类型是用得最广泛的接口之一,因为它提供了所有类型的写入bytes的抽象,包括文件类型,内存缓冲区,网络链接,HTTP客户端,压缩工具,哈希等等。io包中定义了很多其它有用的接口类型。Reader可以代表任意可以读取bytes的类型,Closer可以是任意可以关闭的值,例如一个文件或是网络链接。(到现在你可能注意到了很多Go语言中单方法接口的命名习惯)

+
package io
+type Reader interface {
+	Read(p []byte) (n int, err error)
+}
+type Closer interface {
+	Close() error
+}
+
+

再往下看,我们发现有些新的接口类型通过组合已有的接口来定义。下面是两个例子:

+
type ReadWriter interface {
+	Reader
+	Writer
+}
+type ReadWriteCloser interface {
+	Reader
+	Writer
+	Closer
+}
+
+

上面用到的语法和结构内嵌相似,我们可以用这种方式以一个简写命名一个接口,而不用声明它所有的方法。这种方式称为接口内嵌。尽管略失简洁,我们可以像下面这样,不使用内嵌来声明io.ReadWriter接口。

+
type ReadWriter interface {
+	Read(p []byte) (n int, err error)
+	Write(p []byte) (n int, err error)
+}
+
+

或者甚至使用一种混合的风格:

+
type ReadWriter interface {
+	Read(p []byte) (n int, err error)
+	Writer
+}
+
+

上面3种定义方式都是一样的效果。方法顺序的变化也没有影响,唯一重要的就是这个集合里面的方法。

+

练习 7.4: strings.NewReader函数通过读取一个string参数返回一个满足io.Reader接口类型的值(和其它值)。实现一个简单版本的NewReader,用它来构造一个接收字符串输入的HTML解析器(§5.2)

+

练习 7.5: io包里面的LimitReader函数接收一个io.Reader接口类型的r和字节数n,并且返回另一个从r中读取字节但是当读完n个字节后就表示读到文件结束的Reader。实现这个LimitReader函数:

+
func LimitReader(r io.Reader, n int64) io.Reader
+
+

7.3. 实现接口的条件

+

一个类型如果拥有一个接口需要的所有方法,那么这个类型就实现了这个接口。例如,*os.File类型实现了io.Reader,Writer,Closer,和ReadWriter接口。*bytes.Buffer实现了Reader,Writer,和ReadWriter这些接口,但是它没有实现Closer接口因为它不具有Close方法。Go的程序员经常会简要的把一个具体的类型描述成一个特定的接口类型。举个例子,*bytes.Buffer是io.Writer;*os.Files是io.ReadWriter。

+

接口指定的规则非常简单:表达一个类型属于某个接口只要这个类型实现这个接口。所以:

+
var w io.Writer
+w = os.Stdout           // OK: *os.File has Write method
+w = new(bytes.Buffer)   // OK: *bytes.Buffer has Write method
+w = time.Second         // compile error: time.Duration lacks Write method
+
+var rwc io.ReadWriteCloser
+rwc = os.Stdout         // OK: *os.File has Read, Write, Close methods
+rwc = new(bytes.Buffer) // compile error: *bytes.Buffer lacks Close method
+
+

这个规则甚至适用于等式右边本身也是一个接口类型

+
w = rwc                 // OK: io.ReadWriteCloser has Write method
+rwc = w                 // compile error: io.Writer lacks Close method
+
+

因为ReadWriter和ReadWriteCloser包含有Writer的方法,所以任何实现了ReadWriter和ReadWriteCloser的类型必定也实现了Writer接口

+

在进一步学习前,必须先解释一个类型持有一个方法的表示当中的细节。回想在6.2章中,对于每一个命名过的具体类型T;它的一些方法的接收者是类型T本身然而另一些则是一个*T的指针。还记得在T类型的参数上调用一个*T的方法是合法的,只要这个参数是一个变量;编译器隐式的获取了它的地址。但这仅仅是一个语法糖:T类型的值不拥有所有*T指针的方法,这样它就可能只实现了更少的接口。

+

举个例子可能会更清晰一点。在第6.5章中,IntSet类型的String方法的接收者是一个指针类型,所以我们不能在一个不能寻址的IntSet值上调用这个方法:

+
type IntSet struct { /* ... */ }
+func (*IntSet) String() string
+var _ = IntSet{}.String() // compile error: String requires *IntSet receiver
+
+

但是我们可以在一个IntSet变量上调用这个方法:

+
var s IntSet
+var _ = s.String() // OK: s is a variable and &s has a String method
+
+

然而,由于只有*IntSet类型有String方法,所以也只有*IntSet类型实现了fmt.Stringer接口:

+
var _ fmt.Stringer = &s // OK
+var _ fmt.Stringer = s  // compile error: IntSet lacks String method
+
+

12.8章包含了一个打印出任意值的所有方法的程序,然后可以使用godoc -analysis=type tool(§10.7.4)展示每个类型的方法和具体类型和接口之间的关系

+

就像信封封装和隐藏起信件来一样,接口类型封装和隐藏具体类型和它的值。即使具体类型有其它的方法,也只有接口类型暴露出来的方法会被调用到:

+
os.Stdout.Write([]byte("hello")) // OK: *os.File has Write method
+os.Stdout.Close()                // OK: *os.File has Close method
+
+var w io.Writer
+w = os.Stdout
+w.Write([]byte("hello")) // OK: io.Writer has Write method
+w.Close()                // compile error: io.Writer lacks Close method
+
+

一个有更多方法的接口类型,比如io.ReadWriter,和少一些方法的接口类型例如io.Reader,进行对比;更多方法的接口类型会告诉我们更多关于它的值持有的信息,并且对实现它的类型要求更加严格。那么关于interface{}类型,它没有任何方法,请讲出哪些具体的类型实现了它?

+

这看上去好像没有用,但实际上interface{}被称为空接口类型是不可或缺的。因为空接口类型对实现它的类型没有要求,所以我们可以将任意一个值赋给空接口类型。

+
var any interface{}
+any = true
+any = 12.34
+any = "hello"
+any = map[string]int{"one": 1}
+any = new(bytes.Buffer)
+
+

尽管不是很明显,从本书最早的例子中我们就已经在使用空接口类型。它允许像fmt.Println或者5.7章中的errorf函数接受任何类型的参数。

+

对于创建的一个interface{}值持有一个boolean,float,string,map,pointer,或者任意其它的类型;我们当然不能直接对它持有的值做操作,因为interface{}没有任何方法。我们会在7.10章中学到一种用类型断言来获取interface{}中值的方法。

+

因为接口与实现只依赖于判断两个类型的方法,所以没有必要定义一个具体类型和它实现的接口之间的关系。也就是说,有意地在文档里说明或者程序上断言这种关系偶尔是有用的,但程序上不强制这么做。下面的定义在编译期断言一个*bytes.Buffer的值实现了io.Writer接口类型:

+
// *bytes.Buffer must satisfy io.Writer
+var w io.Writer = new(bytes.Buffer)
+
+

因为任意*bytes.Buffer的值,甚至包括nil通过(*bytes.Buffer)(nil)进行显示的转换都实现了这个接口,所以我们不必分配一个新的变量。并且因为我们绝不会引用变量w,我们可以使用空标识符来进行代替。总的看,这些变化可以让我们得到一个更朴素的版本:

+
// *bytes.Buffer must satisfy io.Writer
+var _ io.Writer = (*bytes.Buffer)(nil)
+
+

非空的接口类型比如io.Writer经常被指针类型实现,尤其当一个或多个接口方法像Write方法那样隐式的给接收者带来变化的时候。一个结构体的指针是非常常见的承载方法的类型。

+

但是并不意味着只有指针类型满足接口类型,甚至连一些有设置方法的接口类型也可能会被Go语言中其它的引用类型实现。我们已经看过slice类型的方法(geometry.Path,§6.1)和map类型的方法(url.Values,§6.2.1),后面还会看到函数类型的方法的例子(http.HandlerFunc,§7.7)。甚至基本的类型也可能会实现一些接口;就如我们在7.4章中看到的time.Duration类型实现了fmt.Stringer接口。

+

一个具体的类型可能实现了很多不相关的接口。考虑在一个组织出售数字文化产品比如音乐,电影和书籍的程序中可能定义了下列的具体类型:

+
Album
+Book
+Movie
+Magazine
+Podcast
+TVEpisode
+Track
+
+

我们可以把每个抽象的特点用接口来表示。一些特性对于所有的这些文化产品都是共通的,例如标题,创作日期和作者列表。

+
type Artifact interface {
+	Title() string
+	Creators() []string
+	Created() time.Time
+}
+
+

其它的一些特性只对特定类型的文化产品才有。和文字排版特性相关的只有books和magazines,还有只有movies和TV剧集和屏幕分辨率相关。

+
type Text interface {
+	Pages() int
+	Words() int
+	PageSize() int
+}
+type Audio interface {
+	Stream() (io.ReadCloser, error)
+	RunningTime() time.Duration
+	Format() string // e.g., "MP3", "WAV"
+}
+type Video interface {
+	Stream() (io.ReadCloser, error)
+	RunningTime() time.Duration
+	Format() string // e.g., "MP4", "WMV"
+	Resolution() (x, y int)
+}
+
+

这些接口不止是一种有用的方式来分组相关的具体类型和表示他们之间的共同特点。我们后面可能会发现其它的分组。举例,如果我们发现我们需要以同样的方式处理Audio和Video,我们可以定义一个Streamer接口来代表它们之间相同的部分而不必对已经存在的类型做改变。

+
type Streamer interface {
+	Stream() (io.ReadCloser, error)
+	RunningTime() time.Duration
+	Format() string
+}
+
+

每一个具体类型的组基于它们相同的行为可以表示成一个接口类型。不像基于类的语言,他们一个类实现的接口集合需要进行显式的定义,在Go语言中我们可以在需要的时候定义一个新的抽象或者特定特点的组,而不需要修改具体类型的定义。当具体的类型来自不同的作者时这种方式会特别有用。当然也确实没有必要在具体的类型中指出这些共性。

+

7.4. flag.Value接口

+

在本章,我们会学到另一个标准的接口类型flag.Value是怎么帮助命令行标记定义新的符号的。思考下面这个会休眠特定时间的程序:

+

gopl.io/ch7/sleep

+
var period = flag.Duration("period", 1*time.Second, "sleep period")
+
+func main() {
+	flag.Parse()
+	fmt.Printf("Sleeping for %v...", *period)
+	time.Sleep(*period)
+	fmt.Println()
+}
+
+

在它休眠前它会打印出休眠的时间周期。fmt包调用time.Duration的String方法打印这个时间周期是以用户友好的注解方式,而不是一个纳秒数字:

+
$ go build gopl.io/ch7/sleep
+$ ./sleep
+Sleeping for 1s...
+
+

默认情况下,休眠周期是一秒,但是可以通过 -period 这个命令行标记来控制。flag.Duration函数创建一个time.Duration类型的标记变量并且允许用户通过多种用户友好的方式来设置这个变量的大小,这种方式还包括和String方法相同的符号排版形式。这种对称设计使得用户交互良好。

+
$ ./sleep -period 50ms
+Sleeping for 50ms...
+$ ./sleep -period 2m30s
+Sleeping for 2m30s...
+$ ./sleep -period 1.5h
+Sleeping for 1h30m0s...
+$ ./sleep -period "1 day"
+invalid value "1 day" for flag -period: time: invalid duration 1 day
+
+

因为时间周期标记值非常的有用,所以这个特性被构建到了flag包中;但是我们为我们自己的数据类型定义新的标记符号是简单容易的。我们只需要定义一个实现flag.Value接口的类型,如下:

+
package flag
+
+// Value is the interface to the value stored in a flag.
+type Value interface {
+	String() string
+	Set(string) error
+}
+
+

String方法格式化标记的值用在命令行帮助消息中;这样每一个flag.Value也是一个fmt.Stringer。Set方法解析它的字符串参数并且更新标记变量的值。实际上,Set方法和String是两个相反的操作,所以最好的办法就是对他们使用相同的注解方式。

+

让我们定义一个允许通过摄氏度或者华氏温度变换的形式指定温度的celsiusFlag类型。注意celsiusFlag内嵌了一个Celsius类型(§2.5),因此不用实现本身就已经有String方法了。为了实现flag.Value,我们只需要定义Set方法:

+

gopl.io/ch7/tempconv

+
// *celsiusFlag satisfies the flag.Value interface.
+type celsiusFlag struct{ Celsius }
+
+func (f *celsiusFlag) Set(s string) error {
+	var unit string
+	var value float64
+	fmt.Sscanf(s, "%f%s", &value, &unit) // no error check needed
+	switch unit {
+	case "C", "°C":
+		f.Celsius = Celsius(value)
+		return nil
+	case "F", "°F":
+		f.Celsius = FToC(Fahrenheit(value))
+		return nil
+	}
+	return fmt.Errorf("invalid temperature %q", s)
+}
+
+

调用fmt.Sscanf函数从输入s中解析一个浮点数(value)和一个字符串(unit)。虽然通常必须检查Sscanf的错误返回,但是在这个例子中我们不需要。因为如果有错误发生,就没有switch case会匹配到。

+

下面的CelsiusFlag函数将所有逻辑都封装在一起。它返回一个内嵌在celsiusFlag变量f中的Celsius指针给调用者。Celsius字段是一个会通过Set方法在标记处理的过程中更新的变量。调用Var方法将标记加入应用的命令行标记集合中,有异常复杂命令行接口的全局变量flag.CommandLine.Programs可能有几个这个类型的变量。调用Var方法将一个*celsiusFlag参数赋值给一个flag.Value参数,导致编译器去检查*celsiusFlag是否有必须的方法。

+
// CelsiusFlag defines a Celsius flag with the specified name,
+// default value, and usage, and returns the address of the flag variable.
+// The flag argument must have a quantity and a unit, e.g., "100C".
+func CelsiusFlag(name string, value Celsius, usage string) *Celsius {
+	f := celsiusFlag{value}
+	flag.CommandLine.Var(&f, name, usage)
+	return &f.Celsius
+}
+
+

现在我们可以开始在我们的程序中使用新的标记:

+

gopl.io/ch7/tempflag

+
var temp = tempconv.CelsiusFlag("temp", 20.0, "the temperature")
+
+func main() {
+	flag.Parse()
+	fmt.Println(*temp)
+}
+
+

下面是典型的场景:

+
$ go build gopl.io/ch7/tempflag
+$ ./tempflag
+20°C
+$ ./tempflag -temp -18C
+-18°C
+$ ./tempflag -temp 212°F
+100°C
+$ ./tempflag -temp 273.15K
+invalid value "273.15K" for flag -temp: invalid temperature "273.15K"
+Usage of ./tempflag:
+  -temp value
+        the temperature (default 20°C)
+$ ./tempflag -help
+Usage of ./tempflag:
+  -temp value
+        the temperature (default 20°C)
+
+

练习 7.6: 对tempFlag加入支持开尔文温度。

+

练习 7.7: 解释为什么帮助信息在它的默认值是20.0没有包含°C的情况下输出了°C。

+

7.5. 接口值

+

概念上讲一个接口的值,接口值,由两个部分组成,一个具体的类型和那个类型的值。它们被称为接口的动态类型和动态值。对于像Go语言这种静态类型的语言,类型是编译期的概念;因此一个类型不是一个值。在我们的概念模型中,一些提供每个类型信息的值被称为类型描述符,比如类型的名称和方法。在一个接口值中,类型部分代表与之相关类型的描述符。

+

下面4个语句中,变量w得到了3个不同的值。(开始和最后的值是相同的)

+
var w io.Writer
+w = os.Stdout
+w = new(bytes.Buffer)
+w = nil
+
+

让我们进一步观察在每一个语句后的w变量的值和动态行为。第一个语句定义了变量w:

+
var w io.Writer
+
+

在Go语言中,变量总是被一个定义明确的值初始化,即使接口类型也不例外。对于一个接口的零值就是它的类型和值的部分都是nil(图7.1)。

+

+

一个接口值基于它的动态类型被描述为空或非空,所以这是一个空的接口值。你可以通过使用w==nil或者w!=nil来判断接口值是否为空。调用一个空接口值上的任意方法都会产生panic:

+
w.Write([]byte("hello")) // panic: nil pointer dereference
+
+

第二个语句将一个*os.File类型的值赋给变量w:

+
w = os.Stdout
+
+

这个赋值过程调用了一个具体类型到接口类型的隐式转换,这和显式的使用io.Writer(os.Stdout)是等价的。这类转换不管是显式的还是隐式的,都会刻画出操作到的类型和值。这个接口值的动态类型被设为*os.File指针的类型描述符,它的动态值持有os.Stdout的拷贝;这是一个代表处理标准输出的os.File类型变量的指针(图7.2)。

+

+

调用一个包含*os.File类型指针的接口值的Write方法,使得(*os.File).Write方法被调用。这个调用输出“hello”。

+
w.Write([]byte("hello")) // "hello"
+
+

通常在编译期,我们不知道接口值的动态类型是什么,所以一个接口上的调用必须使用动态分配。因为不是直接进行调用,所以编译器必须把代码生成在类型描述符的方法Write上,然后间接调用那个地址。这个调用的接收者是一个接口动态值的拷贝,os.Stdout。效果和下面这个直接调用一样:

+
os.Stdout.Write([]byte("hello")) // "hello"
+
+

第三个语句给接口值赋了一个*bytes.Buffer类型的值

+
w = new(bytes.Buffer)
+
+

现在动态类型是*bytes.Buffer并且动态值是一个指向新分配的缓冲区的指针(图7.3)。

+

+

Write方法的调用也使用了和之前一样的机制:

+
w.Write([]byte("hello")) // writes "hello" to the bytes.Buffers
+
+

这次类型描述符是*bytes.Buffer,所以调用了(*bytes.Buffer).Write方法,并且接收者是该缓冲区的地址。这个调用把字符串“hello”添加到缓冲区中。

+

最后,第四个语句将nil赋给了接口值:

+
w = nil
+
+

这个重置将它所有的部分都设为nil值,把变量w恢复到和它之前定义时相同的状态,在图7.1中可以看到。

+

一个接口值可以持有任意大的动态值。例如,表示时间实例的time.Time类型,这个类型有几个对外不公开的字段。我们从它上面创建一个接口值:

+
var x interface{} = time.Now()
+
+

结果可能和图7.4相似。从概念上讲,不论接口值多大,动态值总是可以容下它。(这只是一个概念上的模型;具体的实现可能会非常不同)

+

+

接口值可以使用==和!=来进行比较。两个接口值相等仅当它们都是nil值,或者它们的动态类型相同并且动态值也根据这个动态类型的==操作相等。因为接口值是可比较的,所以它们可以用在map的键或者作为switch语句的操作数。

+

然而,如果两个接口值的动态类型相同,但是这个动态类型是不可比较的(比如切片),将它们进行比较就会失败并且panic:

+
var x interface{} = []int{1, 2, 3}
+fmt.Println(x == x) // panic: comparing uncomparable type []int
+
+

考虑到这点,接口类型是非常与众不同的。其它类型要么是安全的可比较类型(如基本类型和指针)要么是完全不可比较的类型(如切片,映射类型,和函数),但是在比较接口值或者包含了接口值的聚合类型时,我们必须要意识到潜在的panic。同样的风险也存在于使用接口作为map的键或者switch的操作数。只能比较你非常确定它们的动态值是可比较类型的接口值。

+

当我们处理错误或者调试的过程中,得知接口值的动态类型是非常有帮助的。所以我们使用fmt包的%T动作:

+
var w io.Writer
+fmt.Printf("%T\n", w) // "<nil>"
+w = os.Stdout
+fmt.Printf("%T\n", w) // "*os.File"
+w = new(bytes.Buffer)
+fmt.Printf("%T\n", w) // "*bytes.Buffer"
+
+

在fmt包内部,使用反射来获取接口动态类型的名称。我们会在第12章中学到反射相关的知识。

+

7.5.1. 警告:一个包含nil指针的接口不是nil接口

+

一个不包含任何值的nil接口值和一个刚好包含nil指针的接口值是不同的。这个细微区别产生了一个容易绊倒每个Go程序员的陷阱。

+

思考下面的程序。当debug变量设置为true时,main函数会将f函数的输出收集到一个bytes.Buffer类型中。

+
const debug = true
+
+func main() {
+	var buf *bytes.Buffer
+	if debug {
+		buf = new(bytes.Buffer) // enable collection of output
+	}
+	f(buf) // NOTE: subtly incorrect!
+	if debug {
+		// ...use buf...
+	}
+}
+
+// If out is non-nil, output will be written to it.
+func f(out io.Writer) {
+	// ...do something...
+	if out != nil {
+		out.Write([]byte("done!\n"))
+	}
+}
+
+

我们可能会预计当把变量debug设置为false时可以禁止对输出的收集,但是实际上在out.Write方法调用时程序发生了panic:

+
if out != nil {
+	out.Write([]byte("done!\n")) // panic: nil pointer dereference
+}
+
+

当main函数调用函数f时,它给f函数的out参数赋了一个*bytes.Buffer的空指针,所以out的动态值是nil。然而,它的动态类型是*bytes.Buffer,意思就是out变量是一个包含空指针值的非空接口(如图7.5),所以防御性检查out!=nil的结果依然是true。

+

+

动态分配机制依然决定(*bytes.Buffer).Write的方法会被调用,但是这次的接收者的值是nil。对于一些如*os.File的类型,nil是一个有效的接收者(§6.2.1),但是*bytes.Buffer类型不在这些种类中。这个方法会被调用,但是当它尝试去获取缓冲区时会发生panic。

+

问题在于尽管一个nil的*bytes.Buffer指针有实现这个接口的方法,它也不满足这个接口具体的行为上的要求。特别是这个调用违反了(*bytes.Buffer).Write方法的接收者非空的隐含先觉条件,所以将nil指针赋给这个接口是错误的。解决方案就是将main函数中的变量buf的类型改为io.Writer,因此可以避免一开始就将一个不完整的值赋值给这个接口:

+
var buf io.Writer
+if debug {
+	buf = new(bytes.Buffer) // enable collection of output
+}
+f(buf) // OK
+
+

现在我们已经把接口值的技巧都讲完了,让我们来看更多的一些在Go标准库中的重要接口类型。在下面的三章中,我们会看到接口类型是怎样用在排序,web服务,错误处理中的。

+

7.6. sort.Interface接口

+

排序操作和字符串格式化一样是很多程序经常使用的操作。尽管一个最短的快排程序只要15行就可以搞定,但是一个健壮的实现需要更多的代码,并且我们不希望每次我们需要的时候都重写或者拷贝这些代码。

+

幸运的是,sort包内置的提供了根据一些排序函数来对任何序列排序的功能。它的设计非常独到。在很多语言中,排序算法都是和序列数据类型关联,同时排序函数和具体类型元素关联。相比之下,Go语言的sort.Sort函数不会对具体的序列和它的元素做任何假设。相反,它使用了一个接口类型sort.Interface来指定通用的排序算法和可能被排序到的序列类型之间的约定。这个接口的实现由序列的具体表示和它希望排序的元素决定,序列的表示经常是一个切片。

+

一个内置的排序算法需要知道三个东西:序列的长度,表示两个元素比较的结果,一种交换两个元素的方式;这就是sort.Interface的三个方法:

+
package sort
+
+type Interface interface {
+	Len() int
+	Less(i, j int) bool // i, j are indices of sequence elements
+	Swap(i, j int)
+}
+
+

为了对序列进行排序,我们需要定义一个实现了这三个方法的类型,然后对这个类型的一个实例应用sort.Sort函数。思考对一个字符串切片进行排序,这可能是最简单的例子了。下面是这个新的类型StringSlice和它的Len,Less和Swap方法

+
type StringSlice []string
+func (p StringSlice) Len() int           { return len(p) }
+func (p StringSlice) Less(i, j int) bool { return p[i] < p[j] }
+func (p StringSlice) Swap(i, j int)      { p[i], p[j] = p[j], p[i] }
+
+

现在我们可以通过像下面这样将一个切片转换为一个StringSlice类型来进行排序:

+
sort.Sort(StringSlice(names))
+
+

这个转换得到一个相同长度,容量,和基于names数组的切片值;并且这个切片值的类型有三个排序需要的方法。

+

对字符串切片的排序是很常用的需要,所以sort包提供了StringSlice类型,也提供了Strings函数能让上面这些调用简化成sort.Strings(names)。

+

这里用到的技术很容易适用到其它排序序列中,例如我们可以忽略大小写或者含有的特殊字符。(本书使用Go程序对索引词和页码进行排序也用到了这个技术,对罗马数字做了额外逻辑处理。)对于更复杂的排序,我们使用相同的方法,但是会用更复杂的数据结构和更复杂地实现sort.Interface的方法。

+

我们会运行上面的例子来对一个表格中的音乐播放列表进行排序。每个track都是单独的一行,每一列都是这个track的属性像艺术家,标题,和运行时间。想象一个图形用户界面来呈现这个表格,并且点击一个属性的顶部会使这个列表按照这个属性进行排序;再一次点击相同属性的顶部会进行逆向排序。让我们看下每个点击会发生什么响应。

+

下面的变量tracks包含了一个播放列表。(One of the authors apologizes for the other author’s musical tastes.)每个元素都不是Track本身而是指向它的指针。尽管我们在下面的代码中直接存储Tracks也可以工作,sort函数会交换很多对元素,所以如果每个元素都是指针而不是Track类型会更快,指针是一个机器字码长度而Track类型可能是八个或更多。

+

gopl.io/ch7/sorting

+
type Track struct {
+	Title  string
+	Artist string
+	Album  string
+	Year   int
+	Length time.Duration
+}
+
+var tracks = []*Track{
+	{"Go", "Delilah", "From the Roots Up", 2012, length("3m38s")},
+	{"Go", "Moby", "Moby", 1992, length("3m37s")},
+	{"Go Ahead", "Alicia Keys", "As I Am", 2007, length("4m36s")},
+	{"Ready 2 Go", "Martin Solveig", "Smash", 2011, length("4m24s")},
+}
+
+func length(s string) time.Duration {
+	d, err := time.ParseDuration(s)
+	if err != nil {
+		panic(s)
+	}
+	return d
+}
+
+

printTracks函数将播放列表打印成一个表格。一个图形化的展示可能会更好点,但是这个小程序使用text/tabwriter包来生成一个列整齐对齐和隔开的表格,像下面展示的这样。注意到*tabwriter.Writer是满足io.Writer接口的。它会收集每一片写向它的数据;它的Flush方法会格式化整个表格并且将它写向os.Stdout(标准输出)。

+
func printTracks(tracks []*Track) {
+	const format = "%v\t%v\t%v\t%v\t%v\t\n"
+	tw := new(tabwriter.Writer).Init(os.Stdout, 0, 8, 2, ' ', 0)
+	fmt.Fprintf(tw, format, "Title", "Artist", "Album", "Year", "Length")
+	fmt.Fprintf(tw, format, "-----", "------", "-----", "----", "------")
+	for _, t := range tracks {
+		fmt.Fprintf(tw, format, t.Title, t.Artist, t.Album, t.Year, t.Length)
+	}
+	tw.Flush() // calculate column widths and print table
+}
+
+

为了能按照Artist字段对播放列表进行排序,我们会像对StringSlice那样定义一个新的带有必须的Len,Less和Swap方法的切片类型。

+
type byArtist []*Track
+func (x byArtist) Len() int           { return len(x) }
+func (x byArtist) Less(i, j int) bool { return x[i].Artist < x[j].Artist }
+func (x byArtist) Swap(i, j int)      { x[i], x[j] = x[j], x[i] }
+
+

为了调用通用的排序程序,我们必须先将tracks转换为新的byArtist类型,它定义了具体的排序:

+
sort.Sort(byArtist(tracks))
+
+

在按照artist对这个切片进行排序后,printTrack的输出如下

+
Title       Artist          Album               Year Length
+-----       ------          -----               ---- ------
+Go Ahead    Alicia Keys     As I Am             2007 4m36s
+Go          Delilah         From the Roots Up   2012 3m38s
+Ready 2 Go  Martin Solveig  Smash               2011 4m24s
+Go          Moby            Moby                1992 3m37s
+
+

如果用户第二次请求“按照artist排序”,我们会对tracks进行逆向排序。然而我们不需要定义一个有颠倒Less方法的新类型byReverseArtist,因为sort包中提供了Reverse函数将排序顺序转换成逆序。

+
sort.Sort(sort.Reverse(byArtist(tracks)))
+
+

在按照artist对这个切片进行逆向排序后,printTrack的输出如下

+
Title       Artist          Album               Year Length
+-----       ------          -----               ---- ------
+Go          Moby            Moby                1992 3m37s
+Ready 2 Go  Martin Solveig  Smash               2011 4m24s
+Go          Delilah         From the Roots Up   2012 3m38s
+Go Ahead    Alicia Keys     As I Am             2007 4m36s
+
+

sort.Reverse函数值得进行更近一步的学习,因为它使用了(§6.3)章中的组合,这是一个重要的思路。sort包定义了一个不公开的struct类型reverse,它嵌入了一个sort.Interface。reverse的Less方法调用了内嵌的sort.Interface值的Less方法,但是通过交换索引的方式使排序结果变成逆序。

+
package sort
+
+type reverse struct{ Interface } // that is, sort.Interface
+
+func (r reverse) Less(i, j int) bool { return r.Interface.Less(j, i) }
+
+func Reverse(data Interface) Interface { return reverse{data} }
+
+

reverse的另外两个方法Len和Swap隐式地由原有内嵌的sort.Interface提供。因为reverse是一个不公开的类型,所以导出函数Reverse返回一个包含原有sort.Interface值的reverse类型实例。

+

为了可以按照不同的列进行排序,我们必须定义一个新的类型例如byYear:

+
type byYear []*Track
+func (x byYear) Len() int           { return len(x) }
+func (x byYear) Less(i, j int) bool { return x[i].Year < x[j].Year }
+func (x byYear) Swap(i, j int)      { x[i], x[j] = x[j], x[i] }
+
+

在使用sort.Sort(byYear(tracks))按照年对tracks进行排序后,printTrack展示了一个按时间先后顺序的列表:

+
Title       Artist          Album               Year Length
+-----       ------          -----               ---- ------
+Go          Moby            Moby                1992 3m37s
+Go Ahead    Alicia Keys     As I Am             2007 4m36s
+Ready 2 Go  Martin Solveig  Smash               2011 4m24s
+Go          Delilah         From the Roots Up   2012 3m38s
+
+

对于我们需要的每个切片元素类型和每个排序函数,我们需要定义一个新的sort.Interface实现。如你所见,Len和Swap方法对于所有的切片类型都有相同的定义。下个例子,具体的类型customSort会将一个切片和函数结合,使我们只需要写比较函数就可以定义一个新的排序。顺便说下,实现了sort.Interface的具体类型不一定是切片类型;customSort是一个结构体类型。

+
type customSort struct {
+	t    []*Track
+	less func(x, y *Track) bool
+}
+
+func (x customSort) Len() int           { return len(x.t) }
+func (x customSort) Less(i, j int) bool { return x.less(x.t[i], x.t[j]) }
+func (x customSort) Swap(i, j int)	{ x.t[i], x.t[j] = x.t[j], x.t[i] }
+
+

让我们定义一个多层的排序函数,它主要的排序键是标题,第二个键是年,第三个键是运行时间Length。下面是该排序的调用,其中这个排序使用了匿名排序函数:

+
sort.Sort(customSort{tracks, func(x, y *Track) bool {
+	if x.Title != y.Title {
+		return x.Title < y.Title
+	}
+	if x.Year != y.Year {
+		return x.Year < y.Year
+	}
+	if x.Length != y.Length {
+		return x.Length < y.Length
+	}
+	return false
+}})
+
+

这下面是排序的结果。注意到两个标题是“Go”的track按照标题排序是相同的顺序,但是在按照year排序上更久的那个track优先。

+
Title       Artist          Album               Year Length
+-----       ------          -----               ---- ------
+Go          Moby            Moby                1992 3m37s
+Go          Delilah         From the Roots Up   2012 3m38s
+Go Ahead    Alicia Keys     As I Am             2007 4m36s
+Ready 2 Go  Martin Solveig  Smash               2011 4m24s
+
+

尽管对长度为n的序列排序需要 O(n log n)次比较操作,检查一个序列是否已经有序至少需要n-1次比较。sort包中的IsSorted函数帮我们做这样的检查。像sort.Sort一样,它也使用sort.Interface对这个序列和它的排序函数进行抽象,但是它从不会调用Swap方法:这段代码示范了IntsAreSorted和Ints函数在IntSlice类型上的使用:

+
values := []int{3, 1, 4, 1}
+fmt.Println(sort.IntsAreSorted(values)) // "false"
+sort.Ints(values)
+fmt.Println(values)                     // "[1 1 3 4]"
+fmt.Println(sort.IntsAreSorted(values)) // "true"
+sort.Sort(sort.Reverse(sort.IntSlice(values)))
+fmt.Println(values)                     // "[4 3 1 1]"
+fmt.Println(sort.IntsAreSorted(values)) // "false"
+
+

为了使用方便,sort包为[]int、[]string和[]float64的正常排序提供了特定版本的函数和类型。对于其他类型,例如[]int64或者[]uint,尽管路径也很简单,还是依赖我们自己实现。

+

练习 7.8: 很多图形界面提供了一个有状态的多重排序表格插件:主要的排序键是最近一次点击过列头的列,第二个排序键是第二最近点击过列头的列,等等。定义一个sort.Interface的实现用在这样的表格中。比较这个实现方式和重复使用sort.Stable来排序的方式。

+

练习 7.9: 使用html/template包(§4.6)替代printTracks将tracks展示成一个HTML表格。将这个解决方案用在前一个练习中,让每次点击一个列的头部产生一个HTTP请求来排序这个表格。

+

练习 7.10: sort.Interface类型也可以适用在其它地方。编写一个IsPalindrome(s sort.Interface) bool函数表明序列s是否是回文序列,换句话说反向排序不会改变这个序列。假设如果!s.Less(i, j) && !s.Less(j, i)则索引i和j上的元素相等。

+

7.7. http.Handler接口

+

在第一章中,我们粗略的了解了怎么用net/http包去实现网络客户端(§1.5)和服务器(§1.7)。在这个小节中,我们会对那些基于http.Handler接口的服务器API做更进一步的学习:

+

net/http

+
package http
+
+type Handler interface {
+	ServeHTTP(w ResponseWriter, r *Request)
+}
+
+func ListenAndServe(address string, h Handler) error
+
+

ListenAndServe函数需要一个例如“localhost:8000”的服务器地址,和一个所有请求都可以分派的Handler接口实例。它会一直运行,直到这个服务因为一个错误而失败(或者启动失败),它的返回值一定是一个非空的错误。

+

想象一个电子商务网站,为了销售,将数据库中物品的价格映射成美元。下面这个程序可能是能想到的最简单的实现了。它将库存清单模型化为一个命名为database的map类型,我们给这个类型一个ServeHttp方法,这样它可以满足http.Handler接口。这个handler会遍历整个map并输出物品信息。

+

gopl.io/ch7/http1

+
func main() {
+	db := database{"shoes": 50, "socks": 5}
+	log.Fatal(http.ListenAndServe("localhost:8000", db))
+}
+
+type dollars float32
+
+func (d dollars) String() string { return fmt.Sprintf("$%.2f", d) }
+
+type database map[string]dollars
+
+func (db database) ServeHTTP(w http.ResponseWriter, req *http.Request) {
+	for item, price := range db {
+		fmt.Fprintf(w, "%s: %s\n", item, price)
+	}
+}
+
+

如果我们启动这个服务,

+
$ go build gopl.io/ch7/http1
+$ ./http1 &
+
+

然后用1.5节中的获取程序(如果你更喜欢可以使用web浏览器)来连接服务器,我们得到下面的输出:

+
$ go build gopl.io/ch1/fetch
+$ ./fetch http://localhost:8000
+shoes: $50.00
+socks: $5.00
+
+

目前为止,这个服务器不考虑URL,只能为每个请求列出它全部的库存清单。更真实的服务器会定义多个不同的URL,每一个都会触发一个不同的行为。让我们使用/list来调用已经存在的这个行为并且增加另一个/price调用表明单个货品的价格,像这样/price?item=socks来指定一个请求参数。

+

gopl.io/ch7/http2

+
func (db database) ServeHTTP(w http.ResponseWriter, req *http.Request) {
+	switch req.URL.Path {
+	case "/list":
+		for item, price := range db {
+			fmt.Fprintf(w, "%s: %s\n", item, price)
+		}
+	case "/price":
+		item := req.URL.Query().Get("item")
+		price, ok := db[item]
+		if !ok {
+			w.WriteHeader(http.StatusNotFound) // 404
+			fmt.Fprintf(w, "no such item: %q\n", item)
+			return
+		}
+		fmt.Fprintf(w, "%s\n", price)
+	default:
+		w.WriteHeader(http.StatusNotFound) // 404
+		fmt.Fprintf(w, "no such page: %s\n", req.URL)
+	}
+}
+
+

现在handler基于URL的路径部分(req.URL.Path)来决定执行什么逻辑。如果这个handler不能识别这个路径,它会通过调用w.WriteHeader(http.StatusNotFound)返回客户端一个HTTP错误;这个检查应该在向w写入任何值前完成。(顺便提一下,http.ResponseWriter是另一个接口。它在io.Writer上增加了发送HTTP相应头的方法。)等效地,我们可以使用实用的http.Error函数:

+
msg := fmt.Sprintf("no such page: %s\n", req.URL)
+http.Error(w, msg, http.StatusNotFound) // 404
+
+

/price的case会调用URL的Query方法来将HTTP请求参数解析为一个map,或者更准确地说一个net/url包中url.Values(§6.2.1)类型的多重映射。然后找到第一个item参数并查找它的价格。如果这个货品没有找到会返回一个错误。

+

这里是一个和新服务器会话的例子:

+
$ go build gopl.io/ch7/http2
+$ go build gopl.io/ch1/fetch
+$ ./http2 &
+$ ./fetch http://localhost:8000/list
+shoes: $50.00
+socks: $5.00
+$ ./fetch http://localhost:8000/price?item=socks
+$5.00
+$ ./fetch http://localhost:8000/price?item=shoes
+$50.00
+$ ./fetch http://localhost:8000/price?item=hat
+no such item: "hat"
+$ ./fetch http://localhost:8000/help
+no such page: /help
+
+

显然我们可以继续向ServeHTTP方法中添加case,但在一个实际的应用中,将每个case中的逻辑定义到一个分开的方法或函数中会很实用。此外,相近的URL可能需要相似的逻辑;例如几个图片文件可能有形如/images/*.png的URL。因为这些原因,net/http包提供了一个请求多路器ServeMux来简化URL和handlers的联系。一个ServeMux将一批http.Handler聚集到一个单一的http.Handler中。再一次,我们可以看到满足同一接口的不同类型是可替换的:web服务器将请求指派给任意的http.Handler +而不需要考虑它后面的具体类型。

+

对于更复杂的应用,一些ServeMux可以通过组合来处理更加错综复杂的路由需求。Go语言目前没有一个权威的web框架,就像Ruby语言有Rails和python有Django。这并不是说这样的框架不存在,而是Go语言标准库中的构建模块就已经非常灵活以至于这些框架都是不必要的。此外,尽管在一个项目早期使用框架是非常方便的,但是它们带来额外的复杂度会使长期的维护更加困难。

+

在下面的程序中,我们创建一个ServeMux并且使用它将URL和相应处理/list和/price操作的handler联系起来,这些操作逻辑都已经被分到不同的方法中。然后我们在调用ListenAndServe函数中使用ServeMux为主要的handler。

+

gopl.io/ch7/http3

+
func main() {
+	db := database{"shoes": 50, "socks": 5}
+	mux := http.NewServeMux()
+	mux.Handle("/list", http.HandlerFunc(db.list))
+	mux.Handle("/price", http.HandlerFunc(db.price))
+	log.Fatal(http.ListenAndServe("localhost:8000", mux))
+}
+
+type database map[string]dollars
+
+func (db database) list(w http.ResponseWriter, req *http.Request) {
+	for item, price := range db {
+		fmt.Fprintf(w, "%s: %s\n", item, price)
+	}
+}
+
+func (db database) price(w http.ResponseWriter, req *http.Request) {
+	item := req.URL.Query().Get("item")
+	price, ok := db[item]
+	if !ok {
+		w.WriteHeader(http.StatusNotFound) // 404
+		fmt.Fprintf(w, "no such item: %q\n", item)
+		return
+	}
+	fmt.Fprintf(w, "%s\n", price)
+}
+
+

让我们关注这两个注册到handlers上的调用。第一个db.list是一个方法值(§6.4),它是下面这个类型的值。

+
func(w http.ResponseWriter, req *http.Request)
+
+

也就是说db.list的调用会援引一个接收者是db的database.list方法。所以db.list是一个实现了handler类似行为的函数,但是因为它没有方法(理解:该方法没有它自己的方法),所以它不满足http.Handler接口并且不能直接传给mux.Handle。

+

语句http.HandlerFunc(db.list)是一个转换而非一个函数调用,因为http.HandlerFunc是一个类型。它有如下的定义:

+

net/http

+
package http
+
+type HandlerFunc func(w ResponseWriter, r *Request)
+
+func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
+	f(w, r)
+}
+
+

HandlerFunc显示了在Go语言接口机制中一些不同寻常的特点。这是一个实现了接口http.Handler的方法的函数类型。ServeHTTP方法的行为是调用了它的函数本身。因此HandlerFunc是一个让函数值满足一个接口的适配器,这里函数和这个接口仅有的方法有相同的函数签名。实际上,这个技巧让一个单一的类型例如database以多种方式满足http.Handler接口:一种通过它的list方法,一种通过它的price方法等等。

+

因为handler通过这种方式注册非常普遍,ServeMux有一个方便的HandleFunc方法,它帮我们简化handler注册代码成这样:

+

gopl.io/ch7/http3a

+
mux.HandleFunc("/list", db.list)
+mux.HandleFunc("/price", db.price)
+
+

从上面的代码很容易看出应该怎么构建一个程序:由两个不同的web服务器监听不同的端口,并且定义不同的URL将它们指派到不同的handler。我们只要构建另外一个ServeMux并且再调用一次ListenAndServe(可能并行的)。但是在大多数程序中,一个web服务器就足够了。此外,在一个应用程序的多个文件中定义HTTP handler也是非常典型的,如果它们必须全部都显式地注册到这个应用的ServeMux实例上会比较麻烦。

+

所以为了方便,net/http包提供了一个全局的ServeMux实例DefaultServerMux和包级别的http.Handle和http.HandleFunc函数。现在,为了使用DefaultServeMux作为服务器的主handler,我们不需要将它传给ListenAndServe函数;nil值就可以工作。

+

然后服务器的主函数可以简化成:

+

gopl.io/ch7/http4

+
func main() {
+	db := database{"shoes": 50, "socks": 5}
+	http.HandleFunc("/list", db.list)
+	http.HandleFunc("/price", db.price)
+	log.Fatal(http.ListenAndServe("localhost:8000", nil))
+}
+
+

最后,一个重要的提示:就像我们在1.7节中提到的,web服务器在一个新的协程中调用每一个handler,所以当handler获取其它协程或者这个handler本身的其它请求也可以访问到变量时,一定要使用预防措施,比如锁机制。我们后面的两章中将讲到并发相关的知识。

+

练习 7.11: 增加额外的handler让客户端可以创建,读取,更新和删除数据库记录。例如,一个形如 /update?item=socks&price=6 的请求会更新库存清单里一个货品的价格并且当这个货品不存在或价格无效时返回一个错误值。(注意:这个修改会引入变量同时更新的问题)

+

练习 7.12: 修改/list的handler让它把输出打印成一个HTML的表格而不是文本。html/template包(§4.6)可能会对你有帮助。

+

7.8. error接口

+

从本书的开始,我们就已经创建和使用过神秘的预定义error类型,而且没有解释它究竟是什么。实际上它就是interface类型,这个类型有一个返回错误信息的单一方法:

+
type error interface {
+	Error() string
+}
+
+

创建一个error最简单的方法就是调用errors.New函数,它会根据传入的错误信息返回一个新的error。整个errors包仅只有4行:

+
package errors
+
+func New(text string) error { return &errorString{text} }
+
+type errorString struct { text string }
+
+func (e *errorString) Error() string { return e.text }
+
+

承载errorString的类型是一个结构体而非一个字符串,这是为了保护它表示的错误避免粗心(或有意)的更新。并且因为是指针类型*errorString满足error接口而非errorString类型,所以每个New函数的调用都分配了一个独特的和其他错误不相同的实例。我们也不想要重要的error例如io.EOF和一个刚好有相同错误消息的error比较后相等。

+
fmt.Println(errors.New("EOF") == errors.New("EOF")) // "false"
+
+

调用errors.New函数是非常稀少的,因为有一个方便的封装函数fmt.Errorf,它还会处理字符串格式化。我们曾多次在第5章中用到它。

+
package fmt
+
+import "errors"
+
+func Errorf(format string, args ...interface{}) error {
+	return errors.New(Sprintf(format, args...))
+}
+
+

虽然*errorString可能是最简单的错误类型,但远非只有它一个。例如,syscall包提供了Go语言底层系统调用API。在多个平台上,它定义一个实现error接口的数字类型Errno,并且在Unix平台上,Errno的Error方法会从一个字符串表中查找错误消息,如下面展示的这样:

+
package syscall
+
+type Errno uintptr // operating system error code
+
+var errors = [...]string{
+	1:   "operation not permitted",   // EPERM
+	2:   "no such file or directory", // ENOENT
+	3:   "no such process",           // ESRCH
+	// ...
+}
+
+func (e Errno) Error() string {
+	if 0 <= int(e) && int(e) < len(errors) {
+		return errors[e]
+	}
+	return fmt.Sprintf("errno %d", e)
+}
+
+

下面的语句创建了一个持有Errno值为2的接口值,表示POSIX ENOENT状况:

+
var err error = syscall.Errno(2)
+fmt.Println(err.Error()) // "no such file or directory"
+fmt.Println(err)         // "no such file or directory"
+
+

err的值图形化的呈现在图7.6中。

+

+

Errno是一个系统调用错误的高效表示方式,它通过一个有限的集合进行描述,并且它满足标准的错误接口。我们会在第7.11节了解到其它满足这个接口的类型。

+

7.9. 示例: 表达式求值

+

在本节中,我们会构建一个简单算术表达式的求值器。我们将使用一个接口Expr来表示Go语言中任意的表达式。现在这个接口不需要有方法,但是我们后面会为它增加一些。

+
// An Expr is an arithmetic expression.
+type Expr interface{}
+
+

我们的表达式语言包括浮点数符号(小数点);二元操作符+,-,*, 和/;一元操作符-x和+x;调用pow(x,y),sin(x),和sqrt(x)的函数;例如x和pi的变量;当然也有括号和标准的优先级运算符。所有的值都是float64类型。这下面是一些表达式的例子:

+
sqrt(A / pi)
+pow(x, 3) + pow(y, 3)
+(F - 32) * 5 / 9
+
+

下面的五个具体类型表示了具体的表达式类型。Var类型表示对一个变量的引用。(我们很快会知道为什么它可以被输出。)literal类型表示一个浮点型常量。unary和binary类型表示有一到两个运算对象的运算符表达式,这些操作数可以是任意的Expr类型。call类型表示对一个函数的调用;我们限制它的fn字段只能是pow,sin或者sqrt。

+

gopl.io/ch7/eval

+
// A Var identifies a variable, e.g., x.
+type Var string
+
+// A literal is a numeric constant, e.g., 3.141.
+type literal float64
+
+// A unary represents a unary operator expression, e.g., -x.
+type unary struct {
+	op rune // one of '+', '-'
+	x  Expr
+}
+
+// A binary represents a binary operator expression, e.g., x+y.
+type binary struct {
+	op   rune // one of '+', '-', '*', '/'
+	x, y Expr
+}
+
+// A call represents a function call expression, e.g., sin(x).
+type call struct {
+	fn   string // one of "pow", "sin", "sqrt"
+	args []Expr
+}
+
+

为了计算一个包含变量的表达式,我们需要一个environment变量将变量的名字映射成对应的值:

+
type Env map[Var]float64
+
+

我们也需要每个表达式去定义一个Eval方法,这个方法会根据给定的environment变量返回表达式的值。因为每个表达式都必须提供这个方法,我们将它加入到Expr接口中。这个包只会对外公开Expr,Env,和Var类型。调用方不需要获取其它的表达式类型就可以使用这个求值器。

+
type Expr interface {
+	// Eval returns the value of this Expr in the environment env.
+	Eval(env Env) float64
+}
+
+

下面给大家展示一个具体的Eval方法。Var类型的这个方法对一个environment变量进行查找,如果这个变量没有在environment中定义过这个方法会返回一个零值,literal类型的这个方法简单的返回它真实的值。

+
func (v Var) Eval(env Env) float64 {
+	return env[v]
+}
+
+func (l literal) Eval(_ Env) float64 {
+	return float64(l)
+}
+
+

unary和binary的Eval方法会递归的计算它的运算对象,然后将运算符op作用到它们上。我们不将被零或无穷数除作为一个错误,因为它们都会产生一个固定的结果——无限。最后,call的这个方法会计算对于pow,sin,或者sqrt函数的参数值,然后调用对应在math包中的函数。

+
func (u unary) Eval(env Env) float64 {
+	switch u.op {
+	case '+':
+		return +u.x.Eval(env)
+	case '-':
+		return -u.x.Eval(env)
+	}
+	panic(fmt.Sprintf("unsupported unary operator: %q", u.op))
+}
+
+func (b binary) Eval(env Env) float64 {
+	switch b.op {
+	case '+':
+		return b.x.Eval(env) + b.y.Eval(env)
+	case '-':
+		return b.x.Eval(env) - b.y.Eval(env)
+	case '*':
+		return b.x.Eval(env) * b.y.Eval(env)
+	case '/':
+		return b.x.Eval(env) / b.y.Eval(env)
+	}
+	panic(fmt.Sprintf("unsupported binary operator: %q", b.op))
+}
+
+func (c call) Eval(env Env) float64 {
+	switch c.fn {
+	case "pow":
+		return math.Pow(c.args[0].Eval(env), c.args[1].Eval(env))
+	case "sin":
+		return math.Sin(c.args[0].Eval(env))
+	case "sqrt":
+		return math.Sqrt(c.args[0].Eval(env))
+	}
+	panic(fmt.Sprintf("unsupported function call: %s", c.fn))
+}
+
+

一些方法会失败。例如,一个call表达式可能有未知的函数或者错误的参数个数。用一个无效的运算符如!或者<去构建一个unary或者binary表达式也是可能会发生的(尽管下面提到的Parse函数不会这样做)。这些错误会让Eval方法panic。其它的错误,像计算一个没有在environment变量中出现过的Var,只会让Eval方法返回一个错误的结果。所有的这些错误都可以通过在计算前检查Expr来发现。这是我们接下来要讲的Check方法的工作,但是让我们先测试Eval方法。

+

下面的TestEval函数是对evaluator的一个测试。它使用了我们会在第11章讲解的testing包,但是现在知道调用t.Errof会报告一个错误就足够了。这个函数循环遍历一个表格中的输入,这个表格中定义了三个表达式和针对每个表达式不同的环境变量。第一个表达式根据给定圆的面积A计算它的半径,第二个表达式通过两个变量x和y计算两个立方体的体积之和,第三个表达式将华氏温度F转换成摄氏度。

+
func TestEval(t *testing.T) {
+	tests := []struct {
+		expr string
+		env  Env
+		want string
+	}{
+		{"sqrt(A / pi)", Env{"A": 87616, "pi": math.Pi}, "167"},
+		{"pow(x, 3) + pow(y, 3)", Env{"x": 12, "y": 1}, "1729"},
+		{"pow(x, 3) + pow(y, 3)", Env{"x": 9, "y": 10}, "1729"},
+		{"5 / 9 * (F - 32)", Env{"F": -40}, "-40"},
+		{"5 / 9 * (F - 32)", Env{"F": 32}, "0"},
+		{"5 / 9 * (F - 32)", Env{"F": 212}, "100"},
+	}
+	var prevExpr string
+	for _, test := range tests {
+		// Print expr only when it changes.
+		if test.expr != prevExpr {
+			fmt.Printf("\n%s\n", test.expr)
+			prevExpr = test.expr
+		}
+		expr, err := Parse(test.expr)
+		if err != nil {
+			t.Error(err) // parse error
+			continue
+		}
+		got := fmt.Sprintf("%.6g", expr.Eval(test.env))
+		fmt.Printf("\t%v => %s\n", test.env, got)
+		if got != test.want {
+			t.Errorf("%s.Eval() in %v = %q, want %q\n",
+			test.expr, test.env, got, test.want)
+		}
+	}
+}
+
+

对于表格中的每一条记录,这个测试会解析它的表达式然后在环境变量中计算它,输出结果。这里我们没有空间来展示Parse函数,但是如果你使用go get下载这个包你就可以看到这个函数。

+

go test(§11.1) 命令会运行一个包的测试用例:

+
$ go test -v gopl.io/ch7/eval
+
+

这个-v标识可以让我们看到测试用例打印的输出;正常情况下像这样一个成功的测试用例会阻止打印结果的输出。这里是测试用例里fmt.Printf语句的输出:

+
sqrt(A / pi)
+    map[A:87616 pi:3.141592653589793] => 167
+
+pow(x, 3) + pow(y, 3)
+    map[x:12 y:1] => 1729
+    map[x:9 y:10] => 1729
+
+5 / 9 * (F - 32)
+    map[F:-40] => -40
+    map[F:32] => 0
+    map[F:212] => 100
+
+

幸运的是目前为止所有的输入都是适合的格式,但是我们的运气不可能一直都有。甚至在解释型语言中,为了静态错误检查语法是非常常见的;静态错误就是不用运行程序就可以检测出来的错误。通过将静态检查和动态的部分分开,我们可以快速的检查错误并且对于多次检查只执行一次而不是每次表达式计算的时候都进行检查。

+

让我们往Expr接口中增加另一个方法。Check方法对一个表达式语义树检查出静态错误。我们马上会说明它的vars参数。

+
type Expr interface {
+	Eval(env Env) float64
+	// Check reports errors in this Expr and adds its Vars to the set.
+	Check(vars map[Var]bool) error
+}
+
+

具体的Check方法展示在下面。literal和Var类型的计算不可能失败,所以这些类型的Check方法会返回一个nil值。对于unary和binary的Check方法会首先检查操作符是否有效,然后递归的检查运算单元。相似地对于call的这个方法首先检查调用的函数是否已知并且有没有正确个数的参数,然后递归的检查每一个参数。

+
func (v Var) Check(vars map[Var]bool) error {
+	vars[v] = true
+	return nil
+}
+
+func (literal) Check(vars map[Var]bool) error {
+	return nil
+}
+
+func (u unary) Check(vars map[Var]bool) error {
+	if !strings.ContainsRune("+-", u.op) {
+		return fmt.Errorf("unexpected unary op %q", u.op)
+	}
+	return u.x.Check(vars)
+}
+
+func (b binary) Check(vars map[Var]bool) error {
+	if !strings.ContainsRune("+-*/", b.op) {
+		return fmt.Errorf("unexpected binary op %q", b.op)
+	}
+	if err := b.x.Check(vars); err != nil {
+		return err
+	}
+	return b.y.Check(vars)
+}
+
+func (c call) Check(vars map[Var]bool) error {
+	arity, ok := numParams[c.fn]
+	if !ok {
+		return fmt.Errorf("unknown function %q", c.fn)
+	}
+	if len(c.args) != arity {
+		return fmt.Errorf("call to %s has %d args, want %d",
+			c.fn, len(c.args), arity)
+	}
+	for _, arg := range c.args {
+		if err := arg.Check(vars); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+var numParams = map[string]int{"pow": 2, "sin": 1, "sqrt": 1}
+
+

我们在两个组中有选择地列出有问题的输入和它们得出的错误。Parse函数(这里没有出现)会报出一个语法错误和Check函数会报出语义错误。

+
x % 2               unexpected '%'
+math.Pi             unexpected '.'
+!true               unexpected '!'
+"hello"             unexpected '"'
+
+log(10)             unknown function "log"
+sqrt(1, 2)          call to sqrt has 2 args, want 1
+
+

Check方法的参数是一个Var类型的集合,这个集合聚集从表达式中找到的变量名。为了保证成功的计算,这些变量中的每一个都必须出现在环境变量中。从逻辑上讲,这个集合就是调用Check方法返回的结果,但是因为这个方法是递归调用的,所以对于Check方法,填充结果到一个作为参数传入的集合中会更加的方便。调用方在初始调用时必须提供一个空的集合。

+

在第3.2节中,我们绘制了一个在编译期才确定的函数f(x,y)。现在我们可以解析,检查和计算在字符串中的表达式,我们可以构建一个在运行时从客户端接收表达式的web应用并且它会绘制这个函数的表示的曲面。我们可以使用集合vars来检查表达式是否是一个只有两个变量x和y的函数——实际上是3个,因为我们为了方便会提供半径大小r。并且我们会在计算前使用Check方法拒绝有格式问题的表达式,这样我们就不会在下面函数的40000个计算过程(100x100个栅格,每一个有4个角)重复这些检查。

+

这个ParseAndCheck函数混合了解析和检查步骤的过程:

+

gopl.io/ch7/surface

+
import "gopl.io/ch7/eval"
+
+func parseAndCheck(s string) (eval.Expr, error) {
+	if s == "" {
+		return nil, fmt.Errorf("empty expression")
+	}
+	expr, err := eval.Parse(s)
+	if err != nil {
+		return nil, err
+	}
+	vars := make(map[eval.Var]bool)
+	if err := expr.Check(vars); err != nil {
+		return nil, err
+	}
+	for v := range vars {
+		if v != "x" && v != "y" && v != "r" {
+			return nil, fmt.Errorf("undefined variable: %s", v)
+		}
+	}
+	return expr, nil
+}
+
+

为了编写这个web应用,所有我们需要做的就是下面这个plot函数,这个函数有和http.HandlerFunc相似的签名:

+
func plot(w http.ResponseWriter, r *http.Request) {
+	r.ParseForm()
+	expr, err := parseAndCheck(r.Form.Get("expr"))
+	if err != nil {
+		http.Error(w, "bad expr: "+err.Error(), http.StatusBadRequest)
+		return
+	}
+	w.Header().Set("Content-Type", "image/svg+xml")
+	surface(w, func(x, y float64) float64 {
+		r := math.Hypot(x, y) // distance from (0,0)
+		return expr.Eval(eval.Env{"x": x, "y": y, "r": r})
+	})
+}
+
+

+

这个plot函数解析和检查在HTTP请求中指定的表达式并且用它来创建一个两个变量的匿名函数。这个匿名函数和来自原来surface-plotting程序中的固定函数f有相同的签名,但是它计算一个用户提供的表达式。环境变量中定义了x,y和半径r。最后plot调用surface函数,它就是gopl.io/ch3/surface中的主要函数,修改后它可以接受plot中的函数和输出io.Writer作为参数,而不是使用固定的函数f和os.Stdout。图7.7中显示了通过程序产生的3个曲面。

+

练习 7.13: 为Expr增加一个String方法来打印美观的语法树。当再一次解析的时候,检查它的结果是否生成相同的语法树。

+

练习 7.14: 定义一个新的满足Expr接口的具体类型并且提供一个新的操作例如对它运算单元中的最小值的计算。因为Parse函数不会创建这个新类型的实例,为了使用它你可能需要直接构造一个语法树(或者继承parser接口)。

+

练习 7.15: 编写一个从标准输入中读取一个单一表达式的程序,用户及时地提供对于任意变量的值,然后在结果环境变量中计算表达式的值。优雅的处理所有遇到的错误。

+

练习 7.16: 编写一个基于web的计算器程序。

+

7.10. 类型断言

+

类型断言是一个使用在接口值上的操作。语法上它看起来像x.(T)被称为断言类型,这里x表示一个接口的类型和T表示一个类型。一个类型断言检查它操作对象的动态类型是否和断言的类型匹配。

+

这里有两种可能。第一种,如果断言的类型T是一个具体类型,然后类型断言检查x的动态类型是否和T相同。如果这个检查成功了,类型断言的结果是x的动态值,当然它的类型是T。换句话说,具体类型的类型断言从它的操作对象中获得具体的值。如果检查失败,接下来这个操作会抛出panic。例如:

+
var w io.Writer
+w = os.Stdout
+f := w.(*os.File)      // success: f == os.Stdout
+c := w.(*bytes.Buffer) // panic: interface holds *os.File, not *bytes.Buffer
+
+

第二种,如果相反地断言的类型T是一个接口类型,然后类型断言检查是否x的动态类型满足T。如果这个检查成功了,动态值没有获取到;这个结果仍然是一个有相同动态类型和值部分的接口值,但是结果为类型T。换句话说,对一个接口类型的类型断言改变了类型的表述方式,改变了可以获取的方法集合(通常更大),但是它保留了接口值内部的动态类型和值的部分。

+

在下面的第一个类型断言后,w和rw都持有os.Stdout,因此它们都有一个动态类型*os.File,但是变量w是一个io.Writer类型,只对外公开了文件的Write方法,而rw变量还公开了它的Read方法。

+
var w io.Writer
+w = os.Stdout
+rw := w.(io.ReadWriter) // success: *os.File has both Read and Write
+w = new(ByteCounter)
+rw = w.(io.ReadWriter) // panic: *ByteCounter has no Read method
+
+

如果断言操作的对象是一个nil接口值,那么不论被断言的类型是什么这个类型断言都会失败。我们几乎不需要对一个更少限制性的接口类型(更少的方法集合)做断言,因为它表现的就像是赋值操作一样,除了对于nil接口值的情况。

+
w = rw             // io.ReadWriter is assignable to io.Writer
+w = rw.(io.Writer) // fails only if rw == nil
+
+

经常地,对一个接口值的动态类型我们是不确定的,并且我们更愿意去检验它是否是一些特定的类型。如果类型断言出现在一个预期有两个结果的赋值操作中,例如如下的定义,这个操作不会在失败的时候发生panic,但是替代地返回一个额外的第二个结果,这个结果是一个标识成功与否的布尔值:

+
var w io.Writer = os.Stdout
+f, ok := w.(*os.File)      // success:  ok, f == os.Stdout
+b, ok := w.(*bytes.Buffer) // failure: !ok, b == nil
+
+

第二个结果通常赋值给一个命名为ok的变量。如果这个操作失败了,那么ok就是false值,第一个结果等于被断言类型的零值,在这个例子中就是一个nil的*bytes.Buffer类型。

+

这个ok结果经常立即用于决定程序下面做什么。if语句的扩展格式让这个变的很简洁:

+
if f, ok := w.(*os.File); ok {
+	// ...use f...
+}
+
+

当类型断言的操作对象是一个变量,你有时会看见原来的变量名重用而不是声明一个新的本地变量名,这个重用的变量原来的值会被覆盖(理解:其实是声明了一个同名的新的本地变量,外层原来的w不会被改变),如下面这样:

+
if w, ok := w.(*os.File); ok {
+	// ...use w...
+}
+
+

7.11. 基于类型断言区别错误类型

+

思考在os包中文件操作返回的错误集合。I/O可以因为任何数量的原因失败,但是有三种经常的错误必须进行不同的处理:文件已经存在(对于创建操作),找不到文件(对于读取操作),和权限拒绝。os包中提供了三个帮助函数来对给定的错误值表示的失败进行分类:

+
package os
+
+func IsExist(err error) bool
+func IsNotExist(err error) bool
+func IsPermission(err error) bool
+
+

对这些判断的一个缺乏经验的实现可能会去检查错误消息是否包含了特定的子字符串,

+
func IsNotExist(err error) bool {
+	// NOTE: not robust!
+	return strings.Contains(err.Error(), "file does not exist")
+}
+
+

但是处理I/O错误的逻辑可能一个和另一个平台非常的不同,所以这种方案并不健壮,并且对相同的失败可能会报出各种不同的错误消息。在测试的过程中,通过检查错误消息的子字符串来保证特定的函数以期望的方式失败是非常有用的,但对于线上的代码是不够的。

+

一个更可靠的方式是使用一个专门的类型来描述结构化的错误。os包中定义了一个PathError类型来描述在文件路径操作中涉及到的失败,像Open或者Delete操作;并且定义了一个叫LinkError的变体来描述涉及到两个文件路径的操作,像Symlink和Rename。这下面是os.PathError:

+
package os
+
+// PathError records an error and the operation and file path that caused it.
+type PathError struct {
+	Op   string
+	Path string
+	Err  error
+}
+
+func (e *PathError) Error() string {
+	return e.Op + " " + e.Path + ": " + e.Err.Error()
+}
+
+

大多数调用方都不知道PathError并且通过调用错误本身的Error方法来统一处理所有的错误。尽管PathError的Error方法简单地把这些字段连接起来生成错误消息,PathError的结构保护了内部的错误组件。调用方需要使用类型断言来检测错误的具体类型以便将一种失败和另一种区分开;具体的类型可以比字符串提供更多的细节。

+
_, err := os.Open("/no/such/file")
+fmt.Println(err) // "open /no/such/file: No such file or directory"
+fmt.Printf("%#v\n", err)
+// Output:
+// &os.PathError{Op:"open", Path:"/no/such/file", Err:0x2}
+
+

这就是三个帮助函数是怎么工作的。例如下面展示的IsNotExist,它会报出是否一个错误和syscall.ENOENT(§7.8)或者和有名的错误os.ErrNotExist相等(可以在§5.4.2中找到io.EOF);或者是一个*PathError,它内部的错误是syscall.ENOENT和os.ErrNotExist其中之一。

+
import (
+	"errors"
+	"syscall"
+)
+
+var ErrNotExist = errors.New("file does not exist")
+
+// IsNotExist returns a boolean indicating whether the error is known to
+// report that a file or directory does not exist. It is satisfied by
+// ErrNotExist as well as some syscall errors.
+func IsNotExist(err error) bool {
+	if pe, ok := err.(*PathError); ok {
+		err = pe.Err
+	}
+	return err == syscall.ENOENT || err == ErrNotExist
+}
+
+

下面这里是它的实际使用:

+
_, err := os.Open("/no/such/file")
+fmt.Println(os.IsNotExist(err)) // "true"
+
+

如果错误消息结合成一个更大的字符串,当然PathError的结构就不再为人所知,例如通过一个对fmt.Errorf函数的调用。区别错误通常必须在失败操作后,错误传回调用者前进行。

+

7.12. 通过类型断言询问行为

+

下面这段逻辑和net/http包中web服务器负责写入HTTP头字段(例如:"Content-type:text/html")的部分相似。io.Writer接口类型的变量w代表HTTP响应;写入它的字节最终被发送到某个人的web浏览器上。

+
func writeHeader(w io.Writer, contentType string) error {
+	if _, err := w.Write([]byte("Content-Type: ")); err != nil {
+		return err
+	}
+	if _, err := w.Write([]byte(contentType)); err != nil {
+		return err
+	}
+	// ...
+}
+
+

因为Write方法需要传入一个byte切片而我们希望写入的值是一个字符串,所以我们需要使用[]byte(...)进行转换。这个转换分配内存并且做一个拷贝,但是这个拷贝在转换后几乎立马就被丢弃掉。让我们假装这是一个web服务器的核心部分并且我们的性能分析表示这个内存分配使服务器的速度变慢。这里我们可以避免掉内存分配么?

+

这个io.Writer接口告诉我们关于w持有的具体类型的唯一东西:就是可以向它写入字节切片。如果我们回顾net/http包中的内幕,我们知道在这个程序中的w变量持有的动态类型也有一个允许字符串高效写入的WriteString方法;这个方法会避免去分配一个临时的拷贝。(这可能像在黑夜中射击一样,但是许多满足io.Writer接口的重要类型同时也有WriteString方法,包括*bytes.Buffer*os.File*bufio.Writer。)

+

我们不能对任意io.Writer类型的变量w,假设它也拥有WriteString方法。但是我们可以定义一个只有这个方法的新接口并且使用类型断言来检测是否w的动态类型满足这个新接口。

+
// writeString writes s to w.
+// If w has a WriteString method, it is invoked instead of w.Write.
+func writeString(w io.Writer, s string) (n int, err error) {
+	type stringWriter interface {
+		WriteString(string) (n int, err error)
+	}
+	if sw, ok := w.(stringWriter); ok {
+		return sw.WriteString(s) // avoid a copy
+	}
+	return w.Write([]byte(s)) // allocate temporary copy
+}
+
+func writeHeader(w io.Writer, contentType string) error {
+	if _, err := writeString(w, "Content-Type: "); err != nil {
+		return err
+	}
+	if _, err := writeString(w, contentType); err != nil {
+		return err
+	}
+	// ...
+}
+
+

为了避免重复定义,我们将这个检查移入到一个实用工具函数writeString中,但是它太有用了以致于标准库将它作为io.WriteString函数提供。这是向一个io.Writer接口写入字符串的推荐方法。

+

这个例子的神奇之处在于,没有定义了WriteString方法的标准接口,也没有指定它是一个所需行为的标准接口。一个具体类型只会通过它的方法决定它是否满足stringWriter接口,而不是任何它和这个接口类型所表达的关系。它的意思就是上面的技术依赖于一个假设,这个假设就是:如果一个类型满足下面的这个接口,然后WriteString(s)方法就必须和Write([]byte(s))有相同的效果。

+
interface {
+	io.Writer
+	WriteString(s string) (n int, err error)
+}
+
+

尽管io.WriteString实施了这个假设,但是调用它的函数极少可能会去实施类似的假设。定义一个特定类型的方法隐式地获取了对特定行为的协约。对于Go语言的新手,特别是那些来自有强类型语言使用背景的新手,可能会发现它缺乏显式的意图令人感到混乱,但是在实战的过程中这几乎不是一个问题。除了空接口interface{},接口类型很少意外巧合地被实现。

+

上面的writeString函数使用一个类型断言来获知一个普遍接口类型的值是否满足一个更加具体的接口类型;并且如果满足,它会使用这个更具体接口的行为。这个技术可以被很好的使用,不论这个被询问的接口是一个标准如io.ReadWriter,或者用户定义的如stringWriter接口。

+

这也是fmt.Fprintf函数怎么从其它所有值中区分满足error或者fmt.Stringer接口的值。在fmt.Fprintf内部,有一个将单个操作对象转换成一个字符串的步骤,像下面这样:

+
package fmt
+
+func formatOneValue(x interface{}) string {
+	if err, ok := x.(error); ok {
+		return err.Error()
+	}
+	if str, ok := x.(Stringer); ok {
+		return str.String()
+	}
+	// ...all other types...
+}
+
+

如果x满足这两个接口类型中的一个,具体满足的接口决定对值的格式化方式。如果都不满足,默认的case或多或少会统一地使用反射来处理所有的其它类型;我们可以在第12章知道具体是怎么实现的。

+

再一次的,它假设任何有String方法的类型都满足fmt.Stringer中约定的行为,这个行为会返回一个适合打印的字符串。

+

7.13. 类型分支

+

接口被以两种不同的方式使用。在第一个方式中,以io.Reader,io.Writer,fmt.Stringer,sort.Interface,http.Handler和error为典型,一个接口的方法表达了实现这个接口的具体类型间的相似性,但是隐藏了代码的细节和这些具体类型本身的操作。重点在于方法上,而不是具体的类型上。

+

第二个方式是利用一个接口值可以持有各种具体类型值的能力,将这个接口认为是这些类型的联合。类型断言用来动态地区别这些类型,使得对每一种情况都不一样。在这个方式中,重点在于具体的类型满足这个接口,而不在于接口的方法(如果它确实有一些的话),并且没有任何的信息隐藏。我们将以这种方式使用的接口描述为discriminated unions(可辨识联合)。

+

如果你熟悉面向对象编程,你可能会将这两种方式当作是subtype polymorphism(子类型多态)和 ad hoc polymorphism(非参数多态),但是你不需要去记住这些术语。对于本章剩下的部分,我们将会呈现一些第二种方式的例子。

+

和其它那些语言一样,Go语言查询一个SQL数据库的API会干净地将查询中固定的部分和变化的部分分开。一个调用的例子可能看起来像这样:

+
import "database/sql"
+
+func listTracks(db sql.DB, artist string, minYear, maxYear int) {
+	result, err := db.Exec(
+		"SELECT * FROM tracks WHERE artist = ? AND ? <= year AND year <= ?",
+		artist, minYear, maxYear)
+	// ...
+}
+
+

Exec方法使用SQL字面量替换在查询字符串中的每个'?';SQL字面量表示相应参数的值,它有可能是一个布尔值,一个数字,一个字符串,或者nil空值。用这种方式构造查询可以帮助避免SQL注入攻击;这种攻击就是对手可以通过利用输入内容中不正确的引号来控制查询语句。在Exec函数内部,我们可能会找到像下面这样的一个函数,它会将每一个参数值转换成它的SQL字面量符号。

+
func sqlQuote(x interface{}) string {
+	if x == nil {
+		return "NULL"
+	} else if _, ok := x.(int); ok {
+		return fmt.Sprintf("%d", x)
+	} else if _, ok := x.(uint); ok {
+		return fmt.Sprintf("%d", x)
+	} else if b, ok := x.(bool); ok {
+		if b {
+			return "TRUE"
+		}
+		return "FALSE"
+	} else if s, ok := x.(string); ok {
+		return sqlQuoteString(s) // (not shown)
+	} else {
+		panic(fmt.Sprintf("unexpected type %T: %v", x, x))
+	}
+}
+
+

switch语句可以简化if-else链,如果这个if-else链对一连串值做相等测试。一个相似的type switch(类型分支)可以简化类型断言的if-else链。

+

在最简单的形式中,一个类型分支像普通的switch语句一样,它的运算对象是x.(type)——它使用了关键词字面量type——并且每个case有一到多个类型。一个类型分支基于这个接口值的动态类型使一个多路分支有效。这个nil的case和if x == nil匹配,并且这个default的case和如果其它case都不匹配的情况匹配。一个对sqlQuote的类型分支可能会有这些case:

+
switch x.(type) {
+case nil:       // ...
+case int, uint: // ...
+case bool:      // ...
+case string:    // ...
+default:        // ...
+}
+
+

和(§1.8)中的普通switch语句一样,每一个case会被顺序的进行考虑,并且当一个匹配找到时,这个case中的内容会被执行。当一个或多个case类型是接口时,case的顺序就会变得很重要,因为可能会有两个case同时匹配的情况。default case相对其它case的位置是无所谓的。它不会允许落空发生。

+

注意到在原来的函数中,对于bool和string情况的逻辑需要通过类型断言访问提取的值。因为这个做法很典型,类型分支语句有一个扩展的形式,它可以将提取的值绑定到一个在每个case范围内都有效的新变量。

+
switch x := x.(type) { /* ... */ }
+
+

这里我们已经将新的变量也命名为x;和类型断言一样,重用变量名是很常见的。和一个switch语句相似地,一个类型分支隐式的创建了一个词法块,因此新变量x的定义不会和外面块中的x变量冲突。每一个case也会隐式的创建一个单独的词法块。

+

使用类型分支的扩展形式来重写sqlQuote函数会让这个函数更加的清晰:

+
func sqlQuote(x interface{}) string {
+	switch x := x.(type) {
+	case nil:
+		return "NULL"
+	case int, uint:
+		return fmt.Sprintf("%d", x) // x has type interface{} here.
+	case bool:
+		if x {
+			return "TRUE"
+		}
+		return "FALSE"
+	case string:
+		return sqlQuoteString(x) // (not shown)
+	default:
+		panic(fmt.Sprintf("unexpected type %T: %v", x, x))
+	}
+}
+
+

在这个版本的函数中,在每个单一类型的case内部,变量x和这个case的类型相同。例如,变量x在bool的case中是bool类型和string的case中是string类型。在所有其它的情况中,变量x是switch运算对象的类型(接口);在这个例子中运算对象是一个interface{}。当多个case需要相同的操作时,比如int和uint的情况,类型分支可以很容易的合并这些情况。

+

尽管sqlQuote接受一个任意类型的参数,但是这个函数只会在它的参数匹配类型分支中的一个case时运行到结束;其它情况的它会panic出“unexpected type”消息。虽然x的类型是interface{},但是我们把它认为是一个int,uint,bool,string,和nil值的discriminated union(可识别联合)

+

7.14. 示例: 基于标记的XML解码

+

第4.5章节展示了如何使用encoding/json包中的Marshal和Unmarshal函数来将JSON文档转换成Go语言的数据结构。encoding/xml包提供了一个相似的API。当我们想构造一个文档树的表示时使用encoding/xml包会很方便,但是对于很多程序并不是必须的。encoding/xml包也提供了一个更低层的基于标记的API用于XML解码。在基于标记的样式中,解析器消费输入并产生一个标记流;四个主要的标记类型-StartElement,EndElement,CharData,和Comment-每一个都是encoding/xml包中的具体类型。每一个对(*xml.Decoder).Token的调用都返回一个标记。

+

这里显示的是和这个API相关的部分:

+

encoding/xml

+
package xml
+
+type Name struct {
+	Local string // e.g., "Title" or "id"
+}
+
+type Attr struct { // e.g., name="value"
+	Name  Name
+	Value string
+}
+
+// A Token includes StartElement, EndElement, CharData,
+// and Comment, plus a few esoteric types (not shown).
+type Token interface{}
+type StartElement struct { // e.g., <name>
+    Name Name
+    Attr []Attr
+}
+type EndElement struct { Name Name } // e.g., </name>
+type CharData []byte                 // e.g., <p>CharData</p>
+type Comment []byte                  // e.g., <!-- Comment -->
+
+type Decoder struct{ /* ... */ }
+func NewDecoder(io.Reader) *Decoder
+func (*Decoder) Token() (Token, error) // returns next Token in sequence
+
+

这个没有方法的Token接口也是一个可识别联合的例子。传统的接口如io.Reader的目的是隐藏满足它的具体类型的细节,这样就可以创造出新的实现:在这个实现中每个具体类型都被统一地对待。相反,满足可识别联合的具体类型的集合被设计为确定和暴露,而不是隐藏。可识别联合的类型几乎没有方法,操作它们的函数使用一个类型分支的case集合来进行表述,这个case集合中每一个case都有不同的逻辑。

+

下面的xmlselect程序获取和打印在一个XML文档树中确定的元素下找到的文本。使用上面的API,它可以在输入上一次完成它的工作而从来不要实例化这个文档树。

+

gopl.io/ch7/xmlselect

+
// Xmlselect prints the text of selected elements of an XML document.
+package main
+
+import (
+	"encoding/xml"
+	"fmt"
+	"io"
+	"os"
+	"strings"
+)
+
+func main() {
+	dec := xml.NewDecoder(os.Stdin)
+	var stack []string // stack of element names
+	for {
+		tok, err := dec.Token()
+		if err == io.EOF {
+			break
+		} else if err != nil {
+			fmt.Fprintf(os.Stderr, "xmlselect: %v\n", err)
+			os.Exit(1)
+		}
+		switch tok := tok.(type) {
+		case xml.StartElement:
+			stack = append(stack, tok.Name.Local) // push
+		case xml.EndElement:
+			stack = stack[:len(stack)-1] // pop
+		case xml.CharData:
+			if containsAll(stack, os.Args[1:]) {
+				fmt.Printf("%s: %s\n", strings.Join(stack, " "), tok)
+			}
+		}
+	}
+}
+
+// containsAll reports whether x contains the elements of y, in order.
+func containsAll(x, y []string) bool {
+	for len(y) <= len(x) {
+		if len(y) == 0 {
+			return true
+		}
+		if x[0] == y[0] {
+			y = y[1:]
+		}
+		x = x[1:]
+	}
+	return false
+}
+
+

main函数中的循环每遇到一个StartElement时,它把这个元素的名称压到一个栈里,并且每次遇到EndElement时,它将名称从这个栈中推出。这个API保证了StartElement和EndElement的序列可以被完全的匹配,甚至在一个糟糕的文档格式中。注释会被忽略。当xmlselect遇到一个CharData时,只有当栈中有序地包含所有通过命令行参数传入的元素名称时,它才会输出相应的文本。

+

下面的命令打印出任意出现在两层div元素下的h2元素的文本。它的输入是XML的说明文档,并且它自己就是XML文档格式的。

+
$ go build gopl.io/ch1/fetch
+$ ./fetch http://www.w3.org/TR/2006/REC-xml11-20060816 |
+    ./xmlselect div div h2
+html body div div h2: 1 Introduction
+html body div div h2: 2 Documents
+html body div div h2: 3 Logical Structures
+html body div div h2: 4 Physical Structures
+html body div div h2: 5 Conformance
+html body div div h2: 6 Notation
+html body div div h2: A References
+html body div div h2: B Definitions for Character Normalization
+...
+
+

练习 7.17: 扩展xmlselect程序以便让元素不仅可以通过名称选择,也可以通过它们CSS风格的属性进行选择。例如一个像这样

+
<div id="page" class="wide">
+
+

的元素可以通过匹配id或者class,同时还有它的名称来进行选择。

+

练习 7.18: 使用基于标记的解码API,编写一个可以读取任意XML文档并构造这个文档所代表的通用节点树的程序。节点有两种类型:CharData节点表示文本字符串,和 Element节点表示被命名的元素和它们的属性。每一个元素节点有一个子节点的切片。

+

你可能发现下面的定义会对你有帮助。

+
import "encoding/xml"
+
+type Node interface{} // CharData or *Element
+
+type CharData string
+
+type Element struct {
+	Type     xml.Name
+	Attr     []xml.Attr
+	Children []Node
+}
+
+

7.15. 一些建议

+

当设计一个新的包时,新手Go程序员总是先创建一套接口,然后再定义一些满足它们的具体类型。这种方式的结果就是有很多的接口,它们中的每一个仅只有一个实现。不要再这么做了。这种接口是不必要的抽象;它们也有一个运行时损耗。你可以使用导出机制(§6.6)来限制一个类型的方法或一个结构体的字段是否在包外可见。接口只有当有两个或两个以上的具体类型必须以相同的方式进行处理时才需要。

+

当一个接口只被一个单一的具体类型实现时有一个例外,就是由于它的依赖,这个具体类型不能和这个接口存在在一个相同的包中。这种情况下,一个接口是解耦这两个包的一个好方式。

+

因为在Go语言中只有当两个或更多的类型实现一个接口时才使用接口,它们必定会从任意特定的实现细节中抽象出来。结果就是有更少和更简单方法的更小的接口(经常和io.Writer或 fmt.Stringer一样只有一个)。当新的类型出现时,小的接口更容易满足。对于接口设计的一个好的标准就是 ask only for what you need(只考虑你需要的东西)

+

我们完成了对方法和接口的学习过程。Go语言对面向对象风格的编程支持良好,但这并不意味着你只能使用这一风格。不是任何事物都需要被当做一个对象;独立的函数有它们自己的用处,未封装的数据类型也是这样。观察一下,在本书前五章的例子中像input.Scan这样的方法被调用不超过二十次,与之相反的是普遍调用的函数如fmt.Printf。

+

第8章 Goroutines和Channels

+

并发程序指同时进行多个任务的程序,随着硬件的发展,并发程序变得越来越重要。Web服务器会一次处理成千上万的请求。平板电脑和手机app在渲染用户画面同时还会后台执行各种计算任务和网络请求。即使是传统的批处理问题——读取数据、计算、写输出,现在也会用并发来隐藏掉I/O的操作延迟以充分利用现代计算机设备的多个核心。计算机的性能每年都在以非线性的速度增长。

+

Go语言中的并发程序可以用两种手段来实现。本章讲解goroutine和channel,其支持“顺序通信进程”(communicating sequential processes)或被简称为CSP。CSP是一种现代的并发编程模型,在这种编程模型中值会在不同的运行实例(goroutine)中传递,尽管大多数情况下仍然是被限制在单一实例中。第9章覆盖更为传统的并发模型:多线程共享内存,如果你在其它的主流语言中写过并发程序的话可能会更熟悉一些。第9章也会深入介绍一些并发程序带来的风险和陷阱。

+

尽管Go对并发的支持是众多强力特性之一,但跟踪调试并发程序还是很困难,在线性程序中形成的直觉往往还会使我们误入歧途。如果这是读者第一次接触并发,推荐稍微多花一些时间来思考这两个章节中的样例。

+

8.1. Goroutines

+

在Go语言中,每一个并发的执行单元叫作一个goroutine。设想这里的一个程序有两个函数,一个函数做计算,另一个输出结果,假设两个函数没有相互之间的调用关系。一个线性的程序会先调用其中的一个函数,然后再调用另一个。如果程序中包含多个goroutine,对两个函数的调用则可能发生在同一时刻。马上就会看到这样的一个程序。

+

如果你使用过操作系统或者其它语言提供的线程,那么你可以简单地把goroutine类比作一个线程,这样你就可以写出一些正确的程序了。goroutine和线程的本质区别会在9.8节中讲。

+

当一个程序启动时,其主函数即在一个单独的goroutine中运行,我们叫它main goroutine。新的goroutine会用go语句来创建。在语法上,go语句是一个普通的函数或方法调用前加上关键字go。go语句会使其语句中的函数在一个新创建的goroutine中运行。而go语句本身会迅速地完成。

+
f()    // call f(); wait for it to return
+go f() // create a new goroutine that calls f(); don't wait
+
+

下面的例子,main goroutine将计算菲波那契数列的第45个元素值。由于计算函数使用低效的递归,所以会运行相当长时间,在此期间我们想让用户看到一个可见的标识来表明程序依然在正常运行,所以来做一个动画的小图标:

+

gopl.io/ch8/spinner

+
func main() {
+	go spinner(100 * time.Millisecond)
+	const n = 45
+	fibN := fib(n) // slow
+	fmt.Printf("\rFibonacci(%d) = %d\n", n, fibN)
+}
+
+func spinner(delay time.Duration) {
+	for {
+		for _, r := range `-\|/` {
+			fmt.Printf("\r%c", r)
+			time.Sleep(delay)
+		}
+	}
+}
+
+func fib(x int) int {
+	if x < 2 {
+		return x
+	}
+	return fib(x-1) + fib(x-2)
+}
+
+

动画显示了几秒之后,fib(45)的调用成功地返回,并且打印结果:

+
Fibonacci(45) = 1134903170
+
+

然后主函数返回。主函数返回时,所有的goroutine都会被直接打断,程序退出。除了从主函数退出或者直接终止程序之外,没有其它的编程方法能够让一个goroutine来打断另一个的执行,但是之后可以看到一种方式来实现这个目的,通过goroutine之间的通信来让一个goroutine请求其它的goroutine,并让被请求的goroutine自行结束执行。

+

留意一下这里的两个独立的单元是如何进行组合的,spinning和菲波那契的计算。分别在独立的函数中,但两个函数会同时执行。

+

8.2. 示例: 并发的Clock服务

+

网络编程是并发大显身手的一个领域,由于服务器是最典型的需要同时处理很多连接的程序,这些连接一般来自于彼此独立的客户端。在本小节中,我们会讲解go语言的net包,这个包提供编写一个网络客户端或者服务器程序的基本组件,无论两者间通信是使用TCP、UDP或者Unix domain sockets。在第一章中我们使用过的net/http包里的方法,也算是net包的一部分。

+

我们的第一个例子是一个顺序执行的时钟服务器,它会每隔一秒钟将当前时间写到客户端:

+

gopl.io/ch8/clock1

+
// Clock1 is a TCP server that periodically writes the time.
+package main
+
+import (
+	"io"
+	"log"
+	"net"
+	"time"
+)
+
+func main() {
+	listener, err := net.Listen("tcp", "localhost:8000")
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	for {
+		conn, err := listener.Accept()
+		if err != nil {
+			log.Print(err) // e.g., connection aborted
+			continue
+		}
+		handleConn(conn) // handle one connection at a time
+	}
+}
+
+func handleConn(c net.Conn) {
+	defer c.Close()
+	for {
+		_, err := io.WriteString(c, time.Now().Format("15:04:05\n"))
+		if err != nil {
+			return // e.g., client disconnected
+		}
+		time.Sleep(1 * time.Second)
+	}
+}
+
+
+

Listen函数创建了一个net.Listener的对象,这个对象会监听一个网络端口上到来的连接,在这个例子里我们用的是TCP的localhost:8000端口。listener对象的Accept方法会直接阻塞,直到一个新的连接被创建,然后会返回一个net.Conn对象来表示这个连接。

+

handleConn函数会处理一个完整的客户端连接。在一个for死循环中,用time.Now()获取当前时刻,然后写到客户端。由于net.Conn实现了io.Writer接口,我们可以直接向其写入内容。这个死循环会一直执行,直到写入失败。最可能的原因是客户端主动断开连接。这种情况下handleConn函数会用defer调用关闭服务器侧的连接,然后返回到主函数,继续等待下一个连接请求。

+

time.Time.Format方法提供了一种格式化日期和时间信息的方式。它的参数是一个格式化模板,标识如何来格式化时间,而这个格式化模板限定为Mon Jan 2 03:04:05PM 2006 UTC-0700。有8个部分(周几、月份、一个月的第几天……)。可以以任意的形式来组合前面这个模板;出现在模板中的部分会作为参考来对时间格式进行输出。在上面的例子中我们只用到了小时、分钟和秒。time包里定义了很多标准时间格式,比如time.RFC1123。在进行格式化的逆向操作time.Parse时,也会用到同样的策略。(译注:这是go语言和其它语言相比比较奇葩的一个地方。你需要记住格式化字符串是1月2日下午3点4分5秒零六年UTC-0700,而不像其它语言那样Y-m-d H:i:s一样,当然了这里可以用1234567的方式来记忆,倒是也不麻烦。)

+

为了连接例子里的服务器,我们需要一个客户端程序,比如netcat这个工具(nc命令),这个工具可以用来执行网络连接操作。

+
$ go build gopl.io/ch8/clock1
+$ ./clock1 &
+$ nc localhost 8000
+13:58:54
+13:58:55
+13:58:56
+13:58:57
+^C
+
+

客户端将服务器发来的时间显示了出来,我们用Control+C来中断客户端的执行,在Unix系统上,你会看到^C这样的响应。如果你的系统没有装nc这个工具,你可以用telnet来实现同样的效果,或者也可以用我们下面的这个用go写的简单的telnet程序,用net.Dial就可以简单地创建一个TCP连接:

+

gopl.io/ch8/netcat1

+
// Netcat1 is a read-only TCP client.
+package main
+
+import (
+	"io"
+	"log"
+	"net"
+	"os"
+)
+
+func main() {
+	conn, err := net.Dial("tcp", "localhost:8000")
+	if err != nil {
+		log.Fatal(err)
+	}
+	defer conn.Close()
+	mustCopy(os.Stdout, conn)
+}
+
+func mustCopy(dst io.Writer, src io.Reader) {
+	if _, err := io.Copy(dst, src); err != nil {
+		log.Fatal(err)
+	}
+}
+
+

这个程序会从连接中读取数据,并将读到的内容写到标准输出中,直到遇到end of file的条件或者发生错误。mustCopy这个函数我们在本节的几个例子中都会用到。让我们同时运行两个客户端来进行一个测试,这里可以开两个终端窗口,下面左边的是其中的一个的输出,右边的是另一个的输出:

+
$ go build gopl.io/ch8/netcat1
+$ ./netcat1
+13:58:54                               $ ./netcat1
+13:58:55
+13:58:56
+^C
+                                       13:58:57
+                                       13:58:58
+                                       13:58:59
+                                       ^C
+$ killall clock1
+
+

killall命令是一个Unix命令行工具,可以用给定的进程名来杀掉所有名字匹配的进程。

+

第二个客户端必须等待第一个客户端完成工作,这样服务端才能继续向后执行;因为我们这里的服务器程序同一时间只能处理一个客户端连接。我们这里对服务端程序做一点小改动,使其支持并发:在handleConn函数调用的地方增加go关键字,让每一次handleConn的调用都进入一个独立的goroutine。

+

gopl.io/ch8/clock2

+
for {
+	conn, err := listener.Accept()
+	if err != nil {
+		log.Print(err) // e.g., connection aborted
+		continue
+	}
+	go handleConn(conn) // handle connections concurrently
+}
+
+
+

现在多个客户端可以同时接收到时间了:

+
$ go build gopl.io/ch8/clock2
+$ ./clock2 &
+$ go build gopl.io/ch8/netcat1
+$ ./netcat1
+14:02:54                               $ ./netcat1
+14:02:55                               14:02:55
+14:02:56                               14:02:56
+14:02:57                               ^C
+14:02:58
+14:02:59                               $ ./netcat1
+14:03:00                               14:03:00
+14:03:01                               14:03:01
+^C                                     14:03:02
+                                       ^C
+$ killall clock2
+
+

练习 8.1: 修改clock2来支持传入参数作为端口号,然后写一个clockwall的程序,这个程序可以同时与多个clock服务器通信,从多个服务器中读取时间,并且在一个表格中一次显示所有服务器传回的结果,类似于你在某些办公室里看到的时钟墙。如果你有地理学上分布式的服务器可以用的话,让这些服务器跑在不同的机器上面;或者在同一台机器上跑多个不同的实例,这些实例监听不同的端口,假装自己在不同的时区。像下面这样:

+
$ TZ=US/Eastern    ./clock2 -port 8010 &
+$ TZ=Asia/Tokyo    ./clock2 -port 8020 &
+$ TZ=Europe/London ./clock2 -port 8030 &
+$ clockwall NewYork=localhost:8010 Tokyo=localhost:8020 London=localhost:8030
+
+

练习 8.2: 实现一个并发FTP服务器。服务器应该解析客户端发来的一些命令,比如cd命令来切换目录,ls来列出目录内文件,get和send来传输文件,close来关闭连接。你可以用标准的ftp命令来作为客户端,或者也可以自己实现一个。

+

8.3. 示例: 并发的Echo服务

+

clock服务器每一个连接都会起一个goroutine。在本节中我们会创建一个echo服务器,这个服务在每个连接中会有多个goroutine。大多数echo服务仅仅会返回他们读取到的内容,就像下面这个简单的handleConn函数所做的一样:

+
func handleConn(c net.Conn) {
+	io.Copy(c, c) // NOTE: ignoring errors
+	c.Close()
+}
+
+

一个更有意思的echo服务应该模拟一个实际的echo的“回响”,并且一开始要用大写HELLO来表示“声音很大”,之后经过一小段延迟返回一个有所缓和的Hello,然后一个全小写字母的hello表示声音渐渐变小直至消失,像下面这个版本的handleConn(译注:笑看作者脑洞大开):

+

gopl.io/ch8/reverb1

+
func echo(c net.Conn, shout string, delay time.Duration) {
+	fmt.Fprintln(c, "\t", strings.ToUpper(shout))
+	time.Sleep(delay)
+	fmt.Fprintln(c, "\t", shout)
+	time.Sleep(delay)
+	fmt.Fprintln(c, "\t", strings.ToLower(shout))
+}
+
+func handleConn(c net.Conn) {
+	input := bufio.NewScanner(c)
+	for input.Scan() {
+		echo(c, input.Text(), 1*time.Second)
+	}
+	// NOTE: ignoring potential errors from input.Err()
+	c.Close()
+}
+
+

我们需要升级我们的客户端程序,这样它就可以发送终端的输入到服务器,并把服务端的返回输出到终端上,这使我们有了使用并发的另一个好机会:

+

gopl.io/ch8/netcat2

+
func main() {
+	conn, err := net.Dial("tcp", "localhost:8000")
+	if err != nil {
+		log.Fatal(err)
+	}
+	defer conn.Close()
+	go mustCopy(os.Stdout, conn)
+	mustCopy(conn, os.Stdin)
+}
+
+

当main goroutine从标准输入流中读取内容并将其发送给服务器时,另一个goroutine会读取并打印服务端的响应。当main goroutine碰到输入终止时,例如,用户在终端中按了Control-D(^D),在windows上是Control-Z,这时程序就会被终止,尽管其它goroutine中还有进行中的任务。(在8.4.1中引入了channels后我们会明白如何让程序等待两边都结束。)

+

下面这个会话中,客户端的输入是左对齐的,服务端的响应会用缩进来区别显示。 +客户端会向服务器“喊三次话”:

+
$ go build gopl.io/ch8/reverb1
+$ ./reverb1 &
+$ go build gopl.io/ch8/netcat2
+$ ./netcat2
+Hello?
+    HELLO?
+    Hello?
+    hello?
+Is there anybody there?
+    IS THERE ANYBODY THERE?
+Yooo-hooo!
+    Is there anybody there?
+    is there anybody there?
+    YOOO-HOOO!
+    Yooo-hooo!
+    yooo-hooo!
+^D
+$ killall reverb1
+
+

注意客户端的第三次shout在前一个shout处理完成之前一直没有被处理,这貌似看起来不是特别“现实”。真实世界里的回响应该是会由三次shout的回声组合而成的。为了模拟真实世界的回响,我们需要更多的goroutine来做这件事情。这样我们就再一次地需要go这个关键词了,这次我们用它来调用echo:

+

gopl.io/ch8/reverb2

+
func handleConn(c net.Conn) {
+	input := bufio.NewScanner(c)
+	for input.Scan() {
+		go echo(c, input.Text(), 1*time.Second)
+	}
+	// NOTE: ignoring potential errors from input.Err()
+	c.Close()
+}
+
+

go后跟的函数的参数会在go语句自身执行时被求值;因此input.Text()会在main goroutine中被求值。 +现在回响是并发并且会按时间来覆盖掉其它响应了:

+
$ go build gopl.io/ch8/reverb2
+$ ./reverb2 &
+$ ./netcat2
+Is there anybody there?
+    IS THERE ANYBODY THERE?
+Yooo-hooo!
+    Is there anybody there?
+    YOOO-HOOO!
+    is there anybody there?
+    Yooo-hooo!
+    yooo-hooo!
+^D
+$ killall reverb2
+
+

让服务使用并发不只是处理多个客户端的请求,甚至在处理单个连接时也可能会用到,就像我们上面的两个go关键词的用法。然而在我们使用go关键词的同时,需要慎重地考虑net.Conn中的方法在并发地调用时是否安全,事实上对于大多数类型来说也确实不安全。我们会在下一章中详细地探讨并发安全性。

+

8.4. Channels

+

如果说goroutine是Go语言程序的并发体的话,那么channels则是它们之间的通信机制。一个channel是一个通信机制,它可以让一个goroutine通过它给另一个goroutine发送值信息。每个channel都有一个特殊的类型,也就是channels可发送数据的类型。一个可以发送int类型数据的channel一般写为chan int。

+

使用内置的make函数,我们可以创建一个channel:

+
ch := make(chan int) // ch has type 'chan int'
+
+

和map类似,channel也对应一个make创建的底层数据结构的引用。当我们复制一个channel或用于函数参数传递时,我们只是拷贝了一个channel引用,因此调用者和被调用者将引用同一个channel对象。和其它的引用类型一样,channel的零值也是nil。

+

两个相同类型的channel可以使用==运算符比较。如果两个channel引用的是相同的对象,那么比较的结果为真。一个channel也可以和nil进行比较。

+

一个channel有发送和接受两个主要操作,都是通信行为。一个发送语句将一个值从一个goroutine通过channel发送到另一个执行接收操作的goroutine。发送和接收两个操作都使用<-运算符。在发送语句中,<-运算符分割channel和要发送的值。在接收语句中,<-运算符写在channel对象之前。一个不使用接收结果的接收操作也是合法的。

+
ch <- x  // a send statement
+x = <-ch // a receive expression in an assignment statement
+<-ch     // a receive statement; result is discarded
+
+

Channel还支持close操作,用于关闭channel,随后对基于该channel的任何发送操作都将导致panic异常。对一个已经被close过的channel进行接收操作依然可以接受到之前已经成功发送的数据;如果channel中已经没有数据的话将产生一个零值的数据。

+

使用内置的close函数就可以关闭一个channel:

+
close(ch)
+
+

以最简单方式调用make函数创建的是一个无缓存的channel,但是我们也可以指定第二个整型参数,对应channel的容量。如果channel的容量大于零,那么该channel就是带缓存的channel。

+
ch = make(chan int)    // unbuffered channel
+ch = make(chan int, 0) // unbuffered channel
+ch = make(chan int, 3) // buffered channel with capacity 3
+
+

我们将先讨论无缓存的channel,然后在8.4.4节讨论带缓存的channel。

+

8.4.1. 不带缓存的Channels

+

一个基于无缓存Channels的发送操作将导致发送者goroutine阻塞,直到另一个goroutine在相同的Channels上执行接收操作,当发送的值通过Channels成功传输之后,两个goroutine可以继续执行后面的语句。反之,如果接收操作先发生,那么接收者goroutine也将阻塞,直到有另一个goroutine在相同的Channels上执行发送操作。

+

基于无缓存Channels的发送和接收操作将导致两个goroutine做一次同步操作。因为这个原因,无缓存Channels有时候也被称为同步Channels。当通过一个无缓存Channels发送数据时,接收者收到数据发生在再次唤醒发送者goroutine之前(译注:happens before,这是Go语言并发内存模型的一个关键术语!)。

+

在讨论并发编程时,当我们说x事件在y事件之前发生(happens before),我们并不是说x事件在时间上比y时间更早;我们要表达的意思是要保证在此之前的事件都已经完成了,例如在此之前的更新某些变量的操作已经完成,你可以放心依赖这些已完成的事件了。

+

当我们说x事件既不是在y事件之前发生也不是在y事件之后发生,我们就说x事件和y事件是并发的。这并不是意味着x事件和y事件就一定是同时发生的,我们只是不能确定这两个事件发生的先后顺序。在下一章中我们将看到,当两个goroutine并发访问了相同的变量时,我们有必要保证某些事件的执行顺序,以避免出现某些并发问题。

+

在8.3节的客户端程序,它在主goroutine中(译注:就是执行main函数的goroutine)将标准输入复制到server,因此当客户端程序关闭标准输入时,后台goroutine可能依然在工作。我们需要让主goroutine等待后台goroutine完成工作后再退出,我们使用了一个channel来同步两个goroutine:

+

gopl.io/ch8/netcat3

+
func main() {
+	conn, err := net.Dial("tcp", "localhost:8000")
+	if err != nil {
+		log.Fatal(err)
+	}
+	done := make(chan struct{})
+	go func() {
+		io.Copy(os.Stdout, conn) // NOTE: ignoring errors
+		log.Println("done")
+		done <- struct{}{} // signal the main goroutine
+	}()
+	mustCopy(conn, os.Stdin)
+	conn.Close()
+	<-done // wait for background goroutine to finish
+}
+
+

当用户关闭了标准输入,主goroutine中的mustCopy函数调用将返回,然后调用conn.Close()关闭读和写方向的网络连接。关闭网络连接中的写方向的连接将导致server程序收到一个文件(end-of-file)结束的信号。关闭网络连接中读方向的连接将导致后台goroutine的io.Copy函数调用返回一个“read from closed connection”(“从关闭的连接读”)类似的错误,因此我们临时移除了错误日志语句;在练习8.3将会提供一个更好的解决方案。(需要注意的是go语句调用了一个函数字面量,这是Go语言中启动goroutine常用的形式。)

+

在后台goroutine返回之前,它先打印一个日志信息,然后向done对应的channel发送一个值。主goroutine在退出前先等待从done对应的channel接收一个值。因此,总是可以在程序退出前正确输出“done”消息。

+

基于channels发送消息有两个重要方面。首先每个消息都有一个值,但是有时候通讯的事实和发生的时刻也同样重要。当我们更希望强调通讯发生的时刻时,我们将它称为消息事件。有些消息事件并不携带额外的信息,它仅仅是用作两个goroutine之间的同步,这时候我们可以用struct{}空结构体作为channels元素的类型,虽然也可以使用bool或int类型实现同样的功能,done <- 1语句也比done <- struct{}{}更短。

+

练习 8.3: 在netcat3例子中,conn虽然是一个interface类型的值,但是其底层真实类型是*net.TCPConn,代表一个TCP连接。一个TCP连接有读和写两个部分,可以使用CloseRead和CloseWrite方法分别关闭它们。修改netcat3的主goroutine代码,只关闭网络连接中写的部分,这样的话后台goroutine可以在标准输入被关闭后继续打印从reverb1服务器传回的数据。(要在reverb2服务器也完成同样的功能是比较困难的;参考练习 8.4。)

+

8.4.2. 串联的Channels(Pipeline)

+

Channels也可以用于将多个goroutine连接在一起,一个Channel的输出作为下一个Channel的输入。这种串联的Channels就是所谓的管道(pipeline)。下面的程序用两个channels将三个goroutine串联起来,如图8.1所示。

+

+

第一个goroutine是一个计数器,用于生成0、1、2、……形式的整数序列,然后通过channel将该整数序列发送给第二个goroutine;第二个goroutine是一个求平方的程序,对收到的每个整数求平方,然后将平方后的结果通过第二个channel发送给第三个goroutine;第三个goroutine是一个打印程序,打印收到的每个整数。为了保持例子清晰,我们有意选择了非常简单的函数,当然三个goroutine的计算很简单,在现实中确实没有必要为如此简单的运算构建三个goroutine。

+

gopl.io/ch8/pipeline1

+
func main() {
+	naturals := make(chan int)
+	squares := make(chan int)
+
+	// Counter
+	go func() {
+		for x := 0; ; x++ {
+			naturals <- x
+		}
+	}()
+
+	// Squarer
+	go func() {
+		for {
+			x := <-naturals
+			squares <- x * x
+		}
+	}()
+
+	// Printer (in main goroutine)
+	for {
+		fmt.Println(<-squares)
+	}
+}
+
+

如您所料,上面的程序将生成0、1、4、9、……形式的无穷数列。像这样的串联Channels的管道(Pipelines)可以用在需要长时间运行的服务中,每个长时间运行的goroutine可能会包含一个死循环,在不同goroutine的死循环内部使用串联的Channels来通信。但是,如果我们希望通过Channels只发送有限的数列该如何处理呢?

+

如果发送者知道,没有更多的值需要发送到channel的话,那么让接收者也能及时知道没有多余的值可接收将是有用的,因为接收者可以停止不必要的接收等待。这可以通过内置的close函数来关闭channel实现:

+
close(naturals)
+
+

当一个channel被关闭后,再向该channel发送数据将导致panic异常。当一个被关闭的channel中已经发送的数据都被成功接收后,后续的接收操作将不再阻塞,它们会立即返回一个零值。关闭上面例子中的naturals变量对应的channel并不能终止循环,它依然会收到一个永无休止的零值序列,然后将它们发送给打印者goroutine。

+

没有办法直接测试一个channel是否被关闭,但是接收操作有一个变体形式:它多接收一个结果,多接收的第二个结果是一个布尔值ok,ture表示成功从channels接收到值,false表示channels已经被关闭并且里面没有值可接收。使用这个特性,我们可以修改squarer函数中的循环代码,当naturals对应的channel被关闭并没有值可接收时跳出循环,并且也关闭squares对应的channel.

+
// Squarer
+go func() {
+	for {
+		x, ok := <-naturals
+		if !ok {
+			break // channel was closed and drained
+		}
+		squares <- x * x
+	}
+	close(squares)
+}()
+
+

因为上面的语法是笨拙的,而且这种处理模式很常见,因此Go语言的range循环可直接在channels上面迭代。使用range循环是上面处理模式的简洁语法,它依次从channel接收数据,当channel被关闭并且没有值可接收时跳出循环。

+

在下面的改进中,我们的计数器goroutine只生成100个含数字的序列,然后关闭naturals对应的channel,这将导致计算平方数的squarer对应的goroutine可以正常终止循环并关闭squares对应的channel。(在一个更复杂的程序中,可以通过defer语句关闭对应的channel。)最后,主goroutine也可以正常终止循环并退出程序。

+

gopl.io/ch8/pipeline2

+
func main() {
+	naturals := make(chan int)
+	squares := make(chan int)
+
+	// Counter
+	go func() {
+		for x := 0; x < 100; x++ {
+			naturals <- x
+		}
+		close(naturals)
+	}()
+
+	// Squarer
+	go func() {
+		for x := range naturals {
+			squares <- x * x
+		}
+		close(squares)
+	}()
+
+	// Printer (in main goroutine)
+	for x := range squares {
+		fmt.Println(x)
+	}
+}
+
+

其实你并不需要关闭每一个channel。只有当需要告诉接收者goroutine,所有的数据已经全部发送时才需要关闭channel。不管一个channel是否被关闭,当它没有被引用时将会被Go语言的垃圾自动回收器回收。(不要将关闭一个打开文件的操作和关闭一个channel操作混淆。对于每个打开的文件,都需要在不使用的时候调用对应的Close方法来关闭文件。)

+

试图重复关闭一个channel将导致panic异常,试图关闭一个nil值的channel也将导致panic异常。关闭一个channels还会触发一个广播机制,我们将在8.9节讨论。

+

8.4.3. 单方向的Channel

+

随着程序的增长,人们习惯于将大的函数拆分为小的函数。我们前面的例子中使用了三个goroutine,然后用两个channels来连接它们,它们都是main函数的局部变量。将三个goroutine拆分为以下三个函数是自然的想法:

+
func counter(out chan int)
+func squarer(out, in chan int)
+func printer(in chan int)
+
+

其中计算平方的squarer函数在两个串联Channels的中间,因此拥有两个channel类型的参数,一个用于输入一个用于输出。两个channel都拥有相同的类型,但是它们的使用方式相反:一个只用于接收,另一个只用于发送。参数的名字in和out已经明确表示了这个意图,但是并无法保证squarer函数向一个in参数对应的channel发送数据或者从一个out参数对应的channel接收数据。

+

这种场景是典型的。当一个channel作为一个函数参数时,它一般总是被专门用于只发送或者只接收。

+

为了表明这种意图并防止被滥用,Go语言的类型系统提供了单方向的channel类型,分别用于只发送或只接收的channel。类型chan<- int表示一个只发送int的channel,只能发送不能接收。相反,类型<-chan int表示一个只接收int的channel,只能接收不能发送。(箭头<-和关键字chan的相对位置表明了channel的方向。)这种限制将在编译期检测。

+

因为关闭操作只用于断言不再向channel发送新的数据,所以只有在发送者所在的goroutine才会调用close函数,因此对一个只接收的channel调用close将是一个编译错误。

+

这是改进的版本,这一次参数使用了单方向channel类型:

+

gopl.io/ch8/pipeline3

+
func counter(out chan<- int) {
+	for x := 0; x < 100; x++ {
+		out <- x
+	}
+	close(out)
+}
+
+func squarer(out chan<- int, in <-chan int) {
+	for v := range in {
+		out <- v * v
+	}
+	close(out)
+}
+
+func printer(in <-chan int) {
+	for v := range in {
+		fmt.Println(v)
+	}
+}
+
+func main() {
+	naturals := make(chan int)
+	squares := make(chan int)
+	go counter(naturals)
+	go squarer(squares, naturals)
+	printer(squares)
+}
+
+

调用counter(naturals)时,naturals的类型将隐式地从chan int转换成chan<- int。调用printer(squares)也会导致相似的隐式转换,这一次是转换为<-chan int类型只接收型的channel。任何双向channel向单向channel变量的赋值操作都将导致该隐式转换。这里并没有反向转换的语法:也就是不能将一个类似chan<- int类型的单向型的channel转换为chan int类型的双向型的channel。

+

8.4.4. 带缓存的Channels

+

带缓存的Channel内部持有一个元素队列。队列的最大容量是在调用make函数创建channel时通过第二个参数指定的。下面的语句创建了一个可以持有三个字符串元素的带缓存Channel。图8.2是ch变量对应的channel的图形表示形式。

+
ch = make(chan string, 3)
+
+

+

向缓存Channel的发送操作就是向内部缓存队列的尾部插入元素,接收操作则是从队列的头部删除元素。如果内部缓存队列是满的,那么发送操作将阻塞直到因另一个goroutine执行接收操作而释放了新的队列空间。相反,如果channel是空的,接收操作将阻塞直到有另一个goroutine执行发送操作而向队列插入元素。

+

我们可以在无阻塞的情况下连续向新创建的channel发送三个值:

+
ch <- "A"
+ch <- "B"
+ch <- "C"
+
+

此刻,channel的内部缓存队列将是满的(图8.3),如果有第四个发送操作将发生阻塞。

+

+

如果我们接收一个值,

+
fmt.Println(<-ch) // "A"
+
+

那么channel的缓存队列将不是满的也不是空的(图8.4),因此对该channel执行的发送或接收操作都不会发生阻塞。通过这种方式,channel的缓存队列解耦了接收和发送的goroutine。

+

+

在某些特殊情况下,程序可能需要知道channel内部缓存的容量,可以用内置的cap函数获取:

+
fmt.Println(cap(ch)) // "3"
+
+

同样,对于内置的len函数,如果传入的是channel,那么将返回channel内部缓存队列中有效元素的个数。因为在并发程序中该信息会随着接收操作而失效,但是它对某些故障诊断和性能优化会有帮助。

+
fmt.Println(len(ch)) // "2"
+
+

在继续执行两次接收操作后channel内部的缓存队列将又成为空的,如果有第四个接收操作将发生阻塞:

+
fmt.Println(<-ch) // "B"
+fmt.Println(<-ch) // "C"
+
+

在这个例子中,发送和接收操作都发生在同一个goroutine中,但是在真实的程序中它们一般由不同的goroutine执行。Go语言新手有时候会将一个带缓存的channel当作同一个goroutine中的队列使用,虽然语法看似简单,但实际上这是一个错误。Channel和goroutine的调度器机制是紧密相连的,如果没有其他goroutine从channel接收,发送者——或许是整个程序——将会面临永远阻塞的风险。如果你只是需要一个简单的队列,使用slice就可以了。

+

下面的例子展示了一个使用了带缓存channel的应用。它并发地向三个镜像站点发出请求,三个镜像站点分散在不同的地理位置。它们分别将收到的响应发送到带缓存channel,最后接收者只接收第一个收到的响应,也就是最快的那个响应。因此mirroredQuery函数可能在另外两个响应慢的镜像站点响应之前就返回了结果。(顺便说一下,多个goroutines并发地向同一个channel发送数据,或从同一个channel接收数据都是常见的用法。)

+
func mirroredQuery() string {
+	responses := make(chan string, 3)
+	go func() { responses <- request("asia.gopl.io") }()
+	go func() { responses <- request("europe.gopl.io") }()
+	go func() { responses <- request("americas.gopl.io") }()
+	return <-responses // return the quickest response
+}
+
+func request(hostname string) (response string) { /* ... */ }
+
+

如果我们使用了无缓存的channel,那么两个慢的goroutines将会因为没有人接收而被永远卡住。这种情况,称为goroutines泄漏,这将是一个BUG。和垃圾变量不同,泄漏的goroutines并不会被自动回收,因此确保每个不再需要的goroutine能正常退出是重要的。

+

关于无缓存或带缓存channels之间的选择,或者是带缓存channels的容量大小的选择,都可能影响程序的正确性。无缓存channel更强地保证了每个发送操作与相应的同步接收操作;但是对于带缓存channel,这些操作是解耦的。同样,即使我们知道将要发送到一个channel的信息的数量上限,创建一个对应容量大小的带缓存channel也是不现实的,因为这要求在执行任何接收操作之前缓存所有已经发送的值。如果未能分配足够的缓存将导致程序死锁。

+

Channel的缓存也可能影响程序的性能。想象一家蛋糕店有三个厨师,一个烘焙,一个上糖衣,还有一个将每个蛋糕传递到它下一个厨师的生产线。在狭小的厨房空间环境,每个厨师在完成蛋糕后必须等待下一个厨师已经准备好接受它;这类似于在一个无缓存的channel上进行沟通。

+

如果在每个厨师之间有一个放置一个蛋糕的额外空间,那么每个厨师就可以将一个完成的蛋糕临时放在那里而马上进入下一个蛋糕的制作中;这类似于将channel的缓存队列的容量设置为1。只要每个厨师的平均工作效率相近,那么其中大部分的传输工作将是迅速的,个体之间细小的效率差异将在交接过程中弥补。如果厨师之间有更大的额外空间——也是就更大容量的缓存队列——将可以在不停止生产线的前提下消除更大的效率波动,例如一个厨师可以短暂地休息,然后再加快赶上进度而不影响其他人。

+

另一方面,如果生产线的前期阶段一直快于后续阶段,那么它们之间的缓存在大部分时间都将是满的。相反,如果后续阶段比前期阶段更快,那么它们之间的缓存在大部分时间都将是空的。对于这类场景,额外的缓存并没有带来任何好处。

+

生产线的隐喻对于理解channels和goroutines的工作机制是很有帮助的。例如,如果第二阶段是需要精心制作的复杂操作,一个厨师可能无法跟上第一个厨师的进度,或者是无法满足第三阶段厨师的需求。要解决这个问题,我们可以再雇佣另一个厨师来帮助完成第二阶段的工作,他执行相同的任务但是独立工作。这类似于基于相同的channels创建另一个独立的goroutine。

+

我们没有太多的空间展示全部细节,但是gopl.io/ch8/cake包模拟了这个蛋糕店,可以通过不同的参数调整。它还对上面提到的几种场景提供对应的基准测试(§11.4) 。

+

8.5. 并发的循环

+

本节中,我们会探索一些用来在并行时循环迭代的常见并发模型。我们会探究从全尺寸图片生成一些缩略图的问题。gopl.io/ch8/thumbnail包提供了ImageFile函数来帮我们拉伸图片。我们不会说明这个函数的实现,只需要从gopl.io下载它。

+

gopl.io/ch8/thumbnail

+
package thumbnail
+
+// ImageFile reads an image from infile and writes
+// a thumbnail-size version of it in the same directory.
+// It returns the generated file name, e.g., "foo.thumb.jpg".
+func ImageFile(infile string) (string, error)
+
+

下面的程序会循环迭代一些图片文件名,并为每一张图片生成一个缩略图:

+

gopl.io/ch8/thumbnail

+
// makeThumbnails makes thumbnails of the specified files.
+func makeThumbnails(filenames []string) {
+	for _, f := range filenames {
+		if _, err := thumbnail.ImageFile(f); err != nil {
+			log.Println(err)
+		}
+	}
+}
+
+

显然我们处理文件的顺序无关紧要,因为每一个图片的拉伸操作和其它图片的处理操作都是彼此独立的。像这种子问题都是完全彼此独立的问题被叫做易并行问题(译注:embarrassingly parallel,直译的话更像是尴尬并行)。易并行问题是最容易被实现成并行的一类问题(废话),并且最能够享受到并发带来的好处,能够随着并行的规模线性地扩展。

+

下面让我们并行地执行这些操作,从而将文件IO的延迟隐藏掉,并用上多核cpu的计算能力来拉伸图像。我们的第一个并发程序只是使用了一个go关键字。这里我们先忽略掉错误,之后再进行处理。

+
// NOTE: incorrect!
+func makeThumbnails2(filenames []string) {
+	for _, f := range filenames {
+		go thumbnail.ImageFile(f) // NOTE: ignoring errors
+	}
+}
+
+

这个版本运行的实在有点太快,实际上,由于它比最早的版本使用的时间要短得多,即使当文件名的slice中只包含有一个元素。这就有点奇怪了,如果程序没有并发执行的话,那为什么一个并发的版本还是要快呢?答案其实是makeThumbnails在它还没有完成工作之前就已经返回了。它启动了所有的goroutine,每一个文件名对应一个,但没有等待它们一直到执行完毕。

+

没有什么直接的办法能够等待goroutine完成,但是我们可以改变goroutine里的代码让其能够将完成情况报告给外部的goroutine知晓,使用的方式是向一个共享的channel中发送事件。因为我们已经确切地知道有len(filenames)个内部goroutine,所以外部的goroutine只需要在返回之前对这些事件计数。

+
// makeThumbnails3 makes thumbnails of the specified files in parallel.
+func makeThumbnails3(filenames []string) {
+	ch := make(chan struct{})
+	for _, f := range filenames {
+		go func(f string) {
+			thumbnail.ImageFile(f) // NOTE: ignoring errors
+			ch <- struct{}{}
+		}(f)
+	}
+	// Wait for goroutines to complete.
+	for range filenames {
+		<-ch
+	}
+}
+
+

注意我们将f的值作为一个显式的变量传给了函数,而不是在循环的闭包中声明:

+
for _, f := range filenames {
+	go func() {
+		thumbnail.ImageFile(f) // NOTE: incorrect!
+		// ...
+	}()
+}
+
+

回忆一下之前在5.6.1节中,匿名函数中的循环变量快照问题。上面这个单独的变量f是被所有的匿名函数值所共享,且会被连续的循环迭代所更新的。当新的goroutine开始执行字面函数时,for循环可能已经更新了f并且开始了另一轮的迭代或者(更有可能的)已经结束了整个循环,所以当这些goroutine开始读取f的值时,它们所看到的值已经是slice的最后一个元素了。显式地添加这个参数,我们能够确保使用的f是当go语句执行时的“当前”那个f。

+

如果我们想要从每一个worker goroutine往主goroutine中返回值时该怎么办呢?当我们调用thumbnail.ImageFile创建文件失败的时候,它会返回一个错误。下一个版本的makeThumbnails会返回其在做拉伸操作时接收到的第一个错误:

+
// makeThumbnails4 makes thumbnails for the specified files in parallel.
+// It returns an error if any step failed.
+func makeThumbnails4(filenames []string) error {
+	errors := make(chan error)
+
+	for _, f := range filenames {
+		go func(f string) {
+			_, err := thumbnail.ImageFile(f)
+			errors <- err
+		}(f)
+	}
+
+	for range filenames {
+		if err := <-errors; err != nil {
+			return err // NOTE: incorrect: goroutine leak!
+		}
+	}
+
+	return nil
+}
+
+

这个程序有一个微妙的bug。当它遇到第一个非nil的error时会直接将error返回到调用方,使得没有一个goroutine去排空errors channel。这样剩下的worker goroutine在向这个channel中发送值时,都会永远地阻塞下去,并且永远都不会退出。这种情况叫做goroutine泄露(§8.4.4),可能会导致整个程序卡住或者跑出out of memory的错误。

+

最简单的解决办法就是用一个具有合适大小的buffered channel,这样这些worker goroutine向channel中发送错误时就不会被阻塞。(一个可选的解决办法是创建一个另外的goroutine,当main goroutine返回第一个错误的同时去排空channel。)

+

下一个版本的makeThumbnails使用了一个buffered channel来返回生成的图片文件的名字,附带生成时的错误。

+
// makeThumbnails5 makes thumbnails for the specified files in parallel.
+// It returns the generated file names in an arbitrary order,
+// or an error if any step failed.
+func makeThumbnails5(filenames []string) (thumbfiles []string, err error) {
+	type item struct {
+		thumbfile string
+		err       error
+	}
+
+	ch := make(chan item, len(filenames))
+	for _, f := range filenames {
+		go func(f string) {
+			var it item
+			it.thumbfile, it.err = thumbnail.ImageFile(f)
+			ch <- it
+		}(f)
+	}
+
+	for range filenames {
+		it := <-ch
+		if it.err != nil {
+			return nil, it.err
+		}
+		thumbfiles = append(thumbfiles, it.thumbfile)
+	}
+
+	return thumbfiles, nil
+}
+
+

我们最后一个版本的makeThumbnails返回了新文件们的大小总计数(bytes)。和前面的版本都不一样的一点是我们在这个版本里没有把文件名放在slice里,而是通过一个string的channel传过来,所以我们无法对循环的次数进行预测。

+

为了知道最后一个goroutine什么时候结束(最后一个结束并不一定是最后一个开始),我们需要一个递增的计数器,在每一个goroutine启动时加一,在goroutine退出时减一。这需要一种特殊的计数器,这个计数器需要在多个goroutine操作时做到安全并且提供在其减为零之前一直等待的一种方法。这种计数类型被称为sync.WaitGroup,下面的代码就用到了这种方法:

+
// makeThumbnails6 makes thumbnails for each file received from the channel.
+// It returns the number of bytes occupied by the files it creates.
+func makeThumbnails6(filenames <-chan string) int64 {
+	sizes := make(chan int64)
+	var wg sync.WaitGroup // number of working goroutines
+	for f := range filenames {
+		wg.Add(1)
+		// worker
+		go func(f string) {
+			defer wg.Done()
+			thumb, err := thumbnail.ImageFile(f)
+			if err != nil {
+				log.Println(err)
+				return
+			}
+			info, _ := os.Stat(thumb) // OK to ignore error
+			sizes <- info.Size()
+		}(f)
+	}
+
+	// closer
+	go func() {
+		wg.Wait()
+		close(sizes)
+	}()
+
+	var total int64
+	for size := range sizes {
+		total += size
+	}
+	return total
+}
+
+

注意Add和Done方法的不对称。Add是为计数器加一,必须在worker goroutine开始之前调用,而不是在goroutine中;否则的话我们没办法确定Add是在"closer" goroutine调用Wait之前被调用。并且Add还有一个参数,但Done却没有任何参数;其实它和Add(-1)是等价的。我们使用defer来确保计数器即使是在出错的情况下依然能够正确地被减掉。上面的程序代码结构是当我们使用并发循环,但又不知道迭代次数时很通常而且很地道的写法。

+

sizes channel携带了每一个文件的大小到main goroutine,在main goroutine中使用了range loop来计算总和。观察一下我们是怎样创建一个closer goroutine,并让其在所有worker goroutine们结束之后再关闭sizes channel的。两步操作:wait和close,必须是基于sizes的循环的并发。考虑一下另一种方案:如果等待操作被放在了main goroutine中,在循环之前,这样的话就永远都不会结束了,如果在循环之后,那么又变成了不可达的部分,因为没有任何东西去关闭这个channel,这个循环就永远都不会终止。

+

图8.5 表明了makethumbnails6函数中事件的序列。纵列表示goroutine。窄线段代表sleep,粗线段代表活动。斜线箭头代表用来同步两个goroutine的事件。时间向下流动。注意main goroutine是如何大部分的时间被唤醒执行其range循环,等待worker发送值或者closer来关闭channel的。

+

+

练习 8.4: 修改reverb2服务器,在每一个连接中使用sync.WaitGroup来计数活跃的echo goroutine。当计数减为零时,关闭TCP连接的写入,像练习8.3中一样。验证一下你的修改版netcat3客户端会一直等待所有的并发“喊叫”完成,即使是在标准输入流已经关闭的情况下。

+

练习 8.5: 使用一个已有的CPU绑定的顺序程序,比如在3.3节中我们写的Mandelbrot程序或者3.2节中的3-D surface计算程序,并将他们的主循环改为并发形式,使用channel来进行通信。在多核计算机上这个程序得到了多少速度上的改进?使用多少个goroutine是最合适的呢?

+

8.6. 示例: 并发的Web爬虫

+

在5.6节中,我们做了一个简单的web爬虫,用bfs(广度优先)算法来抓取整个网站。在本节中,我们会让这个爬虫并行化,这样每一个彼此独立的抓取命令可以并行进行IO,最大化利用网络资源。crawl函数和gopl.io/ch5/findlinks3中的是一样的。

+

gopl.io/ch8/crawl1

+
func crawl(url string) []string {
+	fmt.Println(url)
+	list, err := links.Extract(url)
+	if err != nil {
+		log.Print(err)
+	}
+	return list
+}
+
+

主函数和5.6节中的breadthFirst(广度优先)类似。像之前一样,一个worklist是一个记录了需要处理的元素的队列,每一个元素都是一个需要抓取的URL列表,不过这一次我们用channel代替slice来做这个队列。每一个对crawl的调用都会在他们自己的goroutine中进行并且会把他们抓到的链接发送回worklist。

+
func main() {
+	worklist := make(chan []string)
+
+	// Start with the command-line arguments.
+	go func() { worklist <- os.Args[1:] }()
+
+	// Crawl the web concurrently.
+	seen := make(map[string]bool)
+	for list := range worklist {
+		for _, link := range list {
+			if !seen[link] {
+				seen[link] = true
+				go func(link string) {
+					worklist <- crawl(link)
+				}(link)
+			}
+		}
+	}
+}
+
+

注意这里的crawl所在的goroutine会将link作为一个显式的参数传入,来避免“循环变量快照”的问题(在5.6.1中有讲解)。另外注意这里将命令行参数传入worklist也是在一个另外的goroutine中进行的,这是为了避免channel两端的main goroutine与crawler goroutine都尝试向对方发送内容,却没有一端接收内容时发生死锁。当然,这里我们也可以用buffered channel来解决问题,这里不再赘述。

+

现在爬虫可以高并发地运行起来,并且可以产生一大坨的URL了,不过还是会有俩问题。一个问题是在运行一段时间后可能会出现在log的错误信息里的:

+
$ go build gopl.io/ch8/crawl1
+$ ./crawl1 http://gopl.io/
+http://gopl.io/
+https://golang.org/help/
+https://golang.org/doc/
+https://golang.org/blog/
+...
+2015/07/15 18:22:12 Get ...: dial tcp: lookup blog.golang.org: no such host
+2015/07/15 18:22:12 Get ...: dial tcp 23.21.222.120:443: socket: too many open files
+...
+
+

最初的错误信息是一个让人莫名的DNS查找失败,即使这个域名是完全可靠的。而随后的错误信息揭示了原因:这个程序一次性创建了太多网络连接,超过了每一个进程的打开文件数限制,既而导致了在调用net.Dial像DNS查找失败这样的问题。

+

这个程序实在是太他妈并行了。无穷无尽地并行化并不是什么好事情,因为不管怎么说,你的系统总是会有一些个限制因素,比如CPU核心数会限制你的计算负载,比如你的硬盘转轴和磁头数限制了你的本地磁盘IO操作频率,比如你的网络带宽限制了你的下载速度上限,或者是你的一个web服务的服务容量上限等等。为了解决这个问题,我们可以限制并发程序所使用的资源来使之适应自己的运行环境。对于我们的例子来说,最简单的方法就是限制对links.Extract在同一时间最多不会有超过n次调用,这里的n一般小于文件描述符的上限值,比如20。这和一个夜店里限制客人数目是一个道理,只有当有客人离开时,才会允许新的客人进入店内。

+

我们可以用一个有容量限制的buffered channel来控制并发,这类似于操作系统里的计数信号量概念。从概念上讲,channel里的n个空槽代表n个可以处理内容的token(通行证),从channel里接收一个值会释放其中的一个token,并且生成一个新的空槽位。这样保证了在没有接收介入时最多有n个发送操作。(这里可能我们拿channel里填充的槽来做token更直观一些,不过还是这样吧。)由于channel里的元素类型并不重要,我们用一个零值的struct{}来作为其元素。

+

让我们重写crawl函数,将对links.Extract的调用操作用获取、释放token的操作包裹起来,来确保同一时间对其只有20个调用。信号量数量和其能操作的IO资源数量应保持接近。

+

gopl.io/ch8/crawl2

+
// tokens is a counting semaphore used to
+// enforce a limit of 20 concurrent requests.
+var tokens = make(chan struct{}, 20)
+
+func crawl(url string) []string {
+	fmt.Println(url)
+	tokens <- struct{}{} // acquire a token
+	list, err := links.Extract(url)
+	<-tokens // release the token
+	if err != nil {
+		log.Print(err)
+	}
+	return list
+}
+
+

第二个问题是这个程序永远都不会终止,即使它已经爬到了所有初始链接衍生出的链接。(当然,除非你慎重地选择了合适的初始化URL或者已经实现了练习8.6中的深度限制,你应该还没有意识到这个问题。)为了使这个程序能够终止,我们需要在worklist为空或者没有crawl的goroutine在运行时退出主循环。

+
func main() {
+	worklist := make(chan []string)
+	var n int // number of pending sends to worklist
+
+	// Start with the command-line arguments.
+	n++
+	go func() { worklist <- os.Args[1:] }()
+
+	// Crawl the web concurrently.
+	seen := make(map[string]bool)
+
+	for ; n > 0; n-- {
+		list := <-worklist
+		for _, link := range list {
+			if !seen[link] {
+				seen[link] = true
+				n++
+				go func(link string) {
+					worklist <- crawl(link)
+				}(link)
+			}
+		}
+	}
+}
+
+

这个版本中,计数器n对worklist的发送操作数量进行了限制。每一次我们发现有元素需要被发送到worklist时,我们都会对n进行++操作,在向worklist中发送初始的命令行参数之前,我们也进行过一次++操作。这里的操作++是在每启动一个crawler的goroutine之前。主循环会在n减为0时终止,这时候说明没活可干了。

+

现在这个并发爬虫会比5.6节中的深度优先搜索版快上20倍,而且不会出什么错,并且在其完成任务时也会正确地终止。

+

下面的程序是避免过度并发的另一种思路。这个版本使用了原来的crawl函数,但没有使用计数信号量,取而代之用了20个常驻的crawler goroutine,这样来保证最多20个HTTP请求在并发。

+
func main() {
+	worklist := make(chan []string)  // lists of URLs, may have duplicates
+	unseenLinks := make(chan string) // de-duplicated URLs
+
+	// Add command-line arguments to worklist.
+	go func() { worklist <- os.Args[1:] }()
+
+	// Create 20 crawler goroutines to fetch each unseen link.
+	for i := 0; i < 20; i++ {
+		go func() {
+			for link := range unseenLinks {
+				foundLinks := crawl(link)
+				go func() { worklist <- foundLinks }()
+			}
+		}()
+	}
+
+	// The main goroutine de-duplicates worklist items
+	// and sends the unseen ones to the crawlers.
+	seen := make(map[string]bool)
+	for list := range worklist {
+		for _, link := range list {
+			if !seen[link] {
+				seen[link] = true
+				unseenLinks <- link
+			}
+		}
+	}
+}
+
+

所有的爬虫goroutine现在都是被同一个channel - unseenLinks喂饱的了。主goroutine负责拆分它从worklist里拿到的元素,然后把没有抓过的经由unseenLinks channel发送给一个爬虫的goroutine。

+

seen这个map被限定在main goroutine中;也就是说这个map只能在main goroutine中进行访问。类似于其它的信息隐藏方式,这样的约束可以让我们从一定程度上保证程序的正确性。例如,内部变量不能够在函数外部被访问到;变量(§2.3.4)在没有发生变量逃逸(译注:局部变量被全局变量引用地址导致变量被分配在堆上)的情况下是无法在函数外部访问的;一个对象的封装字段无法被该对象的方法以外的方法访问到。在所有的情况下,信息隐藏都可以帮助我们约束我们的程序,使其不发生意料之外的情况。

+

crawl函数爬到的链接在一个专有的goroutine中被发送到worklist中来避免死锁。为了节省篇幅,这个例子的终止问题我们先不进行详细阐述了。

+

练习 8.6: 为并发爬虫增加深度限制。也就是说,如果用户设置了depth=3,那么只有从首页跳转三次以内能够跳到的页面才能被抓取到。

+

练习 8.7: 完成一个并发程序来创建一个线上网站的本地镜像,把该站点的所有可达的页面都抓取到本地硬盘。为了省事,我们这里可以只取出现在该域下的所有页面(比如golang.org开头,译注:外链的应该就不算了。)当然了,出现在页面里的链接你也需要进行一些处理,使其能够在你的镜像站点上进行跳转,而不是指向原始的链接。

+

译注: +拓展阅读 Handling 1 Million Requests per Minute with Go

+

8.7. 基于select的多路复用

+

下面的程序会进行火箭发射的倒计时。time.Tick函数返回一个channel,程序会周期性地像一个节拍器一样向这个channel发送事件。每一个事件的值是一个时间戳,不过更有意思的是其传送方式。

+

gopl.io/ch8/countdown1

+
func main() {
+	fmt.Println("Commencing countdown.")
+	tick := time.Tick(1 * time.Second)
+	for countdown := 10; countdown > 0; countdown-- {
+		fmt.Println(countdown)
+		<-tick
+	}
+	launch()
+}
+
+

现在我们让这个程序支持在倒计时中,用户按下return键时直接中断发射流程。首先,我们启动一个goroutine,这个goroutine会尝试从标准输入中读入一个单独的byte并且,如果成功了,会向名为abort的channel发送一个值。

+

gopl.io/ch8/countdown2

+
abort := make(chan struct{})
+go func() {
+	os.Stdin.Read(make([]byte, 1)) // read a single byte
+	abort <- struct{}{}
+}()
+
+

现在每一次计数循环的迭代都需要等待两个channel中的其中一个返回事件了:当一切正常时的ticker channel(就像NASA jorgon的"nominal",译注:这梗估计我们是不懂了)或者异常时返回的abort事件。我们无法做到从每一个channel中接收信息,如果我们这么做的话,如果第一个channel中没有事件发过来那么程序就会立刻被阻塞,这样我们就无法收到第二个channel中发过来的事件。这时候我们需要多路复用(multiplex)这些操作了,为了能够多路复用,我们使用了select语句。

+
select {
+case <-ch1:
+	// ...
+case x := <-ch2:
+	// ...use x...
+case ch3 <- y:
+	// ...
+default:
+	// ...
+}
+
+

上面是select语句的一般形式。和switch语句稍微有点相似,也会有几个case和最后的default选择分支。每一个case代表一个通信操作(在某个channel上进行发送或者接收),并且会包含一些语句组成的一个语句块。一个接收表达式可能只包含接收表达式自身(译注:不把接收到的值赋值给变量什么的),就像上面的第一个case,或者包含在一个简短的变量声明中,像第二个case里一样;第二种形式让你能够引用接收到的值。

+

select会等待case中有能够执行的case时去执行。当条件满足时,select才会去通信并执行case之后的语句;这时候其它通信是不会执行的。一个没有任何case的select语句写作select{},会永远地等待下去。

+

让我们回到我们的火箭发射程序。time.After函数会立即返回一个channel,并起一个新的goroutine在经过特定的时间后向该channel发送一个独立的值。下面的select语句会一直等待直到两个事件中的一个到达,无论是abort事件或者一个10秒经过的事件。如果10秒经过了还没有abort事件进入,那么火箭就会发射。

+
func main() {
+	// ...create abort channel...
+
+	fmt.Println("Commencing countdown.  Press return to abort.")
+	select {
+	case <-time.After(10 * time.Second):
+		// Do nothing.
+	case <-abort:
+		fmt.Println("Launch aborted!")
+		return
+	}
+	launch()
+}
+
+

下面这个例子更微妙。ch这个channel的buffer大小是1,所以会交替的为空或为满,所以只有一个case可以进行下去,无论i是奇数或者偶数,它都会打印0 2 4 6 8。

+
ch := make(chan int, 1)
+for i := 0; i < 10; i++ {
+	select {
+	case x := <-ch:
+		fmt.Println(x) // "0" "2" "4" "6" "8"
+	case ch <- i:
+	}
+}
+
+

如果多个case同时就绪时,select会随机地选择一个执行,这样来保证每一个channel都有平等的被select的机会。增加前一个例子的buffer大小会使其输出变得不确定,因为当buffer既不为满也不为空时,select语句的执行情况就像是抛硬币的行为一样是随机的。

+

下面让我们的发射程序打印倒计时。这里的select语句会使每次循环迭代等待一秒来执行退出操作。

+

gopl.io/ch8/countdown3

+
func main() {
+	// ...create abort channel...
+
+	fmt.Println("Commencing countdown.  Press return to abort.")
+	tick := time.Tick(1 * time.Second)
+	for countdown := 10; countdown > 0; countdown-- {
+		fmt.Println(countdown)
+		select {
+		case <-tick:
+			// Do nothing.
+		case <-abort:
+			fmt.Println("Launch aborted!")
+			return
+		}
+	}
+	launch()
+}
+
+

time.Tick函数表现得好像它创建了一个在循环中调用time.Sleep的goroutine,每次被唤醒时发送一个事件。当countdown函数返回时,它会停止从tick中接收事件,但是ticker这个goroutine还依然存活,继续徒劳地尝试向channel中发送值,然而这时候已经没有其它的goroutine会从该channel中接收值了——这被称为goroutine泄露(§8.4.4)。

+

Tick函数挺方便,但是只有当程序整个生命周期都需要这个时间时我们使用它才比较合适。否则的话,我们应该使用下面的这种模式:

+
ticker := time.NewTicker(1 * time.Second)
+<-ticker.C    // receive from the ticker's channel
+ticker.Stop() // cause the ticker's goroutine to terminate
+
+

有时候我们希望能够从channel中发送或者接收值,并避免因为发送或者接收导致的阻塞,尤其是当channel没有准备好写或者读时。select语句就可以实现这样的功能。select会有一个default来设置当其它的操作都不能够马上被处理时程序需要执行哪些逻辑。

+

下面的select语句会在abort channel中有值时,从其中接收值;无值时什么都不做。这是一个非阻塞的接收操作;反复地做这样的操作叫做“轮询channel”。

+
select {
+case <-abort:
+	fmt.Printf("Launch aborted!\n")
+	return
+default:
+	// do nothing
+}
+
+

channel的零值是nil。也许会让你觉得比较奇怪,nil的channel有时候也是有一些用处的。因为对一个nil的channel发送和接收操作会永远阻塞,在select语句中操作nil的channel永远都不会被select到。

+

这使得我们可以用nil来激活或者禁用case,来达成处理其它输入或输出事件时超时和取消的逻辑。我们会在下一节中看到一个例子。

+

练习 8.8: 使用select来改造8.3节中的echo服务器,为其增加超时,这样服务器可以在客户端10秒中没有任何喊话时自动断开连接。

+

8.9. 并发的退出

+

有时候我们需要通知goroutine停止它正在干的事情,比如一个正在执行计算的web服务,然而它的客户端已经断开了和服务端的连接。

+

Go语言并没有提供在一个goroutine中终止另一个goroutine的方法,由于这样会导致goroutine之间的共享变量落在未定义的状态上。在8.7节中的rocket launch程序中,我们往名字叫abort的channel里发送了一个简单的值,在countdown的goroutine中会把这个值理解为自己的退出信号。但是如果我们想要退出两个或者任意多个goroutine怎么办呢?

+

一种可能的手段是向abort的channel里发送和goroutine数目一样多的事件来退出它们。如果这些goroutine中已经有一些自己退出了,那么会导致我们的channel里的事件数比goroutine还多,这样导致我们的发送直接被阻塞。另一方面,如果这些goroutine又生成了其它的goroutine,我们的channel里的数目又太少了,所以有些goroutine可能会无法接收到退出消息。一般情况下我们是很难知道在某一个时刻具体有多少个goroutine在运行着的。另外,当一个goroutine从abort channel中接收到一个值的时候,他会消费掉这个值,这样其它的goroutine就没法看到这条信息。为了能够达到我们退出goroutine的目的,我们需要更靠谱的策略,来通过一个channel把消息广播出去,这样goroutine们能够看到这条事件消息,并且在事件完成之后,可以知道这件事已经发生过了。

+

回忆一下我们关闭了一个channel并且被消费掉了所有已发送的值,操作channel之后的代码可以立即被执行,并且会产生零值。我们可以将这个机制扩展一下,来作为我们的广播机制:不要向channel发送值,而是用关闭一个channel来进行广播。

+

只要一些小修改,我们就可以把退出逻辑加入到前一节的du程序。首先,我们创建一个退出的channel,不需要向这个channel发送任何值,但其所在的闭包内要写明程序需要退出。我们同时还定义了一个工具函数,cancelled,这个函数在被调用的时候会轮询退出状态。

+

gopl.io/ch8/du4

+
var done = make(chan struct{})
+
+func cancelled() bool {
+	select {
+	case <-done:
+		return true
+	default:
+		return false
+	}
+}
+
+

下面我们创建一个从标准输入流中读取内容的goroutine,这是一个比较典型的连接到终端的程序。每当有输入被读到(比如用户按了回车键),这个goroutine就会把取消消息通过关闭done的channel广播出去。

+
// Cancel traversal when input is detected.
+go func() {
+	os.Stdin.Read(make([]byte, 1)) // read a single byte
+	close(done)
+}()
+
+

现在我们需要使我们的goroutine来对取消进行响应。在main goroutine中,我们添加了select的第三个case语句,尝试从done channel中接收内容。如果这个case被满足的话,在select到的时候即会返回,但在结束之前我们需要把fileSizes channel中的内容“排”空,在channel被关闭之前,舍弃掉所有值。这样可以保证对walkDir的调用不要被向fileSizes发送信息阻塞住,可以正确地完成。

+
for {
+	select {
+	case <-done:
+		// Drain fileSizes to allow existing goroutines to finish.
+		for range fileSizes {
+			// Do nothing.
+		}
+		return
+	case size, ok := <-fileSizes:
+		// ...
+	}
+}
+
+

walkDir这个goroutine一启动就会轮询取消状态,如果取消状态被设置的话会直接返回,并且不做额外的事情。这样我们将所有在取消事件之后创建的goroutine改变为无操作。

+
func walkDir(dir string, n *sync.WaitGroup, fileSizes chan<- int64) {
+	defer n.Done()
+	if cancelled() {
+		return
+	}
+	for _, entry := range dirents(dir) {
+		// ...
+	}
+}
+
+

在walkDir函数的循环中我们对取消状态进行轮询可以带来明显的益处,可以避免在取消事件发生时还去创建goroutine。取消本身是有一些代价的;想要快速的响应需要对程序逻辑进行侵入式的修改。确保在取消发生之后不要有代价太大的操作可能会需要修改你代码里的很多地方,但是在一些重要的地方去检查取消事件也确实能带来很大的好处。

+

对这个程序的一个简单的性能分析可以揭示瓶颈在dirents函数中获取一个信号量。下面的select可以让这种操作可以被取消,并且可以将取消时的延迟从几百毫秒降低到几十毫秒。

+
func dirents(dir string) []os.FileInfo {
+	select {
+	case sema <- struct{}{}: // acquire token
+	case <-done:
+		return nil // cancelled
+	}
+	defer func() { <-sema }() // release token
+	// ...read directory...
+}
+
+

现在当取消发生时,所有后台的goroutine都会迅速停止并且主函数会返回。当然,当主函数返回时,一个程序会退出,而我们又无法在主函数退出的时候确认其已经释放了所有的资源(译注:因为程序都退出了,你的代码都没法执行了)。这里有一个方便的窍门我们可以一用:取代掉直接从主函数返回,我们调用一个panic,然后runtime会把每一个goroutine的栈dump下来。如果main goroutine是唯一一个剩下的goroutine的话,他会清理掉自己的一切资源。但是如果还有其它的goroutine没有退出,他们可能没办法被正确地取消掉,也有可能被取消但是取消操作会很花时间;所以这里的一个调研还是很有必要的。我们用panic来获取到足够的信息来验证我们上面的判断,看看最终到底是什么样的情况。

+

练习 8.10: HTTP请求可能会因http.Request结构体中Cancel channel的关闭而取消。修改8.6节中的web crawler来支持取消http请求。(提示:http.Get并没有提供方便地定制一个请求的方法。你可以用http.NewRequest来取而代之,设置它的Cancel字段,然后用http.DefaultClient.Do(req)来进行这个http请求。)

+

练习 8.11: 紧接着8.4.4中的mirroredQuery流程,实现一个并发请求url的fetch的变种。当第一个请求返回时,直接取消其它的请求。

+

8.10. 示例: 聊天服务

+

我们用一个聊天服务器来终结本章节的内容,这个程序可以让一些用户通过服务器向其它所有用户广播文本消息。这个程序中有四种goroutine。main和broadcaster各自是一个goroutine实例,每一个客户端的连接都会有一个handleConn和clientWriter的goroutine。broadcaster是select用法的不错的样例,因为它需要处理三种不同类型的消息。

+

下面演示的main goroutine的工作,是listen和accept(译注:网络编程里的概念)从客户端过来的连接。对每一个连接,程序都会建立一个新的handleConn的goroutine,就像我们在本章开头的并发的echo服务器里所做的那样。

+

gopl.io/ch8/chat

+
func main() {
+	listener, err := net.Listen("tcp", "localhost:8000")
+	if err != nil {
+		log.Fatal(err)
+	}
+	go broadcaster()
+	for {
+		conn, err := listener.Accept()
+		if err != nil {
+			log.Print(err)
+			continue
+		}
+		go handleConn(conn)
+	}
+}
+
+

然后是broadcaster的goroutine。他的内部变量clients会记录当前建立连接的客户端集合。其记录的内容是每一个客户端的消息发出channel的“资格”信息。

+
type client chan<- string // an outgoing message channel
+
+var (
+	entering = make(chan client)
+	leaving  = make(chan client)
+	messages = make(chan string) // all incoming client messages
+)
+
+func broadcaster() {
+	clients := make(map[client]bool) // all connected clients
+	for {
+		select {
+		case msg := <-messages:
+			// Broadcast incoming message to all
+			// clients' outgoing message channels.
+			for cli := range clients {
+				cli <- msg
+			}
+		case cli := <-entering:
+			clients[cli] = true
+
+		case cli := <-leaving:
+			delete(clients, cli)
+			close(cli)
+		}
+	}
+}
+
+

broadcaster监听来自全局的entering和leaving的channel来获知客户端的到来和离开事件。当其接收到其中的一个事件时,会更新clients集合,当该事件是离开行为时,它会关闭客户端的消息发送channel。broadcaster也会监听全局的消息channel,所有的客户端都会向这个channel中发送消息。当broadcaster接收到什么消息时,就会将其广播至所有连接到服务端的客户端。

+

现在让我们看看每一个客户端的goroutine。handleConn函数会为它的客户端创建一个消息发送channel并通过entering channel来通知客户端的到来。然后它会读取客户端发来的每一行文本,并通过全局的消息channel来将这些文本发送出去,并为每条消息带上发送者的前缀来标明消息身份。当客户端发送完毕后,handleConn会通过leaving这个channel来通知客户端的离开并关闭连接。

+
func handleConn(conn net.Conn) {
+	ch := make(chan string) // outgoing client messages
+	go clientWriter(conn, ch)
+
+	who := conn.RemoteAddr().String()
+	ch <- "You are " + who
+	messages <- who + " has arrived"
+	entering <- ch
+
+	input := bufio.NewScanner(conn)
+	for input.Scan() {
+		messages <- who + ": " + input.Text()
+	}
+	// NOTE: ignoring potential errors from input.Err()
+
+	leaving <- ch
+	messages <- who + " has left"
+	conn.Close()
+}
+
+func clientWriter(conn net.Conn, ch <-chan string) {
+	for msg := range ch {
+		fmt.Fprintln(conn, msg) // NOTE: ignoring network errors
+	}
+}
+
+

另外,handleConn为每一个客户端创建了一个clientWriter的goroutine,用来接收向客户端发送消息的channel中的广播消息,并将它们写入到客户端的网络连接。客户端的读取循环会在broadcaster接收到leaving通知并关闭了channel后终止。

+

下面演示的是当服务器有两个活动的客户端连接,并且在两个窗口中运行的情况,使用netcat来聊天:

+
$ go build gopl.io/ch8/chat
+$ go build gopl.io/ch8/netcat3
+$ ./chat &
+$ ./netcat3
+You are 127.0.0.1:64208               $ ./netcat3
+127.0.0.1:64211 has arrived           You are 127.0.0.1:64211
+Hi!
+127.0.0.1:64208: Hi!                  127.0.0.1:64208: Hi!
+                                      Hi yourself.
+127.0.0.1:64211: Hi yourself.         127.0.0.1:64211: Hi yourself.
+^C
+                                      127.0.0.1:64208 has left
+$ ./netcat3
+You are 127.0.0.1:64216               127.0.0.1:64216 has arrived
+                                      Welcome.
+127.0.0.1:64211: Welcome.             127.0.0.1:64211: Welcome.
+                                      ^C
+127.0.0.1:64211 has left”
+
+

当与n个客户端保持聊天session时,这个程序会有2n+2个并发的goroutine,然而这个程序却并不需要显式的锁(§9.2)。clients这个map被限制在了一个独立的goroutine中,broadcaster,所以它不能被并发地访问。多个goroutine共享的变量只有这些channel和net.Conn的实例,两个东西都是并发安全的。我们会在下一章中更多地讲解约束,并发安全以及goroutine中共享变量的含义。

+

练习 8.12: 使broadcaster能够在每个新的客户端到来时通知它当前的客户端集合。这需要你在clients集合中,以及entering和leaving的channel中记录客户端的名字。

+

练习 8.13: 使聊天服务器能够断开空闲的客户端连接,比如最近五分钟之后没有发送任何消息的那些客户端。提示:可以在其它goroutine中调用conn.Close()来解除Read调用,就像input.Scanner()所做的那样。

+

练习 8.14: 修改聊天服务器的网络协议,这样每一个客户端就可以在entering时提供他们的名字。将消息前缀由之前的网络地址改为这个名字。

+

练习 8.15: 如果一个客户端没有及时地读取数据可能会导致所有的客户端被阻塞。修改broadcaster来跳过一条消息,而不是等待这个客户端一直到其准备好读写。或者为每一个客户端的消息发送channel建立缓冲区,这样大部分的消息便不会被丢掉;broadcaster应该用一个非阻塞的send向这个channel中发消息。

+

第9章 基于共享变量的并发

+

前一章我们介绍了一些使用goroutine和channel这样直接而自然的方式来实现并发的方法。然而这样做我们实际上回避了在写并发代码时必须处理的一些重要而且细微的问题。

+

在本章中,我们会细致地了解并发机制。尤其是在多goroutine之间的共享变量,并发问题的分析手段,以及解决这些问题的基本模式。最后我们会解释goroutine和操作系统线程之间的技术上的一些区别。

+

9.1. 竞争条件

+

在一个线性(就是说只有一个goroutine的)的程序中,程序的执行顺序只由程序的逻辑来决定。例如,我们有一段语句序列,第一个在第二个之前(废话),以此类推。在有两个或更多goroutine的程序中,每一个goroutine内的语句也是按照既定的顺序去执行的,但是一般情况下我们没法去知道分别位于两个goroutine的事件x和y的执行顺序,x是在y之前还是之后还是同时发生是没法判断的。当我们没有办法自信地确认一个事件是在另一个事件的前面或者后面发生的话,就说明x和y这两个事件是并发的。

+

考虑一下,一个函数在线性程序中可以正确地工作。如果在并发的情况下,这个函数依然可以正确地工作的话,那么我们就说这个函数是并发安全的,并发安全的函数不需要额外的同步工作。我们可以把这个概念概括为一个特定类型的一些方法和操作函数,对于某个类型来说,如果其所有可访问的方法和操作都是并发安全的话,那么该类型便是并发安全的。

+

在一个程序中有非并发安全的类型的情况下,我们依然可以使这个程序并发安全。确实,并发安全的类型是例外,而不是规则,所以只有当文档中明确地说明了其是并发安全的情况下,你才可以并发地去访问它。我们会避免并发访问大多数的类型,无论是将变量局限在单一的一个goroutine内,还是用互斥条件维持更高级别的不变性,都是为了这个目的。我们会在本章中说明这些术语。

+

相反,包级别的导出函数一般情况下都是并发安全的。由于package级的变量没法被限制在单一的gorouine,所以修改这些变量“必须”使用互斥条件。

+

一个函数在并发调用时没法工作的原因太多了,比如死锁(deadlock)、活锁(livelock)和饿死(resource starvation)。我们没有空去讨论所有的问题,这里我们只聚焦在竞争条件上。

+

竞争条件指的是程序在多个goroutine交叉执行操作时,没有给出正确的结果。竞争条件是很恶劣的一种场景,因为这种问题会一直潜伏在你的程序里,然后在非常少见的时候蹦出来,或许只是会在很大的负载时才会发生,又或许是会在使用了某一个编译器、某一种平台或者某一种架构的时候才会出现。这些使得竞争条件带来的问题非常难以复现而且难以分析诊断。

+

传统上经常用经济损失来为竞争条件做比喻,所以我们来看一个简单的银行账户程序。

+
// Package bank implements a bank with only one account.
+package bank
+var balance int
+func Deposit(amount int) { balance = balance + amount }
+func Balance() int { return balance }
+
+

(当然我们也可以把Deposit存款函数写成balance += amount,这种形式也是等价的,不过长一些的形式解释起来更方便一些。)

+

对于这个简单的程序而言,我们一眼就能看出,以任意顺序调用函数Deposit和Balance都会得到正确的结果。也就是说,Balance函数会给出之前的所有存入的额度之和。然而,当我们并发地而不是顺序地调用这些函数的话,Balance就再也没办法保证结果正确了。考虑一下下面的两个goroutine,其代表了一个银行联合账户的两笔交易:

+
// Alice:
+go func() {
+	bank.Deposit(200)                // A1
+	fmt.Println("=", bank.Balance()) // A2
+}()
+
+// Bob:
+go bank.Deposit(100)                 // B
+
+

Alice存了$200,然后检查她的余额,同时Bob存了$100。因为A1和A2是和B并发执行的,我们没法预测他们发生的先后顺序。直观地来看的话,我们会认为其执行顺序只有三种可能性:“Alice先”,“Bob先”以及“Alice/Bob/Alice”交错执行。下面的表格会展示经过每一步骤后balance变量的值。引号里的字符串表示余额单。

+
Alice first        Bob first        Alice/Bob/Alice
+          0                0                      0
+  A1    200        B     100             A1     200
+  A2 "= 200"       A1    300             B      300
+  B     300        A2 "= 300"            A2  "= 300"
+
+

所有情况下最终的余额都是$300。唯一的变数是Alice的余额单是否包含了Bob交易,不过无论怎么着客户都不会在意。

+

但是事实是上面的直觉推断是错误的。第四种可能的结果是事实存在的,这种情况下Bob的存款会在Alice存款操作中间,在余额被读到(balance + amount)之后,在余额被更新之前(balance = ...),这样会导致Bob的交易丢失。而这是因为Alice的存款操作A1实际上是两个操作的一个序列,读取然后写;可以称之为A1r和A1w。下面是交叉时产生的问题:

+
Data race
+0
+A1r      0     ... = balance + amount
+B      100
+A1w    200     balance = ...
+A2  "= 200"
+
+

在A1r之后,balance + amount会被计算为200,所以这是A1w会写入的值,并不受其它存款操作的干预。最终的余额是$200。银行的账户上的资产比Bob实际的资产多了$100。(译注:因为丢失了Bob的存款操作,所以其实是说Bob的钱丢了。)

+

这个程序包含了一个特定的竞争条件,叫作数据竞争。无论任何时候,只要有两个goroutine并发访问同一变量,且至少其中的一个是写操作的时候就会发生数据竞争。

+

如果数据竞争的对象是一个比一个机器字(译注:32位机器上一个字=4个字节)更大的类型时,事情就变得更麻烦了,比如interface,string或者slice类型都是如此。下面的代码会并发地更新两个不同长度的slice:

+
var x []int
+go func() { x = make([]int, 10) }()
+go func() { x = make([]int, 1000000) }()
+x[999999] = 1 // NOTE: undefined behavior; memory corruption possible!
+
+

最后一个语句中的x的值是未定义的;其可能是nil,或者也可能是一个长度为10的slice,也可能是一个长度为1,000,000的slice。但是回忆一下slice的三个组成部分:指针(pointer)、长度(length)和容量(capacity)。如果指针是从第一个make调用来,而长度从第二个make来,x就变成了一个混合体,一个自称长度为1,000,000但实际上内部只有10个元素的slice。这样导致的结果是存储999,999元素的位置会碰撞一个遥远的内存位置,这种情况下难以对值进行预测,而且debug也会变成噩梦。这种语义雷区被称为未定义行为,对C程序员来说应该很熟悉;幸运的是在Go语言里造成的麻烦要比C里小得多。

+

尽管并发程序的概念让我们知道并发并不是简单的语句交叉执行。我们将会在9.4节中看到,数据竞争可能会有奇怪的结果。许多程序员,甚至一些非常聪明的人也还是会偶尔提出一些理由来允许数据竞争,比如:“互斥条件代价太高”,“这个逻辑只是用来做logging”,“我不介意丢失一些消息”等等。因为在他们的编译器或者平台上很少遇到问题,可能给了他们错误的信心。一个好的经验法则是根本就没有什么所谓的良性数据竞争。所以我们一定要避免数据竞争,那么在我们的程序中要如何做到呢?

+

我们来重复一下数据竞争的定义,因为实在太重要了:数据竞争会在两个以上的goroutine并发访问相同的变量且至少其中一个为写操作时发生。根据上述定义,有三种方式可以避免数据竞争:

+

第一种方法是不要去写变量。考虑一下下面的map,会被“懒”填充,也就是说在每个key被第一次请求到的时候才会去填值。如果Icon是被顺序调用的话,这个程序会工作很正常,但如果Icon被并发调用,那么对于这个map来说就会存在数据竞争。

+
var icons = make(map[string]image.Image)
+func loadIcon(name string) image.Image
+
+// NOTE: not concurrency-safe!
+func Icon(name string) image.Image {
+	icon, ok := icons[name]
+	if !ok {
+		icon = loadIcon(name)
+		icons[name] = icon
+	}
+	return icon
+}
+
+

反之,如果我们在创建goroutine之前的初始化阶段,就初始化了map中的所有条目并且再也不去修改它们,那么任意数量的goroutine并发访问Icon都是安全的,因为每一个goroutine都只是去读取而已。

+
var icons = map[string]image.Image{
+	"spades.png":   loadIcon("spades.png"),
+	"hearts.png":   loadIcon("hearts.png"),
+	"diamonds.png": loadIcon("diamonds.png"),
+	"clubs.png":    loadIcon("clubs.png"),
+}
+
+// Concurrency-safe.
+func Icon(name string) image.Image { return icons[name] }
+
+

上面的例子里icons变量在包初始化阶段就已经被赋值了,包的初始化是在程序main函数开始执行之前就完成了的。只要初始化完成了,icons就再也不会被修改。数据结构如果从不被修改或是不变量则是并发安全的,无需进行同步。不过显然,如果update操作是必要的,我们就没法用这种方法,比如说银行账户。

+

第二种避免数据竞争的方法是,避免从多个goroutine访问变量。这也是前一章中大多数程序所采用的方法。例如前面的并发web爬虫(§8.6)的main goroutine是唯一一个能够访问seen map的goroutine,而聊天服务器(§8.10)中的broadcaster goroutine是唯一一个能够访问clients map的goroutine。这些变量都被限定在了一个单独的goroutine中。

+

由于其它的goroutine不能够直接访问变量,它们只能使用一个channel来发送请求给指定的goroutine来查询更新变量。这也就是Go的口头禅“不要使用共享数据来通信;使用通信来共享数据”。一个提供对一个指定的变量通过channel来请求的goroutine叫做这个变量的monitor(监控)goroutine。例如broadcaster goroutine会监控clients map的全部访问。

+

下面是一个重写了的银行的例子,这个例子中balance变量被限制在了monitor goroutine中,名为teller:

+

gopl.io/ch9/bank1

+
// Package bank provides a concurrency-safe bank with one account.
+package bank
+
+var deposits = make(chan int) // send amount to deposit
+var balances = make(chan int) // receive balance
+
+func Deposit(amount int) { deposits <- amount }
+func Balance() int       { return <-balances }
+
+func teller() {
+	var balance int // balance is confined to teller goroutine
+	for {
+		select {
+		case amount := <-deposits:
+			balance += amount
+		case balances <- balance:
+		}
+	}
+}
+
+func init() {
+	go teller() // start the monitor goroutine
+}
+
+

即使当一个变量无法在其整个生命周期内被绑定到一个独立的goroutine,绑定依然是并发问题的一个解决方案。例如在一条流水线上的goroutine之间共享变量是很普遍的行为,在这两者间会通过channel来传输地址信息。如果流水线的每一个阶段都能够避免在将变量传送到下一阶段后再去访问它,那么对这个变量的所有访问就是线性的。其效果是变量会被绑定到流水线的一个阶段,传送完之后被绑定到下一个,以此类推。这种规则有时被称为串行绑定。

+

下面的例子中,Cakes会被严格地顺序访问,先是baker gorouine,然后是icer gorouine:

+
type Cake struct{ state string }
+
+func baker(cooked chan<- *Cake) {
+	for {
+		cake := new(Cake)
+		cake.state = "cooked"
+		cooked <- cake // baker never touches this cake again
+	}
+}
+
+func icer(iced chan<- *Cake, cooked <-chan *Cake) {
+	for cake := range cooked {
+		cake.state = "iced"
+		iced <- cake // icer never touches this cake again
+	}
+}
+
+

第三种避免数据竞争的方法是允许很多goroutine去访问变量,但是在同一个时刻最多只有一个goroutine在访问。这种方式被称为“互斥”,在下一节来讨论这个主题。

+

练习 9.1: 给gopl.io/ch9/bank1程序添加一个Withdraw(amount int)取款函数。其返回结果应该要表明事务是成功了还是因为没有足够资金失败了。这条消息会被发送给monitor的goroutine,且消息需要包含取款的额度和一个新的channel,这个新channel会被monitor goroutine来把boolean结果发回给Withdraw。

+

9.2. sync.Mutex互斥锁

+

在8.6节中,我们使用了一个buffered channel作为一个计数信号量,来保证最多只有20个goroutine会同时执行HTTP请求。同理,我们可以用一个容量只有1的channel来保证最多只有一个goroutine在同一时刻访问一个共享变量。一个只能为1和0的信号量叫做二元信号量(binary semaphore)。

+

gopl.io/ch9/bank2

+
var (
+	sema    = make(chan struct{}, 1) // a binary semaphore guarding balance
+	balance int
+)
+
+func Deposit(amount int) {
+	sema <- struct{}{} // acquire token
+	balance = balance + amount
+	<-sema // release token
+}
+
+func Balance() int {
+	sema <- struct{}{} // acquire token
+	b := balance
+	<-sema // release token
+	return b
+}
+
+

这种互斥很实用,而且被sync包里的Mutex类型直接支持。它的Lock方法能够获取到token(这里叫锁),并且Unlock方法会释放这个token:

+

gopl.io/ch9/bank3

+
import "sync"
+
+var (
+	mu      sync.Mutex // guards balance
+	balance int
+)
+
+func Deposit(amount int) {
+	mu.Lock()
+	balance = balance + amount
+	mu.Unlock()
+}
+
+func Balance() int {
+	mu.Lock()
+	b := balance
+	mu.Unlock()
+	return b
+}
+
+

每次一个goroutine访问bank变量时(这里只有balance余额变量),它都会调用mutex的Lock方法来获取一个互斥锁。如果其它的goroutine已经获得了这个锁的话,这个操作会被阻塞直到其它goroutine调用了Unlock使该锁变回可用状态。mutex会保护共享变量。惯例来说,被mutex所保护的变量是在mutex变量声明之后立刻声明的。如果你的做法和惯例不符,确保在文档里对你的做法进行说明。

+

在Lock和Unlock之间的代码段中的内容goroutine可以随便读取或者修改,这个代码段叫做临界区。锁的持有者在其他goroutine获取该锁之前需要调用Unlock。goroutine在结束后释放锁是必要的,无论以哪条路径通过函数都需要释放,即使是在错误路径中,也要记得释放。

+

上面的bank程序例证了一种通用的并发模式。一系列的导出函数封装了一个或多个变量,那么访问这些变量唯一的方式就是通过这些函数来做(或者方法,对于一个对象的变量来说)。每一个函数在一开始就获取互斥锁并在最后释放锁,从而保证共享变量不会被并发访问。这种函数、互斥锁和变量的编排叫作监控monitor(这种老式单词的monitor是受“monitor goroutine”的术语启发而来的。两种用法都是一个代理人保证变量被顺序访问)。

+

由于在存款和查询余额函数中的临界区代码这么短——只有一行,没有分支调用——在代码最后去调用Unlock就显得更为直截了当。在更复杂的临界区的应用中,尤其是必须要尽早处理错误并返回的情况下,就很难去(靠人)判断对Lock和Unlock的调用是在所有路径中都能够严格配对的了。Go语言里的defer简直就是这种情况下的救星:我们用defer来调用Unlock,临界区会隐式地延伸到函数作用域的最后,这样我们就从“总要记得在函数返回之后或者发生错误返回时要记得调用一次Unlock”这种状态中获得了解放。Go会自动帮我们完成这些事情。

+
func Balance() int {
+	mu.Lock()
+	defer mu.Unlock()
+	return balance
+}
+
+

上面的例子里Unlock会在return语句读取完balance的值之后执行,所以Balance函数是并发安全的。这带来的另一点好处是,我们再也不需要一个本地变量b了。

+

此外,一个deferred Unlock即使在临界区发生panic时依然会执行,这对于用recover(§5.10)来恢复的程序来说是很重要的。defer调用只会比显式地调用Unlock成本高那么一点点,不过却在很大程度上保证了代码的整洁性。大多数情况下对于并发程序来说,代码的整洁性比过度的优化更重要。如果可能的话尽量使用defer来将临界区扩展到函数的结束。

+

考虑一下下面的Withdraw函数。成功的时候,它会正确地减掉余额并返回true。但如果银行记录资金对交易来说不足,那么取款就会恢复余额,并返回false。

+
// NOTE: not atomic!
+func Withdraw(amount int) bool {
+	Deposit(-amount)
+	if Balance() < 0 {
+		Deposit(amount)
+		return false // insufficient funds
+	}
+	return true
+}
+
+

函数终于给出了正确的结果,但是还有一点讨厌的副作用。当过多的取款操作同时执行时,balance可能会瞬时被减到0以下。这可能会引起一个并发的取款被不合逻辑地拒绝。所以如果Bob尝试买一辆sports car时,Alice可能就没办法为她的早咖啡付款了。这里的问题是取款不是一个原子操作:它包含了三个步骤,每一步都需要去获取并释放互斥锁,但任何一次锁都不会锁上整个取款流程。

+

理想情况下,取款应该只在整个操作中获得一次互斥锁。下面这样的尝试是错误的:

+
// NOTE: incorrect!
+func Withdraw(amount int) bool {
+	mu.Lock()
+	defer mu.Unlock()
+	Deposit(-amount)
+	if Balance() < 0 {
+		Deposit(amount)
+		return false // insufficient funds
+	}
+	return true
+}
+
+

上面这个例子中,Deposit会调用mu.Lock()第二次去获取互斥锁,但因为mutex已经锁上了,而无法被重入(译注:go里没有重入锁,关于重入锁的概念,请参考java)——也就是说没法对一个已经锁上的mutex来再次上锁——这会导致程序死锁,没法继续执行下去,Withdraw会永远阻塞下去。

+

关于Go的mutex不能重入这一点我们有很充分的理由。mutex的目的是确保共享变量在程序执行时的关键点上能够保证不变性。不变性的一层含义是“没有goroutine访问共享变量”,但实际上这里对于mutex保护的变量来说,不变性还包含更深层含义:当一个goroutine获得了一个互斥锁时,它能断定被互斥锁保护的变量正处于不变状态(译注:即没有其他代码块正在读写共享变量),在其获取并保持锁期间,可能会去更新共享变量,这样不变性只是短暂地被破坏,然而当其释放锁之后,锁必须保证共享变量重获不变性并且多个goroutine按顺序访问共享变量。尽管一个可以重入的mutex也可以保证没有其它的goroutine在访问共享变量,但它不具备不变性更深层含义。(译注:更详细的解释,Russ Cox认为可重入锁是bug的温床,是一个失败的设计)

+

一个通用的解决方案是将一个函数分离为多个函数,比如我们把Deposit分离成两个:一个不导出的函数deposit,这个函数假设锁总是会被保持并去做实际的操作,另一个是导出的函数Deposit,这个函数会调用deposit,但在调用前会先去获取锁。同理我们可以将Withdraw也表示成这种形式:

+
func Withdraw(amount int) bool {
+	mu.Lock()
+	defer mu.Unlock()
+	deposit(-amount)
+	if balance < 0 {
+		deposit(amount)
+		return false // insufficient funds
+	}
+	return true
+}
+
+func Deposit(amount int) {
+	mu.Lock()
+	defer mu.Unlock()
+	deposit(amount)
+}
+
+func Balance() int {
+	mu.Lock()
+	defer mu.Unlock()
+	return balance
+}
+
+// This function requires that the lock be held.
+func deposit(amount int) { balance += amount }
+
+

当然,这里的存款deposit函数很小,实际上取款Withdraw函数不需要理会对它的调用,尽管如此,这里的表达还是表明了规则。

+

封装(§6.6),用限制一个程序中的意外交互的方式,可以使我们获得数据结构的不变性。因为某种原因,封装还帮我们获得了并发的不变性。当你使用mutex时,确保mutex和其保护的变量没有被导出(在go里也就是小写,且不要被大写字母开头的函数访问啦),无论这些变量是包级的变量还是一个struct的字段。

+

9.3. sync.RWMutex读写锁

+

在100刀的存款消失时不做记录多少还是会让我们有一些恐慌,Bob写了一个程序,每秒运行几百次来检查他的银行余额。他会在家,在工作中,甚至会在他的手机上来运行这个程序。银行注意到这些陡增的流量使得存款和取款有了延时,因为所有的余额查询请求是顺序执行的,这样会互斥地获得锁,并且会暂时阻止其它的goroutine运行。

+

由于Balance函数只需要读取变量的状态,所以我们同时让多个Balance调用并发运行事实上是安全的,只要在运行的时候没有存款或者取款操作就行。在这种场景下我们需要一种特殊类型的锁,其允许多个只读操作并行执行,但写操作会完全互斥。这种锁叫作“多读单写”锁(multiple readers, single writer lock),Go语言提供的这样的锁是sync.RWMutex:

+
var mu sync.RWMutex
+var balance int
+func Balance() int {
+	mu.RLock() // readers lock
+	defer mu.RUnlock()
+	return balance
+}
+
+

Balance函数现在调用了RLock和RUnlock方法来获取和释放一个读取或者共享锁。Deposit函数没有变化,会调用mu.Lock和mu.Unlock方法来获取和释放一个写或互斥锁。

+

在这次修改后,Bob的余额查询请求就可以彼此并行地执行并且会很快地完成了。锁在更多的时间范围可用,并且存款请求也能够及时地被响应了。

+

RLock只能在临界区共享变量没有任何写入操作时可用。一般来说,我们不应该假设逻辑上的只读函数/方法也不会去更新某一些变量。比如一个方法功能是访问一个变量,但它也有可能会同时去给一个内部的计数器+1(译注:可能是记录这个方法的访问次数啥的),或者去更新缓存——使即时的调用能够更快。如果有疑惑的话,请使用互斥锁。

+

RWMutex只有当获得锁的大部分goroutine都是读操作,而锁在竞争条件下,也就是说,goroutine们必须等待才能获取到锁的时候,RWMutex才是最能带来好处的。RWMutex需要更复杂的内部记录,所以会让它比一般的无竞争锁的mutex慢一些。

+

9.4. 内存同步

+

你可能比较纠结为什么Balance方法需要用到互斥条件,无论是基于channel还是基于互斥量。毕竟和存款不一样,它只由一个简单的操作组成,所以不会碰到其它goroutine在其执行“期间”执行其它逻辑的风险。这里使用mutex有两方面考虑。第一Balance不会在其它操作比如Withdraw“中间”执行。第二(更重要的)是“同步”不仅仅是一堆goroutine执行顺序的问题,同样也会涉及到内存的问题。

+

在现代计算机中可能会有一堆处理器,每一个都会有其本地缓存(local cache)。为了效率,对内存的写入一般会在每一个处理器中缓冲,并在必要时一起flush到主存。这种情况下这些数据可能会以与当初goroutine写入顺序不同的顺序被提交到主存。像channel通信或者互斥量操作这样的原语会使处理器将其聚集的写入flush并commit,这样goroutine在某个时间点上的执行结果才能被其它处理器上运行的goroutine得到。

+

考虑一下下面代码片段的可能输出:

+
var x, y int
+go func() {
+	x = 1 // A1
+	fmt.Print("y:", y, " ") // A2
+}()
+go func() {
+	y = 1                   // B1
+	fmt.Print("x:", x, " ") // B2
+}()
+
+

因为两个goroutine是并发执行,并且访问共享变量时也没有互斥,会有数据竞争,所以程序的运行结果没法预测的话也请不要惊讶。我们可能希望它能够打印出下面这四种结果中的一种,相当于几种不同的交错执行时的情况:

+
y:0 x:1
+x:0 y:1
+x:1 y:1
+y:1 x:1
+
+

第四行可以被解释为执行顺序A1,B1,A2,B2或者B1,A1,A2,B2的执行结果。然而实际运行时还是有些情况让我们有点惊讶:

+
x:0 y:0
+y:0 x:0
+
+

根据所使用的编译器,CPU,或者其它很多影响因子,这两种情况也是有可能发生的。那么这两种情况要怎么解释呢?

+

在一个独立的goroutine中,每一个语句的执行顺序是可以被保证的,也就是说goroutine内顺序是连贯的。但是在不使用channel且不使用mutex这样的显式同步操作时,我们就没法保证事件在不同的goroutine中看到的执行顺序是一致的了。尽管goroutine A中一定需要观察到x=1执行成功之后才会去读取y,但它没法确保自己观察得到goroutine B中对y的写入,所以A还可能会打印出y的一个旧版的值。

+

尽管去理解并发的一种尝试是去将其运行理解为不同goroutine语句的交错执行,但看看上面的例子,这已经不是现代的编译器和cpu的工作方式了。因为赋值和打印指向不同的变量,编译器可能会断定两条语句的顺序不会影响执行结果,并且会交换两个语句的执行顺序。如果两个goroutine在不同的CPU上执行,每一个核心有自己的缓存,这样一个goroutine的写入对于其它goroutine的Print,在主存同步之前就是不可见的了。

+

所有并发的问题都可以用一致的、简单的既定的模式来规避。所以可能的话,将变量限定在goroutine内部;如果是多个goroutine都需要访问的变量,使用互斥条件来访问。

+

9.6. 竞争条件检测

+

即使我们小心到不能再小心,但在并发程序中犯错还是太容易了。幸运的是,Go的runtime和工具链为我们装备了一个复杂但好用的动态分析工具,竞争检查器(the race detector)。

+

只要在go build,go run或者go test命令后面加上-race的flag,就会使编译器创建一个你的应用的“修改”版或者一个附带了能够记录所有运行期对共享变量访问工具的test,并且会记录下每一个读或者写共享变量的goroutine的身份信息。另外,修改版的程序会记录下所有的同步事件,比如go语句,channel操作,以及对(*sync.Mutex).Lock(*sync.WaitGroup).Wait等等的调用。(完整的同步事件集合是在The Go Memory Model文档中有说明,该文档是和语言文档放在一起的。译注:https://golang.org/ref/mem )

+

竞争检查器会检查这些事件,会寻找在哪一个goroutine中出现了这样的case,例如其读或者写了一个共享变量,这个共享变量是被另一个goroutine在没有进行干预同步操作便直接写入的。这种情况也就表明了是对一个共享变量的并发访问,即数据竞争。这个工具会打印一份报告,内容包含变量身份,读取和写入的goroutine中活跃的函数的调用栈。这些信息在定位问题时通常很有用。9.7节中会有一个竞争检查器的实战样例。

+

竞争检查器会报告所有的已经发生的数据竞争。然而,它只能检测到运行时的竞争条件;并不能证明之后不会发生数据竞争。所以为了使结果尽量正确,请保证你的测试并发地覆盖到了你的包。

+

由于需要额外的记录,因此构建时加了竞争检测的程序跑起来会慢一些,且需要更大的内存,即使是这样,这些代价对于很多生产环境的程序(工作)来说还是可以接受的。对于一些偶发的竞争条件来说,让竞争检查器来干活可以节省无数日夜的debugging。(译注:多少服务端C和C++程序员为此竞折腰。)

+

9.7. 示例: 并发的非阻塞缓存

+

本节中我们会做一个无阻塞的缓存,这种工具可以帮助我们来解决现实世界中并发程序出现但没有现成的库可以解决的问题。这个问题叫作缓存(memoizing)函数(译注:Memoization的定义: memoization 一词是Donald Michie 根据拉丁语memorandum杜撰的一个词。相应的动词、过去分词、ing形式有memoiz、memoized、memoizing),也就是说,我们需要缓存函数的返回结果,这样在对函数进行调用的时候,我们就只需要一次计算,之后只要返回计算的结果就可以了。我们的解决方案会是并发安全且会避免对整个缓存加锁而导致所有操作都去争一个锁的设计。

+

我们将使用下面的httpGetBody函数作为我们需要缓存的函数的一个样例。这个函数会去进行HTTP GET请求并且获取http响应body。对这个函数的调用本身开销是比较大的,所以我们尽量避免在不必要的时候反复调用。

+
func httpGetBody(url string) (interface{}, error) {
+	resp, err := http.Get(url)
+	if err != nil {
+		return nil, err
+	}
+	defer resp.Body.Close()
+	return ioutil.ReadAll(resp.Body)
+}
+
+

最后一行稍微隐藏了一些细节。ReadAll会返回两个结果,一个[]byte数组和一个错误,不过这两个对象可以被赋值给httpGetBody的返回声明里的interface{}和error类型,所以我们也就可以这样返回结果并且不需要额外的工作了。我们在httpGetBody中选用这种返回类型是为了使其可以与缓存匹配。

+

下面是我们要设计的cache的第一个“草稿”:

+

gopl.io/ch9/memo1

+
// Package memo provides a concurrency-unsafe
+// memoization of a function of type Func.
+package memo
+
+// A Memo caches the results of calling a Func.
+type Memo struct {
+	f     Func
+	cache map[string]result
+}
+
+// Func is the type of the function to memoize.
+type Func func(key string) (interface{}, error)
+
+type result struct {
+	value interface{}
+	err   error
+}
+
+func New(f Func) *Memo {
+	return &Memo{f: f, cache: make(map[string]result)}
+}
+
+// NOTE: not concurrency-safe!
+func (memo *Memo) Get(key string) (interface{}, error) {
+	res, ok := memo.cache[key]
+	if !ok {
+		res.value, res.err = memo.f(key)
+		memo.cache[key] = res
+	}
+	return res.value, res.err
+}
+
+

Memo实例会记录需要缓存的函数f(类型为Func),以及缓存内容(里面是一个string到result映射的map)。每一个result都是简单的函数返回的值对儿——一个值和一个错误值。继续下去我们会展示一些Memo的变种,不过所有的例子都会遵循上面的这些方面。

+

下面是一个使用Memo的例子。对于流入的URL的每一个元素我们都会调用Get,并打印调用延时以及其返回的数据大小的log:

+
m := memo.New(httpGetBody)
+for url := range incomingURLs() {
+	start := time.Now()
+	value, err := m.Get(url)
+	if err != nil {
+		log.Print(err)
+	}
+	fmt.Printf("%s, %s, %d bytes\n",
+	url, time.Since(start), len(value.([]byte)))
+}
+
+

我们可以使用测试包(第11章的主题)来系统地鉴定缓存的效果。从下面的测试输出,我们可以看到URL流包含了一些重复的情况,尽管我们第一次对每一个URL的(*Memo).Get的调用都会花上几百毫秒,但第二次就只需要花1毫秒就可以返回完整的数据了。

+
$ go test -v gopl.io/ch9/memo1
+=== RUN   Test
+https://golang.org, 175.026418ms, 7537 bytes
+https://godoc.org, 172.686825ms, 6878 bytes
+https://play.golang.org, 115.762377ms, 5767 bytes
+http://gopl.io, 749.887242ms, 2856 bytes
+https://golang.org, 721ns, 7537 bytes
+https://godoc.org, 152ns, 6878 bytes
+https://play.golang.org, 205ns, 5767 bytes
+http://gopl.io, 326ns, 2856 bytes
+--- PASS: Test (1.21s)
+PASS
+ok  gopl.io/ch9/memo1   1.257s
+
+

这个测试是顺序地去做所有的调用的。

+

由于这种彼此独立的HTTP请求可以很好地并发,我们可以把这个测试改成并发形式。可以使用sync.WaitGroup来等待所有的请求都完成之后再返回。

+
m := memo.New(httpGetBody)
+var n sync.WaitGroup
+for url := range incomingURLs() {
+	n.Add(1)
+	go func(url string) {
+		start := time.Now()
+		value, err := m.Get(url)
+		if err != nil {
+			log.Print(err)
+		}
+		fmt.Printf("%s, %s, %d bytes\n",
+		url, time.Since(start), len(value.([]byte)))
+		n.Done()
+	}(url)
+}
+n.Wait()
+
+

这次测试跑起来更快了,然而不幸的是貌似这个测试不是每次都能够正常工作。我们注意到有一些意料之外的cache miss(缓存未命中),或者命中了缓存但却返回了错误的值,或者甚至会直接崩溃。

+

但更糟糕的是,有时候这个程序还是能正确的运行(译:也就是最让人崩溃的偶发bug),所以我们甚至可能都不会意识到这个程序有bug。但是我们可以使用-race这个flag来运行程序,竞争检测器(§9.6)会打印像下面这样的报告:

+
$ go test -run=TestConcurrent -race -v gopl.io/ch9/memo1
+=== RUN   TestConcurrent
+...
+WARNING: DATA RACE
+Write by goroutine 36:
+  runtime.mapassign1()
+      ~/go/src/runtime/hashmap.go:411 +0x0
+  gopl.io/ch9/memo1.(*Memo).Get()
+      ~/gobook2/src/gopl.io/ch9/memo1/memo.go:32 +0x205
+  ...
+Previous write by goroutine 35:
+  runtime.mapassign1()
+      ~/go/src/runtime/hashmap.go:411 +0x0
+  gopl.io/ch9/memo1.(*Memo).Get()
+      ~/gobook2/src/gopl.io/ch9/memo1/memo.go:32 +0x205
+...
+Found 1 data race(s)
+FAIL    gopl.io/ch9/memo1   2.393s
+
+

memo.go的32行出现了两次,说明有两个goroutine在没有同步干预的情况下更新了cache map。这表明Get不是并发安全的,存在数据竞争。

+
28  func (memo *Memo) Get(key string) (interface{}, error) {
+29      res, ok := memo.cache(key)
+30      if !ok {
+31          res.value, res.err = memo.f(key)
+32          memo.cache[key] = res
+33      }
+34      return res.value, res.err
+35  }
+
+

最简单的使cache并发安全的方式是使用基于监控的同步。只要给Memo加上一个mutex,在Get的一开始获取互斥锁,return的时候释放锁,就可以让cache的操作发生在临界区内了:

+

gopl.io/ch9/memo2

+
type Memo struct {
+	f     Func
+	mu    sync.Mutex // guards cache
+	cache map[string]result
+}
+
+// Get is concurrency-safe.
+func (memo *Memo) Get(key string) (value interface{}, err error) {
+	memo.mu.Lock()
+	res, ok := memo.cache[key]
+    if !ok {
+		res.value, res.err = memo.f(key)
+		memo.cache[key] = res
+	}
+	memo.mu.Unlock()
+	return res.value, res.err
+}
+
+

测试依然并发进行,但这回竞争检查器“沉默”了。不幸的是对于Memo的这一点改变使我们完全丧失了并发的性能优点。每次对f的调用期间都会持有锁,Get将本来可以并行运行的I/O操作串行化了。我们本章的目的是完成一个无锁缓存,而不是现在这样的将所有请求串行化的函数的缓存。

+

下一个Get的实现,调用Get的goroutine会两次获取锁:查找阶段获取一次,如果查找没有返回任何内容,那么进入更新阶段会再次获取。在这两次获取锁的中间阶段,其它goroutine可以随意使用cache。

+

gopl.io/ch9/memo3

+
func (memo *Memo) Get(key string) (value interface{}, err error) {
+	memo.mu.Lock()
+	res, ok := memo.cache[key]
+	memo.mu.Unlock()
+	if !ok {
+		res.value, res.err = memo.f(key)
+
+		// Between the two critical sections, several goroutines
+		// may race to compute f(key) and update the map.
+		memo.mu.Lock()
+		memo.cache[key] = res
+		memo.mu.Unlock()
+	}
+	return res.value, res.err
+}
+
+

这些修改使性能再次得到了提升,但有一些URL被获取了两次。这种情况在两个以上的goroutine同一时刻调用Get来请求同样的URL时会发生。多个goroutine一起查询cache,发现没有值,然后一起调用f这个慢不拉叽的函数。在得到结果后,也都会去更新map。其中一个获得的结果会覆盖掉另一个的结果。

+

理想情况下是应该避免掉多余的工作的。而这种“避免”工作一般被称为duplicate suppression(重复抑制/避免)。下面版本的Memo每一个map元素都是指向一个条目的指针。每一个条目包含对函数f调用结果的内容缓存。与之前不同的是这次entry还包含了一个叫ready的channel。在条目的结果被设置之后,这个channel就会被关闭,以向其它goroutine广播(§8.9)去读取该条目内的结果是安全的了。

+

gopl.io/ch9/memo4

+
type entry struct {
+	res   result
+	ready chan struct{} // closed when res is ready
+}
+
+func New(f Func) *Memo {
+	return &Memo{f: f, cache: make(map[string]*entry)}
+}
+
+type Memo struct {
+	f     Func
+	mu    sync.Mutex // guards cache
+	cache map[string]*entry
+}
+
+func (memo *Memo) Get(key string) (value interface{}, err error) {
+	memo.mu.Lock()
+	e := memo.cache[key]
+	if e == nil {
+		// This is the first request for this key.
+		// This goroutine becomes responsible for computing
+		// the value and broadcasting the ready condition.
+		e = &entry{ready: make(chan struct{})}
+		memo.cache[key] = e
+		memo.mu.Unlock()
+
+		e.res.value, e.res.err = memo.f(key)
+
+		close(e.ready) // broadcast ready condition
+	} else {
+		// This is a repeat request for this key.
+		memo.mu.Unlock()
+
+		<-e.ready // wait for ready condition
+	}
+	return e.res.value, e.res.err
+}
+
+

现在Get函数包括下面这些步骤了:获取互斥锁来保护共享变量cache map,查询map中是否存在指定条目,如果没有找到那么分配空间插入一个新条目,释放互斥锁。如果存在条目的话且其值没有写入完成(也就是有其它的goroutine在调用f这个慢函数)时,goroutine必须等待值ready之后才能读到条目的结果。而想知道是否ready的话,可以直接从ready channel中读取,由于这个读取操作在channel关闭之前一直是阻塞。

+

如果没有条目的话,需要向map中插入一个没有准备好的条目,当前正在调用的goroutine就需要负责调用慢函数、更新条目以及向其它所有goroutine广播条目已经ready可读的消息了。

+

条目中的e.res.value和e.res.err变量是在多个goroutine之间共享的。创建条目的goroutine同时也会设置条目的值,其它goroutine在收到"ready"的广播消息之后立刻会去读取条目的值。尽管会被多个goroutine同时访问,但却并不需要互斥锁。ready channel的关闭一定会发生在其它goroutine接收到广播事件之前,因此第一个goroutine对这些变量的写操作是一定发生在这些读操作之前的。不会发生数据竞争。

+

这样并发、不重复、无阻塞的cache就完成了。

+

上面这样Memo的实现使用了一个互斥量来保护多个goroutine调用Get时的共享map变量。不妨把这种设计和前面提到的把map变量限制在一个单独的monitor goroutine的方案做一些对比,后者在调用Get时需要发消息。

+

Func、result和entry的声明和之前保持一致:

+
// Func is the type of the function to memoize.
+type Func func(key string) (interface{}, error)
+
+// A result is the result of calling a Func.
+type result struct {
+	value interface{}
+	err   error
+}
+
+type entry struct {
+	res   result
+	ready chan struct{} // closed when res is ready
+}
+
+

然而Memo类型现在包含了一个叫做requests的channel,Get的调用方用这个channel来和monitor goroutine来通信。requests channel中的元素类型是request。Get的调用方会把这个结构中的两组key都填充好,实际上用这两个变量来对函数进行缓存的。另一个叫response的channel会被拿来发送响应结果。这个channel只会传回一个单独的值。

+

gopl.io/ch9/memo5

+
// A request is a message requesting that the Func be applied to key.
+type request struct {
+	key      string
+	response chan<- result // the client wants a single result
+}
+
+type Memo struct{ requests chan request }
+// New returns a memoization of f.  Clients must subsequently call Close.
+func New(f Func) *Memo {
+	memo := &Memo{requests: make(chan request)}
+	go memo.server(f)
+	return memo
+}
+
+func (memo *Memo) Get(key string) (interface{}, error) {
+	response := make(chan result)
+	memo.requests <- request{key, response}
+	res := <-response
+	return res.value, res.err
+}
+
+func (memo *Memo) Close() { close(memo.requests) }
+
+

上面的Get方法,会创建一个response channel,把它放进request结构中,然后发送给monitor goroutine,然后马上又会接收它。

+

cache变量被限制在了monitor goroutine ``(*Memo).server`中,下面会看到。monitor会在循环中一直读取请求,直到request channel被Close方法关闭。每一个请求都会去查询cache,如果没有找到条目的话,那么就会创建/插入一个新的条目。

+
func (memo *Memo) server(f Func) {
+	cache := make(map[string]*entry)
+	for req := range memo.requests {
+		e := cache[req.key]
+		if e == nil {
+			// This is the first request for this key.
+			e = &entry{ready: make(chan struct{})}
+			cache[req.key] = e
+			go e.call(f, req.key) // call f(key)
+		}
+		go e.deliver(req.response)
+	}
+}
+
+func (e *entry) call(f Func, key string) {
+	// Evaluate the function.
+	e.res.value, e.res.err = f(key)
+	// Broadcast the ready condition.
+	close(e.ready)
+}
+
+func (e *entry) deliver(response chan<- result) {
+	// Wait for the ready condition.
+	<-e.ready
+	// Send the result to the client.
+	response <- e.res
+}
+
+

和基于互斥量的版本类似,第一个对某个key的请求需要负责去调用函数f并传入这个key,将结果存在条目里,并关闭ready channel来广播条目的ready消息。使用(*entry).call来完成上述工作。

+

紧接着对同一个key的请求会发现map中已经有了存在的条目,然后会等待结果变为ready,并将结果从response发送给客户端的goroutien。上述工作是用(*entry).deliver来完成的。对call和deliver方法的调用必须让它们在自己的goroutine中进行以确保monitor goroutines不会因此而被阻塞住而没法处理新的请求。

+

这个例子说明我们无论用上锁,还是通信来建立并发程序都是可行的。

+

上面的两种方案并不好说特定情境下哪种更好,不过了解他们还是有价值的。有时候从一种方式切换到另一种可以使你的代码更为简洁。(译注:不是说好的golang推崇通信并发么。)

+

练习 9.3: 扩展Func类型和(*Memo).Get方法,支持调用方提供一个可选的done channel,使其具备通过该channel来取消整个操作的能力(§8.9)。一个被取消了的Func的调用结果不应该被缓存。

+

9.8. Goroutines和线程

+

在上一章中我们说goroutine和操作系统的线程区别可以先忽略。尽管两者的区别实际上只是一个量的区别,但量变会引起质变的道理同样适用于goroutine和线程。现在正是我们来区分开两者的最佳时机。

+

9.8.1. 动态栈

+

每一个OS线程都有一个固定大小的内存块(一般会是2MB)来做栈,这个栈会用来存储当前正在被调用或挂起(指在调用其它函数时)的函数的内部变量。这个固定大小的栈同时很大又很小。因为2MB的栈对于一个小小的goroutine来说是很大的内存浪费,比如对于我们用到的,一个只是用来WaitGroup之后关闭channel的goroutine来说。而对于go程序来说,同时创建成百上千个goroutine是非常普遍的,如果每一个goroutine都需要这么大的栈的话,那这么多的goroutine就不太可能了。除去大小的问题之外,固定大小的栈对于更复杂或者更深层次的递归函数调用来说显然是不够的。修改固定的大小可以提升空间的利用率,允许创建更多的线程,并且可以允许更深的递归调用,不过这两者是没法同时兼备的。

+

相反,一个goroutine会以一个很小的栈开始其生命周期,一般只需要2KB。一个goroutine的栈,和操作系统线程一样,会保存其活跃或挂起的函数调用的本地变量,但是和OS线程不太一样的是,一个goroutine的栈大小并不是固定的;栈的大小会根据需要动态地伸缩。而goroutine的栈的最大值有1GB,比传统的固定大小的线程栈要大得多,尽管一般情况下,大多goroutine都不需要这么大的栈。

+

** 练习 9.4:** 创建一个流水线程序,支持用channel连接任意数量的goroutine,在跑爆内存之前,可以创建多少流水线阶段?一个变量通过整个流水线需要用多久?(这个练习题翻译不是很确定)

+

9.8.2. Goroutine调度

+

OS线程会被操作系统内核调度。每几毫秒,一个硬件计时器会中断处理器,这会调用一个叫作scheduler的内核函数。这个函数会挂起当前执行的线程并将它的寄存器内容保存到内存中,检查线程列表并决定下一次哪个线程可以被运行,并从内存中恢复该线程的寄存器信息,然后恢复执行该线程的现场并开始执行线程。因为操作系统线程是被内核所调度,所以从一个线程向另一个“移动”需要完整的上下文切换,也就是说,保存一个用户线程的状态到内存,恢复另一个线程的到寄存器,然后更新调度器的数据结构。这几步操作很慢,因为其局部性很差需要几次内存访问,并且会增加运行的cpu周期。

+

Go的运行时包含了其自己的调度器,这个调度器使用了一些技术手段,比如m:n调度,因为其会在n个操作系统线程上多工(调度)m个goroutine。Go调度器的工作和内核的调度是相似的,但是这个调度器只关注单独的Go程序中的goroutine(译注:按程序独立)。

+

和操作系统的线程调度不同的是,Go调度器并不是用一个硬件定时器,而是被Go语言“建筑”本身进行调度的。例如当一个goroutine调用了time.Sleep,或者被channel调用或者mutex操作阻塞时,调度器会使其进入休眠并开始执行另一个goroutine,直到时机到了再去唤醒第一个goroutine。因为这种调度方式不需要进入内核的上下文,所以重新调度一个goroutine比调度一个线程代价要低得多。

+

** 练习 9.5: ** 写一个有两个goroutine的程序,两个goroutine会向两个无buffer channel反复地发送ping-pong消息。这样的程序每秒可以支持多少次通信?

+

9.8.3. GOMAXPROCS

+

Go的调度器使用了一个叫做GOMAXPROCS的变量来决定会有多少个操作系统的线程同时执行Go的代码。其默认的值是运行机器上的CPU的核心数,所以在一个有8个核心的机器上时,调度器一次会在8个OS线程上去调度GO代码。(GOMAXPROCS是前面说的m:n调度中的n)。在休眠中的或者在通信中被阻塞的goroutine是不需要一个对应的线程来做调度的。在I/O中或系统调用中或调用非Go语言函数时,是需要一个对应的操作系统线程的,但是GOMAXPROCS并不需要将这几种情况计算在内。

+

你可以用GOMAXPROCS的环境变量来显式地控制这个参数,或者也可以在运行时用runtime.GOMAXPROCS函数来修改它。我们在下面的小程序中会看到GOMAXPROCS的效果,这个程序会无限打印0和1。

+
for {
+	go fmt.Print(0)
+	fmt.Print(1)
+}
+
+$ GOMAXPROCS=1 go run hacker-cliché.go
+111111111111111111110000000000000000000011111...
+
+$ GOMAXPROCS=2 go run hacker-cliché.go
+010101010101010101011001100101011010010100110...
+
+

在第一次执行时,最多同时只能有一个goroutine被执行。初始情况下只有main goroutine被执行,所以会打印很多1。过了一段时间后,GO调度器会将其置为休眠,并唤醒另一个goroutine,这时候就开始打印很多0了,在打印的时候,goroutine是被调度到操作系统线程上的。在第二次执行时,我们使用了两个操作系统线程,所以两个goroutine可以一起被执行,以同样的频率交替打印0和1。我们必须强调的是goroutine的调度是受很多因子影响的,而runtime也是在不断地发展演进的,所以这里的你实际得到的结果可能会因为版本的不同而与我们运行的结果有所不同。

+

** 练习9.6:** 测试一下计算密集型的并发程序(练习8.5那样的)会被GOMAXPROCS怎样影响到。在你的电脑上最佳的值是多少?你的电脑CPU有多少个核心?

+

9.8.4. Goroutine没有ID号

+

在大多数支持多线程的操作系统和程序语言中,当前的线程都有一个独特的身份(id),并且这个身份信息可以以一个普通值的形式被很容易地获取到,典型的可以是一个integer或者指针值。这种情况下我们做一个抽象化的thread-local storage(线程本地存储,多线程编程中不希望其它线程访问的内容)就很容易,只需要以线程的id作为key的一个map就可以解决问题,每一个线程以其id就能从中获取到值,且和其它线程互不冲突。

+

goroutine没有可以被程序员获取到的身份(id)的概念。这一点是设计上故意而为之,由于thread-local storage总是会被滥用。比如说,一个web server是用一种支持tls的语言实现的,而非常普遍的是很多函数会去寻找HTTP请求的信息,这代表它们就是去其存储层(这个存储层有可能是tls)查找的。这就像是那些过分依赖全局变量的程序一样,会导致一种非健康的“距离外行为”,在这种行为下,一个函数的行为可能并不仅由自己的参数所决定,而是由其所运行在的线程所决定。因此,如果线程本身的身份会改变——比如一些worker线程之类的——那么函数的行为就会变得神秘莫测。

+

Go鼓励更为简单的模式,这种模式下参数(译注:外部显式参数和内部显式参数。tls 中的内容算是"外部"隐式参数)对函数的影响都是显式的。这样不仅使程序变得更易读,而且会让我们自由地向一些给定的函数分配子任务时不用担心其身份信息影响行为。

+

你现在应该已经明白了写一个Go程序所需要的所有语言特性信息。在后面两章节中,我们会回顾一些之前的实例和工具,支持我们写出更大规模的程序:如何将一个工程组织成一系列的包,如何获取,构建,测试,性能测试,剖析,写文档,并且将这些包分享出去。

+

第10章 包和工具

+

现在随便一个小程序的实现都可能包含超过10000个函数。然而作者一般只需要考虑其中很小的一部分和做很少的设计,因为绝大部分代码都是由他人编写的,它们通过类似包或模块的方式被重用。

+

Go语言有超过100个的标准包(译注:可以用go list std | wc -l命令查看标准包的具体数目),标准库为大多数的程序提供了必要的基础构件。在Go的社区,有很多成熟的包被设计、共享、重用和改进,目前互联网上已经发布了非常多的Go语言开源包,它们可以通过 http://godoc.org 检索。在本章,我们将演示如何使用已有的包和创建新的包。

+

Go还自带了工具箱,里面有很多用来简化工作区和包管理的小工具。在本书开始的时候,我们已经见识过如何使用工具箱自带的工具来下载、构建和运行我们的演示程序了。在本章,我们将看看这些工具的基本设计理论和尝试更多的功能,例如打印工作区中包的文档和查询相关的元数据等。在下一章,我们将探讨testing包的单元测试用法。

+

10.1. 包简介

+

任何包系统设计的目的都是为了简化大型程序的设计和维护工作,通过将一组相关的特性放进一个独立的单元以便于理解和更新,在每个单元更新的同时保持和程序中其它单元的相对独立性。这种模块化的特性允许每个包可以被其它的不同项目共享和重用,在项目范围内、甚至全球范围统一的分发和复用。

+

每个包一般都定义了一个不同的名字空间用于它内部的每个标识符的访问。每个名字空间关联到一个特定的包,让我们给类型、函数等选择简短明了的名字,这样可以在使用它们的时候减少和其它部分名字的冲突。

+

每个包还通过控制包内名字的可见性和是否导出来实现封装特性。通过限制包成员的可见性并隐藏包API的具体实现,将允许包的维护者在不影响外部包用户的前提下调整包的内部实现。通过限制包内变量的可见性,还可以强制用户通过某些特定函数来访问和更新内部变量,这样可以保证内部变量的一致性和并发时的互斥约束。

+

当我们修改了一个源文件,我们必须重新编译该源文件对应的包和所有依赖该包的其他包。即使是从头构建,Go语言编译器的编译速度也明显快于其它编译语言。Go语言的闪电般的编译速度主要得益于三个语言特性。第一点,所有导入的包必须在每个文件的开头显式声明,这样的话编译器就没有必要读取和分析整个源文件来判断包的依赖关系。第二点,禁止包的环状依赖,因为没有循环依赖,包的依赖关系形成一个有向无环图,每个包可以被独立编译,而且很可能是被并发编译。第三点,编译后包的目标文件不仅仅记录包本身的导出信息,目标文件同时还记录了包的依赖关系。因此,在编译一个包的时候,编译器只需要读取每个直接导入包的目标文件,而不需要遍历所有依赖的的文件(译注:很多都是重复的间接依赖)。

+

10.2. 导入路径

+

每个包是由一个全局唯一的字符串所标识的导入路径定位。出现在import语句中的导入路径也是字符串。

+
import (
+	"fmt"
+	"math/rand"
+	"encoding/json"
+
+	"golang.org/x/net/html"
+
+	"github.com/go-sql-driver/mysql"
+)
+
+

就像我们在2.6.1节提到过的,Go语言的规范并没有指明包的导入路径字符串的具体含义,导入路径的具体含义是由构建工具来解释的。在本章,我们将深入讨论Go语言工具箱的功能,包括大家经常使用的构建测试等功能。当然,也有第三方扩展的工具箱存在。例如,Google公司内部的Go语言码农,他们就使用内部的多语言构建系统(译注:Google公司使用的是类似Bazel的构建系统,支持多种编程语言,目前该构件系统还不能完整支持Windows环境),用不同的规则来处理包名字和定位包,用不同的规则来处理单元测试等等,因为这样可以更紧密适配他们内部环境。

+

如果你计划分享或发布包,那么导入路径最好是全球唯一的。为了避免冲突,所有非标准库包的导入路径建议以所在组织的互联网域名为前缀;而且这样也有利于包的检索。例如,上面的import语句导入了Go团队维护的HTML解析器和一个流行的第三方维护的MySQL驱动。

+

10.3. 包声明

+

在每个Go语言源文件的开头都必须有包声明语句。包声明语句的主要目的是确定当前包被其它包导入时默认的标识符(也称为包名)。

+

例如,math/rand包的每个源文件的开头都包含package rand包声明语句,所以当你导入这个包,你就可以用rand.Int、rand.Float64类似的方式访问包的成员。

+
package main
+
+import (
+	"fmt"
+	"math/rand"
+)
+
+func main() {
+	fmt.Println(rand.Int())
+}
+
+

通常来说,默认的包名就是包导入路径名的最后一段,因此即使两个包的导入路径不同,它们依然可能有一个相同的包名。例如,math/rand包和crypto/rand包的包名都是rand。稍后我们将看到如何同时导入两个有相同包名的包。

+

关于默认包名一般采用导入路径名的最后一段的约定也有三种例外情况。第一个例外,包对应一个可执行程序,也就是main包,这时候main包本身的导入路径是无关紧要的。名字为main的包是给go build(§10.7.3)构建命令一个信息,这个包编译完之后必须调用连接器生成一个可执行程序。

+

第二个例外,包所在的目录中可能有一些文件名是以_test.go为后缀的Go源文件(译注:前面必须有其它的字符,因为以_.开头的源文件会被构建工具忽略),并且这些源文件声明的包名也是以_test为后缀名的。这种目录可以包含两种包:一种是普通包,另一种则是测试的外部扩展包。所有以_test为后缀包名的测试外部扩展包都由go test命令独立编译,普通包和测试的外部扩展包是相互独立的。测试的外部扩展包一般用来避免测试代码中的循环导入依赖,具体细节我们将在11.2.4节中介绍。

+

第三个例外,一些依赖版本号的管理工具会在导入路径后追加版本号信息,例如“gopkg.in/yaml.v2”。这种情况下包的名字并不包含版本号后缀,而是yaml。

+

10.4. 导入声明

+

可以在一个Go语言源文件包声明语句之后,其它非导入声明语句之前,包含零到多个导入包声明语句。每个导入声明可以单独指定一个导入路径,也可以通过圆括号同时导入多个导入路径。下面两个导入形式是等价的,但是第二种形式更为常见。

+
import "fmt"
+import "os"
+
+import (
+	"fmt"
+	"os"
+)
+
+

导入的包之间可以通过添加空行来分组;通常将来自不同组织的包独自分组。包的导入顺序无关紧要,但是在每个分组中一般会根据字符串顺序排列。(gofmt和goimports工具都可以将不同分组导入的包独立排序。)

+
import (
+	"fmt"
+	"html/template"
+	"os"
+
+	"golang.org/x/net/html"
+	"golang.org/x/net/ipv4"
+)
+
+

如果我们想同时导入两个有着名字相同的包,例如math/rand包和crypto/rand包,那么导入声明必须至少为一个同名包指定一个新的包名以避免冲突。这叫做导入包的重命名。

+
import (
+	"crypto/rand"
+	mrand "math/rand" // alternative name mrand avoids conflict
+)
+
+

导入包的重命名只影响当前的源文件。其它的源文件如果导入了相同的包,可以用导入包原本默认的名字或重命名为另一个完全不同的名字。

+

导入包重命名是一个有用的特性,它不仅仅只是为了解决名字冲突。如果导入的一个包名很笨重,特别是在一些自动生成的代码中,这时候用一个简短名称会更方便。选择用简短名称重命名导入包时候最好统一,以避免包名混乱。选择另一个包名称还可以帮助避免和本地普通变量名产生冲突。例如,如果文件中已经有了一个名为path的变量,那么我们可以将“path”标准包重命名为pathpkg。

+

每个导入声明语句都明确指定了当前包和被导入包之间的依赖关系。如果遇到包循环导入的情况,Go语言的构建工具将报告错误。

+

10.5. 包的匿名导入

+

如果只是导入一个包而并不使用导入的包将会导致一个编译错误。但是有时候我们只是想利用导入包而产生的副作用:它会计算包级变量的初始化表达式和执行导入包的init初始化函数(§2.6.2)。这时候我们需要抑制“unused import”编译错误,我们可以用下划线_来重命名导入的包。像往常一样,下划线_为空白标识符,并不能被访问。

+
import _ "image/png" // register PNG decoder
+
+

这个被称为包的匿名导入。它通常是用来实现一个编译时机制,然后通过在main主程序入口选择性地导入附加的包。首先,让我们看看如何使用该特性,然后再看看它是如何工作的。

+

标准库的image图像包包含了一个Decode函数,用于从io.Reader接口读取数据并解码图像,它调用底层注册的图像解码器来完成任务,然后返回image.Image类型的图像。使用image.Decode很容易编写一个图像格式的转换工具,读取一种格式的图像,然后编码为另一种图像格式:

+

gopl.io/ch10/jpeg

+
// The jpeg command reads a PNG image from the standard input
+// and writes it as a JPEG image to the standard output.
+package main
+
+import (
+	"fmt"
+	"image"
+	"image/jpeg"
+	_ "image/png" // register PNG decoder
+	"io"
+	"os"
+)
+
+func main() {
+	if err := toJPEG(os.Stdin, os.Stdout); err != nil {
+		fmt.Fprintf(os.Stderr, "jpeg: %v\n", err)
+		os.Exit(1)
+	}
+}
+
+func toJPEG(in io.Reader, out io.Writer) error {
+	img, kind, err := image.Decode(in)
+	if err != nil {
+		return err
+	}
+	fmt.Fprintln(os.Stderr, "Input format =", kind)
+	return jpeg.Encode(out, img, &jpeg.Options{Quality: 95})
+}
+
+

如果我们将gopl.io/ch3/mandelbrot(§3.3)的输出导入到这个程序的标准输入,它将解码输入的PNG格式图像,然后转换为JPEG格式的图像输出(图3.3)。

+
$ go build gopl.io/ch3/mandelbrot
+$ go build gopl.io/ch10/jpeg
+$ ./mandelbrot | ./jpeg >mandelbrot.jpg
+Input format = png
+
+

要注意image/png包的匿名导入语句。如果没有这一行语句,程序依然可以编译和运行,但是它将不能正确识别和解码PNG格式的图像:

+
$ go build gopl.io/ch10/jpeg
+$ ./mandelbrot | ./jpeg >mandelbrot.jpg
+jpeg: image: unknown format
+
+

下面的代码演示了它的工作机制。标准库还提供了GIF、PNG和JPEG等格式图像的解码器,用户也可以提供自己的解码器,但是为了保持程序体积较小,很多解码器并没有被全部包含,除非是明确需要支持的格式。image.Decode函数在解码时会依次查询支持的格式列表。每个格式驱动列表的每个入口指定了四件事情:格式的名称;一个用于描述这种图像数据开头部分模式的字符串,用于解码器检测识别;一个Decode函数用于完成解码图像工作;一个DecodeConfig函数用于解码图像的大小和颜色空间的信息。每个驱动入口是通过调用image.RegisterFormat函数注册,一般是在每个格式包的init初始化函数中调用,例如image/png包是这样注册的:

+
package png // image/png
+
+func Decode(r io.Reader) (image.Image, error)
+func DecodeConfig(r io.Reader) (image.Config, error)
+
+func init() {
+	const pngHeader = "\x89PNG\r\n\x1a\n"
+	image.RegisterFormat("png", pngHeader, Decode, DecodeConfig)
+}
+
+

最终的效果是,主程序只需要匿名导入特定图像驱动包就可以用image.Decode解码对应格式的图像了。

+

数据库包database/sql也是采用了类似的技术,让用户可以根据自己需要选择导入必要的数据库驱动。例如:

+
import (
+	"database/sql"
+	_ "github.com/lib/pq"              // enable support for Postgres
+	_ "github.com/go-sql-driver/mysql" // enable support for MySQL
+)
+
+db, err = sql.Open("postgres", dbname) // OK
+db, err = sql.Open("mysql", dbname)    // OK
+db, err = sql.Open("sqlite3", dbname)  // returns error: unknown driver "sqlite3"
+
+

练习 10.1: 扩展jpeg程序,以支持任意图像格式之间的相互转换,使用image.Decode检测支持的格式类型,然后通过flag命令行标志参数选择输出的格式。

+

练习 10.2: 设计一个通用的压缩文件读取框架,用来读取ZIP(archive/zip)和POSIX tar(archive/tar)格式压缩的文档。使用类似上面的注册技术来扩展支持不同的压缩格式,然后根据需要通过匿名导入选择导入要支持的压缩格式的驱动包。

+

10.6. 包和命名

+

在本节中,我们将提供一些关于Go语言独特的包和成员命名的约定。

+

当创建一个包,一般要用短小的包名,但也不能太短导致难以理解。标准库中最常用的包有bufio、bytes、flag、fmt、http、io、json、os、sort、sync和time等包。

+

尽可能让命名有描述性且无歧义。例如,类似imageutil或ioutilis的工具包命名已经足够简洁了,就无须再命名为util了。要尽量避免包名使用可能被经常用于局部变量的名字,这样可能导致用户重命名导入包,例如前面看到的path包。

+

包名一般采用单数的形式。标准库的bytes、errors和strings使用了复数形式,这是为了避免和预定义的类型冲突,同样还有go/types是为了避免和type关键字冲突。

+

要避免包名有其它的含义。例如,2.5节中我们的温度转换包最初使用了temp包名,虽然并没有持续多久。但这是一个糟糕的尝试,因为temp几乎是临时变量的同义词。然后我们有一段时间使用了temperature作为包名,显然名字并没有表达包的真实用途。最后我们改成了和strconv标准包类似的tempconv包名,这个名字比之前的就好多了。

+

现在让我们看看如何命名包的成员。由于是通过包的导入名字引入包里面的成员,例如fmt.Println,同时包含了包名和成员名信息。因此,我们一般并不需要关注Println的具体内容,因为fmt包名已经包含了这个信息。当设计一个包的时候,需要考虑包名和成员名两个部分如何很好地配合。下面有一些例子:

+
bytes.Equal    flag.Int    http.Get    json.Marshal
+
+

我们可以看到一些常用的命名模式。strings包提供了和字符串相关的诸多操作:

+
package strings
+
+func Index(needle, haystack string) int
+
+type Replacer struct{ /* ... */ }
+func NewReplacer(oldnew ...string) *Replacer
+
+type Reader struct{ /* ... */ }
+func NewReader(s string) *Reader
+
+

包名strings并没有出现在任何成员名字中。因为用户会这样引用这些成员strings.Index、strings.Replacer等。

+

其它一些包,可能只描述了单一的数据类型,例如html/template和math/rand等,只暴露一个主要的数据结构和与它相关的方法,还有一个以New命名的函数用于创建实例。

+
package rand // "math/rand"
+
+type Rand struct{ /* ... */ }
+func New(source Source) *Rand
+
+

这可能导致一些名字重复,例如template.Template或rand.Rand,这就是这些种类的包名往往特别短的原因之一。

+

在另一个极端,还有像net/http包那样含有非常多的名字和种类不多的数据类型,因为它们都是要执行一个复杂的复合任务。尽管有将近二十种类型和更多的函数,但是包中最重要的成员名字却是简单明了的:Get、Post、Handle、Error、Client、Server等。

+

10.7. 工具

+

本章剩下的部分将讨论Go语言工具箱的具体功能,包括如何下载、格式化、构建、测试和安装Go语言编写的程序。

+

Go语言的工具箱集合了一系列功能的命令集。它可以看作是一个包管理器(类似于Linux中的apt和rpm工具),用于包的查询、计算包的依赖关系、从远程版本控制系统下载它们等任务。它也是一个构建系统,计算文件的依赖关系,然后调用编译器、汇编器和链接器构建程序,虽然它故意被设计成没有标准的make命令那么复杂。它也是一个单元测试和基准测试的驱动程序,我们将在第11章讨论测试话题。

+

Go语言工具箱的命令有着类似“瑞士军刀”的风格,带着一打的子命令,有一些我们经常用到,例如get、run、build和fmt等。你可以运行go或go help命令查看内置的帮助文档,为了查询方便,我们列出了最常用的命令:

+
$ go
+...
+	build            compile packages and dependencies
+	clean            remove object files
+	doc              show documentation for package or symbol
+	env              print Go environment information
+	fmt              run gofmt on package sources
+	get              download and install packages and dependencies
+	install          compile and install packages and dependencies
+	list             list packages
+	run              compile and run Go program
+	test             test packages
+	version          print Go version
+	vet              run go tool vet on packages
+
+Use "go help [command]" for more information about a command.
+...
+
+

为了达到零配置的设计目标,Go语言的工具箱很多地方都依赖各种约定。例如,根据给定的源文件的名称,Go语言的工具可以找到源文件对应的包,因为每个目录只包含了单一的包,并且包的导入路径和工作区的目录结构是对应的。给定一个包的导入路径,Go语言的工具可以找到与之对应的存储着实体文件的目录。它还可以根据导入路径找到存储代码的仓库的远程服务器URL。

+

10.7.1. 工作区结构

+

对于大多数的Go语言用户,只需要配置一个名叫GOPATH的环境变量,用来指定当前工作目录即可。当需要切换到不同工作区的时候,只要更新GOPATH就可以了。例如,我们在编写本书时将GOPATH设置为$HOME/gobook

+
$ export GOPATH=$HOME/gobook
+$ go get gopl.io/...
+
+

当你用前面介绍的命令下载本书全部的例子源码之后,你的当前工作区的目录结构应该是这样的:

+
GOPATH/
+	src/
+		gopl.io/
+			.git/
+			ch1/
+				helloworld/
+					main.go
+				dup/
+					main.go
+				...
+		golang.org/x/net/
+			.git/
+			html/
+				parse.go
+				node.go
+				...
+	bin/
+		helloworld
+		dup
+	pkg/
+		darwin_amd64/
+		...
+
+

GOPATH对应的工作区目录有三个子目录。其中src子目录用于存储源代码。每个包被保存在与$GOPATH/src的相对路径为包导入路径的子目录中,例如gopl.io/ch1/helloworld相对应的路径目录。我们看到,一个GOPATH工作区的src目录中可能有多个独立的版本控制系统,例如gopl.io和golang.org分别对应不同的Git仓库。其中pkg子目录用于保存编译后的包的目标文件,bin子目录用于保存编译后的可执行程序,例如helloworld可执行程序。

+

第二个环境变量GOROOT用来指定Go的安装目录,还有它自带的标准库包的位置。GOROOT的目录结构和GOPATH类似,因此存放fmt包的源代码对应目录应该为$GOROOT/src/fmt。用户一般不需要设置GOROOT,默认情况下Go语言安装工具会将其设置为安装的目录路径。

+

其中go env命令用于查看Go语言工具涉及的所有环境变量的值,包括未设置环境变量的默认值。GOOS环境变量用于指定目标操作系统(例如android、linux、darwin或windows),GOARCH环境变量用于指定处理器的类型,例如amd64、386或arm等。虽然GOPATH环境变量是唯一必须要设置的,但是其它环境变量也会偶尔用到。

+
$ go env
+GOPATH="/home/gopher/gobook"
+GOROOT="/usr/local/go"
+GOARCH="amd64"
+GOOS="darwin"
+...
+
+

10.7.2. 下载包

+

使用Go语言工具箱的go命令,不仅可以根据包导入路径找到本地工作区的包,甚至可以从互联网上找到和更新包。

+

使用命令go get可以下载一个单一的包或者用...下载整个子目录里面的每个包。Go语言工具箱的go命令同时计算并下载所依赖的每个包,这也是前一个例子中golang.org/x/net/html自动出现在本地工作区目录的原因。

+

一旦go get命令下载了包,然后就是安装包或包对应的可执行的程序。我们将在下一节再关注它的细节,现在只是展示整个下载过程是如何的简单。第一个命令是获取golint工具,它用于检测Go源代码的编程风格是否有问题。第二个命令是用golint命令对2.6.2节的gopl.io/ch2/popcount包代码进行编码风格检查。它友好地报告了忘记了包的文档:

+
$ go get github.com/golang/lint/golint
+$ $GOPATH/bin/golint gopl.io/ch2/popcount
+src/gopl.io/ch2/popcount/main.go:1:1:
+  package comment should be of the form "Package popcount ..."
+
+

go get命令支持当前流行的托管网站GitHub、Bitbucket和Launchpad,可以直接向它们的版本控制系统请求代码。对于其它的网站,你可能需要指定版本控制系统的具体路径和协议,例如 Git或Mercurial。运行go help importpath获取相关的信息。

+

go get命令获取的代码是真实的本地存储仓库,而不仅仅只是复制源文件,因此你依然可以使用版本管理工具比较本地代码的变更或者切换到其它的版本。例如golang.org/x/net包目录对应一个Git仓库:

+
$ cd $GOPATH/src/golang.org/x/net
+$ git remote -v
+origin  https://go.googlesource.com/net (fetch)
+origin  https://go.googlesource.com/net (push)
+
+

需要注意的是导入路径含有的网站域名和本地Git仓库对应远程服务地址并不相同,真实的Git地址是go.googlesource.com。这其实是Go语言工具的一个特性,可以让包用一个自定义的导入路径,但是真实的代码却是由更通用的服务提供,例如googlesource.com或github.com。因为页面 https://golang.org/x/net/html 包含了如下的元数据,它告诉Go语言的工具当前包真实的Git仓库托管地址:

+
$ go build gopl.io/ch1/fetch
+$ ./fetch https://golang.org/x/net/html | grep go-import
+<meta name="go-import"
+      content="golang.org/x/net git https://go.googlesource.com/net">
+
+

如果指定-u命令行标志参数,go get命令将确保所有的包和依赖的包的版本都是最新的,然后重新编译和安装它们。如果不包含该标志参数的话,而且如果包已经在本地存在,那么代码将不会被自动更新。

+

go get -u命令只是简单地保证每个包是最新版本,如果是第一次下载包则是比较方便的;但是对于发布程序则可能是不合适的,因为本地程序可能需要对依赖的包做精确的版本依赖管理。通常的解决方案是使用vendor的目录用于存储依赖包的固定版本的源代码,对本地依赖的包的版本更新也是谨慎和持续可控的。在Go1.5之前,一般需要修改包的导入路径,所以复制后golang.org/x/net/html导入路径可能会变为gopl.io/vendor/golang.org/x/net/html。最新的Go语言命令已经支持vendor特性,但限于篇幅这里并不讨论vendor的具体细节。不过可以通过go help gopath命令查看Vendor的帮助文档。

+

(译注:墙内用户在上面这些命令的基础上,还需要学习用翻墙来go get。)

+

练习 10.3: 从 http://gopl.io/ch1/helloworld?go-get=1 获取内容,查看本书的代码的真实托管的网址(go get请求HTML页面时包含了go-get参数,以区别普通的浏览器请求)。

+

10.7.3. 构建包

+

go build命令编译命令行参数指定的每个包。如果包是一个库,则忽略输出结果;这可以用于检测包是可以正确编译的。如果包的名字是main,go build将调用链接器在当前目录创建一个可执行程序;以导入路径的最后一段作为可执行程序的名字。

+

由于每个目录只包含一个包,因此每个对应可执行程序或者叫Unix术语中的命令的包,会要求放到一个独立的目录中。这些目录有时候会放在名叫cmd目录的子目录下面,例如用于提供Go文档服务的golang.org/x/tools/cmd/godoc命令就是放在cmd子目录(§10.7.4)。

+

每个包可以由它们的导入路径指定,就像前面看到的那样,或者用一个相对目录的路径名指定,相对路径必须以...开头。如果没有指定参数,那么默认指定为当前目录对应的包。下面的命令用于构建同一个包,虽然它们的写法各不相同:

+
$ cd $GOPATH/src/gopl.io/ch1/helloworld
+$ go build
+
+

或者:

+
$ cd anywhere
+$ go build gopl.io/ch1/helloworld
+
+

或者:

+
$ cd $GOPATH
+$ go build ./src/gopl.io/ch1/helloworld
+
+

但不能这样:

+
$ cd $GOPATH
+$ go build src/gopl.io/ch1/helloworld
+Error: cannot find package "src/gopl.io/ch1/helloworld".
+
+

也可以指定包的源文件列表,这一般只用于构建一些小程序或做一些临时性的实验。如果是main包,将会以第一个Go源文件的基础文件名作为最终的可执行程序的名字。

+
$ cat quoteargs.go
+package main
+
+import (
+	"fmt"
+	"os"
+)
+
+func main() {
+	fmt.Printf("%q\n", os.Args[1:])
+}
+$ go build quoteargs.go
+$ ./quoteargs one "two three" four\ five
+["one" "two three" "four five"]
+
+

特别是对于这类一次性运行的程序,我们希望尽快的构建并运行它。go run命令实际上是结合了构建和运行的两个步骤:

+
$ go run quoteargs.go one "two three" four\ five
+["one" "two three" "four five"]
+
+

(译注:其实也可以偷懒,直接go run *.go)

+

第一行的参数列表中,第一个不是以.go结尾的将作为可执行程序的参数运行。

+

默认情况下,go build命令构建指定的包和它依赖的包,然后丢弃除了最后的可执行文件之外所有的中间编译结果。依赖分析和编译过程虽然都是很快的,但是随着项目增加到几十个包和成千上万行代码,依赖关系分析和编译时间的消耗将变的可观,有时候可能需要几秒种,即使这些依赖项没有改变。

+

go install命令和go build命令很相似,但是它会保存每个包的编译成果,而不是将它们都丢弃。被编译的包会被保存到$GOPATH/pkg目录下,目录路径和 src目录路径对应,可执行程序被保存到$GOPATH/bin目录。(很多用户会将$GOPATH/bin添加到可执行程序的搜索列表中。)还有,go install命令和go build命令都不会重新编译没有发生变化的包,这可以使后续构建更快捷。为了方便编译依赖的包,go build -i命令将安装每个目标所依赖的包。

+

因为编译对应不同的操作系统平台和CPU架构,go install命令会将编译结果安装到GOOS和GOARCH对应的目录。例如,在Mac系统,golang.org/x/net/html包将被安装到$GOPATH/pkg/darwin_amd64目录下的golang.org/x/net/html.a文件。

+

针对不同操作系统或CPU的交叉构建也是很简单的。只需要设置好目标对应的GOOS和GOARCH,然后运行构建命令即可。下面交叉编译的程序将输出它在编译时的操作系统和CPU类型:

+

gopl.io/ch10/cross

+
func main() {
+	fmt.Println(runtime.GOOS, runtime.GOARCH)
+}
+
+

下面以64位和32位环境分别编译和执行:

+
$ go build gopl.io/ch10/cross
+$ ./cross
+darwin amd64
+$ GOARCH=386 go build gopl.io/ch10/cross
+$ ./cross
+darwin 386
+
+

有些包可能需要针对不同平台和处理器类型使用不同版本的代码文件,以便于处理底层的可移植性问题或为一些特定代码提供优化。如果一个文件名包含了一个操作系统或处理器类型名字,例如net_linux.go或asm_amd64.s,Go语言的构建工具将只在对应的平台编译这些文件。还有一个特别的构建注释参数可以提供更多的构建过程控制。例如,文件中可能包含下面的注释:

+
// +build linux darwin
+
+

在包声明和包注释的前面,该构建注释参数告诉go build只在编译程序对应的目标操作系统是Linux或Mac OS X时才编译这个文件。下面的构建注释则表示不编译这个文件:

+
// +build ignore
+
+

更多细节,可以参考go/build包的构建约束部分的文档。

+
$ go doc go/build
+
+

10.7.4. 包文档

+

Go语言的编码风格鼓励为每个包提供良好的文档。包中每个导出的成员和包声明前都应该包含目的和用法说明的注释。

+

Go语言中的文档注释一般是完整的句子,第一行通常是摘要说明,以被注释者的名字开头。注释中函数的参数或其它的标识符并不需要额外的引号或其它标记注明。例如,下面是fmt.Fprintf的文档注释。

+
// Fprintf formats according to a format specifier and writes to w.
+// It returns the number of bytes written and any write error encountered.
+func Fprintf(w io.Writer, format string, a ...interface{}) (int, error)
+
+

Fprintf函数格式化的细节在fmt包文档中描述。如果注释后紧跟着包声明语句,那注释对应整个包的文档。包文档对应的注释只能有一个(译注:其实可以有多个,它们会组合成一个包文档注释),包注释可以出现在任何一个源文件中。如果包的注释内容比较长,一般会放到一个独立的源文件中;fmt包注释就有300行之多。这个专门用于保存包文档的源文件通常叫doc.go。

+

好的文档并不需要面面俱到,文档本身应该是简洁但不可忽略的。事实上,Go语言的风格更喜欢简洁的文档,并且文档也是需要像代码一样维护的。对于一组声明语句,可以用一个精炼的句子描述,如果是显而易见的功能则并不需要注释。

+

在本书中,只要空间允许,我们之前很多包声明都包含了注释文档,但你可以从标准库中发现很多更好的例子。有两个工具可以帮到你。

+

首先是go doc命令,该命令打印其后所指定的实体的声明与文档注释,该实体可能是一个包:

+
$ go doc time
+package time // import "time"
+
+Package time provides functionality for measuring and displaying time.
+
+const Nanosecond Duration = 1 ...
+func After(d Duration) <-chan Time
+func Sleep(d Duration)
+func Since(t Time) Duration
+func Now() Time
+type Duration int64
+type Time struct { ... }
+...many more...
+
+

或者是某个具体的包成员:

+
$ go doc time.Since
+func Since(t Time) Duration
+
+	Since returns the time elapsed since t.
+	It is shorthand for time.Now().Sub(t).
+
+

或者是一个方法:

+
$ go doc time.Duration.Seconds
+func (d Duration) Seconds() float64
+
+	Seconds returns the duration as a floating-point number of seconds.
+
+

该命令并不需要输入完整的包导入路径或正确的大小写。下面的命令将打印encoding/json包的(*json.Decoder).Decode方法的文档:

+
$ go doc json.decode
+func (dec *Decoder) Decode(v interface{}) error
+
+	Decode reads the next JSON-encoded value from its input and stores
+	it in the value pointed to by v.
+
+

第二个工具,名字也叫godoc,它提供可以相互交叉引用的HTML页面,但是包含和go doc命令相同以及更多的信息。图10.1演示了time包的文档,11.6节将看到godoc演示可以交互的示例程序。godoc的在线服务 https://godoc.org ,包含了成千上万的开源包的检索工具。

+

+

你也可以在自己的工作区目录运行godoc服务。运行下面的命令,然后在浏览器查看 http://localhost:8000/pkg 页面:

+
$ godoc -http :8000
+
+

其中-analysis=type-analysis=pointer命令行标志参数用于打开文档和代码中关于静态分析的结果。

+

10.7.5. 内部包

+

在Go语言程序中,包是最重要的封装机制。没有导出的标识符只在同一个包内部可以访问,而导出的标识符则是面向全宇宙都是可见的。

+

有时候,一个中间的状态可能也是有用的,标识符对于一小部分信任的包是可见的,但并不是对所有调用者都可见。例如,当我们计划将一个大的包拆分为很多小的更容易维护的子包,但是我们并不想将内部的子包结构也完全暴露出去。同时,我们可能还希望在内部子包之间共享一些通用的处理包,或者我们只是想实验一个新包的还并不稳定的接口,暂时只暴露给一些受限制的用户使用。

+

为了满足这些需求,Go语言的构建工具对包含internal名字的路径段的包导入路径做了特殊处理。这种包叫internal包,一个internal包只能被和internal目录有同一个父目录的包所导入。例如,net/http/internal/chunked内部包只能被net/http/httputil或net/http包导入,但是不能被net/url包导入。不过net/url包却可以导入net/http/httputil包。

+
net/http
+net/http/internal/chunked
+net/http/httputil
+net/url
+
+

10.7.6. 查询包

+

go list命令可以查询可用包的信息。其最简单的形式,可以测试包是否在工作区并打印它的导入路径:

+
$ go list github.com/go-sql-driver/mysql
+github.com/go-sql-driver/mysql
+
+

go list命令的参数还可以用"..."表示匹配任意的包的导入路径。我们可以用它来列出工作区中的所有包:

+
$ go list ...
+archive/tar
+archive/zip
+bufio
+bytes
+cmd/addr2line
+cmd/api
+...many more...
+
+

或者是特定子目录下的所有包:

+
$ go list gopl.io/ch3/...
+gopl.io/ch3/basename1
+gopl.io/ch3/basename2
+gopl.io/ch3/comma
+gopl.io/ch3/mandelbrot
+gopl.io/ch3/netflag
+gopl.io/ch3/printints
+gopl.io/ch3/surface
+
+

或者是和某个主题相关的所有包:

+
$ go list ...xml...
+encoding/xml
+gopl.io/ch7/xmlselect
+
+

go list命令还可以获取每个包完整的元信息,而不仅仅只是导入路径,这些元信息可以以不同格式提供给用户。其中-json命令行参数表示用JSON格式打印每个包的元信息。

+
$ go list -json hash
+{
+	"Dir": "/home/gopher/go/src/hash",
+	"ImportPath": "hash",
+	"Name": "hash",
+	"Doc": "Package hash provides interfaces for hash functions.",
+	"Target": "/home/gopher/go/pkg/darwin_amd64/hash.a",
+	"Goroot": true,
+	"Standard": true,
+	"Root": "/home/gopher/go",
+	"GoFiles": [
+			"hash.go"
+	],
+	"Imports": [
+		"io"
+	],
+	"Deps": [
+		"errors",
+		"io",
+		"runtime",
+		"sync",
+		"sync/atomic",
+		"unsafe"
+	]
+}
+
+

命令行参数-f则允许用户使用text/template包(§4.6)的模板语言定义输出文本的格式。下面的命令将打印strconv包的依赖的包,然后用join模板函数将结果链接为一行,连接时每个结果之间用一个空格分隔:

+
$ go list -f '{{join .Deps " "}}' strconv
+errors math runtime unicode/utf8 unsafe
+
+

{% endraw %}

+

译注:上面的命令在Windows的命令行运行会遇到template: main:1: unclosed action的错误。产生这个错误的原因是因为命令行对命令中的" "参数进行了转义处理。可以按照下面的方法解决转义字符串的问题:

+
$ go list -f "{{join .Deps \" \"}}" strconv
+
+

{% endraw %}

+

下面的命令打印compress子目录下所有包的导入包列表:

+
$ go list -f '{{.ImportPath}} -> {{join .Imports " "}}' compress/...
+compress/bzip2 -> bufio io sort
+compress/flate -> bufio fmt io math sort strconv
+compress/gzip -> bufio compress/flate errors fmt hash hash/crc32 io time
+compress/lzw -> bufio errors fmt io
+compress/zlib -> bufio compress/flate errors fmt hash hash/adler32 io
+
+

{% endraw %}

+

译注:Windows下有同样有问题,要避免转义字符串的干扰:

+
$ go list -f "{{.ImportPath}} -> {{join .Imports \" \"}}" compress/...
+
+

{% endraw %}

+

go list命令对于一次性的交互式查询或自动化构建或测试脚本都很有帮助。我们将在11.2.4节中再次使用它。每个子命令的更多信息,包括可设置的字段和意义,可以用go help list命令查看。

+

在本章,我们解释了Go语言工具中除了测试命令之外的所有重要的子命令。在下一章,我们将看到如何用go test命令去运行Go语言程序中的测试代码。

+

练习 10.4: 创建一个工具,根据命令行指定的参数,报告工作区所有依赖包指定的其它包集合。提示:你需要运行go list命令两次,一次用于初始化包,一次用于所有包。你可能需要用encoding/json(§4.5)包来分析输出的JSON格式的信息。

+

第11章 测试

+

Maurice Wilkes,第一个存储程序计算机EDSAC的设计者,1949年他在实验室爬楼梯时有一个顿悟。在《计算机先驱回忆录》(Memoirs of a Computer Pioneer)里,他回忆到:“忽然间有一种醍醐灌顶的感觉,我整个后半生的美好时光都将在寻找程序BUG中度过了”。肯定从那之后的大部分正常的码农都会同情Wilkes过分悲观的想法,虽然也许会有人困惑于他对软件开发的难度的天真看法。

+

现在的程序已经远比Wilkes时代的更大也更复杂,也有许多技术可以让软件的复杂性可得到控制。其中有两种技术在实践中证明是比较有效的。第一种是代码在被正式部署前需要进行代码评审。第二种则是测试,也就是本章的讨论主题。

+

我们说测试的时候一般是指自动化测试,也就是写一些小的程序用来检测被测试代码(产品代码)的行为和预期的一样,这些通常都是精心设计的执行某些特定的功能或者是通过随机性的输入待验证边界的处理。

+

软件测试是一个巨大的领域。测试的任务可能已经占据了一些程序员的部分时间和另一些程序员的全部时间。和软件测试技术相关的图书或博客文章有成千上万之多。对于每一种主流的编程语言,都会有一打的用于测试的软件包,同时也有大量的测试相关的理论,而且每种都吸引了大量技术先驱和追随者。这些都足以说服那些想要编写有效测试的程序员重新学习一套全新的技能。

+

Go语言的测试技术是相对低级的。它依赖一个go test测试命令和一组按照约定方式编写的测试函数,测试命令可以运行这些测试函数。编写相对轻量级的纯测试代码是有效的,而且它很容易延伸到基准测试和示例文档。

+

在实践中,编写测试代码和编写程序本身并没有多大区别。我们编写的每一个函数也是针对每个具体的任务。我们必须小心处理边界条件,思考合适的数据结构,推断合适的输入应该产生什么样的结果输出。编写测试代码和编写普通的Go代码过程是类似的;它并不需要学习新的符号、规则和工具。

+

11.1. go test

+

go test命令是一个按照一定的约定和组织来测试代码的程序。在包目录内,所有以_test.go为后缀名的源文件在执行go build时不会被构建成包的一部分,它们是go test测试的一部分。

+

*_test.go文件中,有三种类型的函数:测试函数、基准测试(benchmark)函数、示例函数。一个测试函数是以Test为函数名前缀的函数,用于测试程序的一些逻辑行为是否正确;go test命令会调用这些测试函数并报告测试结果是PASS或FAIL。基准测试函数是以Benchmark为函数名前缀的函数,它们用于衡量一些函数的性能;go test命令会多次运行基准测试函数以计算一个平均的执行时间。示例函数是以Example为函数名前缀的函数,提供一个由编译器保证正确性的示例文档。我们将在11.2节讨论测试函数的所有细节,并在11.4节讨论基准测试函数的细节,然后在11.6节讨论示例函数的细节。

+

go test命令会遍历所有的*_test.go文件中符合上述命名规则的函数,生成一个临时的main包用于调用相应的测试函数,接着构建并运行、报告测试结果,最后清理测试中生成的临时文件。

+

11.2. 测试函数

+

每个测试函数必须导入testing包。测试函数有如下的签名:

+
func TestName(t *testing.T) {
+	// ...
+}
+
+

测试函数的名字必须以Test开头,可选的后缀名必须以大写字母开头:

+
func TestSin(t *testing.T) { /* ... */ }
+func TestCos(t *testing.T) { /* ... */ }
+func TestLog(t *testing.T) { /* ... */ }
+
+

其中t参数用于报告测试失败和附加的日志信息。让我们定义一个实例包gopl.io/ch11/word1,其中只有一个函数IsPalindrome用于检查一个字符串是否从前向后和从后向前读都是一样的。(下面这个实现对于一个字符串是否是回文字符串前后重复测试了两次;我们稍后会再讨论这个问题。)

+

gopl.io/ch11/word1

+
// Package word provides utilities for word games.
+package word
+
+// IsPalindrome reports whether s reads the same forward and backward.
+// (Our first attempt.)
+func IsPalindrome(s string) bool {
+	for i := range s {
+		if s[i] != s[len(s)-1-i] {
+			return false
+		}
+	}
+	return true
+}
+
+

在相同的目录下,word_test.go测试文件中包含了TestPalindrome和TestNonPalindrome两个测试函数。每一个都是测试IsPalindrome是否给出正确的结果,并使用t.Error报告失败信息:

+
package word
+
+import "testing"
+
+func TestPalindrome(t *testing.T) {
+	if !IsPalindrome("detartrated") {
+		t.Error(`IsPalindrome("detartrated") = false`)
+	}
+	if !IsPalindrome("kayak") {
+		t.Error(`IsPalindrome("kayak") = false`)
+	}
+}
+
+func TestNonPalindrome(t *testing.T) {
+	if IsPalindrome("palindrome") {
+		t.Error(`IsPalindrome("palindrome") = true`)
+	}
+}
+
+

go test命令如果没有参数指定包那么将默认采用当前目录对应的包(和go build命令一样)。我们可以用下面的命令构建和运行测试。

+
$ cd $GOPATH/src/gopl.io/ch11/word1
+$ go test
+ok   gopl.io/ch11/word1  0.008s
+
+

结果还比较满意,我们运行了这个程序, 不过没有提前退出是因为还没有遇到BUG报告。不过一个法国名为“Noelle Eve Elleon”的用户会抱怨IsPalindrome函数不能识别“été”。另外一个来自美国中部用户的抱怨则是不能识别“A man, a plan, a canal: Panama.”。执行特殊和小的BUG报告为我们提供了新的更自然的测试用例。

+
func TestFrenchPalindrome(t *testing.T) {
+	if !IsPalindrome("été") {
+		t.Error(`IsPalindrome("été") = false`)
+	}
+}
+
+func TestCanalPalindrome(t *testing.T) {
+	input := "A man, a plan, a canal: Panama"
+	if !IsPalindrome(input) {
+		t.Errorf(`IsPalindrome(%q) = false`, input)
+	}
+}
+
+

为了避免两次输入较长的字符串,我们使用了提供了有类似Printf格式化功能的 Errorf函数来汇报错误结果。

+

当添加了这两个测试用例之后,go test返回了测试失败的信息。

+
$ go test
+--- FAIL: TestFrenchPalindrome (0.00s)
+    word_test.go:28: IsPalindrome("été") = false
+--- FAIL: TestCanalPalindrome (0.00s)
+    word_test.go:35: IsPalindrome("A man, a plan, a canal: Panama") = false
+FAIL
+FAIL    gopl.io/ch11/word1  0.014s
+
+

先编写测试用例并观察到测试用例触发了和用户报告的错误相同的描述是一个好的测试习惯。只有这样,我们才能定位我们要真正解决的问题。

+

先写测试用例的另外的好处是,运行测试通常会比手工描述报告的处理更快,这让我们可以进行快速地迭代。如果测试集有很多运行缓慢的测试,我们可以通过只选择运行某些特定的测试来加快测试速度。

+

参数-v可用于打印每个测试函数的名字和运行时间:

+
$ go test -v
+=== RUN TestPalindrome
+--- PASS: TestPalindrome (0.00s)
+=== RUN TestNonPalindrome
+--- PASS: TestNonPalindrome (0.00s)
+=== RUN TestFrenchPalindrome
+--- FAIL: TestFrenchPalindrome (0.00s)
+    word_test.go:28: IsPalindrome("été") = false
+=== RUN TestCanalPalindrome
+--- FAIL: TestCanalPalindrome (0.00s)
+    word_test.go:35: IsPalindrome("A man, a plan, a canal: Panama") = false
+FAIL
+exit status 1
+FAIL    gopl.io/ch11/word1  0.017s
+
+

参数-run对应一个正则表达式,只有测试函数名被它正确匹配的测试函数才会被go test测试命令运行:

+
$ go test -v -run="French|Canal"
+=== RUN TestFrenchPalindrome
+--- FAIL: TestFrenchPalindrome (0.00s)
+    word_test.go:28: IsPalindrome("été") = false
+=== RUN TestCanalPalindrome
+--- FAIL: TestCanalPalindrome (0.00s)
+    word_test.go:35: IsPalindrome("A man, a plan, a canal: Panama") = false
+FAIL
+exit status 1
+FAIL    gopl.io/ch11/word1  0.014s
+
+

当然,一旦我们已经修复了失败的测试用例,在我们提交代码更新之前,我们应该以不带参数的go test命令运行全部的测试用例,以确保修复失败测试的同时没有引入新的问题。

+

我们现在的任务就是修复这些错误。简要分析后发现第一个BUG的原因是我们采用了 byte而不是rune序列,所以像“été”中的é等非ASCII字符不能正确处理。第二个BUG是因为没有忽略空格和字母的大小写导致的。

+

针对上述两个BUG,我们仔细重写了函数:

+

gopl.io/ch11/word2

+
// Package word provides utilities for word games.
+package word
+
+import "unicode"
+
+// IsPalindrome reports whether s reads the same forward and backward.
+// Letter case is ignored, as are non-letters.
+func IsPalindrome(s string) bool {
+	var letters []rune
+	for _, r := range s {
+		if unicode.IsLetter(r) {
+			letters = append(letters, unicode.ToLower(r))
+		}
+	}
+	for i := range letters {
+		if letters[i] != letters[len(letters)-1-i] {
+			return false
+		}
+	}
+	return true
+}
+
+

同时我们也将之前的所有测试数据合并到了一个测试中的表格中。

+
func TestIsPalindrome(t *testing.T) {
+	var tests = []struct {
+		input string
+		want  bool
+	}{
+		{"", true},
+		{"a", true},
+		{"aa", true},
+		{"ab", false},
+		{"kayak", true},
+		{"detartrated", true},
+		{"A man, a plan, a canal: Panama", true},
+		{"Evil I did dwell; lewd did I live.", true},
+		{"Able was I ere I saw Elba", true},
+		{"été", true},
+		{"Et se resservir, ivresse reste.", true},
+		{"palindrome", false}, // non-palindrome
+		{"desserts", false},   // semi-palindrome
+	}
+	for _, test := range tests {
+		if got := IsPalindrome(test.input); got != test.want {
+			t.Errorf("IsPalindrome(%q) = %v", test.input, got)
+		}
+	}
+}
+
+

现在我们的新测试都通过了:

+
$ go test gopl.io/ch11/word2
+ok      gopl.io/ch11/word2      0.015s
+
+

这种表格驱动的测试在Go语言中很常见。我们可以很容易地向表格添加新的测试数据,并且后面的测试逻辑也没有冗余,这样我们可以有更多的精力去完善错误信息。

+

失败测试的输出并不包括调用t.Errorf时刻的堆栈调用信息。和其他编程语言或测试框架的assert断言不同,t.Errorf调用也没有引起panic异常或停止测试的执行。即使表格中前面的数据导致了测试的失败,表格后面的测试数据依然会运行测试,因此在一个测试中我们可能了解多个失败的信息。

+

如果我们真的需要停止测试,或许是因为初始化失败或可能是早先的错误导致了后续错误等原因,我们可以使用t.Fatal或t.Fatalf停止当前测试函数。它们必须在和测试函数同一个goroutine内调用。

+

测试失败的信息一般的形式是“f(x) = y, want z”,其中f(x)解释了失败的操作和对应的输入,y是实际的运行结果,z是期望的正确的结果。就像前面检查回文字符串的例子,实际的函数用于f(x)部分。显示x是表格驱动型测试中比较重要的部分,因为同一个断言可能对应不同的表格项执行多次。要避免无用和冗余的信息。在测试类似IsPalindrome返回布尔类型的函数时,可以忽略并没有额外信息的z部分。如果x、y或z是y的长度,输出一个相关部分的简明总结即可。测试的作者应该要努力帮助程序员诊断测试失败的原因。

+

练习 11.1: 为4.3节中的charcount程序编写测试。

+

练习 11.2: 为(§6.5)的IntSet编写一组测试,用于检查每个操作后的行为和基于内置map的集合等价,后面练习11.7将会用到。

+

11.2.1. 随机测试

+

表格驱动的测试便于构造基于精心挑选的测试数据的测试用例。另一种测试思路是随机测试,也就是通过构造更广泛的随机输入来测试探索函数的行为。

+

那么对于一个随机的输入,我们如何能知道希望的输出结果呢?这里有两种处理策略。第一个是编写另一个对照函数,使用简单和清晰的算法,虽然效率较低但是行为和要测试的函数是一致的,然后针对相同的随机输入检查两者的输出结果。第二种是生成的随机输入的数据遵循特定的模式,这样我们就可以知道期望的输出的模式。

+

下面的例子使用的是第二种方法:randomPalindrome函数用于随机生成回文字符串。

+
import "math/rand"
+
+// randomPalindrome returns a palindrome whose length and contents
+// are derived from the pseudo-random number generator rng.
+func randomPalindrome(rng *rand.Rand) string {
+	n := rng.Intn(25) // random length up to 24
+	runes := make([]rune, n)
+	for i := 0; i < (n+1)/2; i++ {
+		r := rune(rng.Intn(0x1000)) // random rune up to '\u0999'
+		runes[i] = r
+		runes[n-1-i] = r
+	}
+	return string(runes)
+}
+
+func TestRandomPalindromes(t *testing.T) {
+	// Initialize a pseudo-random number generator.
+	seed := time.Now().UTC().UnixNano()
+	t.Logf("Random seed: %d", seed)
+	rng := rand.New(rand.NewSource(seed))
+
+	for i := 0; i < 1000; i++ {
+		p := randomPalindrome(rng)
+		if !IsPalindrome(p) {
+			t.Errorf("IsPalindrome(%q) = false", p)
+		}
+	}
+}
+
+

虽然随机测试会有不确定因素,但是它也是至关重要的,我们可以从失败测试的日志获取足够的信息。在我们的例子中,输入IsPalindrome的p参数将告诉我们真实的数据,但是对于函数将接受更复杂的输入,不需要保存所有的输入,只要日志中简单地记录随机数种子即可(像上面的方式)。有了这些随机数初始化种子,我们可以很容易修改测试代码以重现失败的随机测试。

+

通过使用当前时间作为随机种子,在整个过程中的每次运行测试命令时都将探索新的随机数据。如果你使用的是定期运行的自动化测试集成系统,随机测试将特别有价值。

+

练习 11.3: TestRandomPalindromes测试函数只测试了回文字符串。编写新的随机测试生成器,用于测试随机生成的非回文字符串。

+

练习 11.4: 修改randomPalindrome函数,以探索IsPalindrome是否对标点和空格做了正确处理。

+

译者注:拓展阅读感兴趣的读者可以再了解一下go-fuzz

+

11.2.2. 测试一个命令

+

对于测试包go test是一个有用的工具,但是稍加努力我们也可以用它来测试可执行程序。如果一个包的名字是 main,那么在构建时会生成一个可执行程序,不过main包可以作为一个包被测试器代码导入。

+

让我们为2.3.2节的echo程序编写一个测试。我们先将程序拆分为两个函数:echo函数完成真正的工作,main函数用于处理命令行输入参数和echo可能返回的错误。

+

gopl.io/ch11/echo

+
// Echo prints its command-line arguments.
+package main
+
+import (
+	"flag"
+	"fmt"
+	"io"
+	"os"
+	"strings"
+)
+
+var (
+	n = flag.Bool("n", false, "omit trailing newline")
+	s = flag.String("s", " ", "separator")
+)
+
+var out io.Writer = os.Stdout // modified during testing
+
+func main() {
+	flag.Parse()
+	if err := echo(!*n, *s, flag.Args()); err != nil {
+		fmt.Fprintf(os.Stderr, "echo: %v\n", err)
+		os.Exit(1)
+	}
+}
+
+func echo(newline bool, sep string, args []string) error {
+	fmt.Fprint(out, strings.Join(args, sep))
+	if newline {
+		fmt.Fprintln(out)
+	}
+	return nil
+}
+
+

在测试中我们可以用各种参数和标志调用echo函数,然后检测它的输出是否正确,我们通过增加参数来减少echo函数对全局变量的依赖。我们还增加了一个全局名为out的变量来替代直接使用os.Stdout,这样测试代码可以根据需要将out修改为不同的对象以便于检查。下面就是echo_test.go文件中的测试代码:

+
package main
+
+import (
+	"bytes"
+	"fmt"
+	"testing"
+)
+
+func TestEcho(t *testing.T) {
+	var tests = []struct {
+		newline bool
+		sep     string
+		args    []string
+		want    string
+	}{
+		{true, "", []string{}, "\n"},
+		{false, "", []string{}, ""},
+		{true, "\t", []string{"one", "two", "three"}, "one\ttwo\tthree\n"},
+		{true, ",", []string{"a", "b", "c"}, "a,b,c\n"},
+		{false, ":", []string{"1", "2", "3"}, "1:2:3"},
+	}
+	for _, test := range tests {
+		descr := fmt.Sprintf("echo(%v, %q, %q)",
+			test.newline, test.sep, test.args)
+
+		out = new(bytes.Buffer) // captured output
+		if err := echo(test.newline, test.sep, test.args); err != nil {
+			t.Errorf("%s failed: %v", descr, err)
+			continue
+		}
+		got := out.(*bytes.Buffer).String()
+		if got != test.want {
+			t.Errorf("%s = %q, want %q", descr, got, test.want)
+		}
+	}
+}
+
+

要注意的是测试代码和产品代码在同一个包。虽然是main包,也有对应的main入口函数,但是在测试的时候main包只是TestEcho测试函数导入的一个普通包,里面main函数并没有被导出,而是被忽略的。

+

通过将测试放到表格中,我们很容易添加新的测试用例。让我通过增加下面的测试用例来看看失败的情况是怎么样的:

+
{true, ",", []string{"a", "b", "c"}, "a b c\n"}, // NOTE: wrong expectation!
+
+

go test输出如下:

+
$ go test gopl.io/ch11/echo
+--- FAIL: TestEcho (0.00s)
+    echo_test.go:31: echo(true, ",", ["a" "b" "c"]) = "a,b,c", want "a b c\n"
+FAIL
+FAIL        gopl.io/ch11/echo         0.006s
+
+

错误信息描述了尝试的操作(使用Go类似语法),实际的结果和期望的结果。通过这样的错误信息,你可以在检视代码之前就很容易定位错误的原因。

+

要注意的是在测试代码中并没有调用log.Fatal或os.Exit,因为调用这类函数会导致程序提前退出;调用这些函数的特权应该放在main函数中。如果真的有意外的事情导致函数发生panic异常,测试驱动应该尝试用recover捕获异常,然后将当前测试当作失败处理。如果是可预期的错误,例如非法的用户输入、找不到文件或配置文件不当等应该通过返回一个非空的error的方式处理。幸运的是(上面的意外只是一个插曲),我们的echo示例是比较简单的也没有需要返回非空error的情况。

+

11.2.3. 白盒测试

+

一种测试分类的方法是基于测试者是否需要了解被测试对象的内部工作原理。黑盒测试只需要测试包公开的文档和API行为,内部实现对测试代码是透明的。相反,白盒测试有访问包内部函数和数据结构的权限,因此可以做到一些普通客户端无法实现的测试。例如,一个白盒测试可以在每个操作之后检测不变量的数据类型。(白盒测试只是一个传统的名称,其实称为clear box测试会更准确。)

+

黑盒和白盒这两种测试方法是互补的。黑盒测试一般更健壮,随着软件实现的完善测试代码很少需要更新。它们可以帮助测试者了解真实客户的需求,也可以帮助发现API设计的一些不足之处。相反,白盒测试则可以对内部一些棘手的实现提供更多的测试覆盖。

+

我们已经看到两种测试的例子。TestIsPalindrome测试仅仅使用导出的IsPalindrome函数,因此这是一个黑盒测试。TestEcho测试则调用了内部的echo函数,并且更新了内部的out包级变量,这两个都是未导出的,因此这是白盒测试。

+

当我们准备TestEcho测试的时候,我们修改了echo函数使用包级的out变量作为输出对象,因此测试代码可以用另一个实现代替标准输出,这样可以方便对比echo输出的数据。使用类似的技术,我们可以将产品代码的其他部分也替换为一个容易测试的伪对象。使用伪对象的好处是我们可以方便配置,容易预测,更可靠,也更容易观察。同时也可以避免一些不良的副作用,例如更新生产数据库或信用卡消费行为。

+

下面的代码演示了为用户提供网络存储的web服务中的配额检测逻辑。当用户使用了超过90%的存储配额之后将发送提醒邮件。(译注:一般在实现业务机器监控,包括磁盘、cpu、网络等的时候,需要类似的到达阈值=>触发报警的逻辑,所以是很实用的案例。)

+

gopl.io/ch11/storage1

+
package storage
+
+import (
+	"fmt"
+	"log"
+	"net/smtp"
+)
+
+func bytesInUse(username string) int64 { return 0 /* ... */ }
+
+// Email sender configuration.
+// NOTE: never put passwords in source code!
+const sender = "notifications@example.com"
+const password = "correcthorsebatterystaple"
+const hostname = "smtp.example.com"
+
+const template = `Warning: you are using %d bytes of storage,
+%d%% of your quota.`
+
+func CheckQuota(username string) {
+	used := bytesInUse(username)
+	const quota = 1000000000 // 1GB
+	percent := 100 * used / quota
+	if percent < 90 {
+		return // OK
+	}
+	msg := fmt.Sprintf(template, used, percent)
+	auth := smtp.PlainAuth("", sender, password, hostname)
+	err := smtp.SendMail(hostname+":587", auth, sender,
+		[]string{username}, []byte(msg))
+	if err != nil {
+		log.Printf("smtp.SendMail(%s) failed: %s", username, err)
+	}
+}
+
+

我们想测试这段代码,但是我们并不希望发送真实的邮件。因此我们将邮件处理逻辑放到一个私有的notifyUser函数中。

+

gopl.io/ch11/storage2

+
var notifyUser = func(username, msg string) {
+	auth := smtp.PlainAuth("", sender, password, hostname)
+	err := smtp.SendMail(hostname+":587", auth, sender,
+		[]string{username}, []byte(msg))
+	if err != nil {
+		log.Printf("smtp.SendEmail(%s) failed: %s", username, err)
+	}
+}
+
+func CheckQuota(username string) {
+	used := bytesInUse(username)
+	const quota = 1000000000 // 1GB
+	percent := 100 * used / quota
+	if percent < 90 {
+		return // OK
+	}
+	msg := fmt.Sprintf(template, used, percent)
+	notifyUser(username, msg)
+}
+
+

现在我们可以在测试中用伪邮件发送函数替代真实的邮件发送函数。它只是简单记录要通知的用户和邮件的内容。

+
package storage
+
+import (
+	"strings"
+	"testing"
+)
+func TestCheckQuotaNotifiesUser(t *testing.T) {
+	var notifiedUser, notifiedMsg string
+	notifyUser = func(user, msg string) {
+		notifiedUser, notifiedMsg = user, msg
+	}
+
+	// ...simulate a 980MB-used condition...
+
+	const user = "joe@example.org"
+	CheckQuota(user)
+	if notifiedUser == "" && notifiedMsg == "" {
+		t.Fatalf("notifyUser not called")
+	}
+	if notifiedUser != user {
+		t.Errorf("wrong user (%s) notified, want %s",
+			notifiedUser, user)
+	}
+	const wantSubstring = "98% of your quota"
+	if !strings.Contains(notifiedMsg, wantSubstring) {
+		t.Errorf("unexpected notification message <<%s>>, "+
+			"want substring %q", notifiedMsg, wantSubstring)
+	}
+}
+
+

这里有一个问题:当测试函数返回后,CheckQuota将不能正常工作,因为notifyUsers依然使用的是测试函数的伪发送邮件函数(当更新全局对象的时候总会有这种风险)。 我们必须修改测试代码恢复notifyUsers原先的状态以便后续其他的测试没有影响,要确保所有的执行路径后都能恢复,包括测试失败或panic异常的情形。在这种情况下,我们建议使用defer语句来延后执行处理恢复的代码。

+
func TestCheckQuotaNotifiesUser(t *testing.T) {
+	// Save and restore original notifyUser.
+	saved := notifyUser
+	defer func() { notifyUser = saved }()
+
+	// Install the test's fake notifyUser.
+	var notifiedUser, notifiedMsg string
+	notifyUser = func(user, msg string) {
+		notifiedUser, notifiedMsg = user, msg
+	}
+	// ...rest of test...
+}
+
+

这种处理模式可以用来暂时保存和恢复所有的全局变量,包括命令行标志参数、调试选项和优化参数;安装和移除导致生产代码产生一些调试信息的钩子函数;还有有些诱导生产代码进入某些重要状态的改变,比如超时、错误,甚至是一些刻意制造的并发行为等因素。

+

以这种方式使用全局变量是安全的,因为go test命令并不会同时并发地执行多个测试。

+

11.2.4. 外部测试包

+

考虑下这两个包:net/url包,提供了URL解析的功能;net/http包,提供了web服务和HTTP客户端的功能。如我们所料,上层的net/http包依赖下层的net/url包。然后,net/url包中的一个测试是演示不同URL和HTTP客户端的交互行为。也就是说,一个下层包的测试代码导入了上层的包。

+

+

这样的行为在net/url包的测试代码中会导致包的循环依赖,正如图11.1中向上箭头所示,同时正如我们在10.1节所讲的,Go语言规范是禁止包的循环依赖的。

+

不过我们可以通过外部测试包的方式解决循环依赖的问题,也就是在net/url包所在的目录声明一个独立的url_test测试包。其中包名的_test后缀告诉go test工具它应该建立一个额外的包来运行测试。我们将这个外部测试包的导入路径视作是net/url_test会更容易理解,但实际上它并不能被其他任何包导入。

+

因为外部测试包是一个独立的包,所以能够导入那些依赖待测代码本身的其他辅助包;包内的测试代码就无法做到这点。在设计层面,外部测试包是在所有它依赖的包的上层,正如图11.2所示。

+

+

通过避免循环的导入依赖,外部测试包可以更灵活地编写测试,特别是集成测试(需要测试多个组件之间的交互),可以像普通应用程序那样自由地导入其他包。

+

我们可以用go list命令查看包对应目录中哪些Go源文件是产品代码,哪些是包内测试,还有哪些是外部测试包。我们以fmt包作为一个例子:GoFiles表示产品代码对应的Go源文件列表;也就是go build命令要编译的部分。

+
$ go list -f={{.GoFiles}} fmt
+[doc.go format.go print.go scan.go]
+
+

{% endraw %}

+

TestGoFiles表示的是fmt包内部测试代码,以_test.go为后缀文件名,不过只在测试时被构建:

+
$ go list -f={{.TestGoFiles}} fmt
+[export_test.go]
+
+

{% endraw %}

+

包的测试代码通常都在这些文件中,不过fmt包并非如此;稍后我们再解释export_test.go文件的作用。

+

XTestGoFiles表示的是属于外部测试包的测试代码,也就是fmt_test包,因此它们必须先导入fmt包。同样,这些文件也只是在测试时被构建运行:

+
$ go list -f={{.XTestGoFiles}} fmt
+[fmt_test.go scan_test.go stringer_test.go]
+
+

{% endraw %}

+

有时候外部测试包也需要访问被测试包内部的代码,例如在一个为了避免循环导入而被独立到外部测试包的白盒测试。在这种情况下,我们可以通过一些技巧解决:我们在包内的一个_test.go文件中导出一个内部的实现给外部测试包。因为这些代码只有在测试时才需要,因此一般会放在export_test.go文件中。

+

例如,fmt包的fmt.Scanf函数需要unicode.IsSpace函数提供的功能。但是为了避免太多的依赖,fmt包并没有导入包含巨大表格数据的unicode包;相反fmt包有一个叫isSpace内部的简易实现。

+

为了确保fmt.isSpace和unicode.IsSpace函数的行为保持一致,fmt包谨慎地包含了一个测试。一个在外部测试包内的白盒测试,是无法直接访问到isSpace内部函数的,因此fmt通过一个后门导出了isSpace函数。export_test.go文件就是专门用于外部测试包的后门。

+
package fmt
+
+var IsSpace = isSpace
+
+

这个测试文件并没有定义测试代码;它只是通过fmt.IsSpace简单导出了内部的isSpace函数,提供给外部测试包使用。这个技巧可以广泛用于位于外部测试包的白盒测试。

+

11.2.5. 编写有效的测试

+

许多Go语言新人会惊异于Go语言极简的测试框架。很多其它语言的测试框架都提供了识别测试函数的机制(通常使用反射或元数据),通过设置一些“setup”和“teardown”的钩子函数来执行测试用例运行的初始化和之后的清理操作,同时测试工具箱还提供了很多类似assert断言、值比较函数、格式化输出错误信息和停止一个失败的测试等辅助函数(通常使用异常机制)。虽然这些机制可以使得测试非常简洁,但是测试输出的日志却会像火星文一般难以理解。此外,虽然测试最终也会输出PASS或FAIL的报告,但是它们提供的信息格式却非常不利于代码维护者快速定位问题,因为失败信息的具体含义非常隐晦,比如“assert: 0 == 1”或成页的海量跟踪日志。

+

Go语言的测试风格则形成鲜明对比。它期望测试者自己完成大部分的工作,定义函数避免重复,就像普通编程那样。编写测试并不是一个机械的填空过程;一个测试也有自己的接口,尽管它的维护者也是测试仅有的一个用户。一个好的测试不应该引发其他无关的错误信息,它只要清晰简洁地描述问题的症状即可,有时候可能还需要一些上下文信息。在理想情况下,维护者可以在不看代码的情况下就能根据错误信息定位错误产生的原因。一个好的测试不应该在遇到一点小错误时就立刻退出测试,它应该尝试报告更多的相关的错误信息,因为我们可能从多个失败测试的模式中发现错误产生的规律。

+

下面的断言函数比较两个值,然后生成一个通用的错误信息,并停止程序。它很好用也确实有效,但是当测试失败的时候,打印的错误信息却几乎是没有价值的。它并没有为快速解决问题提供一个很好的入口。

+
import (
+	"fmt"
+	"strings"
+	"testing"
+)
+// A poor assertion function.
+func assertEqual(x, y int) {
+	if x != y {
+		panic(fmt.Sprintf("%d != %d", x, y))
+	}
+}
+func TestSplit(t *testing.T) {
+	words := strings.Split("a:b:c", ":")
+	assertEqual(len(words), 3)
+	// ...
+}
+
+

从这个意义上说,断言函数犯了过早抽象的错误:仅仅测试两个整数是否相同,而没能根据上下文提供更有意义的错误信息。我们可以根据具体的错误打印一个更有价值的错误信息,就像下面例子那样。只有在测试中出现重复模式时才采用抽象。

+
func TestSplit(t *testing.T) {
+	s, sep := "a:b:c", ":"
+	words := strings.Split(s, sep)
+	if got, want := len(words), 3; got != want {
+		t.Errorf("Split(%q, %q) returned %d words, want %d",
+			s, sep, got, want)
+	}
+	// ...
+}
+
+

现在的测试不仅报告了调用的具体函数、它的输入和结果的意义;并且打印的真实返回的值和期望返回的值;并且即使断言失败依然会继续尝试运行更多的测试。一旦我们写了这样结构的测试,下一步自然不是用更多的if语句来扩展测试用例,我们可以用像IsPalindrome的表驱动测试那样来准备更多的s和sep测试用例。

+

前面的例子并不需要额外的辅助函数,如果有可以使测试代码更简单的方法我们也乐意接受。(我们将在13.3节看到一个类似reflect.DeepEqual辅助函数。)一个好的测试的关键是首先实现你期望的具体行为,然后才是考虑简化测试代码、避免重复。如果直接从抽象、通用的测试库着手,很难取得良好结果。

+

练习11.5: 用表格驱动的技术扩展TestSplit测试,并打印期望的输出结果。

+

11.2.6. 避免脆弱的测试

+

如果一个应用程序对于新出现的但有效的输入经常失败说明程序容易出bug(不够稳健);同样,如果一个测试仅仅对程序做了微小变化就失败则称为脆弱。就像一个不够稳健的程序会挫败它的用户一样,一个脆弱的测试同样会激怒它的维护者。最脆弱的测试代码会在程序没有任何变化的时候产生不同的结果,时好时坏,处理它们会耗费大量的时间但是并不会得到任何好处。

+

当一个测试函数会产生一个复杂的输出如一个很长的字符串、一个精心设计的数据结构或一个文件时,人们很容易想预先写下一系列固定的用于对比的标杆数据。但是随着项目的发展,有些输出可能会发生变化,尽管很可能是一个改进的实现导致的。而且不仅仅是输出部分,函数复杂的输入部分可能也跟着变化了,因此测试使用的输入也就不再有效了。

+

避免脆弱测试代码的方法是只检测你真正关心的属性。保持测试代码的简洁和内部结构的稳定。特别是对断言部分要有所选择。不要对字符串进行全字匹配,而是针对那些在项目的发展中是比较稳定不变的子串。很多时候值得花力气来编写一个从复杂输出中提取用于断言的必要信息的函数,虽然这可能会带来很多前期的工作,但是它可以帮助迅速及时修复因为项目演化而导致的不合逻辑的失败测试。

+

11.3. 测试覆盖率

+

就其性质而言,测试不可能是完整的。计算机科学家Edsger Dijkstra曾说过:“测试能证明缺陷存在,而无法证明没有缺陷。”再多的测试也不能证明一个程序没有BUG。在最好的情况下,测试可以增强我们的信心:代码在很多重要场景下是可以正常工作的。

+

对待测程序执行的测试的程度称为测试的覆盖率。测试覆盖率并不能量化——即使最简单的程序的动态也是难以精确测量的——但是有启发式方法来帮助我们编写有效的测试代码。

+

这些启发式方法中,语句的覆盖率是最简单和最广泛使用的。语句的覆盖率是指在测试中至少被运行一次的代码占总代码数的比例。在本节中,我们使用go test命令中集成的测试覆盖率工具,来度量下面代码的测试覆盖率,帮助我们识别测试和我们期望间的差距。

+

下面的代码是一个表格驱动的测试,用于测试第七章的表达式求值程序:

+

gopl.io/ch7/eval

+
func TestCoverage(t *testing.T) {
+	var tests = []struct {
+		input string
+		env   Env
+		want  string // expected error from Parse/Check or result from Eval
+	}{
+		{"x % 2", nil, "unexpected '%'"},
+		{"!true", nil, "unexpected '!'"},
+		{"log(10)", nil, `unknown function "log"`},
+		{"sqrt(1, 2)", nil, "call to sqrt has 2 args, want 1"},
+		{"sqrt(A / pi)", Env{"A": 87616, "pi": math.Pi}, "167"},
+		{"pow(x, 3) + pow(y, 3)", Env{"x": 9, "y": 10}, "1729"},
+		{"5 / 9 * (F - 32)", Env{"F": -40}, "-40"},
+	}
+
+	for _, test := range tests {
+		expr, err := Parse(test.input)
+		if err == nil {
+			err = expr.Check(map[Var]bool{})
+		}
+		if err != nil {
+			if err.Error() != test.want {
+				t.Errorf("%s: got %q, want %q", test.input, err, test.want)
+			}
+			continue
+		}
+		got := fmt.Sprintf("%.6g", expr.Eval(test.env))
+		if got != test.want {
+			t.Errorf("%s: %v => %s, want %s",
+				test.input, test.env, got, test.want)
+		}
+	}
+}
+
+

首先,我们要确保所有的测试都正常通过:

+
$ go test -v -run=Coverage gopl.io/ch7/eval
+=== RUN TestCoverage
+--- PASS: TestCoverage (0.00s)
+PASS
+ok      gopl.io/ch7/eval         0.011s
+
+

下面这个命令可以显示测试覆盖率工具的使用用法:

+
$ go tool cover
+Usage of 'go tool cover':
+Given a coverage profile produced by 'go test':
+    go test -coverprofile=c.out
+
+Open a web browser displaying annotated source code:
+    go tool cover -html=c.out
+...
+
+

go tool命令运行Go工具链的底层可执行程序。这些底层可执行程序放在$GOROOT/pkg/tool/${GOOS}_${GOARCH}目录。因为有go build命令的原因,我们很少直接调用这些底层工具。

+

现在我们可以用-coverprofile标志参数重新运行测试:

+
$ go test -run=Coverage -coverprofile=c.out gopl.io/ch7/eval
+ok      gopl.io/ch7/eval         0.032s      coverage: 68.5% of statements
+
+

这个标志参数通过在测试代码中插入生成钩子来统计覆盖率数据。也就是说,在运行每个测试前,它将待测代码拷贝一份并做修改,在每个词法块都会设置一个布尔标志变量。当被修改后的被测试代码运行退出时,将统计日志数据写入c.out文件,并打印一部分执行的语句的一个总结。(如果你需要的是摘要,使用go test -cover。)

+

如果使用了-covermode=count标志参数,那么将在每个代码块插入一个计数器而不是布尔标志量。在统计结果中记录了每个块的执行次数,这可以用于衡量哪些是被频繁执行的热点代码。

+

为了收集数据,我们运行了测试覆盖率工具,打印了测试日志,生成一个HTML报告,然后在浏览器中打开(图11.3)。

+
$ go tool cover -html=c.out
+
+

+

绿色的代码块被测试覆盖到了,红色的则表示没有被覆盖到。为了清晰起见,我们将背景红色文本的背景设置成了阴影效果。我们可以马上发现unary操作的Eval方法并没有被执行到。如果我们针对这部分未被覆盖的代码添加下面的测试用例,然后重新运行上面的命令,那么我们将会看到那个红色部分的代码也变成绿色了:

+
{"-x * -x", eval.Env{"x": 2}, "4"}
+
+

不过两个panic语句依然是红色的。这是没有问题的,因为这两个语句并不会被执行到。

+

实现100%的测试覆盖率听起来很美,但是在具体实践中通常是不可行的,也不是值得推荐的做法。因为那只能说明代码被执行过而已,并不意味着代码就是没有BUG的;因为对于逻辑复杂的语句需要针对不同的输入执行多次。有一些语句,例如上面的panic语句则永远都不会被执行到。另外,还有一些隐晦的错误在现实中很少遇到也很难编写对应的测试代码。测试从本质上来说是一个比较务实的工作,编写测试代码和编写应用代码的成本对比是需要考虑的。测试覆盖率工具可以帮助我们快速识别测试薄弱的地方,但是设计好的测试用例和编写应用代码一样需要严密的思考。

+

11.4. 基准测试

+

基准测试是测量一个程序在固定工作负载下的性能。在Go语言中,基准测试函数和普通测试函数写法类似,但是以Benchmark为前缀名,并且带有一个*testing.B类型的参数;*testing.B参数除了提供和*testing.T类似的方法,还有额外一些和性能测量相关的方法。它还提供了一个整数N,用于指定操作执行的循环次数。

+

下面是IsPalindrome函数的基准测试,其中循环将执行N次。

+
import "testing"
+
+func BenchmarkIsPalindrome(b *testing.B) {
+	for i := 0; i < b.N; i++ {
+		IsPalindrome("A man, a plan, a canal: Panama")
+	}
+}
+
+

我们用下面的命令运行基准测试。和普通测试不同的是,默认情况下不运行任何基准测试。我们需要通过-bench命令行标志参数手工指定要运行的基准测试函数。该参数是一个正则表达式,用于匹配要执行的基准测试函数的名字,默认值是空的。其中“.”模式将可以匹配所有基准测试函数,但因为这里只有一个基准测试函数,因此和-bench=IsPalindrome参数是等价的效果。

+
$ cd $GOPATH/src/gopl.io/ch11/word2
+$ go test -bench=.
+PASS
+BenchmarkIsPalindrome-8 1000000                1035 ns/op
+ok      gopl.io/ch11/word2      2.179s
+
+

结果中基准测试名的数字后缀部分,这里是8,表示运行时对应的GOMAXPROCS的值,这对于一些与并发相关的基准测试是重要的信息。

+

报告显示每次调用IsPalindrome函数花费1.035微秒,是执行1,000,000次的平均时间。因为基准测试驱动器开始时并不知道每个基准测试函数运行所花的时间,它会尝试在真正运行基准测试前先尝试用较小的N运行测试来估算基准测试函数所需要的时间,然后推断一个较大的时间保证稳定的测量结果。

+

循环在基准测试函数内实现,而不是放在基准测试框架内实现,这样可以让每个基准测试函数有机会在循环启动前执行初始化代码,这样并不会显著影响每次迭代的平均运行时间。如果还是担心初始化代码部分对测量时间带来干扰,那么可以通过testing.B参数提供的方法来临时关闭或重置计时器,不过这些一般很少会用到。

+

现在我们有了一个基准测试和普通测试,我们可以很容易测试改进程序运行速度的想法。也许最明显的优化是在IsPalindrome函数中第二个循环的停止检查,这样可以避免每个比较都做两次:

+
n := len(letters)/2
+for i := 0; i < n; i++ {
+	if letters[i] != letters[len(letters)-1-i] {
+		return false
+	}
+}
+return true
+
+

不过很多情况下,一个显而易见的优化未必能带来预期的效果。这个改进在基准测试中只带来了4%的性能提升。

+
$ go test -bench=.
+PASS
+BenchmarkIsPalindrome-8 1000000              992 ns/op
+ok      gopl.io/ch11/word2      2.093s
+
+

另一个改进想法是在开始为每个字符预先分配一个足够大的数组,这样就可以避免在append调用时可能会导致内存的多次重新分配。声明一个letters数组变量,并指定合适的大小,像下面这样,

+
letters := make([]rune, 0, len(s))
+for _, r := range s {
+	if unicode.IsLetter(r) {
+		letters = append(letters, unicode.ToLower(r))
+	}
+}
+
+

这个改进提升性能约35%,报告结果是基于2,000,000次迭代的平均运行时间统计。

+
$ go test -bench=.
+PASS
+BenchmarkIsPalindrome-8 2000000                      697 ns/op
+ok      gopl.io/ch11/word2      1.468s
+
+

如这个例子所示,快的程序往往是伴随着较少的内存分配。-benchmem命令行标志参数将在报告中包含内存的分配数据统计。我们可以比较优化前后内存的分配情况:

+
$ go test -bench=. -benchmem
+PASS
+BenchmarkIsPalindrome    1000000   1026 ns/op    304 B/op  4 allocs/op
+
+

这是优化之后的结果:

+
$ go test -bench=. -benchmem
+PASS
+BenchmarkIsPalindrome    2000000    807 ns/op    128 B/op  1 allocs/op
+
+

用一次内存分配代替多次的内存分配节省了75%的分配调用次数和减少近一半的内存需求。

+

这个基准测试告诉了我们某个具体操作所需的绝对时间,但我们往往想知道的是两个不同的操作的时间对比。例如,如果一个函数需要1ms处理1,000个元素,那么处理10000或1百万将需要多少时间呢?这样的比较揭示了渐近增长函数的运行时间。另一个例子:I/O缓存该设置为多大呢?基准测试可以帮助我们选择在性能达标情况下所需的最小内存。第三个例子:对于一个确定的工作哪种算法更好?基准测试可以评估两种不同算法对于相同的输入在不同的场景和负载下的优缺点。

+

比较型的基准测试就是普通程序代码。它们通常是单参数的函数,由几个不同数量级的基准测试函数调用,就像这样:

+
func benchmark(b *testing.B, size int) { /* ... */ }
+func Benchmark10(b *testing.B)         { benchmark(b, 10) }
+func Benchmark100(b *testing.B)        { benchmark(b, 100) }
+func Benchmark1000(b *testing.B)       { benchmark(b, 1000) }
+
+

通过函数参数来指定输入的大小,但是参数变量对于每个具体的基准测试都是固定的。要避免直接修改b.N来控制输入的大小。除非你将它作为一个固定大小的迭代计算输入,否则基准测试的结果将毫无意义。

+

比较型的基准测试反映出的模式在程序设计阶段是很有帮助的,但是即使程序完工了也应当保留基准测试代码。因为随着项目的发展,或者是输入的增加,或者是部署到新的操作系统或不同的处理器,我们可以再次用基准测试来帮助我们改进设计。

+

练习 11.6: 为2.6.2节的练习2.4和练习2.5的PopCount函数编写基准测试。看看基于表格算法在不同情况下对提升性能会有多大帮助。

+

练习 11.7:*IntSet(§6.5)的Add、UnionWith和其他方法编写基准测试,使用大量随机输入。你可以让这些方法跑多快?选择字的大小对于性能的影响如何?IntSet和基于内建map的实现相比有多快?

+

11.5. 剖析

+

基准测试(Benchmark)对于衡量特定操作的性能是有帮助的,但是当我们试图让程序跑的更快的时候,我们通常并不知道从哪里开始优化。每个码农都应该知道Donald Knuth在1974年的“Structured Programming with go to Statements”上所说的格言。虽然经常被解读为不重视性能的意思,但是从原文我们可以看到不同的含义:

+
+

毫无疑问,对效率的片面追求会导致各种滥用。程序员会浪费大量的时间在非关键程序的速度上,实际上这些尝试提升效率的行为反倒可能产生很大的负面影响,特别是当调试和维护的时候。我们不应该过度纠结于细节的优化,应该说约97%的场景:过早的优化是万恶之源。

+

当然我们也不应该放弃对那关键3%的优化。一个好的程序员不会因为这个比例小就裹足不前,他们会明智地观察和识别哪些是关键的代码;但是仅当关键代码已经被确认的前提下才会进行优化。对于很多程序员来说,判断哪部分是关键的性能瓶颈,是很容易犯经验上的错误的,因此一般应该借助测量工具来证明。

+
+

当我们想仔细观察我们程序的运行速度的时候,最好的方法是性能剖析。剖析技术是基于程序执行期间一些自动抽样,然后在收尾时进行推断;最后产生的统计结果就称为剖析数据。

+

Go语言支持多种类型的剖析性能分析,每一种关注不同的方面,但它们都涉及到每个采样记录的感兴趣的一系列事件消息,每个事件都包含函数调用时函数调用堆栈的信息。内建的go test工具对几种分析方式都提供了支持。

+

CPU剖析数据标识了最耗CPU时间的函数。在每个CPU上运行的线程在每隔几毫秒都会遇到操作系统的中断事件,每次中断时都会记录一个剖析数据然后恢复正常的运行。

+

堆剖析则标识了最耗内存的语句。剖析库会记录调用内部内存分配的操作,平均每512KB的内存申请会触发一个剖析数据。

+

阻塞剖析则记录阻塞goroutine最久的操作,例如系统调用、管道发送和接收,还有获取锁等。每当goroutine被这些操作阻塞时,剖析库都会记录相应的事件。

+

只需要开启下面其中一个标志参数就可以生成各种分析文件。当同时使用多个标志参数时需要当心,因为一项分析操作可能会影响其他项的分析结果。

+
$ go test -cpuprofile=cpu.out
+$ go test -blockprofile=block.out
+$ go test -memprofile=mem.out
+
+

对于一些非测试程序也很容易进行剖析,具体的实现方式,与程序是短时间运行的小工具还是长时间运行的服务会有很大不同。剖析对于长期运行的程序尤其有用,因此可以通过调用Go的runtime API来启用运行时剖析。

+

一旦我们已经收集到了用于分析的采样数据,我们就可以使用pprof来分析这些数据。这是Go工具箱自带的一个工具,但并不是一个日常工具,它对应go tool pprof命令。该命令有许多特性和选项,但是最基本的是两个参数:生成这个概要文件的可执行程序和对应的剖析数据。

+

为了提高分析效率和减少空间,分析日志本身并不包含函数的名字;它只包含函数对应的地址。也就是说pprof需要对应的可执行程序来解读剖析数据。虽然go test通常在测试完成后就丢弃临时用的测试程序,但是在启用分析的时候会将测试程序保存为foo.test文件,其中foo部分对应待测包的名字。

+

下面的命令演示了如何收集并展示一个CPU分析文件。我们选择net/http包的一个基准测试为例。通常最好是对业务关键代码的部分设计专门的基准测试。因为简单的基准测试几乎没法代表业务场景,因此我们用-run=NONE参数禁止那些简单测试。

+
$ go test -run=NONE -bench=ClientServerParallelTLS64 \
+    -cpuprofile=cpu.log net/http
+ PASS
+ BenchmarkClientServerParallelTLS64-8  1000
+    3141325 ns/op  143010 B/op  1747 allocs/op
+ok       net/http       3.395s
+
+$ go tool pprof -text -nodecount=10 ./http.test cpu.log
+2570ms of 3590ms total (71.59%)
+Dropped 129 nodes (cum <= 17.95ms)
+Showing top 10 nodes out of 166 (cum >= 60ms)
+    flat  flat%   sum%     cum   cum%
+  1730ms 48.19% 48.19%  1750ms 48.75%  crypto/elliptic.p256ReduceDegree
+   230ms  6.41% 54.60%   250ms  6.96%  crypto/elliptic.p256Diff
+   120ms  3.34% 57.94%   120ms  3.34%  math/big.addMulVVW
+   110ms  3.06% 61.00%   110ms  3.06%  syscall.Syscall
+    90ms  2.51% 63.51%  1130ms 31.48%  crypto/elliptic.p256Square
+    70ms  1.95% 65.46%   120ms  3.34%  runtime.scanobject
+    60ms  1.67% 67.13%   830ms 23.12%  crypto/elliptic.p256Mul
+    60ms  1.67% 68.80%   190ms  5.29%  math/big.nat.montgomery
+    50ms  1.39% 70.19%    50ms  1.39%  crypto/elliptic.p256ReduceCarry
+    50ms  1.39% 71.59%    60ms  1.67%  crypto/elliptic.p256Sum
+
+

参数-text用于指定输出格式,在这里每行是一个函数,根据使用CPU的时间长短来排序。其中-nodecount=10参数限制了只输出前10行的结果。对于严重的性能问题,这个文本格式基本可以帮助查明原因了。

+

这个概要文件告诉我们,HTTPS基准测试中crypto/elliptic.p256ReduceDegree函数占用了将近一半的CPU资源,对性能占很大比重。相比之下,如果一个概要文件中主要是runtime包的内存分配的函数,那么减少内存消耗可能是一个值得尝试的优化策略。

+

对于一些更微妙的问题,你可能需要使用pprof的图形显示功能。这个需要安装GraphViz工具,可以从 http://www.graphviz.org 下载。参数-web用于生成函数的有向图,标注有CPU的使用和最热点的函数等信息。

+

这一节我们只是简单看了下Go语言的数据分析工具。如果想了解更多,可以阅读Go官方博客的“Profiling Go Programs”一文。

+

11.6. 示例函数

+

第三种被go test特别对待的函数是示例函数,以Example为函数名开头。示例函数没有函数参数和返回值。下面是IsPalindrome函数对应的示例函数:

+
func ExampleIsPalindrome() {
+	fmt.Println(IsPalindrome("A man, a plan, a canal: Panama"))
+	fmt.Println(IsPalindrome("palindrome"))
+	// Output:
+	// true
+	// false
+}
+
+

示例函数有三个用处。最主要的一个是作为文档:一个包的例子可以更简洁直观的方式来演示函数的用法,比文字描述更直接易懂,特别是作为一个提醒或快速参考时。一个示例函数也可以方便展示属于同一个接口的几种类型或函数之间的关系,所有的文档都必须关联到一个地方,就像一个类型或函数声明都统一到包一样。同时,示例函数和注释并不一样,示例函数是真实的Go代码,需要接受编译器的编译时检查,这样可以保证源代码更新时,示例代码不会脱节。

+

根据示例函数的后缀名部分,godoc这个web文档服务器会将示例函数关联到某个具体函数或包本身,因此ExampleIsPalindrome示例函数将是IsPalindrome函数文档的一部分,Example示例函数将是包文档的一部分。

+

示例函数的第二个用处是,在go test执行测试的时候也会运行示例函数测试。如果示例函数内含有类似上面例子中的// Output:格式的注释,那么测试工具会执行这个示例函数,然后检查示例函数的标准输出与注释是否匹配。

+

示例函数的第三个目的提供一个真实的演练场。 http://golang.org 就是由godoc提供的文档服务,它使用了Go Playground让用户可以在浏览器中在线编辑和运行每个示例函数,就像图11.4所示的那样。这通常是学习函数使用或Go语言特性最快捷的方式。

+

+

本书最后的两章是讨论reflect和unsafe包,一般的Go程序员很少使用它们,事实上也很少需要用到。因此,如果你还没有写过任何真实的Go程序的话,现在可以先去写些代码了。

+

第12章 反射

+

Go语言提供了一种机制,能够在运行时更新变量和检查它们的值、调用它们的方法和它们支持的内在操作,而不需要在编译时就知道这些变量的具体类型。这种机制被称为反射。反射也可以让我们将类型本身作为第一类的值类型处理。

+

在本章,我们将探讨Go语言的反射特性,看看它可以给语言增加哪些表达力,以及在两个至关重要的API是如何使用反射机制的:一个是fmt包提供的字符串格式化功能,另一个是类似encoding/json和encoding/xml提供的针对特定协议的编解码功能。对于我们在4.6节中看到过的text/template和html/template包,它们的实现也是依赖反射技术的。然后,反射是一个复杂的内省技术,不应该随意使用,因此,尽管上面这些包内部都是用反射技术实现的,但是它们自己的API都没有公开反射相关的接口。

+

12.1. 为何需要反射?

+

有时候我们需要编写一个函数能够处理一类并不满足普通公共接口的类型的值,也可能是因为它们并没有确定的表示方式,或者是在我们设计该函数的时候这些类型可能还不存在。

+

一个大家熟悉的例子是fmt.Fprintf函数提供的字符串格式化处理逻辑,它可以用来对任意类型的值格式化并打印,甚至支持用户自定义的类型。让我们也来尝试实现一个类似功能的函数。为了简单起见,我们的函数只接收一个参数,然后返回和fmt.Sprint类似的格式化后的字符串。我们实现的函数名也叫Sprint。

+

我们首先用switch类型分支来测试输入参数是否实现了String方法,如果是的话就调用该方法。然后继续增加类型测试分支,检查这个值的动态类型是否是string、int、bool等基础类型,并在每种情况下执行相应的格式化操作。

+
func Sprint(x interface{}) string {
+	type stringer interface {
+		String() string
+	}
+	switch x := x.(type) {
+	case stringer:
+		return x.String()
+	case string:
+		return x
+	case int:
+		return strconv.Itoa(x)
+	// ...similar cases for int16, uint32, and so on...
+	case bool:
+		if x {
+			return "true"
+		}
+		return "false"
+	default:
+		// array, chan, func, map, pointer, slice, struct
+		return "???"
+	}
+}
+
+

但是我们如何处理其它类似[]float64、map[string][]string等类型呢?我们当然可以添加更多的测试分支,但是这些组合类型的数目基本是无穷的。还有如何处理类似url.Values这样的具名类型呢?即使类型分支可以识别出底层的基础类型是map[string][]string,但是它并不匹配url.Values类型,因为它们是两种不同的类型,而且switch类型分支也不可能包含每个类似url.Values的类型,这会导致对这些库的依赖。

+

没有办法来检查未知类型的表示方式,我们被卡住了。这就是我们需要反射的原因。

+

12.2. reflect.Type 和 reflect.Value

+

反射是由 reflect 包提供的。它定义了两个重要的类型,Type 和 Value。一个 Type 表示一个Go类型。它是一个接口,有许多方法来区分类型以及检查它们的组成部分,例如一个结构体的成员或一个函数的参数等。唯一能反映 reflect.Type 实现的是接口的类型描述信息(§7.5),也正是这个实体标识了接口值的动态类型。

+

函数 reflect.TypeOf 接受任意的 interface{} 类型,并以 reflect.Type 形式返回其动态类型:

+
t := reflect.TypeOf(3)  // a reflect.Type
+fmt.Println(t.String()) // "int"
+fmt.Println(t)          // "int"
+
+

其中 TypeOf(3) 调用将值 3 传给 interface{} 参数。回到 7.5节 的将一个具体的值转为接口类型会有一个隐式的接口转换操作,它会创建一个包含两个信息的接口值:操作数的动态类型(这里是 int)和它的动态的值(这里是 3)。

+

因为 reflect.TypeOf 返回的是一个动态类型的接口值,它总是返回具体的类型。因此,下面的代码将打印 "*os.File" 而不是 "io.Writer"。稍后,我们将看到能够表达接口类型的 reflect.Type。

+
var w io.Writer = os.Stdout
+fmt.Println(reflect.TypeOf(w)) // "*os.File"
+
+

要注意的是 reflect.Type 接口是满足 fmt.Stringer 接口的。因为打印一个接口的动态类型对于调试和日志是有帮助的, fmt.Printf 提供了一个缩写 %T 参数,内部使用 reflect.TypeOf 来输出:

+
fmt.Printf("%T\n", 3) // "int"
+
+

reflect 包中另一个重要的类型是 Value。一个 reflect.Value 可以装载任意类型的值。函数 reflect.ValueOf 接受任意的 interface{} 类型,并返回一个装载着其动态值的 reflect.Value。和 reflect.TypeOf 类似,reflect.ValueOf 返回的结果也是具体的类型,但是 reflect.Value 也可以持有一个接口值。

+
v := reflect.ValueOf(3) // a reflect.Value
+fmt.Println(v)          // "3"
+fmt.Printf("%v\n", v)   // "3"
+fmt.Println(v.String()) // NOTE: "<int Value>"
+
+

和 reflect.Type 类似,reflect.Value 也满足 fmt.Stringer 接口,但是除非 Value 持有的是字符串,否则 String 方法只返回其类型。而使用 fmt 包的 %v 标志参数会对 reflect.Values 特殊处理。

+

对 Value 调用 Type 方法将返回具体类型所对应的 reflect.Type:

+
t := v.Type()           // a reflect.Type
+fmt.Println(t.String()) // "int"
+
+

reflect.ValueOf 的逆操作是 reflect.Value.Interface 方法。它返回一个 interface{} 类型,装载着与 reflect.Value 相同的具体值:

+
v := reflect.ValueOf(3) // a reflect.Value
+x := v.Interface()      // an interface{}
+i := x.(int)            // an int
+fmt.Printf("%d\n", i)   // "3"
+
+

reflect.Value 和 interface{} 都能装载任意的值。所不同的是,一个空的接口隐藏了值内部的表示方式和所有方法,因此只有我们知道具体的动态类型才能使用类型断言来访问内部的值(就像上面那样),内部值我们没法访问。相比之下,一个 Value 则有很多方法来检查其内容,无论它的具体类型是什么。让我们再次尝试实现我们的格式化函数 format.Any。

+

我们使用 reflect.Value 的 Kind 方法来替代之前的类型 switch。虽然还是有无穷多的类型,但是它们的 kinds 类型却是有限的:Bool、String 和 所有数字类型的基础类型;Array 和 Struct 对应的聚合类型;Chan、Func、Ptr、Slice 和 Map 对应的引用类型;interface 类型;还有表示空值的 Invalid 类型。(空的 reflect.Value 的 kind 即为 Invalid。)

+

gopl.io/ch12/format

+
package format
+
+import (
+	"reflect"
+	"strconv"
+)
+
+// Any formats any value as a string.
+func Any(value interface{}) string {
+	return formatAtom(reflect.ValueOf(value))
+}
+
+// formatAtom formats a value without inspecting its internal structure.
+func formatAtom(v reflect.Value) string {
+	switch v.Kind() {
+	case reflect.Invalid:
+		return "invalid"
+	case reflect.Int, reflect.Int8, reflect.Int16,
+		reflect.Int32, reflect.Int64:
+		return strconv.FormatInt(v.Int(), 10)
+	case reflect.Uint, reflect.Uint8, reflect.Uint16,
+		reflect.Uint32, reflect.Uint64, reflect.Uintptr:
+		return strconv.FormatUint(v.Uint(), 10)
+	// ...floating-point and complex cases omitted for brevity...
+	case reflect.Bool:
+		return strconv.FormatBool(v.Bool())
+	case reflect.String:
+		return strconv.Quote(v.String())
+	case reflect.Chan, reflect.Func, reflect.Ptr, reflect.Slice, reflect.Map:
+		return v.Type().String() + " 0x" +
+			strconv.FormatUint(uint64(v.Pointer()), 16)
+	default: // reflect.Array, reflect.Struct, reflect.Interface
+		return v.Type().String() + " value"
+	}
+}
+
+

到目前为止,我们的函数将每个值视作一个不可分割没有内部结构的物品,因此它叫 formatAtom。对于聚合类型(结构体和数组)和接口,只是打印值的类型,对于引用类型(channels、functions、pointers、slices 和 maps),打印类型和十六进制的引用地址。虽然还不够理想,但是依然是一个重大的进步,并且 Kind 只关心底层表示,format.Any 也支持具名类型。例如:

+
var x int64 = 1
+var d time.Duration = 1 * time.Nanosecond
+fmt.Println(format.Any(x))                  // "1"
+fmt.Println(format.Any(d))                  // "1"
+fmt.Println(format.Any([]int64{x}))         // "[]int64 0x8202b87b0"
+fmt.Println(format.Any([]time.Duration{d})) // "[]time.Duration 0x8202b87e0"
+
+

12.3. Display,一个递归的值打印器

+

接下来,让我们看看如何改善聚合数据类型的显示。我们并不想完全克隆一个fmt.Sprint函数,我们只是构建一个用于调试用的Display函数:给定任意一个复杂类型 x,打印这个值对应的完整结构,同时标记每个元素的发现路径。让我们从一个例子开始。

+
e, _ := eval.Parse("sqrt(A / pi)")
+Display("e", e)
+
+

在上面的调用中,传入Display函数的参数是在7.9节一个表达式求值函数返回的语法树。Display函数的输出如下:

+
Display e (eval.call):
+e.fn = "sqrt"
+e.args[0].type = eval.binary
+e.args[0].value.op = 47
+e.args[0].value.x.type = eval.Var
+e.args[0].value.x.value = "A"
+e.args[0].value.y.type = eval.Var
+e.args[0].value.y.value = "pi"
+
+

你应该尽量避免在一个包的API中暴露涉及反射的接口。我们将定义一个未导出的display函数用于递归处理工作,导出的是Display函数,它只是display函数简单的包装以接受interface{}类型的参数:

+

gopl.io/ch12/display

+
func Display(name string, x interface{}) {
+	fmt.Printf("Display %s (%T):\n", name, x)
+	display(name, reflect.ValueOf(x))
+}
+
+

在display函数中,我们使用了前面定义的打印基础类型——基本类型、函数和chan等——元素值的formatAtom函数,但是我们会使用reflect.Value的方法来递归显示复杂类型的每一个成员。在递归下降过程中,path字符串,从最开始传入的起始值(这里是“e”),将逐步增长来表示是如何达到当前值(例如“e.args[0].value”)的。

+

因为我们不再模拟fmt.Sprint函数,我们将直接使用fmt包来简化我们的例子实现。

+
func display(path string, v reflect.Value) {
+	switch v.Kind() {
+	case reflect.Invalid:
+		fmt.Printf("%s = invalid\n", path)
+	case reflect.Slice, reflect.Array:
+		for i := 0; i < v.Len(); i++ {
+			display(fmt.Sprintf("%s[%d]", path, i), v.Index(i))
+		}
+	case reflect.Struct:
+		for i := 0; i < v.NumField(); i++ {
+			fieldPath := fmt.Sprintf("%s.%s", path, v.Type().Field(i).Name)
+			display(fieldPath, v.Field(i))
+		}
+	case reflect.Map:
+		for _, key := range v.MapKeys() {
+			display(fmt.Sprintf("%s[%s]", path,
+				formatAtom(key)), v.MapIndex(key))
+		}
+	case reflect.Ptr:
+		if v.IsNil() {
+			fmt.Printf("%s = nil\n", path)
+		} else {
+			display(fmt.Sprintf("(*%s)", path), v.Elem())
+		}
+	case reflect.Interface:
+		if v.IsNil() {
+			fmt.Printf("%s = nil\n", path)
+		} else {
+			fmt.Printf("%s.type = %s\n", path, v.Elem().Type())
+			display(path+".value", v.Elem())
+		}
+	default: // basic types, channels, funcs
+		fmt.Printf("%s = %s\n", path, formatAtom(v))
+	}
+}
+
+

让我们针对不同类型分别讨论。

+

Slice和数组: 两种的处理逻辑是一样的。Len方法返回slice或数组值中的元素个数,Index(i)获得索引i对应的元素,返回的也是一个reflect.Value;如果索引i超出范围的话将导致panic异常,这与数组或slice类型内建的len(a)和a[i]操作类似。display针对序列中的每个元素递归调用自身处理,我们通过在递归处理时向path附加“[i]”来表示访问路径。

+

虽然reflect.Value类型带有很多方法,但是只有少数的方法能对任意值都安全调用。例如,Index方法只能对Slice、数组或字符串类型的值调用,如果对其它类型调用则会导致panic异常。

+

结构体: NumField方法报告结构体中成员的数量,Field(i)以reflect.Value类型返回第i个成员的值。成员列表也包括通过匿名字段提升上来的成员。为了在path添加“.f”来表示成员路径,我们必须获得结构体对应的reflect.Type类型信息,然后访问结构体第i个成员的名字。

+

Maps: MapKeys方法返回一个reflect.Value类型的slice,每一个元素对应map的一个key。和往常一样,遍历map时顺序是随机的。MapIndex(key)返回map中key对应的value。我们向path添加“[key]”来表示访问路径。(我们这里有一个未完成的工作。其实map的key的类型并不局限于formatAtom能完美处理的类型;数组、结构体和接口都可以作为map的key。针对这种类型,完善key的显示信息是练习12.1的任务。)

+

指针: Elem方法返回指针指向的变量,依然是reflect.Value类型。即使指针是nil,这个操作也是安全的,在这种情况下指针是Invalid类型,但是我们可以用IsNil方法来显式地测试一个空指针,这样我们可以打印更合适的信息。我们在path前面添加“*”,并用括弧包含以避免歧义。

+

接口: 再一次,我们使用IsNil方法来测试接口是否是nil,如果不是,我们可以调用v.Elem()来获取接口对应的动态值,并且打印对应的类型和值。

+

现在我们的Display函数总算完工了,让我们看看它的表现吧。下面的Movie类型是在4.5节的电影类型上演变来的:

+
type Movie struct {
+	Title, Subtitle string
+	Year            int
+	Color           bool
+	Actor           map[string]string
+	Oscars          []string
+	Sequel          *string
+}
+
+

让我们声明一个该类型的变量,然后看看Display函数如何显示它:

+
strangelove := Movie{
+	Title:    "Dr. Strangelove",
+	Subtitle: "How I Learned to Stop Worrying and Love the Bomb",
+	Year:     1964,
+	Color:    false,
+	Actor: map[string]string{
+		"Dr. Strangelove":            "Peter Sellers",
+		"Grp. Capt. Lionel Mandrake": "Peter Sellers",
+		"Pres. Merkin Muffley":       "Peter Sellers",
+		"Gen. Buck Turgidson":        "George C. Scott",
+		"Brig. Gen. Jack D. Ripper":  "Sterling Hayden",
+		`Maj. T.J. "King" Kong`:      "Slim Pickens",
+	},
+
+	Oscars: []string{
+		"Best Actor (Nomin.)",
+		"Best Adapted Screenplay (Nomin.)",
+		"Best Director (Nomin.)",
+		"Best Picture (Nomin.)",
+	},
+}
+
+

Display("strangelove", strangelove)调用将显示(strangelove电影对应的中文名是《奇爱博士》):

+
Display strangelove (display.Movie):
+strangelove.Title = "Dr. Strangelove"
+strangelove.Subtitle = "How I Learned to Stop Worrying and Love the Bomb"
+strangelove.Year = 1964
+strangelove.Color = false
+strangelove.Actor["Gen. Buck Turgidson"] = "George C. Scott"
+strangelove.Actor["Brig. Gen. Jack D. Ripper"] = "Sterling Hayden"
+strangelove.Actor["Maj. T.J. \"King\" Kong"] = "Slim Pickens"
+strangelove.Actor["Dr. Strangelove"] = "Peter Sellers"
+strangelove.Actor["Grp. Capt. Lionel Mandrake"] = "Peter Sellers"
+strangelove.Actor["Pres. Merkin Muffley"] = "Peter Sellers"
+strangelove.Oscars[0] = "Best Actor (Nomin.)"
+strangelove.Oscars[1] = "Best Adapted Screenplay (Nomin.)"
+strangelove.Oscars[2] = "Best Director (Nomin.)"
+strangelove.Oscars[3] = "Best Picture (Nomin.)"
+strangelove.Sequel = nil
+
+

我们也可以使用Display函数来显示标准库中类型的内部结构,例如*os.File类型:

+
Display("os.Stderr", os.Stderr)
+// Output:
+// Display os.Stderr (*os.File):
+// (*(*os.Stderr).file).fd = 2
+// (*(*os.Stderr).file).name = "/dev/stderr"
+// (*(*os.Stderr).file).nepipe = 0
+
+

可以看出,反射能够访问到结构体中未导出的成员。需要当心的是这个例子的输出在不同操作系统上可能是不同的,并且随着标准库的发展也可能导致结果不同。(这也是将这些成员定义为私有成员的原因之一!)我们甚至可以用Display函数来显示reflect.Value 的内部构造(在这里设置为*os.File的类型描述体)。Display("rV", reflect.ValueOf(os.Stderr))调用的输出如下,当然不同环境得到的结果可能有差异:

+
Display rV (reflect.Value):
+(*rV.typ).size = 8
+(*rV.typ).hash = 871609668
+(*rV.typ).align = 8
+(*rV.typ).fieldAlign = 8
+(*rV.typ).kind = 22
+(*(*rV.typ).string) = "*os.File"
+
+(*(*(*rV.typ).uncommonType).methods[0].name) = "Chdir"
+(*(*(*(*rV.typ).uncommonType).methods[0].mtyp).string) = "func() error"
+(*(*(*(*rV.typ).uncommonType).methods[0].typ).string) = "func(*os.File) error"
+...
+
+

观察下面两个例子的区别:

+
var i interface{} = 3
+
+Display("i", i)
+// Output:
+// Display i (int):
+// i = 3
+
+Display("&i", &i)
+// Output:
+// Display &i (*interface {}):
+// (*&i).type = int
+// (*&i).value = 3
+
+

在第一个例子中,Display函数调用reflect.ValueOf(i),它返回一个Int类型的值。正如我们在12.2节中提到的,reflect.ValueOf总是返回一个具体类型的 Value,因为它是从一个接口值提取的内容。

+

在第二个例子中,Display函数调用的是reflect.ValueOf(&i),它返回一个指向i的指针,对应Ptr类型。在switch的Ptr分支中,对这个值调用 Elem 方法,返回一个Value来表示变量 i 本身,对应Interface类型。像这样一个间接获得的Value,可能代表任意类型的值,包括接口类型。display函数递归调用自身,这次它分别打印了这个接口的动态类型和值。

+

对于目前的实现,如果遇到对象图中含有回环,Display将会陷入死循环,例如下面这个首尾相连的链表:

+
// a struct that points to itself
+type Cycle struct{ Value int; Tail *Cycle }
+var c Cycle
+c = Cycle{42, &c}
+Display("c", c)
+
+

Display会永远不停地进行深度递归打印:

+
Display c (display.Cycle):
+c.Value = 42
+(*c.Tail).Value = 42
+(*(*c.Tail).Tail).Value = 42
+(*(*(*c.Tail).Tail).Tail).Value = 42
+...ad infinitum...
+
+

许多Go语言程序都包含了一些循环的数据。让Display支持这类带环的数据结构需要些技巧,需要额外记录迄今访问的路径;相应会带来成本。通用的解决方案是采用 unsafe 的语言特性,我们将在13.3节看到具体的解决方案。

+

带环的数据结构很少会对fmt.Sprint函数造成问题,因为它很少尝试打印完整的数据结构。例如,当它遇到一个指针的时候,它只是简单地打印指针的数字值。在打印包含自身的slice或map时可能卡住,但是这种情况很罕见,不值得付出为了处理回环所需的开销。

+

练习 12.1: 扩展Display函数,使它可以显示包含以结构体或数组作为map的key类型的值。

+

练习 12.2: 增强display函数的稳健性,通过记录边界的步数来确保在超出一定限制后放弃递归。(在13.3节,我们会看到另一种探测数据结构是否存在环的技术。)

+

12.4. 示例: 编码为S表达式

+

Display是一个用于显示结构化数据的调试工具,但是它并不能将任意的Go语言对象编码为通用消息然后用于进程间通信。

+

正如我们在4.5节中中看到的,Go语言的标准库支持了包括JSON、XML和ASN.1等多种编码格式。还有另一种依然被广泛使用的格式是S表达式格式,采用Lisp语言的语法。但是和其他编码格式不同的是,Go语言自带的标准库并不支持S表达式,主要是因为它没有一个公认的标准规范。

+

在本节中,我们将定义一个包用于将任意的Go语言对象编码为S表达式格式,它支持以下结构:

+
42          integer
+"hello"     string(带有Go风格的引号)
+foo         symbol(未用引号括起来的名字)
+(1 2 3)     list  (括号包起来的0个或多个元素)
+
+

布尔型习惯上使用t符号表示true,空列表或nil符号表示false,但是为了简单起见,我们暂时忽略布尔类型。同时忽略的还有chan管道和函数,因为通过反射并无法知道它们的确切状态。我们忽略的还有浮点数、复数和interface。支持它们是练习12.3的任务。

+

我们将Go语言的类型编码为S表达式的方法如下。整数和字符串以显而易见的方式编码。空值编码为nil符号。数组和slice被编码为列表。

+

结构体被编码为成员对象的列表,每个成员对象对应一个有两个元素的子列表,子列表的第一个元素是成员的名字,第二个元素是成员的值。Map被编码为键值对的列表。传统上,S表达式使用点状符号列表(key . value)结构来表示key/value对,而不是用一个含双元素的列表,不过为了简单我们忽略了点状符号列表。

+

编码是由一个encode递归函数完成,如下所示。它的结构本质上和前面的Display函数类似:

+

gopl.io/ch12/sexpr

+
func encode(buf *bytes.Buffer, v reflect.Value) error {
+	switch v.Kind() {
+	case reflect.Invalid:
+		buf.WriteString("nil")
+
+	case reflect.Int, reflect.Int8, reflect.Int16,
+		reflect.Int32, reflect.Int64:
+		fmt.Fprintf(buf, "%d", v.Int())
+
+	case reflect.Uint, reflect.Uint8, reflect.Uint16,
+		reflect.Uint32, reflect.Uint64, reflect.Uintptr:
+		fmt.Fprintf(buf, "%d", v.Uint())
+
+	case reflect.String:
+		fmt.Fprintf(buf, "%q", v.String())
+
+	case reflect.Ptr:
+		return encode(buf, v.Elem())
+
+	case reflect.Array, reflect.Slice: // (value ...)
+		buf.WriteByte('(')
+		for i := 0; i < v.Len(); i++ {
+			if i > 0 {
+				buf.WriteByte(' ')
+			}
+			if err := encode(buf, v.Index(i)); err != nil {
+				return err
+			}
+		}
+		buf.WriteByte(')')
+
+	case reflect.Struct: // ((name value) ...)
+		buf.WriteByte('(')
+		for i := 0; i < v.NumField(); i++ {
+			if i > 0 {
+				buf.WriteByte(' ')
+			}
+			fmt.Fprintf(buf, "(%s ", v.Type().Field(i).Name)
+			if err := encode(buf, v.Field(i)); err != nil {
+				return err
+			}
+			buf.WriteByte(')')
+		}
+		buf.WriteByte(')')
+
+	case reflect.Map: // ((key value) ...)
+		buf.WriteByte('(')
+		for i, key := range v.MapKeys() {
+			if i > 0 {
+				buf.WriteByte(' ')
+			}
+			buf.WriteByte('(')
+			if err := encode(buf, key); err != nil {
+				return err
+			}
+			buf.WriteByte(' ')
+			if err := encode(buf, v.MapIndex(key)); err != nil {
+				return err
+			}
+			buf.WriteByte(')')
+		}
+		buf.WriteByte(')')
+
+	default: // float, complex, bool, chan, func, interface
+		return fmt.Errorf("unsupported type: %s", v.Type())
+	}
+	return nil
+}
+
+

Marshal函数是对encode的包装,以保持和encoding/...下其它包有着相似的API:

+
// Marshal encodes a Go value in S-expression form.
+func Marshal(v interface{}) ([]byte, error) {
+	var buf bytes.Buffer
+	if err := encode(&buf, reflect.ValueOf(v)); err != nil {
+		return nil, err
+	}
+	return buf.Bytes(), nil
+}
+
+

下面是Marshal对12.3节的strangelove变量编码后的结果:

+
((Title "Dr. Strangelove") (Subtitle "How I Learned to Stop Worrying and Lo
+ve the Bomb") (Year 1964) (Actor (("Grp. Capt. Lionel Mandrake" "Peter Sell
+ers") ("Pres. Merkin Muffley" "Peter Sellers") ("Gen. Buck Turgidson" "Geor
+ge C. Scott") ("Brig. Gen. Jack D. Ripper" "Sterling Hayden") ("Maj. T.J. \
+"King\" Kong" "Slim Pickens") ("Dr. Strangelove" "Peter Sellers"))) (Oscars
+("Best Actor (Nomin.)" "Best Adapted Screenplay (Nomin.)" "Best Director (N
+omin.)" "Best Picture (Nomin.)")) (Sequel nil))
+
+

整个输出编码为一行中以减少输出的大小,但是也很难阅读。下面是对S表达式手动格式化的结果。编写一个S表达式的美化格式化函数将作为一个具有挑战性的练习任务;不过 http://gopl.io 也提供了一个简单的版本。

+
((Title "Dr. Strangelove")
+ (Subtitle "How I Learned to Stop Worrying and Love the Bomb")
+ (Year 1964)
+ (Actor (("Grp. Capt. Lionel Mandrake" "Peter Sellers")
+         ("Pres. Merkin Muffley" "Peter Sellers")
+         ("Gen. Buck Turgidson" "George C. Scott")
+         ("Brig. Gen. Jack D. Ripper" "Sterling Hayden")
+         ("Maj. T.J. \"King\" Kong" "Slim Pickens")
+         ("Dr. Strangelove" "Peter Sellers")))
+ (Oscars ("Best Actor (Nomin.)"
+          "Best Adapted Screenplay (Nomin.)"
+          "Best Director (Nomin.)"
+          "Best Picture (Nomin.)"))
+ (Sequel nil))
+
+

和fmt.Print、json.Marshal、Display函数类似,sexpr.Marshal函数处理带环的数据结构也会陷入死循环。

+

在12.6节中,我们将给出S表达式解码器的实现步骤,但是在那之前,我们还需要先了解如何通过反射技术来更新程序的变量。

+

练习 12.3: 实现encode函数缺少的分支。将布尔类型编码为t和nil,浮点数编码为Go语言的格式,复数1+2i编码为#C(1.0 2.0)格式。接口编码为类型名和值对,例如("[]int" (1 2 3)),但是这个形式可能会造成歧义:reflect.Type.String方法对于不同的类型可能返回相同的结果。

+

练习 12.4: 修改encode函数,以上面的格式化形式输出S表达式。

+

练习 12.5: 修改encode函数,用JSON格式代替S表达式格式。然后使用标准库提供的json.Unmarshal解码器来验证函数是正确的。

+

练习 12.6: 修改encode,作为一个优化,忽略对是零值对象的编码。

+

练习 12.7: 创建一个基于流式的API,用于S表达式的解码,和json.Decoder(§4.5)函数功能类似。

+

12.5. 通过reflect.Value修改值

+

到目前为止,反射还只是程序中变量的另一种读取方式。然而,在本节中我们将重点讨论如何通过反射机制来修改变量。

+

回想一下,Go语言中类似x、x.f[1]和*p形式的表达式都可以表示变量,但是其它如x + 1和f(2)则不是变量。一个变量就是一个可寻址的内存空间,里面存储了一个值,并且存储的值可以通过内存地址来更新。

+

对于reflect.Values也有类似的区别。有一些reflect.Values是可取地址的;其它一些则不可以。考虑以下的声明语句:

+
x := 2                   // value   type    variable?
+a := reflect.ValueOf(2)  // 2       int     no
+b := reflect.ValueOf(x)  // 2       int     no
+c := reflect.ValueOf(&x) // &x      *int    no
+d := c.Elem()            // 2       int     yes (x)
+
+

其中a对应的变量不可取地址。因为a中的值仅仅是整数2的拷贝副本。b中的值也同样不可取地址。c中的值还是不可取地址,它只是一个指针&x的拷贝。实际上,所有通过reflect.ValueOf(x)返回的reflect.Value都是不可取地址的。但是对于d,它是c的解引用方式生成的,指向另一个变量,因此是可取地址的。我们可以通过调用reflect.ValueOf(&x).Elem(),来获取任意变量x对应的可取地址的Value。

+

我们可以通过调用reflect.Value的CanAddr方法来判断其是否可以被取地址:

+
fmt.Println(a.CanAddr()) // "false"
+fmt.Println(b.CanAddr()) // "false"
+fmt.Println(c.CanAddr()) // "false"
+fmt.Println(d.CanAddr()) // "true"
+
+

每当我们通过指针间接地获取的reflect.Value都是可取地址的,即使开始的是一个不可取地址的Value。在反射机制中,所有关于是否支持取地址的规则都是类似的。例如,slice的索引表达式e[i]将隐式地包含一个指针,它就是可取地址的,即使开始的e表达式不支持也没有关系。以此类推,reflect.ValueOf(e).Index(i)对应的值也是可取地址的,即使原始的reflect.ValueOf(e)不支持也没有关系。

+

要从变量对应的可取地址的reflect.Value来访问变量需要三个步骤。第一步是调用Addr()方法,它返回一个Value,里面保存了指向变量的指针。然后是在Value上调用Interface()方法,也就是返回一个interface{},里面包含指向变量的指针。最后,如果我们知道变量的类型,我们可以使用类型的断言机制将得到的interface{}类型的接口强制转为普通的类型指针。这样我们就可以通过这个普通指针来更新变量了:

+
x := 2
+d := reflect.ValueOf(&x).Elem()   // d refers to the variable x
+px := d.Addr().Interface().(*int) // px := &x
+*px = 3                           // x = 3
+fmt.Println(x)                    // "3"
+
+

或者,不使用指针,而是通过调用可取地址的reflect.Value的reflect.Value.Set方法来更新对应的值:

+
d.Set(reflect.ValueOf(4))
+fmt.Println(x) // "4"
+
+

Set方法将在运行时执行和编译时进行类似的可赋值性约束的检查。以上代码,变量和值都是int类型,但是如果变量是int64类型,那么程序将抛出一个panic异常,所以关键问题是要确保改类型的变量可以接受对应的值:

+
d.Set(reflect.ValueOf(int64(5))) // panic: int64 is not assignable to int
+
+

同样,对一个不可取地址的reflect.Value调用Set方法也会导致panic异常:

+
x := 2
+b := reflect.ValueOf(x)
+b.Set(reflect.ValueOf(3)) // panic: Set using unaddressable value
+
+

这里有很多用于基本数据类型的Set方法:SetInt、SetUint、SetString和SetFloat等。

+
d := reflect.ValueOf(&x).Elem()
+d.SetInt(3)
+fmt.Println(x) // "3"
+
+

从某种程度上说,这些Set方法总是尽可能地完成任务。以SetInt为例,只要变量是某种类型的有符号整数就可以工作,即使是一些命名的类型、甚至只要底层数据类型是有符号整数就可以,而且如果对于变量类型值太大的话会被自动截断。但需要谨慎的是:对于一个引用interface{}类型的reflect.Value调用SetInt会导致panic异常,即使那个interface{}变量对于整数类型也不行。

+
x := 1
+rx := reflect.ValueOf(&x).Elem()
+rx.SetInt(2)                     // OK, x = 2
+rx.Set(reflect.ValueOf(3))       // OK, x = 3
+rx.SetString("hello")            // panic: string is not assignable to int
+rx.Set(reflect.ValueOf("hello")) // panic: string is not assignable to int
+
+var y interface{}
+ry := reflect.ValueOf(&y).Elem()
+ry.SetInt(2)                     // panic: SetInt called on interface Value
+ry.Set(reflect.ValueOf(3))       // OK, y = int(3)
+ry.SetString("hello")            // panic: SetString called on interface Value
+ry.Set(reflect.ValueOf("hello")) // OK, y = "hello"
+
+

当我们用Display显示os.Stdout结构时,我们发现反射可以越过Go语言的导出规则的限制读取结构体中未导出的成员,比如在类Unix系统上os.File结构体中的fd int成员。然而,利用反射机制并不能修改这些未导出的成员:

+
stdout := reflect.ValueOf(os.Stdout).Elem() // *os.Stdout, an os.File var
+fmt.Println(stdout.Type())                  // "os.File"
+fd := stdout.FieldByName("fd")
+fmt.Println(fd.Int()) // "1"
+fd.SetInt(2)          // panic: unexported field
+
+

一个可取地址的reflect.Value会记录一个结构体成员是否是未导出成员,如果是的话则拒绝修改操作。因此,CanAddr方法并不能正确反映一个变量是否是可以被修改的。另一个相关的方法CanSet是用于检查对应的reflect.Value是否是可取地址并可被修改的:

+
fmt.Println(fd.CanAddr(), fd.CanSet()) // "true false"
+
+

12.6. 示例: 解码S表达式

+

标准库中encoding/...下每个包中提供的Marshal编码函数都有一个对应的Unmarshal函数用于解码。例如,我们在4.5节中看到的,要将包含JSON编码格式的字节slice数据解码为我们自己的Movie类型(§12.3),我们可以这样做:

+
data := []byte{/* ... */}
+var movie Movie
+err := json.Unmarshal(data, &movie)
+
+

Unmarshal函数使用了反射机制类修改movie变量的每个成员,根据输入的内容为Movie成员创建对应的map、结构体和slice。

+

现在让我们为S表达式编码实现一个简易的Unmarshal,类似于前面的json.Unmarshal标准库函数,对应我们之前实现的sexpr.Marshal函数的逆操作。我们必须提醒一下,一个健壮的和通用的实现通常需要比例子更多的代码,为了便于演示我们采用了精简的实现。我们只支持S表达式有限的子集,同时处理错误的方式也比较粗暴,代码的目的是为了演示反射的用法,而不是构造一个实用的S表达式的解码器。

+

词法分析器lexer使用了标准库中的text/scanner包将输入流的字节数据解析为一个个类似注释、标识符、字符串面值和数字面值之类的标记。输入扫描器scanner的Scan方法将提前扫描和返回下一个记号,对于rune类型。大多数记号,比如“(”,对应一个单一rune可表示的Unicode字符,但是text/scanner也可以用小的负数表示记号标识符、字符串等由多个字符组成的记号。调用Scan方法将返回这些记号的类型,接着调用TokenText方法将返回记号对应的文本内容。

+

因为每个解析器可能需要多次使用当前的记号,但是Scan会一直向前扫描,所以我们包装了一个lexer扫描器辅助类型,用于跟踪最近由Scan方法返回的记号。

+

gopl.io/ch12/sexpr

+
type lexer struct {
+	scan  scanner.Scanner
+	token rune // the current token
+}
+
+func (lex *lexer) next()        { lex.token = lex.scan.Scan() }
+func (lex *lexer) text() string { return lex.scan.TokenText() }
+
+func (lex *lexer) consume(want rune) {
+	if lex.token != want { // NOTE: Not an example of good error handling.
+		panic(fmt.Sprintf("got %q, want %q", lex.text(), want))
+	}
+	lex.next()
+}
+
+

现在让我们转到语法解析器。它主要包含两个功能。第一个是read函数,用于读取S表达式的当前标记,然后根据S表达式的当前标记更新可取地址的reflect.Value对应的变量v。

+
func read(lex *lexer, v reflect.Value) {
+	switch lex.token {
+	case scanner.Ident:
+		// The only valid identifiers are
+		// "nil" and struct field names.
+		if lex.text() == "nil" {
+			v.Set(reflect.Zero(v.Type()))
+			lex.next()
+			return
+		}
+	case scanner.String:
+		s, _ := strconv.Unquote(lex.text()) // NOTE: ignoring errors
+		v.SetString(s)
+		lex.next()
+		return
+	case scanner.Int:
+		i, _ := strconv.Atoi(lex.text()) // NOTE: ignoring errors
+		v.SetInt(int64(i))
+		lex.next()
+		return
+	case '(':
+		lex.next()
+		readList(lex, v)
+		lex.next() // consume ')'
+		return
+	}
+	panic(fmt.Sprintf("unexpected token %q", lex.text()))
+}
+
+

我们的S表达式使用标识符区分两个不同类型,结构体成员名和nil值的指针。read函数值处理nil类型的标识符。当遇到scanner.Ident为“nil”时,使用reflect.Zero函数将变量v设置为零值。而其它任何类型的标识符,我们都作为错误处理。后面的readList函数将处理结构体的成员名。

+

一个“(”标记对应一个列表的开始。第二个函数readList,将一个列表解码到一个聚合类型中(map、结构体、slice或数组),具体类型依赖于传入待填充变量的类型。每次遇到这种情况,循环继续解析每个元素直到遇到于开始标记匹配的结束标记“)”,endList函数用于检测结束标记。

+

最有趣的部分是递归。最简单的是对数组类型的处理。直到遇到“)”结束标记,我们使用Index函数来获取数组每个元素的地址,然后递归调用read函数处理。和其它错误类似,如果输入数据导致解码器的引用超出了数组的范围,解码器将抛出panic异常。slice也采用类似方法解析,不同的是我们将为每个元素创建新的变量,然后将元素添加到slice的末尾。

+

在循环处理结构体和map每个元素时必须解码一个(key value)格式的对应子列表。对于结构体,key部分对于成员的名字。和数组类似,我们使用FieldByName找到结构体对应成员的变量,然后递归调用read函数处理。对于map,key可能是任意类型,对元素的处理方式和slice类似,我们创建一个新的变量,然后递归填充它,最后将新解析到的key/value对添加到map。

+
func readList(lex *lexer, v reflect.Value) {
+	switch v.Kind() {
+	case reflect.Array: // (item ...)
+		for i := 0; !endList(lex); i++ {
+			read(lex, v.Index(i))
+		}
+
+	case reflect.Slice: // (item ...)
+		for !endList(lex) {
+			item := reflect.New(v.Type().Elem()).Elem()
+			read(lex, item)
+			v.Set(reflect.Append(v, item))
+		}
+
+	case reflect.Struct: // ((name value) ...)
+		for !endList(lex) {
+			lex.consume('(')
+			if lex.token != scanner.Ident {
+				panic(fmt.Sprintf("got token %q, want field name", lex.text()))
+			}
+			name := lex.text()
+			lex.next()
+			read(lex, v.FieldByName(name))
+			lex.consume(')')
+		}
+
+	case reflect.Map: // ((key value) ...)
+		v.Set(reflect.MakeMap(v.Type()))
+		for !endList(lex) {
+			lex.consume('(')
+			key := reflect.New(v.Type().Key()).Elem()
+			read(lex, key)
+			value := reflect.New(v.Type().Elem()).Elem()
+			read(lex, value)
+			v.SetMapIndex(key, value)
+			lex.consume(')')
+		}
+
+	default:
+		panic(fmt.Sprintf("cannot decode list into %v", v.Type()))
+	}
+}
+
+func endList(lex *lexer) bool {
+	switch lex.token {
+	case scanner.EOF:
+		panic("end of file")
+	case ')':
+		return true
+	}
+	return false
+}
+
+

最后,我们将解析器包装为导出的Unmarshal解码函数,隐藏了一些初始化和清理等边缘处理。内部解析器以panic的方式抛出错误,但是Unmarshal函数通过在defer语句调用recover函数来捕获内部panic(§5.10),然后返回一个对panic对应的错误信息。

+
// Unmarshal parses S-expression data and populates the variable
+// whose address is in the non-nil pointer out.
+func Unmarshal(data []byte, out interface{}) (err error) {
+	lex := &lexer{scan: scanner.Scanner{Mode: scanner.GoTokens}}
+	lex.scan.Init(bytes.NewReader(data))
+	lex.next() // get the first token
+	defer func() {
+		// NOTE: this is not an example of ideal error handling.
+		if x := recover(); x != nil {
+			err = fmt.Errorf("error at %s: %v", lex.scan.Position, x)
+		}
+	}()
+	read(lex, reflect.ValueOf(out).Elem())
+	return nil
+}
+
+

生产实现不应该对任何输入问题都用panic形式报告,而且应该报告一些错误相关的信息,例如出现错误输入的行号和位置等。尽管如此,我们希望通过这个例子来展示类似encoding/json等包底层代码的实现思路,以及如何使用反射机制来填充数据结构。

+

练习 12.8: sexpr.Unmarshal函数和json.Unmarshal一样,都要求在解码前输入完整的字节slice。定义一个和json.Decoder类似的sexpr.Decoder类型,支持从一个io.Reader流解码。修改sexpr.Unmarshal函数,使用这个新的类型实现。

+

练习 12.9: 编写一个基于标记的API用于解码S表达式,参考xml.Decoder(7.14)的风格。你将需要五种类型的标记:Symbol、String、Int、StartList和EndList。

+

练习 12.10: 扩展sexpr.Unmarshal函数,支持布尔型、浮点数和interface类型的解码,使用 练习 12.3: 的方案。(提示:要解码接口,你需要将name映射到每个支持类型的reflect.Type。)

+

12.8. 显示一个类型的方法集

+

我们的最后一个例子是使用reflect.Type来打印任意值的类型和枚举它的方法:

+

gopl.io/ch12/methods

+
// Print prints the method set of the value x.
+func Print(x interface{}) {
+	v := reflect.ValueOf(x)
+	t := v.Type()
+	fmt.Printf("type %s\n", t)
+
+	for i := 0; i < v.NumMethod(); i++ {
+		methType := v.Method(i).Type()
+		fmt.Printf("func (%s) %s%s\n", t, t.Method(i).Name,
+			strings.TrimPrefix(methType.String(), "func"))
+	}
+}
+
+

reflect.Type和reflect.Value都提供了一个Method方法。每次t.Method(i)调用将一个reflect.Method的实例,对应一个用于描述一个方法的名称和类型的结构体。每次v.Method(i)方法调用都返回一个reflect.Value以表示对应的值(§6.4),也就是一个方法是绑到它的接收者的。使用reflect.Value.Call方法(我们这里没有演示),将可以调用一个Func类型的Value,但是这个例子中只用到了它的类型。

+

这是属于time.Duration和*strings.Replacer两个类型的方法:

+
methods.Print(time.Hour)
+// Output:
+// type time.Duration
+// func (time.Duration) Hours() float64
+// func (time.Duration) Minutes() float64
+// func (time.Duration) Nanoseconds() int64
+// func (time.Duration) Seconds() float64
+// func (time.Duration) String() string
+
+methods.Print(new(strings.Replacer))
+// Output:
+// type *strings.Replacer
+// func (*strings.Replacer) Replace(string) string
+// func (*strings.Replacer) WriteString(io.Writer, string) (int, error)
+
+

12.9. 几点忠告

+

虽然反射提供的API远多于我们讲到的,我们前面的例子主要是给出了一个方向,通过反射可以实现哪些功能。反射是一个强大并富有表达力的工具,但是它应该被小心地使用,原因有三。

+

第一个原因是,基于反射的代码是比较脆弱的。对于每一个会导致编译器报告类型错误的问题,在反射中都有与之相对应的误用问题,不同的是编译器会在构建时马上报告错误,而反射则是在真正运行到的时候才会抛出panic异常,可能是写完代码很久之后了,而且程序也可能运行了很长的时间。

+

以前面的readList函数(§12.6)为例,为了从输入读取字符串并填充int类型的变量而调用的reflect.Value.SetString方法可能导致panic异常。绝大多数使用反射的程序都有类似的风险,需要非常小心地检查每个reflect.Value的对应值的类型、是否可取地址,还有是否可以被修改等。

+

避免这种因反射而导致的脆弱性的问题的最好方法,是将所有的反射相关的使用控制在包的内部,如果可能的话避免在包的API中直接暴露reflect.Value类型,这样可以限制一些非法输入。如果无法做到这一点,在每个有风险的操作前指向额外的类型检查。以标准库中的代码为例,当fmt.Printf收到一个非法的操作数时,它并不会抛出panic异常,而是打印相关的错误信息。程序虽然还有BUG,但是会更加容易诊断。

+
fmt.Printf("%d %s\n", "hello", 42) // "%!d(string=hello) %!s(int=42)"
+
+

反射同样降低了程序的安全性,还影响了自动化重构和分析工具的准确性,因为它们无法识别运行时才能确认的类型信息。

+

避免使用反射的第二个原因是,即使对应类型提供了相同文档,但是反射的操作不能做静态类型检查,而且大量反射的代码通常难以理解。总是需要小心翼翼地为每个导出的类型和其它接受interface{}或reflect.Value类型参数的函数维护说明文档。

+

第三个原因,基于反射的代码通常比正常的代码运行速度慢一到两个数量级。对于一个典型的项目,大部分函数的性能和程序的整体性能关系不大,所以当反射能使程序更加清晰的时候可以考虑使用。测试是一个特别适合使用反射的场景,因为每个测试的数据集都很小。但是对于性能关键路径的函数,最好避免使用反射。

+

第13章 底层编程

+

Go语言的设计包含了诸多安全策略,限制了可能导致程序运行出错的用法。编译时类型检查可以发现大多数类型不匹配的操作,例如两个字符串做减法的错误。字符串、map、slice和chan等所有的内置类型,都有严格的类型转换规则。

+

对于无法静态检测到的错误,例如数组访问越界或使用空指针,运行时动态检测可以保证程序在遇到问题的时候立即终止并打印相关的错误信息。自动内存管理(垃圾内存自动回收)可以消除大部分野指针和内存泄漏相关的问题。

+

Go语言的实现刻意隐藏了很多底层细节。我们无法知道一个结构体真实的内存布局,也无法获取一个运行时函数对应的机器码,也无法知道当前的goroutine是运行在哪个操作系统线程之上。事实上,Go语言的调度器会自己决定是否需要将某个goroutine从一个操作系统线程转移到另一个操作系统线程。一个指向变量的指针也并没有展示变量真实的地址。因为垃圾回收器可能会根据需要移动变量的内存位置,当然变量对应的地址也会被自动更新。

+

总的来说,Go语言的这些特性使得Go程序相比较低级的C语言来说更容易预测和理解,程序也不容易崩溃。通过隐藏底层的实现细节,也使得Go语言编写的程序具有高度的可移植性,因为语言的语义在很大程度上是独立于任何编译器实现、操作系统和CPU系统结构的(当然也不是完全绝对独立:例如int等类型就依赖于CPU机器字的大小,某些表达式求值的具体顺序,还有编译器实现的一些额外的限制等)。

+

有时候我们可能会放弃使用部分语言特性而优先选择具有更好性能的方法,例如需要与其他语言编写的库进行互操作,或者用纯Go语言无法实现的某些函数。

+

在本章,我们将展示如何使用unsafe包来摆脱Go语言规则带来的限制,讲述如何创建C语言函数库的绑定,以及如何进行系统调用。

+

本章提供的方法不应该轻易使用(译注:属于黑魔法,虽然功能很强大,但是也容易误伤到自己)。如果没有处理好细节,它们可能导致各种不可预测的并且隐晦的错误,甚至连有经验的C语言程序员也无法理解这些错误。使用unsafe包的同时也放弃了Go语言保证与未来版本的兼容性的承诺,因为它必然会有意无意中使用很多非公开的实现细节,而这些实现的细节在未来的Go语言中很可能会被改变。

+

要注意的是,unsafe包是一个采用特殊方式实现的包。虽然它可以和普通包一样的导入和使用,但它实际上是由编译器实现的。它提供了一些访问语言内部特性的方法,特别是内存布局相关的细节。将这些特性封装到一个独立的包中,是为在极少数情况下需要使用的时候,同时引起人们的注意(译注:因为看包的名字就知道使用unsafe包是不安全的)。此外,有一些环境因为安全的因素可能限制这个包的使用。

+

不过unsafe包被广泛地用于比较低级的包,例如runtime、os、syscall还有net包等,因为它们需要和操作系统密切配合,但是对于普通的程序一般是不需要使用unsafe包的。

+

13.1. unsafe.Sizeof, Alignof 和 Offsetof

+

unsafe.Sizeof函数返回操作数在内存中的字节大小,参数可以是任意类型的表达式,但是它并不会对表达式进行求值。一个Sizeof函数调用是一个对应uintptr类型的常量表达式,因此返回的结果可以用作数组类型的长度大小,或者用作计算其他的常量。

+
import "unsafe"
+fmt.Println(unsafe.Sizeof(float64(0))) // "8"
+
+

Sizeof函数返回的大小只包括数据结构中固定的部分,例如字符串对应结构体中的指针和字符串长度部分,但是并不包含指针指向的字符串的内容。Go语言中非聚合类型通常有一个固定的大小,尽管在不同工具链下生成的实际大小可能会有所不同。考虑到可移植性,引用类型或包含引用类型的大小在32位平台上是4个字节,在64位平台上是8个字节。

+

计算机在加载和保存数据时,如果内存地址合理地对齐的将会更有效率。例如2字节大小的int16类型的变量地址应该是偶数,一个4字节大小的rune类型变量的地址应该是4的倍数,一个8字节大小的float64、uint64或64-bit指针类型变量的地址应该是8字节对齐的。但是对于再大的地址对齐倍数则是不需要的,即使是complex128等较大的数据类型最多也只是8字节对齐。

+

由于地址对齐这个因素,一个聚合类型(结构体或数组)的大小至少是所有字段或元素大小的总和,或者更大因为可能存在内存空洞。内存空洞是编译器自动添加的没有被使用的内存空间,用于保证后面每个字段或元素的地址相对于结构或数组的开始地址能够合理地对齐(译注:内存空洞可能会存在一些随机数据,可能会对用unsafe包直接操作内存的处理产生影响)。

+ + + + + + + + + + + +
类型大小
bool1个字节
intN, uintN, floatN, complexNN/8个字节(例如float64是8个字节)
int, uint, uintptr1个机器字
*T1个机器字
string2个机器字(data、len)
[]T3个机器字(data、len、cap)
map1个机器字
func1个机器字
chan1个机器字
interface2个机器字(type、value)
+

Go语言的规范并没有要求一个字段的声明顺序和内存中的顺序是一致的,所以理论上一个编译器可以随意地重新排列每个字段的内存位置,虽然在写作本书的时候编译器还没有这么做。下面的三个结构体虽然有着相同的字段,但是第一种写法比另外的两个需要多50%的内存。

+
                               // 64-bit  32-bit
+struct{ bool; float64; int16 } // 3 words 4words
+struct{ float64; int16; bool } // 2 words 3words
+struct{ bool; int16; float64 } // 2 words 3words
+
+

关于内存地址对齐算法的细节超出了本书的范围,也不是每一个结构体都需要担心这个问题,不过有效的包装可以使数据结构更加紧凑(译注:未来的Go语言编译器应该会默认优化结构体的顺序,当然应该也能够指定具体的内存布局,相同讨论请参考 Issue10014 ),内存使用率和性能都可能会受益。

+

unsafe.Alignof 函数返回对应参数的类型需要对齐的倍数。和 Sizeof 类似, Alignof 也是返回一个常量表达式,对应一个常量。通常情况下布尔和数字类型需要对齐到它们本身的大小(最多8个字节),其它的类型对齐到机器字大小。

+

unsafe.Offsetof 函数的参数必须是一个字段 x.f,然后返回 f 字段相对于 x 起始地址的偏移量,包括可能的空洞。

+

图 13.1 显示了一个结构体变量 x 以及其在32位和64位机器上的典型的内存。灰色区域是空洞。

+
var x struct {
+	a bool
+	b int16
+	c []int
+}
+
+

下面显示了对x和它的三个字段调用unsafe包相关函数的计算结果:

+

+

32位系统:

+
Sizeof(x)   = 16  Alignof(x)   = 4
+Sizeof(x.a) = 1   Alignof(x.a) = 1 Offsetof(x.a) = 0
+Sizeof(x.b) = 2   Alignof(x.b) = 2 Offsetof(x.b) = 2
+Sizeof(x.c) = 12  Alignof(x.c) = 4 Offsetof(x.c) = 4
+
+

64位系统:

+
Sizeof(x)   = 32  Alignof(x)   = 8
+Sizeof(x.a) = 1   Alignof(x.a) = 1 Offsetof(x.a) = 0
+Sizeof(x.b) = 2   Alignof(x.b) = 2 Offsetof(x.b) = 2
+Sizeof(x.c) = 24  Alignof(x.c) = 8 Offsetof(x.c) = 8
+
+

虽然这几个函数在不安全的unsafe包,但是这几个函数调用并不是真的不安全,特别在需要优化内存空间时它们返回的结果对于理解原生的内存布局很有帮助。

+

13.2. unsafe.Pointer

+

大多数指针类型会写成*T,表示是“一个指向T类型变量的指针”。unsafe.Pointer是特别定义的一种指针类型(译注:类似C语言中的void*类型的指针),它可以包含任意类型变量的地址。当然,我们不可以直接通过*p来获取unsafe.Pointer指针指向的真实变量的值,因为我们并不知道变量的具体类型。和普通指针一样,unsafe.Pointer指针也是可以比较的,并且支持和nil常量比较判断是否为空指针。

+

一个普通的*T类型指针可以被转化为unsafe.Pointer类型指针,并且一个unsafe.Pointer类型指针也可以被转回普通的指针,被转回普通的指针类型并不需要和原始的*T类型相同。通过将*float64类型指针转化为*uint64类型指针,我们可以查看一个浮点数变量的位模式。

+
package math
+
+func Float64bits(f float64) uint64 { return *(*uint64)(unsafe.Pointer(&f)) }
+
+fmt.Printf("%#016x\n", Float64bits(1.0)) // "0x3ff0000000000000"
+
+

通过转为新类型指针,我们可以更新浮点数的位模式。通过位模式操作浮点数是可以的,但是更重要的意义是指针转换语法让我们可以在不破坏类型系统的前提下向内存写入任意的值。

+

一个unsafe.Pointer指针也可以被转化为uintptr类型,然后保存到指针型数值变量中(译注:这只是和当前指针相同的一个数字值,并不是一个指针),然后用以做必要的指针数值运算。(第三章内容,uintptr是一个无符号的整型数,足以保存一个地址)这种转换虽然也是可逆的,但是将uintptr转为unsafe.Pointer指针可能会破坏类型系统,因为并不是所有的数字都是有效的内存地址。

+

许多将unsafe.Pointer指针转为原生数字,然后再转回为unsafe.Pointer类型指针的操作也是不安全的。比如下面的例子需要将变量x的地址加上b字段地址偏移量转化为*int16类型指针,然后通过该指针更新x.b:

+

gopl.io/ch13/unsafeptr

+
var x struct {
+	a bool
+	b int16
+	c []int
+}
+
+// 和 pb := &x.b 等价
+pb := (*int16)(unsafe.Pointer(
+	uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)))
+*pb = 42
+fmt.Println(x.b) // "42"
+
+

上面的写法尽管很繁琐,但在这里并不是一件坏事,因为这些功能应该很谨慎地使用。不要试图引入一个uintptr类型的临时变量,因为它可能会破坏代码的安全性(译注:这是真正可以体会unsafe包为何不安全的例子)。下面段代码是错误的:

+
// NOTE: subtly incorrect!
+tmp := uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)
+pb := (*int16)(unsafe.Pointer(tmp))
+*pb = 42
+
+

产生错误的原因很微妙。有时候垃圾回收器会移动一些变量以降低内存碎片等问题。这类垃圾回收器被称为移动GC。当一个变量被移动,所有的保存该变量旧地址的指针必须同时被更新为变量移动后的新地址。从垃圾收集器的视角来看,一个unsafe.Pointer是一个指向变量的指针,因此当变量被移动时对应的指针也必须被更新;但是uintptr类型的临时变量只是一个普通的数字,所以其值不应该被改变。上面错误的代码因为引入一个非指针的临时变量tmp,导致垃圾收集器无法正确识别这个是一个指向变量x的指针。当第二个语句执行时,变量x可能已经被转移,这时候临时变量tmp也就不再是现在的&x.b地址。第三个向之前无效地址空间的赋值语句将彻底摧毁整个程序!

+

还有很多类似原因导致的错误。例如这条语句:

+
pT := uintptr(unsafe.Pointer(new(T))) // 提示: 错误!
+
+

这里并没有指针引用new新创建的变量,因此该语句执行完成之后,垃圾收集器有权马上回收其内存空间,所以返回的pT将是无效的地址。

+

虽然目前的Go语言实现还没有使用移动GC(译注:未来可能实现),但这不该是编写错误代码侥幸的理由:当前的Go语言实现已经有移动变量的场景。在5.2节我们提到goroutine的栈是根据需要动态增长的。当发生栈动态增长的时候,原来栈中的所有变量可能需要被移动到新的更大的栈中,所以我们并不能确保变量的地址在整个使用周期内是不变的。

+

在编写本文时,还没有清晰的原则来指引Go程序员,什么样的unsafe.Pointer和uintptr的转换是不安全的(参考 Issue7192 ). 译注: 该问题已经关闭),因此我们强烈建议按照最坏的方式处理。将所有包含变量地址的uintptr类型变量当作BUG处理,同时减少不必要的unsafe.Pointer类型到uintptr类型的转换。在第一个例子中,有三个转换——字段偏移量到uintptr的转换和转回unsafe.Pointer类型的操作——所有的转换全在一个表达式完成。

+

当调用一个库函数,并且返回的是uintptr类型地址时(译注:普通方法实现的函数尽量不要返回该类型。下面例子是reflect包的函数,reflect包和unsafe包一样都是采用特殊技术实现的,编译器可能给它们开了后门),比如下面反射包中的相关函数,返回的结果应该立即转换为unsafe.Pointer以确保指针指向的是相同的变量。

+
package reflect
+
+func (Value) Pointer() uintptr
+func (Value) UnsafeAddr() uintptr
+func (Value) InterfaceData() [2]uintptr // (index 1)
+
+

13.3. 示例: 深度相等判断

+

来自reflect包的DeepEqual函数可以对两个值进行深度相等判断。DeepEqual函数使用内建的==比较操作符对基础类型进行相等判断,对于复合类型则递归该变量的每个基础类型然后做类似的比较判断。因为它可以工作在任意的类型上,甚至对于一些不支持==操作运算符的类型也可以工作,因此在一些测试代码中广泛地使用该函数。比如下面的代码是用DeepEqual函数比较两个字符串slice是否相等。

+
func TestSplit(t *testing.T) {
+	got := strings.Split("a:b:c", ":")
+	want := []string{"a", "b", "c"};
+	if !reflect.DeepEqual(got, want) { /* ... */ }
+}
+
+

尽管DeepEqual函数很方便,而且可以支持任意的数据类型,但是它也有不足之处。例如,它将一个nil值的map和非nil值但是空的map视作不相等,同样nil值的slice 和非nil但是空的slice也视作不相等。

+
var a, b []string = nil, []string{}
+fmt.Println(reflect.DeepEqual(a, b)) // "false"
+
+var c, d map[string]int = nil, make(map[string]int)
+fmt.Println(reflect.DeepEqual(c, d)) // "false"
+
+

我们希望在这里实现一个自己的Equal函数,用于比较类型的值。和DeepEqual函数类似的地方是它也是基于slice和map的每个元素进行递归比较,不同之处是它将nil值的slice(map类似)和非nil值但是空的slice视作相等的值。基础部分的比较可以基于reflect包完成,和12.3章的Display函数的实现方法类似。同样,我们也定义了一个内部函数equal,用于内部的递归比较。读者目前不用关心seen参数的具体含义。对于每一对需要比较的x和y,equal函数首先检测它们是否都有效(或都无效),然后检测它们是否是相同的类型。剩下的部分是一个巨大的switch分支,用于相同基础类型的元素比较。因为页面空间的限制,我们省略了一些相似的分支。

+

gopl.io/ch13/equal

+
func equal(x, y reflect.Value, seen map[comparison]bool) bool {
+	if !x.IsValid() || !y.IsValid() {
+		return x.IsValid() == y.IsValid()
+	}
+	if x.Type() != y.Type() {
+		return false
+	}
+
+	// ...cycle check omitted (shown later)...
+
+	switch x.Kind() {
+	case reflect.Bool:
+		return x.Bool() == y.Bool()
+	case reflect.String:
+		return x.String() == y.String()
+
+	// ...numeric cases omitted for brevity...
+
+	case reflect.Chan, reflect.UnsafePointer, reflect.Func:
+		return x.Pointer() == y.Pointer()
+	case reflect.Ptr, reflect.Interface:
+		return equal(x.Elem(), y.Elem(), seen)
+	case reflect.Array, reflect.Slice:
+		if x.Len() != y.Len() {
+			return false
+		}
+		for i := 0; i < x.Len(); i++ {
+			if !equal(x.Index(i), y.Index(i), seen) {
+				return false
+			}
+		}
+		return true
+
+	// ...struct and map cases omitted for brevity...
+	}
+	panic("unreachable")
+}
+
+

和前面的建议一样,我们并不公开reflect包相关的接口,所以导出的函数需要在内部自己将变量转为reflect.Value类型。

+
// Equal reports whether x and y are deeply equal.
+func Equal(x, y interface{}) bool {
+	seen := make(map[comparison]bool)
+	return equal(reflect.ValueOf(x), reflect.ValueOf(y), seen)
+}
+
+type comparison struct {
+	x, y unsafe.Pointer
+	t reflect.Type
+}
+
+

为了确保算法对于有环的数据结构也能正常退出,我们必须记录每次已经比较的变量,从而避免进入第二次的比较。Equal函数分配了一组用于比较的结构体,包含每对比较对象的地址(unsafe.Pointer形式保存)和类型。我们要记录类型的原因是,有些不同的变量可能对应相同的地址。例如,如果x和y都是数组类型,那么x和x[0]将对应相同的地址,y和y[0]也是对应相同的地址,这可以用于区分x与y之间的比较或x[0]与y[0]之间的比较是否进行过了。

+
// cycle check
+if x.CanAddr() && y.CanAddr() {
+	xptr := unsafe.Pointer(x.UnsafeAddr())
+	yptr := unsafe.Pointer(y.UnsafeAddr())
+	if xptr == yptr {
+		return true // identical references
+	}
+	c := comparison{xptr, yptr, x.Type()}
+	if seen[c] {
+		return true // already seen
+	}
+	seen[c] = true
+}
+
+

这是Equal函数用法的例子:

+
fmt.Println(Equal([]int{1, 2, 3}, []int{1, 2, 3}))        // "true"
+fmt.Println(Equal([]string{"foo"}, []string{"bar"}))      // "false"
+fmt.Println(Equal([]string(nil), []string{}))             // "true"
+fmt.Println(Equal(map[string]int(nil), map[string]int{})) // "true"
+
+

Equal函数甚至可以处理类似12.3章中导致Display陷入死循环的带有环的数据。

+
// Circular linked lists a -> b -> a and c -> c.
+type link struct {
+	value string
+	tail *link
+}
+a, b, c := &link{value: "a"}, &link{value: "b"}, &link{value: "c"}
+a.tail, b.tail, c.tail = b, a, c
+fmt.Println(Equal(a, a)) // "true"
+fmt.Println(Equal(b, b)) // "true"
+fmt.Println(Equal(c, c)) // "true"
+fmt.Println(Equal(a, b)) // "false"
+fmt.Println(Equal(a, c)) // "false"
+
+

练习 13.1: 定义一个深比较函数,对于十亿以内的数字比较,忽略类型差异。

+

练习 13.2: 编写一个函数,报告其参数是否为循环数据结构。

+

13.4. 通过cgo调用C代码

+

Go程序可能会遇到要访问C语言的某些硬件驱动函数的场景,或者是从一个C++语言实现的嵌入式数据库查询记录的场景,或者是使用Fortran语言实现的一些线性代数库的场景。C语言作为一个通用语言,很多库会选择提供一个C兼容的API,然后用其他不同的编程语言实现(译者:Go语言需要也应该拥抱这些巨大的代码遗产)。

+

在本节中,我们将构建一个简易的数据压缩程序,使用了一个Go语言自带的叫cgo的用于支援C语言函数调用的工具。这类工具一般被称为 foreign-function interfaces (简称ffi),并且在类似工具中cgo也不是唯一的。SWIG(http://swig.org)是另一个类似的且被广泛使用的工具,SWIG提供了很多复杂特性以支援C++的特性,但SWIG并不是我们要讨论的主题。

+

在标准库的compress/...子包有很多流行的压缩算法的编码和解码实现,包括流行的LZW压缩算法(Unix的compress命令用的算法)和DEFLATE压缩算法(GNU gzip命令用的算法)。这些包的API的细节虽然有些差异,但是它们都提供了针对 io.Writer类型输出的压缩接口和提供了针对io.Reader类型输入的解压缩接口。例如:

+
package gzip // compress/gzip
+func NewWriter(w io.Writer) io.WriteCloser
+func NewReader(r io.Reader) (io.ReadCloser, error)
+
+

bzip2压缩算法,是基于优雅的Burrows-Wheeler变换算法,运行速度比gzip要慢,但是可以提供更高的压缩比。标准库的compress/bzip2包目前还没有提供bzip2压缩算法的实现。完全从头开始实现一个压缩算法是一件繁琐的工作,而且 http://bzip.org 已经有现成的libbzip2的开源实现,不仅文档齐全而且性能又好。

+

如果是比较小的C语言库,我们完全可以用纯Go语言重新实现一遍。如果我们对性能也没有特殊要求的话,我们还可以用os/exec包的方法将C编写的应用程序作为一个子进程运行。只有当你需要使用复杂而且性能更高的底层C接口时,就是使用cgo的场景了(译注:用os/exec包调用子进程的方法会导致程序运行时依赖那个应用程序)。下面我们将通过一个例子讲述cgo的具体用法。

+

译注:本章采用的代码都是最新的。因为之前已经出版的书中包含的代码只能在Go1.5之前使用。从Go1.6开始,Go语言已经明确规定了哪些Go语言指针可以直接传入C语言函数。新代码重点是增加了bz2alloc和bz2free的两个函数,用于bz_stream对象空间的申请和释放操作。下面是新代码中增加的注释,说明这个问题:

+
// The version of this program that appeared in the first and second
+// printings did not comply with the proposed rules for passing
+// pointers between Go and C, described here:
+// https://github.com/golang/proposal/blob/master/design/12416-cgo-pointers.md
+//
+// The rules forbid a C function like bz2compress from storing 'in'
+// and 'out' (pointers to variables allocated by Go) into the Go
+// variable 's', even temporarily.
+//
+// The version below, which appears in the third printing, has been
+// corrected.  To comply with the rules, the bz_stream variable must
+// be allocated by C code.  We have introduced two C functions,
+// bz2alloc and bz2free, to allocate and free instances of the
+// bz_stream type.  Also, we have changed bz2compress so that before
+// it returns, it clears the fields of the bz_stream that contain
+// pointers to Go variables.
+
+

要使用libbzip2,我们需要先构建一个bz_stream结构体,用于保持输入和输出缓存。然后有三个函数:BZ2_bzCompressInit用于初始化缓存,BZ2_bzCompress用于将输入缓存的数据压缩到输出缓存,BZ2_bzCompressEnd用于释放不需要的缓存。(目前不要担心包的具体结构,这个例子的目的就是演示各个部分如何组合在一起的。)

+

我们可以在Go代码中直接调用BZ2_bzCompressInit和BZ2_bzCompressEnd,但是对于BZ2_bzCompress,我们将定义一个C语言的包装函数,用它完成真正的工作。下面是C代码,对应一个独立的文件。

+

gopl.io/ch13/bzip

+
/* This file is gopl.io/ch13/bzip/bzip2.c,         */
+/* a simple wrapper for libbzip2 suitable for cgo. */
+#include <bzlib.h>
+
+int bz2compress(bz_stream *s, int action,
+                char *in, unsigned *inlen, char *out, unsigned *outlen) {
+	s->next_in = in;
+	s->avail_in = *inlen;
+	s->next_out = out;
+	s->avail_out = *outlen;
+	int r = BZ2_bzCompress(s, action);
+	*inlen -= s->avail_in;
+	*outlen -= s->avail_out;
+	s->next_in = s->next_out = NULL;
+	return r;
+}
+
+

现在让我们转到Go语言部分,第一部分如下所示。其中import "C"的语句是比较特别的。其实并没有一个叫C的包,但是这行语句会让Go编译程序在编译之前先运行cgo工具。

+
// Package bzip provides a writer that uses bzip2 compression (bzip.org).
+package bzip
+
+/*
+#cgo CFLAGS: -I/usr/include
+#cgo LDFLAGS: -L/usr/lib -lbz2
+#include <bzlib.h>
+#include <stdlib.h>
+bz_stream* bz2alloc() { return calloc(1, sizeof(bz_stream)); }
+int bz2compress(bz_stream *s, int action,
+                char *in, unsigned *inlen, char *out, unsigned *outlen);
+void bz2free(bz_stream* s) { free(s); }
+*/
+import "C"
+
+import (
+	"io"
+	"unsafe"
+)
+
+type writer struct {
+	w      io.Writer // underlying output stream
+	stream *C.bz_stream
+	outbuf [64 * 1024]byte
+}
+
+// NewWriter returns a writer for bzip2-compressed streams.
+func NewWriter(out io.Writer) io.WriteCloser {
+	const blockSize = 9
+	const verbosity = 0
+	const workFactor = 30
+	w := &writer{w: out, stream: C.bz2alloc()}
+	C.BZ2_bzCompressInit(w.stream, blockSize, verbosity, workFactor)
+	return w
+}
+
+

在预处理过程中,cgo工具生成一个临时包用于包含所有在Go语言中访问的C语言的函数或类型。例如C.bz_stream和C.BZ2_bzCompressInit。cgo工具通过以某种特殊的方式调用本地的C编译器来发现在Go源文件导入声明前的注释中包含的C头文件中的内容(译注:import "C"语句前紧挨着的注释是对应cgo的特殊语法,对应必要的构建参数选项和C语言代码)。

+

在cgo注释中还可以包含#cgo指令,用于给C语言工具链指定特殊的参数。例如CFLAGS和LDFLAGS分别对应传给C语言编译器的编译参数和链接器参数,使它们可以从特定目录找到bzlib.h头文件和libbz2.a库文件。这个例子假设你已经在/usr目录成功安装了bzip2库。如果bzip2库是安装在不同的位置,你需要更新这些参数(译注:这里有一个从纯C代码生成的cgo绑定,不依赖bzip2静态库和操作系统的具体环境,具体请访问 https://github.com/chai2010/bzip2 )。

+

NewWriter函数通过调用C语言的BZ2_bzCompressInit函数来初始化stream中的缓存。在writer结构中还包括了另一个buffer,用于输出缓存。

+

下面是Write方法的实现,返回成功压缩数据的大小,主体是一个循环中调用C语言的bz2compress函数实现的。从代码可以看到,Go程序可以访问C语言的bz_stream、char和uint类型,还可以访问bz2compress等函数,甚至可以访问C语言中像BZ_RUN那样的宏定义,全部都是以C.x语法访问。其中C.uint类型和Go语言的uint类型并不相同,即使它们具有相同的大小也是不同的类型。

+
func (w *writer) Write(data []byte) (int, error) {
+	if w.stream == nil {
+		panic("closed")
+	}
+	var total int // uncompressed bytes written
+
+	for len(data) > 0 {
+		inlen, outlen := C.uint(len(data)), C.uint(cap(w.outbuf))
+		C.bz2compress(w.stream, C.BZ_RUN,
+			(*C.char)(unsafe.Pointer(&data[0])), &inlen,
+			(*C.char)(unsafe.Pointer(&w.outbuf)), &outlen)
+		total += int(inlen)
+		data = data[inlen:]
+		if _, err := w.w.Write(w.outbuf[:outlen]); err != nil {
+			return total, err
+		}
+	}
+	return total, nil
+}
+
+

在循环的每次迭代中,向bz2compress传入数据的地址和剩余部分的长度,还有输出缓存w.outbuf的地址和容量。这两个长度信息通过它们的地址传入而不是值传入,因为bz2compress函数可能会根据已经压缩的数据和压缩后数据的大小来更新这两个值。每个块压缩后的数据被写入到底层的io.Writer。

+

Close方法和Write方法有着类似的结构,通过一个循环将剩余的压缩数据刷新到输出缓存。

+
// Close flushes the compressed data and closes the stream.
+// It does not close the underlying io.Writer.
+func (w *writer) Close() error {
+	if w.stream == nil {
+		panic("closed")
+	}
+	defer func() {
+		C.BZ2_bzCompressEnd(w.stream)
+		C.bz2free(w.stream)
+		w.stream = nil
+	}()
+	for {
+		inlen, outlen := C.uint(0), C.uint(cap(w.outbuf))
+		r := C.bz2compress(w.stream, C.BZ_FINISH, nil, &inlen,
+			(*C.char)(unsafe.Pointer(&w.outbuf)), &outlen)
+		if _, err := w.w.Write(w.outbuf[:outlen]); err != nil {
+			return err
+		}
+		if r == C.BZ_STREAM_END {
+			return nil
+		}
+	}
+}
+
+

压缩完成后,Close方法用了defer函数确保函数退出前调用C.BZ2_bzCompressEnd和C.bz2free释放相关的C语言运行时资源。此刻w.stream指针将不再有效,我们将它设置为nil以保证安全,然后在每个方法中增加了nil检测,以防止用户在关闭后依然错误使用相关方法。

+

上面的实现中,不仅仅写是非并发安全的,甚至并发调用Close和Write方法也可能导致程序的的崩溃。修复这个问题是练习13.3的内容。

+

下面的bzipper程序,使用我们自己包实现的bzip2压缩命令。它的行为和许多Unix系统的bzip2命令类似。

+

gopl.io/ch13/bzipper

+
// Bzipper reads input, bzip2-compresses it, and writes it out.
+package main
+
+import (
+	"io"
+	"log"
+	"os"
+	"gopl.io/ch13/bzip"
+)
+
+func main() {
+	w := bzip.NewWriter(os.Stdout)
+	if _, err := io.Copy(w, os.Stdin); err != nil {
+		log.Fatalf("bzipper: %v\n", err)
+	}
+	if err := w.Close(); err != nil {
+		log.Fatalf("bzipper: close: %v\n", err)
+	}
+}
+
+

在上面的场景中,我们使用bzipper压缩了/usr/share/dict/words系统自带的词典,从938,848字节压缩到335,405字节。大约是原始数据大小的三分之一。然后使用系统自带的bunzip2命令进行解压。压缩前后文件的SHA256哈希码是相同了,这也说明了我们的压缩工具是正确的。(如果你的系统没有sha256sum命令,那么请先按照练习4.2实现一个类似的工具)

+
$ go build gopl.io/ch13/bzipper
+$ wc -c < /usr/share/dict/words
+938848
+$ sha256sum < /usr/share/dict/words
+126a4ef38493313edc50b86f90dfdaf7c59ec6c948451eac228f2f3a8ab1a6ed -
+$ ./bzipper < /usr/share/dict/words | wc -c
+335405
+$ ./bzipper < /usr/share/dict/words | bunzip2 | sha256sum
+126a4ef38493313edc50b86f90dfdaf7c59ec6c948451eac228f2f3a8ab1a6ed -
+
+

我们演示了如何将一个C语言库链接到Go语言程序。相反,将Go编译为静态库然后链接到C程序,或者将Go程序编译为动态库然后在C程序中动态加载也都是可行的(译注:在Go1.5中,Windows系统的Go语言实现并不支持生成C语言动态库或静态库的特性。不过好消息是,目前已经有人在尝试解决这个问题,具体请访问 Issue11058 )。这里我们只展示的cgo很小的一些方面,更多的关于内存管理、指针、回调函数、中断信号处理、字符串、errno处理、终结器,以及goroutines和系统线程的关系等,有很多细节可以讨论。特别是如何将Go语言的指针传入C函数的规则也是异常复杂的(译注:简单来说,要传入C函数的Go指针指向的数据本身不能包含指针或其他引用类型;并且C函数在返回后不能继续持有Go指针;并且在C函数返回之前,Go指针是被锁定的,不能导致对应指针数据被移动或栈的调整),部分的原因在13.2节有讨论到,但是在Go1.5中还没有被明确(译注:Go1.6将会明确cgo中的指针使用规则)。如果要进一步阅读,可以从 https://golang.org/cmd/cgo 开始。

+

练习 13.3: 使用sync.Mutex以保证bzip2.writer在多个goroutines中被并发调用是安全的。

+

练习 13.4: 因为C库依赖的限制。 使用os/exec包启动/bin/bzip2命令作为一个子进程,提供一个纯Go的bzip.NewWriter的替代实现(译注:虽然是纯Go实现,但是运行时将依赖/bin/bzip2命令,其他操作系统可能无法运行)。

+

13.5. 几点忠告

+

我们在前一章结尾的时候,我们警告要谨慎使用reflect包。那些警告同样适用于本章的unsafe包。

+

高级语言使得程序员不用再关心真正运行程序的指令细节,同时也不再需要关注许多如内存布局之类的实现细节。因为高级语言这个绝缘的抽象层,我们可以编写安全健壮的,并且可以运行在不同操作系统上的具有高度可移植性的程序。

+

但是unsafe包,它让程序员可以透过这个绝缘的抽象层直接使用一些必要的功能,虽然可能是为了获得更好的性能。但是代价就是牺牲了可移植性和程序安全,因此使用unsafe包是一个危险的行为。我们对何时以及如何使用unsafe包的建议和我们在11.5节提到的Knuth对过早优化的建议类似。大多数Go程序员可能永远不会需要直接使用unsafe包。当然,也永远都会有一些需要使用unsafe包实现会更简单的场景。如果确实认为使用unsafe包是最理想的方式,那么应该尽可能将它限制在较小的范围,这样其它代码就可以忽略unsafe的影响。

+

现在,赶紧将最后两章抛入脑后吧。编写一些实实在在的应用是真理。请远离reflect和unsafe包,除非你确实需要它们。

+

最后,用Go快乐地编程。我们希望你能像我们一样喜欢Go语言。

+

附录

+

英文原版并没有包含附录部分,只有一个索引部分。中文版增加附录部分主要用于收录一些和本书相关的内容,比如英文原版的勘误(有些读者可能会对照中文和英文原阅读)、英文作者和中文译者、译文授权等内容。以后还可能会考虑增加一些习题解答相关的内容。

+

需要特别说明的是,中文版附录并没有包含英文原版的索引信息。因为英文原版的索引信息主要是记录每个索引所在的英文页面位置,而中文版是以GitBook方式组织的html网页形式,将英文页面位置转为章节位置可能会更合理,不过这个会涉及到繁琐的手工操作。如果大家有更好的建议,请告知我们。

+

附录A:原文勘误

+

p.9, ¶2: for "can compared", read "can be compared". (Thanks to Antonio Macías Ojeda, 2015-10-22. Corrected in the second printing.)

+

p.13: As printed, the gopl.io/ch1/lissajous program is deterministic, not random. We've added the statement below to the downloadable program so that it prints a pseudo-random image each time it is run. (Thanks to Randall McPherson, 2015-10-19.)

+

rand.Seed(time.Now().UTC().UnixNano())

+

p.15, ¶2: For "inner loop", read "outer loop". (Thanks to Ralph Corderoy, 2015-11-28. Corrected in the third printing.)

+

p.19, ¶2: For "Go's libraries makes", read "Go's library makes". (Thanks to Victor Farazdagi, 2015-11-30. Corrected in the third printing.)

+

p.40, ¶4: For "value of the underlying type", read "value of an unnamed type with the same underlying type". (Thanks to Carlos Romero Brox, 2015-12-19.)

+

p.40, ¶1: The paragraph should end with a period, not a comma. (Thanks to Victor Farazdagi, 2015-11-30. Corrected in the third printing.)

+

p.43, ¶3: Import declarations are explained in §10.4, not §10.3. (Thanks to Peter Jurgensen, 2015-11-21. Corrected in the third printing.)

+

p.48: f.ReadByte() serves as an example of a reference to f, but *os.File has no such method. For "ReadByte", read "Stat", four times. (Thanks to Peter Olsen, 2016-01-06. Corrected in the third printing.)

+

p.52, ¶2: for "an synonym", read "a synonym", twice. (Corrected in the second printing.)

+

p.52, ¶9: for "The integer arithmetic operators", read "The arithmetic operators". (Thanks to Yoshiki Shibata, 2015-12-20.)

+

p.68: the table of UTF-8 encodings is missing a bit from each first byte. The corrected table is shown below. (Thanks to Akshay Kumar, 2015-11-02. Corrected in the second printing.)

+
0xxxxxxx                             runes 0−127     (ASCII)
+110xxxxx 10xxxxxx                    128−2047        (values <128 unused)
+1110xxxx 10xxxxxx 10xxxxxx           2048−65535      (values <2048 unused)
+11110xxx 10xxxxxx 10xxxxxx 10xxxxxx  65536−0x10ffff  (other values unused)
+
+

p.73, ¶1: For "a exercise", read "an exercise". (Thanks to vrajmohan, 2015-12-28.)

+

p.74: the comment in gopl.io/ch3/printints should say fmt.Sprint, not fmt.Sprintf. (Corrected in the second printing.)

+

p.75, ¶4: for "%u", read "%o". (Thanks to William Hannish, 2015-12-21.)

+

p.76: the comment // "time.Duration 5m0s should have a closing double-quotation mark. (Corrected in the second printing.)

+

p.79, ¶4: "When an untyped constant is assigned to a variable, as in the first statement below, or +appears on the right-hand side of a variable declaration with an explicit type, as in the other three statements, ..." has it backwards: the first statement is a declaration; the other three are assignments. (Thanks to Yoshiki Shibata, 2015-11-09. Corrected in the third printing.)

+

p.112: Exercise 4.11 calls for a "CRUD" (create, read, update, delete) tool for GitHub Issues. Since GitHub does not currently allow Issues to be deleted, for "delete", read "close". (Thanks to Yoshiki Shibata, 2016-01-18.)

+

p.115: The anchor element in gopl.io/ch4/issueshtml's template is missing a closing </a> tag. (Thanks to Taj Khattra, 2016-01-19.)

+

p.132, code display following ¶3: the final comment should read: // compile error: can't assign func(int, int) int to func(int) int (Thanks to Toni Suter, 2015-11-21. Corrected in the third printing.)

+

p.160, ¶4: For Get("item")), read Get("item"). (Thanks to Yoshiki Shibata, 2016-02-01.)

+

p.166, ¶2: for "way", read "a way". (Corrected in the third printing.)

+

p.200, TestEval function: the format string in the final call to t.Errorf should format test.env with %v, not %s. (Thanks to Mitsuteru Sawa, 2015-12-07. Corrected in the third printing.)

+

p.222, Exercise 8.1: The port numbers for London and Tokyo should be swapped in the final command to match the earlier commands. (Thanks to Kiyoshi Kamishima, 2016-01-08.)

+

p.272, ¶3: for "the request body", read "the response body". (Thanks to 曹春晖, 2016-01-19.)

+

p.288, code display following ¶4: In the import declaration, for "database/mysql", read "database/sql". (Thanks to Jose Colon Rodriguez, 2016-01-09.)

+

p.347, Exercise 12.8: for "like json.Marshal", read "like json.Unmarshal". (Thanks to chai2010, 2016-01-01.)

+

p.362: the gopl.io/ch13/bzip program does not comply with the proposed rules for passing pointers between Go and C code because the C function bz2compress temporarily stores a Go pointer (in) into the Go heap (the bz_stream variable). The bz_stream variable should be allocated, and explicitly freed after the call to BZ2_bzCompressEnd, by C functions. (Thanks to Joe Tsai, 2015-11-18. Corrected in the third printing.)

+

附录B:作者/译者

+

英文作者

+ +
+

中文译者

+ + + + + +
中文译者章节
chai2010 <chaishushan@gmail.com>前言/第2 ~ 4章/第10 ~ 13章
Xargin <cao1988228@163.com>第1章/第6章/第8 ~ 9章
CrazySssst第5章
foreversmart <njutree@gmail.com>第7章
+

附录C:译文授权

+

除特别注明外,本站内容均采用知识共享-署名(CC-BY) 3.0协议授权,代码遵循Go项目的BSD协议授权。

+

Creative Commons License

+

附录D:其它语言

+

下表是 The Go Programming Language 其它语言版本:

+ + + + + + + + + +
语言链接时间译者ISBN
中文《Go语言圣经》2016/2/1chai2010, Xargin, CrazySssst, foreversmart?
韩语Acorn Publishing (Korea)2016Seung Lee9788960778320
俄语Williams Publishing (Russia)2016?9785845920515
波兰语Helion (Poland)2016??
日语Maruzen Publishing (Japan)2017Yoshiki Shibata9784621300251
葡萄牙语Novatec Editora (Brazil)2017??
中文简体Pearson Education Asia2017??
中文繁体Gotop Information (Taiwan)2017??
+ + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/searcher.js b/searcher.js new file mode 100644 index 0000000..d2b0aee --- /dev/null +++ b/searcher.js @@ -0,0 +1,483 @@ +"use strict"; +window.search = window.search || {}; +(function search(search) { + // Search functionality + // + // You can use !hasFocus() to prevent keyhandling in your key + // event handlers while the user is typing their search. + + if (!Mark || !elasticlunr) { + return; + } + + //IE 11 Compatibility from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/startsWith + if (!String.prototype.startsWith) { + String.prototype.startsWith = function(search, pos) { + return this.substr(!pos || pos < 0 ? 0 : +pos, search.length) === search; + }; + } + + var search_wrap = document.getElementById('search-wrapper'), + searchbar = document.getElementById('searchbar'), + searchbar_outer = document.getElementById('searchbar-outer'), + searchresults = document.getElementById('searchresults'), + searchresults_outer = document.getElementById('searchresults-outer'), + searchresults_header = document.getElementById('searchresults-header'), + searchicon = document.getElementById('search-toggle'), + content = document.getElementById('content'), + + searchindex = null, + doc_urls = [], + results_options = { + teaser_word_count: 30, + limit_results: 30, + }, + search_options = { + bool: "AND", + expand: true, + fields: { + title: {boost: 1}, + body: {boost: 1}, + breadcrumbs: {boost: 0} + } + }, + mark_exclude = [], + marker = new Mark(content), + current_searchterm = "", + URL_SEARCH_PARAM = 'search', + URL_MARK_PARAM = 'highlight', + teaser_count = 0, + + SEARCH_HOTKEY_KEYCODE = 83, + ESCAPE_KEYCODE = 27, + DOWN_KEYCODE = 40, + UP_KEYCODE = 38, + SELECT_KEYCODE = 13; + + function hasFocus() { + return searchbar === document.activeElement; + } + + function removeChildren(elem) { + while (elem.firstChild) { + elem.removeChild(elem.firstChild); + } + } + + // Helper to parse a url into its building blocks. + function parseURL(url) { + var a = document.createElement('a'); + a.href = url; + return { + source: url, + protocol: a.protocol.replace(':',''), + host: a.hostname, + port: a.port, + params: (function(){ + var ret = {}; + var seg = a.search.replace(/^\?/,'').split('&'); + var len = seg.length, i = 0, s; + for (;i': '>', + '"': '"', + "'": ''' + }; + var repl = function(c) { return MAP[c]; }; + return function(s) { + return s.replace(/[&<>'"]/g, repl); + }; + })(); + + function formatSearchMetric(count, searchterm) { + if (count == 1) { + return count + " search result for '" + searchterm + "':"; + } else if (count == 0) { + return "No search results for '" + searchterm + "'."; + } else { + return count + " search results for '" + searchterm + "':"; + } + } + + function formatSearchResult(result, searchterms) { + var teaser = makeTeaser(escapeHTML(result.doc.body), searchterms); + teaser_count++; + + // The ?URL_MARK_PARAM= parameter belongs inbetween the page and the #heading-anchor + var url = doc_urls[result.ref].split("#"); + if (url.length == 1) { // no anchor found + url.push(""); + } + + // encodeURIComponent escapes all chars that could allow an XSS except + // for '. Due to that we also manually replace ' with its url-encoded + // representation (%27). + var searchterms = encodeURIComponent(searchterms.join(" ")).replace(/\'/g, "%27"); + + return '' + result.doc.breadcrumbs + '' + + '' + + teaser + ''; + } + + function makeTeaser(body, searchterms) { + // The strategy is as follows: + // First, assign a value to each word in the document: + // Words that correspond to search terms (stemmer aware): 40 + // Normal words: 2 + // First word in a sentence: 8 + // Then use a sliding window with a constant number of words and count the + // sum of the values of the words within the window. Then use the window that got the + // maximum sum. If there are multiple maximas, then get the last one. + // Enclose the terms in . + var stemmed_searchterms = searchterms.map(function(w) { + return elasticlunr.stemmer(w.toLowerCase()); + }); + var searchterm_weight = 40; + var weighted = []; // contains elements of ["word", weight, index_in_document] + // split in sentences, then words + var sentences = body.toLowerCase().split('. '); + var index = 0; + var value = 0; + var searchterm_found = false; + for (var sentenceindex in sentences) { + var words = sentences[sentenceindex].split(' '); + value = 8; + for (var wordindex in words) { + var word = words[wordindex]; + if (word.length > 0) { + for (var searchtermindex in stemmed_searchterms) { + if (elasticlunr.stemmer(word).startsWith(stemmed_searchterms[searchtermindex])) { + value = searchterm_weight; + searchterm_found = true; + } + }; + weighted.push([word, value, index]); + value = 2; + } + index += word.length; + index += 1; // ' ' or '.' if last word in sentence + }; + index += 1; // because we split at a two-char boundary '. ' + }; + + if (weighted.length == 0) { + return body; + } + + var window_weight = []; + var window_size = Math.min(weighted.length, results_options.teaser_word_count); + + var cur_sum = 0; + for (var wordindex = 0; wordindex < window_size; wordindex++) { + cur_sum += weighted[wordindex][1]; + }; + window_weight.push(cur_sum); + for (var wordindex = 0; wordindex < weighted.length - window_size; wordindex++) { + cur_sum -= weighted[wordindex][1]; + cur_sum += weighted[wordindex + window_size][1]; + window_weight.push(cur_sum); + }; + + if (searchterm_found) { + var max_sum = 0; + var max_sum_window_index = 0; + // backwards + for (var i = window_weight.length - 1; i >= 0; i--) { + if (window_weight[i] > max_sum) { + max_sum = window_weight[i]; + max_sum_window_index = i; + } + }; + } else { + max_sum_window_index = 0; + } + + // add around searchterms + var teaser_split = []; + var index = weighted[max_sum_window_index][2]; + for (var i = max_sum_window_index; i < max_sum_window_index+window_size; i++) { + var word = weighted[i]; + if (index < word[2]) { + // missing text from index to start of `word` + teaser_split.push(body.substring(index, word[2])); + index = word[2]; + } + if (word[1] == searchterm_weight) { + teaser_split.push("") + } + index = word[2] + word[0].length; + teaser_split.push(body.substring(word[2], index)); + if (word[1] == searchterm_weight) { + teaser_split.push("") + } + }; + + return teaser_split.join(''); + } + + function init(config) { + results_options = config.results_options; + search_options = config.search_options; + searchbar_outer = config.searchbar_outer; + doc_urls = config.doc_urls; + searchindex = elasticlunr.Index.load(config.index); + + // Set up events + searchicon.addEventListener('click', function(e) { searchIconClickHandler(); }, false); + searchbar.addEventListener('keyup', function(e) { searchbarKeyUpHandler(); }, false); + document.addEventListener('keydown', function(e) { globalKeyHandler(e); }, false); + // If the user uses the browser buttons, do the same as if a reload happened + window.onpopstate = function(e) { doSearchOrMarkFromUrl(); }; + // Suppress "submit" events so the page doesn't reload when the user presses Enter + document.addEventListener('submit', function(e) { e.preventDefault(); }, false); + + // If reloaded, do the search or mark again, depending on the current url parameters + doSearchOrMarkFromUrl(); + } + + function unfocusSearchbar() { + // hacky, but just focusing a div only works once + var tmp = document.createElement('input'); + tmp.setAttribute('style', 'position: absolute; opacity: 0;'); + searchicon.appendChild(tmp); + tmp.focus(); + tmp.remove(); + } + + // On reload or browser history backwards/forwards events, parse the url and do search or mark + function doSearchOrMarkFromUrl() { + // Check current URL for search request + var url = parseURL(window.location.href); + if (url.params.hasOwnProperty(URL_SEARCH_PARAM) + && url.params[URL_SEARCH_PARAM] != "") { + showSearch(true); + searchbar.value = decodeURIComponent( + (url.params[URL_SEARCH_PARAM]+'').replace(/\+/g, '%20')); + searchbarKeyUpHandler(); // -> doSearch() + } else { + showSearch(false); + } + + if (url.params.hasOwnProperty(URL_MARK_PARAM)) { + var words = decodeURIComponent(url.params[URL_MARK_PARAM]).split(' '); + marker.mark(words, { + exclude: mark_exclude + }); + + var markers = document.querySelectorAll("mark"); + function hide() { + for (var i = 0; i < markers.length; i++) { + markers[i].classList.add("fade-out"); + window.setTimeout(function(e) { marker.unmark(); }, 300); + } + } + for (var i = 0; i < markers.length; i++) { + markers[i].addEventListener('click', hide); + } + } + } + + // Eventhandler for keyevents on `document` + function globalKeyHandler(e) { + if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey || e.target.type === 'textarea' || e.target.type === 'text') { return; } + + if (e.keyCode === ESCAPE_KEYCODE) { + e.preventDefault(); + searchbar.classList.remove("active"); + setSearchUrlParameters("", + (searchbar.value.trim() !== "") ? "push" : "replace"); + if (hasFocus()) { + unfocusSearchbar(); + } + showSearch(false); + marker.unmark(); + } else if (!hasFocus() && e.keyCode === SEARCH_HOTKEY_KEYCODE) { + e.preventDefault(); + showSearch(true); + window.scrollTo(0, 0); + searchbar.select(); + } else if (hasFocus() && e.keyCode === DOWN_KEYCODE) { + e.preventDefault(); + unfocusSearchbar(); + searchresults.firstElementChild.classList.add("focus"); + } else if (!hasFocus() && (e.keyCode === DOWN_KEYCODE + || e.keyCode === UP_KEYCODE + || e.keyCode === SELECT_KEYCODE)) { + // not `:focus` because browser does annoying scrolling + var focused = searchresults.querySelector("li.focus"); + if (!focused) return; + e.preventDefault(); + if (e.keyCode === DOWN_KEYCODE) { + var next = focused.nextElementSibling; + if (next) { + focused.classList.remove("focus"); + next.classList.add("focus"); + } + } else if (e.keyCode === UP_KEYCODE) { + focused.classList.remove("focus"); + var prev = focused.previousElementSibling; + if (prev) { + prev.classList.add("focus"); + } else { + searchbar.select(); + } + } else { // SELECT_KEYCODE + window.location.assign(focused.querySelector('a')); + } + } + } + + function showSearch(yes) { + if (yes) { + search_wrap.classList.remove('hidden'); + searchicon.setAttribute('aria-expanded', 'true'); + } else { + search_wrap.classList.add('hidden'); + searchicon.setAttribute('aria-expanded', 'false'); + var results = searchresults.children; + for (var i = 0; i < results.length; i++) { + results[i].classList.remove("focus"); + } + } + } + + function showResults(yes) { + if (yes) { + searchresults_outer.classList.remove('hidden'); + } else { + searchresults_outer.classList.add('hidden'); + } + } + + // Eventhandler for search icon + function searchIconClickHandler() { + if (search_wrap.classList.contains('hidden')) { + showSearch(true); + window.scrollTo(0, 0); + searchbar.select(); + } else { + showSearch(false); + } + } + + // Eventhandler for keyevents while the searchbar is focused + function searchbarKeyUpHandler() { + var searchterm = searchbar.value.trim(); + if (searchterm != "") { + searchbar.classList.add("active"); + doSearch(searchterm); + } else { + searchbar.classList.remove("active"); + showResults(false); + removeChildren(searchresults); + } + + setSearchUrlParameters(searchterm, "push_if_new_search_else_replace"); + + // Remove marks + marker.unmark(); + } + + // Update current url with ?URL_SEARCH_PARAM= parameter, remove ?URL_MARK_PARAM and #heading-anchor . + // `action` can be one of "push", "replace", "push_if_new_search_else_replace" + // and replaces or pushes a new browser history item. + // "push_if_new_search_else_replace" pushes if there is no `?URL_SEARCH_PARAM=abc` yet. + function setSearchUrlParameters(searchterm, action) { + var url = parseURL(window.location.href); + var first_search = ! url.params.hasOwnProperty(URL_SEARCH_PARAM); + if (searchterm != "" || action == "push_if_new_search_else_replace") { + url.params[URL_SEARCH_PARAM] = searchterm; + delete url.params[URL_MARK_PARAM]; + url.hash = ""; + } else { + delete url.params[URL_MARK_PARAM]; + delete url.params[URL_SEARCH_PARAM]; + } + // A new search will also add a new history item, so the user can go back + // to the page prior to searching. A updated search term will only replace + // the url. + if (action == "push" || (action == "push_if_new_search_else_replace" && first_search) ) { + history.pushState({}, document.title, renderURL(url)); + } else if (action == "replace" || (action == "push_if_new_search_else_replace" && !first_search) ) { + history.replaceState({}, document.title, renderURL(url)); + } + } + + function doSearch(searchterm) { + + // Don't search the same twice + if (current_searchterm == searchterm) { return; } + else { current_searchterm = searchterm; } + + if (searchindex == null) { return; } + + // Do the actual search + var results = searchindex.search(searchterm, search_options); + var resultcount = Math.min(results.length, results_options.limit_results); + + // Display search metrics + searchresults_header.innerText = formatSearchMetric(resultcount, searchterm); + + // Clear and insert results + var searchterms = searchterm.split(' '); + removeChildren(searchresults); + for(var i = 0; i < resultcount ; i++){ + var resultElem = document.createElement('li'); + resultElem.innerHTML = formatSearchResult(results[i], searchterms); + searchresults.appendChild(resultElem); + } + + // Display results + showResults(true); + } + + fetch(path_to_root + 'searchindex.json') + .then(response => response.json()) + .then(json => init(json)) + .catch(error => { // Try to load searchindex.js if fetch failed + var script = document.createElement('script'); + script.src = path_to_root + 'searchindex.js'; + script.onload = () => init(window.search); + document.head.appendChild(script); + }); + + // Exported functions + search.hasFocus = hasFocus; +})(window.search); diff --git a/searchindex.js b/searchindex.js new file mode 100644 index 0000000..59852cb --- /dev/null +++ b/searchindex.js @@ -0,0 +1 @@ +Object.assign(window.search, {"doc_urls":["index.html#go语言圣经中文版","preface.html#前言","preface.html#go语言起源","preface.html#go语言项目","preface.html#本书的组织","preface.html#更多的信息","preface.html#致谢","ch1/ch1.html#第1章-入门","ch1/ch1-01.html#11-hello-world","ch1/ch1-02.html#12-命令行参数","ch1/ch1-03.html#13-查找重复的行","ch1/ch1-04.html#14-gif动画","ch1/ch1-05.html#15-获取url","ch1/ch1-06.html#16-并发获取多个url","ch1/ch1-07.html#17-web服务","ch1/ch1-08.html#18-本章要点","ch2/ch2.html#第2章-程序结构","ch2/ch2-01.html#21-命名","ch2/ch2-02.html#22-声明","ch2/ch2-03.html#23-变量","ch2/ch2-03.html#231-简短变量声明","ch2/ch2-03.html#232-指针","ch2/ch2-03.html#233-new函数","ch2/ch2-03.html#234-变量的生命周期","ch2/ch2-04.html#24-赋值","ch2/ch2-04.html#241-元组赋值","ch2/ch2-04.html#242-可赋值性","ch2/ch2-05.html#25-类型","ch2/ch2-06.html#26-包和文件","ch2/ch2-06.html#261-导入包","ch2/ch2-06.html#262-包的初始化","ch2/ch2-07.html#27-作用域","ch3/ch3.html#第3章-基础数据类型","ch3/ch3-01.html#31-整型","ch3/ch3-02.html#32-浮点数","ch3/ch3-03.html#33-复数","ch3/ch3-04.html#34-布尔型","ch3/ch3-05.html#35-字符串","ch3/ch3-05.html#351-字符串面值","ch3/ch3-05.html#352-unicode","ch3/ch3-05.html#353-utf-8","ch3/ch3-05.html#354-字符串和byte切片","ch3/ch3-05.html#355-字符串和数字的转换","ch3/ch3-06.html#36-常量","ch3/ch3-06.html#361-iota-常量生成器","ch3/ch3-06.html#362-无类型常量","ch4/ch4.html#第4章-复合数据类型","ch4/ch4-01.html#41-数组","ch4/ch4-02.html#42-slice","ch4/ch4-02.html#421-append函数","ch4/ch4-02.html#422-slice内存技巧","ch4/ch4-03.html#43-map","ch4/ch4-04.html#44-结构体","ch4/ch4-04.html#441-结构体字面值","ch4/ch4-04.html#442-结构体比较","ch4/ch4-04.html#443-结构体嵌入和匿名成员","ch4/ch4-05.html#45-json","ch4/ch4-06.html#46-文本和html模板","ch5/ch5.html#第5章-函数","ch5/ch5-01.html#51-函数声明","ch5/ch5-02.html#52-递归","ch5/ch5-03.html#53-多返回值","ch5/ch5-04.html#54-错误","ch5/ch5-04.html#541-错误处理策略","ch5/ch5-04.html#542-文件结尾错误eof","ch5/ch5-05.html#55-函数值","ch5/ch5-06.html#56-匿名函数","ch5/ch5-06.html#561-警告捕获迭代变量","ch5/ch5-07.html#57-可变参数","ch5/ch5-08.html#58-deferred函数","ch5/ch5-09.html#59-panic异常","ch5/ch5-10.html#510-recover捕获异常","ch6/ch6.html#第6章-方法","ch6/ch6-01.html#61-方法声明","ch6/ch6-02.html#62-基于指针对象的方法","ch6/ch6-02.html#621-nil也是一个合法的接收器类型","ch6/ch6-03.html#63-通过嵌入结构体来扩展类型","ch6/ch6-04.html#64-方法值和方法表达式","ch6/ch6-05.html#65-示例-bit数组","ch6/ch6-06.html#66-封装","ch7/ch7.html#第7章-接口","ch7/ch7-01.html#71-接口约定","ch7/ch7-02.html#72-接口类型","ch7/ch7-03.html#73-实现接口的条件","ch7/ch7-04.html#74-flagvalue接口","ch7/ch7-05.html#75--接口值","ch7/ch7-05.html#751--警告一个包含nil指针的接口不是nil接口","ch7/ch7-06.html#76-sortinterface接口","ch7/ch7-07.html#77-httphandler接口","ch7/ch7-08.html#78-error接口","ch7/ch7-09.html#79-示例-表达式求值","ch7/ch7-10.html#710-类型断言","ch7/ch7-11.html#711-基于类型断言区别错误类型","ch7/ch7-12.html#712-通过类型断言询问行为","ch7/ch7-13.html#713-类型分支","ch7/ch7-14.html#714-示例-基于标记的xml解码","ch7/ch7-15.html#715-一些建议","ch8/ch8.html#第8章-goroutines和channels","ch8/ch8-01.html#81-goroutines","ch8/ch8-02.html#82-示例-并发的clock服务","ch8/ch8-03.html#83-示例-并发的echo服务","ch8/ch8-04.html#84-channels","ch8/ch8-04.html#841-不带缓存的channels","ch8/ch8-04.html#842-串联的channelspipeline","ch8/ch8-04.html#843-单方向的channel","ch8/ch8-04.html#844-带缓存的channels","ch8/ch8-05.html#85-并发的循环","ch8/ch8-06.html#86-示例-并发的web爬虫","ch8/ch8-07.html#87-基于select的多路复用","ch8/ch8-09.html#89-并发的退出","ch8/ch8-10.html#810-示例-聊天服务","ch9/ch9.html#第9章-基于共享变量的并发","ch9/ch9-01.html#91-竞争条件","ch9/ch9-02.html#92-syncmutex互斥锁","ch9/ch9-03.html#93-syncrwmutex读写锁","ch9/ch9-04.html#94-内存同步","ch9/ch9-06.html#96-竞争条件检测","ch9/ch9-07.html#97-示例-并发的非阻塞缓存","ch9/ch9-08.html#98-goroutines和线程","ch9/ch9-08.html#981-动态栈","ch9/ch9-08.html#982-goroutine调度","ch9/ch9-08.html#983-gomaxprocs","ch9/ch9-08.html#984-goroutine没有id号","ch10/ch10.html#第10章-包和工具","ch10/ch10-01.html#101-包简介","ch10/ch10-02.html#102-导入路径","ch10/ch10-03.html#103-包声明","ch10/ch10-04.html#104-导入声明","ch10/ch10-05.html#105-包的匿名导入","ch10/ch10-06.html#106-包和命名","ch10/ch10-07.html#107-工具","ch10/ch10-07.html#1071-工作区结构","ch10/ch10-07.html#1072-下载包","ch10/ch10-07.html#1073-构建包","ch10/ch10-07.html#1074-包文档","ch10/ch10-07.html#1075-内部包","ch10/ch10-07.html#1076-查询包","ch11/ch11.html#第11章-测试","ch11/ch11-01.html#111-go-test","ch11/ch11-02.html#112-测试函数","ch11/ch11-02.html#1121-随机测试","ch11/ch11-02.html#1122-测试一个命令","ch11/ch11-02.html#1123-白盒测试","ch11/ch11-02.html#1124-外部测试包","ch11/ch11-02.html#1125-编写有效的测试","ch11/ch11-02.html#1126-避免脆弱的测试","ch11/ch11-03.html#113-测试覆盖率","ch11/ch11-04.html#114-基准测试","ch11/ch11-05.html#115-剖析","ch11/ch11-06.html#116-示例函数","ch12/ch12.html#第12章-反射","ch12/ch12-01.html#121-为何需要反射","ch12/ch12-02.html#122-reflecttype-和-reflectvalue","ch12/ch12-03.html#123-display一个递归的值打印器","ch12/ch12-04.html#124-示例-编码为s表达式","ch12/ch12-05.html#125-通过reflectvalue修改值","ch12/ch12-06.html#126-示例-解码s表达式","ch12/ch12-08.html#128-显示一个类型的方法集","ch12/ch12-09.html#129-几点忠告","ch13/ch13.html#第13章-底层编程","ch13/ch13-01.html#131-unsafesizeof-alignof-和-offsetof","ch13/ch13-02.html#132-unsafepointer","ch13/ch13-03.html#133-示例-深度相等判断","ch13/ch13-04.html#134-通过cgo调用c代码","ch13/ch13-05.html#135-几点忠告","appendix/appendix.html#附录","appendix/appendix-a-errata.html#附录a-原文勘误","appendix/appendix-b-author.html#附录b作者译者","appendix/appendix-b-author.html#英文作者","appendix/appendix-b-author.html#中文译者","appendix/appendix-c-cpoyright.html#附录c译文授权","appendix/appendix-d-translations.html#附录d其它语言"],"index":{"documentStore":{"docInfo":{"0":{"body":25,"breadcrumbs":2,"title":1},"1":{"body":0,"breadcrumbs":0,"title":0},"10":{"body":223,"breadcrumbs":1,"title":1},"100":{"body":135,"breadcrumbs":4,"title":2},"101":{"body":54,"breadcrumbs":4,"title":2},"102":{"body":57,"breadcrumbs":4,"title":2},"103":{"body":93,"breadcrumbs":4,"title":2},"104":{"body":75,"breadcrumbs":4,"title":2},"105":{"body":66,"breadcrumbs":4,"title":2},"106":{"body":316,"breadcrumbs":2,"title":1},"107":{"body":257,"breadcrumbs":4,"title":2},"108":{"body":154,"breadcrumbs":4,"title":2},"109":{"body":106,"breadcrumbs":2,"title":1},"11":{"body":169,"breadcrumbs":3,"title":2},"110":{"body":181,"breadcrumbs":2,"title":1},"111":{"body":2,"breadcrumbs":1,"title":1},"112":{"body":279,"breadcrumbs":1,"title":1},"113":{"body":175,"breadcrumbs":3,"title":2},"114":{"body":26,"breadcrumbs":3,"title":2},"115":{"body":43,"breadcrumbs":1,"title":1},"116":{"body":13,"breadcrumbs":1,"title":1},"117":{"body":617,"breadcrumbs":1,"title":1},"118":{"body":1,"breadcrumbs":3,"title":2},"119":{"body":4,"breadcrumbs":2,"title":1},"12":{"body":96,"breadcrumbs":3,"title":2},"120":{"body":7,"breadcrumbs":3,"title":2},"121":{"body":21,"breadcrumbs":3,"title":2},"122":{"body":9,"breadcrumbs":3,"title":2},"123":{"body":8,"breadcrumbs":1,"title":1},"124":{"body":2,"breadcrumbs":1,"title":1},"125":{"body":13,"breadcrumbs":1,"title":1},"126":{"body":17,"breadcrumbs":1,"title":1},"127":{"body":27,"breadcrumbs":1,"title":1},"128":{"body":158,"breadcrumbs":1,"title":1},"129":{"body":46,"breadcrumbs":1,"title":1},"13":{"body":120,"breadcrumbs":3,"title":2},"130":{"body":66,"breadcrumbs":1,"title":1},"131":{"body":34,"breadcrumbs":1,"title":1},"132":{"body":61,"breadcrumbs":1,"title":1},"133":{"body":117,"breadcrumbs":1,"title":1},"134":{"body":128,"breadcrumbs":1,"title":1},"135":{"body":6,"breadcrumbs":1,"title":1},"136":{"body":160,"breadcrumbs":1,"title":1},"137":{"body":8,"breadcrumbs":1,"title":1},"138":{"body":9,"breadcrumbs":5,"title":3},"139":{"body":331,"breadcrumbs":1,"title":1},"14":{"body":234,"breadcrumbs":3,"title":2},"140":{"body":74,"breadcrumbs":1,"title":1},"141":{"body":173,"breadcrumbs":1,"title":1},"142":{"body":225,"breadcrumbs":1,"title":1},"143":{"body":43,"breadcrumbs":1,"title":1},"144":{"body":55,"breadcrumbs":1,"title":1},"145":{"body":1,"breadcrumbs":1,"title":1},"146":{"body":173,"breadcrumbs":1,"title":1},"147":{"body":138,"breadcrumbs":1,"title":1},"148":{"body":147,"breadcrumbs":1,"title":1},"149":{"body":22,"breadcrumbs":1,"title":1},"15":{"body":83,"breadcrumbs":1,"title":1},"150":{"body":2,"breadcrumbs":1,"title":1},"151":{"body":47,"breadcrumbs":1,"title":1},"152":{"body":216,"breadcrumbs":4,"title":3},"153":{"body":376,"breadcrumbs":3,"title":2},"154":{"body":314,"breadcrumbs":3,"title":2},"155":{"body":152,"breadcrumbs":3,"title":2},"156":{"body":256,"breadcrumbs":3,"title":2},"157":{"body":69,"breadcrumbs":1,"title":1},"158":{"body":11,"breadcrumbs":1,"title":1},"159":{"body":8,"breadcrumbs":1,"title":1},"16":{"body":2,"breadcrumbs":1,"title":1},"160":{"body":129,"breadcrumbs":7,"title":4},"161":{"body":70,"breadcrumbs":3,"title":2},"162":{"body":217,"breadcrumbs":1,"title":1},"163":{"body":408,"breadcrumbs":3,"title":2},"164":{"body":4,"breadcrumbs":1,"title":1},"165":{"body":1,"breadcrumbs":0,"title":0},"166":{"body":497,"breadcrumbs":0,"title":0},"167":{"body":0,"breadcrumbs":2,"title":1},"168":{"body":85,"breadcrumbs":1,"title":0},"169":{"body":14,"breadcrumbs":1,"title":0},"17":{"body":66,"breadcrumbs":1,"title":1},"170":{"body":3,"breadcrumbs":2,"title":1},"171":{"body":44,"breadcrumbs":2,"title":1},"18":{"body":79,"breadcrumbs":1,"title":1},"19":{"body":35,"breadcrumbs":1,"title":1},"2":{"body":31,"breadcrumbs":1,"title":1},"20":{"body":62,"breadcrumbs":1,"title":1},"21":{"body":144,"breadcrumbs":1,"title":1},"22":{"body":49,"breadcrumbs":2,"title":2},"23":{"body":54,"breadcrumbs":1,"title":1},"24":{"body":25,"breadcrumbs":1,"title":1},"25":{"body":99,"breadcrumbs":1,"title":1},"26":{"body":14,"breadcrumbs":1,"title":1},"27":{"body":169,"breadcrumbs":1,"title":1},"28":{"body":93,"breadcrumbs":1,"title":1},"29":{"body":74,"breadcrumbs":1,"title":1},"3":{"body":9,"breadcrumbs":1,"title":1},"30":{"body":94,"breadcrumbs":1,"title":1},"31":{"body":203,"breadcrumbs":1,"title":1},"32":{"body":2,"breadcrumbs":1,"title":1},"33":{"body":230,"breadcrumbs":1,"title":1},"34":{"body":309,"breadcrumbs":1,"title":1},"35":{"body":149,"breadcrumbs":1,"title":1},"36":{"body":50,"breadcrumbs":1,"title":1},"37":{"body":63,"breadcrumbs":1,"title":1},"38":{"body":25,"breadcrumbs":1,"title":1},"39":{"body":8,"breadcrumbs":2,"title":2},"4":{"body":28,"breadcrumbs":0,"title":0},"40":{"body":160,"breadcrumbs":3,"title":3},"41":{"body":212,"breadcrumbs":2,"title":2},"42":{"body":38,"breadcrumbs":1,"title":1},"43":{"body":68,"breadcrumbs":1,"title":1},"44":{"body":135,"breadcrumbs":2,"title":2},"45":{"body":191,"breadcrumbs":1,"title":1},"46":{"body":2,"breadcrumbs":1,"title":1},"47":{"body":161,"breadcrumbs":1,"title":1},"48":{"body":182,"breadcrumbs":3,"title":2},"49":{"body":244,"breadcrumbs":3,"title":2},"5":{"body":24,"breadcrumbs":0,"title":0},"50":{"body":153,"breadcrumbs":3,"title":2},"51":{"body":345,"breadcrumbs":3,"title":2},"52":{"body":187,"breadcrumbs":1,"title":1},"53":{"body":96,"breadcrumbs":1,"title":1},"54":{"body":32,"breadcrumbs":1,"title":1},"55":{"body":165,"breadcrumbs":1,"title":1},"56":{"body":466,"breadcrumbs":3,"title":2},"57":{"body":195,"breadcrumbs":3,"title":2},"58":{"body":1,"breadcrumbs":1,"title":1},"59":{"body":106,"breadcrumbs":1,"title":1},"6":{"body":52,"breadcrumbs":0,"title":0},"60":{"body":238,"breadcrumbs":1,"title":1},"61":{"body":195,"breadcrumbs":1,"title":1},"62":{"body":24,"breadcrumbs":1,"title":1},"63":{"body":168,"breadcrumbs":1,"title":1},"64":{"body":35,"breadcrumbs":2,"title":2},"65":{"body":180,"breadcrumbs":1,"title":1},"66":{"body":383,"breadcrumbs":1,"title":1},"67":{"body":78,"breadcrumbs":1,"title":1},"68":{"body":93,"breadcrumbs":1,"title":1},"69":{"body":367,"breadcrumbs":3,"title":2},"7":{"body":2,"breadcrumbs":1,"title":1},"70":{"body":140,"breadcrumbs":3,"title":2},"71":{"body":119,"breadcrumbs":3,"title":2},"72":{"body":19,"breadcrumbs":1,"title":1},"73":{"body":128,"breadcrumbs":1,"title":1},"74":{"body":80,"breadcrumbs":1,"title":1},"75":{"body":122,"breadcrumbs":2,"title":2},"76":{"body":160,"breadcrumbs":1,"title":1},"77":{"body":150,"breadcrumbs":1,"title":1},"78":{"body":210,"breadcrumbs":3,"title":2},"79":{"body":121,"breadcrumbs":1,"title":1},"8":{"body":56,"breadcrumbs":5,"title":3},"80":{"body":2,"breadcrumbs":1,"title":1},"81":{"body":178,"breadcrumbs":1,"title":1},"82":{"body":65,"breadcrumbs":1,"title":1},"83":{"body":233,"breadcrumbs":1,"title":1},"84":{"body":212,"breadcrumbs":3,"title":2},"85":{"body":86,"breadcrumbs":1,"title":1},"86":{"body":58,"breadcrumbs":2,"title":2},"87":{"body":472,"breadcrumbs":3,"title":2},"88":{"body":280,"breadcrumbs":3,"title":2},"89":{"body":102,"breadcrumbs":3,"title":2},"9":{"body":148,"breadcrumbs":1,"title":1},"90":{"body":566,"breadcrumbs":1,"title":1},"91":{"body":86,"breadcrumbs":1,"title":1},"92":{"body":115,"breadcrumbs":1,"title":1},"93":{"body":123,"breadcrumbs":1,"title":1},"94":{"body":146,"breadcrumbs":1,"title":1},"95":{"body":268,"breadcrumbs":3,"title":2},"96":{"body":6,"breadcrumbs":1,"title":1},"97":{"body":5,"breadcrumbs":3,"title":2},"98":{"body":63,"breadcrumbs":4,"title":2},"99":{"body":205,"breadcrumbs":4,"title":2}},"docs":{"0":{"body":"Go语言圣经 《The Go Programming Language》 中文版本,仅供学习交流之用。对于希望学习CGO、Go汇编语言等高级用法的同学,我们推荐 《Go语言高级编程》 开源图书。如果希望深入学习Go语言语法树结构,可以参考 《Go语法树入门——开启自制编程语言和编译器之旅》 。如果想从头实现一个玩具Go语言可以参考 《从头实现µGo语言》 。 项目主页: https://github.com/gopl-zh 项目主页(旧): http://github.com/golang-china/gopl-zh 原版官网: http://gopl.io 译者信息: 译者:柴树杉,Github @chai2010 ,Twitter @chaishushan 译者:Xargin, https://github.com/cch123 译者:CrazySssst 译者:foreversmart njutree@gmail.com","breadcrumbs":"Go语言圣经 » Go语言圣经(中文版)","id":"0","title":"Go语言圣经(中文版)"},"1":{"body":"","breadcrumbs":"前言 » 前言","id":"1","title":"前言"},"10":{"body":"对文件做拷贝、打印、搜索、排序、统计或类似事情的程序都有一个差不多的程序结构:一个处理输入的循环,在每个元素上执行计算处理,在处理的同时或最后产生输出。我们会展示一个名为dup的程序的三个版本;灵感来自于Unix的uniq命令,其寻找相邻的重复行。该程序使用的结构和包是个参考范例,可以方便地修改。 dup的第一个版本打印标准输入中多次出现的行,以重复次数开头。该程序将引入if语句,map数据类型以及bufio包。 gopl.io/ch1/dup1 // Dup1 prints the text of each line that appears more than\n// once in the standard input, preceded by its count.\npackage main import ( \"bufio\" \"fmt\" \"os\"\n) func main() { counts := make(map[string]int) input := bufio.NewScanner(os.Stdin) for input.Scan() { counts[input.Text()]++ } // NOTE: ignoring potential errors from input.Err() for line, n := range counts { if n > 1 { fmt.Printf(\"%d\\t%s\\n\", n, line) } }\n} 正如for循环一样,if语句条件两边也不加括号,但是主体部分需要加。if语句的else部分是可选的,在if的条件为false时执行。 map 存储了键/值(key/value)的集合,对集合元素,提供常数时间的存、取或测试操作。键可以是任意类型,只要其值能用==运算符比较,最常见的例子是字符串;值则可以是任意类型。这个例子中的键是字符串,值是整数。内置函数make创建空map,此外,它还有别的作用。4.3节讨论map。 (译注:从功能和实现上说,Go的map类似于Java语言中的HashMap,Python语言中的dict,Lua语言中的table,通常使用hash实现。遗憾的是,对于该词的翻译并不统一,数学界术语为映射,而计算机界众说纷纭莫衷一是。为了防止对读者造成误解,保留不译。) 每次dup读取一行输入,该行被当做键存入map,其对应的值递增。counts[input.Text()]++语句等价下面两句: line := input.Text()\ncounts[line] = counts[line] + 1 map中不含某个键时不用担心,首次读到新行时,等号右边的表达式counts[line]的值将被计算为其类型的零值,对于int即0。 为了打印结果,我们使用了基于range的循环,并在counts这个map上迭代。跟之前类似,每次迭代得到两个结果,键和其在map中对应的值。map的迭代顺序并不确定,从实践来看,该顺序随机,每次运行都会变化。这种设计是有意为之的,因为能防止程序依赖特定遍历顺序,而这是无法保证的。(译注:具体可以参见这里http://stackoverflow.com/questions/11853396/google-go-lang-assignment-order) 继续来看bufio包,它使处理输入和输出方便又高效。Scanner类型是该包最有用的特性之一,它读取输入并将其拆成行或单词;通常是处理行形式的输入最简单的方法。 程序使用短变量声明创建bufio.Scanner类型的变量input。 input := bufio.NewScanner(os.Stdin) 该变量从程序的标准输入中读取内容。每次调用input.Scan(),即读入下一行,并移除行末的换行符;读取的内容可以调用input.Text()得到。Scan函数在读到一行时返回true,不再有输入时返回false。 类似于C或其它语言里的printf函数,fmt.Printf函数对一些表达式产生格式化输出。该函数的首个参数是个格式字符串,指定后续参数被如何格式化。各个参数的格式取决于“转换字符”(conversion character),形式为百分号后跟一个字母。举个例子,%d表示以十进制形式打印一个整型操作数,而%s则表示把字符串型操作数的值展开。 Printf有一大堆这种转换,Go程序员称之为 动词(verb) 。下面的表格虽然远不是完整的规范,但展示了可用的很多特性: %d 十进制整数\n%x, %o, %b 十六进制,八进制,二进制整数。\n%f, %g, %e 浮点数: 3.141593 3.141592653589793 3.141593e+00\n%t 布尔:true或false\n%c 字符(rune) (Unicode码点)\n%s 字符串\n%q 带双引号的字符串\"abc\"或带单引号的字符'c'\n%v 变量的自然形式(natural format)\n%T 变量的类型\n%% 字面上的百分号标志(无操作数) dup1的格式字符串中还含有制表符\\t和换行符\\n。字符串字面上可能含有这些代表不可见字符的 转义字符(escape sequences) 。默认情况下,Printf不会换行。按照惯例,以字母f结尾的格式化函数,如log.Printf和fmt.Errorf,都采用fmt.Printf的格式化准则。而以ln结尾的格式化函数,则遵循Println的方式,以跟%v差不多的方式格式化参数,并在最后添加一个换行符。(译注:后缀f指format,ln指line。) 很多程序要么从标准输入中读取数据,如上面的例子所示,要么从一系列具名文件中读取数据。dup程序的下个版本读取标准输入或是使用os.Open打开各个具名文件,并操作它们。 gopl.io/ch1/dup2 // Dup2 prints the count and text of lines that appear more than once\n// in the input. It reads from stdin or from a list of named files.\npackage main import ( \"bufio\" \"fmt\" \"os\"\n) func main() { counts := make(map[string]int) files := os.Args[1:] if len(files) == 0 { countLines(os.Stdin, counts) } else { for _, arg := range files { f, err := os.Open(arg) if err != nil { fmt.Fprintf(os.Stderr, \"dup2: %v\\n\", err) continue } countLines(f, counts) f.Close() } } for line, n := range counts { if n > 1 { fmt.Printf(\"%d\\t%s\\n\", n, line) } }\n} func countLines(f *os.File, counts map[string]int) { input := bufio.NewScanner(f) for input.Scan() { counts[input.Text()]++ } // NOTE: ignoring potential errors from input.Err()\n} os.Open函数返回两个值。第一个值是被打开的文件(*os.File),其后被Scanner读取。 os.Open返回的第二个值是内置error类型的值。如果err等于内置值nil(译注:相当于其它语言里的NULL),那么文件被成功打开。读取文件,直到文件结束,然后调用Close关闭该文件,并释放占用的所有资源。相反的话,如果err的值不是nil,说明打开文件时出错了。这种情况下,错误值描述了所遇到的问题。我们的错误处理非常简单,只是使用Fprintf与表示任意类型默认格式值的动词%v,向标准错误流打印一条信息,然后dup继续处理下一个文件;continue语句直接跳到for循环的下个迭代开始执行。 为了使示例代码保持合理的大小,本书开始的一些示例有意简化了错误处理,显而易见的是,应该检查os.Open返回的错误值,然而,使用input.Scan读取文件过程中,不大可能出现错误,因此我们忽略了错误处理。我们会在跳过错误检查的地方做说明。5.4节中深入介绍错误处理。 注意countLines函数在其声明前被调用。函数和包级别的变量(package-level entities)可以任意顺序声明,并不影响其被调用。(译注:最好还是遵循一定的规范) map是一个由make函数创建的数据结构的引用。map作为参数传递给某函数时,该函数接收这个引用的一份拷贝(copy,或译为副本),被调用函数对map底层数据结构的任何修改,调用者函数都可以通过持有的map引用看到。在我们的例子中,countLines函数向counts插入的值,也会被main函数看到。(译注:类似于C++里的引用传递,实际上指针是另一个指针了,但内部存的值指向同一块内存) dup的前两个版本以\"流”模式读取输入,并根据需要拆分成多个行。理论上,这些程序可以处理任意数量的输入数据。还有另一个方法,就是一口气把全部输入数据读到内存中,一次分割为多行,然后处理它们。下面这个版本,dup3,就是这么操作的。这个例子引入了ReadFile函数(来自于io/ioutil包),其读取指定文件的全部内容,strings.Split函数把字符串分割成子串的切片。(Split的作用与前文提到的strings.Join相反。) 我们略微简化了dup3。首先,由于ReadFile函数需要文件名作为参数,因此只读指定文件,不读标准输入。其次,由于行计数代码只在一处用到,故将其移回main函数。 gopl.io/ch1/dup3 package main import ( \"fmt\" \"io/ioutil\" \"os\" \"strings\"\n) func main() { counts := make(map[string]int) for _, filename := range os.Args[1:] { data, err := ioutil.ReadFile(filename) if err != nil { fmt.Fprintf(os.Stderr, \"dup3: %v\\n\", err) continue } for _, line := range strings.Split(string(data), \"\\n\") { counts[line]++ } } for line, n := range counts { if n > 1 { fmt.Printf(\"%d\\t%s\\n\", n, line) } }\n} ReadFile函数返回一个字节切片(byte slice),必须把它转换为string,才能用strings.Split分割。我们会在3.5.4节详细讲解字符串和字节切片。 实现上,bufio.Scanner、ioutil.ReadFile和ioutil.WriteFile都使用*os.File的Read和Write方法,但是,大多数程序员很少需要直接调用那些低级(lower-level)函数。高级(higher-level)函数,像bufio和io/ioutil包中所提供的那些,用起来要容易点。 练习 1.4: 修改dup2,出现重复的行时打印文件名称。","breadcrumbs":"入门 » 查找重复的行 » 1.3. 查找重复的行","id":"10","title":"1.3. 查找重复的行"},"100":{"body":"clock服务器每一个连接都会起一个goroutine。在本节中我们会创建一个echo服务器,这个服务在每个连接中会有多个goroutine。大多数echo服务仅仅会返回他们读取到的内容,就像下面这个简单的handleConn函数所做的一样: func handleConn(c net.Conn) { io.Copy(c, c) // NOTE: ignoring errors c.Close()\n} 一个更有意思的echo服务应该模拟一个实际的echo的“回响”,并且一开始要用大写HELLO来表示“声音很大”,之后经过一小段延迟返回一个有所缓和的Hello,然后一个全小写字母的hello表示声音渐渐变小直至消失,像下面这个版本的handleConn(译注:笑看作者脑洞大开): gopl.io/ch8/reverb1 func echo(c net.Conn, shout string, delay time.Duration) { fmt.Fprintln(c, \"\\t\", strings.ToUpper(shout)) time.Sleep(delay) fmt.Fprintln(c, \"\\t\", shout) time.Sleep(delay) fmt.Fprintln(c, \"\\t\", strings.ToLower(shout))\n} func handleConn(c net.Conn) { input := bufio.NewScanner(c) for input.Scan() { echo(c, input.Text(), 1*time.Second) } // NOTE: ignoring potential errors from input.Err() c.Close()\n} 我们需要升级我们的客户端程序,这样它就可以发送终端的输入到服务器,并把服务端的返回输出到终端上,这使我们有了使用并发的另一个好机会: gopl.io/ch8/netcat2 func main() { conn, err := net.Dial(\"tcp\", \"localhost:8000\") if err != nil { log.Fatal(err) } defer conn.Close() go mustCopy(os.Stdout, conn) mustCopy(conn, os.Stdin)\n} 当main goroutine从标准输入流中读取内容并将其发送给服务器时,另一个goroutine会读取并打印服务端的响应。当main goroutine碰到输入终止时,例如,用户在终端中按了Control-D(^D),在windows上是Control-Z,这时程序就会被终止,尽管其它goroutine中还有进行中的任务。(在8.4.1中引入了channels后我们会明白如何让程序等待两边都结束。) 下面这个会话中,客户端的输入是左对齐的,服务端的响应会用缩进来区别显示。 客户端会向服务器“喊三次话”: $ go build gopl.io/ch8/reverb1\n$ ./reverb1 &\n$ go build gopl.io/ch8/netcat2\n$ ./netcat2\nHello? HELLO? Hello? hello?\nIs there anybody there? IS THERE ANYBODY THERE?\nYooo-hooo! Is there anybody there? is there anybody there? YOOO-HOOO! Yooo-hooo! yooo-hooo!\n^D\n$ killall reverb1 注意客户端的第三次shout在前一个shout处理完成之前一直没有被处理,这貌似看起来不是特别“现实”。真实世界里的回响应该是会由三次shout的回声组合而成的。为了模拟真实世界的回响,我们需要更多的goroutine来做这件事情。这样我们就再一次地需要go这个关键词了,这次我们用它来调用echo: gopl.io/ch8/reverb2 func handleConn(c net.Conn) { input := bufio.NewScanner(c) for input.Scan() { go echo(c, input.Text(), 1*time.Second) } // NOTE: ignoring potential errors from input.Err() c.Close()\n} go后跟的函数的参数会在go语句自身执行时被求值;因此input.Text()会在main goroutine中被求值。 现在回响是并发并且会按时间来覆盖掉其它响应了: $ go build gopl.io/ch8/reverb2\n$ ./reverb2 &\n$ ./netcat2\nIs there anybody there? IS THERE ANYBODY THERE?\nYooo-hooo! Is there anybody there? YOOO-HOOO! is there anybody there? Yooo-hooo! yooo-hooo!\n^D\n$ killall reverb2 让服务使用并发不只是处理多个客户端的请求,甚至在处理单个连接时也可能会用到,就像我们上面的两个go关键词的用法。然而在我们使用go关键词的同时,需要慎重地考虑net.Conn中的方法在并发地调用时是否安全,事实上对于大多数类型来说也确实不安全。我们会在下一章中详细地探讨并发安全性。","breadcrumbs":"Goroutines和Channels » 示例: 并发的Echo服务 » 8.3. 示例: 并发的Echo服务","id":"100","title":"8.3. 示例: 并发的Echo服务"},"101":{"body":"如果说goroutine是Go语言程序的并发体的话,那么channels则是它们之间的通信机制。一个channel是一个通信机制,它可以让一个goroutine通过它给另一个goroutine发送值信息。每个channel都有一个特殊的类型,也就是channels可发送数据的类型。一个可以发送int类型数据的channel一般写为chan int。 使用内置的make函数,我们可以创建一个channel: ch := make(chan int) // ch has type 'chan int' 和map类似,channel也对应一个make创建的底层数据结构的引用。当我们复制一个channel或用于函数参数传递时,我们只是拷贝了一个channel引用,因此调用者和被调用者将引用同一个channel对象。和其它的引用类型一样,channel的零值也是nil。 两个相同类型的channel可以使用==运算符比较。如果两个channel引用的是相同的对象,那么比较的结果为真。一个channel也可以和nil进行比较。 一个channel有发送和接受两个主要操作,都是通信行为。一个发送语句将一个值从一个goroutine通过channel发送到另一个执行接收操作的goroutine。发送和接收两个操作都使用<-运算符。在发送语句中,<-运算符分割channel和要发送的值。在接收语句中,<-运算符写在channel对象之前。一个不使用接收结果的接收操作也是合法的。 ch <- x // a send statement\nx = <-ch // a receive expression in an assignment statement\n<-ch // a receive statement; result is discarded Channel还支持close操作,用于关闭channel,随后对基于该channel的任何发送操作都将导致panic异常。对一个已经被close过的channel进行接收操作依然可以接受到之前已经成功发送的数据;如果channel中已经没有数据的话将产生一个零值的数据。 使用内置的close函数就可以关闭一个channel: close(ch) 以最简单方式调用make函数创建的是一个无缓存的channel,但是我们也可以指定第二个整型参数,对应channel的容量。如果channel的容量大于零,那么该channel就是带缓存的channel。 ch = make(chan int) // unbuffered channel\nch = make(chan int, 0) // unbuffered channel\nch = make(chan int, 3) // buffered channel with capacity 3 我们将先讨论无缓存的channel,然后在8.4.4节讨论带缓存的channel。","breadcrumbs":"Goroutines和Channels » Channels » 8.4. Channels","id":"101","title":"8.4. Channels"},"102":{"body":"一个基于无缓存Channels的发送操作将导致发送者goroutine阻塞,直到另一个goroutine在相同的Channels上执行接收操作,当发送的值通过Channels成功传输之后,两个goroutine可以继续执行后面的语句。反之,如果接收操作先发生,那么接收者goroutine也将阻塞,直到有另一个goroutine在相同的Channels上执行发送操作。 基于无缓存Channels的发送和接收操作将导致两个goroutine做一次同步操作。因为这个原因,无缓存Channels有时候也被称为同步Channels。当通过一个无缓存Channels发送数据时,接收者收到数据发生在再次唤醒发送者goroutine之前(译注: happens before ,这是Go语言并发内存模型的一个关键术语!)。 在讨论并发编程时,当我们说x事件在y事件之前发生( happens before ),我们并不是说x事件在时间上比y时间更早;我们要表达的意思是要保证在此之前的事件都已经完成了,例如在此之前的更新某些变量的操作已经完成,你可以放心依赖这些已完成的事件了。 当我们说x事件既不是在y事件之前发生也不是在y事件之后发生,我们就说x事件和y事件是并发的。这并不是意味着x事件和y事件就一定是同时发生的,我们只是不能确定这两个事件发生的先后顺序。在下一章中我们将看到,当两个goroutine并发访问了相同的变量时,我们有必要保证某些事件的执行顺序,以避免出现某些并发问题。 在8.3节的客户端程序,它在主goroutine中(译注:就是执行main函数的goroutine)将标准输入复制到server,因此当客户端程序关闭标准输入时,后台goroutine可能依然在工作。我们需要让主goroutine等待后台goroutine完成工作后再退出,我们使用了一个channel来同步两个goroutine: gopl.io/ch8/netcat3 func main() { conn, err := net.Dial(\"tcp\", \"localhost:8000\") if err != nil { log.Fatal(err) } done := make(chan struct{}) go func() { io.Copy(os.Stdout, conn) // NOTE: ignoring errors log.Println(\"done\") done <- struct{}{} // signal the main goroutine }() mustCopy(conn, os.Stdin) conn.Close() <-done // wait for background goroutine to finish\n} 当用户关闭了标准输入,主goroutine中的mustCopy函数调用将返回,然后调用conn.Close()关闭读和写方向的网络连接。关闭网络连接中的写方向的连接将导致server程序收到一个文件(end-of-file)结束的信号。关闭网络连接中读方向的连接将导致后台goroutine的io.Copy函数调用返回一个“read from closed connection”(“从关闭的连接读”)类似的错误,因此我们临时移除了错误日志语句;在练习8.3将会提供一个更好的解决方案。(需要注意的是go语句调用了一个函数字面量,这是Go语言中启动goroutine常用的形式。) 在后台goroutine返回之前,它先打印一个日志信息,然后向done对应的channel发送一个值。主goroutine在退出前先等待从done对应的channel接收一个值。因此,总是可以在程序退出前正确输出“done”消息。 基于channels发送消息有两个重要方面。首先每个消息都有一个值,但是有时候通讯的事实和发生的时刻也同样重要。当我们更希望强调通讯发生的时刻时,我们将它称为 消息事件 。有些消息事件并不携带额外的信息,它仅仅是用作两个goroutine之间的同步,这时候我们可以用struct{}空结构体作为channels元素的类型,虽然也可以使用bool或int类型实现同样的功能,done <- 1语句也比done <- struct{}{}更短。 练习 8.3: 在netcat3例子中,conn虽然是一个interface类型的值,但是其底层真实类型是*net.TCPConn,代表一个TCP连接。一个TCP连接有读和写两个部分,可以使用CloseRead和CloseWrite方法分别关闭它们。修改netcat3的主goroutine代码,只关闭网络连接中写的部分,这样的话后台goroutine可以在标准输入被关闭后继续打印从reverb1服务器传回的数据。(要在reverb2服务器也完成同样的功能是比较困难的;参考 练习 8.4 。)","breadcrumbs":"Goroutines和Channels » Channels » 8.4.1. 不带缓存的Channels","id":"102","title":"8.4.1. 不带缓存的Channels"},"103":{"body":"Channels也可以用于将多个goroutine连接在一起,一个Channel的输出作为下一个Channel的输入。这种串联的Channels就是所谓的管道(pipeline)。下面的程序用两个channels将三个goroutine串联起来,如图8.1所示。 第一个goroutine是一个计数器,用于生成0、1、2、……形式的整数序列,然后通过channel将该整数序列发送给第二个goroutine;第二个goroutine是一个求平方的程序,对收到的每个整数求平方,然后将平方后的结果通过第二个channel发送给第三个goroutine;第三个goroutine是一个打印程序,打印收到的每个整数。为了保持例子清晰,我们有意选择了非常简单的函数,当然三个goroutine的计算很简单,在现实中确实没有必要为如此简单的运算构建三个goroutine。 gopl.io/ch8/pipeline1 func main() { naturals := make(chan int) squares := make(chan int) // Counter go func() { for x := 0; ; x++ { naturals <- x } }() // Squarer go func() { for { x := <-naturals squares <- x * x } }() // Printer (in main goroutine) for { fmt.Println(<-squares) }\n} 如您所料,上面的程序将生成0、1、4、9、……形式的无穷数列。像这样的串联Channels的管道(Pipelines)可以用在需要长时间运行的服务中,每个长时间运行的goroutine可能会包含一个死循环,在不同goroutine的死循环内部使用串联的Channels来通信。但是,如果我们希望通过Channels只发送有限的数列该如何处理呢? 如果发送者知道,没有更多的值需要发送到channel的话,那么让接收者也能及时知道没有多余的值可接收将是有用的,因为接收者可以停止不必要的接收等待。这可以通过内置的close函数来关闭channel实现: close(naturals) 当一个channel被关闭后,再向该channel发送数据将导致panic异常。当一个被关闭的channel中已经发送的数据都被成功接收后,后续的接收操作将不再阻塞,它们会立即返回一个零值。关闭上面例子中的naturals变量对应的channel并不能终止循环,它依然会收到一个永无休止的零值序列,然后将它们发送给打印者goroutine。 没有办法直接测试一个channel是否被关闭,但是接收操作有一个变体形式:它多接收一个结果,多接收的第二个结果是一个布尔值ok,ture表示成功从channels接收到值,false表示channels已经被关闭并且里面没有值可接收。使用这个特性,我们可以修改squarer函数中的循环代码,当naturals对应的channel被关闭并没有值可接收时跳出循环,并且也关闭squares对应的channel. // Squarer\ngo func() { for { x, ok := <-naturals if !ok { break // channel was closed and drained } squares <- x * x } close(squares)\n}() 因为上面的语法是笨拙的,而且这种处理模式很常见,因此Go语言的range循环可直接在channels上面迭代。使用range循环是上面处理模式的简洁语法,它依次从channel接收数据,当channel被关闭并且没有值可接收时跳出循环。 在下面的改进中,我们的计数器goroutine只生成100个含数字的序列,然后关闭naturals对应的channel,这将导致计算平方数的squarer对应的goroutine可以正常终止循环并关闭squares对应的channel。(在一个更复杂的程序中,可以通过defer语句关闭对应的channel。)最后,主goroutine也可以正常终止循环并退出程序。 gopl.io/ch8/pipeline2 func main() { naturals := make(chan int) squares := make(chan int) // Counter go func() { for x := 0; x < 100; x++ { naturals <- x } close(naturals) }() // Squarer go func() { for x := range naturals { squares <- x * x } close(squares) }() // Printer (in main goroutine) for x := range squares { fmt.Println(x) }\n} 其实你并不需要关闭每一个channel。只有当需要告诉接收者goroutine,所有的数据已经全部发送时才需要关闭channel。不管一个channel是否被关闭,当它没有被引用时将会被Go语言的垃圾自动回收器回收。(不要将关闭一个打开文件的操作和关闭一个channel操作混淆。对于每个打开的文件,都需要在不使用的时候调用对应的Close方法来关闭文件。) 试图重复关闭一个channel将导致panic异常,试图关闭一个nil值的channel也将导致panic异常。关闭一个channels还会触发一个广播机制,我们将在8.9节讨论。","breadcrumbs":"Goroutines和Channels » Channels » 8.4.2. 串联的Channels(Pipeline)","id":"103","title":"8.4.2. 串联的Channels(Pipeline)"},"104":{"body":"随着程序的增长,人们习惯于将大的函数拆分为小的函数。我们前面的例子中使用了三个goroutine,然后用两个channels来连接它们,它们都是main函数的局部变量。将三个goroutine拆分为以下三个函数是自然的想法: func counter(out chan int)\nfunc squarer(out, in chan int)\nfunc printer(in chan int) 其中计算平方的squarer函数在两个串联Channels的中间,因此拥有两个channel类型的参数,一个用于输入一个用于输出。两个channel都拥有相同的类型,但是它们的使用方式相反:一个只用于接收,另一个只用于发送。参数的名字in和out已经明确表示了这个意图,但是并无法保证squarer函数向一个in参数对应的channel发送数据或者从一个out参数对应的channel接收数据。 这种场景是典型的。当一个channel作为一个函数参数时,它一般总是被专门用于只发送或者只接收。 为了表明这种意图并防止被滥用,Go语言的类型系统提供了单方向的channel类型,分别用于只发送或只接收的channel。类型chan<- int表示一个只发送int的channel,只能发送不能接收。相反,类型<-chan int表示一个只接收int的channel,只能接收不能发送。(箭头<-和关键字chan的相对位置表明了channel的方向。)这种限制将在编译期检测。 因为关闭操作只用于断言不再向channel发送新的数据,所以只有在发送者所在的goroutine才会调用close函数,因此对一个只接收的channel调用close将是一个编译错误。 这是改进的版本,这一次参数使用了单方向channel类型: gopl.io/ch8/pipeline3 func counter(out chan<- int) { for x := 0; x < 100; x++ { out <- x } close(out)\n} func squarer(out chan<- int, in <-chan int) { for v := range in { out <- v * v } close(out)\n} func printer(in <-chan int) { for v := range in { fmt.Println(v) }\n} func main() { naturals := make(chan int) squares := make(chan int) go counter(naturals) go squarer(squares, naturals) printer(squares)\n} 调用counter(naturals)时,naturals的类型将隐式地从chan int转换成chan<- int。调用printer(squares)也会导致相似的隐式转换,这一次是转换为<-chan int类型只接收型的channel。任何双向channel向单向channel变量的赋值操作都将导致该隐式转换。这里并没有反向转换的语法:也就是不能将一个类似chan<- int类型的单向型的channel转换为chan int类型的双向型的channel。","breadcrumbs":"Goroutines和Channels » Channels » 8.4.3. 单方向的Channel","id":"104","title":"8.4.3. 单方向的Channel"},"105":{"body":"带缓存的Channel内部持有一个元素队列。队列的最大容量是在调用make函数创建channel时通过第二个参数指定的。下面的语句创建了一个可以持有三个字符串元素的带缓存Channel。图8.2是ch变量对应的channel的图形表示形式。 ch = make(chan string, 3) 向缓存Channel的发送操作就是向内部缓存队列的尾部插入元素,接收操作则是从队列的头部删除元素。如果内部缓存队列是满的,那么发送操作将阻塞直到因另一个goroutine执行接收操作而释放了新的队列空间。相反,如果channel是空的,接收操作将阻塞直到有另一个goroutine执行发送操作而向队列插入元素。 我们可以在无阻塞的情况下连续向新创建的channel发送三个值: ch <- \"A\"\nch <- \"B\"\nch <- \"C\" 此刻,channel的内部缓存队列将是满的(图8.3),如果有第四个发送操作将发生阻塞。 如果我们接收一个值, fmt.Println(<-ch) // \"A\" 那么channel的缓存队列将不是满的也不是空的(图8.4),因此对该channel执行的发送或接收操作都不会发生阻塞。通过这种方式,channel的缓存队列解耦了接收和发送的goroutine。 在某些特殊情况下,程序可能需要知道channel内部缓存的容量,可以用内置的cap函数获取: fmt.Println(cap(ch)) // \"3\" 同样,对于内置的len函数,如果传入的是channel,那么将返回channel内部缓存队列中有效元素的个数。因为在并发程序中该信息会随着接收操作而失效,但是它对某些故障诊断和性能优化会有帮助。 fmt.Println(len(ch)) // \"2\" 在继续执行两次接收操作后channel内部的缓存队列将又成为空的,如果有第四个接收操作将发生阻塞: fmt.Println(<-ch) // \"B\"\nfmt.Println(<-ch) // \"C\" 在这个例子中,发送和接收操作都发生在同一个goroutine中,但是在真实的程序中它们一般由不同的goroutine执行。Go语言新手有时候会将一个带缓存的channel当作同一个goroutine中的队列使用,虽然语法看似简单,但实际上这是一个错误。Channel和goroutine的调度器机制是紧密相连的,如果没有其他goroutine从channel接收,发送者——或许是整个程序——将会面临永远阻塞的风险。如果你只是需要一个简单的队列,使用slice就可以了。 下面的例子展示了一个使用了带缓存channel的应用。它并发地向三个镜像站点发出请求,三个镜像站点分散在不同的地理位置。它们分别将收到的响应发送到带缓存channel,最后接收者只接收第一个收到的响应,也就是最快的那个响应。因此mirroredQuery函数可能在另外两个响应慢的镜像站点响应之前就返回了结果。(顺便说一下,多个goroutines并发地向同一个channel发送数据,或从同一个channel接收数据都是常见的用法。) func mirroredQuery() string { responses := make(chan string, 3) go func() { responses <- request(\"asia.gopl.io\") }() go func() { responses <- request(\"europe.gopl.io\") }() go func() { responses <- request(\"americas.gopl.io\") }() return <-responses // return the quickest response\n} func request(hostname string) (response string) { /* ... */ } 如果我们使用了无缓存的channel,那么两个慢的goroutines将会因为没有人接收而被永远卡住。这种情况,称为goroutines泄漏,这将是一个BUG。和垃圾变量不同,泄漏的goroutines并不会被自动回收,因此确保每个不再需要的goroutine能正常退出是重要的。 关于无缓存或带缓存channels之间的选择,或者是带缓存channels的容量大小的选择,都可能影响程序的正确性。无缓存channel更强地保证了每个发送操作与相应的同步接收操作;但是对于带缓存channel,这些操作是解耦的。同样,即使我们知道将要发送到一个channel的信息的数量上限,创建一个对应容量大小的带缓存channel也是不现实的,因为这要求在执行任何接收操作之前缓存所有已经发送的值。如果未能分配足够的缓存将导致程序死锁。 Channel的缓存也可能影响程序的性能。想象一家蛋糕店有三个厨师,一个烘焙,一个上糖衣,还有一个将每个蛋糕传递到它下一个厨师的生产线。在狭小的厨房空间环境,每个厨师在完成蛋糕后必须等待下一个厨师已经准备好接受它;这类似于在一个无缓存的channel上进行沟通。 如果在每个厨师之间有一个放置一个蛋糕的额外空间,那么每个厨师就可以将一个完成的蛋糕临时放在那里而马上进入下一个蛋糕的制作中;这类似于将channel的缓存队列的容量设置为1。只要每个厨师的平均工作效率相近,那么其中大部分的传输工作将是迅速的,个体之间细小的效率差异将在交接过程中弥补。如果厨师之间有更大的额外空间——也是就更大容量的缓存队列——将可以在不停止生产线的前提下消除更大的效率波动,例如一个厨师可以短暂地休息,然后再加快赶上进度而不影响其他人。 另一方面,如果生产线的前期阶段一直快于后续阶段,那么它们之间的缓存在大部分时间都将是满的。相反,如果后续阶段比前期阶段更快,那么它们之间的缓存在大部分时间都将是空的。对于这类场景,额外的缓存并没有带来任何好处。 生产线的隐喻对于理解channels和goroutines的工作机制是很有帮助的。例如,如果第二阶段是需要精心制作的复杂操作,一个厨师可能无法跟上第一个厨师的进度,或者是无法满足第三阶段厨师的需求。要解决这个问题,我们可以再雇佣另一个厨师来帮助完成第二阶段的工作,他执行相同的任务但是独立工作。这类似于基于相同的channels创建另一个独立的goroutine。 我们没有太多的空间展示全部细节,但是gopl.io/ch8/cake包模拟了这个蛋糕店,可以通过不同的参数调整。它还对上面提到的几种场景提供对应的基准测试(§11.4) 。","breadcrumbs":"Goroutines和Channels » Channels » 8.4.4. 带缓存的Channels","id":"105","title":"8.4.4. 带缓存的Channels"},"106":{"body":"本节中,我们会探索一些用来在并行时循环迭代的常见并发模型。我们会探究从全尺寸图片生成一些缩略图的问题。gopl.io/ch8/thumbnail包提供了ImageFile函数来帮我们拉伸图片。我们不会说明这个函数的实现,只需要从gopl.io下载它。 gopl.io/ch8/thumbnail package thumbnail // ImageFile reads an image from infile and writes\n// a thumbnail-size version of it in the same directory.\n// It returns the generated file name, e.g., \"foo.thumb.jpg\".\nfunc ImageFile(infile string) (string, error) 下面的程序会循环迭代一些图片文件名,并为每一张图片生成一个缩略图: gopl.io/ch8/thumbnail // makeThumbnails makes thumbnails of the specified files.\nfunc makeThumbnails(filenames []string) { for _, f := range filenames { if _, err := thumbnail.ImageFile(f); err != nil { log.Println(err) } }\n} 显然我们处理文件的顺序无关紧要,因为每一个图片的拉伸操作和其它图片的处理操作都是彼此独立的。像这种子问题都是完全彼此独立的问题被叫做易并行问题(译注:embarrassingly parallel,直译的话更像是尴尬并行)。易并行问题是最容易被实现成并行的一类问题(废话),并且最能够享受到并发带来的好处,能够随着并行的规模线性地扩展。 下面让我们并行地执行这些操作,从而将文件IO的延迟隐藏掉,并用上多核cpu的计算能力来拉伸图像。我们的第一个并发程序只是使用了一个go关键字。这里我们先忽略掉错误,之后再进行处理。 // NOTE: incorrect!\nfunc makeThumbnails2(filenames []string) { for _, f := range filenames { go thumbnail.ImageFile(f) // NOTE: ignoring errors }\n} 这个版本运行的实在有点太快,实际上,由于它比最早的版本使用的时间要短得多,即使当文件名的slice中只包含有一个元素。这就有点奇怪了,如果程序没有并发执行的话,那为什么一个并发的版本还是要快呢?答案其实是makeThumbnails在它还没有完成工作之前就已经返回了。它启动了所有的goroutine,每一个文件名对应一个,但没有等待它们一直到执行完毕。 没有什么直接的办法能够等待goroutine完成,但是我们可以改变goroutine里的代码让其能够将完成情况报告给外部的goroutine知晓,使用的方式是向一个共享的channel中发送事件。因为我们已经确切地知道有len(filenames)个内部goroutine,所以外部的goroutine只需要在返回之前对这些事件计数。 // makeThumbnails3 makes thumbnails of the specified files in parallel.\nfunc makeThumbnails3(filenames []string) { ch := make(chan struct{}) for _, f := range filenames { go func(f string) { thumbnail.ImageFile(f) // NOTE: ignoring errors ch <- struct{}{} }(f) } // Wait for goroutines to complete. for range filenames { <-ch }\n} 注意我们将f的值作为一个显式的变量传给了函数,而不是在循环的闭包中声明: for _, f := range filenames { go func() { thumbnail.ImageFile(f) // NOTE: incorrect! // ... }()\n} 回忆一下之前在5.6.1节中,匿名函数中的循环变量快照问题。上面这个单独的变量f是被所有的匿名函数值所共享,且会被连续的循环迭代所更新的。当新的goroutine开始执行字面函数时,for循环可能已经更新了f并且开始了另一轮的迭代或者(更有可能的)已经结束了整个循环,所以当这些goroutine开始读取f的值时,它们所看到的值已经是slice的最后一个元素了。显式地添加这个参数,我们能够确保使用的f是当go语句执行时的“当前”那个f。 如果我们想要从每一个worker goroutine往主goroutine中返回值时该怎么办呢?当我们调用thumbnail.ImageFile创建文件失败的时候,它会返回一个错误。下一个版本的makeThumbnails会返回其在做拉伸操作时接收到的第一个错误: // makeThumbnails4 makes thumbnails for the specified files in parallel.\n// It returns an error if any step failed.\nfunc makeThumbnails4(filenames []string) error { errors := make(chan error) for _, f := range filenames { go func(f string) { _, err := thumbnail.ImageFile(f) errors <- err }(f) } for range filenames { if err := <-errors; err != nil { return err // NOTE: incorrect: goroutine leak! } } return nil\n} 这个程序有一个微妙的bug。当它遇到第一个非nil的error时会直接将error返回到调用方,使得没有一个goroutine去排空errors channel。这样剩下的worker goroutine在向这个channel中发送值时,都会永远地阻塞下去,并且永远都不会退出。这种情况叫做goroutine泄露(§8.4.4),可能会导致整个程序卡住或者跑出out of memory的错误。 最简单的解决办法就是用一个具有合适大小的buffered channel,这样这些worker goroutine向channel中发送错误时就不会被阻塞。(一个可选的解决办法是创建一个另外的goroutine,当main goroutine返回第一个错误的同时去排空channel。) 下一个版本的makeThumbnails使用了一个buffered channel来返回生成的图片文件的名字,附带生成时的错误。 // makeThumbnails5 makes thumbnails for the specified files in parallel.\n// It returns the generated file names in an arbitrary order,\n// or an error if any step failed.\nfunc makeThumbnails5(filenames []string) (thumbfiles []string, err error) { type item struct { thumbfile string err error } ch := make(chan item, len(filenames)) for _, f := range filenames { go func(f string) { var it item it.thumbfile, it.err = thumbnail.ImageFile(f) ch <- it }(f) } for range filenames { it := <-ch if it.err != nil { return nil, it.err } thumbfiles = append(thumbfiles, it.thumbfile) } return thumbfiles, nil\n} 我们最后一个版本的makeThumbnails返回了新文件们的大小总计数(bytes)。和前面的版本都不一样的一点是我们在这个版本里没有把文件名放在slice里,而是通过一个string的channel传过来,所以我们无法对循环的次数进行预测。 为了知道最后一个goroutine什么时候结束(最后一个结束并不一定是最后一个开始),我们需要一个递增的计数器,在每一个goroutine启动时加一,在goroutine退出时减一。这需要一种特殊的计数器,这个计数器需要在多个goroutine操作时做到安全并且提供在其减为零之前一直等待的一种方法。这种计数类型被称为sync.WaitGroup,下面的代码就用到了这种方法: // makeThumbnails6 makes thumbnails for each file received from the channel.\n// It returns the number of bytes occupied by the files it creates.\nfunc makeThumbnails6(filenames <-chan string) int64 { sizes := make(chan int64) var wg sync.WaitGroup // number of working goroutines for f := range filenames { wg.Add(1) // worker go func(f string) { defer wg.Done() thumb, err := thumbnail.ImageFile(f) if err != nil { log.Println(err) return } info, _ := os.Stat(thumb) // OK to ignore error sizes <- info.Size() }(f) } // closer go func() { wg.Wait() close(sizes) }() var total int64 for size := range sizes { total += size } return total\n} 注意Add和Done方法的不对称。Add是为计数器加一,必须在worker goroutine开始之前调用,而不是在goroutine中;否则的话我们没办法确定Add是在\"closer\" goroutine调用Wait之前被调用。并且Add还有一个参数,但Done却没有任何参数;其实它和Add(-1)是等价的。我们使用defer来确保计数器即使是在出错的情况下依然能够正确地被减掉。上面的程序代码结构是当我们使用并发循环,但又不知道迭代次数时很通常而且很地道的写法。 sizes channel携带了每一个文件的大小到main goroutine,在main goroutine中使用了range loop来计算总和。观察一下我们是怎样创建一个closer goroutine,并让其在所有worker goroutine们结束之后再关闭sizes channel的。两步操作:wait和close,必须是基于sizes的循环的并发。考虑一下另一种方案:如果等待操作被放在了main goroutine中,在循环之前,这样的话就永远都不会结束了,如果在循环之后,那么又变成了不可达的部分,因为没有任何东西去关闭这个channel,这个循环就永远都不会终止。 图8.5 表明了makethumbnails6函数中事件的序列。纵列表示goroutine。窄线段代表sleep,粗线段代表活动。斜线箭头代表用来同步两个goroutine的事件。时间向下流动。注意main goroutine是如何大部分的时间被唤醒执行其range循环,等待worker发送值或者closer来关闭channel的。 练习 8.4: 修改reverb2服务器,在每一个连接中使用sync.WaitGroup来计数活跃的echo goroutine。当计数减为零时,关闭TCP连接的写入,像练习8.3中一样。验证一下你的修改版netcat3客户端会一直等待所有的并发“喊叫”完成,即使是在标准输入流已经关闭的情况下。 练习 8.5: 使用一个已有的CPU绑定的顺序程序,比如在3.3节中我们写的Mandelbrot程序或者3.2节中的3-D surface计算程序,并将他们的主循环改为并发形式,使用channel来进行通信。在多核计算机上这个程序得到了多少速度上的改进?使用多少个goroutine是最合适的呢?","breadcrumbs":"Goroutines和Channels » 并发的循环 » 8.5. 并发的循环","id":"106","title":"8.5. 并发的循环"},"107":{"body":"在5.6节中,我们做了一个简单的web爬虫,用bfs(广度优先)算法来抓取整个网站。在本节中,我们会让这个爬虫并行化,这样每一个彼此独立的抓取命令可以并行进行IO,最大化利用网络资源。crawl函数和gopl.io/ch5/findlinks3中的是一样的。 gopl.io/ch8/crawl1 func crawl(url string) []string { fmt.Println(url) list, err := links.Extract(url) if err != nil { log.Print(err) } return list\n} 主函数和5.6节中的breadthFirst(广度优先)类似。像之前一样,一个worklist是一个记录了需要处理的元素的队列,每一个元素都是一个需要抓取的URL列表,不过这一次我们用channel代替slice来做这个队列。每一个对crawl的调用都会在他们自己的goroutine中进行并且会把他们抓到的链接发送回worklist。 func main() { worklist := make(chan []string) // Start with the command-line arguments. go func() { worklist <- os.Args[1:] }() // Crawl the web concurrently. seen := make(map[string]bool) for list := range worklist { for _, link := range list { if !seen[link] { seen[link] = true go func(link string) { worklist <- crawl(link) }(link) } } }\n} 注意这里的crawl所在的goroutine会将link作为一个显式的参数传入,来避免“循环变量快照”的问题(在5.6.1中有讲解)。另外注意这里将命令行参数传入worklist也是在一个另外的goroutine中进行的,这是为了避免channel两端的main goroutine与crawler goroutine都尝试向对方发送内容,却没有一端接收内容时发生死锁。当然,这里我们也可以用buffered channel来解决问题,这里不再赘述。 现在爬虫可以高并发地运行起来,并且可以产生一大坨的URL了,不过还是会有俩问题。一个问题是在运行一段时间后可能会出现在log的错误信息里的: $ go build gopl.io/ch8/crawl1\n$ ./crawl1 http://gopl.io/\nhttp://gopl.io/\nhttps://golang.org/help/\nhttps://golang.org/doc/\nhttps://golang.org/blog/\n...\n2015/07/15 18:22:12 Get ...: dial tcp: lookup blog.golang.org: no such host\n2015/07/15 18:22:12 Get ...: dial tcp 23.21.222.120:443: socket: too many open files\n... 最初的错误信息是一个让人莫名的DNS查找失败,即使这个域名是完全可靠的。而随后的错误信息揭示了原因:这个程序一次性创建了太多网络连接,超过了每一个进程的打开文件数限制,既而导致了在调用net.Dial像DNS查找失败这样的问题。 这个程序实在是太他妈并行了。无穷无尽地并行化并不是什么好事情,因为不管怎么说,你的系统总是会有一些个限制因素,比如CPU核心数会限制你的计算负载,比如你的硬盘转轴和磁头数限制了你的本地磁盘IO操作频率,比如你的网络带宽限制了你的下载速度上限,或者是你的一个web服务的服务容量上限等等。为了解决这个问题,我们可以限制并发程序所使用的资源来使之适应自己的运行环境。对于我们的例子来说,最简单的方法就是限制对links.Extract在同一时间最多不会有超过n次调用,这里的n一般小于文件描述符的上限值,比如20。这和一个夜店里限制客人数目是一个道理,只有当有客人离开时,才会允许新的客人进入店内。 我们可以用一个有容量限制的buffered channel来控制并发,这类似于操作系统里的计数信号量概念。从概念上讲,channel里的n个空槽代表n个可以处理内容的token(通行证),从channel里接收一个值会释放其中的一个token,并且生成一个新的空槽位。这样保证了在没有接收介入时最多有n个发送操作。(这里可能我们拿channel里填充的槽来做token更直观一些,不过还是这样吧。)由于channel里的元素类型并不重要,我们用一个零值的struct{}来作为其元素。 让我们重写crawl函数,将对links.Extract的调用操作用获取、释放token的操作包裹起来,来确保同一时间对其只有20个调用。信号量数量和其能操作的IO资源数量应保持接近。 gopl.io/ch8/crawl2 // tokens is a counting semaphore used to\n// enforce a limit of 20 concurrent requests.\nvar tokens = make(chan struct{}, 20) func crawl(url string) []string { fmt.Println(url) tokens <- struct{}{} // acquire a token list, err := links.Extract(url) <-tokens // release the token if err != nil { log.Print(err) } return list\n} 第二个问题是这个程序永远都不会终止,即使它已经爬到了所有初始链接衍生出的链接。(当然,除非你慎重地选择了合适的初始化URL或者已经实现了练习8.6中的深度限制,你应该还没有意识到这个问题。)为了使这个程序能够终止,我们需要在worklist为空或者没有crawl的goroutine在运行时退出主循环。 func main() { worklist := make(chan []string) var n int // number of pending sends to worklist // Start with the command-line arguments. n++ go func() { worklist <- os.Args[1:] }() // Crawl the web concurrently. seen := make(map[string]bool) for ; n > 0; n-- { list := <-worklist for _, link := range list { if !seen[link] { seen[link] = true n++ go func(link string) { worklist <- crawl(link) }(link) } } }\n} 这个版本中,计数器n对worklist的发送操作数量进行了限制。每一次我们发现有元素需要被发送到worklist时,我们都会对n进行++操作,在向worklist中发送初始的命令行参数之前,我们也进行过一次++操作。这里的操作++是在每启动一个crawler的goroutine之前。主循环会在n减为0时终止,这时候说明没活可干了。 现在这个并发爬虫会比5.6节中的深度优先搜索版快上20倍,而且不会出什么错,并且在其完成任务时也会正确地终止。 下面的程序是避免过度并发的另一种思路。这个版本使用了原来的crawl函数,但没有使用计数信号量,取而代之用了20个常驻的crawler goroutine,这样来保证最多20个HTTP请求在并发。 func main() { worklist := make(chan []string) // lists of URLs, may have duplicates unseenLinks := make(chan string) // de-duplicated URLs // Add command-line arguments to worklist. go func() { worklist <- os.Args[1:] }() // Create 20 crawler goroutines to fetch each unseen link. for i := 0; i < 20; i++ { go func() { for link := range unseenLinks { foundLinks := crawl(link) go func() { worklist <- foundLinks }() } }() } // The main goroutine de-duplicates worklist items // and sends the unseen ones to the crawlers. seen := make(map[string]bool) for list := range worklist { for _, link := range list { if !seen[link] { seen[link] = true unseenLinks <- link } } }\n} 所有的爬虫goroutine现在都是被同一个channel - unseenLinks喂饱的了。主goroutine负责拆分它从worklist里拿到的元素,然后把没有抓过的经由unseenLinks channel发送给一个爬虫的goroutine。 seen这个map被限定在main goroutine中;也就是说这个map只能在main goroutine中进行访问。类似于其它的信息隐藏方式,这样的约束可以让我们从一定程度上保证程序的正确性。例如,内部变量不能够在函数外部被访问到;变量(§2.3.4)在没有发生变量逃逸(译注:局部变量被全局变量引用地址导致变量被分配在堆上)的情况下是无法在函数外部访问的;一个对象的封装字段无法被该对象的方法以外的方法访问到。在所有的情况下,信息隐藏都可以帮助我们约束我们的程序,使其不发生意料之外的情况。 crawl函数爬到的链接在一个专有的goroutine中被发送到worklist中来避免死锁。为了节省篇幅,这个例子的终止问题我们先不进行详细阐述了。 练习 8.6: 为并发爬虫增加深度限制。也就是说,如果用户设置了depth=3,那么只有从首页跳转三次以内能够跳到的页面才能被抓取到。 练习 8.7: 完成一个并发程序来创建一个线上网站的本地镜像,把该站点的所有可达的页面都抓取到本地硬盘。为了省事,我们这里可以只取出现在该域下的所有页面(比如golang.org开头,译注:外链的应该就不算了。)当然了,出现在页面里的链接你也需要进行一些处理,使其能够在你的镜像站点上进行跳转,而不是指向原始的链接。 译注: 拓展阅读 Handling 1 Million Requests per Minute with Go 。","breadcrumbs":"Goroutines和Channels » 示例: 并发的Web爬虫 » 8.6. 示例: 并发的Web爬虫","id":"107","title":"8.6. 示例: 并发的Web爬虫"},"108":{"body":"下面的程序会进行火箭发射的倒计时。time.Tick函数返回一个channel,程序会周期性地像一个节拍器一样向这个channel发送事件。每一个事件的值是一个时间戳,不过更有意思的是其传送方式。 gopl.io/ch8/countdown1 func main() { fmt.Println(\"Commencing countdown.\") tick := time.Tick(1 * time.Second) for countdown := 10; countdown > 0; countdown-- { fmt.Println(countdown) <-tick } launch()\n} 现在我们让这个程序支持在倒计时中,用户按下return键时直接中断发射流程。首先,我们启动一个goroutine,这个goroutine会尝试从标准输入中读入一个单独的byte并且,如果成功了,会向名为abort的channel发送一个值。 gopl.io/ch8/countdown2 abort := make(chan struct{})\ngo func() { os.Stdin.Read(make([]byte, 1)) // read a single byte abort <- struct{}{}\n}() 现在每一次计数循环的迭代都需要等待两个channel中的其中一个返回事件了:当一切正常时的ticker channel(就像NASA jorgon的\"nominal\",译注:这梗估计我们是不懂了)或者异常时返回的abort事件。我们无法做到从每一个channel中接收信息,如果我们这么做的话,如果第一个channel中没有事件发过来那么程序就会立刻被阻塞,这样我们就无法收到第二个channel中发过来的事件。这时候我们需要多路复用(multiplex)这些操作了,为了能够多路复用,我们使用了select语句。 select {\ncase <-ch1: // ...\ncase x := <-ch2: // ...use x...\ncase ch3 <- y: // ...\ndefault: // ...\n} 上面是select语句的一般形式。和switch语句稍微有点相似,也会有几个case和最后的default选择分支。每一个case代表一个通信操作(在某个channel上进行发送或者接收),并且会包含一些语句组成的一个语句块。一个接收表达式可能只包含接收表达式自身(译注:不把接收到的值赋值给变量什么的),就像上面的第一个case,或者包含在一个简短的变量声明中,像第二个case里一样;第二种形式让你能够引用接收到的值。 select会等待case中有能够执行的case时去执行。当条件满足时,select才会去通信并执行case之后的语句;这时候其它通信是不会执行的。一个没有任何case的select语句写作select{},会永远地等待下去。 让我们回到我们的火箭发射程序。time.After函数会立即返回一个channel,并起一个新的goroutine在经过特定的时间后向该channel发送一个独立的值。下面的select语句会一直等待直到两个事件中的一个到达,无论是abort事件或者一个10秒经过的事件。如果10秒经过了还没有abort事件进入,那么火箭就会发射。 func main() { // ...create abort channel... fmt.Println(\"Commencing countdown. Press return to abort.\") select { case <-time.After(10 * time.Second): // Do nothing. case <-abort: fmt.Println(\"Launch aborted!\") return } launch()\n} 下面这个例子更微妙。ch这个channel的buffer大小是1,所以会交替的为空或为满,所以只有一个case可以进行下去,无论i是奇数或者偶数,它都会打印0 2 4 6 8。 ch := make(chan int, 1)\nfor i := 0; i < 10; i++ { select { case x := <-ch: fmt.Println(x) // \"0\" \"2\" \"4\" \"6\" \"8\" case ch <- i: }\n} 如果多个case同时就绪时,select会随机地选择一个执行,这样来保证每一个channel都有平等的被select的机会。增加前一个例子的buffer大小会使其输出变得不确定,因为当buffer既不为满也不为空时,select语句的执行情况就像是抛硬币的行为一样是随机的。 下面让我们的发射程序打印倒计时。这里的select语句会使每次循环迭代等待一秒来执行退出操作。 gopl.io/ch8/countdown3 func main() { // ...create abort channel... fmt.Println(\"Commencing countdown. Press return to abort.\") tick := time.Tick(1 * time.Second) for countdown := 10; countdown > 0; countdown-- { fmt.Println(countdown) select { case <-tick: // Do nothing. case <-abort: fmt.Println(\"Launch aborted!\") return } } launch()\n} time.Tick函数表现得好像它创建了一个在循环中调用time.Sleep的goroutine,每次被唤醒时发送一个事件。当countdown函数返回时,它会停止从tick中接收事件,但是ticker这个goroutine还依然存活,继续徒劳地尝试向channel中发送值,然而这时候已经没有其它的goroutine会从该channel中接收值了——这被称为goroutine泄露(§8.4.4)。 Tick函数挺方便,但是只有当程序整个生命周期都需要这个时间时我们使用它才比较合适。否则的话,我们应该使用下面的这种模式: ticker := time.NewTicker(1 * time.Second)\n<-ticker.C // receive from the ticker's channel\nticker.Stop() // cause the ticker's goroutine to terminate 有时候我们希望能够从channel中发送或者接收值,并避免因为发送或者接收导致的阻塞,尤其是当channel没有准备好写或者读时。select语句就可以实现这样的功能。select会有一个default来设置当其它的操作都不能够马上被处理时程序需要执行哪些逻辑。 下面的select语句会在abort channel中有值时,从其中接收值;无值时什么都不做。这是一个非阻塞的接收操作;反复地做这样的操作叫做“轮询channel”。 select {\ncase <-abort: fmt.Printf(\"Launch aborted!\\n\") return\ndefault: // do nothing\n} channel的零值是nil。也许会让你觉得比较奇怪,nil的channel有时候也是有一些用处的。因为对一个nil的channel发送和接收操作会永远阻塞,在select语句中操作nil的channel永远都不会被select到。 这使得我们可以用nil来激活或者禁用case,来达成处理其它输入或输出事件时超时和取消的逻辑。我们会在下一节中看到一个例子。 练习 8.8: 使用select来改造8.3节中的echo服务器,为其增加超时,这样服务器可以在客户端10秒中没有任何喊话时自动断开连接。","breadcrumbs":"Goroutines和Channels » 基于select的多路复用 » 8.7. 基于select的多路复用","id":"108","title":"8.7. 基于select的多路复用"},"109":{"body":"有时候我们需要通知goroutine停止它正在干的事情,比如一个正在执行计算的web服务,然而它的客户端已经断开了和服务端的连接。 Go语言并没有提供在一个goroutine中终止另一个goroutine的方法,由于这样会导致goroutine之间的共享变量落在未定义的状态上。在8.7节中的rocket launch程序中,我们往名字叫abort的channel里发送了一个简单的值,在countdown的goroutine中会把这个值理解为自己的退出信号。但是如果我们想要退出两个或者任意多个goroutine怎么办呢? 一种可能的手段是向abort的channel里发送和goroutine数目一样多的事件来退出它们。如果这些goroutine中已经有一些自己退出了,那么会导致我们的channel里的事件数比goroutine还多,这样导致我们的发送直接被阻塞。另一方面,如果这些goroutine又生成了其它的goroutine,我们的channel里的数目又太少了,所以有些goroutine可能会无法接收到退出消息。一般情况下我们是很难知道在某一个时刻具体有多少个goroutine在运行着的。另外,当一个goroutine从abort channel中接收到一个值的时候,他会消费掉这个值,这样其它的goroutine就没法看到这条信息。为了能够达到我们退出goroutine的目的,我们需要更靠谱的策略,来通过一个channel把消息广播出去,这样goroutine们能够看到这条事件消息,并且在事件完成之后,可以知道这件事已经发生过了。 回忆一下我们关闭了一个channel并且被消费掉了所有已发送的值,操作channel之后的代码可以立即被执行,并且会产生零值。我们可以将这个机制扩展一下,来作为我们的广播机制:不要向channel发送值,而是用关闭一个channel来进行广播。 只要一些小修改,我们就可以把退出逻辑加入到前一节的du程序。首先,我们创建一个退出的channel,不需要向这个channel发送任何值,但其所在的闭包内要写明程序需要退出。我们同时还定义了一个工具函数,cancelled,这个函数在被调用的时候会轮询退出状态。 gopl.io/ch8/du4 var done = make(chan struct{}) func cancelled() bool { select { case <-done: return true default: return false }\n} 下面我们创建一个从标准输入流中读取内容的goroutine,这是一个比较典型的连接到终端的程序。每当有输入被读到(比如用户按了回车键),这个goroutine就会把取消消息通过关闭done的channel广播出去。 // Cancel traversal when input is detected.\ngo func() { os.Stdin.Read(make([]byte, 1)) // read a single byte close(done)\n}() 现在我们需要使我们的goroutine来对取消进行响应。在main goroutine中,我们添加了select的第三个case语句,尝试从done channel中接收内容。如果这个case被满足的话,在select到的时候即会返回,但在结束之前我们需要把fileSizes channel中的内容“排”空,在channel被关闭之前,舍弃掉所有值。这样可以保证对walkDir的调用不要被向fileSizes发送信息阻塞住,可以正确地完成。 for { select { case <-done: // Drain fileSizes to allow existing goroutines to finish. for range fileSizes { // Do nothing. } return case size, ok := <-fileSizes: // ... }\n} walkDir这个goroutine一启动就会轮询取消状态,如果取消状态被设置的话会直接返回,并且不做额外的事情。这样我们将所有在取消事件之后创建的goroutine改变为无操作。 func walkDir(dir string, n *sync.WaitGroup, fileSizes chan<- int64) { defer n.Done() if cancelled() { return } for _, entry := range dirents(dir) { // ... }\n} 在walkDir函数的循环中我们对取消状态进行轮询可以带来明显的益处,可以避免在取消事件发生时还去创建goroutine。取消本身是有一些代价的;想要快速的响应需要对程序逻辑进行侵入式的修改。确保在取消发生之后不要有代价太大的操作可能会需要修改你代码里的很多地方,但是在一些重要的地方去检查取消事件也确实能带来很大的好处。 对这个程序的一个简单的性能分析可以揭示瓶颈在dirents函数中获取一个信号量。下面的select可以让这种操作可以被取消,并且可以将取消时的延迟从几百毫秒降低到几十毫秒。 func dirents(dir string) []os.FileInfo { select { case sema <- struct{}{}: // acquire token case <-done: return nil // cancelled } defer func() { <-sema }() // release token // ...read directory...\n} 现在当取消发生时,所有后台的goroutine都会迅速停止并且主函数会返回。当然,当主函数返回时,一个程序会退出,而我们又无法在主函数退出的时候确认其已经释放了所有的资源(译注:因为程序都退出了,你的代码都没法执行了)。这里有一个方便的窍门我们可以一用:取代掉直接从主函数返回,我们调用一个panic,然后runtime会把每一个goroutine的栈dump下来。如果main goroutine是唯一一个剩下的goroutine的话,他会清理掉自己的一切资源。但是如果还有其它的goroutine没有退出,他们可能没办法被正确地取消掉,也有可能被取消但是取消操作会很花时间;所以这里的一个调研还是很有必要的。我们用panic来获取到足够的信息来验证我们上面的判断,看看最终到底是什么样的情况。 练习 8.10: HTTP请求可能会因http.Request结构体中Cancel channel的关闭而取消。修改8.6节中的web crawler来支持取消http请求。(提示:http.Get并没有提供方便地定制一个请求的方法。你可以用http.NewRequest来取而代之,设置它的Cancel字段,然后用http.DefaultClient.Do(req)来进行这个http请求。) 练习 8.11: 紧接着8.4.4中的mirroredQuery流程,实现一个并发请求url的fetch的变种。当第一个请求返回时,直接取消其它的请求。","breadcrumbs":"Goroutines和Channels » 并发的退出 » 8.9. 并发的退出","id":"109","title":"8.9. 并发的退出"},"11":{"body":"下面的程序会演示Go语言标准库里的image这个package的用法,我们会用这个包来生成一系列的bit-mapped图,然后将这些图片编码为一个GIF动画。我们生成的图形名字叫利萨如图形(Lissajous figures),这种效果是在1960年代的老电影里出现的一种视觉特效。它们是协振子在两个纬度上振动所产生的曲线,比如两个sin正弦波分别在x轴和y轴输入会产生的曲线。图1.1是这样的一个例子: 译注:要看这个程序的结果,需要将标准输出重定向到一个GIF图像文件(使用 ./lissajous > output.gif 命令)。下面是GIF图像动画效果: 这段代码里我们用了一些新的结构,包括const声明,struct结构体类型,复合声明。和我们举的其它的例子不太一样,这一个例子包含了浮点数运算。这些概念我们只在这里简单地说明一下,之后的章节会更详细地讲解。 gopl.io/ch1/lissajous // Lissajous generates GIF animations of random Lissajous figures.\npackage main import ( \"image\" \"image/color\" \"image/gif\" \"io\" \"math\" \"math/rand\" \"os\" \"time\"\n) var palette = []color.Color{color.White, color.Black} const ( whiteIndex = 0 // first color in palette blackIndex = 1 // next color in palette\n) func main() { // The sequence of images is deterministic unless we seed // the pseudo-random number generator using the current time. // Thanks to Randall McPherson for pointing out the omission. rand.Seed(time.Now().UTC().UnixNano()) lissajous(os.Stdout)\n} func lissajous(out io.Writer) { const ( cycles = 5 // number of complete x oscillator revolutions res = 0.001 // angular resolution size = 100 // image canvas covers [-size..+size] nframes = 64 // number of animation frames delay = 8 // delay between frames in 10ms units ) freq := rand.Float64() * 3.0 // relative frequency of y oscillator anim := gif.GIF{LoopCount: nframes} phase := 0.0 // phase difference for i := 0; i < nframes; i++ { rect := image.Rect(0, 0, 2*size+1, 2*size+1) img := image.NewPaletted(rect, palette) for t := 0.0; t < cycles*2*math.Pi; t += res { x := math.Sin(t) y := math.Sin(t*freq + phase) img.SetColorIndex(size+int(x*size+0.5), size+int(y*size+0.5), blackIndex) } phase += 0.1 anim.Delay = append(anim.Delay, delay) anim.Image = append(anim.Image, img) } gif.EncodeAll(out, &anim) // NOTE: ignoring encoding errors\n} 当我们import了一个包路径包含有多个单词的package时,比如image/color(image和color两个单词),通常我们只需要用最后那个单词表示这个包就可以。所以当我们写color.White时,这个变量指向的是image/color包里的变量,同理gif.GIF是属于image/gif包里的变量。 这个程序里的常量声明给出了一系列的常量值,常量是指在程序编译后运行时始终都不会变化的值,比如圈数、帧数、延迟值。常量声明和变量声明一般都会出现在包级别,所以这些常量在整个包中都是可以共享的,或者你也可以把常量声明定义在函数体内部,那么这种常量就只能在函数体内用。目前常量声明的值必须是一个数字值、字符串或者一个固定的boolean值。 []color.Color{...}和gif.GIF{...}这两个表达式就是我们说的复合声明(4.2和4.4.1节有说明)。这是实例化Go语言里的复合类型的一种写法。这里的前者生成的是一个slice切片,后者生成的是一个struct结构体。 gif.GIF是一个struct类型(参考4.4节)。struct是一组值或者叫字段的集合,不同的类型集合在一个struct可以让我们以一个统一的单元进行处理。anim是一个gif.GIF类型的struct变量。这种写法会生成一个struct变量,并且其内部变量LoopCount字段会被设置为nframes;而其它的字段会被设置为各自类型默认的零值。struct内部的变量可以以一个点(.)来进行访问,就像在最后两个赋值语句中显式地更新了anim这个struct的Delay和Image字段。 lissajous函数内部有两层嵌套的for循环。外层循环会循环64次,每一次都会生成一个单独的动画帧。它生成了一个包含两种颜色的201*201大小的图片,白色和黑色。所有像素点都会被默认设置为其零值(也就是调色板palette里的第0个值),这里我们设置的是白色。每次外层循环都会生成一张新图片,并将一些像素设置为黑色。其结果会append到之前结果之后。这里我们用到了append(参考4.2.1)内置函数,将结果append到anim中的帧列表末尾,并设置一个默认的80ms的延迟值。循环结束后所有的延迟值被编码进了GIF图片中,并将结果写入到输出流。out这个变量是io.Writer类型,这个类型支持把输出结果写到很多目标,很快我们就可以看到例子。 内层循环设置两个偏振值。x轴偏振使用sin函数。y轴偏振也是正弦波,但其相对x轴的偏振是一个0-3的随机值,初始偏振值是一个零值,随着动画的每一帧逐渐增加。循环会一直跑到x轴完成五次完整的循环。每一步它都会调用SetColorIndex来为(x,y)点来染黑色。 main函数调用lissajous函数,用它来向标准输出流打印信息,所以下面这个命令会像图1.1中产生一个GIF动画。 $ go build gopl.io/ch1/lissajous\n$ ./lissajous >out.gif 练习 1.5: 修改前面的Lissajous程序里的调色板,由黑色改为绿色。我们可以用color.RGBA{0xRR, 0xGG, 0xBB, 0xff}来得到#RRGGBB这个色值,三个十六进制的字符串分别代表红、绿、蓝像素。 练习 1.6: 修改Lissajous程序,修改其调色板来生成更丰富的颜色,然后修改SetColorIndex的第三个参数,看看显示结果吧。","breadcrumbs":"入门 » GIF动画 » 1.4. GIF动画","id":"11","title":"1.4. GIF动画"},"110":{"body":"我们用一个聊天服务器来终结本章节的内容,这个程序可以让一些用户通过服务器向其它所有用户广播文本消息。这个程序中有四种goroutine。main和broadcaster各自是一个goroutine实例,每一个客户端的连接都会有一个handleConn和clientWriter的goroutine。broadcaster是select用法的不错的样例,因为它需要处理三种不同类型的消息。 下面演示的main goroutine的工作,是listen和accept(译注:网络编程里的概念)从客户端过来的连接。对每一个连接,程序都会建立一个新的handleConn的goroutine,就像我们在本章开头的并发的echo服务器里所做的那样。 gopl.io/ch8/chat func main() { listener, err := net.Listen(\"tcp\", \"localhost:8000\") if err != nil { log.Fatal(err) } go broadcaster() for { conn, err := listener.Accept() if err != nil { log.Print(err) continue } go handleConn(conn) }\n} 然后是broadcaster的goroutine。他的内部变量clients会记录当前建立连接的客户端集合。其记录的内容是每一个客户端的消息发出channel的“资格”信息。 type client chan<- string // an outgoing message channel var ( entering = make(chan client) leaving = make(chan client) messages = make(chan string) // all incoming client messages\n) func broadcaster() { clients := make(map[client]bool) // all connected clients for { select { case msg := <-messages: // Broadcast incoming message to all // clients' outgoing message channels. for cli := range clients { cli <- msg } case cli := <-entering: clients[cli] = true case cli := <-leaving: delete(clients, cli) close(cli) } }\n} broadcaster监听来自全局的entering和leaving的channel来获知客户端的到来和离开事件。当其接收到其中的一个事件时,会更新clients集合,当该事件是离开行为时,它会关闭客户端的消息发送channel。broadcaster也会监听全局的消息channel,所有的客户端都会向这个channel中发送消息。当broadcaster接收到什么消息时,就会将其广播至所有连接到服务端的客户端。 现在让我们看看每一个客户端的goroutine。handleConn函数会为它的客户端创建一个消息发送channel并通过entering channel来通知客户端的到来。然后它会读取客户端发来的每一行文本,并通过全局的消息channel来将这些文本发送出去,并为每条消息带上发送者的前缀来标明消息身份。当客户端发送完毕后,handleConn会通过leaving这个channel来通知客户端的离开并关闭连接。 func handleConn(conn net.Conn) { ch := make(chan string) // outgoing client messages go clientWriter(conn, ch) who := conn.RemoteAddr().String() ch <- \"You are \" + who messages <- who + \" has arrived\" entering <- ch input := bufio.NewScanner(conn) for input.Scan() { messages <- who + \": \" + input.Text() } // NOTE: ignoring potential errors from input.Err() leaving <- ch messages <- who + \" has left\" conn.Close()\n} func clientWriter(conn net.Conn, ch <-chan string) { for msg := range ch { fmt.Fprintln(conn, msg) // NOTE: ignoring network errors }\n} 另外,handleConn为每一个客户端创建了一个clientWriter的goroutine,用来接收向客户端发送消息的channel中的广播消息,并将它们写入到客户端的网络连接。客户端的读取循环会在broadcaster接收到leaving通知并关闭了channel后终止。 下面演示的是当服务器有两个活动的客户端连接,并且在两个窗口中运行的情况,使用netcat来聊天: $ go build gopl.io/ch8/chat\n$ go build gopl.io/ch8/netcat3\n$ ./chat &\n$ ./netcat3\nYou are 127.0.0.1:64208 $ ./netcat3\n127.0.0.1:64211 has arrived You are 127.0.0.1:64211\nHi!\n127.0.0.1:64208: Hi! 127.0.0.1:64208: Hi! Hi yourself.\n127.0.0.1:64211: Hi yourself. 127.0.0.1:64211: Hi yourself.\n^C 127.0.0.1:64208 has left\n$ ./netcat3\nYou are 127.0.0.1:64216 127.0.0.1:64216 has arrived Welcome.\n127.0.0.1:64211: Welcome. 127.0.0.1:64211: Welcome. ^C\n127.0.0.1:64211 has left” 当与n个客户端保持聊天session时,这个程序会有2n+2个并发的goroutine,然而这个程序却并不需要显式的锁(§9.2)。clients这个map被限制在了一个独立的goroutine中,broadcaster,所以它不能被并发地访问。多个goroutine共享的变量只有这些channel和net.Conn的实例,两个东西都是并发安全的。我们会在下一章中更多地讲解约束,并发安全以及goroutine中共享变量的含义。 练习 8.12: 使broadcaster能够在每个新的客户端到来时通知它当前的客户端集合。这需要你在clients集合中,以及entering和leaving的channel中记录客户端的名字。 练习 8.13: 使聊天服务器能够断开空闲的客户端连接,比如最近五分钟之后没有发送任何消息的那些客户端。提示:可以在其它goroutine中调用conn.Close()来解除Read调用,就像input.Scanner()所做的那样。 练习 8.14: 修改聊天服务器的网络协议,这样每一个客户端就可以在entering时提供他们的名字。将消息前缀由之前的网络地址改为这个名字。 练习 8.15: 如果一个客户端没有及时地读取数据可能会导致所有的客户端被阻塞。修改broadcaster来跳过一条消息,而不是等待这个客户端一直到其准备好读写。或者为每一个客户端的消息发送channel建立缓冲区,这样大部分的消息便不会被丢掉;broadcaster应该用一个非阻塞的send向这个channel中发消息。","breadcrumbs":"Goroutines和Channels » 示例: 聊天服务 » 8.10. 示例: 聊天服务","id":"110","title":"8.10. 示例: 聊天服务"},"111":{"body":"前一章我们介绍了一些使用goroutine和channel这样直接而自然的方式来实现并发的方法。然而这样做我们实际上回避了在写并发代码时必须处理的一些重要而且细微的问题。 在本章中,我们会细致地了解并发机制。尤其是在多goroutine之间的共享变量,并发问题的分析手段,以及解决这些问题的基本模式。最后我们会解释goroutine和操作系统线程之间的技术上的一些区别。","breadcrumbs":"基于共享变量的并发 » 第9章 基于共享变量的并发","id":"111","title":"第9章 基于共享变量的并发"},"112":{"body":"在一个线性(就是说只有一个goroutine的)的程序中,程序的执行顺序只由程序的逻辑来决定。例如,我们有一段语句序列,第一个在第二个之前(废话),以此类推。在有两个或更多goroutine的程序中,每一个goroutine内的语句也是按照既定的顺序去执行的,但是一般情况下我们没法去知道分别位于两个goroutine的事件x和y的执行顺序,x是在y之前还是之后还是同时发生是没法判断的。当我们没有办法自信地确认一个事件是在另一个事件的前面或者后面发生的话,就说明x和y这两个事件是并发的。 考虑一下,一个函数在线性程序中可以正确地工作。如果在并发的情况下,这个函数依然可以正确地工作的话,那么我们就说这个函数是并发安全的,并发安全的函数不需要额外的同步工作。我们可以把这个概念概括为一个特定类型的一些方法和操作函数,对于某个类型来说,如果其所有可访问的方法和操作都是并发安全的话,那么该类型便是并发安全的。 在一个程序中有非并发安全的类型的情况下,我们依然可以使这个程序并发安全。确实,并发安全的类型是例外,而不是规则,所以只有当文档中明确地说明了其是并发安全的情况下,你才可以并发地去访问它。我们会避免并发访问大多数的类型,无论是将变量局限在单一的一个goroutine内,还是用互斥条件维持更高级别的不变性,都是为了这个目的。我们会在本章中说明这些术语。 相反,包级别的导出函数一般情况下都是并发安全的。由于package级的变量没法被限制在单一的gorouine,所以修改这些变量“必须”使用互斥条件。 一个函数在并发调用时没法工作的原因太多了,比如死锁(deadlock)、活锁(livelock)和饿死(resource starvation)。我们没有空去讨论所有的问题,这里我们只聚焦在竞争条件上。 竞争条件指的是程序在多个goroutine交叉执行操作时,没有给出正确的结果。竞争条件是很恶劣的一种场景,因为这种问题会一直潜伏在你的程序里,然后在非常少见的时候蹦出来,或许只是会在很大的负载时才会发生,又或许是会在使用了某一个编译器、某一种平台或者某一种架构的时候才会出现。这些使得竞争条件带来的问题非常难以复现而且难以分析诊断。 传统上经常用经济损失来为竞争条件做比喻,所以我们来看一个简单的银行账户程序。 // Package bank implements a bank with only one account.\npackage bank\nvar balance int\nfunc Deposit(amount int) { balance = balance + amount }\nfunc Balance() int { return balance } (当然我们也可以把Deposit存款函数写成balance += amount,这种形式也是等价的,不过长一些的形式解释起来更方便一些。) 对于这个简单的程序而言,我们一眼就能看出,以任意顺序调用函数Deposit和Balance都会得到正确的结果。也就是说,Balance函数会给出之前的所有存入的额度之和。然而,当我们并发地而不是顺序地调用这些函数的话,Balance就再也没办法保证结果正确了。考虑一下下面的两个goroutine,其代表了一个银行联合账户的两笔交易: // Alice:\ngo func() { bank.Deposit(200) // A1 fmt.Println(\"=\", bank.Balance()) // A2\n}() // Bob:\ngo bank.Deposit(100) // B Alice存了$200,然后检查她的余额,同时Bob存了$100。因为A1和A2是和B并发执行的,我们没法预测他们发生的先后顺序。直观地来看的话,我们会认为其执行顺序只有三种可能性:“Alice先”,“Bob先”以及“Alice/Bob/Alice”交错执行。下面的表格会展示经过每一步骤后balance变量的值。引号里的字符串表示余额单。 Alice first Bob first Alice/Bob/Alice 0 0 0 A1 200 B 100 A1 200 A2 \"= 200\" A1 300 B 300 B 300 A2 \"= 300\" A2 \"= 300\" 所有情况下最终的余额都是$300。唯一的变数是Alice的余额单是否包含了Bob交易,不过无论怎么着客户都不会在意。 但是事实是上面的直觉推断是错误的。第四种可能的结果是事实存在的,这种情况下Bob的存款会在Alice存款操作中间,在余额被读到(balance + amount)之后,在余额被更新之前(balance = ...),这样会导致Bob的交易丢失。而这是因为Alice的存款操作A1实际上是两个操作的一个序列,读取然后写;可以称之为A1r和A1w。下面是交叉时产生的问题: Data race\n0\nA1r 0 ... = balance + amount\nB 100\nA1w 200 balance = ...\nA2 \"= 200\" 在A1r之后,balance + amount会被计算为200,所以这是A1w会写入的值,并不受其它存款操作的干预。最终的余额是$200。银行的账户上的资产比Bob实际的资产多了$100。(译注:因为丢失了Bob的存款操作,所以其实是说Bob的钱丢了。) 这个程序包含了一个特定的竞争条件,叫作数据竞争。无论任何时候,只要有两个goroutine并发访问同一变量,且至少其中的一个是写操作的时候就会发生数据竞争。 如果数据竞争的对象是一个比一个机器字(译注:32位机器上一个字=4个字节)更大的类型时,事情就变得更麻烦了,比如interface,string或者slice类型都是如此。下面的代码会并发地更新两个不同长度的slice: var x []int\ngo func() { x = make([]int, 10) }()\ngo func() { x = make([]int, 1000000) }()\nx[999999] = 1 // NOTE: undefined behavior; memory corruption possible! 最后一个语句中的x的值是未定义的;其可能是nil,或者也可能是一个长度为10的slice,也可能是一个长度为1,000,000的slice。但是回忆一下slice的三个组成部分:指针(pointer)、长度(length)和容量(capacity)。如果指针是从第一个make调用来,而长度从第二个make来,x就变成了一个混合体,一个自称长度为1,000,000但实际上内部只有10个元素的slice。这样导致的结果是存储999,999元素的位置会碰撞一个遥远的内存位置,这种情况下难以对值进行预测,而且debug也会变成噩梦。这种语义雷区被称为未定义行为,对C程序员来说应该很熟悉;幸运的是在Go语言里造成的麻烦要比C里小得多。 尽管并发程序的概念让我们知道并发并不是简单的语句交叉执行。我们将会在9.4节中看到,数据竞争可能会有奇怪的结果。许多程序员,甚至一些非常聪明的人也还是会偶尔提出一些理由来允许数据竞争,比如:“互斥条件代价太高”,“这个逻辑只是用来做logging”,“我不介意丢失一些消息”等等。因为在他们的编译器或者平台上很少遇到问题,可能给了他们错误的信心。一个好的经验法则是根本就没有什么所谓的良性数据竞争。所以我们一定要避免数据竞争,那么在我们的程序中要如何做到呢? 我们来重复一下数据竞争的定义,因为实在太重要了:数据竞争会在两个以上的goroutine并发访问相同的变量且至少其中一个为写操作时发生。根据上述定义,有三种方式可以避免数据竞争: 第一种方法是不要去写变量。考虑一下下面的map,会被“懒”填充,也就是说在每个key被第一次请求到的时候才会去填值。如果Icon是被顺序调用的话,这个程序会工作很正常,但如果Icon被并发调用,那么对于这个map来说就会存在数据竞争。 var icons = make(map[string]image.Image)\nfunc loadIcon(name string) image.Image // NOTE: not concurrency-safe!\nfunc Icon(name string) image.Image { icon, ok := icons[name] if !ok { icon = loadIcon(name) icons[name] = icon } return icon\n} 反之,如果我们在创建goroutine之前的初始化阶段,就初始化了map中的所有条目并且再也不去修改它们,那么任意数量的goroutine并发访问Icon都是安全的,因为每一个goroutine都只是去读取而已。 var icons = map[string]image.Image{ \"spades.png\": loadIcon(\"spades.png\"), \"hearts.png\": loadIcon(\"hearts.png\"), \"diamonds.png\": loadIcon(\"diamonds.png\"), \"clubs.png\": loadIcon(\"clubs.png\"),\n} // Concurrency-safe.\nfunc Icon(name string) image.Image { return icons[name] } 上面的例子里icons变量在包初始化阶段就已经被赋值了,包的初始化是在程序main函数开始执行之前就完成了的。只要初始化完成了,icons就再也不会被修改。数据结构如果从不被修改或是不变量则是并发安全的,无需进行同步。不过显然,如果update操作是必要的,我们就没法用这种方法,比如说银行账户。 第二种避免数据竞争的方法是,避免从多个goroutine访问变量。这也是前一章中大多数程序所采用的方法。例如前面的并发web爬虫(§8.6)的main goroutine是唯一一个能够访问seen map的goroutine,而聊天服务器(§8.10)中的broadcaster goroutine是唯一一个能够访问clients map的goroutine。这些变量都被限定在了一个单独的goroutine中。 由于其它的goroutine不能够直接访问变量,它们只能使用一个channel来发送请求给指定的goroutine来查询更新变量。这也就是Go的口头禅“不要使用共享数据来通信;使用通信来共享数据”。一个提供对一个指定的变量通过channel来请求的goroutine叫做这个变量的monitor(监控)goroutine。例如broadcaster goroutine会监控clients map的全部访问。 下面是一个重写了的银行的例子,这个例子中balance变量被限制在了monitor goroutine中,名为teller: gopl.io/ch9/bank1 // Package bank provides a concurrency-safe bank with one account.\npackage bank var deposits = make(chan int) // send amount to deposit\nvar balances = make(chan int) // receive balance func Deposit(amount int) { deposits <- amount }\nfunc Balance() int { return <-balances } func teller() { var balance int // balance is confined to teller goroutine for { select { case amount := <-deposits: balance += amount case balances <- balance: } }\n} func init() { go teller() // start the monitor goroutine\n} 即使当一个变量无法在其整个生命周期内被绑定到一个独立的goroutine,绑定依然是并发问题的一个解决方案。例如在一条流水线上的goroutine之间共享变量是很普遍的行为,在这两者间会通过channel来传输地址信息。如果流水线的每一个阶段都能够避免在将变量传送到下一阶段后再去访问它,那么对这个变量的所有访问就是线性的。其效果是变量会被绑定到流水线的一个阶段,传送完之后被绑定到下一个,以此类推。这种规则有时被称为串行绑定。 下面的例子中,Cakes会被严格地顺序访问,先是baker gorouine,然后是icer gorouine: type Cake struct{ state string } func baker(cooked chan<- *Cake) { for { cake := new(Cake) cake.state = \"cooked\" cooked <- cake // baker never touches this cake again }\n} func icer(iced chan<- *Cake, cooked <-chan *Cake) { for cake := range cooked { cake.state = \"iced\" iced <- cake // icer never touches this cake again }\n} 第三种避免数据竞争的方法是允许很多goroutine去访问变量,但是在同一个时刻最多只有一个goroutine在访问。这种方式被称为“互斥”,在下一节来讨论这个主题。 练习 9.1: 给gopl.io/ch9/bank1程序添加一个Withdraw(amount int)取款函数。其返回结果应该要表明事务是成功了还是因为没有足够资金失败了。这条消息会被发送给monitor的goroutine,且消息需要包含取款的额度和一个新的channel,这个新channel会被monitor goroutine来把boolean结果发回给Withdraw。","breadcrumbs":"基于共享变量的并发 » 竞争条件 » 9.1. 竞争条件","id":"112","title":"9.1. 竞争条件"},"113":{"body":"在8.6节中,我们使用了一个buffered channel作为一个计数信号量,来保证最多只有20个goroutine会同时执行HTTP请求。同理,我们可以用一个容量只有1的channel来保证最多只有一个goroutine在同一时刻访问一个共享变量。一个只能为1和0的信号量叫做二元信号量(binary semaphore)。 gopl.io/ch9/bank2 var ( sema = make(chan struct{}, 1) // a binary semaphore guarding balance balance int\n) func Deposit(amount int) { sema <- struct{}{} // acquire token balance = balance + amount <-sema // release token\n} func Balance() int { sema <- struct{}{} // acquire token b := balance <-sema // release token return b\n} 这种互斥很实用,而且被sync包里的Mutex类型直接支持。它的Lock方法能够获取到token(这里叫锁),并且Unlock方法会释放这个token: gopl.io/ch9/bank3 import \"sync\" var ( mu sync.Mutex // guards balance balance int\n) func Deposit(amount int) { mu.Lock() balance = balance + amount mu.Unlock()\n} func Balance() int { mu.Lock() b := balance mu.Unlock() return b\n} 每次一个goroutine访问bank变量时(这里只有balance余额变量),它都会调用mutex的Lock方法来获取一个互斥锁。如果其它的goroutine已经获得了这个锁的话,这个操作会被阻塞直到其它goroutine调用了Unlock使该锁变回可用状态。mutex会保护共享变量。惯例来说,被mutex所保护的变量是在mutex变量声明之后立刻声明的。如果你的做法和惯例不符,确保在文档里对你的做法进行说明。 在Lock和Unlock之间的代码段中的内容goroutine可以随便读取或者修改,这个代码段叫做临界区。锁的持有者在其他goroutine获取该锁之前需要调用Unlock。goroutine在结束后释放锁是必要的,无论以哪条路径通过函数都需要释放,即使是在错误路径中,也要记得释放。 上面的bank程序例证了一种通用的并发模式。一系列的导出函数封装了一个或多个变量,那么访问这些变量唯一的方式就是通过这些函数来做(或者方法,对于一个对象的变量来说)。每一个函数在一开始就获取互斥锁并在最后释放锁,从而保证共享变量不会被并发访问。这种函数、互斥锁和变量的编排叫作监控monitor(这种老式单词的monitor是受“monitor goroutine”的术语启发而来的。两种用法都是一个代理人保证变量被顺序访问)。 由于在存款和查询余额函数中的临界区代码这么短——只有一行,没有分支调用——在代码最后去调用Unlock就显得更为直截了当。在更复杂的临界区的应用中,尤其是必须要尽早处理错误并返回的情况下,就很难去(靠人)判断对Lock和Unlock的调用是在所有路径中都能够严格配对的了。Go语言里的defer简直就是这种情况下的救星:我们用defer来调用Unlock,临界区会隐式地延伸到函数作用域的最后,这样我们就从“总要记得在函数返回之后或者发生错误返回时要记得调用一次Unlock”这种状态中获得了解放。Go会自动帮我们完成这些事情。 func Balance() int { mu.Lock() defer mu.Unlock() return balance\n} 上面的例子里Unlock会在return语句读取完balance的值之后执行,所以Balance函数是并发安全的。这带来的另一点好处是,我们再也不需要一个本地变量b了。 此外,一个deferred Unlock即使在临界区发生panic时依然会执行,这对于用recover(§5.10)来恢复的程序来说是很重要的。defer调用只会比显式地调用Unlock成本高那么一点点,不过却在很大程度上保证了代码的整洁性。大多数情况下对于并发程序来说,代码的整洁性比过度的优化更重要。如果可能的话尽量使用defer来将临界区扩展到函数的结束。 考虑一下下面的Withdraw函数。成功的时候,它会正确地减掉余额并返回true。但如果银行记录资金对交易来说不足,那么取款就会恢复余额,并返回false。 // NOTE: not atomic!\nfunc Withdraw(amount int) bool { Deposit(-amount) if Balance() < 0 { Deposit(amount) return false // insufficient funds } return true\n} 函数终于给出了正确的结果,但是还有一点讨厌的副作用。当过多的取款操作同时执行时,balance可能会瞬时被减到0以下。这可能会引起一个并发的取款被不合逻辑地拒绝。所以如果Bob尝试买一辆sports car时,Alice可能就没办法为她的早咖啡付款了。这里的问题是取款不是一个原子操作:它包含了三个步骤,每一步都需要去获取并释放互斥锁,但任何一次锁都不会锁上整个取款流程。 理想情况下,取款应该只在整个操作中获得一次互斥锁。下面这样的尝试是错误的: // NOTE: incorrect!\nfunc Withdraw(amount int) bool { mu.Lock() defer mu.Unlock() Deposit(-amount) if Balance() < 0 { Deposit(amount) return false // insufficient funds } return true\n} 上面这个例子中,Deposit会调用mu.Lock()第二次去获取互斥锁,但因为mutex已经锁上了,而无法被重入(译注:go里没有重入锁,关于重入锁的概念,请参考java)——也就是说没法对一个已经锁上的mutex来再次上锁——这会导致程序死锁,没法继续执行下去,Withdraw会永远阻塞下去。 关于Go的mutex不能重入这一点我们有很充分的理由。mutex的目的是确保共享变量在程序执行时的关键点上能够保证不变性。不变性的一层含义是“没有goroutine访问共享变量”,但实际上这里对于mutex保护的变量来说,不变性还包含更深层含义:当一个goroutine获得了一个互斥锁时,它能断定被互斥锁保护的变量正处于不变状态(译注:即没有其他代码块正在读写共享变量),在其获取并保持锁期间,可能会去更新共享变量,这样不变性只是短暂地被破坏,然而当其释放锁之后,锁必须保证共享变量重获不变性并且多个goroutine按顺序访问共享变量。尽管一个可以重入的mutex也可以保证没有其它的goroutine在访问共享变量,但它不具备不变性更深层含义。(译注: 更详细的解释 ,Russ Cox认为可重入锁是bug的温床,是一个失败的设计) 一个通用的解决方案是将一个函数分离为多个函数,比如我们把Deposit分离成两个:一个不导出的函数deposit,这个函数假设锁总是会被保持并去做实际的操作,另一个是导出的函数Deposit,这个函数会调用deposit,但在调用前会先去获取锁。同理我们可以将Withdraw也表示成这种形式: func Withdraw(amount int) bool { mu.Lock() defer mu.Unlock() deposit(-amount) if balance < 0 { deposit(amount) return false // insufficient funds } return true\n} func Deposit(amount int) { mu.Lock() defer mu.Unlock() deposit(amount)\n} func Balance() int { mu.Lock() defer mu.Unlock() return balance\n} // This function requires that the lock be held.\nfunc deposit(amount int) { balance += amount } 当然,这里的存款deposit函数很小,实际上取款Withdraw函数不需要理会对它的调用,尽管如此,这里的表达还是表明了规则。 封装(§6.6),用限制一个程序中的意外交互的方式,可以使我们获得数据结构的不变性。因为某种原因,封装还帮我们获得了并发的不变性。当你使用mutex时,确保mutex和其保护的变量没有被导出(在go里也就是小写,且不要被大写字母开头的函数访问啦),无论这些变量是包级的变量还是一个struct的字段。","breadcrumbs":"基于共享变量的并发 » sync.Mutex互斥锁 » 9.2. sync.Mutex互斥锁","id":"113","title":"9.2. sync.Mutex互斥锁"},"114":{"body":"在100刀的存款消失时不做记录多少还是会让我们有一些恐慌,Bob写了一个程序,每秒运行几百次来检查他的银行余额。他会在家,在工作中,甚至会在他的手机上来运行这个程序。银行注意到这些陡增的流量使得存款和取款有了延时,因为所有的余额查询请求是顺序执行的,这样会互斥地获得锁,并且会暂时阻止其它的goroutine运行。 由于Balance函数只需要读取变量的状态,所以我们同时让多个Balance调用并发运行事实上是安全的,只要在运行的时候没有存款或者取款操作就行。在这种场景下我们需要一种特殊类型的锁,其允许多个只读操作并行执行,但写操作会完全互斥。这种锁叫作“多读单写”锁(multiple readers, single writer lock),Go语言提供的这样的锁是sync.RWMutex: var mu sync.RWMutex\nvar balance int\nfunc Balance() int { mu.RLock() // readers lock defer mu.RUnlock() return balance\n} Balance函数现在调用了RLock和RUnlock方法来获取和释放一个读取或者共享锁。Deposit函数没有变化,会调用mu.Lock和mu.Unlock方法来获取和释放一个写或互斥锁。 在这次修改后,Bob的余额查询请求就可以彼此并行地执行并且会很快地完成了。锁在更多的时间范围可用,并且存款请求也能够及时地被响应了。 RLock只能在临界区共享变量没有任何写入操作时可用。一般来说,我们不应该假设逻辑上的只读函数/方法也不会去更新某一些变量。比如一个方法功能是访问一个变量,但它也有可能会同时去给一个内部的计数器+1(译注:可能是记录这个方法的访问次数啥的),或者去更新缓存——使即时的调用能够更快。如果有疑惑的话,请使用互斥锁。 RWMutex只有当获得锁的大部分goroutine都是读操作,而锁在竞争条件下,也就是说,goroutine们必须等待才能获取到锁的时候,RWMutex才是最能带来好处的。RWMutex需要更复杂的内部记录,所以会让它比一般的无竞争锁的mutex慢一些。","breadcrumbs":"基于共享变量的并发 » sync.RWMutex读写锁 » 9.3. sync.RWMutex读写锁","id":"114","title":"9.3. sync.RWMutex读写锁"},"115":{"body":"你可能比较纠结为什么Balance方法需要用到互斥条件,无论是基于channel还是基于互斥量。毕竟和存款不一样,它只由一个简单的操作组成,所以不会碰到其它goroutine在其执行“期间”执行其它逻辑的风险。这里使用mutex有两方面考虑。第一Balance不会在其它操作比如Withdraw“中间”执行。第二(更重要的)是“同步”不仅仅是一堆goroutine执行顺序的问题,同样也会涉及到内存的问题。 在现代计算机中可能会有一堆处理器,每一个都会有其本地缓存(local cache)。为了效率,对内存的写入一般会在每一个处理器中缓冲,并在必要时一起flush到主存。这种情况下这些数据可能会以与当初goroutine写入顺序不同的顺序被提交到主存。像channel通信或者互斥量操作这样的原语会使处理器将其聚集的写入flush并commit,这样goroutine在某个时间点上的执行结果才能被其它处理器上运行的goroutine得到。 考虑一下下面代码片段的可能输出: var x, y int\ngo func() { x = 1 // A1 fmt.Print(\"y:\", y, \" \") // A2\n}()\ngo func() { y = 1 // B1 fmt.Print(\"x:\", x, \" \") // B2\n}() 因为两个goroutine是并发执行,并且访问共享变量时也没有互斥,会有数据竞争,所以程序的运行结果没法预测的话也请不要惊讶。我们可能希望它能够打印出下面这四种结果中的一种,相当于几种不同的交错执行时的情况: y:0 x:1\nx:0 y:1\nx:1 y:1\ny:1 x:1 第四行可以被解释为执行顺序A1,B1,A2,B2或者B1,A1,A2,B2的执行结果。然而实际运行时还是有些情况让我们有点惊讶: x:0 y:0\ny:0 x:0 根据所使用的编译器,CPU,或者其它很多影响因子,这两种情况也是有可能发生的。那么这两种情况要怎么解释呢? 在一个独立的goroutine中,每一个语句的执行顺序是可以被保证的,也就是说goroutine内顺序是连贯的。但是在不使用channel且不使用mutex这样的显式同步操作时,我们就没法保证事件在不同的goroutine中看到的执行顺序是一致的了。尽管goroutine A中一定需要观察到x=1执行成功之后才会去读取y,但它没法确保自己观察得到goroutine B中对y的写入,所以A还可能会打印出y的一个旧版的值。 尽管去理解并发的一种尝试是去将其运行理解为不同goroutine语句的交错执行,但看看上面的例子,这已经不是现代的编译器和cpu的工作方式了。因为赋值和打印指向不同的变量,编译器可能会断定两条语句的顺序不会影响执行结果,并且会交换两个语句的执行顺序。如果两个goroutine在不同的CPU上执行,每一个核心有自己的缓存,这样一个goroutine的写入对于其它goroutine的Print,在主存同步之前就是不可见的了。 所有并发的问题都可以用一致的、简单的既定的模式来规避。所以可能的话,将变量限定在goroutine内部;如果是多个goroutine都需要访问的变量,使用互斥条件来访问。","breadcrumbs":"基于共享变量的并发 » 内存同步 » 9.4. 内存同步","id":"115","title":"9.4. 内存同步"},"116":{"body":"即使我们小心到不能再小心,但在并发程序中犯错还是太容易了。幸运的是,Go的runtime和工具链为我们装备了一个复杂但好用的动态分析工具,竞争检查器(the race detector)。 只要在go build,go run或者go test命令后面加上-race的flag,就会使编译器创建一个你的应用的“修改”版或者一个附带了能够记录所有运行期对共享变量访问工具的test,并且会记录下每一个读或者写共享变量的goroutine的身份信息。另外,修改版的程序会记录下所有的同步事件,比如go语句,channel操作,以及对(*sync.Mutex).Lock,(*sync.WaitGroup).Wait等等的调用。(完整的同步事件集合是在The Go Memory Model文档中有说明,该文档是和语言文档放在一起的。译注:https://golang.org/ref/mem ) 竞争检查器会检查这些事件,会寻找在哪一个goroutine中出现了这样的case,例如其读或者写了一个共享变量,这个共享变量是被另一个goroutine在没有进行干预同步操作便直接写入的。这种情况也就表明了是对一个共享变量的并发访问,即数据竞争。这个工具会打印一份报告,内容包含变量身份,读取和写入的goroutine中活跃的函数的调用栈。这些信息在定位问题时通常很有用。9.7节中会有一个竞争检查器的实战样例。 竞争检查器会报告所有的已经发生的数据竞争。然而,它只能检测到运行时的竞争条件;并不能证明之后不会发生数据竞争。所以为了使结果尽量正确,请保证你的测试并发地覆盖到了你的包。 由于需要额外的记录,因此构建时加了竞争检测的程序跑起来会慢一些,且需要更大的内存,即使是这样,这些代价对于很多生产环境的程序(工作)来说还是可以接受的。对于一些偶发的竞争条件来说,让竞争检查器来干活可以节省无数日夜的debugging。(译注:多少服务端C和C++程序员为此竞折腰。)","breadcrumbs":"基于共享变量的并发 » 竞争条件检测 » 9.6. 竞争条件检测","id":"116","title":"9.6. 竞争条件检测"},"117":{"body":"本节中我们会做一个无阻塞的缓存,这种工具可以帮助我们来解决现实世界中并发程序出现但没有现成的库可以解决的问题。这个问题叫作缓存(memoizing)函数(译注:Memoization的定义: memoization 一词是Donald Michie 根据拉丁语memorandum杜撰的一个词。相应的动词、过去分词、ing形式有memoiz、memoized、memoizing),也就是说,我们需要缓存函数的返回结果,这样在对函数进行调用的时候,我们就只需要一次计算,之后只要返回计算的结果就可以了。我们的解决方案会是并发安全且会避免对整个缓存加锁而导致所有操作都去争一个锁的设计。 我们将使用下面的httpGetBody函数作为我们需要缓存的函数的一个样例。这个函数会去进行HTTP GET请求并且获取http响应body。对这个函数的调用本身开销是比较大的,所以我们尽量避免在不必要的时候反复调用。 func httpGetBody(url string) (interface{}, error) { resp, err := http.Get(url) if err != nil { return nil, err } defer resp.Body.Close() return ioutil.ReadAll(resp.Body)\n} 最后一行稍微隐藏了一些细节。ReadAll会返回两个结果,一个[]byte数组和一个错误,不过这两个对象可以被赋值给httpGetBody的返回声明里的interface{}和error类型,所以我们也就可以这样返回结果并且不需要额外的工作了。我们在httpGetBody中选用这种返回类型是为了使其可以与缓存匹配。 下面是我们要设计的cache的第一个“草稿”: gopl.io/ch9/memo1 // Package memo provides a concurrency-unsafe\n// memoization of a function of type Func.\npackage memo // A Memo caches the results of calling a Func.\ntype Memo struct { f Func cache map[string]result\n} // Func is the type of the function to memoize.\ntype Func func(key string) (interface{}, error) type result struct { value interface{} err error\n} func New(f Func) *Memo { return &Memo{f: f, cache: make(map[string]result)}\n} // NOTE: not concurrency-safe!\nfunc (memo *Memo) Get(key string) (interface{}, error) { res, ok := memo.cache[key] if !ok { res.value, res.err = memo.f(key) memo.cache[key] = res } return res.value, res.err\n} Memo实例会记录需要缓存的函数f(类型为Func),以及缓存内容(里面是一个string到result映射的map)。每一个result都是简单的函数返回的值对儿——一个值和一个错误值。继续下去我们会展示一些Memo的变种,不过所有的例子都会遵循上面的这些方面。 下面是一个使用Memo的例子。对于流入的URL的每一个元素我们都会调用Get,并打印调用延时以及其返回的数据大小的log: m := memo.New(httpGetBody)\nfor url := range incomingURLs() { start := time.Now() value, err := m.Get(url) if err != nil { log.Print(err) } fmt.Printf(\"%s, %s, %d bytes\\n\", url, time.Since(start), len(value.([]byte)))\n} 我们可以使用测试包(第11章的主题)来系统地鉴定缓存的效果。从下面的测试输出,我们可以看到URL流包含了一些重复的情况,尽管我们第一次对每一个URL的(*Memo).Get的调用都会花上几百毫秒,但第二次就只需要花1毫秒就可以返回完整的数据了。 $ go test -v gopl.io/ch9/memo1\n=== RUN Test\nhttps://golang.org, 175.026418ms, 7537 bytes\nhttps://godoc.org, 172.686825ms, 6878 bytes\nhttps://play.golang.org, 115.762377ms, 5767 bytes\nhttp://gopl.io, 749.887242ms, 2856 bytes\nhttps://golang.org, 721ns, 7537 bytes\nhttps://godoc.org, 152ns, 6878 bytes\nhttps://play.golang.org, 205ns, 5767 bytes\nhttp://gopl.io, 326ns, 2856 bytes\n--- PASS: Test (1.21s)\nPASS\nok gopl.io/ch9/memo1 1.257s 这个测试是顺序地去做所有的调用的。 由于这种彼此独立的HTTP请求可以很好地并发,我们可以把这个测试改成并发形式。可以使用sync.WaitGroup来等待所有的请求都完成之后再返回。 m := memo.New(httpGetBody)\nvar n sync.WaitGroup\nfor url := range incomingURLs() { n.Add(1) go func(url string) { start := time.Now() value, err := m.Get(url) if err != nil { log.Print(err) } fmt.Printf(\"%s, %s, %d bytes\\n\", url, time.Since(start), len(value.([]byte))) n.Done() }(url)\n}\nn.Wait() 这次测试跑起来更快了,然而不幸的是貌似这个测试不是每次都能够正常工作。我们注意到有一些意料之外的cache miss(缓存未命中),或者命中了缓存但却返回了错误的值,或者甚至会直接崩溃。 但更糟糕的是,有时候这个程序还是能正确的运行(译:也就是最让人崩溃的偶发bug),所以我们甚至可能都不会意识到这个程序有bug。但是我们可以使用-race这个flag来运行程序,竞争检测器(§9.6)会打印像下面这样的报告: $ go test -run=TestConcurrent -race -v gopl.io/ch9/memo1\n=== RUN TestConcurrent\n...\nWARNING: DATA RACE\nWrite by goroutine 36: runtime.mapassign1() ~/go/src/runtime/hashmap.go:411 +0x0 gopl.io/ch9/memo1.(*Memo).Get() ~/gobook2/src/gopl.io/ch9/memo1/memo.go:32 +0x205 ...\nPrevious write by goroutine 35: runtime.mapassign1() ~/go/src/runtime/hashmap.go:411 +0x0 gopl.io/ch9/memo1.(*Memo).Get() ~/gobook2/src/gopl.io/ch9/memo1/memo.go:32 +0x205\n...\nFound 1 data race(s)\nFAIL gopl.io/ch9/memo1 2.393s memo.go的32行出现了两次,说明有两个goroutine在没有同步干预的情况下更新了cache map。这表明Get不是并发安全的,存在数据竞争。 28 func (memo *Memo) Get(key string) (interface{}, error) {\n29 res, ok := memo.cache(key)\n30 if !ok {\n31 res.value, res.err = memo.f(key)\n32 memo.cache[key] = res\n33 }\n34 return res.value, res.err\n35 } 最简单的使cache并发安全的方式是使用基于监控的同步。只要给Memo加上一个mutex,在Get的一开始获取互斥锁,return的时候释放锁,就可以让cache的操作发生在临界区内了: gopl.io/ch9/memo2 type Memo struct { f Func mu sync.Mutex // guards cache cache map[string]result\n} // Get is concurrency-safe.\nfunc (memo *Memo) Get(key string) (value interface{}, err error) { memo.mu.Lock() res, ok := memo.cache[key] if !ok { res.value, res.err = memo.f(key) memo.cache[key] = res } memo.mu.Unlock() return res.value, res.err\n} 测试依然并发进行,但这回竞争检查器“沉默”了。不幸的是对于Memo的这一点改变使我们完全丧失了并发的性能优点。每次对f的调用期间都会持有锁,Get将本来可以并行运行的I/O操作串行化了。我们本章的目的是完成一个无锁缓存,而不是现在这样的将所有请求串行化的函数的缓存。 下一个Get的实现,调用Get的goroutine会两次获取锁:查找阶段获取一次,如果查找没有返回任何内容,那么进入更新阶段会再次获取。在这两次获取锁的中间阶段,其它goroutine可以随意使用cache。 gopl.io/ch9/memo3 func (memo *Memo) Get(key string) (value interface{}, err error) { memo.mu.Lock() res, ok := memo.cache[key] memo.mu.Unlock() if !ok { res.value, res.err = memo.f(key) // Between the two critical sections, several goroutines // may race to compute f(key) and update the map. memo.mu.Lock() memo.cache[key] = res memo.mu.Unlock() } return res.value, res.err\n} 这些修改使性能再次得到了提升,但有一些URL被获取了两次。这种情况在两个以上的goroutine同一时刻调用Get来请求同样的URL时会发生。多个goroutine一起查询cache,发现没有值,然后一起调用f这个慢不拉叽的函数。在得到结果后,也都会去更新map。其中一个获得的结果会覆盖掉另一个的结果。 理想情况下是应该避免掉多余的工作的。而这种“避免”工作一般被称为duplicate suppression(重复抑制/避免)。下面版本的Memo每一个map元素都是指向一个条目的指针。每一个条目包含对函数f调用结果的内容缓存。与之前不同的是这次entry还包含了一个叫ready的channel。在条目的结果被设置之后,这个channel就会被关闭,以向其它goroutine广播(§8.9)去读取该条目内的结果是安全的了。 gopl.io/ch9/memo4 type entry struct { res result ready chan struct{} // closed when res is ready\n} func New(f Func) *Memo { return &Memo{f: f, cache: make(map[string]*entry)}\n} type Memo struct { f Func mu sync.Mutex // guards cache cache map[string]*entry\n} func (memo *Memo) Get(key string) (value interface{}, err error) { memo.mu.Lock() e := memo.cache[key] if e == nil { // This is the first request for this key. // This goroutine becomes responsible for computing // the value and broadcasting the ready condition. e = &entry{ready: make(chan struct{})} memo.cache[key] = e memo.mu.Unlock() e.res.value, e.res.err = memo.f(key) close(e.ready) // broadcast ready condition } else { // This is a repeat request for this key. memo.mu.Unlock() <-e.ready // wait for ready condition } return e.res.value, e.res.err\n} 现在Get函数包括下面这些步骤了:获取互斥锁来保护共享变量cache map,查询map中是否存在指定条目,如果没有找到那么分配空间插入一个新条目,释放互斥锁。如果存在条目的话且其值没有写入完成(也就是有其它的goroutine在调用f这个慢函数)时,goroutine必须等待值ready之后才能读到条目的结果。而想知道是否ready的话,可以直接从ready channel中读取,由于这个读取操作在channel关闭之前一直是阻塞。 如果没有条目的话,需要向map中插入一个没有准备好的条目,当前正在调用的goroutine就需要负责调用慢函数、更新条目以及向其它所有goroutine广播条目已经ready可读的消息了。 条目中的e.res.value和e.res.err变量是在多个goroutine之间共享的。创建条目的goroutine同时也会设置条目的值,其它goroutine在收到\"ready\"的广播消息之后立刻会去读取条目的值。尽管会被多个goroutine同时访问,但却并不需要互斥锁。ready channel的关闭一定会发生在其它goroutine接收到广播事件之前,因此第一个goroutine对这些变量的写操作是一定发生在这些读操作之前的。不会发生数据竞争。 这样并发、不重复、无阻塞的cache就完成了。 上面这样Memo的实现使用了一个互斥量来保护多个goroutine调用Get时的共享map变量。不妨把这种设计和前面提到的把map变量限制在一个单独的monitor goroutine的方案做一些对比,后者在调用Get时需要发消息。 Func、result和entry的声明和之前保持一致: // Func is the type of the function to memoize.\ntype Func func(key string) (interface{}, error) // A result is the result of calling a Func.\ntype result struct { value interface{} err error\n} type entry struct { res result ready chan struct{} // closed when res is ready\n} 然而Memo类型现在包含了一个叫做requests的channel,Get的调用方用这个channel来和monitor goroutine来通信。requests channel中的元素类型是request。Get的调用方会把这个结构中的两组key都填充好,实际上用这两个变量来对函数进行缓存的。另一个叫response的channel会被拿来发送响应结果。这个channel只会传回一个单独的值。 gopl.io/ch9/memo5 // A request is a message requesting that the Func be applied to key.\ntype request struct { key string response chan<- result // the client wants a single result\n} type Memo struct{ requests chan request }\n// New returns a memoization of f. Clients must subsequently call Close.\nfunc New(f Func) *Memo { memo := &Memo{requests: make(chan request)} go memo.server(f) return memo\n} func (memo *Memo) Get(key string) (interface{}, error) { response := make(chan result) memo.requests <- request{key, response} res := <-response return res.value, res.err\n} func (memo *Memo) Close() { close(memo.requests) } 上面的Get方法,会创建一个response channel,把它放进request结构中,然后发送给monitor goroutine,然后马上又会接收它。 cache变量被限制在了monitor goroutine ``(*Memo).server`中,下面会看到。monitor会在循环中一直读取请求,直到request channel被Close方法关闭。每一个请求都会去查询cache,如果没有找到条目的话,那么就会创建/插入一个新的条目。 func (memo *Memo) server(f Func) { cache := make(map[string]*entry) for req := range memo.requests { e := cache[req.key] if e == nil { // This is the first request for this key. e = &entry{ready: make(chan struct{})} cache[req.key] = e go e.call(f, req.key) // call f(key) } go e.deliver(req.response) }\n} func (e *entry) call(f Func, key string) { // Evaluate the function. e.res.value, e.res.err = f(key) // Broadcast the ready condition. close(e.ready)\n} func (e *entry) deliver(response chan<- result) { // Wait for the ready condition. <-e.ready // Send the result to the client. response <- e.res\n} 和基于互斥量的版本类似,第一个对某个key的请求需要负责去调用函数f并传入这个key,将结果存在条目里,并关闭ready channel来广播条目的ready消息。使用(*entry).call来完成上述工作。 紧接着对同一个key的请求会发现map中已经有了存在的条目,然后会等待结果变为ready,并将结果从response发送给客户端的goroutien。上述工作是用(*entry).deliver来完成的。对call和deliver方法的调用必须让它们在自己的goroutine中进行以确保monitor goroutines不会因此而被阻塞住而没法处理新的请求。 这个例子说明我们无论用上锁,还是通信来建立并发程序都是可行的。 上面的两种方案并不好说特定情境下哪种更好,不过了解他们还是有价值的。有时候从一种方式切换到另一种可以使你的代码更为简洁。(译注:不是说好的golang推崇通信并发么。) 练习 9.3: 扩展Func类型和(*Memo).Get方法,支持调用方提供一个可选的done channel,使其具备通过该channel来取消整个操作的能力(§8.9)。一个被取消了的Func的调用结果不应该被缓存。","breadcrumbs":"基于共享变量的并发 » 示例: 并发的非阻塞缓存 » 9.7. 示例: 并发的非阻塞缓存","id":"117","title":"9.7. 示例: 并发的非阻塞缓存"},"118":{"body":"在上一章中我们说goroutine和操作系统的线程区别可以先忽略。尽管两者的区别实际上只是一个量的区别,但量变会引起质变的道理同样适用于goroutine和线程。现在正是我们来区分开两者的最佳时机。","breadcrumbs":"基于共享变量的并发 » Goroutines和线程 » 9.8. Goroutines和线程","id":"118","title":"9.8. Goroutines和线程"},"119":{"body":"每一个OS线程都有一个固定大小的内存块(一般会是2MB)来做栈,这个栈会用来存储当前正在被调用或挂起(指在调用其它函数时)的函数的内部变量。这个固定大小的栈同时很大又很小。因为2MB的栈对于一个小小的goroutine来说是很大的内存浪费,比如对于我们用到的,一个只是用来WaitGroup之后关闭channel的goroutine来说。而对于go程序来说,同时创建成百上千个goroutine是非常普遍的,如果每一个goroutine都需要这么大的栈的话,那这么多的goroutine就不太可能了。除去大小的问题之外,固定大小的栈对于更复杂或者更深层次的递归函数调用来说显然是不够的。修改固定的大小可以提升空间的利用率,允许创建更多的线程,并且可以允许更深的递归调用,不过这两者是没法同时兼备的。 相反,一个goroutine会以一个很小的栈开始其生命周期,一般只需要2KB。一个goroutine的栈,和操作系统线程一样,会保存其活跃或挂起的函数调用的本地变量,但是和OS线程不太一样的是,一个goroutine的栈大小并不是固定的;栈的大小会根据需要动态地伸缩。而goroutine的栈的最大值有1GB,比传统的固定大小的线程栈要大得多,尽管一般情况下,大多goroutine都不需要这么大的栈。 ** 练习 9.4:** 创建一个流水线程序,支持用channel连接任意数量的goroutine,在跑爆内存之前,可以创建多少流水线阶段?一个变量通过整个流水线需要用多久?(这个练习题翻译不是很确定)","breadcrumbs":"基于共享变量的并发 » Goroutines和线程 » 9.8.1. 动态栈","id":"119","title":"9.8.1. 动态栈"},"12":{"body":"对于很多现代应用来说,访问互联网上的信息和访问本地文件系统一样重要。Go语言在net这个强大package的帮助下提供了一系列的package来做这件事情,使用这些包可以更简单地用网络收发信息,还可以建立更底层的网络连接,编写服务器程序。在这些情景下,Go语言原生的并发特性(在第八章中会介绍)显得尤其好用。 为了最简单地展示基于HTTP获取信息的方式,下面给出一个示例程序fetch,这个程序将获取对应的url,并将其源文本打印出来;这个例子的灵感来源于curl工具(译注:unix下的一个用来发http请求的工具,具体可以man curl)。当然,curl提供的功能更为复杂丰富,这里只编写最简单的样例。这个样例之后还会多次被用到。 gopl.io/ch1/fetch // Fetch prints the content found at a URL.\npackage main import ( \"fmt\" \"io/ioutil\" \"net/http\" \"os\"\n) func main() { for _, url := range os.Args[1:] { resp, err := http.Get(url) if err != nil { fmt.Fprintf(os.Stderr, \"fetch: %v\\n\", err) os.Exit(1) } b, err := ioutil.ReadAll(resp.Body) resp.Body.Close() if err != nil { fmt.Fprintf(os.Stderr, \"fetch: reading %s: %v\\n\", url, err) os.Exit(1) } fmt.Printf(\"%s\", b) }\n} 这个程序从两个package中导入了函数,net/http和io/ioutil包,http.Get函数是创建HTTP请求的函数,如果获取过程没有出错,那么会在resp这个结构体中得到访问的请求结果。resp的Body字段包括一个可读的服务器响应流。ioutil.ReadAll函数从response中读取到全部内容;将其结果保存在变量b中。resp.Body.Close关闭resp的Body流,防止资源泄露,Printf函数会将结果b写出到标准输出流中。 $ go build gopl.io/ch1/fetch\n$ ./fetch http://gopl.io\n\n\nThe Go Programming Languagetitle>\n... HTTP请求如果失败了的话,会得到下面这样的结果: $ ./fetch http://bad.gopl.io\nfetch: Get http://bad.gopl.io: dial tcp: lookup bad.gopl.io: no such host 译注:在大天朝的网络环境下很容易重现这种错误,下面是Windows下运行得到的错误信息: $ go run main.go http://gopl.io\nfetch: Get http://gopl.io: dial tcp: lookup gopl.io: getaddrinfow: No such host is known. 无论哪种失败原因,我们的程序都用了os.Exit函数来终止进程,并且返回一个status错误码,其值为1。 练习 1.7: 函数调用io.Copy(dst, src)会从src中读取内容,并将读到的结果写入到dst中,使用这个函数替代掉例子中的ioutil.ReadAll来拷贝响应结构体到os.Stdout,避免申请一个缓冲区(例子中的b)来存储。记得处理io.Copy返回结果中的错误。 练习 1.8: 修改fetch这个范例,如果输入的url参数没有 http:// 前缀的话,为这个url加上该前缀。你可能会用到strings.HasPrefix这个函数。 练习 1.9: 修改fetch打印出HTTP协议的状态码,可以从resp.Status变量得到该状态码。","breadcrumbs":"入门 » 获取URL » 1.5. 获取URL","id":"12","title":"1.5. 获取URL"},"120":{"body":"OS线程会被操作系统内核调度。每几毫秒,一个硬件计时器会中断处理器,这会调用一个叫作scheduler的内核函数。这个函数会挂起当前执行的线程并将它的寄存器内容保存到内存中,检查线程列表并决定下一次哪个线程可以被运行,并从内存中恢复该线程的寄存器信息,然后恢复执行该线程的现场并开始执行线程。因为操作系统线程是被内核所调度,所以从一个线程向另一个“移动”需要完整的上下文切换,也就是说,保存一个用户线程的状态到内存,恢复另一个线程的到寄存器,然后更新调度器的数据结构。这几步操作很慢,因为其局部性很差需要几次内存访问,并且会增加运行的cpu周期。 Go的运行时包含了其自己的调度器,这个调度器使用了一些技术手段,比如m:n调度,因为其会在n个操作系统线程上多工(调度)m个goroutine。Go调度器的工作和内核的调度是相似的,但是这个调度器只关注单独的Go程序中的goroutine(译注:按程序独立)。 和操作系统的线程调度不同的是,Go调度器并不是用一个硬件定时器,而是被Go语言“建筑”本身进行调度的。例如当一个goroutine调用了time.Sleep,或者被channel调用或者mutex操作阻塞时,调度器会使其进入休眠并开始执行另一个goroutine,直到时机到了再去唤醒第一个goroutine。因为这种调度方式不需要进入内核的上下文,所以重新调度一个goroutine比调度一个线程代价要低得多。 ** 练习 9.5: ** 写一个有两个goroutine的程序,两个goroutine会向两个无buffer channel反复地发送ping-pong消息。这样的程序每秒可以支持多少次通信?","breadcrumbs":"基于共享变量的并发 » Goroutines和线程 » 9.8.2. Goroutine调度","id":"120","title":"9.8.2. Goroutine调度"},"121":{"body":"Go的调度器使用了一个叫做GOMAXPROCS的变量来决定会有多少个操作系统的线程同时执行Go的代码。其默认的值是运行机器上的CPU的核心数,所以在一个有8个核心的机器上时,调度器一次会在8个OS线程上去调度GO代码。(GOMAXPROCS是前面说的m:n调度中的n)。在休眠中的或者在通信中被阻塞的goroutine是不需要一个对应的线程来做调度的。在I/O中或系统调用中或调用非Go语言函数时,是需要一个对应的操作系统线程的,但是GOMAXPROCS并不需要将这几种情况计算在内。 你可以用GOMAXPROCS的环境变量来显式地控制这个参数,或者也可以在运行时用runtime.GOMAXPROCS函数来修改它。我们在下面的小程序中会看到GOMAXPROCS的效果,这个程序会无限打印0和1。 for { go fmt.Print(0) fmt.Print(1)\n} $ GOMAXPROCS=1 go run hacker-cliché.go\n111111111111111111110000000000000000000011111... $ GOMAXPROCS=2 go run hacker-cliché.go\n010101010101010101011001100101011010010100110... 在第一次执行时,最多同时只能有一个goroutine被执行。初始情况下只有main goroutine被执行,所以会打印很多1。过了一段时间后,GO调度器会将其置为休眠,并唤醒另一个goroutine,这时候就开始打印很多0了,在打印的时候,goroutine是被调度到操作系统线程上的。在第二次执行时,我们使用了两个操作系统线程,所以两个goroutine可以一起被执行,以同样的频率交替打印0和1。我们必须强调的是goroutine的调度是受很多因子影响的,而runtime也是在不断地发展演进的,所以这里的你实际得到的结果可能会因为版本的不同而与我们运行的结果有所不同。 ** 练习9.6:** 测试一下计算密集型的并发程序(练习8.5那样的)会被GOMAXPROCS怎样影响到。在你的电脑上最佳的值是多少?你的电脑CPU有多少个核心?","breadcrumbs":"基于共享变量的并发 » Goroutines和线程 » 9.8.3. GOMAXPROCS","id":"121","title":"9.8.3. GOMAXPROCS"},"122":{"body":"在大多数支持多线程的操作系统和程序语言中,当前的线程都有一个独特的身份(id),并且这个身份信息可以以一个普通值的形式被很容易地获取到,典型的可以是一个integer或者指针值。这种情况下我们做一个抽象化的thread-local storage(线程本地存储,多线程编程中不希望其它线程访问的内容)就很容易,只需要以线程的id作为key的一个map就可以解决问题,每一个线程以其id就能从中获取到值,且和其它线程互不冲突。 goroutine没有可以被程序员获取到的身份(id)的概念。这一点是设计上故意而为之,由于thread-local storage总是会被滥用。比如说,一个web server是用一种支持tls的语言实现的,而非常普遍的是很多函数会去寻找HTTP请求的信息,这代表它们就是去其存储层(这个存储层有可能是tls)查找的。这就像是那些过分依赖全局变量的程序一样,会导致一种非健康的“距离外行为”,在这种行为下,一个函数的行为可能并不仅由自己的参数所决定,而是由其所运行在的线程所决定。因此,如果线程本身的身份会改变——比如一些worker线程之类的——那么函数的行为就会变得神秘莫测。 Go鼓励更为简单的模式,这种模式下参数(译注:外部显式参数和内部显式参数。tls 中的内容算是\"外部\"隐式参数)对函数的影响都是显式的。这样不仅使程序变得更易读,而且会让我们自由地向一些给定的函数分配子任务时不用担心其身份信息影响行为。 你现在应该已经明白了写一个Go程序所需要的所有语言特性信息。在后面两章节中,我们会回顾一些之前的实例和工具,支持我们写出更大规模的程序:如何将一个工程组织成一系列的包,如何获取,构建,测试,性能测试,剖析,写文档,并且将这些包分享出去。","breadcrumbs":"基于共享变量的并发 » Goroutines和线程 » 9.8.4. Goroutine没有ID号","id":"122","title":"9.8.4. Goroutine没有ID号"},"123":{"body":"现在随便一个小程序的实现都可能包含超过10000个函数。然而作者一般只需要考虑其中很小的一部分和做很少的设计,因为绝大部分代码都是由他人编写的,它们通过类似包或模块的方式被重用。 Go语言有超过100个的标准包(译注:可以用go list std | wc -l命令查看标准包的具体数目),标准库为大多数的程序提供了必要的基础构件。在Go的社区,有很多成熟的包被设计、共享、重用和改进,目前互联网上已经发布了非常多的Go语言开源包,它们可以通过 http://godoc.org 检索。在本章,我们将演示如何使用已有的包和创建新的包。 Go还自带了工具箱,里面有很多用来简化工作区和包管理的小工具。在本书开始的时候,我们已经见识过如何使用工具箱自带的工具来下载、构建和运行我们的演示程序了。在本章,我们将看看这些工具的基本设计理论和尝试更多的功能,例如打印工作区中包的文档和查询相关的元数据等。在下一章,我们将探讨testing包的单元测试用法。","breadcrumbs":"包和工具 » 第10章 包和工具","id":"123","title":"第10章 包和工具"},"124":{"body":"任何包系统设计的目的都是为了简化大型程序的设计和维护工作,通过将一组相关的特性放进一个独立的单元以便于理解和更新,在每个单元更新的同时保持和程序中其它单元的相对独立性。这种模块化的特性允许每个包可以被其它的不同项目共享和重用,在项目范围内、甚至全球范围统一的分发和复用。 每个包一般都定义了一个不同的名字空间用于它内部的每个标识符的访问。每个名字空间关联到一个特定的包,让我们给类型、函数等选择简短明了的名字,这样可以在使用它们的时候减少和其它部分名字的冲突。 每个包还通过控制包内名字的可见性和是否导出来实现封装特性。通过限制包成员的可见性并隐藏包API的具体实现,将允许包的维护者在不影响外部包用户的前提下调整包的内部实现。通过限制包内变量的可见性,还可以强制用户通过某些特定函数来访问和更新内部变量,这样可以保证内部变量的一致性和并发时的互斥约束。 当我们修改了一个源文件,我们必须重新编译该源文件对应的包和所有依赖该包的其他包。即使是从头构建,Go语言编译器的编译速度也明显快于其它编译语言。Go语言的闪电般的编译速度主要得益于三个语言特性。第一点,所有导入的包必须在每个文件的开头显式声明,这样的话编译器就没有必要读取和分析整个源文件来判断包的依赖关系。第二点,禁止包的环状依赖,因为没有循环依赖,包的依赖关系形成一个有向无环图,每个包可以被独立编译,而且很可能是被并发编译。第三点,编译后包的目标文件不仅仅记录包本身的导出信息,目标文件同时还记录了包的依赖关系。因此,在编译一个包的时候,编译器只需要读取每个直接导入包的目标文件,而不需要遍历所有依赖的的文件(译注:很多都是重复的间接依赖)。","breadcrumbs":"包和工具 » 包简介 » 10.1. 包简介","id":"124","title":"10.1. 包简介"},"125":{"body":"每个包是由一个全局唯一的字符串所标识的导入路径定位。出现在import语句中的导入路径也是字符串。 import ( \"fmt\" \"math/rand\" \"encoding/json\" \"golang.org/x/net/html\" \"github.com/go-sql-driver/mysql\"\n) 就像我们在2.6.1节提到过的,Go语言的规范并没有指明包的导入路径字符串的具体含义,导入路径的具体含义是由构建工具来解释的。在本章,我们将深入讨论Go语言工具箱的功能,包括大家经常使用的构建测试等功能。当然,也有第三方扩展的工具箱存在。例如,Google公司内部的Go语言码农,他们就使用内部的多语言构建系统(译注:Google公司使用的是类似 Bazel 的构建系统,支持多种编程语言,目前该构件系统还不能完整支持Windows环境),用不同的规则来处理包名字和定位包,用不同的规则来处理单元测试等等,因为这样可以更紧密适配他们内部环境。 如果你计划分享或发布包,那么导入路径最好是全球唯一的。为了避免冲突,所有非标准库包的导入路径建议以所在组织的互联网域名为前缀;而且这样也有利于包的检索。例如,上面的import语句导入了Go团队维护的HTML解析器和一个流行的第三方维护的MySQL驱动。","breadcrumbs":"包和工具 » 导入路径 » 10.2. 导入路径","id":"125","title":"10.2. 导入路径"},"126":{"body":"在每个Go语言源文件的开头都必须有包声明语句。包声明语句的主要目的是确定当前包被其它包导入时默认的标识符(也称为包名)。 例如,math/rand包的每个源文件的开头都包含package rand包声明语句,所以当你导入这个包,你就可以用rand.Int、rand.Float64类似的方式访问包的成员。 package main import ( \"fmt\" \"math/rand\"\n) func main() { fmt.Println(rand.Int())\n} 通常来说,默认的包名就是包导入路径名的最后一段,因此即使两个包的导入路径不同,它们依然可能有一个相同的包名。例如,math/rand包和crypto/rand包的包名都是rand。稍后我们将看到如何同时导入两个有相同包名的包。 关于默认包名一般采用导入路径名的最后一段的约定也有三种例外情况。第一个例外,包对应一个可执行程序,也就是main包,这时候main包本身的导入路径是无关紧要的。名字为main的包是给go build(§10.7.3)构建命令一个信息,这个包编译完之后必须调用连接器生成一个可执行程序。 第二个例外,包所在的目录中可能有一些文件名是以_test.go为后缀的Go源文件(译注:前面必须有其它的字符,因为以_或.开头的源文件会被构建工具忽略),并且这些源文件声明的包名也是以_test为后缀名的。这种目录可以包含两种包:一种是普通包,另一种则是测试的外部扩展包。所有以_test为后缀包名的测试外部扩展包都由go test命令独立编译,普通包和测试的外部扩展包是相互独立的。测试的外部扩展包一般用来避免测试代码中的循环导入依赖,具体细节我们将在11.2.4节中介绍。 第三个例外,一些依赖版本号的管理工具会在导入路径后追加版本号信息,例如“gopkg.in/yaml.v2”。这种情况下包的名字并不包含版本号后缀,而是yaml。","breadcrumbs":"包和工具 » 包声明 » 10.3. 包声明","id":"126","title":"10.3. 包声明"},"127":{"body":"可以在一个Go语言源文件包声明语句之后,其它非导入声明语句之前,包含零到多个导入包声明语句。每个导入声明可以单独指定一个导入路径,也可以通过圆括号同时导入多个导入路径。下面两个导入形式是等价的,但是第二种形式更为常见。 import \"fmt\"\nimport \"os\" import ( \"fmt\" \"os\"\n) 导入的包之间可以通过添加空行来分组;通常将来自不同组织的包独自分组。包的导入顺序无关紧要,但是在每个分组中一般会根据字符串顺序排列。(gofmt和goimports工具都可以将不同分组导入的包独立排序。) import ( \"fmt\" \"html/template\" \"os\" \"golang.org/x/net/html\" \"golang.org/x/net/ipv4\"\n) 如果我们想同时导入两个有着名字相同的包,例如math/rand包和crypto/rand包,那么导入声明必须至少为一个同名包指定一个新的包名以避免冲突。这叫做导入包的重命名。 import ( \"crypto/rand\" mrand \"math/rand\" // alternative name mrand avoids conflict\n) 导入包的重命名只影响当前的源文件。其它的源文件如果导入了相同的包,可以用导入包原本默认的名字或重命名为另一个完全不同的名字。 导入包重命名是一个有用的特性,它不仅仅只是为了解决名字冲突。如果导入的一个包名很笨重,特别是在一些自动生成的代码中,这时候用一个简短名称会更方便。选择用简短名称重命名导入包时候最好统一,以避免包名混乱。选择另一个包名称还可以帮助避免和本地普通变量名产生冲突。例如,如果文件中已经有了一个名为path的变量,那么我们可以将“path”标准包重命名为pathpkg。 每个导入声明语句都明确指定了当前包和被导入包之间的依赖关系。如果遇到包循环导入的情况,Go语言的构建工具将报告错误。","breadcrumbs":"包和工具 » 导入声明 » 10.4. 导入声明","id":"127","title":"10.4. 导入声明"},"128":{"body":"如果只是导入一个包而并不使用导入的包将会导致一个编译错误。但是有时候我们只是想利用导入包而产生的副作用:它会计算包级变量的初始化表达式和执行导入包的init初始化函数(§2.6.2)。这时候我们需要抑制“unused import”编译错误,我们可以用下划线_来重命名导入的包。像往常一样,下划线_为空白标识符,并不能被访问。 import _ \"image/png\" // register PNG decoder 这个被称为包的匿名导入。它通常是用来实现一个编译时机制,然后通过在main主程序入口选择性地导入附加的包。首先,让我们看看如何使用该特性,然后再看看它是如何工作的。 标准库的image图像包包含了一个Decode函数,用于从io.Reader接口读取数据并解码图像,它调用底层注册的图像解码器来完成任务,然后返回image.Image类型的图像。使用image.Decode很容易编写一个图像格式的转换工具,读取一种格式的图像,然后编码为另一种图像格式: gopl.io/ch10/jpeg // The jpeg command reads a PNG image from the standard input\n// and writes it as a JPEG image to the standard output.\npackage main import ( \"fmt\" \"image\" \"image/jpeg\" _ \"image/png\" // register PNG decoder \"io\" \"os\"\n) func main() { if err := toJPEG(os.Stdin, os.Stdout); err != nil { fmt.Fprintf(os.Stderr, \"jpeg: %v\\n\", err) os.Exit(1) }\n} func toJPEG(in io.Reader, out io.Writer) error { img, kind, err := image.Decode(in) if err != nil { return err } fmt.Fprintln(os.Stderr, \"Input format =\", kind) return jpeg.Encode(out, img, &jpeg.Options{Quality: 95})\n} 如果我们将gopl.io/ch3/mandelbrot(§3.3)的输出导入到这个程序的标准输入,它将解码输入的PNG格式图像,然后转换为JPEG格式的图像输出(图3.3)。 $ go build gopl.io/ch3/mandelbrot\n$ go build gopl.io/ch10/jpeg\n$ ./mandelbrot | ./jpeg >mandelbrot.jpg\nInput format = png 要注意image/png包的匿名导入语句。如果没有这一行语句,程序依然可以编译和运行,但是它将不能正确识别和解码PNG格式的图像: $ go build gopl.io/ch10/jpeg\n$ ./mandelbrot | ./jpeg >mandelbrot.jpg\njpeg: image: unknown format 下面的代码演示了它的工作机制。标准库还提供了GIF、PNG和JPEG等格式图像的解码器,用户也可以提供自己的解码器,但是为了保持程序体积较小,很多解码器并没有被全部包含,除非是明确需要支持的格式。image.Decode函数在解码时会依次查询支持的格式列表。每个格式驱动列表的每个入口指定了四件事情:格式的名称;一个用于描述这种图像数据开头部分模式的字符串,用于解码器检测识别;一个Decode函数用于完成解码图像工作;一个DecodeConfig函数用于解码图像的大小和颜色空间的信息。每个驱动入口是通过调用image.RegisterFormat函数注册,一般是在每个格式包的init初始化函数中调用,例如image/png包是这样注册的: package png // image/png func Decode(r io.Reader) (image.Image, error)\nfunc DecodeConfig(r io.Reader) (image.Config, error) func init() { const pngHeader = \"\\x89PNG\\r\\n\\x1a\\n\" image.RegisterFormat(\"png\", pngHeader, Decode, DecodeConfig)\n} 最终的效果是,主程序只需要匿名导入特定图像驱动包就可以用image.Decode解码对应格式的图像了。 数据库包database/sql也是采用了类似的技术,让用户可以根据自己需要选择导入必要的数据库驱动。例如: import ( \"database/sql\" _ \"github.com/lib/pq\" // enable support for Postgres _ \"github.com/go-sql-driver/mysql\" // enable support for MySQL\n) db, err = sql.Open(\"postgres\", dbname) // OK\ndb, err = sql.Open(\"mysql\", dbname) // OK\ndb, err = sql.Open(\"sqlite3\", dbname) // returns error: unknown driver \"sqlite3\" 练习 10.1: 扩展jpeg程序,以支持任意图像格式之间的相互转换,使用image.Decode检测支持的格式类型,然后通过flag命令行标志参数选择输出的格式。 练习 10.2: 设计一个通用的压缩文件读取框架,用来读取ZIP(archive/zip)和POSIX tar(archive/tar)格式压缩的文档。使用类似上面的注册技术来扩展支持不同的压缩格式,然后根据需要通过匿名导入选择导入要支持的压缩格式的驱动包。","breadcrumbs":"包和工具 » 包的匿名导入 » 10.5. 包的匿名导入","id":"128","title":"10.5. 包的匿名导入"},"129":{"body":"在本节中,我们将提供一些关于Go语言独特的包和成员命名的约定。 当创建一个包,一般要用短小的包名,但也不能太短导致难以理解。标准库中最常用的包有bufio、bytes、flag、fmt、http、io、json、os、sort、sync和time等包。 尽可能让命名有描述性且无歧义。例如,类似imageutil或ioutilis的工具包命名已经足够简洁了,就无须再命名为util了。要尽量避免包名使用可能被经常用于局部变量的名字,这样可能导致用户重命名导入包,例如前面看到的path包。 包名一般采用单数的形式。标准库的bytes、errors和strings使用了复数形式,这是为了避免和预定义的类型冲突,同样还有go/types是为了避免和type关键字冲突。 要避免包名有其它的含义。例如,2.5节中我们的温度转换包最初使用了temp包名,虽然并没有持续多久。但这是一个糟糕的尝试,因为temp几乎是临时变量的同义词。然后我们有一段时间使用了temperature作为包名,显然名字并没有表达包的真实用途。最后我们改成了和strconv标准包类似的tempconv包名,这个名字比之前的就好多了。 现在让我们看看如何命名包的成员。由于是通过包的导入名字引入包里面的成员,例如fmt.Println,同时包含了包名和成员名信息。因此,我们一般并不需要关注Println的具体内容,因为fmt包名已经包含了这个信息。当设计一个包的时候,需要考虑包名和成员名两个部分如何很好地配合。下面有一些例子: bytes.Equal flag.Int http.Get json.Marshal 我们可以看到一些常用的命名模式。strings包提供了和字符串相关的诸多操作: package strings func Index(needle, haystack string) int type Replacer struct{ /* ... */ }\nfunc NewReplacer(oldnew ...string) *Replacer type Reader struct{ /* ... */ }\nfunc NewReader(s string) *Reader 包名strings并没有出现在任何成员名字中。因为用户会这样引用这些成员strings.Index、strings.Replacer等。 其它一些包,可能只描述了单一的数据类型,例如html/template和math/rand等,只暴露一个主要的数据结构和与它相关的方法,还有一个以New命名的函数用于创建实例。 package rand // \"math/rand\" type Rand struct{ /* ... */ }\nfunc New(source Source) *Rand 这可能导致一些名字重复,例如template.Template或rand.Rand,这就是这些种类的包名往往特别短的原因之一。 在另一个极端,还有像net/http包那样含有非常多的名字和种类不多的数据类型,因为它们都是要执行一个复杂的复合任务。尽管有将近二十种类型和更多的函数,但是包中最重要的成员名字却是简单明了的:Get、Post、Handle、Error、Client、Server等。","breadcrumbs":"包和工具 » 包和命名 » 10.6. 包和命名","id":"129","title":"10.6. 包和命名"},"13":{"body":"Go语言最有意思并且最新奇的特性就是对并发编程的支持。并发编程是一个大话题,在第八章和第九章中会专门讲到。这里我们只浅尝辄止地来体验一下Go语言里的goroutine和channel。 下面的例子fetchall,和前面小节的fetch程序所要做的工作基本一致,fetchall的特别之处在于它会同时去获取所有的URL,所以这个程序的总执行时间不会超过执行时间最长的那一个任务,前面的fetch程序执行时间则是所有任务执行时间之和。fetchall程序只会打印获取的内容大小和经过的时间,不会像之前那样打印获取的内容。 gopl.io/ch1/fetchall // Fetchall fetches URLs in parallel and reports their times and sizes.\npackage main import ( \"fmt\" \"io\" \"io/ioutil\" \"net/http\" \"os\" \"time\"\n) func main() { start := time.Now() ch := make(chan string) for _, url := range os.Args[1:] { go fetch(url, ch) // start a goroutine } for range os.Args[1:] { fmt.Println(<-ch) // receive from channel ch } fmt.Printf(\"%.2fs elapsed\\n\", time.Since(start).Seconds())\n} func fetch(url string, ch chan<- string) { start := time.Now() resp, err := http.Get(url) if err != nil { ch <- fmt.Sprint(err) // send to channel ch return } nbytes, err := io.Copy(ioutil.Discard, resp.Body) resp.Body.Close() // don't leak resources if err != nil { ch <- fmt.Sprintf(\"while reading %s: %v\", url, err) return } secs := time.Since(start).Seconds() ch <- fmt.Sprintf(\"%.2fs %7d %s\", secs, nbytes, url)\n} 下面使用fetchall来请求几个地址: $ go build gopl.io/ch1/fetchall\n$ ./fetchall https://golang.org http://gopl.io https://godoc.org\n0.14s 6852 https://godoc.org\n0.16s 7261 https://golang.org\n0.48s 2475 http://gopl.io\n0.48s elapsed goroutine是一种函数的并发执行方式,而channel是用来在goroutine之间进行参数传递。main函数本身也运行在一个goroutine中,而go function则表示创建一个新的goroutine,并在这个新的goroutine中执行这个函数。 main函数中用make函数创建了一个传递string类型参数的channel,对每一个命令行参数,我们都用go这个关键字来创建一个goroutine,并且让函数在这个goroutine异步执行http.Get方法。这个程序里的io.Copy会把响应的Body内容拷贝到ioutil.Discard输出流中(译注:可以把这个变量看作一个垃圾桶,可以向里面写一些不需要的数据),因为我们需要这个方法返回的字节数,但是又不想要其内容。每当请求返回内容时,fetch函数都会往ch这个channel里写入一个字符串,由main函数里的第二个for循环来处理并打印channel里的这个字符串。 当一个goroutine尝试在一个channel上做send或者receive操作时,这个goroutine会阻塞在调用处,直到另一个goroutine从这个channel里接收或者写入值,这样两个goroutine才会继续执行channel操作之后的逻辑。在这个例子中,每一个fetch函数在执行时都会往channel里发送一个值(ch <- expression),主函数负责接收这些值(<-ch)。这个程序中我们用main函数来完整地处理/接收所有fetch函数传回的字符串,可以避免因为有两个goroutine同时完成而使得其输出交错在一起的危险。 练习 1.10: 找一个数据量比较大的网站,用本小节中的程序调研网站的缓存策略,对每个URL执行两遍请求,查看两次时间是否有较大的差别,并且每次获取到的响应内容是否一致,修改本节中的程序,将响应结果输出到文件,以便于进行对比。 练习 1.11: 在fetchall中尝试使用长一些的参数列表,比如使用在alexa.com的上百万网站里排名靠前的。如果一个网站没有回应,程序将采取怎样的行为?(Section8.9 描述了在这种情况下的应对机制)。","breadcrumbs":"入门 » 并发获取多个URL » 1.6. 并发获取多个URL","id":"13","title":"1.6. 并发获取多个URL"},"130":{"body":"本章剩下的部分将讨论Go语言工具箱的具体功能,包括如何下载、格式化、构建、测试和安装Go语言编写的程序。 Go语言的工具箱集合了一系列功能的命令集。它可以看作是一个包管理器(类似于Linux中的apt和rpm工具),用于包的查询、计算包的依赖关系、从远程版本控制系统下载它们等任务。它也是一个构建系统,计算文件的依赖关系,然后调用编译器、汇编器和链接器构建程序,虽然它故意被设计成没有标准的make命令那么复杂。它也是一个单元测试和基准测试的驱动程序,我们将在第11章讨论测试话题。 Go语言工具箱的命令有着类似“瑞士军刀”的风格,带着一打的子命令,有一些我们经常用到,例如get、run、build和fmt等。你可以运行go或go help命令查看内置的帮助文档,为了查询方便,我们列出了最常用的命令: $ go\n... build compile packages and dependencies clean remove object files doc show documentation for package or symbol env print Go environment information fmt run gofmt on package sources get download and install packages and dependencies install compile and install packages and dependencies list list packages run compile and run Go program test test packages version print Go version vet run go tool vet on packages Use \"go help [command]\" for more information about a command.\n... 为了达到零配置的设计目标,Go语言的工具箱很多地方都依赖各种约定。例如,根据给定的源文件的名称,Go语言的工具可以找到源文件对应的包,因为每个目录只包含了单一的包,并且包的导入路径和工作区的目录结构是对应的。给定一个包的导入路径,Go语言的工具可以找到与之对应的存储着实体文件的目录。它还可以根据导入路径找到存储代码的仓库的远程服务器URL。","breadcrumbs":"包和工具 » 工具 » 10.7. 工具","id":"130","title":"10.7. 工具"},"131":{"body":"对于大多数的Go语言用户,只需要配置一个名叫GOPATH的环境变量,用来指定当前工作目录即可。当需要切换到不同工作区的时候,只要更新GOPATH就可以了。例如,我们在编写本书时将GOPATH设置为$HOME/gobook: $ export GOPATH=$HOME/gobook\n$ go get gopl.io/... 当你用前面介绍的命令下载本书全部的例子源码之后,你的当前工作区的目录结构应该是这样的: GOPATH/ src/ gopl.io/ .git/ ch1/ helloworld/ main.go dup/ main.go ... golang.org/x/net/ .git/ html/ parse.go node.go ... bin/ helloworld dup pkg/ darwin_amd64/ ... GOPATH对应的工作区目录有三个子目录。其中src子目录用于存储源代码。每个包被保存在与$GOPATH/src的相对路径为包导入路径的子目录中,例如gopl.io/ch1/helloworld相对应的路径目录。我们看到,一个GOPATH工作区的src目录中可能有多个独立的版本控制系统,例如gopl.io和golang.org分别对应不同的Git仓库。其中pkg子目录用于保存编译后的包的目标文件,bin子目录用于保存编译后的可执行程序,例如helloworld可执行程序。 第二个环境变量GOROOT用来指定Go的安装目录,还有它自带的标准库包的位置。GOROOT的目录结构和GOPATH类似,因此存放fmt包的源代码对应目录应该为$GOROOT/src/fmt。用户一般不需要设置GOROOT,默认情况下Go语言安装工具会将其设置为安装的目录路径。 其中go env命令用于查看Go语言工具涉及的所有环境变量的值,包括未设置环境变量的默认值。GOOS环境变量用于指定目标操作系统(例如android、linux、darwin或windows),GOARCH环境变量用于指定处理器的类型,例如amd64、386或arm等。虽然GOPATH环境变量是唯一必须要设置的,但是其它环境变量也会偶尔用到。 $ go env\nGOPATH=\"/home/gopher/gobook\"\nGOROOT=\"/usr/local/go\"\nGOARCH=\"amd64\"\nGOOS=\"darwin\"\n...","breadcrumbs":"包和工具 » 工具 » 10.7.1. 工作区结构","id":"131","title":"10.7.1. 工作区结构"},"132":{"body":"使用Go语言工具箱的go命令,不仅可以根据包导入路径找到本地工作区的包,甚至可以从互联网上找到和更新包。 使用命令go get可以下载一个单一的包或者用...下载整个子目录里面的每个包。Go语言工具箱的go命令同时计算并下载所依赖的每个包,这也是前一个例子中golang.org/x/net/html自动出现在本地工作区目录的原因。 一旦go get命令下载了包,然后就是安装包或包对应的可执行的程序。我们将在下一节再关注它的细节,现在只是展示整个下载过程是如何的简单。第一个命令是获取golint工具,它用于检测Go源代码的编程风格是否有问题。第二个命令是用golint命令对2.6.2节的gopl.io/ch2/popcount包代码进行编码风格检查。它友好地报告了忘记了包的文档: $ go get github.com/golang/lint/golint\n$ $GOPATH/bin/golint gopl.io/ch2/popcount\nsrc/gopl.io/ch2/popcount/main.go:1:1: package comment should be of the form \"Package popcount ...\" go get命令支持当前流行的托管网站GitHub、Bitbucket和Launchpad,可以直接向它们的版本控制系统请求代码。对于其它的网站,你可能需要指定版本控制系统的具体路径和协议,例如 Git或Mercurial。运行go help importpath获取相关的信息。 go get命令获取的代码是真实的本地存储仓库,而不仅仅只是复制源文件,因此你依然可以使用版本管理工具比较本地代码的变更或者切换到其它的版本。例如golang.org/x/net包目录对应一个Git仓库: $ cd $GOPATH/src/golang.org/x/net\n$ git remote -v\norigin https://go.googlesource.com/net (fetch)\norigin https://go.googlesource.com/net (push) 需要注意的是导入路径含有的网站域名和本地Git仓库对应远程服务地址并不相同,真实的Git地址是go.googlesource.com。这其实是Go语言工具的一个特性,可以让包用一个自定义的导入路径,但是真实的代码却是由更通用的服务提供,例如googlesource.com或github.com。因为页面 https://golang.org/x/net/html 包含了如下的元数据,它告诉Go语言的工具当前包真实的Git仓库托管地址: $ go build gopl.io/ch1/fetch\n$ ./fetch https://golang.org/x/net/html | grep go-import\n 如果指定-u命令行标志参数,go get命令将确保所有的包和依赖的包的版本都是最新的,然后重新编译和安装它们。如果不包含该标志参数的话,而且如果包已经在本地存在,那么代码将不会被自动更新。 go get -u命令只是简单地保证每个包是最新版本,如果是第一次下载包则是比较方便的;但是对于发布程序则可能是不合适的,因为本地程序可能需要对依赖的包做精确的版本依赖管理。通常的解决方案是使用vendor的目录用于存储依赖包的固定版本的源代码,对本地依赖的包的版本更新也是谨慎和持续可控的。在Go1.5之前,一般需要修改包的导入路径,所以复制后golang.org/x/net/html导入路径可能会变为gopl.io/vendor/golang.org/x/net/html。最新的Go语言命令已经支持vendor特性,但限于篇幅这里并不讨论vendor的具体细节。不过可以通过go help gopath命令查看Vendor的帮助文档。 (译注:墙内用户在上面这些命令的基础上,还需要学习用翻墙来go get。) 练习 10.3: 从 http://gopl.io/ch1/helloworld?go-get=1 获取内容,查看本书的代码的真实托管的网址(go get请求HTML页面时包含了go-get参数,以区别普通的浏览器请求)。","breadcrumbs":"包和工具 » 工具 » 10.7.2. 下载包","id":"132","title":"10.7.2. 下载包"},"133":{"body":"go build命令编译命令行参数指定的每个包。如果包是一个库,则忽略输出结果;这可以用于检测包是可以正确编译的。如果包的名字是main,go build将调用链接器在当前目录创建一个可执行程序;以导入路径的最后一段作为可执行程序的名字。 由于每个目录只包含一个包,因此每个对应可执行程序或者叫Unix术语中的命令的包,会要求放到一个独立的目录中。这些目录有时候会放在名叫cmd目录的子目录下面,例如用于提供Go文档服务的golang.org/x/tools/cmd/godoc命令就是放在cmd子目录(§10.7.4)。 每个包可以由它们的导入路径指定,就像前面看到的那样,或者用一个相对目录的路径名指定,相对路径必须以.或..开头。如果没有指定参数,那么默认指定为当前目录对应的包。下面的命令用于构建同一个包,虽然它们的写法各不相同: $ cd $GOPATH/src/gopl.io/ch1/helloworld\n$ go build 或者: $ cd anywhere\n$ go build gopl.io/ch1/helloworld 或者: $ cd $GOPATH\n$ go build ./src/gopl.io/ch1/helloworld 但不能这样: $ cd $GOPATH\n$ go build src/gopl.io/ch1/helloworld\nError: cannot find package \"src/gopl.io/ch1/helloworld\". 也可以指定包的源文件列表,这一般只用于构建一些小程序或做一些临时性的实验。如果是main包,将会以第一个Go源文件的基础文件名作为最终的可执行程序的名字。 $ cat quoteargs.go\npackage main import ( \"fmt\" \"os\"\n) func main() { fmt.Printf(\"%q\\n\", os.Args[1:])\n}\n$ go build quoteargs.go\n$ ./quoteargs one \"two three\" four\\ five\n[\"one\" \"two three\" \"four five\"] 特别是对于这类一次性运行的程序,我们希望尽快的构建并运行它。go run命令实际上是结合了构建和运行的两个步骤: $ go run quoteargs.go one \"two three\" four\\ five\n[\"one\" \"two three\" \"four five\"] (译注:其实也可以偷懒,直接go run *.go) 第一行的参数列表中,第一个不是以.go结尾的将作为可执行程序的参数运行。 默认情况下,go build命令构建指定的包和它依赖的包,然后丢弃除了最后的可执行文件之外所有的中间编译结果。依赖分析和编译过程虽然都是很快的,但是随着项目增加到几十个包和成千上万行代码,依赖关系分析和编译时间的消耗将变的可观,有时候可能需要几秒种,即使这些依赖项没有改变。 go install命令和go build命令很相似,但是它会保存每个包的编译成果,而不是将它们都丢弃。被编译的包会被保存到$GOPATH/pkg目录下,目录路径和 src目录路径对应,可执行程序被保存到$GOPATH/bin目录。(很多用户会将$GOPATH/bin添加到可执行程序的搜索列表中。)还有,go install命令和go build命令都不会重新编译没有发生变化的包,这可以使后续构建更快捷。为了方便编译依赖的包,go build -i命令将安装每个目标所依赖的包。 因为编译对应不同的操作系统平台和CPU架构,go install命令会将编译结果安装到GOOS和GOARCH对应的目录。例如,在Mac系统,golang.org/x/net/html包将被安装到$GOPATH/pkg/darwin_amd64目录下的golang.org/x/net/html.a文件。 针对不同操作系统或CPU的交叉构建也是很简单的。只需要设置好目标对应的GOOS和GOARCH,然后运行构建命令即可。下面交叉编译的程序将输出它在编译时的操作系统和CPU类型: gopl.io/ch10/cross func main() { fmt.Println(runtime.GOOS, runtime.GOARCH)\n} 下面以64位和32位环境分别编译和执行: $ go build gopl.io/ch10/cross\n$ ./cross\ndarwin amd64\n$ GOARCH=386 go build gopl.io/ch10/cross\n$ ./cross\ndarwin 386 有些包可能需要针对不同平台和处理器类型使用不同版本的代码文件,以便于处理底层的可移植性问题或为一些特定代码提供优化。如果一个文件名包含了一个操作系统或处理器类型名字,例如net_linux.go或asm_amd64.s,Go语言的构建工具将只在对应的平台编译这些文件。还有一个特别的构建注释参数可以提供更多的构建过程控制。例如,文件中可能包含下面的注释: // +build linux darwin 在包声明和包注释的前面,该构建注释参数告诉go build只在编译程序对应的目标操作系统是Linux或Mac OS X时才编译这个文件。下面的构建注释则表示不编译这个文件: // +build ignore 更多细节,可以参考go/build包的构建约束部分的文档。 $ go doc go/build","breadcrumbs":"包和工具 » 工具 » 10.7.3. 构建包","id":"133","title":"10.7.3. 构建包"},"134":{"body":"Go语言的编码风格鼓励为每个包提供良好的文档。包中每个导出的成员和包声明前都应该包含目的和用法说明的注释。 Go语言中的文档注释一般是完整的句子,第一行通常是摘要说明,以被注释者的名字开头。注释中函数的参数或其它的标识符并不需要额外的引号或其它标记注明。例如,下面是fmt.Fprintf的文档注释。 // Fprintf formats according to a format specifier and writes to w.\n// It returns the number of bytes written and any write error encountered.\nfunc Fprintf(w io.Writer, format string, a ...interface{}) (int, error) Fprintf函数格式化的细节在fmt包文档中描述。如果注释后紧跟着包声明语句,那注释对应整个包的文档。包文档对应的注释只能有一个(译注:其实可以有多个,它们会组合成一个包文档注释),包注释可以出现在任何一个源文件中。如果包的注释内容比较长,一般会放到一个独立的源文件中;fmt包注释就有300行之多。这个专门用于保存包文档的源文件通常叫doc.go。 好的文档并不需要面面俱到,文档本身应该是简洁但不可忽略的。事实上,Go语言的风格更喜欢简洁的文档,并且文档也是需要像代码一样维护的。对于一组声明语句,可以用一个精炼的句子描述,如果是显而易见的功能则并不需要注释。 在本书中,只要空间允许,我们之前很多包声明都包含了注释文档,但你可以从标准库中发现很多更好的例子。有两个工具可以帮到你。 首先是go doc命令,该命令打印其后所指定的实体的声明与文档注释,该实体可能是一个包: $ go doc time\npackage time // import \"time\" Package time provides functionality for measuring and displaying time. const Nanosecond Duration = 1 ...\nfunc After(d Duration) <-chan Time\nfunc Sleep(d Duration)\nfunc Since(t Time) Duration\nfunc Now() Time\ntype Duration int64\ntype Time struct { ... }\n...many more... 或者是某个具体的包成员: $ go doc time.Since\nfunc Since(t Time) Duration Since returns the time elapsed since t. It is shorthand for time.Now().Sub(t). 或者是一个方法: $ go doc time.Duration.Seconds\nfunc (d Duration) Seconds() float64 Seconds returns the duration as a floating-point number of seconds. 该命令并不需要输入完整的包导入路径或正确的大小写。下面的命令将打印encoding/json包的(*json.Decoder).Decode方法的文档: $ go doc json.decode\nfunc (dec *Decoder) Decode(v interface{}) error Decode reads the next JSON-encoded value from its input and stores it in the value pointed to by v. 第二个工具,名字也叫godoc,它提供可以相互交叉引用的HTML页面,但是包含和go doc命令相同以及更多的信息。图10.1演示了time包的文档,11.6节将看到godoc演示可以交互的示例程序。godoc的在线服务 https://godoc.org ,包含了成千上万的开源包的检索工具。 你也可以在自己的工作区目录运行godoc服务。运行下面的命令,然后在浏览器查看 http://localhost:8000/pkg 页面: $ godoc -http :8000 其中-analysis=type和-analysis=pointer命令行标志参数用于打开文档和代码中关于静态分析的结果。","breadcrumbs":"包和工具 » 工具 » 10.7.4. 包文档","id":"134","title":"10.7.4. 包文档"},"135":{"body":"在Go语言程序中,包是最重要的封装机制。没有导出的标识符只在同一个包内部可以访问,而导出的标识符则是面向全宇宙都是可见的。 有时候,一个中间的状态可能也是有用的,标识符对于一小部分信任的包是可见的,但并不是对所有调用者都可见。例如,当我们计划将一个大的包拆分为很多小的更容易维护的子包,但是我们并不想将内部的子包结构也完全暴露出去。同时,我们可能还希望在内部子包之间共享一些通用的处理包,或者我们只是想实验一个新包的还并不稳定的接口,暂时只暴露给一些受限制的用户使用。 为了满足这些需求,Go语言的构建工具对包含internal名字的路径段的包导入路径做了特殊处理。这种包叫internal包,一个internal包只能被和internal目录有同一个父目录的包所导入。例如,net/http/internal/chunked内部包只能被net/http/httputil或net/http包导入,但是不能被net/url包导入。不过net/url包却可以导入net/http/httputil包。 net/http\nnet/http/internal/chunked\nnet/http/httputil\nnet/url","breadcrumbs":"包和工具 » 工具 » 10.7.5. 内部包","id":"135","title":"10.7.5. 内部包"},"136":{"body":"go list命令可以查询可用包的信息。其最简单的形式,可以测试包是否在工作区并打印它的导入路径: $ go list github.com/go-sql-driver/mysql\ngithub.com/go-sql-driver/mysql go list命令的参数还可以用\"...\"表示匹配任意的包的导入路径。我们可以用它来列出工作区中的所有包: $ go list ...\narchive/tar\narchive/zip\nbufio\nbytes\ncmd/addr2line\ncmd/api\n...many more... 或者是特定子目录下的所有包: $ go list gopl.io/ch3/...\ngopl.io/ch3/basename1\ngopl.io/ch3/basename2\ngopl.io/ch3/comma\ngopl.io/ch3/mandelbrot\ngopl.io/ch3/netflag\ngopl.io/ch3/printints\ngopl.io/ch3/surface 或者是和某个主题相关的所有包: $ go list ...xml...\nencoding/xml\ngopl.io/ch7/xmlselect go list命令还可以获取每个包完整的元信息,而不仅仅只是导入路径,这些元信息可以以不同格式提供给用户。其中-json命令行参数表示用JSON格式打印每个包的元信息。 $ go list -json hash\n{ \"Dir\": \"/home/gopher/go/src/hash\", \"ImportPath\": \"hash\", \"Name\": \"hash\", \"Doc\": \"Package hash provides interfaces for hash functions.\", \"Target\": \"/home/gopher/go/pkg/darwin_amd64/hash.a\", \"Goroot\": true, \"Standard\": true, \"Root\": \"/home/gopher/go\", \"GoFiles\": [ \"hash.go\" ], \"Imports\": [ \"io\" ], \"Deps\": [ \"errors\", \"io\", \"runtime\", \"sync\", \"sync/atomic\", \"unsafe\" ]\n} 命令行参数-f则允许用户使用text/template包(§4.6)的模板语言定义输出文本的格式。下面的命令将打印strconv包的依赖的包,然后用join模板函数将结果链接为一行,连接时每个结果之间用一个空格分隔: $ go list -f '{{join .Deps \" \"}}' strconv\nerrors math runtime unicode/utf8 unsafe {% endraw %} 译注:上面的命令在Windows的命令行运行会遇到template: main:1: unclosed action的错误。产生这个错误的原因是因为命令行对命令中的\" \"参数进行了转义处理。可以按照下面的方法解决转义字符串的问题: $ go list -f \"{{join .Deps \\\" \\\"}}\" strconv {% endraw %} 下面的命令打印compress子目录下所有包的导入包列表: $ go list -f '{{.ImportPath}} -> {{join .Imports \" \"}}' compress/...\ncompress/bzip2 -> bufio io sort\ncompress/flate -> bufio fmt io math sort strconv\ncompress/gzip -> bufio compress/flate errors fmt hash hash/crc32 io time\ncompress/lzw -> bufio errors fmt io\ncompress/zlib -> bufio compress/flate errors fmt hash hash/adler32 io {% endraw %} 译注:Windows下有同样有问题,要避免转义字符串的干扰: $ go list -f \"{{.ImportPath}} -> {{join .Imports \\\" \\\"}}\" compress/... {% endraw %} go list命令对于一次性的交互式查询或自动化构建或测试脚本都很有帮助。我们将在11.2.4节中再次使用它。每个子命令的更多信息,包括可设置的字段和意义,可以用go help list命令查看。 在本章,我们解释了Go语言工具中除了测试命令之外的所有重要的子命令。在下一章,我们将看到如何用go test命令去运行Go语言程序中的测试代码。 练习 10.4: 创建一个工具,根据命令行指定的参数,报告工作区所有依赖包指定的其它包集合。提示:你需要运行go list命令两次,一次用于初始化包,一次用于所有包。你可能需要用encoding/json(§4.5)包来分析输出的JSON格式的信息。","breadcrumbs":"包和工具 » 工具 » 10.7.6. 查询包","id":"136","title":"10.7.6. 查询包"},"137":{"body":"Maurice Wilkes,第一个存储程序计算机EDSAC的设计者,1949年他在实验室爬楼梯时有一个顿悟。在《计算机先驱回忆录》(Memoirs of a Computer Pioneer)里,他回忆到:“忽然间有一种醍醐灌顶的感觉,我整个后半生的美好时光都将在寻找程序BUG中度过了”。肯定从那之后的大部分正常的码农都会同情Wilkes过分悲观的想法,虽然也许会有人困惑于他对软件开发的难度的天真看法。 现在的程序已经远比Wilkes时代的更大也更复杂,也有许多技术可以让软件的复杂性可得到控制。其中有两种技术在实践中证明是比较有效的。第一种是代码在被正式部署前需要进行代码评审。第二种则是测试,也就是本章的讨论主题。 我们说测试的时候一般是指自动化测试,也就是写一些小的程序用来检测被测试代码(产品代码)的行为和预期的一样,这些通常都是精心设计的执行某些特定的功能或者是通过随机性的输入待验证边界的处理。 软件测试是一个巨大的领域。测试的任务可能已经占据了一些程序员的部分时间和另一些程序员的全部时间。和软件测试技术相关的图书或博客文章有成千上万之多。对于每一种主流的编程语言,都会有一打的用于测试的软件包,同时也有大量的测试相关的理论,而且每种都吸引了大量技术先驱和追随者。这些都足以说服那些想要编写有效测试的程序员重新学习一套全新的技能。 Go语言的测试技术是相对低级的。它依赖一个go test测试命令和一组按照约定方式编写的测试函数,测试命令可以运行这些测试函数。编写相对轻量级的纯测试代码是有效的,而且它很容易延伸到基准测试和示例文档。 在实践中,编写测试代码和编写程序本身并没有多大区别。我们编写的每一个函数也是针对每个具体的任务。我们必须小心处理边界条件,思考合适的数据结构,推断合适的输入应该产生什么样的结果输出。编写测试代码和编写普通的Go代码过程是类似的;它并不需要学习新的符号、规则和工具。","breadcrumbs":"测试 » 第11章 测试","id":"137","title":"第11章 测试"},"138":{"body":"go test命令是一个按照一定的约定和组织来测试代码的程序。在包目录内,所有以_test.go为后缀名的源文件在执行go build时不会被构建成包的一部分,它们是go test测试的一部分。 在*_test.go文件中,有三种类型的函数:测试函数、基准测试(benchmark)函数、示例函数。一个测试函数是以Test为函数名前缀的函数,用于测试程序的一些逻辑行为是否正确;go test命令会调用这些测试函数并报告测试结果是PASS或FAIL。基准测试函数是以Benchmark为函数名前缀的函数,它们用于衡量一些函数的性能;go test命令会多次运行基准测试函数以计算一个平均的执行时间。示例函数是以Example为函数名前缀的函数,提供一个由编译器保证正确性的示例文档。我们将在11.2节讨论测试函数的所有细节,并在11.4节讨论基准测试函数的细节,然后在11.6节讨论示例函数的细节。 go test命令会遍历所有的*_test.go文件中符合上述命名规则的函数,生成一个临时的main包用于调用相应的测试函数,接着构建并运行、报告测试结果,最后清理测试中生成的临时文件。","breadcrumbs":"测试 » go test » 11.1. go test","id":"138","title":"11.1. go test"},"139":{"body":"每个测试函数必须导入testing包。测试函数有如下的签名: func TestName(t *testing.T) { // ...\n} 测试函数的名字必须以Test开头,可选的后缀名必须以大写字母开头: func TestSin(t *testing.T) { /* ... */ }\nfunc TestCos(t *testing.T) { /* ... */ }\nfunc TestLog(t *testing.T) { /* ... */ } 其中t参数用于报告测试失败和附加的日志信息。让我们定义一个实例包gopl.io/ch11/word1,其中只有一个函数IsPalindrome用于检查一个字符串是否从前向后和从后向前读都是一样的。(下面这个实现对于一个字符串是否是回文字符串前后重复测试了两次;我们稍后会再讨论这个问题。) gopl.io/ch11/word1 // Package word provides utilities for word games.\npackage word // IsPalindrome reports whether s reads the same forward and backward.\n// (Our first attempt.)\nfunc IsPalindrome(s string) bool { for i := range s { if s[i] != s[len(s)-1-i] { return false } } return true\n} 在相同的目录下,word_test.go测试文件中包含了TestPalindrome和TestNonPalindrome两个测试函数。每一个都是测试IsPalindrome是否给出正确的结果,并使用t.Error报告失败信息: package word import \"testing\" func TestPalindrome(t *testing.T) { if !IsPalindrome(\"detartrated\") { t.Error(`IsPalindrome(\"detartrated\") = false`) } if !IsPalindrome(\"kayak\") { t.Error(`IsPalindrome(\"kayak\") = false`) }\n} func TestNonPalindrome(t *testing.T) { if IsPalindrome(\"palindrome\") { t.Error(`IsPalindrome(\"palindrome\") = true`) }\n} go test命令如果没有参数指定包那么将默认采用当前目录对应的包(和go build命令一样)。我们可以用下面的命令构建和运行测试。 $ cd $GOPATH/src/gopl.io/ch11/word1\n$ go test\nok gopl.io/ch11/word1 0.008s 结果还比较满意,我们运行了这个程序, 不过没有提前退出是因为还没有遇到BUG报告。不过一个法国名为“Noelle Eve Elleon”的用户会抱怨IsPalindrome函数不能识别“été”。另外一个来自美国中部用户的抱怨则是不能识别“A man, a plan, a canal: Panama.”。执行特殊和小的BUG报告为我们提供了新的更自然的测试用例。 func TestFrenchPalindrome(t *testing.T) { if !IsPalindrome(\"été\") { t.Error(`IsPalindrome(\"été\") = false`) }\n} func TestCanalPalindrome(t *testing.T) { input := \"A man, a plan, a canal: Panama\" if !IsPalindrome(input) { t.Errorf(`IsPalindrome(%q) = false`, input) }\n} 为了避免两次输入较长的字符串,我们使用了提供了有类似Printf格式化功能的 Errorf函数来汇报错误结果。 当添加了这两个测试用例之后,go test返回了测试失败的信息。 $ go test\n--- FAIL: TestFrenchPalindrome (0.00s) word_test.go:28: IsPalindrome(\"été\") = false\n--- FAIL: TestCanalPalindrome (0.00s) word_test.go:35: IsPalindrome(\"A man, a plan, a canal: Panama\") = false\nFAIL\nFAIL gopl.io/ch11/word1 0.014s 先编写测试用例并观察到测试用例触发了和用户报告的错误相同的描述是一个好的测试习惯。只有这样,我们才能定位我们要真正解决的问题。 先写测试用例的另外的好处是,运行测试通常会比手工描述报告的处理更快,这让我们可以进行快速地迭代。如果测试集有很多运行缓慢的测试,我们可以通过只选择运行某些特定的测试来加快测试速度。 参数-v可用于打印每个测试函数的名字和运行时间: $ go test -v\n=== RUN TestPalindrome\n--- PASS: TestPalindrome (0.00s)\n=== RUN TestNonPalindrome\n--- PASS: TestNonPalindrome (0.00s)\n=== RUN TestFrenchPalindrome\n--- FAIL: TestFrenchPalindrome (0.00s) word_test.go:28: IsPalindrome(\"été\") = false\n=== RUN TestCanalPalindrome\n--- FAIL: TestCanalPalindrome (0.00s) word_test.go:35: IsPalindrome(\"A man, a plan, a canal: Panama\") = false\nFAIL\nexit status 1\nFAIL gopl.io/ch11/word1 0.017s 参数-run对应一个正则表达式,只有测试函数名被它正确匹配的测试函数才会被go test测试命令运行: $ go test -v -run=\"French|Canal\"\n=== RUN TestFrenchPalindrome\n--- FAIL: TestFrenchPalindrome (0.00s) word_test.go:28: IsPalindrome(\"été\") = false\n=== RUN TestCanalPalindrome\n--- FAIL: TestCanalPalindrome (0.00s) word_test.go:35: IsPalindrome(\"A man, a plan, a canal: Panama\") = false\nFAIL\nexit status 1\nFAIL gopl.io/ch11/word1 0.014s 当然,一旦我们已经修复了失败的测试用例,在我们提交代码更新之前,我们应该以不带参数的go test命令运行全部的测试用例,以确保修复失败测试的同时没有引入新的问题。 我们现在的任务就是修复这些错误。简要分析后发现第一个BUG的原因是我们采用了 byte而不是rune序列,所以像“été”中的é等非ASCII字符不能正确处理。第二个BUG是因为没有忽略空格和字母的大小写导致的。 针对上述两个BUG,我们仔细重写了函数: gopl.io/ch11/word2 // Package word provides utilities for word games.\npackage word import \"unicode\" // IsPalindrome reports whether s reads the same forward and backward.\n// Letter case is ignored, as are non-letters.\nfunc IsPalindrome(s string) bool { var letters []rune for _, r := range s { if unicode.IsLetter(r) { letters = append(letters, unicode.ToLower(r)) } } for i := range letters { if letters[i] != letters[len(letters)-1-i] { return false } } return true\n} 同时我们也将之前的所有测试数据合并到了一个测试中的表格中。 func TestIsPalindrome(t *testing.T) { var tests = []struct { input string want bool }{ {\"\", true}, {\"a\", true}, {\"aa\", true}, {\"ab\", false}, {\"kayak\", true}, {\"detartrated\", true}, {\"A man, a plan, a canal: Panama\", true}, {\"Evil I did dwell; lewd did I live.\", true}, {\"Able was I ere I saw Elba\", true}, {\"été\", true}, {\"Et se resservir, ivresse reste.\", true}, {\"palindrome\", false}, // non-palindrome {\"desserts\", false}, // semi-palindrome } for _, test := range tests { if got := IsPalindrome(test.input); got != test.want { t.Errorf(\"IsPalindrome(%q) = %v\", test.input, got) } }\n} 现在我们的新测试都通过了: $ go test gopl.io/ch11/word2\nok gopl.io/ch11/word2 0.015s 这种表格驱动的测试在Go语言中很常见。我们可以很容易地向表格添加新的测试数据,并且后面的测试逻辑也没有冗余,这样我们可以有更多的精力去完善错误信息。 失败测试的输出并不包括调用t.Errorf时刻的堆栈调用信息。和其他编程语言或测试框架的assert断言不同,t.Errorf调用也没有引起panic异常或停止测试的执行。即使表格中前面的数据导致了测试的失败,表格后面的测试数据依然会运行测试,因此在一个测试中我们可能了解多个失败的信息。 如果我们真的需要停止测试,或许是因为初始化失败或可能是早先的错误导致了后续错误等原因,我们可以使用t.Fatal或t.Fatalf停止当前测试函数。它们必须在和测试函数同一个goroutine内调用。 测试失败的信息一般的形式是“f(x) = y, want z”,其中f(x)解释了失败的操作和对应的输入,y是实际的运行结果,z是期望的正确的结果。就像前面检查回文字符串的例子,实际的函数用于f(x)部分。显示x是表格驱动型测试中比较重要的部分,因为同一个断言可能对应不同的表格项执行多次。要避免无用和冗余的信息。在测试类似IsPalindrome返回布尔类型的函数时,可以忽略并没有额外信息的z部分。如果x、y或z是y的长度,输出一个相关部分的简明总结即可。测试的作者应该要努力帮助程序员诊断测试失败的原因。 练习 11.1: 为4.3节中的charcount程序编写测试。 练习 11.2: 为(§6.5)的IntSet编写一组测试,用于检查每个操作后的行为和基于内置map的集合等价,后面练习11.7将会用到。","breadcrumbs":"测试 » 测试函数 » 11.2. 测试函数","id":"139","title":"11.2. 测试函数"},"14":{"body":"Go语言的内置库使得写一个类似fetch的web服务器变得异常地简单。在本节中,我们会展示一个微型服务器,这个服务器的功能是返回当前用户正在访问的URL。比如用户访问的是 http://localhost:8000/hello ,那么响应是URL.Path = \"hello\"。 gopl.io/ch1/server1 // Server1 is a minimal \"echo\" server.\npackage main import ( \"fmt\" \"log\" \"net/http\"\n) func main() { http.HandleFunc(\"/\", handler) // each request calls handler log.Fatal(http.ListenAndServe(\"localhost:8000\", nil))\n} // handler echoes the Path component of the request URL r.\nfunc handler(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, \"URL.Path = %q\\n\", r.URL.Path)\n} 我们只用了八九行代码就实现了一个Web服务程序,这都是多亏了标准库里的方法已经帮我们完成了大量工作。main函数将所有发送到/路径下的请求和handler函数关联起来,/开头的请求其实就是所有发送到当前站点上的请求,服务监听8000端口。发送到这个服务的“请求”是一个http.Request类型的对象,这个对象中包含了请求中的一系列相关字段,其中就包括我们需要的URL。当请求到达服务器时,这个请求会被传给handler函数来处理,这个函数会将/hello这个路径从请求的URL中解析出来,然后把其发送到响应中,这里我们用的是标准输出流的fmt.Fprintf。Web服务会在第7.7节中做更详细的阐述。 让我们在后台运行这个服务程序。如果你的操作系统是Mac OS X或者Linux,那么在运行命令的末尾加上一个&符号,即可让程序简单地跑在后台,windows下可以在另外一个命令行窗口去运行这个程序。 $ go run src/gopl.io/ch1/server1/main.go & 现在可以通过命令行来发送客户端请求了: $ go build gopl.io/ch1/fetch\n$ ./fetch http://localhost:8000\nURL.Path = \"/\"\n$ ./fetch http://localhost:8000/help\nURL.Path = \"/help\" 还可以直接在浏览器里访问这个URL,然后得到返回结果,如图1.2: 在这个服务的基础上叠加特性是很容易的。一种比较实用的修改是为访问的url添加某种状态。比如,下面这个版本输出了同样的内容,但是会对请求的次数进行计算;对URL的请求结果会包含各种URL被访问的总次数,直接对/count这个URL的访问要除外。 gopl.io/ch1/server2 // Server2 is a minimal \"echo\" and counter server.\npackage main import ( \"fmt\" \"log\" \"net/http\" \"sync\"\n) var mu sync.Mutex\nvar count int func main() { http.HandleFunc(\"/\", handler) http.HandleFunc(\"/count\", counter) log.Fatal(http.ListenAndServe(\"localhost:8000\", nil))\n} // handler echoes the Path component of the requested URL.\nfunc handler(w http.ResponseWriter, r *http.Request) { mu.Lock() count++ mu.Unlock() fmt.Fprintf(w, \"URL.Path = %q\\n\", r.URL.Path)\n} // counter echoes the number of calls so far.\nfunc counter(w http.ResponseWriter, r *http.Request) { mu.Lock() fmt.Fprintf(w, \"Count %d\\n\", count) mu.Unlock()\n} 这个服务器有两个请求处理函数,根据请求的url不同会调用不同的函数:对/count这个url的请求会调用到counter这个函数,其它的url都会调用默认的处理函数。如果你的请求pattern是以/结尾,那么所有以该url为前缀的url都会被这条规则匹配。在这些代码的背后,服务器每一次接收请求处理时都会另起一个goroutine,这样服务器就可以同一时间处理多个请求。然而在并发情况下,假如真的有两个请求同一时刻去更新count,那么这个值可能并不会被正确地增加;这个程序可能会引发一个严重的bug:竞态条件(参见9.1)。为了避免这个问题,我们必须保证每次修改变量的最多只能有一个goroutine,这也就是代码里的mu.Lock()和mu.Unlock()调用将修改count的所有行为包在中间的目的。第九章中我们会进一步讲解共享变量。 下面是一个更为丰富的例子,handler函数会把请求的http头和请求的form数据都打印出来,这样可以使检查和调试这个服务更为方便: gopl.io/ch1/server3 // handler echoes the HTTP request.\nfunc handler(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, \"%s %s %s\\n\", r.Method, r.URL, r.Proto) for k, v := range r.Header { fmt.Fprintf(w, \"Header[%q] = %q\\n\", k, v) } fmt.Fprintf(w, \"Host = %q\\n\", r.Host) fmt.Fprintf(w, \"RemoteAddr = %q\\n\", r.RemoteAddr) if err := r.ParseForm(); err != nil { log.Print(err) } for k, v := range r.Form { fmt.Fprintf(w, \"Form[%q] = %q\\n\", k, v) }\n} 我们用http.Request这个struct里的字段来输出下面这样的内容: GET /?q=query HTTP/1.1\nHeader[\"Accept-Encoding\"] = [\"gzip, deflate, sdch\"]\nHeader[\"Accept-Language\"] = [\"en-US,en;q=0.8\"]\nHeader[\"Connection\"] = [\"keep-alive\"]\nHeader[\"Accept\"] = [\"text/html,application/xhtml+xml,application/xml;...\"]\nHeader[\"User-Agent\"] = [\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_5)...\"]\nHost = \"localhost:8000\"\nRemoteAddr = \"127.0.0.1:59911\"\nForm[\"q\"] = [\"query\"] 可以看到这里的ParseForm被嵌套在了if语句中。Go语言允许这样的一个简单的语句结果作为局部的变量声明出现在if语句的最前面,这一点对错误处理很有用处。我们还可以像下面这样写(当然看起来就长了一些): err := r.ParseForm()\nif err != nil { log.Print(err)\n} 用if和ParseForm结合可以让代码更加简单,并且可以限制err这个变量的作用域,这么做是很不错的。我们会在2.7节中讲解作用域。 在这些程序中,我们看到了很多不同的类型被输出到标准输出流中。比如前面的fetch程序,把HTTP的响应数据拷贝到了os.Stdout,lissajous程序里我们输出的是一个文件。fetchall程序则完全忽略到了HTTP的响应Body,只是计算了一下响应Body的大小,这个程序中把响应Body拷贝到了ioutil.Discard。在本节的web服务器程序中则是用fmt.Fprintf直接写到了http.ResponseWriter中。 尽管三种具体的实现流程并不太一样,他们都实现一个共同的接口,即当它们被调用需要一个标准流输出时都可以满足。这个接口叫作io.Writer,在7.1节中会详细讨论。 Go语言的接口机制会在第7章中讲解,为了在这里简单说明接口能做什么,让我们简单地将这里的web服务器和之前写的lissajous函数结合起来,这样GIF动画可以被写到HTTP的客户端,而不是之前的标准输出流。只要在web服务器的代码里加入下面这几行。 handler := func(w http.ResponseWriter, r *http.Request) { lissajous(w)\n}\nhttp.HandleFunc(\"/\", handler) 或者另一种等价形式: http.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) { lissajous(w)\n}) HandleFunc函数的第二个参数是一个函数的字面值,也就是一个在使用时定义的匿名函数。这些内容我们会在5.6节中讲解。 做完这些修改之后,在浏览器里访问 http://localhost:8000 。每次你载入这个页面都可以看到一个像图1.3那样的动画。 练习 1.12: 修改Lissajour服务,从URL读取变量,比如你可以访问 http://localhost:8000/?cycles=20 这个URL,这样访问可以将程序里的cycles默认的5修改为20。字符串转换为数字可以调用strconv.Atoi函数。你可以在godoc里查看strconv.Atoi的详细说明。","breadcrumbs":"入门 » Web服务 » 1.7. Web服务","id":"14","title":"1.7. Web服务"},"140":{"body":"表格驱动的测试便于构造基于精心挑选的测试数据的测试用例。另一种测试思路是随机测试,也就是通过构造更广泛的随机输入来测试探索函数的行为。 那么对于一个随机的输入,我们如何能知道希望的输出结果呢?这里有两种处理策略。第一个是编写另一个对照函数,使用简单和清晰的算法,虽然效率较低但是行为和要测试的函数是一致的,然后针对相同的随机输入检查两者的输出结果。第二种是生成的随机输入的数据遵循特定的模式,这样我们就可以知道期望的输出的模式。 下面的例子使用的是第二种方法:randomPalindrome函数用于随机生成回文字符串。 import \"math/rand\" // randomPalindrome returns a palindrome whose length and contents\n// are derived from the pseudo-random number generator rng.\nfunc randomPalindrome(rng *rand.Rand) string { n := rng.Intn(25) // random length up to 24 runes := make([]rune, n) for i := 0; i < (n+1)/2; i++ { r := rune(rng.Intn(0x1000)) // random rune up to '\\u0999' runes[i] = r runes[n-1-i] = r } return string(runes)\n} func TestRandomPalindromes(t *testing.T) { // Initialize a pseudo-random number generator. seed := time.Now().UTC().UnixNano() t.Logf(\"Random seed: %d\", seed) rng := rand.New(rand.NewSource(seed)) for i := 0; i < 1000; i++ { p := randomPalindrome(rng) if !IsPalindrome(p) { t.Errorf(\"IsPalindrome(%q) = false\", p) } }\n} 虽然随机测试会有不确定因素,但是它也是至关重要的,我们可以从失败测试的日志获取足够的信息。在我们的例子中,输入IsPalindrome的p参数将告诉我们真实的数据,但是对于函数将接受更复杂的输入,不需要保存所有的输入,只要日志中简单地记录随机数种子即可(像上面的方式)。有了这些随机数初始化种子,我们可以很容易修改测试代码以重现失败的随机测试。 通过使用当前时间作为随机种子,在整个过程中的每次运行测试命令时都将探索新的随机数据。如果你使用的是定期运行的自动化测试集成系统,随机测试将特别有价值。 练习 11.3: TestRandomPalindromes测试函数只测试了回文字符串。编写新的随机测试生成器,用于测试随机生成的非回文字符串。 练习 11.4: 修改randomPalindrome函数,以探索IsPalindrome是否对标点和空格做了正确处理。 译者注: 拓展阅读 感兴趣的读者可以再了解一下go-fuzz","breadcrumbs":"测试 » 测试函数 » 11.2.1. 随机测试","id":"140","title":"11.2.1. 随机测试"},"141":{"body":"对于测试包go test是一个有用的工具,但是稍加努力我们也可以用它来测试可执行程序。如果一个包的名字是 main,那么在构建时会生成一个可执行程序,不过main包可以作为一个包被测试器代码导入。 让我们为2.3.2节的echo程序编写一个测试。我们先将程序拆分为两个函数:echo函数完成真正的工作,main函数用于处理命令行输入参数和echo可能返回的错误。 gopl.io/ch11/echo // Echo prints its command-line arguments.\npackage main import ( \"flag\" \"fmt\" \"io\" \"os\" \"strings\"\n) var ( n = flag.Bool(\"n\", false, \"omit trailing newline\") s = flag.String(\"s\", \" \", \"separator\")\n) var out io.Writer = os.Stdout // modified during testing func main() { flag.Parse() if err := echo(!*n, *s, flag.Args()); err != nil { fmt.Fprintf(os.Stderr, \"echo: %v\\n\", err) os.Exit(1) }\n} func echo(newline bool, sep string, args []string) error { fmt.Fprint(out, strings.Join(args, sep)) if newline { fmt.Fprintln(out) } return nil\n} 在测试中我们可以用各种参数和标志调用echo函数,然后检测它的输出是否正确,我们通过增加参数来减少echo函数对全局变量的依赖。我们还增加了一个全局名为out的变量来替代直接使用os.Stdout,这样测试代码可以根据需要将out修改为不同的对象以便于检查。下面就是echo_test.go文件中的测试代码: package main import ( \"bytes\" \"fmt\" \"testing\"\n) func TestEcho(t *testing.T) { var tests = []struct { newline bool sep string args []string want string }{ {true, \"\", []string{}, \"\\n\"}, {false, \"\", []string{}, \"\"}, {true, \"\\t\", []string{\"one\", \"two\", \"three\"}, \"one\\ttwo\\tthree\\n\"}, {true, \",\", []string{\"a\", \"b\", \"c\"}, \"a,b,c\\n\"}, {false, \":\", []string{\"1\", \"2\", \"3\"}, \"1:2:3\"}, } for _, test := range tests { descr := fmt.Sprintf(\"echo(%v, %q, %q)\", test.newline, test.sep, test.args) out = new(bytes.Buffer) // captured output if err := echo(test.newline, test.sep, test.args); err != nil { t.Errorf(\"%s failed: %v\", descr, err) continue } got := out.(*bytes.Buffer).String() if got != test.want { t.Errorf(\"%s = %q, want %q\", descr, got, test.want) } }\n} 要注意的是测试代码和产品代码在同一个包。虽然是main包,也有对应的main入口函数,但是在测试的时候main包只是TestEcho测试函数导入的一个普通包,里面main函数并没有被导出,而是被忽略的。 通过将测试放到表格中,我们很容易添加新的测试用例。让我通过增加下面的测试用例来看看失败的情况是怎么样的: {true, \",\", []string{\"a\", \"b\", \"c\"}, \"a b c\\n\"}, // NOTE: wrong expectation! go test输出如下: $ go test gopl.io/ch11/echo\n--- FAIL: TestEcho (0.00s) echo_test.go:31: echo(true, \",\", [\"a\" \"b\" \"c\"]) = \"a,b,c\", want \"a b c\\n\"\nFAIL\nFAIL gopl.io/ch11/echo 0.006s 错误信息描述了尝试的操作(使用Go类似语法),实际的结果和期望的结果。通过这样的错误信息,你可以在检视代码之前就很容易定位错误的原因。 要注意的是在测试代码中并没有调用log.Fatal或os.Exit,因为调用这类函数会导致程序提前退出;调用这些函数的特权应该放在main函数中。如果真的有意外的事情导致函数发生panic异常,测试驱动应该尝试用recover捕获异常,然后将当前测试当作失败处理。如果是可预期的错误,例如非法的用户输入、找不到文件或配置文件不当等应该通过返回一个非空的error的方式处理。幸运的是(上面的意外只是一个插曲),我们的echo示例是比较简单的也没有需要返回非空error的情况。","breadcrumbs":"测试 » 测试函数 » 11.2.2. 测试一个命令","id":"141","title":"11.2.2. 测试一个命令"},"142":{"body":"一种测试分类的方法是基于测试者是否需要了解被测试对象的内部工作原理。黑盒测试只需要测试包公开的文档和API行为,内部实现对测试代码是透明的。相反,白盒测试有访问包内部函数和数据结构的权限,因此可以做到一些普通客户端无法实现的测试。例如,一个白盒测试可以在每个操作之后检测不变量的数据类型。(白盒测试只是一个传统的名称,其实称为clear box测试会更准确。) 黑盒和白盒这两种测试方法是互补的。黑盒测试一般更健壮,随着软件实现的完善测试代码很少需要更新。它们可以帮助测试者了解真实客户的需求,也可以帮助发现API设计的一些不足之处。相反,白盒测试则可以对内部一些棘手的实现提供更多的测试覆盖。 我们已经看到两种测试的例子。TestIsPalindrome测试仅仅使用导出的IsPalindrome函数,因此这是一个黑盒测试。TestEcho测试则调用了内部的echo函数,并且更新了内部的out包级变量,这两个都是未导出的,因此这是白盒测试。 当我们准备TestEcho测试的时候,我们修改了echo函数使用包级的out变量作为输出对象,因此测试代码可以用另一个实现代替标准输出,这样可以方便对比echo输出的数据。使用类似的技术,我们可以将产品代码的其他部分也替换为一个容易测试的伪对象。使用伪对象的好处是我们可以方便配置,容易预测,更可靠,也更容易观察。同时也可以避免一些不良的副作用,例如更新生产数据库或信用卡消费行为。 下面的代码演示了为用户提供网络存储的web服务中的配额检测逻辑。当用户使用了超过90%的存储配额之后将发送提醒邮件。(译注:一般在实现业务机器监控,包括磁盘、cpu、网络等的时候,需要类似的到达阈值=>触发报警的逻辑,所以是很实用的案例。) gopl.io/ch11/storage1 package storage import ( \"fmt\" \"log\" \"net/smtp\"\n) func bytesInUse(username string) int64 { return 0 /* ... */ } // Email sender configuration.\n// NOTE: never put passwords in source code!\nconst sender = \"notifications@example.com\"\nconst password = \"correcthorsebatterystaple\"\nconst hostname = \"smtp.example.com\" const template = `Warning: you are using %d bytes of storage,\n%d%% of your quota.` func CheckQuota(username string) { used := bytesInUse(username) const quota = 1000000000 // 1GB percent := 100 * used / quota if percent < 90 { return // OK } msg := fmt.Sprintf(template, used, percent) auth := smtp.PlainAuth(\"\", sender, password, hostname) err := smtp.SendMail(hostname+\":587\", auth, sender, []string{username}, []byte(msg)) if err != nil { log.Printf(\"smtp.SendMail(%s) failed: %s\", username, err) }\n} 我们想测试这段代码,但是我们并不希望发送真实的邮件。因此我们将邮件处理逻辑放到一个私有的notifyUser函数中。 gopl.io/ch11/storage2 var notifyUser = func(username, msg string) { auth := smtp.PlainAuth(\"\", sender, password, hostname) err := smtp.SendMail(hostname+\":587\", auth, sender, []string{username}, []byte(msg)) if err != nil { log.Printf(\"smtp.SendEmail(%s) failed: %s\", username, err) }\n} func CheckQuota(username string) { used := bytesInUse(username) const quota = 1000000000 // 1GB percent := 100 * used / quota if percent < 90 { return // OK } msg := fmt.Sprintf(template, used, percent) notifyUser(username, msg)\n} 现在我们可以在测试中用伪邮件发送函数替代真实的邮件发送函数。它只是简单记录要通知的用户和邮件的内容。 package storage import ( \"strings\" \"testing\"\n)\nfunc TestCheckQuotaNotifiesUser(t *testing.T) { var notifiedUser, notifiedMsg string notifyUser = func(user, msg string) { notifiedUser, notifiedMsg = user, msg } // ...simulate a 980MB-used condition... const user = \"joe@example.org\" CheckQuota(user) if notifiedUser == \"\" && notifiedMsg == \"\" { t.Fatalf(\"notifyUser not called\") } if notifiedUser != user { t.Errorf(\"wrong user (%s) notified, want %s\", notifiedUser, user) } const wantSubstring = \"98% of your quota\" if !strings.Contains(notifiedMsg, wantSubstring) { t.Errorf(\"unexpected notification message <<%s>>, \"+ \"want substring %q\", notifiedMsg, wantSubstring) }\n} 这里有一个问题:当测试函数返回后,CheckQuota将不能正常工作,因为notifyUsers依然使用的是测试函数的伪发送邮件函数(当更新全局对象的时候总会有这种风险)。 我们必须修改测试代码恢复notifyUsers原先的状态以便后续其他的测试没有影响,要确保所有的执行路径后都能恢复,包括测试失败或panic异常的情形。在这种情况下,我们建议使用defer语句来延后执行处理恢复的代码。 func TestCheckQuotaNotifiesUser(t *testing.T) { // Save and restore original notifyUser. saved := notifyUser defer func() { notifyUser = saved }() // Install the test's fake notifyUser. var notifiedUser, notifiedMsg string notifyUser = func(user, msg string) { notifiedUser, notifiedMsg = user, msg } // ...rest of test...\n} 这种处理模式可以用来暂时保存和恢复所有的全局变量,包括命令行标志参数、调试选项和优化参数;安装和移除导致生产代码产生一些调试信息的钩子函数;还有有些诱导生产代码进入某些重要状态的改变,比如超时、错误,甚至是一些刻意制造的并发行为等因素。 以这种方式使用全局变量是安全的,因为go test命令并不会同时并发地执行多个测试。","breadcrumbs":"测试 » 测试函数 » 11.2.3. 白盒测试","id":"142","title":"11.2.3. 白盒测试"},"143":{"body":"考虑下这两个包:net/url包,提供了URL解析的功能;net/http包,提供了web服务和HTTP客户端的功能。如我们所料,上层的net/http包依赖下层的net/url包。然后,net/url包中的一个测试是演示不同URL和HTTP客户端的交互行为。也就是说,一个下层包的测试代码导入了上层的包。 这样的行为在net/url包的测试代码中会导致包的循环依赖,正如图11.1中向上箭头所示,同时正如我们在10.1节所讲的,Go语言规范是禁止包的循环依赖的。 不过我们可以通过外部测试包的方式解决循环依赖的问题,也就是在net/url包所在的目录声明一个独立的url_test测试包。其中包名的_test后缀告诉go test工具它应该建立一个额外的包来运行测试。我们将这个外部测试包的导入路径视作是net/url_test会更容易理解,但实际上它并不能被其他任何包导入。 因为外部测试包是一个独立的包,所以能够导入那些依赖待测代码本身的其他辅助包;包内的测试代码就无法做到这点。在设计层面,外部测试包是在所有它依赖的包的上层,正如图11.2所示。 通过避免循环的导入依赖,外部测试包可以更灵活地编写测试,特别是集成测试(需要测试多个组件之间的交互),可以像普通应用程序那样自由地导入其他包。 我们可以用go list命令查看包对应目录中哪些Go源文件是产品代码,哪些是包内测试,还有哪些是外部测试包。我们以fmt包作为一个例子:GoFiles表示产品代码对应的Go源文件列表;也就是go build命令要编译的部分。 $ go list -f={{.GoFiles}} fmt\n[doc.go format.go print.go scan.go] {% endraw %} TestGoFiles表示的是fmt包内部测试代码,以_test.go为后缀文件名,不过只在测试时被构建: $ go list -f={{.TestGoFiles}} fmt\n[export_test.go] {% endraw %} 包的测试代码通常都在这些文件中,不过fmt包并非如此;稍后我们再解释export_test.go文件的作用。 XTestGoFiles表示的是属于外部测试包的测试代码,也就是fmt_test包,因此它们必须先导入fmt包。同样,这些文件也只是在测试时被构建运行: $ go list -f={{.XTestGoFiles}} fmt\n[fmt_test.go scan_test.go stringer_test.go] {% endraw %} 有时候外部测试包也需要访问被测试包内部的代码,例如在一个为了避免循环导入而被独立到外部测试包的白盒测试。在这种情况下,我们可以通过一些技巧解决:我们在包内的一个_test.go文件中导出一个内部的实现给外部测试包。因为这些代码只有在测试时才需要,因此一般会放在export_test.go文件中。 例如,fmt包的fmt.Scanf函数需要unicode.IsSpace函数提供的功能。但是为了避免太多的依赖,fmt包并没有导入包含巨大表格数据的unicode包;相反fmt包有一个叫isSpace内部的简易实现。 为了确保fmt.isSpace和unicode.IsSpace函数的行为保持一致,fmt包谨慎地包含了一个测试。一个在外部测试包内的白盒测试,是无法直接访问到isSpace内部函数的,因此fmt通过一个后门导出了isSpace函数。export_test.go文件就是专门用于外部测试包的后门。 package fmt var IsSpace = isSpace 这个测试文件并没有定义测试代码;它只是通过fmt.IsSpace简单导出了内部的isSpace函数,提供给外部测试包使用。这个技巧可以广泛用于位于外部测试包的白盒测试。","breadcrumbs":"测试 » 测试函数 » 11.2.4. 外部测试包","id":"143","title":"11.2.4. 外部测试包"},"144":{"body":"许多Go语言新人会惊异于Go语言极简的测试框架。很多其它语言的测试框架都提供了识别测试函数的机制(通常使用反射或元数据),通过设置一些“setup”和“teardown”的钩子函数来执行测试用例运行的初始化和之后的清理操作,同时测试工具箱还提供了很多类似assert断言、值比较函数、格式化输出错误信息和停止一个失败的测试等辅助函数(通常使用异常机制)。虽然这些机制可以使得测试非常简洁,但是测试输出的日志却会像火星文一般难以理解。此外,虽然测试最终也会输出PASS或FAIL的报告,但是它们提供的信息格式却非常不利于代码维护者快速定位问题,因为失败信息的具体含义非常隐晦,比如“assert: 0 == 1”或成页的海量跟踪日志。 Go语言的测试风格则形成鲜明对比。它期望测试者自己完成大部分的工作,定义函数避免重复,就像普通编程那样。编写测试并不是一个机械的填空过程;一个测试也有自己的接口,尽管它的维护者也是测试仅有的一个用户。一个好的测试不应该引发其他无关的错误信息,它只要清晰简洁地描述问题的症状即可,有时候可能还需要一些上下文信息。在理想情况下,维护者可以在不看代码的情况下就能根据错误信息定位错误产生的原因。一个好的测试不应该在遇到一点小错误时就立刻退出测试,它应该尝试报告更多的相关的错误信息,因为我们可能从多个失败测试的模式中发现错误产生的规律。 下面的断言函数比较两个值,然后生成一个通用的错误信息,并停止程序。它很好用也确实有效,但是当测试失败的时候,打印的错误信息却几乎是没有价值的。它并没有为快速解决问题提供一个很好的入口。 import ( \"fmt\" \"strings\" \"testing\"\n)\n// A poor assertion function.\nfunc assertEqual(x, y int) { if x != y { panic(fmt.Sprintf(\"%d != %d\", x, y)) }\n}\nfunc TestSplit(t *testing.T) { words := strings.Split(\"a:b:c\", \":\") assertEqual(len(words), 3) // ...\n} 从这个意义上说,断言函数犯了过早抽象的错误:仅仅测试两个整数是否相同,而没能根据上下文提供更有意义的错误信息。我们可以根据具体的错误打印一个更有价值的错误信息,就像下面例子那样。只有在测试中出现重复模式时才采用抽象。 func TestSplit(t *testing.T) { s, sep := \"a:b:c\", \":\" words := strings.Split(s, sep) if got, want := len(words), 3; got != want { t.Errorf(\"Split(%q, %q) returned %d words, want %d\", s, sep, got, want) } // ...\n} 现在的测试不仅报告了调用的具体函数、它的输入和结果的意义;并且打印的真实返回的值和期望返回的值;并且即使断言失败依然会继续尝试运行更多的测试。一旦我们写了这样结构的测试,下一步自然不是用更多的if语句来扩展测试用例,我们可以用像IsPalindrome的表驱动测试那样来准备更多的s和sep测试用例。 前面的例子并不需要额外的辅助函数,如果有可以使测试代码更简单的方法我们也乐意接受。(我们将在13.3节看到一个类似reflect.DeepEqual辅助函数。)一个好的测试的关键是首先实现你期望的具体行为,然后才是考虑简化测试代码、避免重复。如果直接从抽象、通用的测试库着手,很难取得良好结果。 练习11.5: 用表格驱动的技术扩展TestSplit测试,并打印期望的输出结果。","breadcrumbs":"测试 » 测试函数 » 11.2.5. 编写有效的测试","id":"144","title":"11.2.5. 编写有效的测试"},"145":{"body":"如果一个应用程序对于新出现的但有效的输入经常失败说明程序容易出bug(不够稳健);同样,如果一个测试仅仅对程序做了微小变化就失败则称为脆弱。就像一个不够稳健的程序会挫败它的用户一样,一个脆弱的测试同样会激怒它的维护者。最脆弱的测试代码会在程序没有任何变化的时候产生不同的结果,时好时坏,处理它们会耗费大量的时间但是并不会得到任何好处。 当一个测试函数会产生一个复杂的输出如一个很长的字符串、一个精心设计的数据结构或一个文件时,人们很容易想预先写下一系列固定的用于对比的标杆数据。但是随着项目的发展,有些输出可能会发生变化,尽管很可能是一个改进的实现导致的。而且不仅仅是输出部分,函数复杂的输入部分可能也跟着变化了,因此测试使用的输入也就不再有效了。 避免脆弱测试代码的方法是只检测你真正关心的属性。保持测试代码的简洁和内部结构的稳定。特别是对断言部分要有所选择。不要对字符串进行全字匹配,而是针对那些在项目的发展中是比较稳定不变的子串。很多时候值得花力气来编写一个从复杂输出中提取用于断言的必要信息的函数,虽然这可能会带来很多前期的工作,但是它可以帮助迅速及时修复因为项目演化而导致的不合逻辑的失败测试。","breadcrumbs":"测试 » 测试函数 » 11.2.6. 避免脆弱的测试","id":"145","title":"11.2.6. 避免脆弱的测试"},"146":{"body":"就其性质而言,测试不可能是完整的。计算机科学家Edsger Dijkstra曾说过:“测试能证明缺陷存在,而无法证明没有缺陷。”再多的测试也不能证明一个程序没有BUG。在最好的情况下,测试可以增强我们的信心:代码在很多重要场景下是可以正常工作的。 对待测程序执行的测试的程度称为测试的覆盖率。测试覆盖率并不能量化——即使最简单的程序的动态也是难以精确测量的——但是有启发式方法来帮助我们编写有效的测试代码。 这些启发式方法中,语句的覆盖率是最简单和最广泛使用的。语句的覆盖率是指在测试中至少被运行一次的代码占总代码数的比例。在本节中,我们使用go test命令中集成的测试覆盖率工具,来度量下面代码的测试覆盖率,帮助我们识别测试和我们期望间的差距。 下面的代码是一个表格驱动的测试,用于测试第七章的表达式求值程序: gopl.io/ch7/eval func TestCoverage(t *testing.T) { var tests = []struct { input string env Env want string // expected error from Parse/Check or result from Eval }{ {\"x % 2\", nil, \"unexpected '%'\"}, {\"!true\", nil, \"unexpected '!'\"}, {\"log(10)\", nil, `unknown function \"log\"`}, {\"sqrt(1, 2)\", nil, \"call to sqrt has 2 args, want 1\"}, {\"sqrt(A / pi)\", Env{\"A\": 87616, \"pi\": math.Pi}, \"167\"}, {\"pow(x, 3) + pow(y, 3)\", Env{\"x\": 9, \"y\": 10}, \"1729\"}, {\"5 / 9 * (F - 32)\", Env{\"F\": -40}, \"-40\"}, } for _, test := range tests { expr, err := Parse(test.input) if err == nil { err = expr.Check(map[Var]bool{}) } if err != nil { if err.Error() != test.want { t.Errorf(\"%s: got %q, want %q\", test.input, err, test.want) } continue } got := fmt.Sprintf(\"%.6g\", expr.Eval(test.env)) if got != test.want { t.Errorf(\"%s: %v => %s, want %s\", test.input, test.env, got, test.want) } }\n} 首先,我们要确保所有的测试都正常通过: $ go test -v -run=Coverage gopl.io/ch7/eval\n=== RUN TestCoverage\n--- PASS: TestCoverage (0.00s)\nPASS\nok gopl.io/ch7/eval 0.011s 下面这个命令可以显示测试覆盖率工具的使用用法: $ go tool cover\nUsage of 'go tool cover':\nGiven a coverage profile produced by 'go test': go test -coverprofile=c.out Open a web browser displaying annotated source code: go tool cover -html=c.out\n... go tool命令运行Go工具链的底层可执行程序。这些底层可执行程序放在$GOROOT/pkg/tool/${GOOS}_${GOARCH}目录。因为有go build命令的原因,我们很少直接调用这些底层工具。 现在我们可以用-coverprofile标志参数重新运行测试: $ go test -run=Coverage -coverprofile=c.out gopl.io/ch7/eval\nok gopl.io/ch7/eval 0.032s coverage: 68.5% of statements 这个标志参数通过在测试代码中插入生成钩子来统计覆盖率数据。也就是说,在运行每个测试前,它将待测代码拷贝一份并做修改,在每个词法块都会设置一个布尔标志变量。当被修改后的被测试代码运行退出时,将统计日志数据写入c.out文件,并打印一部分执行的语句的一个总结。(如果你需要的是摘要,使用go test -cover。) 如果使用了-covermode=count标志参数,那么将在每个代码块插入一个计数器而不是布尔标志量。在统计结果中记录了每个块的执行次数,这可以用于衡量哪些是被频繁执行的热点代码。 为了收集数据,我们运行了测试覆盖率工具,打印了测试日志,生成一个HTML报告,然后在浏览器中打开(图11.3)。 $ go tool cover -html=c.out 绿色的代码块被测试覆盖到了,红色的则表示没有被覆盖到。为了清晰起见,我们将背景红色文本的背景设置成了阴影效果。我们可以马上发现unary操作的Eval方法并没有被执行到。如果我们针对这部分未被覆盖的代码添加下面的测试用例,然后重新运行上面的命令,那么我们将会看到那个红色部分的代码也变成绿色了: {\"-x * -x\", eval.Env{\"x\": 2}, \"4\"} 不过两个panic语句依然是红色的。这是没有问题的,因为这两个语句并不会被执行到。 实现100%的测试覆盖率听起来很美,但是在具体实践中通常是不可行的,也不是值得推荐的做法。因为那只能说明代码被执行过而已,并不意味着代码就是没有BUG的;因为对于逻辑复杂的语句需要针对不同的输入执行多次。有一些语句,例如上面的panic语句则永远都不会被执行到。另外,还有一些隐晦的错误在现实中很少遇到也很难编写对应的测试代码。测试从本质上来说是一个比较务实的工作,编写测试代码和编写应用代码的成本对比是需要考虑的。测试覆盖率工具可以帮助我们快速识别测试薄弱的地方,但是设计好的测试用例和编写应用代码一样需要严密的思考。","breadcrumbs":"测试 » 测试覆盖率 » 11.3. 测试覆盖率","id":"146","title":"11.3. 测试覆盖率"},"147":{"body":"基准测试是测量一个程序在固定工作负载下的性能。在Go语言中,基准测试函数和普通测试函数写法类似,但是以Benchmark为前缀名,并且带有一个*testing.B类型的参数;*testing.B参数除了提供和*testing.T类似的方法,还有额外一些和性能测量相关的方法。它还提供了一个整数N,用于指定操作执行的循环次数。 下面是IsPalindrome函数的基准测试,其中循环将执行N次。 import \"testing\" func BenchmarkIsPalindrome(b *testing.B) { for i := 0; i < b.N; i++ { IsPalindrome(\"A man, a plan, a canal: Panama\") }\n} 我们用下面的命令运行基准测试。和普通测试不同的是,默认情况下不运行任何基准测试。我们需要通过-bench命令行标志参数手工指定要运行的基准测试函数。该参数是一个正则表达式,用于匹配要执行的基准测试函数的名字,默认值是空的。其中“.”模式将可以匹配所有基准测试函数,但因为这里只有一个基准测试函数,因此和-bench=IsPalindrome参数是等价的效果。 $ cd $GOPATH/src/gopl.io/ch11/word2\n$ go test -bench=.\nPASS\nBenchmarkIsPalindrome-8 1000000 1035 ns/op\nok gopl.io/ch11/word2 2.179s 结果中基准测试名的数字后缀部分,这里是8,表示运行时对应的GOMAXPROCS的值,这对于一些与并发相关的基准测试是重要的信息。 报告显示每次调用IsPalindrome函数花费1.035微秒,是执行1,000,000次的平均时间。因为基准测试驱动器开始时并不知道每个基准测试函数运行所花的时间,它会尝试在真正运行基准测试前先尝试用较小的N运行测试来估算基准测试函数所需要的时间,然后推断一个较大的时间保证稳定的测量结果。 循环在基准测试函数内实现,而不是放在基准测试框架内实现,这样可以让每个基准测试函数有机会在循环启动前执行初始化代码,这样并不会显著影响每次迭代的平均运行时间。如果还是担心初始化代码部分对测量时间带来干扰,那么可以通过testing.B参数提供的方法来临时关闭或重置计时器,不过这些一般很少会用到。 现在我们有了一个基准测试和普通测试,我们可以很容易测试改进程序运行速度的想法。也许最明显的优化是在IsPalindrome函数中第二个循环的停止检查,这样可以避免每个比较都做两次: n := len(letters)/2\nfor i := 0; i < n; i++ { if letters[i] != letters[len(letters)-1-i] { return false }\n}\nreturn true 不过很多情况下,一个显而易见的优化未必能带来预期的效果。这个改进在基准测试中只带来了4%的性能提升。 $ go test -bench=.\nPASS\nBenchmarkIsPalindrome-8 1000000 992 ns/op\nok gopl.io/ch11/word2 2.093s 另一个改进想法是在开始为每个字符预先分配一个足够大的数组,这样就可以避免在append调用时可能会导致内存的多次重新分配。声明一个letters数组变量,并指定合适的大小,像下面这样, letters := make([]rune, 0, len(s))\nfor _, r := range s { if unicode.IsLetter(r) { letters = append(letters, unicode.ToLower(r)) }\n} 这个改进提升性能约35%,报告结果是基于2,000,000次迭代的平均运行时间统计。 $ go test -bench=.\nPASS\nBenchmarkIsPalindrome-8 2000000 697 ns/op\nok gopl.io/ch11/word2 1.468s 如这个例子所示,快的程序往往是伴随着较少的内存分配。-benchmem命令行标志参数将在报告中包含内存的分配数据统计。我们可以比较优化前后内存的分配情况: $ go test -bench=. -benchmem\nPASS\nBenchmarkIsPalindrome 1000000 1026 ns/op 304 B/op 4 allocs/op 这是优化之后的结果: $ go test -bench=. -benchmem\nPASS\nBenchmarkIsPalindrome 2000000 807 ns/op 128 B/op 1 allocs/op 用一次内存分配代替多次的内存分配节省了75%的分配调用次数和减少近一半的内存需求。 这个基准测试告诉了我们某个具体操作所需的绝对时间,但我们往往想知道的是两个不同的操作的时间对比。例如,如果一个函数需要1ms处理1,000个元素,那么处理10000或1百万将需要多少时间呢?这样的比较揭示了渐近增长函数的运行时间。另一个例子:I/O缓存该设置为多大呢?基准测试可以帮助我们选择在性能达标情况下所需的最小内存。第三个例子:对于一个确定的工作哪种算法更好?基准测试可以评估两种不同算法对于相同的输入在不同的场景和负载下的优缺点。 比较型的基准测试就是普通程序代码。它们通常是单参数的函数,由几个不同数量级的基准测试函数调用,就像这样: func benchmark(b *testing.B, size int) { /* ... */ }\nfunc Benchmark10(b *testing.B) { benchmark(b, 10) }\nfunc Benchmark100(b *testing.B) { benchmark(b, 100) }\nfunc Benchmark1000(b *testing.B) { benchmark(b, 1000) } 通过函数参数来指定输入的大小,但是参数变量对于每个具体的基准测试都是固定的。要避免直接修改b.N来控制输入的大小。除非你将它作为一个固定大小的迭代计算输入,否则基准测试的结果将毫无意义。 比较型的基准测试反映出的模式在程序设计阶段是很有帮助的,但是即使程序完工了也应当保留基准测试代码。因为随着项目的发展,或者是输入的增加,或者是部署到新的操作系统或不同的处理器,我们可以再次用基准测试来帮助我们改进设计。 练习 11.6: 为2.6.2节的练习2.4和练习2.5的PopCount函数编写基准测试。看看基于表格算法在不同情况下对提升性能会有多大帮助。 练习 11.7: 为*IntSet(§6.5)的Add、UnionWith和其他方法编写基准测试,使用大量随机输入。你可以让这些方法跑多快?选择字的大小对于性能的影响如何?IntSet和基于内建map的实现相比有多快?","breadcrumbs":"测试 » 基准测试 » 11.4. 基准测试","id":"147","title":"11.4. 基准测试"},"148":{"body":"基准测试(Benchmark)对于衡量特定操作的性能是有帮助的,但是当我们试图让程序跑的更快的时候,我们通常并不知道从哪里开始优化。每个码农都应该知道Donald Knuth在1974年的“Structured Programming with go to Statements”上所说的格言。虽然经常被解读为不重视性能的意思,但是从原文我们可以看到不同的含义: 毫无疑问,对效率的片面追求会导致各种滥用。程序员会浪费大量的时间在非关键程序的速度上,实际上这些尝试提升效率的行为反倒可能产生很大的负面影响,特别是当调试和维护的时候。我们不应该过度纠结于细节的优化,应该说约97%的场景:过早的优化是万恶之源。 当然我们也不应该放弃对那关键3%的优化。一个好的程序员不会因为这个比例小就裹足不前,他们会明智地观察和识别哪些是关键的代码;但是仅当关键代码已经被确认的前提下才会进行优化。对于很多程序员来说,判断哪部分是关键的性能瓶颈,是很容易犯经验上的错误的,因此一般应该借助测量工具来证明。 当我们想仔细观察我们程序的运行速度的时候,最好的方法是性能剖析。剖析技术是基于程序执行期间一些自动抽样,然后在收尾时进行推断;最后产生的统计结果就称为剖析数据。 Go语言支持多种类型的剖析性能分析,每一种关注不同的方面,但它们都涉及到每个采样记录的感兴趣的一系列事件消息,每个事件都包含函数调用时函数调用堆栈的信息。内建的go test工具对几种分析方式都提供了支持。 CPU剖析数据标识了最耗CPU时间的函数。在每个CPU上运行的线程在每隔几毫秒都会遇到操作系统的中断事件,每次中断时都会记录一个剖析数据然后恢复正常的运行。 堆剖析则标识了最耗内存的语句。剖析库会记录调用内部内存分配的操作,平均每512KB的内存申请会触发一个剖析数据。 阻塞剖析则记录阻塞goroutine最久的操作,例如系统调用、管道发送和接收,还有获取锁等。每当goroutine被这些操作阻塞时,剖析库都会记录相应的事件。 只需要开启下面其中一个标志参数就可以生成各种分析文件。当同时使用多个标志参数时需要当心,因为一项分析操作可能会影响其他项的分析结果。 $ go test -cpuprofile=cpu.out\n$ go test -blockprofile=block.out\n$ go test -memprofile=mem.out 对于一些非测试程序也很容易进行剖析,具体的实现方式,与程序是短时间运行的小工具还是长时间运行的服务会有很大不同。剖析对于长期运行的程序尤其有用,因此可以通过调用Go的runtime API来启用运行时剖析。 一旦我们已经收集到了用于分析的采样数据,我们就可以使用pprof来分析这些数据。这是Go工具箱自带的一个工具,但并不是一个日常工具,它对应go tool pprof命令。该命令有许多特性和选项,但是最基本的是两个参数:生成这个概要文件的可执行程序和对应的剖析数据。 为了提高分析效率和减少空间,分析日志本身并不包含函数的名字;它只包含函数对应的地址。也就是说pprof需要对应的可执行程序来解读剖析数据。虽然go test通常在测试完成后就丢弃临时用的测试程序,但是在启用分析的时候会将测试程序保存为foo.test文件,其中foo部分对应待测包的名字。 下面的命令演示了如何收集并展示一个CPU分析文件。我们选择net/http包的一个基准测试为例。通常最好是对业务关键代码的部分设计专门的基准测试。因为简单的基准测试几乎没法代表业务场景,因此我们用-run=NONE参数禁止那些简单测试。 $ go test -run=NONE -bench=ClientServerParallelTLS64 \\ -cpuprofile=cpu.log net/http PASS BenchmarkClientServerParallelTLS64-8 1000 3141325 ns/op 143010 B/op 1747 allocs/op\nok net/http 3.395s $ go tool pprof -text -nodecount=10 ./http.test cpu.log\n2570ms of 3590ms total (71.59%)\nDropped 129 nodes (cum <= 17.95ms)\nShowing top 10 nodes out of 166 (cum >= 60ms) flat flat% sum% cum cum% 1730ms 48.19% 48.19% 1750ms 48.75% crypto/elliptic.p256ReduceDegree 230ms 6.41% 54.60% 250ms 6.96% crypto/elliptic.p256Diff 120ms 3.34% 57.94% 120ms 3.34% math/big.addMulVVW 110ms 3.06% 61.00% 110ms 3.06% syscall.Syscall 90ms 2.51% 63.51% 1130ms 31.48% crypto/elliptic.p256Square 70ms 1.95% 65.46% 120ms 3.34% runtime.scanobject 60ms 1.67% 67.13% 830ms 23.12% crypto/elliptic.p256Mul 60ms 1.67% 68.80% 190ms 5.29% math/big.nat.montgomery 50ms 1.39% 70.19% 50ms 1.39% crypto/elliptic.p256ReduceCarry 50ms 1.39% 71.59% 60ms 1.67% crypto/elliptic.p256Sum 参数-text用于指定输出格式,在这里每行是一个函数,根据使用CPU的时间长短来排序。其中-nodecount=10参数限制了只输出前10行的结果。对于严重的性能问题,这个文本格式基本可以帮助查明原因了。 这个概要文件告诉我们,HTTPS基准测试中crypto/elliptic.p256ReduceDegree函数占用了将近一半的CPU资源,对性能占很大比重。相比之下,如果一个概要文件中主要是runtime包的内存分配的函数,那么减少内存消耗可能是一个值得尝试的优化策略。 对于一些更微妙的问题,你可能需要使用pprof的图形显示功能。这个需要安装GraphViz工具,可以从 http://www.graphviz.org 下载。参数-web用于生成函数的有向图,标注有CPU的使用和最热点的函数等信息。 这一节我们只是简单看了下Go语言的数据分析工具。如果想了解更多,可以阅读Go官方博客的“Profiling Go Programs”一文。","breadcrumbs":"测试 » 剖析 » 11.5. 剖析","id":"148","title":"11.5. 剖析"},"149":{"body":"第三种被go test特别对待的函数是示例函数,以Example为函数名开头。示例函数没有函数参数和返回值。下面是IsPalindrome函数对应的示例函数: func ExampleIsPalindrome() { fmt.Println(IsPalindrome(\"A man, a plan, a canal: Panama\")) fmt.Println(IsPalindrome(\"palindrome\")) // Output: // true // false\n} 示例函数有三个用处。最主要的一个是作为文档:一个包的例子可以更简洁直观的方式来演示函数的用法,比文字描述更直接易懂,特别是作为一个提醒或快速参考时。一个示例函数也可以方便展示属于同一个接口的几种类型或函数之间的关系,所有的文档都必须关联到一个地方,就像一个类型或函数声明都统一到包一样。同时,示例函数和注释并不一样,示例函数是真实的Go代码,需要接受编译器的编译时检查,这样可以保证源代码更新时,示例代码不会脱节。 根据示例函数的后缀名部分,godoc这个web文档服务器会将示例函数关联到某个具体函数或包本身,因此ExampleIsPalindrome示例函数将是IsPalindrome函数文档的一部分,Example示例函数将是包文档的一部分。 示例函数的第二个用处是,在go test执行测试的时候也会运行示例函数测试。如果示例函数内含有类似上面例子中的// Output:格式的注释,那么测试工具会执行这个示例函数,然后检查示例函数的标准输出与注释是否匹配。 示例函数的第三个目的提供一个真实的演练场。 http://golang.org 就是由godoc提供的文档服务,它使用了Go Playground让用户可以在浏览器中在线编辑和运行每个示例函数,就像图11.4所示的那样。这通常是学习函数使用或Go语言特性最快捷的方式。 本书最后的两章是讨论reflect和unsafe包,一般的Go程序员很少使用它们,事实上也很少需要用到。因此,如果你还没有写过任何真实的Go程序的话,现在可以先去写些代码了。","breadcrumbs":"测试 » 示例函数 » 11.6. 示例函数","id":"149","title":"11.6. 示例函数"},"15":{"body":"本章对Go语言做了一些介绍,Go语言很多方面在有限的篇幅中无法覆盖到。本节会把没有讲到的内容也做一些简单的介绍,这样读者在读到完整的内容之前,可以有个简单的印象。 控制流: 在本章我们只介绍了if控制和for,但是没有提到switch多路选择。这里是一个简单的switch的例子: switch coinflip() {\ncase \"heads\": heads++\ncase \"tails\": tails++\ndefault: fmt.Println(\"landed on edge!\")\n} 在翻转硬币的时候,例子里的coinflip函数返回几种不同的结果,每一个case都会对应一个返回结果,这里需要注意,Go语言并不需要显式地在每一个case后写break,语言默认执行完case后的逻辑语句会自动退出。当然了,如果你想要相邻的几个case都执行同一逻辑的话,需要自己显式地写上一个fallthrough语句来覆盖这种默认行为。不过fallthrough语句在一般的程序中很少用到。 Go语言里的switch还可以不带操作对象(译注:switch不带操作对象时默认用true值代替,然后将每个case的表达式和true值进行比较);可以直接罗列多种条件,像其它语言里面的多个if else一样,下面是一个例子: func Signum(x int) int { switch { case x > 0: return +1 default: return 0 case x < 0: return -1 }\n} 这种形式叫做无tag switch(tagless switch);这和switch true是等价的。 像for和if控制语句一样,switch也可以紧跟一个简短的变量声明,一个自增表达式、赋值语句,或者一个函数调用(译注:比其它语言丰富)。 break和continue语句会改变控制流。和其它语言中的break和continue一样,break会中断当前的循环,并开始执行循环之后的内容,而continue会跳过当前循环,并开始执行下一次循环。这两个语句除了可以控制for循环,还可以用来控制switch和select语句(之后会讲到),在1.3节中我们看到,continue会跳过内层的循环,如果我们想跳过的是更外层的循环的话,我们可以在相应的位置加上label,这样break和continue就可以根据我们的想法来continue和break任意循环。这看起来甚至有点像goto语句的作用了。当然,一般程序员也不会用到这种操作。这两种行为更多地被用到机器生成的代码中。 命名类型: 类型声明使得我们可以很方便地给一个特殊类型一个名字。因为struct类型声明通常非常地长,所以我们总要给这种struct取一个名字。本章中就有这样一个例子,二维点类型: type Point struct { X, Y int\n}\nvar p Point 类型声明和命名类型会在第二章中介绍。 指针: Go语言提供了指针。指针是一种直接存储了变量的内存地址的数据类型。在其它语言中,比如C语言,指针操作是完全不受约束的。在另外一些语言中,指针一般被处理为“引用”,除了到处传递这些指针之外,并不能对这些指针做太多事情。Go语言在这两种范围中取了一种平衡。指针是可见的内存地址,&操作符可以返回一个变量的内存地址,并且*操作符可以获取指针指向的变量内容,但是在Go语言里没有指针运算,也就是不能像c语言里可以对指针进行加或减操作。我们会在2.3.2中进行详细介绍。 方法和接口: 方法是和命名类型关联的一类函数。Go语言里比较特殊的是方法可以被关联到任意一种命名类型。在第六章我们会详细地讲方法。接口是一种抽象类型,这种类型可以让我们以同样的方式来处理不同的固有类型,不用关心它们的具体实现,而只需要关注它们提供的方法。第七章中会详细说明这些内容。 包(packages): Go语言提供了一些很好用的package,并且这些package是可以扩展的。Go语言社区已经创造并且分享了很多很多。所以Go语言编程大多数情况下就是用已有的package来写我们自己的代码。通过这本书,我们会讲解一些重要的标准库内的package,但是还是有很多限于篇幅没有去说明,因为我们没法在这样的厚度的书里去做一部代码大全。 在你开始写一个新程序之前,最好先去检查一下是不是已经有了现成的库可以帮助你更高效地完成这件事情。你可以在 https://golang.org/pkg 和 https://godoc.org 中找到标准库和社区写的package。godoc这个工具可以让你直接在本地命令行阅读标准库的文档。比如下面这个例子。 $ go doc http.ListenAndServe\npackage http // import \"net/http\"\nfunc ListenAndServe(addr string, handler Handler) error ListenAndServe listens on the TCP network address addr and then calls Serve with handler to handle requests on incoming connections.\n... 注释: 我们之前已经提到过了在源文件的开头写的注释是这个源文件的文档。在每一个函数之前写一个说明函数行为的注释也是一个好习惯。这些惯例很重要,因为这些内容会被像godoc这样的工具检测到,并且在执行命令时显示这些注释。具体可以参考10.7.4。 多行注释可以用 /* ... */ 来包裹,和其它大多数语言一样。在文件一开头的注释一般都是这种形式,或者一大段的解释性的注释文字也会被这符号包住,来避免每一行都需要加//。在注释中//和/*是没什么意义的,所以不要在注释中再嵌入注释。","breadcrumbs":"入门 » 本章要点 » 1.8. 本章要点","id":"15","title":"1.8. 本章要点"},"150":{"body":"Go语言提供了一种机制,能够在运行时更新变量和检查它们的值、调用它们的方法和它们支持的内在操作,而不需要在编译时就知道这些变量的具体类型。这种机制被称为反射。反射也可以让我们将类型本身作为第一类的值类型处理。 在本章,我们将探讨Go语言的反射特性,看看它可以给语言增加哪些表达力,以及在两个至关重要的API是如何使用反射机制的:一个是fmt包提供的字符串格式化功能,另一个是类似encoding/json和encoding/xml提供的针对特定协议的编解码功能。对于我们在4.6节中看到过的text/template和html/template包,它们的实现也是依赖反射技术的。然后,反射是一个复杂的内省技术,不应该随意使用,因此,尽管上面这些包内部都是用反射技术实现的,但是它们自己的API都没有公开反射相关的接口。","breadcrumbs":"反射 » 第12章 反射","id":"150","title":"第12章 反射"},"151":{"body":"有时候我们需要编写一个函数能够处理一类并不满足普通公共接口的类型的值,也可能是因为它们并没有确定的表示方式,或者是在我们设计该函数的时候这些类型可能还不存在。 一个大家熟悉的例子是fmt.Fprintf函数提供的字符串格式化处理逻辑,它可以用来对任意类型的值格式化并打印,甚至支持用户自定义的类型。让我们也来尝试实现一个类似功能的函数。为了简单起见,我们的函数只接收一个参数,然后返回和fmt.Sprint类似的格式化后的字符串。我们实现的函数名也叫Sprint。 我们首先用switch类型分支来测试输入参数是否实现了String方法,如果是的话就调用该方法。然后继续增加类型测试分支,检查这个值的动态类型是否是string、int、bool等基础类型,并在每种情况下执行相应的格式化操作。 func Sprint(x interface{}) string { type stringer interface { String() string } switch x := x.(type) { case stringer: return x.String() case string: return x case int: return strconv.Itoa(x) // ...similar cases for int16, uint32, and so on... case bool: if x { return \"true\" } return \"false\" default: // array, chan, func, map, pointer, slice, struct return \"???\" }\n} 但是我们如何处理其它类似[]float64、map[string][]string等类型呢?我们当然可以添加更多的测试分支,但是这些组合类型的数目基本是无穷的。还有如何处理类似url.Values这样的具名类型呢?即使类型分支可以识别出底层的基础类型是map[string][]string,但是它并不匹配url.Values类型,因为它们是两种不同的类型,而且switch类型分支也不可能包含每个类似url.Values的类型,这会导致对这些库的依赖。 没有办法来检查未知类型的表示方式,我们被卡住了。这就是我们需要反射的原因。","breadcrumbs":"反射 » 为何需要反射? » 12.1. 为何需要反射?","id":"151","title":"12.1. 为何需要反射?"},"152":{"body":"反射是由 reflect 包提供的。它定义了两个重要的类型,Type 和 Value。一个 Type 表示一个Go类型。它是一个接口,有许多方法来区分类型以及检查它们的组成部分,例如一个结构体的成员或一个函数的参数等。唯一能反映 reflect.Type 实现的是接口的类型描述信息(§7.5),也正是这个实体标识了接口值的动态类型。 函数 reflect.TypeOf 接受任意的 interface{} 类型,并以 reflect.Type 形式返回其动态类型: t := reflect.TypeOf(3) // a reflect.Type\nfmt.Println(t.String()) // \"int\"\nfmt.Println(t) // \"int\" 其中 TypeOf(3) 调用将值 3 传给 interface{} 参数。回到 7.5节 的将一个具体的值转为接口类型会有一个隐式的接口转换操作,它会创建一个包含两个信息的接口值:操作数的动态类型(这里是 int)和它的动态的值(这里是 3)。 因为 reflect.TypeOf 返回的是一个动态类型的接口值,它总是返回具体的类型。因此,下面的代码将打印 \"*os.File\" 而不是 \"io.Writer\"。稍后,我们将看到能够表达接口类型的 reflect.Type。 var w io.Writer = os.Stdout\nfmt.Println(reflect.TypeOf(w)) // \"*os.File\" 要注意的是 reflect.Type 接口是满足 fmt.Stringer 接口的。因为打印一个接口的动态类型对于调试和日志是有帮助的, fmt.Printf 提供了一个缩写 %T 参数,内部使用 reflect.TypeOf 来输出: fmt.Printf(\"%T\\n\", 3) // \"int\" reflect 包中另一个重要的类型是 Value。一个 reflect.Value 可以装载任意类型的值。函数 reflect.ValueOf 接受任意的 interface{} 类型,并返回一个装载着其动态值的 reflect.Value。和 reflect.TypeOf 类似,reflect.ValueOf 返回的结果也是具体的类型,但是 reflect.Value 也可以持有一个接口值。 v := reflect.ValueOf(3) // a reflect.Value\nfmt.Println(v) // \"3\"\nfmt.Printf(\"%v\\n\", v) // \"3\"\nfmt.Println(v.String()) // NOTE: \"\" 和 reflect.Type 类似,reflect.Value 也满足 fmt.Stringer 接口,但是除非 Value 持有的是字符串,否则 String 方法只返回其类型。而使用 fmt 包的 %v 标志参数会对 reflect.Values 特殊处理。 对 Value 调用 Type 方法将返回具体类型所对应的 reflect.Type: t := v.Type() // a reflect.Type\nfmt.Println(t.String()) // \"int\" reflect.ValueOf 的逆操作是 reflect.Value.Interface 方法。它返回一个 interface{} 类型,装载着与 reflect.Value 相同的具体值: v := reflect.ValueOf(3) // a reflect.Value\nx := v.Interface() // an interface{}\ni := x.(int) // an int\nfmt.Printf(\"%d\\n\", i) // \"3\" reflect.Value 和 interface{} 都能装载任意的值。所不同的是,一个空的接口隐藏了值内部的表示方式和所有方法,因此只有我们知道具体的动态类型才能使用类型断言来访问内部的值(就像上面那样),内部值我们没法访问。相比之下,一个 Value 则有很多方法来检查其内容,无论它的具体类型是什么。让我们再次尝试实现我们的格式化函数 format.Any。 我们使用 reflect.Value 的 Kind 方法来替代之前的类型 switch。虽然还是有无穷多的类型,但是它们的 kinds 类型却是有限的:Bool、String 和 所有数字类型的基础类型;Array 和 Struct 对应的聚合类型;Chan、Func、Ptr、Slice 和 Map 对应的引用类型;interface 类型;还有表示空值的 Invalid 类型。(空的 reflect.Value 的 kind 即为 Invalid。) gopl.io/ch12/format package format import ( \"reflect\" \"strconv\"\n) // Any formats any value as a string.\nfunc Any(value interface{}) string { return formatAtom(reflect.ValueOf(value))\n} // formatAtom formats a value without inspecting its internal structure.\nfunc formatAtom(v reflect.Value) string { switch v.Kind() { case reflect.Invalid: return \"invalid\" case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: return strconv.FormatInt(v.Int(), 10) case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: return strconv.FormatUint(v.Uint(), 10) // ...floating-point and complex cases omitted for brevity... case reflect.Bool: return strconv.FormatBool(v.Bool()) case reflect.String: return strconv.Quote(v.String()) case reflect.Chan, reflect.Func, reflect.Ptr, reflect.Slice, reflect.Map: return v.Type().String() + \" 0x\" + strconv.FormatUint(uint64(v.Pointer()), 16) default: // reflect.Array, reflect.Struct, reflect.Interface return v.Type().String() + \" value\" }\n} 到目前为止,我们的函数将每个值视作一个不可分割没有内部结构的物品,因此它叫 formatAtom。对于聚合类型(结构体和数组)和接口,只是打印值的类型,对于引用类型(channels、functions、pointers、slices 和 maps),打印类型和十六进制的引用地址。虽然还不够理想,但是依然是一个重大的进步,并且 Kind 只关心底层表示,format.Any 也支持具名类型。例如: var x int64 = 1\nvar d time.Duration = 1 * time.Nanosecond\nfmt.Println(format.Any(x)) // \"1\"\nfmt.Println(format.Any(d)) // \"1\"\nfmt.Println(format.Any([]int64{x})) // \"[]int64 0x8202b87b0\"\nfmt.Println(format.Any([]time.Duration{d})) // \"[]time.Duration 0x8202b87e0\"","breadcrumbs":"反射 » reflect.Type和reflect.Value » 12.2. reflect.Type 和 reflect.Value","id":"152","title":"12.2. reflect.Type 和 reflect.Value"},"153":{"body":"接下来,让我们看看如何改善聚合数据类型的显示。我们并不想完全克隆一个fmt.Sprint函数,我们只是构建一个用于调试用的Display函数:给定任意一个复杂类型 x,打印这个值对应的完整结构,同时标记每个元素的发现路径。让我们从一个例子开始。 e, _ := eval.Parse(\"sqrt(A / pi)\")\nDisplay(\"e\", e) 在上面的调用中,传入Display函数的参数是在7.9节一个表达式求值函数返回的语法树。Display函数的输出如下: Display e (eval.call):\ne.fn = \"sqrt\"\ne.args[0].type = eval.binary\ne.args[0].value.op = 47\ne.args[0].value.x.type = eval.Var\ne.args[0].value.x.value = \"A\"\ne.args[0].value.y.type = eval.Var\ne.args[0].value.y.value = \"pi\" 你应该尽量避免在一个包的API中暴露涉及反射的接口。我们将定义一个未导出的display函数用于递归处理工作,导出的是Display函数,它只是display函数简单的包装以接受interface{}类型的参数: gopl.io/ch12/display func Display(name string, x interface{}) { fmt.Printf(\"Display %s (%T):\\n\", name, x) display(name, reflect.ValueOf(x))\n} 在display函数中,我们使用了前面定义的打印基础类型——基本类型、函数和chan等——元素值的formatAtom函数,但是我们会使用reflect.Value的方法来递归显示复杂类型的每一个成员。在递归下降过程中,path字符串,从最开始传入的起始值(这里是“e”),将逐步增长来表示是如何达到当前值(例如“e.args[0].value”)的。 因为我们不再模拟fmt.Sprint函数,我们将直接使用fmt包来简化我们的例子实现。 func display(path string, v reflect.Value) { switch v.Kind() { case reflect.Invalid: fmt.Printf(\"%s = invalid\\n\", path) case reflect.Slice, reflect.Array: for i := 0; i < v.Len(); i++ { display(fmt.Sprintf(\"%s[%d]\", path, i), v.Index(i)) } case reflect.Struct: for i := 0; i < v.NumField(); i++ { fieldPath := fmt.Sprintf(\"%s.%s\", path, v.Type().Field(i).Name) display(fieldPath, v.Field(i)) } case reflect.Map: for _, key := range v.MapKeys() { display(fmt.Sprintf(\"%s[%s]\", path, formatAtom(key)), v.MapIndex(key)) } case reflect.Ptr: if v.IsNil() { fmt.Printf(\"%s = nil\\n\", path) } else { display(fmt.Sprintf(\"(*%s)\", path), v.Elem()) } case reflect.Interface: if v.IsNil() { fmt.Printf(\"%s = nil\\n\", path) } else { fmt.Printf(\"%s.type = %s\\n\", path, v.Elem().Type()) display(path+\".value\", v.Elem()) } default: // basic types, channels, funcs fmt.Printf(\"%s = %s\\n\", path, formatAtom(v)) }\n} 让我们针对不同类型分别讨论。 Slice和数组: 两种的处理逻辑是一样的。Len方法返回slice或数组值中的元素个数,Index(i)获得索引i对应的元素,返回的也是一个reflect.Value;如果索引i超出范围的话将导致panic异常,这与数组或slice类型内建的len(a)和a[i]操作类似。display针对序列中的每个元素递归调用自身处理,我们通过在递归处理时向path附加“[i]”来表示访问路径。 虽然reflect.Value类型带有很多方法,但是只有少数的方法能对任意值都安全调用。例如,Index方法只能对Slice、数组或字符串类型的值调用,如果对其它类型调用则会导致panic异常。 结构体: NumField方法报告结构体中成员的数量,Field(i)以reflect.Value类型返回第i个成员的值。成员列表也包括通过匿名字段提升上来的成员。为了在path添加“.f”来表示成员路径,我们必须获得结构体对应的reflect.Type类型信息,然后访问结构体第i个成员的名字。 Maps: MapKeys方法返回一个reflect.Value类型的slice,每一个元素对应map的一个key。和往常一样,遍历map时顺序是随机的。MapIndex(key)返回map中key对应的value。我们向path添加“[key]”来表示访问路径。(我们这里有一个未完成的工作。其实map的key的类型并不局限于formatAtom能完美处理的类型;数组、结构体和接口都可以作为map的key。针对这种类型,完善key的显示信息是练习12.1的任务。) 指针: Elem方法返回指针指向的变量,依然是reflect.Value类型。即使指针是nil,这个操作也是安全的,在这种情况下指针是Invalid类型,但是我们可以用IsNil方法来显式地测试一个空指针,这样我们可以打印更合适的信息。我们在path前面添加“*”,并用括弧包含以避免歧义。 接口: 再一次,我们使用IsNil方法来测试接口是否是nil,如果不是,我们可以调用v.Elem()来获取接口对应的动态值,并且打印对应的类型和值。 现在我们的Display函数总算完工了,让我们看看它的表现吧。下面的Movie类型是在4.5节的电影类型上演变来的: type Movie struct { Title, Subtitle string Year int Color bool Actor map[string]string Oscars []string Sequel *string\n} 让我们声明一个该类型的变量,然后看看Display函数如何显示它: strangelove := Movie{ Title: \"Dr. Strangelove\", Subtitle: \"How I Learned to Stop Worrying and Love the Bomb\", Year: 1964, Color: false, Actor: map[string]string{ \"Dr. Strangelove\": \"Peter Sellers\", \"Grp. Capt. Lionel Mandrake\": \"Peter Sellers\", \"Pres. Merkin Muffley\": \"Peter Sellers\", \"Gen. Buck Turgidson\": \"George C. Scott\", \"Brig. Gen. Jack D. Ripper\": \"Sterling Hayden\", `Maj. T.J. \"King\" Kong`: \"Slim Pickens\", }, Oscars: []string{ \"Best Actor (Nomin.)\", \"Best Adapted Screenplay (Nomin.)\", \"Best Director (Nomin.)\", \"Best Picture (Nomin.)\", },\n} Display(\"strangelove\", strangelove)调用将显示(strangelove电影对应的中文名是《奇爱博士》): Display strangelove (display.Movie):\nstrangelove.Title = \"Dr. Strangelove\"\nstrangelove.Subtitle = \"How I Learned to Stop Worrying and Love the Bomb\"\nstrangelove.Year = 1964\nstrangelove.Color = false\nstrangelove.Actor[\"Gen. Buck Turgidson\"] = \"George C. Scott\"\nstrangelove.Actor[\"Brig. Gen. Jack D. Ripper\"] = \"Sterling Hayden\"\nstrangelove.Actor[\"Maj. T.J. \\\"King\\\" Kong\"] = \"Slim Pickens\"\nstrangelove.Actor[\"Dr. Strangelove\"] = \"Peter Sellers\"\nstrangelove.Actor[\"Grp. Capt. Lionel Mandrake\"] = \"Peter Sellers\"\nstrangelove.Actor[\"Pres. Merkin Muffley\"] = \"Peter Sellers\"\nstrangelove.Oscars[0] = \"Best Actor (Nomin.)\"\nstrangelove.Oscars[1] = \"Best Adapted Screenplay (Nomin.)\"\nstrangelove.Oscars[2] = \"Best Director (Nomin.)\"\nstrangelove.Oscars[3] = \"Best Picture (Nomin.)\"\nstrangelove.Sequel = nil 我们也可以使用Display函数来显示标准库中类型的内部结构,例如*os.File类型: Display(\"os.Stderr\", os.Stderr)\n// Output:\n// Display os.Stderr (*os.File):\n// (*(*os.Stderr).file).fd = 2\n// (*(*os.Stderr).file).name = \"/dev/stderr\"\n// (*(*os.Stderr).file).nepipe = 0 可以看出,反射能够访问到结构体中未导出的成员。需要当心的是这个例子的输出在不同操作系统上可能是不同的,并且随着标准库的发展也可能导致结果不同。(这也是将这些成员定义为私有成员的原因之一!)我们甚至可以用Display函数来显示reflect.Value 的内部构造(在这里设置为*os.File的类型描述体)。Display(\"rV\", reflect.ValueOf(os.Stderr))调用的输出如下,当然不同环境得到的结果可能有差异: Display rV (reflect.Value):\n(*rV.typ).size = 8\n(*rV.typ).hash = 871609668\n(*rV.typ).align = 8\n(*rV.typ).fieldAlign = 8\n(*rV.typ).kind = 22\n(*(*rV.typ).string) = \"*os.File\" (*(*(*rV.typ).uncommonType).methods[0].name) = \"Chdir\"\n(*(*(*(*rV.typ).uncommonType).methods[0].mtyp).string) = \"func() error\"\n(*(*(*(*rV.typ).uncommonType).methods[0].typ).string) = \"func(*os.File) error\"\n... 观察下面两个例子的区别: var i interface{} = 3 Display(\"i\", i)\n// Output:\n// Display i (int):\n// i = 3 Display(\"&i\", &i)\n// Output:\n// Display &i (*interface {}):\n// (*&i).type = int\n// (*&i).value = 3 在第一个例子中,Display函数调用reflect.ValueOf(i),它返回一个Int类型的值。正如我们在12.2节中提到的,reflect.ValueOf总是返回一个具体类型的 Value,因为它是从一个接口值提取的内容。 在第二个例子中,Display函数调用的是reflect.ValueOf(&i),它返回一个指向i的指针,对应Ptr类型。在switch的Ptr分支中,对这个值调用 Elem 方法,返回一个Value来表示变量 i 本身,对应Interface类型。像这样一个间接获得的Value,可能代表任意类型的值,包括接口类型。display函数递归调用自身,这次它分别打印了这个接口的动态类型和值。 对于目前的实现,如果遇到对象图中含有回环,Display将会陷入死循环,例如下面这个首尾相连的链表: // a struct that points to itself\ntype Cycle struct{ Value int; Tail *Cycle }\nvar c Cycle\nc = Cycle{42, &c}\nDisplay(\"c\", c) Display会永远不停地进行深度递归打印: Display c (display.Cycle):\nc.Value = 42\n(*c.Tail).Value = 42\n(*(*c.Tail).Tail).Value = 42\n(*(*(*c.Tail).Tail).Tail).Value = 42\n...ad infinitum... 许多Go语言程序都包含了一些循环的数据。让Display支持这类带环的数据结构需要些技巧,需要额外记录迄今访问的路径;相应会带来成本。通用的解决方案是采用 unsafe 的语言特性,我们将在13.3节看到具体的解决方案。 带环的数据结构很少会对fmt.Sprint函数造成问题,因为它很少尝试打印完整的数据结构。例如,当它遇到一个指针的时候,它只是简单地打印指针的数字值。在打印包含自身的slice或map时可能卡住,但是这种情况很罕见,不值得付出为了处理回环所需的开销。 练习 12.1: 扩展Display函数,使它可以显示包含以结构体或数组作为map的key类型的值。 练习 12.2: 增强display函数的稳健性,通过记录边界的步数来确保在超出一定限制后放弃递归。(在13.3节,我们会看到另一种探测数据结构是否存在环的技术。)","breadcrumbs":"反射 » Display递归打印 » 12.3. Display,一个递归的值打印器","id":"153","title":"12.3. Display,一个递归的值打印器"},"154":{"body":"Display是一个用于显示结构化数据的调试工具,但是它并不能将任意的Go语言对象编码为通用消息然后用于进程间通信。 正如我们在4.5节中中看到的,Go语言的标准库支持了包括JSON、XML和ASN.1等多种编码格式。还有另一种依然被广泛使用的格式是S表达式格式,采用Lisp语言的语法。但是和其他编码格式不同的是,Go语言自带的标准库并不支持S表达式,主要是因为它没有一个公认的标准规范。 在本节中,我们将定义一个包用于将任意的Go语言对象编码为S表达式格式,它支持以下结构: 42 integer\n\"hello\" string(带有Go风格的引号)\nfoo symbol(未用引号括起来的名字)\n(1 2 3) list (括号包起来的0个或多个元素) 布尔型习惯上使用t符号表示true,空列表或nil符号表示false,但是为了简单起见,我们暂时忽略布尔类型。同时忽略的还有chan管道和函数,因为通过反射并无法知道它们的确切状态。我们忽略的还有浮点数、复数和interface。支持它们是练习12.3的任务。 我们将Go语言的类型编码为S表达式的方法如下。整数和字符串以显而易见的方式编码。空值编码为nil符号。数组和slice被编码为列表。 结构体被编码为成员对象的列表,每个成员对象对应一个有两个元素的子列表,子列表的第一个元素是成员的名字,第二个元素是成员的值。Map被编码为键值对的列表。传统上,S表达式使用点状符号列表(key . value)结构来表示key/value对,而不是用一个含双元素的列表,不过为了简单我们忽略了点状符号列表。 编码是由一个encode递归函数完成,如下所示。它的结构本质上和前面的Display函数类似: gopl.io/ch12/sexpr func encode(buf *bytes.Buffer, v reflect.Value) error { switch v.Kind() { case reflect.Invalid: buf.WriteString(\"nil\") case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: fmt.Fprintf(buf, \"%d\", v.Int()) case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: fmt.Fprintf(buf, \"%d\", v.Uint()) case reflect.String: fmt.Fprintf(buf, \"%q\", v.String()) case reflect.Ptr: return encode(buf, v.Elem()) case reflect.Array, reflect.Slice: // (value ...) buf.WriteByte('(') for i := 0; i < v.Len(); i++ { if i > 0 { buf.WriteByte(' ') } if err := encode(buf, v.Index(i)); err != nil { return err } } buf.WriteByte(')') case reflect.Struct: // ((name value) ...) buf.WriteByte('(') for i := 0; i < v.NumField(); i++ { if i > 0 { buf.WriteByte(' ') } fmt.Fprintf(buf, \"(%s \", v.Type().Field(i).Name) if err := encode(buf, v.Field(i)); err != nil { return err } buf.WriteByte(')') } buf.WriteByte(')') case reflect.Map: // ((key value) ...) buf.WriteByte('(') for i, key := range v.MapKeys() { if i > 0 { buf.WriteByte(' ') } buf.WriteByte('(') if err := encode(buf, key); err != nil { return err } buf.WriteByte(' ') if err := encode(buf, v.MapIndex(key)); err != nil { return err } buf.WriteByte(')') } buf.WriteByte(')') default: // float, complex, bool, chan, func, interface return fmt.Errorf(\"unsupported type: %s\", v.Type()) } return nil\n} Marshal函数是对encode的包装,以保持和encoding/...下其它包有着相似的API: // Marshal encodes a Go value in S-expression form.\nfunc Marshal(v interface{}) ([]byte, error) { var buf bytes.Buffer if err := encode(&buf, reflect.ValueOf(v)); err != nil { return nil, err } return buf.Bytes(), nil\n} 下面是Marshal对12.3节的strangelove变量编码后的结果: ((Title \"Dr. Strangelove\") (Subtitle \"How I Learned to Stop Worrying and Lo\nve the Bomb\") (Year 1964) (Actor ((\"Grp. Capt. Lionel Mandrake\" \"Peter Sell\ners\") (\"Pres. Merkin Muffley\" \"Peter Sellers\") (\"Gen. Buck Turgidson\" \"Geor\nge C. Scott\") (\"Brig. Gen. Jack D. Ripper\" \"Sterling Hayden\") (\"Maj. T.J. \\\n\"King\\\" Kong\" \"Slim Pickens\") (\"Dr. Strangelove\" \"Peter Sellers\"))) (Oscars\n(\"Best Actor (Nomin.)\" \"Best Adapted Screenplay (Nomin.)\" \"Best Director (N\nomin.)\" \"Best Picture (Nomin.)\")) (Sequel nil)) 整个输出编码为一行中以减少输出的大小,但是也很难阅读。下面是对S表达式手动格式化的结果。编写一个S表达式的美化格式化函数将作为一个具有挑战性的练习任务;不过 http://gopl.io 也提供了一个简单的版本。 ((Title \"Dr. Strangelove\") (Subtitle \"How I Learned to Stop Worrying and Love the Bomb\") (Year 1964) (Actor ((\"Grp. Capt. Lionel Mandrake\" \"Peter Sellers\") (\"Pres. Merkin Muffley\" \"Peter Sellers\") (\"Gen. Buck Turgidson\" \"George C. Scott\") (\"Brig. Gen. Jack D. Ripper\" \"Sterling Hayden\") (\"Maj. T.J. \\\"King\\\" Kong\" \"Slim Pickens\") (\"Dr. Strangelove\" \"Peter Sellers\"))) (Oscars (\"Best Actor (Nomin.)\" \"Best Adapted Screenplay (Nomin.)\" \"Best Director (Nomin.)\" \"Best Picture (Nomin.)\")) (Sequel nil)) 和fmt.Print、json.Marshal、Display函数类似,sexpr.Marshal函数处理带环的数据结构也会陷入死循环。 在12.6节中,我们将给出S表达式解码器的实现步骤,但是在那之前,我们还需要先了解如何通过反射技术来更新程序的变量。 练习 12.3: 实现encode函数缺少的分支。将布尔类型编码为t和nil,浮点数编码为Go语言的格式,复数1+2i编码为#C(1.0 2.0)格式。接口编码为类型名和值对,例如(\"[]int\" (1 2 3)),但是这个形式可能会造成歧义:reflect.Type.String方法对于不同的类型可能返回相同的结果。 练习 12.4: 修改encode函数,以上面的格式化形式输出S表达式。 练习 12.5: 修改encode函数,用JSON格式代替S表达式格式。然后使用标准库提供的json.Unmarshal解码器来验证函数是正确的。 练习 12.6: 修改encode,作为一个优化,忽略对是零值对象的编码。 练习 12.7: 创建一个基于流式的API,用于S表达式的解码,和json.Decoder(§4.5)函数功能类似。","breadcrumbs":"反射 » 示例: 编码S表达式 » 12.4. 示例: 编码为S表达式","id":"154","title":"12.4. 示例: 编码为S表达式"},"155":{"body":"到目前为止,反射还只是程序中变量的另一种读取方式。然而,在本节中我们将重点讨论如何通过反射机制来修改变量。 回想一下,Go语言中类似x、x.f[1]和*p形式的表达式都可以表示变量,但是其它如x + 1和f(2)则不是变量。一个变量就是一个可寻址的内存空间,里面存储了一个值,并且存储的值可以通过内存地址来更新。 对于reflect.Values也有类似的区别。有一些reflect.Values是可取地址的;其它一些则不可以。考虑以下的声明语句: x := 2 // value type variable?\na := reflect.ValueOf(2) // 2 int no\nb := reflect.ValueOf(x) // 2 int no\nc := reflect.ValueOf(&x) // &x *int no\nd := c.Elem() // 2 int yes (x) 其中a对应的变量不可取地址。因为a中的值仅仅是整数2的拷贝副本。b中的值也同样不可取地址。c中的值还是不可取地址,它只是一个指针&x的拷贝。实际上,所有通过reflect.ValueOf(x)返回的reflect.Value都是不可取地址的。但是对于d,它是c的解引用方式生成的,指向另一个变量,因此是可取地址的。我们可以通过调用reflect.ValueOf(&x).Elem(),来获取任意变量x对应的可取地址的Value。 我们可以通过调用reflect.Value的CanAddr方法来判断其是否可以被取地址: fmt.Println(a.CanAddr()) // \"false\"\nfmt.Println(b.CanAddr()) // \"false\"\nfmt.Println(c.CanAddr()) // \"false\"\nfmt.Println(d.CanAddr()) // \"true\" 每当我们通过指针间接地获取的reflect.Value都是可取地址的,即使开始的是一个不可取地址的Value。在反射机制中,所有关于是否支持取地址的规则都是类似的。例如,slice的索引表达式e[i]将隐式地包含一个指针,它就是可取地址的,即使开始的e表达式不支持也没有关系。以此类推,reflect.ValueOf(e).Index(i)对应的值也是可取地址的,即使原始的reflect.ValueOf(e)不支持也没有关系。 要从变量对应的可取地址的reflect.Value来访问变量需要三个步骤。第一步是调用Addr()方法,它返回一个Value,里面保存了指向变量的指针。然后是在Value上调用Interface()方法,也就是返回一个interface{},里面包含指向变量的指针。最后,如果我们知道变量的类型,我们可以使用类型的断言机制将得到的interface{}类型的接口强制转为普通的类型指针。这样我们就可以通过这个普通指针来更新变量了: x := 2\nd := reflect.ValueOf(&x).Elem() // d refers to the variable x\npx := d.Addr().Interface().(*int) // px := &x\n*px = 3 // x = 3\nfmt.Println(x) // \"3\" 或者,不使用指针,而是通过调用可取地址的reflect.Value的reflect.Value.Set方法来更新对应的值: d.Set(reflect.ValueOf(4))\nfmt.Println(x) // \"4\" Set方法将在运行时执行和编译时进行类似的可赋值性约束的检查。以上代码,变量和值都是int类型,但是如果变量是int64类型,那么程序将抛出一个panic异常,所以关键问题是要确保改类型的变量可以接受对应的值: d.Set(reflect.ValueOf(int64(5))) // panic: int64 is not assignable to int 同样,对一个不可取地址的reflect.Value调用Set方法也会导致panic异常: x := 2\nb := reflect.ValueOf(x)\nb.Set(reflect.ValueOf(3)) // panic: Set using unaddressable value 这里有很多用于基本数据类型的Set方法:SetInt、SetUint、SetString和SetFloat等。 d := reflect.ValueOf(&x).Elem()\nd.SetInt(3)\nfmt.Println(x) // \"3\" 从某种程度上说,这些Set方法总是尽可能地完成任务。以SetInt为例,只要变量是某种类型的有符号整数就可以工作,即使是一些命名的类型、甚至只要底层数据类型是有符号整数就可以,而且如果对于变量类型值太大的话会被自动截断。但需要谨慎的是:对于一个引用interface{}类型的reflect.Value调用SetInt会导致panic异常,即使那个interface{}变量对于整数类型也不行。 x := 1\nrx := reflect.ValueOf(&x).Elem()\nrx.SetInt(2) // OK, x = 2\nrx.Set(reflect.ValueOf(3)) // OK, x = 3\nrx.SetString(\"hello\") // panic: string is not assignable to int\nrx.Set(reflect.ValueOf(\"hello\")) // panic: string is not assignable to int var y interface{}\nry := reflect.ValueOf(&y).Elem()\nry.SetInt(2) // panic: SetInt called on interface Value\nry.Set(reflect.ValueOf(3)) // OK, y = int(3)\nry.SetString(\"hello\") // panic: SetString called on interface Value\nry.Set(reflect.ValueOf(\"hello\")) // OK, y = \"hello\" 当我们用Display显示os.Stdout结构时,我们发现反射可以越过Go语言的导出规则的限制读取结构体中未导出的成员,比如在类Unix系统上os.File结构体中的fd int成员。然而,利用反射机制并不能修改这些未导出的成员: stdout := reflect.ValueOf(os.Stdout).Elem() // *os.Stdout, an os.File var\nfmt.Println(stdout.Type()) // \"os.File\"\nfd := stdout.FieldByName(\"fd\")\nfmt.Println(fd.Int()) // \"1\"\nfd.SetInt(2) // panic: unexported field 一个可取地址的reflect.Value会记录一个结构体成员是否是未导出成员,如果是的话则拒绝修改操作。因此,CanAddr方法并不能正确反映一个变量是否是可以被修改的。另一个相关的方法CanSet是用于检查对应的reflect.Value是否是可取地址并可被修改的: fmt.Println(fd.CanAddr(), fd.CanSet()) // \"true false\"","breadcrumbs":"反射 » 通过reflect.Value修改值 » 12.5. 通过reflect.Value修改值","id":"155","title":"12.5. 通过reflect.Value修改值"},"156":{"body":"标准库中encoding/...下每个包中提供的Marshal编码函数都有一个对应的Unmarshal函数用于解码。例如,我们在4.5节中看到的,要将包含JSON编码格式的字节slice数据解码为我们自己的Movie类型(§12.3),我们可以这样做: data := []byte{/* ... */}\nvar movie Movie\nerr := json.Unmarshal(data, &movie) Unmarshal函数使用了反射机制类修改movie变量的每个成员,根据输入的内容为Movie成员创建对应的map、结构体和slice。 现在让我们为S表达式编码实现一个简易的Unmarshal,类似于前面的json.Unmarshal标准库函数,对应我们之前实现的sexpr.Marshal函数的逆操作。我们必须提醒一下,一个健壮的和通用的实现通常需要比例子更多的代码,为了便于演示我们采用了精简的实现。我们只支持S表达式有限的子集,同时处理错误的方式也比较粗暴,代码的目的是为了演示反射的用法,而不是构造一个实用的S表达式的解码器。 词法分析器lexer使用了标准库中的text/scanner包将输入流的字节数据解析为一个个类似注释、标识符、字符串面值和数字面值之类的标记。输入扫描器scanner的Scan方法将提前扫描和返回下一个记号,对于rune类型。大多数记号,比如“(”,对应一个单一rune可表示的Unicode字符,但是text/scanner也可以用小的负数表示记号标识符、字符串等由多个字符组成的记号。调用Scan方法将返回这些记号的类型,接着调用TokenText方法将返回记号对应的文本内容。 因为每个解析器可能需要多次使用当前的记号,但是Scan会一直向前扫描,所以我们包装了一个lexer扫描器辅助类型,用于跟踪最近由Scan方法返回的记号。 gopl.io/ch12/sexpr type lexer struct { scan scanner.Scanner token rune // the current token\n} func (lex *lexer) next() { lex.token = lex.scan.Scan() }\nfunc (lex *lexer) text() string { return lex.scan.TokenText() } func (lex *lexer) consume(want rune) { if lex.token != want { // NOTE: Not an example of good error handling. panic(fmt.Sprintf(\"got %q, want %q\", lex.text(), want)) } lex.next()\n} 现在让我们转到语法解析器。它主要包含两个功能。第一个是read函数,用于读取S表达式的当前标记,然后根据S表达式的当前标记更新可取地址的reflect.Value对应的变量v。 func read(lex *lexer, v reflect.Value) { switch lex.token { case scanner.Ident: // The only valid identifiers are // \"nil\" and struct field names. if lex.text() == \"nil\" { v.Set(reflect.Zero(v.Type())) lex.next() return } case scanner.String: s, _ := strconv.Unquote(lex.text()) // NOTE: ignoring errors v.SetString(s) lex.next() return case scanner.Int: i, _ := strconv.Atoi(lex.text()) // NOTE: ignoring errors v.SetInt(int64(i)) lex.next() return case '(': lex.next() readList(lex, v) lex.next() // consume ')' return } panic(fmt.Sprintf(\"unexpected token %q\", lex.text()))\n} 我们的S表达式使用标识符区分两个不同类型,结构体成员名和nil值的指针。read函数值处理nil类型的标识符。当遇到scanner.Ident为“nil”时,使用reflect.Zero函数将变量v设置为零值。而其它任何类型的标识符,我们都作为错误处理。后面的readList函数将处理结构体的成员名。 一个“(”标记对应一个列表的开始。第二个函数readList,将一个列表解码到一个聚合类型中(map、结构体、slice或数组),具体类型依赖于传入待填充变量的类型。每次遇到这种情况,循环继续解析每个元素直到遇到于开始标记匹配的结束标记“)”,endList函数用于检测结束标记。 最有趣的部分是递归。最简单的是对数组类型的处理。直到遇到“)”结束标记,我们使用Index函数来获取数组每个元素的地址,然后递归调用read函数处理。和其它错误类似,如果输入数据导致解码器的引用超出了数组的范围,解码器将抛出panic异常。slice也采用类似方法解析,不同的是我们将为每个元素创建新的变量,然后将元素添加到slice的末尾。 在循环处理结构体和map每个元素时必须解码一个(key value)格式的对应子列表。对于结构体,key部分对于成员的名字。和数组类似,我们使用FieldByName找到结构体对应成员的变量,然后递归调用read函数处理。对于map,key可能是任意类型,对元素的处理方式和slice类似,我们创建一个新的变量,然后递归填充它,最后将新解析到的key/value对添加到map。 func readList(lex *lexer, v reflect.Value) { switch v.Kind() { case reflect.Array: // (item ...) for i := 0; !endList(lex); i++ { read(lex, v.Index(i)) } case reflect.Slice: // (item ...) for !endList(lex) { item := reflect.New(v.Type().Elem()).Elem() read(lex, item) v.Set(reflect.Append(v, item)) } case reflect.Struct: // ((name value) ...) for !endList(lex) { lex.consume('(') if lex.token != scanner.Ident { panic(fmt.Sprintf(\"got token %q, want field name\", lex.text())) } name := lex.text() lex.next() read(lex, v.FieldByName(name)) lex.consume(')') } case reflect.Map: // ((key value) ...) v.Set(reflect.MakeMap(v.Type())) for !endList(lex) { lex.consume('(') key := reflect.New(v.Type().Key()).Elem() read(lex, key) value := reflect.New(v.Type().Elem()).Elem() read(lex, value) v.SetMapIndex(key, value) lex.consume(')') } default: panic(fmt.Sprintf(\"cannot decode list into %v\", v.Type())) }\n} func endList(lex *lexer) bool { switch lex.token { case scanner.EOF: panic(\"end of file\") case ')': return true } return false\n} 最后,我们将解析器包装为导出的Unmarshal解码函数,隐藏了一些初始化和清理等边缘处理。内部解析器以panic的方式抛出错误,但是Unmarshal函数通过在defer语句调用recover函数来捕获内部panic(§5.10),然后返回一个对panic对应的错误信息。 // Unmarshal parses S-expression data and populates the variable\n// whose address is in the non-nil pointer out.\nfunc Unmarshal(data []byte, out interface{}) (err error) { lex := &lexer{scan: scanner.Scanner{Mode: scanner.GoTokens}} lex.scan.Init(bytes.NewReader(data)) lex.next() // get the first token defer func() { // NOTE: this is not an example of ideal error handling. if x := recover(); x != nil { err = fmt.Errorf(\"error at %s: %v\", lex.scan.Position, x) } }() read(lex, reflect.ValueOf(out).Elem()) return nil\n} 生产实现不应该对任何输入问题都用panic形式报告,而且应该报告一些错误相关的信息,例如出现错误输入的行号和位置等。尽管如此,我们希望通过这个例子来展示类似encoding/json等包底层代码的实现思路,以及如何使用反射机制来填充数据结构。 练习 12.8: sexpr.Unmarshal函数和json.Unmarshal一样,都要求在解码前输入完整的字节slice。定义一个和json.Decoder类似的sexpr.Decoder类型,支持从一个io.Reader流解码。修改sexpr.Unmarshal函数,使用这个新的类型实现。 练习 12.9: 编写一个基于标记的API用于解码S表达式,参考xml.Decoder(7.14)的风格。你将需要五种类型的标记:Symbol、String、Int、StartList和EndList。 练习 12.10: 扩展sexpr.Unmarshal函数,支持布尔型、浮点数和interface类型的解码,使用 练习 12.3: 的方案。(提示:要解码接口,你需要将name映射到每个支持类型的reflect.Type。)","breadcrumbs":"反射 » 示例: 解码S表达式 » 12.6. 示例: 解码S表达式","id":"156","title":"12.6. 示例: 解码S表达式"},"157":{"body":"我们的最后一个例子是使用reflect.Type来打印任意值的类型和枚举它的方法: gopl.io/ch12/methods // Print prints the method set of the value x.\nfunc Print(x interface{}) { v := reflect.ValueOf(x) t := v.Type() fmt.Printf(\"type %s\\n\", t) for i := 0; i < v.NumMethod(); i++ { methType := v.Method(i).Type() fmt.Printf(\"func (%s) %s%s\\n\", t, t.Method(i).Name, strings.TrimPrefix(methType.String(), \"func\")) }\n} reflect.Type和reflect.Value都提供了一个Method方法。每次t.Method(i)调用将一个reflect.Method的实例,对应一个用于描述一个方法的名称和类型的结构体。每次v.Method(i)方法调用都返回一个reflect.Value以表示对应的值(§6.4),也就是一个方法是绑到它的接收者的。使用reflect.Value.Call方法(我们这里没有演示),将可以调用一个Func类型的Value,但是这个例子中只用到了它的类型。 这是属于time.Duration和*strings.Replacer两个类型的方法: methods.Print(time.Hour)\n// Output:\n// type time.Duration\n// func (time.Duration) Hours() float64\n// func (time.Duration) Minutes() float64\n// func (time.Duration) Nanoseconds() int64\n// func (time.Duration) Seconds() float64\n// func (time.Duration) String() string methods.Print(new(strings.Replacer))\n// Output:\n// type *strings.Replacer\n// func (*strings.Replacer) Replace(string) string\n// func (*strings.Replacer) WriteString(io.Writer, string) (int, error)","breadcrumbs":"反射 » 显示一个类型的方法集 » 12.8. 显示一个类型的方法集","id":"157","title":"12.8. 显示一个类型的方法集"},"158":{"body":"虽然反射提供的API远多于我们讲到的,我们前面的例子主要是给出了一个方向,通过反射可以实现哪些功能。反射是一个强大并富有表达力的工具,但是它应该被小心地使用,原因有三。 第一个原因是,基于反射的代码是比较脆弱的。对于每一个会导致编译器报告类型错误的问题,在反射中都有与之相对应的误用问题,不同的是编译器会在构建时马上报告错误,而反射则是在真正运行到的时候才会抛出panic异常,可能是写完代码很久之后了,而且程序也可能运行了很长的时间。 以前面的readList函数(§12.6)为例,为了从输入读取字符串并填充int类型的变量而调用的reflect.Value.SetString方法可能导致panic异常。绝大多数使用反射的程序都有类似的风险,需要非常小心地检查每个reflect.Value的对应值的类型、是否可取地址,还有是否可以被修改等。 避免这种因反射而导致的脆弱性的问题的最好方法,是将所有的反射相关的使用控制在包的内部,如果可能的话避免在包的API中直接暴露reflect.Value类型,这样可以限制一些非法输入。如果无法做到这一点,在每个有风险的操作前指向额外的类型检查。以标准库中的代码为例,当fmt.Printf收到一个非法的操作数时,它并不会抛出panic异常,而是打印相关的错误信息。程序虽然还有BUG,但是会更加容易诊断。 fmt.Printf(\"%d %s\\n\", \"hello\", 42) // \"%!d(string=hello) %!s(int=42)\" 反射同样降低了程序的安全性,还影响了自动化重构和分析工具的准确性,因为它们无法识别运行时才能确认的类型信息。 避免使用反射的第二个原因是,即使对应类型提供了相同文档,但是反射的操作不能做静态类型检查,而且大量反射的代码通常难以理解。总是需要小心翼翼地为每个导出的类型和其它接受interface{}或reflect.Value类型参数的函数维护说明文档。 第三个原因,基于反射的代码通常比正常的代码运行速度慢一到两个数量级。对于一个典型的项目,大部分函数的性能和程序的整体性能关系不大,所以当反射能使程序更加清晰的时候可以考虑使用。测试是一个特别适合使用反射的场景,因为每个测试的数据集都很小。但是对于性能关键路径的函数,最好避免使用反射。","breadcrumbs":"反射 » 几点忠告 » 12.9. 几点忠告","id":"158","title":"12.9. 几点忠告"},"159":{"body":"Go语言的设计包含了诸多安全策略,限制了可能导致程序运行出错的用法。编译时类型检查可以发现大多数类型不匹配的操作,例如两个字符串做减法的错误。字符串、map、slice和chan等所有的内置类型,都有严格的类型转换规则。 对于无法静态检测到的错误,例如数组访问越界或使用空指针,运行时动态检测可以保证程序在遇到问题的时候立即终止并打印相关的错误信息。自动内存管理(垃圾内存自动回收)可以消除大部分野指针和内存泄漏相关的问题。 Go语言的实现刻意隐藏了很多底层细节。我们无法知道一个结构体真实的内存布局,也无法获取一个运行时函数对应的机器码,也无法知道当前的goroutine是运行在哪个操作系统线程之上。事实上,Go语言的调度器会自己决定是否需要将某个goroutine从一个操作系统线程转移到另一个操作系统线程。一个指向变量的指针也并没有展示变量真实的地址。因为垃圾回收器可能会根据需要移动变量的内存位置,当然变量对应的地址也会被自动更新。 总的来说,Go语言的这些特性使得Go程序相比较低级的C语言来说更容易预测和理解,程序也不容易崩溃。通过隐藏底层的实现细节,也使得Go语言编写的程序具有高度的可移植性,因为语言的语义在很大程度上是独立于任何编译器实现、操作系统和CPU系统结构的(当然也不是完全绝对独立:例如int等类型就依赖于CPU机器字的大小,某些表达式求值的具体顺序,还有编译器实现的一些额外的限制等)。 有时候我们可能会放弃使用部分语言特性而优先选择具有更好性能的方法,例如需要与其他语言编写的库进行互操作,或者用纯Go语言无法实现的某些函数。 在本章,我们将展示如何使用unsafe包来摆脱Go语言规则带来的限制,讲述如何创建C语言函数库的绑定,以及如何进行系统调用。 本章提供的方法不应该轻易使用(译注:属于黑魔法,虽然功能很强大,但是也容易误伤到自己)。如果没有处理好细节,它们可能导致各种不可预测的并且隐晦的错误,甚至连有经验的C语言程序员也无法理解这些错误。使用unsafe包的同时也放弃了Go语言保证与未来版本的兼容性的承诺,因为它必然会有意无意中使用很多非公开的实现细节,而这些实现的细节在未来的Go语言中很可能会被改变。 要注意的是,unsafe包是一个采用特殊方式实现的包。虽然它可以和普通包一样的导入和使用,但它实际上是由编译器实现的。它提供了一些访问语言内部特性的方法,特别是内存布局相关的细节。将这些特性封装到一个独立的包中,是为在极少数情况下需要使用的时候,同时引起人们的注意(译注:因为看包的名字就知道使用unsafe包是不安全的)。此外,有一些环境因为安全的因素可能限制这个包的使用。 不过unsafe包被广泛地用于比较低级的包,例如runtime、os、syscall还有net包等,因为它们需要和操作系统密切配合,但是对于普通的程序一般是不需要使用unsafe包的。","breadcrumbs":"底层编程 » 第13章 底层编程","id":"159","title":"第13章 底层编程"},"16":{"body":"Go语言和其他编程语言一样,一个大的程序是由很多小的基础构件组成的。变量保存值,简单的加法和减法运算被组合成较复杂的表达式。基础类型被聚合为数组或结构体等更复杂的数据结构。然后使用if和for之类的控制语句来组织和控制表达式的执行流程。然后多个语句被组织到一个个函数中,以便代码的隔离和复用。函数以源文件和包的方式被组织。 我们已经在前面章节的例子中看到了很多例子。在本章中,我们将深入讨论Go程序基础结构方面的一些细节。每个示例程序都是刻意写的简单,这样我们可以减少复杂的算法或数据结构等不相关的问题带来的干扰,从而可以专注于Go语言本身的学习。","breadcrumbs":"程序结构 » 第2章 程序结构","id":"16","title":"第2章 程序结构"},"160":{"body":"unsafe.Sizeof函数返回操作数在内存中的字节大小,参数可以是任意类型的表达式,但是它并不会对表达式进行求值。一个Sizeof函数调用是一个对应uintptr类型的常量表达式,因此返回的结果可以用作数组类型的长度大小,或者用作计算其他的常量。 import \"unsafe\"\nfmt.Println(unsafe.Sizeof(float64(0))) // \"8\" Sizeof函数返回的大小只包括数据结构中固定的部分,例如字符串对应结构体中的指针和字符串长度部分,但是并不包含指针指向的字符串的内容。Go语言中非聚合类型通常有一个固定的大小,尽管在不同工具链下生成的实际大小可能会有所不同。考虑到可移植性,引用类型或包含引用类型的大小在32位平台上是4个字节,在64位平台上是8个字节。 计算机在加载和保存数据时,如果内存地址合理地对齐的将会更有效率。例如2字节大小的int16类型的变量地址应该是偶数,一个4字节大小的rune类型变量的地址应该是4的倍数,一个8字节大小的float64、uint64或64-bit指针类型变量的地址应该是8字节对齐的。但是对于再大的地址对齐倍数则是不需要的,即使是complex128等较大的数据类型最多也只是8字节对齐。 由于地址对齐这个因素,一个聚合类型(结构体或数组)的大小至少是所有字段或元素大小的总和,或者更大因为可能存在内存空洞。内存空洞是编译器自动添加的没有被使用的内存空间,用于保证后面每个字段或元素的地址相对于结构或数组的开始地址能够合理地对齐(译注:内存空洞可能会存在一些随机数据,可能会对用unsafe包直接操作内存的处理产生影响)。 类型 大小 bool 1个字节 intN, uintN, floatN, complexN N/8个字节(例如float64是8个字节) int, uint, uintptr 1个机器字 *T 1个机器字 string 2个机器字(data、len) []T 3个机器字(data、len、cap) map 1个机器字 func 1个机器字 chan 1个机器字 interface 2个机器字(type、value) Go语言的规范并没有要求一个字段的声明顺序和内存中的顺序是一致的,所以理论上一个编译器可以随意地重新排列每个字段的内存位置,虽然在写作本书的时候编译器还没有这么做。下面的三个结构体虽然有着相同的字段,但是第一种写法比另外的两个需要多50%的内存。 // 64-bit 32-bit\nstruct{ bool; float64; int16 } // 3 words 4words\nstruct{ float64; int16; bool } // 2 words 3words\nstruct{ bool; int16; float64 } // 2 words 3words 关于内存地址对齐算法的细节超出了本书的范围,也不是每一个结构体都需要担心这个问题,不过有效的包装可以使数据结构更加紧凑(译注:未来的Go语言编译器应该会默认优化结构体的顺序,当然应该也能够指定具体的内存布局,相同讨论请参考 Issue10014 ),内存使用率和性能都可能会受益。 unsafe.Alignof 函数返回对应参数的类型需要对齐的倍数。和 Sizeof 类似, Alignof 也是返回一个常量表达式,对应一个常量。通常情况下布尔和数字类型需要对齐到它们本身的大小(最多8个字节),其它的类型对齐到机器字大小。 unsafe.Offsetof 函数的参数必须是一个字段 x.f,然后返回 f 字段相对于 x 起始地址的偏移量,包括可能的空洞。 图 13.1 显示了一个结构体变量 x 以及其在32位和64位机器上的典型的内存。灰色区域是空洞。 var x struct { a bool b int16 c []int\n} 下面显示了对x和它的三个字段调用unsafe包相关函数的计算结果: 32位系统: Sizeof(x) = 16 Alignof(x) = 4\nSizeof(x.a) = 1 Alignof(x.a) = 1 Offsetof(x.a) = 0\nSizeof(x.b) = 2 Alignof(x.b) = 2 Offsetof(x.b) = 2\nSizeof(x.c) = 12 Alignof(x.c) = 4 Offsetof(x.c) = 4 64位系统: Sizeof(x) = 32 Alignof(x) = 8\nSizeof(x.a) = 1 Alignof(x.a) = 1 Offsetof(x.a) = 0\nSizeof(x.b) = 2 Alignof(x.b) = 2 Offsetof(x.b) = 2\nSizeof(x.c) = 24 Alignof(x.c) = 8 Offsetof(x.c) = 8 虽然这几个函数在不安全的unsafe包,但是这几个函数调用并不是真的不安全,特别在需要优化内存空间时它们返回的结果对于理解原生的内存布局很有帮助。","breadcrumbs":"底层编程 » unsafe.Sizeof, Alignof 和 Offsetof » 13.1. unsafe.Sizeof, Alignof 和 Offsetof","id":"160","title":"13.1. unsafe.Sizeof, Alignof 和 Offsetof"},"161":{"body":"大多数指针类型会写成*T,表示是“一个指向T类型变量的指针”。unsafe.Pointer是特别定义的一种指针类型(译注:类似C语言中的void*类型的指针),它可以包含任意类型变量的地址。当然,我们不可以直接通过*p来获取unsafe.Pointer指针指向的真实变量的值,因为我们并不知道变量的具体类型。和普通指针一样,unsafe.Pointer指针也是可以比较的,并且支持和nil常量比较判断是否为空指针。 一个普通的*T类型指针可以被转化为unsafe.Pointer类型指针,并且一个unsafe.Pointer类型指针也可以被转回普通的指针,被转回普通的指针类型并不需要和原始的*T类型相同。通过将*float64类型指针转化为*uint64类型指针,我们可以查看一个浮点数变量的位模式。 package math func Float64bits(f float64) uint64 { return *(*uint64)(unsafe.Pointer(&f)) } fmt.Printf(\"%#016x\\n\", Float64bits(1.0)) // \"0x3ff0000000000000\" 通过转为新类型指针,我们可以更新浮点数的位模式。通过位模式操作浮点数是可以的,但是更重要的意义是指针转换语法让我们可以在不破坏类型系统的前提下向内存写入任意的值。 一个unsafe.Pointer指针也可以被转化为uintptr类型,然后保存到指针型数值变量中(译注:这只是和当前指针相同的一个数字值,并不是一个指针),然后用以做必要的指针数值运算。(第三章内容,uintptr是一个无符号的整型数,足以保存一个地址)这种转换虽然也是可逆的,但是将uintptr转为unsafe.Pointer指针可能会破坏类型系统,因为并不是所有的数字都是有效的内存地址。 许多将unsafe.Pointer指针转为原生数字,然后再转回为unsafe.Pointer类型指针的操作也是不安全的。比如下面的例子需要将变量x的地址加上b字段地址偏移量转化为*int16类型指针,然后通过该指针更新x.b: gopl.io/ch13/unsafeptr var x struct { a bool b int16 c []int\n} // 和 pb := &x.b 等价\npb := (*int16)(unsafe.Pointer( uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)))\n*pb = 42\nfmt.Println(x.b) // \"42\" 上面的写法尽管很繁琐,但在这里并不是一件坏事,因为这些功能应该很谨慎地使用。不要试图引入一个uintptr类型的临时变量,因为它可能会破坏代码的安全性(译注:这是真正可以体会unsafe包为何不安全的例子)。下面段代码是错误的: // NOTE: subtly incorrect!\ntmp := uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)\npb := (*int16)(unsafe.Pointer(tmp))\n*pb = 42 产生错误的原因很微妙。有时候垃圾回收器会移动一些变量以降低内存碎片等问题。这类垃圾回收器被称为移动GC。当一个变量被移动,所有的保存该变量旧地址的指针必须同时被更新为变量移动后的新地址。从垃圾收集器的视角来看,一个unsafe.Pointer是一个指向变量的指针,因此当变量被移动时对应的指针也必须被更新;但是uintptr类型的临时变量只是一个普通的数字,所以其值不应该被改变。上面错误的代码因为引入一个非指针的临时变量tmp,导致垃圾收集器无法正确识别这个是一个指向变量x的指针。当第二个语句执行时,变量x可能已经被转移,这时候临时变量tmp也就不再是现在的&x.b地址。第三个向之前无效地址空间的赋值语句将彻底摧毁整个程序! 还有很多类似原因导致的错误。例如这条语句: pT := uintptr(unsafe.Pointer(new(T))) // 提示: 错误! 这里并没有指针引用new新创建的变量,因此该语句执行完成之后,垃圾收集器有权马上回收其内存空间,所以返回的pT将是无效的地址。 虽然目前的Go语言实现还没有使用移动GC(译注:未来可能实现),但这不该是编写错误代码侥幸的理由:当前的Go语言实现已经有移动变量的场景。在5.2节我们提到goroutine的栈是根据需要动态增长的。当发生栈动态增长的时候,原来栈中的所有变量可能需要被移动到新的更大的栈中,所以我们并不能确保变量的地址在整个使用周期内是不变的。 在编写本文时,还没有清晰的原则来指引Go程序员,什么样的unsafe.Pointer和uintptr的转换是不安全的(参考 Issue7192 ). 译注: 该问题已经关闭),因此我们强烈建议按照最坏的方式处理。将所有包含变量地址的uintptr类型变量当作BUG处理,同时减少不必要的unsafe.Pointer类型到uintptr类型的转换。在第一个例子中,有三个转换——字段偏移量到uintptr的转换和转回unsafe.Pointer类型的操作——所有的转换全在一个表达式完成。 当调用一个库函数,并且返回的是uintptr类型地址时(译注:普通方法实现的函数尽量不要返回该类型。下面例子是reflect包的函数,reflect包和unsafe包一样都是采用特殊技术实现的,编译器可能给它们开了后门),比如下面反射包中的相关函数,返回的结果应该立即转换为unsafe.Pointer以确保指针指向的是相同的变量。 package reflect func (Value) Pointer() uintptr\nfunc (Value) UnsafeAddr() uintptr\nfunc (Value) InterfaceData() [2]uintptr // (index 1)","breadcrumbs":"底层编程 » unsafe.Pointer » 13.2. unsafe.Pointer","id":"161","title":"13.2. unsafe.Pointer"},"162":{"body":"来自reflect包的DeepEqual函数可以对两个值进行深度相等判断。DeepEqual函数使用内建的==比较操作符对基础类型进行相等判断,对于复合类型则递归该变量的每个基础类型然后做类似的比较判断。因为它可以工作在任意的类型上,甚至对于一些不支持==操作运算符的类型也可以工作,因此在一些测试代码中广泛地使用该函数。比如下面的代码是用DeepEqual函数比较两个字符串slice是否相等。 func TestSplit(t *testing.T) { got := strings.Split(\"a:b:c\", \":\") want := []string{\"a\", \"b\", \"c\"}; if !reflect.DeepEqual(got, want) { /* ... */ }\n} 尽管DeepEqual函数很方便,而且可以支持任意的数据类型,但是它也有不足之处。例如,它将一个nil值的map和非nil值但是空的map视作不相等,同样nil值的slice 和非nil但是空的slice也视作不相等。 var a, b []string = nil, []string{}\nfmt.Println(reflect.DeepEqual(a, b)) // \"false\" var c, d map[string]int = nil, make(map[string]int)\nfmt.Println(reflect.DeepEqual(c, d)) // \"false\" 我们希望在这里实现一个自己的Equal函数,用于比较类型的值。和DeepEqual函数类似的地方是它也是基于slice和map的每个元素进行递归比较,不同之处是它将nil值的slice(map类似)和非nil值但是空的slice视作相等的值。基础部分的比较可以基于reflect包完成,和12.3章的Display函数的实现方法类似。同样,我们也定义了一个内部函数equal,用于内部的递归比较。读者目前不用关心seen参数的具体含义。对于每一对需要比较的x和y,equal函数首先检测它们是否都有效(或都无效),然后检测它们是否是相同的类型。剩下的部分是一个巨大的switch分支,用于相同基础类型的元素比较。因为页面空间的限制,我们省略了一些相似的分支。 gopl.io/ch13/equal func equal(x, y reflect.Value, seen map[comparison]bool) bool { if !x.IsValid() || !y.IsValid() { return x.IsValid() == y.IsValid() } if x.Type() != y.Type() { return false } // ...cycle check omitted (shown later)... switch x.Kind() { case reflect.Bool: return x.Bool() == y.Bool() case reflect.String: return x.String() == y.String() // ...numeric cases omitted for brevity... case reflect.Chan, reflect.UnsafePointer, reflect.Func: return x.Pointer() == y.Pointer() case reflect.Ptr, reflect.Interface: return equal(x.Elem(), y.Elem(), seen) case reflect.Array, reflect.Slice: if x.Len() != y.Len() { return false } for i := 0; i < x.Len(); i++ { if !equal(x.Index(i), y.Index(i), seen) { return false } } return true // ...struct and map cases omitted for brevity... } panic(\"unreachable\")\n} 和前面的建议一样,我们并不公开reflect包相关的接口,所以导出的函数需要在内部自己将变量转为reflect.Value类型。 // Equal reports whether x and y are deeply equal.\nfunc Equal(x, y interface{}) bool { seen := make(map[comparison]bool) return equal(reflect.ValueOf(x), reflect.ValueOf(y), seen)\n} type comparison struct { x, y unsafe.Pointer t reflect.Type\n} 为了确保算法对于有环的数据结构也能正常退出,我们必须记录每次已经比较的变量,从而避免进入第二次的比较。Equal函数分配了一组用于比较的结构体,包含每对比较对象的地址(unsafe.Pointer形式保存)和类型。我们要记录类型的原因是,有些不同的变量可能对应相同的地址。例如,如果x和y都是数组类型,那么x和x[0]将对应相同的地址,y和y[0]也是对应相同的地址,这可以用于区分x与y之间的比较或x[0]与y[0]之间的比较是否进行过了。 // cycle check\nif x.CanAddr() && y.CanAddr() { xptr := unsafe.Pointer(x.UnsafeAddr()) yptr := unsafe.Pointer(y.UnsafeAddr()) if xptr == yptr { return true // identical references } c := comparison{xptr, yptr, x.Type()} if seen[c] { return true // already seen } seen[c] = true\n} 这是Equal函数用法的例子: fmt.Println(Equal([]int{1, 2, 3}, []int{1, 2, 3})) // \"true\"\nfmt.Println(Equal([]string{\"foo\"}, []string{\"bar\"})) // \"false\"\nfmt.Println(Equal([]string(nil), []string{})) // \"true\"\nfmt.Println(Equal(map[string]int(nil), map[string]int{})) // \"true\" Equal函数甚至可以处理类似12.3章中导致Display陷入死循环的带有环的数据。 // Circular linked lists a -> b -> a and c -> c.\ntype link struct { value string tail *link\n}\na, b, c := &link{value: \"a\"}, &link{value: \"b\"}, &link{value: \"c\"}\na.tail, b.tail, c.tail = b, a, c\nfmt.Println(Equal(a, a)) // \"true\"\nfmt.Println(Equal(b, b)) // \"true\"\nfmt.Println(Equal(c, c)) // \"true\"\nfmt.Println(Equal(a, b)) // \"false\"\nfmt.Println(Equal(a, c)) // \"false\" 练习 13.1: 定义一个深比较函数,对于十亿以内的数字比较,忽略类型差异。 练习 13.2: 编写一个函数,报告其参数是否为循环数据结构。","breadcrumbs":"底层编程 » 示例: 深度相等判断 » 13.3. 示例: 深度相等判断","id":"162","title":"13.3. 示例: 深度相等判断"},"163":{"body":"Go程序可能会遇到要访问C语言的某些硬件驱动函数的场景,或者是从一个C++语言实现的嵌入式数据库查询记录的场景,或者是使用Fortran语言实现的一些线性代数库的场景。C语言作为一个通用语言,很多库会选择提供一个C兼容的API,然后用其他不同的编程语言实现(译者:Go语言需要也应该拥抱这些巨大的代码遗产)。 在本节中,我们将构建一个简易的数据压缩程序,使用了一个Go语言自带的叫cgo的用于支援C语言函数调用的工具。这类工具一般被称为 foreign-function interfaces (简称ffi),并且在类似工具中cgo也不是唯一的。SWIG( http://swig.org )是另一个类似的且被广泛使用的工具,SWIG提供了很多复杂特性以支援C++的特性,但SWIG并不是我们要讨论的主题。 在标准库的compress/...子包有很多流行的压缩算法的编码和解码实现,包括流行的LZW压缩算法(Unix的compress命令用的算法)和DEFLATE压缩算法(GNU gzip命令用的算法)。这些包的API的细节虽然有些差异,但是它们都提供了针对 io.Writer类型输出的压缩接口和提供了针对io.Reader类型输入的解压缩接口。例如: package gzip // compress/gzip\nfunc NewWriter(w io.Writer) io.WriteCloser\nfunc NewReader(r io.Reader) (io.ReadCloser, error) bzip2压缩算法,是基于优雅的Burrows-Wheeler变换算法,运行速度比gzip要慢,但是可以提供更高的压缩比。标准库的compress/bzip2包目前还没有提供bzip2压缩算法的实现。完全从头开始实现一个压缩算法是一件繁琐的工作,而且 http://bzip.org 已经有现成的libbzip2的开源实现,不仅文档齐全而且性能又好。 如果是比较小的C语言库,我们完全可以用纯Go语言重新实现一遍。如果我们对性能也没有特殊要求的话,我们还可以用os/exec包的方法将C编写的应用程序作为一个子进程运行。只有当你需要使用复杂而且性能更高的底层C接口时,就是使用cgo的场景了(译注:用os/exec包调用子进程的方法会导致程序运行时依赖那个应用程序)。下面我们将通过一个例子讲述cgo的具体用法。 译注:本章采用的代码都是最新的。因为之前已经出版的书中包含的代码只能在Go1.5之前使用。从Go1.6开始,Go语言已经明确规定了哪些Go语言指针可以直接传入C语言函数。新代码重点是增加了bz2alloc和bz2free的两个函数,用于bz_stream对象空间的申请和释放操作。下面是新代码中增加的注释,说明这个问题: // The version of this program that appeared in the first and second\n// printings did not comply with the proposed rules for passing\n// pointers between Go and C, described here:\n// https://github.com/golang/proposal/blob/master/design/12416-cgo-pointers.md\n//\n// The rules forbid a C function like bz2compress from storing 'in'\n// and 'out' (pointers to variables allocated by Go) into the Go\n// variable 's', even temporarily.\n//\n// The version below, which appears in the third printing, has been\n// corrected. To comply with the rules, the bz_stream variable must\n// be allocated by C code. We have introduced two C functions,\n// bz2alloc and bz2free, to allocate and free instances of the\n// bz_stream type. Also, we have changed bz2compress so that before\n// it returns, it clears the fields of the bz_stream that contain\n// pointers to Go variables. 要使用libbzip2,我们需要先构建一个bz_stream结构体,用于保持输入和输出缓存。然后有三个函数:BZ2_bzCompressInit用于初始化缓存,BZ2_bzCompress用于将输入缓存的数据压缩到输出缓存,BZ2_bzCompressEnd用于释放不需要的缓存。(目前不要担心包的具体结构,这个例子的目的就是演示各个部分如何组合在一起的。) 我们可以在Go代码中直接调用BZ2_bzCompressInit和BZ2_bzCompressEnd,但是对于BZ2_bzCompress,我们将定义一个C语言的包装函数,用它完成真正的工作。下面是C代码,对应一个独立的文件。 gopl.io/ch13/bzip /* This file is gopl.io/ch13/bzip/bzip2.c, */\n/* a simple wrapper for libbzip2 suitable for cgo. */\n#include int bz2compress(bz_stream *s, int action, char *in, unsigned *inlen, char *out, unsigned *outlen) { s->next_in = in; s->avail_in = *inlen; s->next_out = out; s->avail_out = *outlen; int r = BZ2_bzCompress(s, action); *inlen -= s->avail_in; *outlen -= s->avail_out; s->next_in = s->next_out = NULL; return r;\n} 现在让我们转到Go语言部分,第一部分如下所示。其中import \"C\"的语句是比较特别的。其实并没有一个叫C的包,但是这行语句会让Go编译程序在编译之前先运行cgo工具。 // Package bzip provides a writer that uses bzip2 compression (bzip.org).\npackage bzip /*\n#cgo CFLAGS: -I/usr/include\n#cgo LDFLAGS: -L/usr/lib -lbz2\n#include \n#include \nbz_stream* bz2alloc() { return calloc(1, sizeof(bz_stream)); }\nint bz2compress(bz_stream *s, int action, char *in, unsigned *inlen, char *out, unsigned *outlen);\nvoid bz2free(bz_stream* s) { free(s); }\n*/\nimport \"C\" import ( \"io\" \"unsafe\"\n) type writer struct { w io.Writer // underlying output stream stream *C.bz_stream outbuf [64 * 1024]byte\n} // NewWriter returns a writer for bzip2-compressed streams.\nfunc NewWriter(out io.Writer) io.WriteCloser { const blockSize = 9 const verbosity = 0 const workFactor = 30 w := &writer{w: out, stream: C.bz2alloc()} C.BZ2_bzCompressInit(w.stream, blockSize, verbosity, workFactor) return w\n} 在预处理过程中,cgo工具生成一个临时包用于包含所有在Go语言中访问的C语言的函数或类型。例如C.bz_stream和C.BZ2_bzCompressInit。cgo工具通过以某种特殊的方式调用本地的C编译器来发现在Go源文件导入声明前的注释中包含的C头文件中的内容(译注:import \"C\"语句前紧挨着的注释是对应cgo的特殊语法,对应必要的构建参数选项和C语言代码)。 在cgo注释中还可以包含#cgo指令,用于给C语言工具链指定特殊的参数。例如CFLAGS和LDFLAGS分别对应传给C语言编译器的编译参数和链接器参数,使它们可以从特定目录找到bzlib.h头文件和libbz2.a库文件。这个例子假设你已经在/usr目录成功安装了bzip2库。如果bzip2库是安装在不同的位置,你需要更新这些参数(译注:这里有一个从纯C代码生成的cgo绑定,不依赖bzip2静态库和操作系统的具体环境,具体请访问 https://github.com/chai2010/bzip2 )。 NewWriter函数通过调用C语言的BZ2_bzCompressInit函数来初始化stream中的缓存。在writer结构中还包括了另一个buffer,用于输出缓存。 下面是Write方法的实现,返回成功压缩数据的大小,主体是一个循环中调用C语言的bz2compress函数实现的。从代码可以看到,Go程序可以访问C语言的bz_stream、char和uint类型,还可以访问bz2compress等函数,甚至可以访问C语言中像BZ_RUN那样的宏定义,全部都是以C.x语法访问。其中C.uint类型和Go语言的uint类型并不相同,即使它们具有相同的大小也是不同的类型。 func (w *writer) Write(data []byte) (int, error) { if w.stream == nil { panic(\"closed\") } var total int // uncompressed bytes written for len(data) > 0 { inlen, outlen := C.uint(len(data)), C.uint(cap(w.outbuf)) C.bz2compress(w.stream, C.BZ_RUN, (*C.char)(unsafe.Pointer(&data[0])), &inlen, (*C.char)(unsafe.Pointer(&w.outbuf)), &outlen) total += int(inlen) data = data[inlen:] if _, err := w.w.Write(w.outbuf[:outlen]); err != nil { return total, err } } return total, nil\n} 在循环的每次迭代中,向bz2compress传入数据的地址和剩余部分的长度,还有输出缓存w.outbuf的地址和容量。这两个长度信息通过它们的地址传入而不是值传入,因为bz2compress函数可能会根据已经压缩的数据和压缩后数据的大小来更新这两个值。每个块压缩后的数据被写入到底层的io.Writer。 Close方法和Write方法有着类似的结构,通过一个循环将剩余的压缩数据刷新到输出缓存。 // Close flushes the compressed data and closes the stream.\n// It does not close the underlying io.Writer.\nfunc (w *writer) Close() error { if w.stream == nil { panic(\"closed\") } defer func() { C.BZ2_bzCompressEnd(w.stream) C.bz2free(w.stream) w.stream = nil }() for { inlen, outlen := C.uint(0), C.uint(cap(w.outbuf)) r := C.bz2compress(w.stream, C.BZ_FINISH, nil, &inlen, (*C.char)(unsafe.Pointer(&w.outbuf)), &outlen) if _, err := w.w.Write(w.outbuf[:outlen]); err != nil { return err } if r == C.BZ_STREAM_END { return nil } }\n} 压缩完成后,Close方法用了defer函数确保函数退出前调用C.BZ2_bzCompressEnd和C.bz2free释放相关的C语言运行时资源。此刻w.stream指针将不再有效,我们将它设置为nil以保证安全,然后在每个方法中增加了nil检测,以防止用户在关闭后依然错误使用相关方法。 上面的实现中,不仅仅写是非并发安全的,甚至并发调用Close和Write方法也可能导致程序的的崩溃。修复这个问题是练习13.3的内容。 下面的bzipper程序,使用我们自己包实现的bzip2压缩命令。它的行为和许多Unix系统的bzip2命令类似。 gopl.io/ch13/bzipper // Bzipper reads input, bzip2-compresses it, and writes it out.\npackage main import ( \"io\" \"log\" \"os\" \"gopl.io/ch13/bzip\"\n) func main() { w := bzip.NewWriter(os.Stdout) if _, err := io.Copy(w, os.Stdin); err != nil { log.Fatalf(\"bzipper: %v\\n\", err) } if err := w.Close(); err != nil { log.Fatalf(\"bzipper: close: %v\\n\", err) }\n} 在上面的场景中,我们使用bzipper压缩了/usr/share/dict/words系统自带的词典,从938,848字节压缩到335,405字节。大约是原始数据大小的三分之一。然后使用系统自带的bunzip2命令进行解压。压缩前后文件的SHA256哈希码是相同了,这也说明了我们的压缩工具是正确的。(如果你的系统没有sha256sum命令,那么请先按照练习4.2实现一个类似的工具) $ go build gopl.io/ch13/bzipper\n$ wc -c < /usr/share/dict/words\n938848\n$ sha256sum < /usr/share/dict/words\n126a4ef38493313edc50b86f90dfdaf7c59ec6c948451eac228f2f3a8ab1a6ed -\n$ ./bzipper < /usr/share/dict/words | wc -c\n335405\n$ ./bzipper < /usr/share/dict/words | bunzip2 | sha256sum\n126a4ef38493313edc50b86f90dfdaf7c59ec6c948451eac228f2f3a8ab1a6ed - 我们演示了如何将一个C语言库链接到Go语言程序。相反,将Go编译为静态库然后链接到C程序,或者将Go程序编译为动态库然后在C程序中动态加载也都是可行的(译注:在Go1.5中,Windows系统的Go语言实现并不支持生成C语言动态库或静态库的特性。不过好消息是,目前已经有人在尝试解决这个问题,具体请访问 Issue11058 )。这里我们只展示的cgo很小的一些方面,更多的关于内存管理、指针、回调函数、中断信号处理、字符串、errno处理、终结器,以及goroutines和系统线程的关系等,有很多细节可以讨论。特别是如何将Go语言的指针传入C函数的规则也是异常复杂的(译注:简单来说,要传入C函数的Go指针指向的数据本身不能包含指针或其他引用类型;并且C函数在返回后不能继续持有Go指针;并且在C函数返回之前,Go指针是被锁定的,不能导致对应指针数据被移动或栈的调整),部分的原因在13.2节有讨论到,但是在Go1.5中还没有被明确(译注:Go1.6将会明确cgo中的指针使用规则)。如果要进一步阅读,可以从 https://golang.org/cmd/cgo 开始。 练习 13.3: 使用sync.Mutex以保证bzip2.writer在多个goroutines中被并发调用是安全的。 练习 13.4: 因为C库依赖的限制。 使用os/exec包启动/bin/bzip2命令作为一个子进程,提供一个纯Go的bzip.NewWriter的替代实现(译注:虽然是纯Go实现,但是运行时将依赖/bin/bzip2命令,其他操作系统可能无法运行)。","breadcrumbs":"底层编程 » 通过cgo调用C代码 » 13.4. 通过cgo调用C代码","id":"163","title":"13.4. 通过cgo调用C代码"},"164":{"body":"我们在前一章结尾的时候,我们警告要谨慎使用reflect包。那些警告同样适用于本章的unsafe包。 高级语言使得程序员不用再关心真正运行程序的指令细节,同时也不再需要关注许多如内存布局之类的实现细节。因为高级语言这个绝缘的抽象层,我们可以编写安全健壮的,并且可以运行在不同操作系统上的具有高度可移植性的程序。 但是unsafe包,它让程序员可以透过这个绝缘的抽象层直接使用一些必要的功能,虽然可能是为了获得更好的性能。但是代价就是牺牲了可移植性和程序安全,因此使用unsafe包是一个危险的行为。我们对何时以及如何使用unsafe包的建议和我们在11.5节提到的Knuth对过早优化的建议类似。大多数Go程序员可能永远不会需要直接使用unsafe包。当然,也永远都会有一些需要使用unsafe包实现会更简单的场景。如果确实认为使用unsafe包是最理想的方式,那么应该尽可能将它限制在较小的范围,这样其它代码就可以忽略unsafe的影响。 现在,赶紧将最后两章抛入脑后吧。编写一些实实在在的应用是真理。请远离reflect和unsafe包,除非你确实需要它们。 最后,用Go快乐地编程。我们希望你能像我们一样喜欢Go语言。","breadcrumbs":"底层编程 » 几点忠告 » 13.5. 几点忠告","id":"164","title":"13.5. 几点忠告"},"165":{"body":"英文原版并没有包含附录部分,只有一个索引部分。中文版增加附录部分主要用于收录一些和本书相关的内容,比如英文原版的勘误(有些读者可能会对照中文和英文原阅读)、英文作者和中文译者、译文授权等内容。以后还可能会考虑增加一些习题解答相关的内容。 需要特别说明的是,中文版附录并没有包含英文原版的索引信息。因为英文原版的索引信息主要是记录每个索引所在的英文页面位置,而中文版是以GitBook方式组织的html网页形式,将英文页面位置转为章节位置可能会更合理,不过这个会涉及到繁琐的手工操作。如果大家有更好的建议,请告知我们。","breadcrumbs":"附录 » 附录","id":"165","title":"附录"},"166":{"body":"p.9, ¶2: for \"can compared\", read \"can be compared\". (Thanks to Antonio Macías Ojeda, 2015-10-22. Corrected in the second printing.) p.13: As printed, the gopl.io/ch1/lissajous program is deterministic, not random. We've added the statement below to the downloadable program so that it prints a pseudo-random image each time it is run. (Thanks to Randall McPherson, 2015-10-19.) rand.Seed(time.Now().UTC().UnixNano()) p.15, ¶2: For \"inner loop\", read \"outer loop\". (Thanks to Ralph Corderoy, 2015-11-28. Corrected in the third printing.) p.19, ¶2: For \"Go's libraries makes\", read \"Go's library makes\". (Thanks to Victor Farazdagi, 2015-11-30. Corrected in the third printing.) p.40, ¶4: For \"value of the underlying type\", read \"value of an unnamed type with the same underlying type\". (Thanks to Carlos Romero Brox, 2015-12-19.) p.40, ¶1: The paragraph should end with a period, not a comma. (Thanks to Victor Farazdagi, 2015-11-30. Corrected in the third printing.) p.43, ¶3: Import declarations are explained in §10.4, not §10.3. (Thanks to Peter Jurgensen, 2015-11-21. Corrected in the third printing.) p.48: f.ReadByte() serves as an example of a reference to f, but *os.File has no such method. For \"ReadByte\", read \"Stat\", four times. (Thanks to Peter Olsen, 2016-01-06. Corrected in the third printing.) p.52, ¶2: for \"an synonym\", read \"a synonym\", twice. (Corrected in the second printing.) p.52, ¶9: for \"The integer arithmetic operators\", read \"The arithmetic operators\". (Thanks to Yoshiki Shibata, 2015-12-20.) p.68: the table of UTF-8 encodings is missing a bit from each first byte. The corrected table is shown below. (Thanks to Akshay Kumar, 2015-11-02. Corrected in the second printing.) 0xxxxxxx runes 0−127 (ASCII)\n110xxxxx 10xxxxxx 128−2047 (values <128 unused)\n1110xxxx 10xxxxxx 10xxxxxx 2048−65535 (values <2048 unused)\n11110xxx 10xxxxxx 10xxxxxx 10xxxxxx 65536−0x10ffff (other values unused) p.73, ¶1: For \"a exercise\", read \"an exercise\". (Thanks to vrajmohan, 2015-12-28.) p.74: the comment in gopl.io/ch3/printints should say fmt.Sprint, not fmt.Sprintf. (Corrected in the second printing.) p.75, ¶4: for \"%u\", read \"%o\". (Thanks to William Hannish, 2015-12-21.) p.76: the comment // \"time.Duration 5m0s should have a closing double-quotation mark. (Corrected in the second printing.) p.79, ¶4: \"When an untyped constant is assigned to a variable, as in the first statement below, or appears on the right-hand side of a variable declaration with an explicit type, as in the other three statements, ...\" has it backwards: the first statement is a declaration; the other three are assignments. (Thanks to Yoshiki Shibata, 2015-11-09. Corrected in the third printing.) p.112: Exercise 4.11 calls for a \"CRUD\" (create, read, update, delete) tool for GitHub Issues. Since GitHub does not currently allow Issues to be deleted, for \"delete\", read \"close\". (Thanks to Yoshiki Shibata, 2016-01-18.) p.115: The anchor element in gopl.io/ch4/issueshtml's template is missing a closing tag. (Thanks to Taj Khattra, 2016-01-19.) p.132, code display following ¶3: the final comment should read: // compile error: can't assign func(int, int) int to func(int) int (Thanks to Toni Suter, 2015-11-21. Corrected in the third printing.) p.160, ¶4: For Get(\"item\")), read Get(\"item\"). (Thanks to Yoshiki Shibata, 2016-02-01.) p.166, ¶2: for \"way\", read \"a way\". (Corrected in the third printing.) p.200, TestEval function: the format string in the final call to t.Errorf should format test.env with %v, not %s. (Thanks to Mitsuteru Sawa, 2015-12-07. Corrected in the third printing.) p.222, Exercise 8.1: The port numbers for London and Tokyo should be swapped in the final command to match the earlier commands. (Thanks to Kiyoshi Kamishima, 2016-01-08.) p.272, ¶3: for \"the request body\", read \"the response body\". (Thanks to 曹春晖 , 2016-01-19.) p.288, code display following ¶4: In the import declaration, for \"database/mysql\", read \"database/sql\". (Thanks to Jose Colon Rodriguez, 2016-01-09.) p.347, Exercise 12.8: for \"like json.Marshal\", read \"like json.Unmarshal\". (Thanks to chai2010 , 2016-01-01.) p.362: the gopl.io/ch13/bzip program does not comply with the proposed rules for passing pointers between Go and C code because the C function bz2compress temporarily stores a Go pointer (in) into the Go heap (the bz_stream variable). The bz_stream variable should be allocated, and explicitly freed after the call to BZ2_bzCompressEnd, by C functions. (Thanks to Joe Tsai, 2015-11-18. Corrected in the third printing.)","breadcrumbs":"附录 » 附录A:原文勘误 » 附录A: 原文勘误","id":"166","title":"附录A: 原文勘误"},"167":{"body":"","breadcrumbs":"附录 » 附录B:作者译者 » 附录B:作者/译者","id":"167","title":"附录B:作者/译者"},"168":{"body":"Alan A. A. Donovan is a member of Google’s Go team in New York. He holds computer science degrees from Cambridge and MIT and has been programming in industry since 1996. Since 2005, he has worked at Google on infrastructure projects and was the co-designer of its proprietary build system, Blaze . He has built many libraries and tools for static analysis of Go programs, including oracle , godoc -analysis , eg, and gorename . Brian W. Kernighan is a professor in the Computer Science Department at Princeton University. He was a member of technical staff in the Computing Science Research Center at Bell Labs from 1969 until 2000, where he worked on languages and tools for Unix . He is the co-author of several books, including The C Programming Language, Second Edition (Prentice Hall, 1988) , and The Practice of Programming (Addison-Wesley, 1999) .","breadcrumbs":"附录 » 附录B:作者译者 » 英文作者","id":"168","title":"英文作者"},"169":{"body":"中文译者 章节 chai2010 前言/第2 ~ 4章/第10 ~ 13章 Xargin 第1章/第6章/第8 ~ 9章 CrazySssst 第5章 foreversmart 第7章","breadcrumbs":"附录 » 附录B:作者译者 » 中文译者","id":"169","title":"中文译者"},"17":{"body":"Go语言中的函数名、变量名、常量名、类型名、语句标号和包名等所有的命名,都遵循一个简单的命名规则:一个名字必须以一个字母(Unicode字母)或下划线开头,后面可以跟任意数量的字母、数字或下划线。大写字母和小写字母是不同的:heapSort和Heapsort是两个不同的名字。 Go语言中类似if和switch的关键字有25个;关键字不能用于自定义名字,只能在特定语法结构中使用。 break default func interface select\ncase defer go map struct\nchan else goto package switch\nconst fallthrough if range type\ncontinue for import return var 此外,还有大约30多个预定义的名字,比如int和true等,主要对应内建的常量、类型和函数。 内建常量: true false iota nil 内建类型: int int8 int16 int32 int64 uint uint8 uint16 uint32 uint64 uintptr float32 float64 complex128 complex64 bool byte rune string error 内建函数: make len cap new append copy close delete complex real imag panic recover 这些内部预先定义的名字并不是关键字,你可以在定义中重新使用它们。在一些特殊的场景中重新定义它们也是有意义的,但是也要注意避免过度而引起语义混乱。 如果一个名字是在函数内部定义,那么它就只在函数内部有效。如果是在函数外部定义,那么将在当前包的所有文件中都可以访问。名字的开头字母的大小写决定了名字在包外的可见性。如果一个名字是大写字母开头的(译注:必须是在函数外部定义的包级名字;包级函数名本身也是包级名字),那么它将是导出的,也就是说可以被外部的包访问,例如fmt包的Printf函数就是导出的,可以在fmt包外部访问。包本身的名字一般总是用小写字母。 名字的长度没有逻辑限制,但是Go语言的风格是尽量使用短小的名字,对于局部变量尤其是这样;你会经常看到i之类的短名字,而不是冗长的theLoopIndex命名。通常来说,如果一个名字的作用域比较大,生命周期也比较长,那么用长的名字将会更有意义。 在习惯上,Go语言程序员推荐使用 驼峰式 命名,当名字由几个单词组成时优先使用大小写分隔,而不是优先用下划线分隔。因此,在标准库有QuoteRuneToASCII和parseRequestLine这样的函数命名,但是一般不会用quote_rune_to_ASCII和parse_request_line这样的命名。而像ASCII和HTML这样的缩略词则避免使用大小写混合的写法,它们可能被称为htmlEscape、HTMLEscape或escapeHTML,但不会是escapeHtml。","breadcrumbs":"程序结构 » 命名 » 2.1. 命名","id":"17","title":"2.1. 命名"},"170":{"body":"除特别注明外,本站内容均采用 知识共享-署名(CC-BY) 3.0协议 授权,代码遵循 Go项目的BSD协议 授权。","breadcrumbs":"附录 » 附录C:译文授权 » 附录C:译文授权","id":"170","title":"附录C:译文授权"},"171":{"body":"下表是 The Go Programming Language 其它语言版本: 语言 链接 时间 译者 ISBN 中文 《Go语言圣经》 2016/2/1 chai2010 , Xargin , CrazySssst , foreversmart ? 韩语 Acorn Publishing (Korea) 2016 Seung Lee 9788960778320 俄语 Williams Publishing (Russia) 2016 ? 9785845920515 波兰语 Helion (Poland) 2016 ? ? 日语 Maruzen Publishing (Japan) 2017 Yoshiki Shibata 9784621300251 葡萄牙语 Novatec Editora (Brazil) 2017 ? ? 中文简体 Pearson Education Asia 2017 ? ? 中文繁体 Gotop Information (Taiwan) 2017 ? ?","breadcrumbs":"附录 » 附录D:其它语言 » 附录D:其它语言","id":"171","title":"附录D:其它语言"},"18":{"body":"声明语句定义了程序的各种实体对象以及部分或全部的属性。Go语言主要有四种类型的声明语句:var、const、type和func,分别对应变量、常量、类型和函数实体对象的声明。这一章我们重点讨论变量和类型的声明,第三章将讨论常量的声明,第五章将讨论函数的声明。 一个Go语言编写的程序对应一个或多个以.go为文件后缀名的源文件。每个源文件中以包的声明语句开始,说明该源文件是属于哪个包。包声明语句之后是import语句导入依赖的其它包,然后是包一级的类型、变量、常量、函数的声明语句,包一级的各种类型的声明语句的顺序无关紧要(译注:函数内部的名字则必须先声明之后才能使用)。例如,下面的例子中声明了一个常量、一个函数和两个变量: gopl.io/ch2/boiling // Boiling prints the boiling point of water.\npackage main import \"fmt\" const boilingF = 212.0 func main() { var f = boilingF var c = (f - 32) * 5 / 9 fmt.Printf(\"boiling point = %g°F or %g°C\\n\", f, c) // Output: // boiling point = 212°F or 100°C\n} 其中常量boilingF是在包一级范围声明语句声明的,然后f和c两个变量是在main函数内部声明的声明语句声明的。在包一级声明语句声明的名字可在整个包对应的每个源文件中访问,而不是仅仅在其声明语句所在的源文件中访问。相比之下,局部声明的名字就只能在函数内部很小的范围被访问。 一个函数的声明由一个函数名字、参数列表(由函数的调用者提供参数变量的具体值)、一个可选的返回值列表和包含函数定义的函数体组成。如果函数没有返回值,那么返回值列表是省略的。执行函数从函数的第一个语句开始,依次顺序执行直到遇到return返回语句,如果没有返回语句则是执行到函数末尾,然后返回到函数调用者。 我们已经看到过很多函数声明和函数调用的例子了,在第五章将深入讨论函数的相关细节,这里只简单解释下。下面的fToC函数封装了温度转换的处理逻辑,这样它只需要被定义一次,就可以在多个地方多次被使用。在这个例子中,main函数就调用了两次fToC函数,分别使用在局部定义的两个常量作为调用函数的参数。 gopl.io/ch2/ftoc // Ftoc prints two Fahrenheit-to-Celsius conversions.\npackage main import \"fmt\" func main() { const freezingF, boilingF = 32.0, 212.0 fmt.Printf(\"%g°F = %g°C\\n\", freezingF, fToC(freezingF)) // \"32°F = 0°C\" fmt.Printf(\"%g°F = %g°C\\n\", boilingF, fToC(boilingF)) // \"212°F = 100°C\"\n} func fToC(f float64) float64 { return (f - 32) * 5 / 9\n}","breadcrumbs":"程序结构 » 声明 » 2.2. 声明","id":"18","title":"2.2. 声明"},"19":{"body":"var声明语句可以创建一个特定类型的变量,然后给变量附加一个名字,并且设置变量的初始值。变量声明的一般语法如下: var 变量名字 类型 = 表达式 其中“ 类型 ”或“ = 表达式 ”两个部分可以省略其中的一个。如果省略的是类型信息,那么将根据初始化表达式来推导变量的类型信息。如果初始化表达式被省略,那么将用零值初始化该变量。 数值类型变量对应的零值是0,布尔类型变量对应的零值是false,字符串类型对应的零值是空字符串,接口或引用类型(包括slice、指针、map、chan和函数)变量对应的零值是nil。数组或结构体等聚合类型对应的零值是每个元素或字段都是对应该类型的零值。 零值初始化机制可以确保每个声明的变量总是有一个良好定义的值,因此在Go语言中不存在未初始化的变量。这个特性可以简化很多代码,而且可以在没有增加额外工作的前提下确保边界条件下的合理行为。例如: var s string\nfmt.Println(s) // \"\" 这段代码将打印一个空字符串,而不是导致错误或产生不可预知的行为。Go语言程序员应该让一些聚合类型的零值也具有意义,这样可以保证不管任何类型的变量总是有一个合理有效的零值状态。 也可以在一个声明语句中同时声明一组变量,或用一组初始化表达式声明并初始化一组变量。如果省略每个变量的类型,将可以声明多个类型不同的变量(类型由初始化表达式推导): var i, j, k int // int, int, int\nvar b, f, s = true, 2.3, \"four\" // bool, float64, string 初始化表达式可以是字面量或任意的表达式。在包级别声明的变量会在main入口函数执行前完成初始化(§2.6.2),局部变量将在声明语句被执行到的时候完成初始化。 一组变量也可以通过调用一个函数,由函数返回的多个返回值初始化: var f, err = os.Open(name) // os.Open returns a file and an error","breadcrumbs":"程序结构 » 变量 » 2.3. 变量","id":"19","title":"2.3. 变量"},"2":{"body":"编程语言的演化跟生物物种的演化类似,一个成功的编程语言的后代一般都会继承它们祖先的优点;当然有时多种语言杂合也可能会产生令人惊讶的特性;还有一些激进的新特性可能并没有先例。通过观察这些影响,我们可以学到为什么一门语言是这样子的,它已经适应了怎样的环境。 下图展示了有哪些早期的编程语言对Go语言的设计产生了重要影响。 Go语言有时候被描述为“C类似语言”,或者是“21世纪的C语言”。Go从C语言继承了相似的表达式语法、控制流结构、基础数据类型、调用参数传值、指针等很多思想,还有C语言一直所看中的编译后机器码的运行效率以及和现有操作系统的无缝适配。 但是在Go语言的家族树中还有其它的祖先。其中一个有影响力的分支来自 Niklaus Wirth 所设计的[Pascal][Pascal]语言。然后[Modula-2][Modula-2]语言激发了包的概念。然后[Oberon][Oberon]语言摒弃了模块接口文件和模块实现文件之间的区别。第二代的[Oberon-2][Oberon-2]语言直接影响了包的导入和声明的语法,还有[Oberon][Oberon]语言的面向对象特性所提供的方法的声明语法等。 Go语言的另一支祖先,带来了Go语言区别其他语言的重要特性,灵感来自于贝尔实验室的 Tony Hoare 于1978年发表的鲜为外界所知的关于并发研究的基础文献 顺序通信进程 ( [communicating sequential processes][CSP] ,缩写为[CSP][CSP]。在[CSP][CSP]中,程序是一组中间没有共享状态的平行运行的处理过程,它们之间使用管道进行通信和控制同步。不过 Tony Hoare 的[CSP][CSP]只是一个用于描述并发性基本概念的描述语言,并不是一个可以编写可执行程序的通用编程语言。 接下来,Rob Pike和其他人开始不断尝试将 CSP 引入实际的编程语言中。他们第一次尝试引入 CSP 特性的编程语言叫 Squeak (老鼠间交流的语言),是一个提供鼠标和键盘事件处理的编程语言,它的管道是静态创建的。然后是改进版的 Newsqueak 语言,提供了类似C语言语句和表达式的语法和类似[Pascal][Pascal]语言的推导语法。Newsqueak是一个带垃圾回收的纯函数式语言,它再次针对键盘、鼠标和窗口事件管理。但是在Newsqueak语言中管道是动态创建的,属于第一类值,可以保存到变量中。 在Plan9操作系统中,这些优秀的想法被吸收到了一个叫[Alef][Alef]的编程语言中。Alef试图将Newsqueak语言改造为系统编程语言,但是因为缺少垃圾回收机制而导致并发编程很痛苦。(译注:在Alef之后还有一个叫[Limbo][Limbo]的编程语言,Go语言从其中借鉴了很多特性。 具体请参考Pike的讲稿:http://talks.golang.org/2012/concurrency.slide#9 ) Go语言的其他的一些特性零散地来自于其他一些编程语言;比如iota语法是从[APL][APL]语言借鉴,词法作用域与嵌套函数来自于[Scheme][Scheme]语言(和其他很多语言)。当然,我们也可以从Go中发现很多创新的设计。比如Go语言的切片为动态数组提供了有效的随机存取的性能,这可能会让人联想到链表的底层的共享机制。还有Go语言新发明的defer语句。","breadcrumbs":"前言 » Go语言起源","id":"2","title":"Go语言起源"},"20":{"body":"在函数内部,有一种称为简短变量声明语句的形式可用于声明和初始化局部变量。它以“名字 := 表达式”形式声明变量,变量的类型根据表达式来自动推导。下面是lissajous函数中的三个简短变量声明语句(§1.4): anim := gif.GIF{LoopCount: nframes}\nfreq := rand.Float64() * 3.0\nt := 0.0 因为简洁和灵活的特点,简短变量声明被广泛用于大部分的局部变量的声明和初始化。var形式的声明语句往往是用于需要显式指定变量类型的地方,或者因为变量稍后会被重新赋值而初始值无关紧要的地方。 i := 100 // an int\nvar boiling float64 = 100 // a float64\nvar names []string\nvar err error\nvar p Point 和var形式声明语句一样,简短变量声明语句也可以用来声明和初始化一组变量: i, j := 0, 1 但是这种同时声明多个变量的方式应该限制只在可以提高代码可读性的地方使用,比如for语句的循环的初始化语句部分。 请记住“:=”是一个变量声明语句,而“=”是一个变量赋值操作。也不要混淆多个变量的声明和元组的多重赋值(§2.4.1),后者是将右边各个表达式的值赋值给左边对应位置的各个变量: i, j = j, i // 交换 i 和 j 的值 和普通var形式的变量声明语句一样,简短变量声明语句也可以用函数的返回值来声明和初始化变量,像下面的os.Open函数调用将返回两个值: f, err := os.Open(name)\nif err != nil { return err\n}\n// ...use f...\nf.Close() 这里有一个比较微妙的地方:简短变量声明左边的变量可能并不是全部都是刚刚声明的。如果有一些已经在相同的词法域声明过了(§2.7),那么简短变量声明语句对这些已经声明过的变量就只有赋值行为了。 在下面的代码中,第一个语句声明了in和err两个变量。在第二个语句只声明了out一个变量,然后对已经声明的err进行了赋值操作。 in, err := os.Open(infile)\n// ...\nout, err := os.Create(outfile) 简短变量声明语句中必须至少要声明一个新的变量,下面的代码将不能编译通过: f, err := os.Open(infile)\n// ...\nf, err := os.Create(outfile) // compile error: no new variables 解决的方法是第二个简短变量声明语句改用普通的多重赋值语句。 简短变量声明语句只有对已经在同级词法域声明过的变量才和赋值操作语句等价,如果变量是在外部词法域声明的,那么简短变量声明语句将会在当前词法域重新声明一个新的变量。我们在本章后面将会看到类似的例子。","breadcrumbs":"程序结构 » 变量 » 2.3.1. 简短变量声明","id":"20","title":"2.3.1. 简短变量声明"},"21":{"body":"一个变量对应一个保存了变量对应类型值的内存空间。普通变量在声明语句创建时被绑定到一个变量名,比如叫x的变量,但是还有很多变量始终以表达式方式引入,例如x[i]或x.f变量。所有这些表达式一般都是读取一个变量的值,除非它们是出现在赋值语句的左边,这种时候是给对应变量赋予一个新的值。 一个指针的值是另一个变量的地址。一个指针对应变量在内存中的存储位置。并不是每一个值都会有一个内存地址,但是对于每一个变量必然有对应的内存地址。通过指针,我们可以直接读或更新对应变量的值,而不需要知道该变量的名字(如果变量有名字的话)。 如果用“var x int”声明语句声明一个x变量,那么&x表达式(取x变量的内存地址)将产生一个指向该整数变量的指针,指针对应的数据类型是*int,指针被称之为“指向int类型的指针”。如果指针名字为p,那么可以说“p指针指向变量x”,或者说“p指针保存了x变量的内存地址”。同时*p表达式对应p指针指向的变量的值。一般*p表达式读取指针指向的变量的值,这里为int类型的值,同时因为*p对应一个变量,所以该表达式也可以出现在赋值语句的左边,表示更新指针所指向的变量的值。 x := 1\np := &x // p, of type *int, points to x\nfmt.Println(*p) // \"1\"\n*p = 2 // equivalent to x = 2\nfmt.Println(x) // \"2\" 对于聚合类型每个成员——比如结构体的每个字段、或者是数组的每个元素——也都是对应一个变量,因此可以被取地址。 变量有时候被称为可寻址的值。即使变量由表达式临时生成,那么表达式也必须能接受&取地址操作。 任何类型的指针的零值都是nil。如果p指向某个有效变量,那么p != nil测试为真。指针之间也是可以进行相等测试的,只有当它们指向同一个变量或全部是nil时才相等。 var x, y int\nfmt.Println(&x == &x, &x == &y, &x == nil) // \"true false false\" 在Go语言中,返回函数中局部变量的地址也是安全的。例如下面的代码,调用f函数时创建局部变量v,在局部变量地址被返回之后依然有效,因为指针p依然引用这个变量。 var p = f() func f() *int { v := 1 return &v\n} 每次调用f函数都将返回不同的结果: fmt.Println(f() == f()) // \"false\" 因为指针包含了一个变量的地址,因此如果将指针作为参数调用函数,那将可以在函数中通过该指针来更新变量的值。例如下面这个例子就是通过指针来更新变量的值,然后返回更新后的值,可用在一个表达式中(译注:这是对C语言中++v操作的模拟,这里只是为了说明指针的用法,incr函数模拟的做法并不推荐): func incr(p *int) int { *p++ // 非常重要:只是增加p指向的变量的值,并不改变p指针!!! return *p\n} v := 1\nincr(&v) // side effect: v is now 2\nfmt.Println(incr(&v)) // \"3\" (and v is 3) 每次我们对一个变量取地址,或者复制指针,我们都是为原变量创建了新的别名。例如,*p就是变量v的别名。指针特别有价值的地方在于我们可以不用名字而访问一个变量,但是这是一把双刃剑:要找到一个变量的所有访问者并不容易,我们必须知道变量全部的别名(译注:这是Go语言的垃圾回收器所做的工作)。不仅仅是指针会创建别名,很多其他引用类型也会创建别名,例如slice、map和chan,甚至结构体、数组和接口都会创建所引用变量的别名。 指针是实现标准库中flag包的关键技术,它使用命令行参数来设置对应变量的值,而这些对应命令行标志参数的变量可能会零散分布在整个程序中。为了说明这一点,在早些的echo版本中,就包含了两个可选的命令行参数:-n用于忽略行尾的换行符,-s sep用于指定分隔字符(默认是空格)。下面这是第四个版本,对应包路径为gopl.io/ch2/echo4。 gopl.io/ch2/echo4 // Echo4 prints its command-line arguments.\npackage main import ( \"flag\" \"fmt\" \"strings\"\n) var n = flag.Bool(\"n\", false, \"omit trailing newline\")\nvar sep = flag.String(\"s\", \" \", \"separator\") func main() { flag.Parse() fmt.Print(strings.Join(flag.Args(), *sep)) if !*n { fmt.Println() }\n} 调用flag.Bool函数会创建一个新的对应布尔型标志参数的变量。它有三个属性:第一个是命令行标志参数的名字“n”,然后是该标志参数的默认值(这里是false),最后是该标志参数对应的描述信息。如果用户在命令行输入了一个无效的标志参数,或者输入-h或-help参数,那么将打印所有标志参数的名字、默认值和描述信息。类似的,调用flag.String函数将创建一个对应字符串类型的标志参数变量,同样包含命令行标志参数对应的参数名、默认值、和描述信息。程序中的sep和n变量分别是指向对应命令行标志参数变量的指针,因此必须用*sep和*n形式的指针语法间接引用它们。 当程序运行时,必须在使用标志参数对应的变量之前先调用flag.Parse函数,用于更新每个标志参数对应变量的值(之前是默认值)。对于非标志参数的普通命令行参数可以通过调用flag.Args()函数来访问,返回值对应一个字符串类型的slice。如果在flag.Parse函数解析命令行参数时遇到错误,默认将打印相关的提示信息,然后调用os.Exit(2)终止程序。 让我们运行一些echo测试用例: $ go build gopl.io/ch2/echo4\n$ ./echo4 a bc def\na bc def\n$ ./echo4 -s / a bc def\na/bc/def\n$ ./echo4 -n a bc def\na bc def$\n$ ./echo4 -help\nUsage of ./echo4: -n omit trailing newline -s string separator (default \" \")","breadcrumbs":"程序结构 » 变量 » 2.3.2. 指针","id":"21","title":"2.3.2. 指针"},"22":{"body":"另一个创建变量的方法是调用内建的new函数。表达式new(T)将创建一个T类型的匿名变量,初始化为T类型的零值,然后返回变量地址,返回的指针类型为*T。 p := new(int) // p, *int 类型, 指向匿名的 int 变量\nfmt.Println(*p) // \"0\"\n*p = 2 // 设置 int 匿名变量的值为 2\nfmt.Println(*p) // \"2\" 用new创建变量和普通变量声明语句方式创建变量没有什么区别,除了不需要声明一个临时变量的名字外,我们还可以在表达式中使用new(T)。换言之,new函数类似是一种语法糖,而不是一个新的基础概念。 下面的两个newInt函数有着相同的行为: func newInt() *int { return new(int)\n} func newInt() *int { var dummy int return &dummy\n} 每次调用new函数都是返回一个新的变量的地址,因此下面两个地址是不同的: p := new(int)\nq := new(int)\nfmt.Println(p == q) // \"false\" 当然也可能有特殊情况:如果两个类型都是空的,也就是说类型的大小是0,例如struct{}和[0]int,有可能有相同的地址(依赖具体的语言实现)(译注:请谨慎使用大小为0的类型,因为如果类型的大小为0的话,可能导致Go语言的自动垃圾回收器有不同的行为,具体请查看runtime.SetFinalizer函数相关文档)。 new函数使用通常相对比较少,因为对于结构体来说,直接用字面量语法创建新变量的方法会更灵活(§4.4.1)。 由于new只是一个预定义的函数,它并不是一个关键字,因此我们可以将new名字重新定义为别的类型。例如下面的例子: func delta(old, new int) int { return new - old } 由于new被定义为int类型的变量名,因此在delta函数内部是无法使用内置的new函数的。","breadcrumbs":"程序结构 » 变量 » 2.3.3. new函数","id":"22","title":"2.3.3. new函数"},"23":{"body":"变量的生命周期指的是在程序运行期间变量有效存在的时间段。对于在包一级声明的变量来说,它们的生命周期和整个程序的运行周期是一致的。而相比之下,局部变量的生命周期则是动态的:每次从创建一个新变量的声明语句开始,直到该变量不再被引用为止,然后变量的存储空间可能被回收。函数的参数变量和返回值变量都是局部变量。它们在函数每次被调用的时候创建。 例如,下面是从1.4节的Lissajous程序摘录的代码片段: for t := 0.0; t < cycles*2*math.Pi; t += res { x := math.Sin(t) y := math.Sin(t*freq + phase) img.SetColorIndex(size+int(x*size+0.5), size+int(y*size+0.5), blackIndex)\n} 译注:函数的右小括弧也可以另起一行缩进,同时为了防止编译器在行尾自动插入分号而导致的编译错误,可以在末尾的参数变量后面显式插入逗号。像下面这样: for t := 0.0; t < cycles*2*math.Pi; t += res { x := math.Sin(t) y := math.Sin(t*freq + phase) img.SetColorIndex( size+int(x*size+0.5), size+int(y*size+0.5), blackIndex, // 最后插入的逗号不会导致编译错误,这是Go编译器的一个特性 ) // 小括弧另起一行缩进,和大括弧的风格保存一致\n} 在每次循环的开始会创建临时变量t,然后在每次循环迭代中创建临时变量x和y。 那么Go语言的自动垃圾收集器是如何知道一个变量是何时可以被回收的呢?这里我们可以避开完整的技术细节,基本的实现思路是,从每个包级的变量和每个当前运行函数的每一个局部变量开始,通过指针或引用的访问路径遍历,是否可以找到该变量。如果不存在这样的访问路径,那么说明该变量是不可达的,也就是说它是否存在并不会影响程序后续的计算结果。 因为一个变量的有效周期只取决于是否可达,因此一个循环迭代内部的局部变量的生命周期可能超出其局部作用域。同时,局部变量可能在函数返回之后依然存在。 编译器会自动选择在栈上还是在堆上分配局部变量的存储空间,但可能令人惊讶的是,这个选择并不是由用var还是new声明变量的方式决定的。 var global *int func f() { var x int x = 1 global = &x\n} func g() { y := new(int) *y = 1\n} f函数里的x变量必须在堆上分配,因为它在函数退出后依然可以通过包一级的global变量找到,虽然它是在函数内部定义的;用Go语言的术语说,这个x局部变量从函数f中逃逸了。相反,当g函数返回时,变量*y将是不可达的,也就是说可以马上被回收的。因此,*y并没有从函数g中逃逸,编译器可以选择在栈上分配*y的存储空间(译注:也可以选择在堆上分配,然后由Go语言的GC回收这个变量的内存空间),虽然这里用的是new方式。其实在任何时候,你并不需为了编写正确的代码而要考虑变量的逃逸行为,要记住的是,逃逸的变量需要额外分配内存,同时对性能的优化可能会产生细微的影响。 Go语言的自动垃圾收集器对编写正确的代码是一个巨大的帮助,但也并不是说你完全不用考虑内存了。你虽然不需要显式地分配和释放内存,但是要编写高效的程序你依然需要了解变量的生命周期。例如,如果将指向短生命周期对象的指针保存到具有长生命周期的对象中,特别是保存到全局变量时,会阻止对短生命周期对象的垃圾回收(从而可能影响程序的性能)。","breadcrumbs":"程序结构 » 变量 » 2.3.4. 变量的生命周期","id":"23","title":"2.3.4. 变量的生命周期"},"24":{"body":"使用赋值语句可以更新一个变量的值,最简单的赋值语句是将要被赋值的变量放在=的左边,新值的表达式放在=的右边。 x = 1 // 命名变量的赋值\n*p = true // 通过指针间接赋值\nperson.name = \"bob\" // 结构体字段赋值\ncount[x] = count[x] * scale // 数组、slice或map的元素赋值 特定的二元算术运算符和赋值语句的复合操作有一个简洁形式,例如上面最后的语句可以重写为: count[x] *= scale 这样可以省去对变量表达式的重复计算。 数值变量也可以支持++递增和--递减语句(译注:自增和自减是语句,而不是表达式,因此x = i++之类的表达式是错误的): v := 1\nv++ // 等价方式 v = v + 1;v 变成 2\nv-- // 等价方式 v = v - 1;v 变成 1","breadcrumbs":"程序结构 » 赋值 » 2.4. 赋值","id":"24","title":"2.4. 赋值"},"25":{"body":"元组赋值是另一种形式的赋值语句,它允许同时更新多个变量的值。在赋值之前,赋值语句右边的所有表达式将会先进行求值,然后再统一更新左边对应变量的值。这对于处理有些同时出现在元组赋值语句左右两边的变量很有帮助,例如我们可以这样交换两个变量的值: x, y = y, x a[i], a[j] = a[j], a[i] 或者是计算两个整数值的的最大公约数(GCD)(译注:GCD不是那个敏感字,而是greatest common divisor的缩写,欧几里德的GCD是最早的非平凡算法): func gcd(x, y int) int { for y != 0 { x, y = y, x%y } return x\n} 或者是计算斐波纳契数列(Fibonacci)的第N个数: func fib(n int) int { x, y := 0, 1 for i := 0; i < n; i++ { x, y = y, x+y } return x\n} 元组赋值也可以使一系列琐碎赋值更加紧凑(译注: 特别是在for循环的初始化部分), i, j, k = 2, 3, 5 但如果表达式太复杂的话,应该尽量避免过度使用元组赋值;因为每个变量单独赋值语句的写法可读性会更好。 有些表达式会产生多个值,比如调用一个有多个返回值的函数。当这样一个函数调用出现在元组赋值右边的表达式中时(译注:右边不能再有其它表达式),左边变量的数目必须和右边一致。 f, err = os.Open(\"foo.txt\") // function call returns two values 通常,这类函数会用额外的返回值来表达某种错误类型,例如os.Open是用额外的返回值返回一个error类型的错误,还有一些是用来返回布尔值,通常被称为ok。在稍后我们将看到的三个操作都是类似的用法。如果map查找(§4.3)、类型断言(§7.10)或通道接收(§8.4.2)出现在赋值语句的右边,它们都可能会产生两个结果,有一个额外的布尔结果表示操作是否成功: v, ok = m[key] // map lookup\nv, ok = x.(T) // type assertion\nv, ok = <-ch // channel receive 译注:map查找(§4.3)、类型断言(§7.10)或通道接收(§8.4.2)出现在赋值语句的右边时,并不一定是产生两个结果,也可能只产生一个结果。对于只产生一个结果的情形,map查找失败时会返回零值,类型断言失败时会发生运行时panic异常,通道接收失败时会返回零值(阻塞不算是失败)。例如下面的例子: v = m[key] // map查找,失败时返回零值\nv = x.(T) // type断言,失败时panic异常\nv = <-ch // 管道接收,失败时返回零值(阻塞不算是失败) _, ok = m[key] // map返回2个值\n_, ok = mm[\"\"], false // map返回1个值\n_ = mm[\"\"] // map返回1个值 和变量声明一样,我们可以用下划线空白标识符_来丢弃不需要的值。 _, err = io.Copy(dst, src) // 丢弃字节数\n_, ok = x.(T) // 只检测类型,忽略具体值","breadcrumbs":"程序结构 » 赋值 » 2.4.1. 元组赋值","id":"25","title":"2.4.1. 元组赋值"},"26":{"body":"赋值语句是显式的赋值形式,但是程序中还有很多地方会发生隐式的赋值行为:函数调用会隐式地将调用参数的值赋值给函数的参数变量,一个返回语句会隐式地将返回操作的值赋值给结果变量,一个复合类型的字面量(§4.2)也会产生赋值行为。例如下面的语句: medals := []string{\"gold\", \"silver\", \"bronze\"} 隐式地对slice的每个元素进行赋值操作,类似这样写的行为: medals[0] = \"gold\"\nmedals[1] = \"silver\"\nmedals[2] = \"bronze\" map和chan的元素,虽然不是普通的变量,但是也有类似的隐式赋值行为。 不管是隐式还是显式地赋值,在赋值语句左边的变量和右边最终的求到的值必须有相同的数据类型。更直白地说,只有右边的值对于左边的变量是可赋值的,赋值语句才是允许的。 可赋值性的规则对于不同类型有着不同要求,对每个新类型特殊的地方我们会专门解释。对于目前我们已经讨论过的类型,它的规则是简单的:类型必须完全匹配,nil可以赋值给任何指针或引用类型的变量。常量(§3.6)则有更灵活的赋值规则,因为这样可以避免不必要的显式的类型转换。 对于两个值是否可以用==或!=进行相等比较的能力也和可赋值能力有关系:对于任何类型的值的相等比较,第二个值必须是对第一个值类型对应的变量是可赋值的,反之亦然。和前面一样,我们会对每个新类型比较特殊的地方做专门的解释。","breadcrumbs":"程序结构 » 赋值 » 2.4.2. 可赋值性","id":"26","title":"2.4.2. 可赋值性"},"27":{"body":"变量或表达式的类型定义了对应存储值的属性特征,例如数值在内存的存储大小(或者是元素的bit个数),它们在内部是如何表达的,是否支持一些操作符,以及它们自己关联的方法集等。 在任何程序中都会存在一些变量有着相同的内部结构,但是却表示完全不同的概念。例如,一个int类型的变量可以用来表示一个循环的迭代索引、或者一个时间戳、或者一个文件描述符、或者一个月份;一个float64类型的变量可以用来表示每秒移动几米的速度、或者是不同温度单位下的温度;一个字符串可以用来表示一个密码或者一个颜色的名称。 一个类型声明语句创建了一个新的类型名称,和现有类型具有相同的底层结构。新命名的类型提供了一个方法,用来分隔不同概念的类型,这样即使它们底层类型相同也是不兼容的。 type 类型名字 底层类型 类型声明语句一般出现在包一级,因此如果新创建的类型名字的首字符大写,则在包外部也可以使用。 译注:对于中文汉字,Unicode标志都作为小写字母处理,因此中文的命名默认不能导出;不过国内的用户针对该问题提出了不同的看法,根据RobPike的回复,在Go2中有可能会将中日韩等字符当作大写字母处理。下面是RobPik在 Issue763 的回复: A solution that's been kicking around for a while: For Go 2 (can't do it before then): Change the definition to “lower case letters and _ are package-local; all else is exported”. Then with non-cased languages, such as Japanese, we can write 日本语 for an exported name and _日本语 for a local name. This rule has no effect, relative to the Go 1 rule, with cased languages. They behave exactly the same. 为了说明类型声明,我们将不同温度单位分别定义为不同的类型: gopl.io/ch2/tempconv0 // Package tempconv performs Celsius and Fahrenheit temperature computations.\npackage tempconv import \"fmt\" type Celsius float64 // 摄氏温度\ntype Fahrenheit float64 // 华氏温度 const ( AbsoluteZeroC Celsius = -273.15 // 绝对零度 FreezingC Celsius = 0 // 结冰点温度 BoilingC Celsius = 100 // 沸水温度\n) func CToF(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) } func FToC(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) } 我们在这个包声明了两种类型:Celsius和Fahrenheit分别对应不同的温度单位。它们虽然有着相同的底层类型float64,但是它们是不同的数据类型,因此它们不可以被相互比较或混在一个表达式运算。刻意区分类型,可以避免一些像无意中使用不同单位的温度混合计算导致的错误;因此需要一个类似Celsius(t)或Fahrenheit(t)形式的显式转型操作才能将float64转为对应的类型。Celsius(t)和Fahrenheit(t)是类型转换操作,它们并不是函数调用。类型转换不会改变值本身,但是会使它们的语义发生变化。另一方面,CToF和FToC两个函数则是对不同温度单位下的温度进行换算,它们会返回不同的值。 对于每一个类型T,都有一个对应的类型转换操作T(x),用于将x转为T类型(译注:如果T是指针类型,可能会需要用小括弧包装T,比如(*int)(0))。只有当两个类型的底层基础类型相同时,才允许这种转型操作,或者是两者都是指向相同底层结构的指针类型,这些转换只改变类型而不会影响值本身。如果x是可以赋值给T类型的值,那么x必然也可以被转为T类型,但是一般没有这个必要。 数值类型之间的转型也是允许的,并且在字符串和一些特定类型的slice之间也是可以转换的,在下一章我们会看到这样的例子。这类转换可能改变值的表现。例如,将一个浮点数转为整数将丢弃小数部分,将一个字符串转为[]byte类型的slice将拷贝一个字符串数据的副本。在任何情况下,运行时不会发生转换失败的错误(译注: 错误只会发生在编译阶段)。 底层数据类型决定了内部结构和表达方式,也决定是否可以像底层类型一样对内置运算符的支持。这意味着,Celsius和Fahrenheit类型的算术运算行为和底层的float64类型是一样的,正如我们所期望的那样。 fmt.Printf(\"%g\\n\", BoilingC-FreezingC) // \"100\" °C\nboilingF := CToF(BoilingC)\nfmt.Printf(\"%g\\n\", boilingF-CToF(FreezingC)) // \"180\" °F\nfmt.Printf(\"%g\\n\", boilingF-FreezingC) // compile error: type mismatch 比较运算符==和<也可以用来比较一个命名类型的变量和另一个有相同类型的变量,或有着相同底层类型的未命名类型的值之间做比较。但是如果两个值有着不同的类型,则不能直接进行比较: var c Celsius\nvar f Fahrenheit\nfmt.Println(c == 0) // \"true\"\nfmt.Println(f >= 0) // \"true\"\nfmt.Println(c == f) // compile error: type mismatch\nfmt.Println(c == Celsius(f)) // \"true\"! 注意最后那个语句。尽管看起来像函数调用,但是Celsius(f)是类型转换操作,它并不会改变值,仅仅是改变值的类型而已。测试为真的原因是因为c和f都是零值。 一个命名的类型可以提供书写方便,特别是可以避免一遍又一遍地书写复杂类型(译注:例如用匿名的结构体定义变量)。虽然对于像float64这种简单的底层类型没有简洁很多,但是如果是复杂的类型将会简洁很多,特别是我们即将讨论的结构体类型。 命名类型还可以为该类型的值定义新的行为。这些行为表示为一组关联到该类型的函数集合,我们称为类型的方法集。我们将在第六章中讨论方法的细节,这里只说些简单用法。 下面的声明语句,Celsius类型的参数c出现在了函数名的前面,表示声明的是Celsius类型的一个名叫String的方法,该方法返回该类型对象c带着°C温度单位的字符串: func (c Celsius) String() string { return fmt.Sprintf(\"%g°C\", c) } 许多类型都会定义一个String方法,因为当使用fmt包的打印方法时,将会优先使用该类型对应的String方法返回的结果打印,我们将在7.1节讲述。 c := FToC(212.0)\nfmt.Println(c.String()) // \"100°C\"\nfmt.Printf(\"%v\\n\", c) // \"100°C\"; no need to call String explicitly\nfmt.Printf(\"%s\\n\", c) // \"100°C\"\nfmt.Println(c) // \"100°C\"\nfmt.Printf(\"%g\\n\", c) // \"100\"; does not call String\nfmt.Println(float64(c)) // \"100\"; does not call String","breadcrumbs":"程序结构 » 类型 » 2.5. 类型","id":"27","title":"2.5. 类型"},"28":{"body":"Go语言中的包和其他语言的库或模块的概念类似,目的都是为了支持模块化、封装、单独编译和代码重用。一个包的源代码保存在一个或多个以.go为文件后缀名的源文件中,通常一个包所在目录路径的后缀是包的导入路径;例如包gopl.io/ch1/helloworld对应的目录路径是$GOPATH/src/gopl.io/ch1/helloworld。 每个包都对应一个独立的名字空间。例如,在image包中的Decode函数和在unicode/utf16包中的 Decode函数是不同的。要在外部引用该函数,必须显式使用image.Decode或utf16.Decode形式访问。 包还可以让我们通过控制哪些名字是外部可见的来隐藏内部实现信息。在Go语言中,一个简单的规则是:如果一个名字是大写字母开头的,那么该名字是导出的(译注:因为汉字不区分大小写,因此汉字开头的名字是没有导出的)。 为了演示包基本的用法,先假设我们的温度转换软件已经很流行,我们希望到Go语言社区也能使用这个包。我们该如何做呢? 让我们创建一个名为gopl.io/ch2/tempconv的包,这是前面例子的一个改进版本。(这里我们没有按照惯例按顺序对例子进行编号,因此包路径看起来更像一个真实的包)包代码存储在两个源文件中,用来演示如何在一个源文件声明然后在其他的源文件访问;虽然在现实中,这样小的包一般只需要一个文件。 我们把变量的声明、对应的常量,还有方法都放到tempconv.go源文件中: gopl.io/ch2/tempconv // Package tempconv performs Celsius and Fahrenheit conversions.\npackage tempconv import \"fmt\" type Celsius float64\ntype Fahrenheit float64 const ( AbsoluteZeroC Celsius = -273.15 FreezingC Celsius = 0 BoilingC Celsius = 100\n) func (c Celsius) String() string { return fmt.Sprintf(\"%g°C\", c) }\nfunc (f Fahrenheit) String() string { return fmt.Sprintf(\"%g°F\", f) } 转换函数则放在另一个conv.go源文件中: package tempconv // CToF converts a Celsius temperature to Fahrenheit.\nfunc CToF(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) } // FToC converts a Fahrenheit temperature to Celsius.\nfunc FToC(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) } 每个源文件都是以包的声明语句开始,用来指明包的名字。当包被导入的时候,包内的成员将通过类似tempconv.CToF的形式访问。而包级别的名字,例如在一个文件声明的类型和常量,在同一个包的其他源文件也是可以直接访问的,就好像所有代码都在一个文件一样。要注意的是tempconv.go源文件导入了fmt包,但是conv.go源文件并没有,因为这个源文件中的代码并没有用到fmt包。 因为包级别的常量名都是以大写字母开头,它们可以像tempconv.AbsoluteZeroC这样被外部代码访问: fmt.Printf(\"Brrrr! %v\\n\", tempconv.AbsoluteZeroC) // \"Brrrr! -273.15°C\" 要将摄氏温度转换为华氏温度,需要先用import语句导入gopl.io/ch2/tempconv包,然后就可以使用下面的代码进行转换了: fmt.Println(tempconv.CToF(tempconv.BoilingC)) // \"212°F\" 在每个源文件的包声明前紧跟着的注释是包注释(§10.7.4)。通常,包注释的第一句应该先是包的功能概要说明。一个包通常只有一个源文件有包注释(译注:如果有多个包注释,目前的文档工具会根据源文件名的先后顺序将它们链接为一个包注释)。如果包注释很大,通常会放到一个独立的doc.go文件中。 练习 2.1: 向tempconv包添加类型、常量和函数用来处理Kelvin绝对温度的转换,Kelvin 绝对零度是−273.15°C,Kelvin绝对温度1K和摄氏度1°C的单位间隔是一样的。","breadcrumbs":"程序结构 » 包和文件 » 2.6. 包和文件","id":"28","title":"2.6. 包和文件"},"29":{"body":"在Go语言程序中,每个包都有一个全局唯一的导入路径。导入语句中类似\"gopl.io/ch2/tempconv\"的字符串对应包的导入路径。Go语言的规范并没有定义这些字符串的具体含义或包来自哪里,它们是由构建工具来解释的。当使用Go语言自带的go工具箱时(第十章),一个导入路径代表一个目录中的一个或多个Go源文件。 除了包的导入路径,每个包还有一个包名,包名一般是短小的名字(并不要求包名是唯一的),包名在包的声明处指定。按照惯例,一个包的名字和包的导入路径的最后一个字段相同,例如gopl.io/ch2/tempconv包的名字一般是tempconv。 要使用gopl.io/ch2/tempconv包,需要先导入: gopl.io/ch2/cf // Cf converts its numeric argument to Celsius and Fahrenheit.\npackage main import ( \"fmt\" \"os\" \"strconv\" \"gopl.io/ch2/tempconv\"\n) func main() { for _, arg := range os.Args[1:] { t, err := strconv.ParseFloat(arg, 64) if err != nil { fmt.Fprintf(os.Stderr, \"cf: %v\\n\", err) os.Exit(1) } f := tempconv.Fahrenheit(t) c := tempconv.Celsius(t) fmt.Printf(\"%s = %s, %s = %s\\n\", f, tempconv.FToC(f), c, tempconv.CToF(c)) }\n} 导入语句将导入的包绑定到一个短小的名字,然后通过该短小的名字就可以引用包中导出的全部内容。上面的导入声明将允许我们以tempconv.CToF的形式来访问gopl.io/ch2/tempconv包中的内容。在默认情况下,导入的包绑定到tempconv名字(译注:指包声明语句指定的名字),但是我们也可以绑定到另一个名称,以避免名字冲突(§10.4)。 cf程序将命令行输入的一个温度在Celsius和Fahrenheit温度单位之间转换: $ go build gopl.io/ch2/cf\n$ ./cf 32\n32°F = 0°C, 32°C = 89.6°F\n$ ./cf 212\n212°F = 100°C, 212°C = 413.6°F\n$ ./cf -40\n-40°F = -40°C, -40°C = -40°F 如果导入了一个包,但是又没有使用该包将被当作一个编译错误处理。这种强制规则可以有效减少不必要的依赖,虽然在调试期间可能会让人讨厌,因为删除一个类似log.Print(\"got here!\")的打印语句可能导致需要同时删除log包导入声明,否则,编译器将会发出一个错误。在这种情况下,我们需要将不必要的导入删除或注释掉。 不过有更好的解决方案,我们可以使用golang.org/x/tools/cmd/goimports导入工具,它可以根据需要自动添加或删除导入的包;许多编辑器都可以集成goimports工具,然后在保存文件的时候自动运行。类似的还有gofmt工具,可以用来格式化Go源文件。 练习 2.2: 写一个通用的单位转换程序,用类似cf程序的方式从命令行读取参数,如果缺省的话则是从标准输入读取参数,然后做类似Celsius和Fahrenheit的单位转换,长度单位可以对应英尺和米,重量单位可以对应磅和公斤等。","breadcrumbs":"程序结构 » 包和文件 » 2.6.1. 导入包","id":"29","title":"2.6.1. 导入包"},"3":{"body":"所有的编程语言都反映了语言设计者对编程哲学的反思,通常包括之前的语言所暴露的一些不足地方的改进。Go项目是在Google公司维护超级复杂的几个软件系统遇到的一些问题的反思(但是这类问题绝不是Google公司所特有的)。 正如 Rob Pike 所说,“软件的复杂性是乘法级相关的”,通过增加一个部分的复杂性来修复问题通常将慢慢地增加其他部分的复杂性。通过增加功能、选项和配置是修复问题的最快的途径,但是这很容易让人忘记简洁的内涵,即从长远来看,简洁依然是好软件的关键因素。 简洁的设计需要在工作开始的时候舍弃不必要的想法,并且在软件的生命周期内严格区别好的改变和坏的改变。通过足够的努力,一个好的改变可以在不破坏原有完整概念的前提下保持自适应,正如 Fred Brooks 所说的“概念完整性”;而一个坏的改变则不能达到这个效果,它们仅仅是通过肤浅的和简单的妥协来破坏原有设计的一致性。只有通过简洁的设计,才能让一个系统保持稳定、安全和持续的进化。 Go项目包括编程语言本身,附带了相关的工具和标准库,最后但并非代表不重要的是,关于简洁编程哲学的宣言。就事后诸葛的角度来看,Go语言的这些地方都做的还不错:拥有自动垃圾回收、一个包系统、函数作为一等公民、词法作用域、系统调用接口、只读的UTF8字符串等。但是Go语言本身只有很少的特性,也不太可能添加太多的特性。例如,它没有隐式的数值转换,没有构造函数和析构函数,没有运算符重载,没有默认参数,也没有继承,没有泛型,没有异常,没有宏,没有函数修饰,更没有线程局部存储。但是,语言本身是成熟和稳定的,而且承诺保证向后兼容:用之前的Go语言编写程序可以用新版本的Go语言编译器和标准库直接构建而不需要修改代码。 Go语言有足够的类型系统以避免动态语言中那些粗心的类型错误,但是,Go语言的类型系统相比传统的强类型语言又要简洁很多。虽然,有时候这会导致一个“无类型”的抽象类型概念,但是Go语言程序员并不需要像C++或Haskell程序员那样纠结于具体类型的安全属性。在实践中,Go语言简洁的类型系统给程序员带来了更多的安全性和更好的运行时性能。 Go语言鼓励当代计算机系统设计的原则,特别是局部的重要性。它的内置数据类型和大多数的准库数据结构都经过精心设计而避免显式的初始化或隐式的构造函数,因为很少的内存分配和内存初始化代码被隐藏在库代码中了。Go语言的聚合类型(结构体和数组)可以直接操作它们的元素,只需要更少的存储空间、更少的内存写操作,而且指针操作比其他间接操作的语言也更有效率。由于现代计算机是一个并行的机器,Go语言提供了基于CSP的并发特性支持。Go语言的动态栈使得轻量级线程goroutine的初始栈可以很小,因此,创建一个goroutine的代价很小,创建百万级的goroutine完全是可行的。 Go语言的标准库(通常被称为语言自带的电池),提供了清晰的构建模块和公共接口,包含I/O操作、文本处理、图像、密码学、网络和分布式应用程序等,并支持许多标准化的文件格式和编解码协议。库和工具使用了大量的约定来减少额外的配置和解释,从而最终简化程序的逻辑,而且,每个Go程序结构都是如此的相似,因此,Go程序也很容易学习。使用Go语言自带工具构建Go语言项目只需要使用文件名和标识符名称,一个偶尔的特殊注释来确定所有的库、可执行文件、测试、基准测试、例子、以及特定于平台的变量、项目的文档等;Go语言源代码本身就包含了构建规范。","breadcrumbs":"前言 » Go语言项目","id":"3","title":"Go语言项目"},"30":{"body":"包的初始化首先是解决包级变量的依赖顺序,然后按照包级变量声明出现的顺序依次初始化: var a = b + c // a 第三个初始化, 为 3\nvar b = f() // b 第二个初始化, 为 2, 通过调用 f (依赖c)\nvar c = 1 // c 第一个初始化, 为 1 func f() int { return c + 1 } 如果包中含有多个.go源文件,它们将按照发给编译器的顺序进行初始化,Go语言的构建工具首先会将.go文件根据文件名排序,然后依次调用编译器编译。 对于在包级别声明的变量,如果有初始化表达式则用表达式初始化,还有一些没有初始化表达式的,例如某些表格数据初始化并不是一个简单的赋值过程。在这种情况下,我们可以用一个特殊的init初始化函数来简化初始化工作。每个文件都可以包含多个init初始化函数 func init() { /* ... */ } 这样的init初始化函数除了不能被调用或引用外,其他行为和普通函数类似。在每个文件中的init初始化函数,在程序开始执行时按照它们声明的顺序被自动调用。 每个包在解决依赖的前提下,以导入声明的顺序初始化,每个包只会被初始化一次。因此,如果一个p包导入了q包,那么在p包初始化的时候可以认为q包必然已经初始化过了。初始化工作是自下而上进行的,main包最后被初始化。以这种方式,可以确保在main函数执行之前,所有依赖的包都已经完成初始化工作了。 下面的代码定义了一个PopCount函数,用于返回一个数字中含二进制1bit的个数。它使用init初始化函数来生成辅助表格pc,pc表格用于处理每个8bit宽度的数字含二进制的1bit的bit个数,这样的话在处理64bit宽度的数字时就没有必要循环64次,只需要8次查表就可以了。(这并不是最快的统计1bit数目的算法,但是它可以方便演示init函数的用法,并且演示了如何预生成辅助表格,这是编程中常用的技术)。 gopl.io/ch2/popcount package popcount // pc[i] is the population count of i.\nvar pc [256]byte func init() { for i := range pc { pc[i] = pc[i/2] + byte(i&1) }\n} // PopCount returns the population count (number of set bits) of x.\nfunc PopCount(x uint64) int { return int(pc[byte(x>>(0*8))] + pc[byte(x>>(1*8))] + pc[byte(x>>(2*8))] + pc[byte(x>>(3*8))] + pc[byte(x>>(4*8))] + pc[byte(x>>(5*8))] + pc[byte(x>>(6*8))] + pc[byte(x>>(7*8))])\n} 译注:对于pc这类需要复杂处理的初始化,可以通过将初始化逻辑包装为一个匿名函数处理,像下面这样: // pc[i] is the population count of i.\nvar pc [256]byte = func() (pc [256]byte) { for i := range pc { pc[i] = pc[i/2] + byte(i&1) } return\n}() 要注意的是在init函数中,range循环只使用了索引,省略了没有用到的值部分。循环也可以这样写: for i, _ := range pc { 我们在下一节和10.5节还将看到其它使用init函数的地方。 练习 2.3: 重写PopCount函数,用一个循环代替单一的表达式。比较两个版本的性能。(11.4节将展示如何系统地比较两个不同实现的性能。) 练习 2.4: 用移位算法重写PopCount函数,每次测试最右边的1bit,然后统计总数。比较和查表算法的性能差异。 练习 2.5: 表达式x&(x-1)用于将x的最低的一个非零的bit位清零。使用这个算法重写PopCount函数,然后比较性能。","breadcrumbs":"程序结构 » 包和文件 » 2.6.2. 包的初始化","id":"30","title":"2.6.2. 包的初始化"},"31":{"body":"一个声明语句将程序中的实体和一个名字关联,比如一个函数或一个变量。声明语句的作用域是指源代码中可以有效使用这个名字的范围。 不要将作用域和生命周期混为一谈。声明语句的作用域对应的是一个源代码的文本区域;它是一个编译时的属性。一个变量的生命周期是指程序运行时变量存在的有效时间段,在此时间区域内它可以被程序的其他部分引用;是一个运行时的概念。 句法块是由花括弧所包含的一系列语句,就像函数体或循环体花括弧包裹的内容一样。句法块内部声明的名字是无法被外部块访问的。这个块决定了内部声明的名字的作用域范围。我们可以把块(block)的概念推广到包括其他声明的群组,这些声明在代码中并未显式地使用花括号包裹起来,我们称之为词法块。对全局的源代码来说,存在一个整体的词法块,称为全局词法块;对于每个包;每个for、if和switch语句,也都有对应词法块;每个switch或select的分支也有独立的词法块;当然也包括显式书写的词法块(花括弧包含的语句)。 声明语句对应的词法域决定了作用域范围的大小。对于内置的类型、函数和常量,比如int、len和true等是在全局作用域的,因此可以在整个程序中直接使用。任何在函数外部(也就是包级语法域)声明的名字可以在同一个包的任何源文件中访问的。对于导入的包,例如tempconv导入的fmt包,则是对应源文件级的作用域,因此只能在当前的文件中访问导入的fmt包,当前包的其它源文件无法访问在当前源文件导入的包。还有许多声明语句,比如tempconv.CToF函数中的变量c,则是局部作用域的,它只能在函数内部(甚至只能是局部的某些部分)访问。 控制流标号,就是break、continue或goto语句后面跟着的那种标号,则是函数级的作用域。 一个程序可能包含多个同名的声明,只要它们在不同的词法域就没有关系。例如,你可以声明一个局部变量,和包级的变量同名。或者是像2.3.3节的例子那样,你可以将一个函数参数的名字声明为new,虽然内置的new是全局作用域的。但是物极必反,如果滥用不同词法域可重名的特性的话,可能导致程序很难阅读。 当编译器遇到一个名字引用时,它会对其定义进行查找,查找过程从最内层的词法域向全局的作用域进行。如果查找失败,则报告“未声明的名字”这样的错误。如果该名字在内部和外部的块分别声明过,则内部块的声明首先被找到。在这种情况下,内部声明屏蔽了外部同名的声明,让外部的声明的名字无法被访问: func f() {} var g = \"g\" func main() { f := \"f\" fmt.Println(f) // \"f\"; local var f shadows package-level func f fmt.Println(g) // \"g\"; package-level var fmt.Println(h) // compile error: undefined: h\n} 在函数中词法域可以深度嵌套,因此内部的一个声明可能屏蔽外部的声明。还有许多语法块是if或for等控制流语句构造的。下面的代码有三个不同的变量x,因为它们是定义在不同的词法域(这个例子只是为了演示作用域规则,但不是好的编程风格)。 func main() { x := \"hello!\" for i := 0; i < len(x); i++ { x := x[i] if x != '!' { x := x + 'A' - 'a' fmt.Printf(\"%c\", x) // \"HELLO\" (one letter per iteration) } }\n} 在x[i]和x + 'A' - 'a'声明语句的初始化的表达式中都引用了外部作用域声明的x变量,稍后我们会解释这个。(注意,后面的表达式与unicode.ToUpper并不等价。) 正如上面例子所示,并不是所有的词法域都显式地对应到由花括弧包含的语句;还有一些隐含的规则。上面的for语句创建了两个词法域:花括弧包含的是显式的部分,是for的循环体部分词法域,另外一个隐式的部分则是循环的初始化部分,比如用于迭代变量i的初始化。隐式的词法域部分的作用域还包含条件测试部分和循环后的迭代部分(i++),当然也包含循环体词法域。 下面的例子同样有三个不同的x变量,每个声明在不同的词法域,一个在函数体词法域,一个在for隐式的初始化词法域,一个在for循环体词法域;只有两个块是显式创建的: func main() { x := \"hello\" for _, x := range x { x := x + 'A' - 'a' fmt.Printf(\"%c\", x) // \"HELLO\" (one letter per iteration) }\n} 和for循环类似,if和switch语句也会在条件部分创建隐式词法域,还有它们对应的执行体词法域。下面的if-else测试链演示了x和y的有效作用域范围: if x := f(); x == 0 { fmt.Println(x)\n} else if y := g(x); x == y { fmt.Println(x, y)\n} else { fmt.Println(x, y)\n}\nfmt.Println(x, y) // compile error: x and y are not visible here 第二个if语句嵌套在第一个内部,因此第一个if语句条件初始化词法域声明的变量在第二个if中也可以访问。switch语句的每个分支也有类似的词法域规则:条件部分为一个隐式词法域,然后是每个分支的词法域。 在包级别,声明的顺序并不会影响作用域范围,因此一个先声明的可以引用它自身或者是引用后面的一个声明,这可以让我们定义一些相互嵌套或递归的类型或函数。但是如果一个变量或常量递归引用了自身,则会产生编译错误。 在这个程序中: if f, err := os.Open(fname); err != nil { // compile error: unused: f return err\n}\nf.ReadByte() // compile error: undefined f\nf.Close() // compile error: undefined f 变量f的作用域只在if语句内,因此后面的语句将无法引入它,这将导致编译错误。你可能会收到一个局部变量f没有声明的错误提示,具体错误信息依赖编译器的实现。 通常需要在if之前声明变量,这样可以确保后面的语句依然可以访问变量: f, err := os.Open(fname)\nif err != nil { return err\n}\nf.ReadByte()\nf.Close() 你可能会考虑通过将ReadByte和Close移动到if的else块来解决这个问题: if f, err := os.Open(fname); err != nil { return err\n} else { // f and err are visible here too f.ReadByte() f.Close()\n} 但这不是Go语言推荐的做法,Go语言的习惯是在if中处理错误然后直接返回,这样可以确保正常执行的语句不需要代码缩进。 要特别注意短变量声明语句的作用域范围,考虑下面的程序,它的目的是获取当前的工作目录然后保存到一个包级的变量中。这本来可以通过直接调用os.Getwd完成,但是将这个从主逻辑中分离出来可能会更好,特别是在需要处理错误的时候。函数log.Fatalf用于打印日志信息,然后调用os.Exit(1)终止程序。 var cwd string func init() { cwd, err := os.Getwd() // compile error: unused: cwd if err != nil { log.Fatalf(\"os.Getwd failed: %v\", err) }\n} 虽然cwd在外部已经声明过,但是:=语句还是将cwd和err重新声明为新的局部变量。因为内部声明的cwd将屏蔽外部的声明,因此上面的代码并不会正确更新包级声明的cwd变量。 由于当前的编译器会检测到局部声明的cwd并没有使用,然后报告这可能是一个错误,但是这种检测并不可靠。因为一些小的代码变更,例如增加一个局部cwd的打印语句,就可能导致这种检测失效。 var cwd string func init() { cwd, err := os.Getwd() // NOTE: wrong! if err != nil { log.Fatalf(\"os.Getwd failed: %v\", err) } log.Printf(\"Working directory = %s\", cwd)\n} 全局的cwd变量依然是没有被正确初始化的,而且看似正常的日志输出更是让这个BUG更加隐晦。 有许多方式可以避免出现类似潜在的问题。最直接的方法是通过单独声明err变量,来避免使用:=的简短声明方式: var cwd string func init() { var err error cwd, err = os.Getwd() if err != nil { log.Fatalf(\"os.Getwd failed: %v\", err) }\n} 我们已经看到包、文件、声明和语句如何来表达一个程序结构。在下面的两个章节,我们将探讨数据的结构。","breadcrumbs":"程序结构 » 作用域 » 2.7. 作用域","id":"31","title":"2.7. 作用域"},"32":{"body":"虽然从底层而言,所有的数据都是由比特组成,但计算机一般操作的是固定大小的数,如整数、浮点数、比特数组、内存地址等。进一步将这些数组织在一起,就可表达更多的对象,例如数据包、像素点、诗歌,甚至其他任何对象。Go语言提供了丰富的数据组织形式,这依赖于Go语言内置的数据类型。这些内置的数据类型,兼顾了硬件的特性和表达复杂数据结构的便捷性。 Go语言将数据类型分为四类:基础类型、复合类型、引用类型和接口类型。本章介绍基础类型,包括:数字、字符串和布尔型。复合数据类型——数组(§4.1)和结构体(§4.2)——是通过组合简单类型,来表达更加复杂的数据结构。引用类型包括指针(§2.3.2)、切片(§4.2))、字典(§4.3)、函数(§5)、通道(§8),虽然数据种类很多,但它们都是对程序中一个变量或状态的间接引用。这意味着对任一引用类型数据的修改都会影响所有该引用的拷贝。我们将在第7章介绍接口类型。","breadcrumbs":"基础数据类型 » 第3章 基础数据类型","id":"32","title":"第3章 基础数据类型"},"33":{"body":"Go语言的数值类型包括几种不同大小的整数、浮点数和复数。每种数值类型都决定了对应的大小范围和是否支持正负符号。让我们先从整数类型开始介绍。 Go语言同时提供了有符号和无符号类型的整数运算。这里有int8、int16、int32和int64四种截然不同大小的有符号整数类型,分别对应8、16、32、64bit大小的有符号整数,与此对应的是uint8、uint16、uint32和uint64四种无符号整数类型。 这里还有两种一般对应特定CPU平台机器字大小的有符号和无符号整数int和uint;其中int是应用最广泛的数值类型。这两种类型都有同样的大小,32或64bit,但是我们不能对此做任何的假设;因为不同的编译器即使在相同的硬件平台上可能产生不同的大小。 Unicode字符rune类型是和int32等价的类型,通常用于表示一个Unicode码点。这两个名称可以互换使用。同样byte也是uint8类型的等价类型,byte类型一般用于强调数值是一个原始的数据而不是一个小的整数。 最后,还有一种无符号的整数类型uintptr,没有指定具体的bit大小但是足以容纳指针。uintptr类型只有在底层编程时才需要,特别是Go语言和C语言函数库或操作系统接口相交互的地方。我们将在第十三章的unsafe包相关部分看到类似的例子。 不管它们的具体大小,int、uint和uintptr是不同类型的兄弟类型。其中int和int32也是不同的类型,即使int的大小也是32bit,在需要将int当作int32类型的地方需要一个显式的类型转换操作,反之亦然。 其中有符号整数采用2的补码形式表示,也就是最高bit位用来表示符号位,一个n-bit的有符号数的值域是从$-2^{n-1}$到$2^{n-1}-1$。无符号整数的所有bit位都用于表示非负数,值域是0到$2^n-1$。例如,int8类型整数的值域是从-128到127,而uint8类型整数的值域是从0到255。 下面是Go语言中关于算术运算、逻辑运算和比较运算的二元运算符,它们按照优先级递减的顺序排列: * / % << >> & &^\n+ - | ^\n== != < <= > >=\n&&\n|| 二元运算符有五种优先级。在同一个优先级,使用左优先结合规则,但是使用括号可以明确优先顺序,使用括号也可以用于提升优先级,例如mask & (1 << 28)。 对于上表中前两行的运算符,例如+运算符还有一个与赋值相结合的对应运算符+=,可以用于简化赋值语句。 算术运算符+、-、*和/可以适用于整数、浮点数和复数,但是取模运算符%仅用于整数间的运算。对于不同编程语言,%取模运算的行为可能并不相同。在Go语言中,%取模运算符的符号和被取模数的符号总是一致的,因此-5%3和-5%-3结果都是-2。除法运算符/的行为则依赖于操作数是否全为整数,比如5.0/4.0的结果是1.25,但是5/4的结果是1,因为整数除法会向着0方向截断余数。 一个算术运算的结果,不管是有符号或者是无符号的,如果需要更多的bit位才能正确表示的话,就说明计算结果是溢出了。超出的高位的bit位部分将被丢弃。如果原始的数值是有符号类型,而且最左边的bit位是1的话,那么最终结果可能是负的,例如int8的例子: var u uint8 = 255\nfmt.Println(u, u+1, u*u) // \"255 0 1\" var i int8 = 127\nfmt.Println(i, i+1, i*i) // \"127 -128 1\" 两个相同的整数类型可以使用下面的二元比较运算符进行比较;比较表达式的结果是布尔类型。 == 等于\n!= 不等于\n< 小于\n<= 小于等于\n> 大于\n>= 大于等于 事实上,布尔型、数字类型和字符串等基本类型都是可比较的,也就是说两个相同类型的值可以用==和!=进行比较。此外,整数、浮点数和字符串可以根据比较结果排序。许多其它类型的值可能是不可比较的,因此也就可能是不可排序的。对于我们遇到的每种类型,我们需要保证规则的一致性。 这里是一元的加法和减法运算符: + 一元加法(无效果)\n- 负数 对于整数,+x是0+x的简写,-x则是0-x的简写;对于浮点数和复数,+x就是x,-x则是x 的负数。 Go语言还提供了以下的bit位操作运算符,前面4个操作运算符并不区分是有符号还是无符号数: & 位运算 AND\n| 位运算 OR\n^ 位运算 XOR\n&^ 位清空(AND NOT)\n<< 左移\n>> 右移 位操作运算符^作为二元运算符时是按位异或(XOR),当用作一元运算符时表示按位取反;也就是说,它返回一个每个bit位都取反的数。位操作运算符&^用于按位置零(AND NOT):如果对应y中bit位为1的话,表达式z = x &^ y结果z的对应的bit位为0,否则z对应的bit位等于x相应的bit位的值。 下面的代码演示了如何使用位操作解释uint8类型值的8个独立的bit位。它使用了Printf函数的%b参数打印二进制格式的数字;其中%08b中08表示打印至少8个字符宽度,不足的前缀部分用0填充。 var x uint8 = 1<<1 | 1<<5\nvar y uint8 = 1<<1 | 1<<2 fmt.Printf(\"%08b\\n\", x) // \"00100010\", the set {1, 5}\nfmt.Printf(\"%08b\\n\", y) // \"00000110\", the set {1, 2} fmt.Printf(\"%08b\\n\", x&y) // \"00000010\", the intersection {1}\nfmt.Printf(\"%08b\\n\", x|y) // \"00100110\", the union {1, 2, 5}\nfmt.Printf(\"%08b\\n\", x^y) // \"00100100\", the symmetric difference {2, 5}\nfmt.Printf(\"%08b\\n\", x&^y) // \"00100000\", the difference {5} for i := uint(0); i < 8; i++ { if x&(1<>1) // \"00010001\", the set {0, 4} (6.5节给出了一个可以远大于一个字节的整数集的实现。) 在x<>n移位运算中,决定了移位操作的bit数部分必须是无符号数;被操作的x可以是有符号数或无符号数。算术上,一个x<>n右移运算等价于除以$2^n$。 左移运算用零填充右边空缺的bit位,无符号数的右移运算也是用0填充左边空缺的bit位,但是有符号数的右移运算会用符号位的值填充左边空缺的bit位。因为这个原因,最好用无符号运算,这样你可以将整数完全当作一个bit位模式处理。 尽管Go语言提供了无符号数的运算,但即使数值本身不可能出现负数,我们还是倾向于使用有符号的int类型,就像数组的长度那样,虽然使用uint无符号类型似乎是一个更合理的选择。事实上,内置的len函数返回一个有符号的int,我们可以像下面例子那样处理逆序循环。 medals := []string{\"gold\", \"silver\", \"bronze\"}\nfor i := len(medals) - 1; i >= 0; i-- { fmt.Println(medals[i]) // \"bronze\", \"silver\", \"gold\"\n} 另一个选择对于上面的例子来说将是灾难性的。如果len函数返回一个无符号数,那么i也将是无符号的uint类型,然后条件i >= 0则永远为真。在三次迭代之后,也就是i == 0时,i--语句将不会产生-1,而是变成一个uint类型的最大值(可能是$2^64-1$),然后medals[i]表达式运行时将发生panic异常(§5.9),也就是试图访问一个slice范围以外的元素。 出于这个原因,无符号数往往只有在位运算或其它特殊的运算场景才会使用,就像bit集合、分析二进制文件格式或者是哈希和加密操作等。它们通常并不用于仅仅是表达非负数量的场合。 一般来说,需要一个显式的转换将一个值从一种类型转化为另一种类型,并且算术和逻辑运算的二元操作中必须是相同的类型。虽然这偶尔会导致需要很长的表达式,但是它消除了所有和类型相关的问题,而且也使得程序容易理解。 在很多场景,会遇到类似下面代码的常见的错误: var apples int32 = 1\nvar oranges int16 = 2\nvar compote int = apples + oranges // compile error 当尝试编译这三个语句时,将产生一个错误信息: invalid operation: apples + oranges (mismatched types int32 and int16) 这种类型不匹配的问题可以有几种不同的方法修复,最常见方法是将它们都显式转型为一个常见类型: var compote = int(apples) + int(oranges) 如2.5节所述,对于每种类型T,如果转换允许的话,类型转换操作T(x)将x转换为T类型。许多整数之间的相互转换并不会改变数值;它们只是告诉编译器如何解释这个值。但是对于将一个大尺寸的整数类型转为一个小尺寸的整数类型,或者是将一个浮点数转为整数,可能会改变数值或丢失精度: f := 3.141 // a float64\ni := int(f)\nfmt.Println(f, i) // \"3.141 3\"\nf = 1.99\nfmt.Println(int(f)) // \"1\" 浮点数到整数的转换将丢失任何小数部分,然后向数轴零方向截断。你应该避免对可能会超出目标类型表示范围的数值做类型转换,因为截断的行为可能依赖于具体的实现: f := 1e100 // a float64\ni := int(f) // 结果依赖于具体实现 任何大小的整数字面值都可以用以0开始的八进制格式书写,例如0666;或用以0x或0X开头的十六进制格式书写,例如0xdeadbeef。十六进制数字可以用大写或小写字母。如今八进制数据通常用于POSIX操作系统上的文件访问权限标志,十六进制数字则更强调数字值的bit位模式。 当使用fmt包打印一个数值时,我们可以用%d、%o或%x参数控制输出的进制格式,就像下面的例子: o := 0666\nfmt.Printf(\"%d %[1]o %#[1]o\\n\", o) // \"438 666 0666\"\nx := int64(0xdeadbeef)\nfmt.Printf(\"%d %[1]x %#[1]x %#[1]X\\n\", x)\n// Output:\n// 3735928559 deadbeef 0xdeadbeef 0XDEADBEEF 请注意fmt的两个使用技巧。通常Printf格式化字符串包含多个%参数时将会包含对应相同数量的额外操作数,但是%之后的[1]副词告诉Printf函数再次使用第一个操作数。第二,%后的#副词告诉Printf在用%o、%x或%X输出时生成0、0x或0X前缀。 字符面值通过一对单引号直接包含对应字符。最简单的例子是ASCII中类似'a'写法的字符面值,但是我们也可以通过转义的数值来表示任意的Unicode码点对应的字符,马上将会看到这样的例子。 字符使用%c参数打印,或者是用%q参数打印带单引号的字符: ascii := 'a'\nunicode := '国'\nnewline := '\\n'\nfmt.Printf(\"%d %[1]c %[1]q\\n\", ascii) // \"97 a 'a'\"\nfmt.Printf(\"%d %[1]c %[1]q\\n\", unicode) // \"22269 国 '国'\"\nfmt.Printf(\"%d %[1]q\\n\", newline) // \"10 '\\n'\"","breadcrumbs":"基础数据类型 » 整型 » 3.1. 整型","id":"33","title":"3.1. 整型"},"34":{"body":"Go语言提供了两种精度的浮点数,float32和float64。它们的算术规范由IEEE754浮点数国际标准定义,该浮点数规范被所有现代的CPU支持。 这些浮点数类型的取值范围可以从很微小到很巨大。浮点数的范围极限值可以在math包找到。常量math.MaxFloat32表示float32能表示的最大数值,大约是 3.4e38;对应的math.MaxFloat64常量大约是1.8e308。它们分别能表示的最小值近似为1.4e-45和4.9e-324。 一个float32类型的浮点数可以提供大约6个十进制数的精度,而float64则可以提供约15个十进制数的精度;通常应该优先使用float64类型,因为float32类型的累计计算误差很容易扩散,并且float32能精确表示的正整数并不是很大(译注:因为float32的有效bit位只有23个,其它的bit位用于指数和符号;当整数大于23bit能表达的范围时,float32的表示将出现误差): var f float32 = 16777216 // 1 << 24\nfmt.Println(f == f+1) // \"true\"! 浮点数的字面值可以直接写小数部分,像这样: const e = 2.71828 // (approximately) 小数点前面或后面的数字都可能被省略(例如.707或1.)。很小或很大的数最好用科学计数法书写,通过e或E来指定指数部分: const Avogadro = 6.02214129e23 // 阿伏伽德罗常数\nconst Planck = 6.62606957e-34 // 普朗克常数 用Printf函数的%g参数打印浮点数,将采用更紧凑的表示形式打印,并提供足够的精度,但是对应表格的数据,使用%e(带指数)或%f的形式打印可能更合适。所有的这三个打印形式都可以指定打印的宽度和控制打印精度。 for x := 0; x < 8; x++ { fmt.Printf(\"x = %d e^x = %8.3f\\n\", x, math.Exp(float64(x)))\n} 上面代码打印e的幂,打印精度是小数点后三个小数精度和8个字符宽度: x = 0 e^x = 1.000\nx = 1 e^x = 2.718\nx = 2 e^x = 7.389\nx = 3 e^x = 20.086\nx = 4 e^x = 54.598\nx = 5 e^x = 148.413\nx = 6 e^x = 403.429\nx = 7 e^x = 1096.633 math包中除了提供大量常用的数学函数外,还提供了IEEE754浮点数标准中定义的特殊值的创建和测试:正无穷大和负无穷大,分别用于表示太大溢出的数字和除零的结果;还有NaN非数,一般用于表示无效的除法操作结果0/0或Sqrt(-1). var z float64\nfmt.Println(z, -z, 1/z, -1/z, z/z) // \"0 -0 +Inf -Inf NaN\" 函数math.IsNaN用于测试一个数是否是非数NaN,math.NaN则返回非数对应的值。虽然可以用math.NaN来表示一个非法的结果,但是测试一个结果是否是非数NaN则是充满风险的,因为NaN和任何数都是不相等的(译注:在浮点数中,NaN、正无穷大和负无穷大都不是唯一的,每个都有非常多种的bit模式表示): nan := math.NaN()\nfmt.Println(nan == nan, nan < nan, nan > nan) // \"false false false\" 如果一个函数返回的浮点数结果可能失败,最好的做法是用单独的标志报告失败,像这样: func compute() (value float64, ok bool) { // ... if failed { return 0, false } return result, true\n} 接下来的程序演示了通过浮点计算生成的图形。它是带有两个参数的z = f(x, y)函数的三维形式,使用了可缩放矢量图形(SVG)格式输出,SVG是一个用于矢量线绘制的XML标准。图3.1显示了sin(r)/r函数的输出图形,其中r是sqrt(x*x+y*y)。 gopl.io/ch3/surface // Surface computes an SVG rendering of a 3-D surface function.\npackage main import ( \"fmt\" \"math\"\n) const ( width, height = 600, 320 // canvas size in pixels cells = 100 // number of grid cells xyrange = 30.0 // axis ranges (-xyrange..+xyrange) xyscale = width / 2 / xyrange // pixels per x or y unit zscale = height * 0.4 // pixels per z unit angle = math.Pi / 6 // angle of x, y axes (=30°)\n) var sin30, cos30 = math.Sin(angle), math.Cos(angle) // sin(30°), cos(30°) func main() { fmt.Printf(\"\", width, height) for i := 0; i < cells; i++ { for j := 0; j < cells; j++ { ax, ay := corner(i+1, j) bx, by := corner(i, j) cx, cy := corner(i, j+1) dx, dy := corner(i+1, j+1) fmt.Printf(\"\\n\", ax, ay, bx, by, cx, cy, dx, dy) } } fmt.Println(\"\")\n} func corner(i, j int) (float64, float64) { // Find point (x,y) at corner of cell (i,j). x := xyrange * (float64(i)/cells - 0.5) y := xyrange * (float64(j)/cells - 0.5) // Compute surface height z. z := f(x, y) // Project (x,y,z) isometrically onto 2-D SVG canvas (sx,sy). sx := width/2 + (x-y)*cos30*xyscale sy := height/2 + (x+y)*sin30*xyscale - z*zscale return sx, sy\n} func f(x, y float64) float64 { r := math.Hypot(x, y) // distance from (0,0) return math.Sin(r) / r\n} 要注意的是corner函数返回了两个结果,分别对应每个网格顶点的坐标参数。 要解释这个程序是如何工作的需要一些基本的几何学知识,但是我们可以跳过几何学原理,因为程序的重点是演示浮点数运算。程序的本质是三个不同的坐标系中映射关系,如图3.2所示。第一个是100x100的二维网格,对应整数坐标(i,j),从远处的(0,0)位置开始。我们从远处向前面绘制,因此远处先绘制的多边形有可能被前面后绘制的多边形覆盖。 第二个坐标系是一个三维的网格浮点坐标(x,y,z),其中x和y是i和j的线性函数,通过平移转换为网格单元的中心,然后用xyrange系数缩放。高度z是函数f(x,y)的值。 第三个坐标系是一个二维的画布,起点(0,0)在左上角。画布中点的坐标用(sx,sy)表示。我们使用等角投影将三维点(x,y,z)投影到二维的画布中。 画布中从远处到右边的点对应较大的x值和较大的y值。并且画布中x和y值越大,则对应的z值越小。x和y的垂直和水平缩放系数来自30度角的正弦和余弦值。z的缩放系数0.4,是一个任意选择的参数。 对于二维网格中的每一个网格单元,main函数计算单元的四个顶点在画布中对应多边形ABCD的顶点,其中B对应(i,j)顶点位置,A、C和D是其它相邻的顶点,然后输出SVG的绘制指令。 练习 3.1: 如果f函数返回的是无限制的float64值,那么SVG文件可能输出无效的多边形元素(虽然许多SVG渲染器会妥善处理这类问题)。修改程序跳过无效的多边形。 练习 3.2: 试验math包中其他函数的渲染图形。你是否能输出一个egg box、moguls或a saddle图案? 练习 3.3: 根据高度给每个多边形上色,那样峰值部将是红色(#ff0000),谷部将是蓝色(#0000ff)。 练习 3.4: 参考1.7节Lissajous例子的函数,构造一个web服务器,用于计算函数曲面然后返回SVG数据给客户端。服务器必须设置Content-Type头部: w.Header().Set(\"Content-Type\", \"image/svg+xml\") (这一步在Lissajous例子中不是必须的,因为服务器使用标准的PNG图像格式,可以根据前面的512个字节自动输出对应的头部。)允许客户端通过HTTP请求参数设置高度、宽度和颜色等参数。","breadcrumbs":"基础数据类型 » 浮点数 » 3.2. 浮点数","id":"34","title":"3.2. 浮点数"},"35":{"body":"Go语言提供了两种精度的复数类型:complex64和complex128,分别对应float32和float64两种浮点数精度。内置的complex函数用于构建复数,内建的real和imag函数分别返回复数的实部和虚部: var x complex128 = complex(1, 2) // 1+2i\nvar y complex128 = complex(3, 4) // 3+4i\nfmt.Println(x*y) // \"(-5+10i)\"\nfmt.Println(real(x*y)) // \"-5\"\nfmt.Println(imag(x*y)) // \"10\" 如果一个浮点数面值或一个十进制整数面值后面跟着一个i,例如3.141592i或2i,它将构成一个复数的虚部,复数的实部是0: fmt.Println(1i * 1i) // \"(-1+0i)\", i^2 = -1 在常量算术规则下,一个复数常量可以加到另一个普通数值常量(整数或浮点数、实部或虚部),我们可以用自然的方式书写复数,就像1+2i或与之等价的写法2i+1。上面x和y的声明语句还可以简化: x := 1 + 2i\ny := 3 + 4i 复数也可以用==和!=进行相等比较。只有两个复数的实部和虚部都相等的时候它们才是相等的(译注:浮点数的相等比较是危险的,需要特别小心处理精度问题)。 math/cmplx包提供了复数处理的许多函数,例如求复数的平方根函数和求幂函数。 fmt.Println(cmplx.Sqrt(-1)) // \"(0+1i)\" 下面的程序使用complex128复数算法来生成一个Mandelbrot图像。 gopl.io/ch3/mandelbrot // Mandelbrot emits a PNG image of the Mandelbrot fractal.\npackage main import ( \"image\" \"image/color\" \"image/png\" \"math/cmplx\" \"os\"\n) func main() { const ( xmin, ymin, xmax, ymax = -2, -2, +2, +2 width, height = 1024, 1024 ) img := image.NewRGBA(image.Rect(0, 0, width, height)) for py := 0; py < height; py++ { y := float64(py)/height*(ymax-ymin) + ymin for px := 0; px < width; px++ { x := float64(px)/width*(xmax-xmin) + xmin z := complex(x, y) // Image point (px, py) represents complex value z. img.Set(px, py, mandelbrot(z)) } } png.Encode(os.Stdout, img) // NOTE: ignoring errors\n} func mandelbrot(z complex128) color.Color { const iterations = 200 const contrast = 15 var v complex128 for n := uint8(0); n < iterations; n++ { v = v*v + z if cmplx.Abs(v) > 2 { return color.Gray{255 - contrast*n} } } return color.Black\n} 用于遍历1024x1024图像每个点的两个嵌套的循环对应-2到+2区间的复数平面。程序反复测试每个点对应复数值平方值加一个增量值对应的点是否超出半径为2的圆。如果超过了,通过根据预设置的逃逸迭代次数对应的灰度颜色来代替。如果不是,那么该点属于Mandelbrot集合,使用黑色颜色标记。最终程序将生成的PNG格式分形图像输出到标准输出,如图3.3所示。 练习 3.5: 实现一个彩色的Mandelbrot图像,使用image.NewRGBA创建图像,使用color.RGBA或color.YCbCr生成颜色。 练习 3.6: 升采样技术可以降低每个像素对计算颜色值和平均值的影响。简单的方法是将每个像素分成四个子像素,实现它。 练习 3.7: 另一个生成分形图像的方式是使用牛顿法来求解一个复数方程,例如$z^4-1=0$。每个起点到四个根的迭代次数对应阴影的灰度。方程根对应的点用颜色表示。 练习 3.8: 通过提高精度来生成更多级别的分形。使用四种不同精度类型的数字实现相同的分形:complex64、complex128、big.Float和big.Rat。(后面两种类型在math/big包声明。Float是有指定限精度的浮点数;Rat是无限精度的有理数。)它们间的性能和内存使用对比如何?当渲染图可见时缩放的级别是多少? 练习 3.9: 编写一个web服务器,用于给客户端生成分形的图像。运行客户端通过HTTP参数指定x、y和zoom参数。","breadcrumbs":"基础数据类型 » 复数 » 3.3. 复数","id":"35","title":"3.3. 复数"},"36":{"body":"一个布尔类型的值只有两种:true和false。if和for语句的条件部分都是布尔类型的值,并且==和<等比较操作也会产生布尔型的值。一元操作符!对应逻辑非操作,因此!true的值为false,更罗嗦的说法是(!true==false)==true,虽然表达方式不一样,不过我们一般会采用简洁的布尔表达式,就像用x来表示x==true。 布尔值可以和&&(AND)和||(OR)操作符结合,并且有短路行为:如果运算符左边值已经可以确定整个布尔表达式的值,那么运算符右边的值将不再被求值,因此下面的表达式总是安全的: s != \"\" && s[0] == 'x' 其中s[0]操作如果应用于空字符串将会导致panic异常。 因为&&的优先级比||高(助记:&&对应逻辑乘法,||对应逻辑加法,乘法比加法优先级要高),下面形式的布尔表达式是不需要加小括弧的: if 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || '0' <= c && c <= '9' { // ...ASCII letter or digit...\n} 布尔值并不会隐式转换为数字值0或1,反之亦然。必须使用一个显式的if语句辅助转换: i := 0\nif b { i = 1\n} 如果需要经常做类似的转换,包装成一个函数会更方便: // btoi returns 1 if b is true and 0 if false.\nfunc btoi(b bool) int { if b { return 1 } return 0\n} 数字到布尔型的逆转换则非常简单,不过为了保持对称,我们也可以包装一个函数: // itob reports whether i is non-zero.\nfunc itob(i int) bool { return i != 0 }","breadcrumbs":"基础数据类型 » 布尔型 » 3.4. 布尔型","id":"36","title":"3.4. 布尔型"},"37":{"body":"一个字符串是一个不可改变的字节序列。字符串可以包含任意的数据,包括byte值0,但是通常是用来包含人类可读的文本。文本字符串通常被解释为采用UTF8编码的Unicode码点(rune)序列,我们稍后会详细讨论这个问题。 内置的len函数可以返回一个字符串中的字节数目(不是rune字符数目),索引操作s[i]返回第i个字节的字节值,i必须满足0 ≤ i< len(s)条件约束。 s := \"hello, world\"\nfmt.Println(len(s)) // \"12\"\nfmt.Println(s[0], s[7]) // \"104 119\" ('h' and 'w') 如果试图访问超出字符串索引范围的字节将会导致panic异常: c := s[len(s)] // panic: index out of range 第i个字节并不一定是字符串的第i个字符,因为对于非ASCII字符的UTF8编码会要两个或多个字节。我们先简单说下字符的工作方式。 子字符串操作s[i:j]基于原始的s字符串的第i个字节开始到第j个字节(并不包含j本身)生成一个新字符串。生成的新字符串将包含j-i个字节。 fmt.Println(s[0:5]) // \"hello\" 同样,如果索引超出字符串范围或者j小于i的话将导致panic异常。 不管i还是j都可能被忽略,当它们被忽略时将采用0作为开始位置,采用len(s)作为结束的位置。 fmt.Println(s[:5]) // \"hello\"\nfmt.Println(s[7:]) // \"world\"\nfmt.Println(s[:]) // \"hello, world\" 其中+操作符将两个字符串连接构造一个新字符串: fmt.Println(\"goodbye\" + s[5:]) // \"goodbye, world\" 字符串可以用==和<进行比较;比较通过逐个字节比较完成的,因此比较的结果是字符串自然编码的顺序。 字符串的值是不可变的:一个字符串包含的字节序列永远不会被改变,当然我们也可以给一个字符串变量分配一个新字符串值。可以像下面这样将一个字符串追加到另一个字符串: s := \"left foot\"\nt := s\ns += \", right foot\" 这并不会导致原始的字符串值被改变,但是变量s将因为+=语句持有一个新的字符串值,但是t依然是包含原先的字符串值。 fmt.Println(s) // \"left foot, right foot\"\nfmt.Println(t) // \"left foot\" 因为字符串是不可修改的,因此尝试修改字符串内部数据的操作也是被禁止的: s[0] = 'L' // compile error: cannot assign to s[0] 不变性意味着如果两个字符串共享相同的底层数据的话也是安全的,这使得复制任何长度的字符串代价是低廉的。同样,一个字符串s和对应的子字符串切片s[7:]的操作也可以安全地共享相同的内存,因此字符串切片操作代价也是低廉的。在这两种情况下都没有必要分配新的内存。 图3.4演示了一个字符串和两个子串共享相同的底层数据。","breadcrumbs":"基础数据类型 » 字符串 » 3.5. 字符串","id":"37","title":"3.5. 字符串"},"38":{"body":"字符串值也可以用字符串面值方式编写,只要将一系列字节序列包含在双引号内即可: \"Hello, 世界\" 因为Go语言源文件总是用UTF8编码,并且Go语言的文本字符串也以UTF8编码的方式处理,因此我们可以将Unicode码点也写到字符串面值中。 在一个双引号包含的字符串面值中,可以用以反斜杠\\开头的转义序列插入任意的数据。下面的换行、回车和制表符等是常见的ASCII控制代码的转义方式: \\a 响铃\n\\b 退格\n\\f 换页\n\\n 换行\n\\r 回车\n\\t 制表符\n\\v 垂直制表符\n\\' 单引号(只用在 '\\'' 形式的rune符号面值中)\n\\\" 双引号(只用在 \"...\" 形式的字符串面值中)\n\\\\ 反斜杠 可以通过十六进制或八进制转义在字符串面值中包含任意的字节。一个十六进制的转义形式是\\xhh,其中两个h表示十六进制数字(大写或小写都可以)。一个八进制转义形式是\\ooo,包含三个八进制的o数字(0到7),但是不能超过\\377(译注:对应一个字节的范围,十进制为255)。每一个单一的字节表达一个特定的值。稍后我们将看到如何将一个Unicode码点写到字符串面值中。 一个原生的字符串面值形式是`...`,使用反引号代替双引号。在原生的字符串面值中,没有转义操作;全部的内容都是字面的意思,包含退格和换行,因此一个程序中的原生字符串面值可能跨越多行(译注:在原生字符串面值内部是无法直接写`字符的,可以用八进制或十六进制转义或+\"`\"连接字符串常量完成)。唯一的特殊处理是会删除回车以保证在所有平台上的值都是一样的,包括那些把回车也放入文本文件的系统(译注:Windows系统会把回车和换行一起放入文本文件中)。 原生字符串面值用于编写正则表达式会很方便,因为正则表达式往往会包含很多反斜杠。原生字符串面值同时被广泛应用于HTML模板、JSON面值、命令行提示信息以及那些需要扩展到多行的场景。 const GoUsage = `Go is a tool for managing Go source code. Usage: go command [arguments]\n...`","breadcrumbs":"基础数据类型 » 字符串 » 3.5.1. 字符串面值","id":"38","title":"3.5.1. 字符串面值"},"39":{"body":"在很久以前,世界还是比较简单的,起码计算机世界就只有一个ASCII字符集:美国信息交换标准代码。ASCII,更准确地说是美国的ASCII,使用7bit来表示128个字符:包含英文字母的大小写、数字、各种标点符号和设备控制符。对于早期的计算机程序来说,这些就足够了,但是这也导致了世界上很多其他地区的用户无法直接使用自己的符号系统。随着互联网的发展,混合多种语言的数据变得很常见(译注:比如本身的英文原文或中文翻译都包含了ASCII、中文、日文等多种语言字符)。如何有效处理这些包含了各种语言的丰富多样的文本数据呢? 答案就是使用Unicode( http://unicode.org ),它收集了这个世界上所有的符号系统,包括重音符号和其它变音符号,制表符和回车符,还有很多神秘的符号,每个符号都分配一个唯一的Unicode码点,Unicode码点对应Go语言中的rune整数类型(译注:rune是int32等价类型)。 在第八版本的Unicode标准里收集了超过120,000个字符,涵盖超过100多种语言。这些在计算机程序和数据中是如何体现的呢?通用的表示一个Unicode码点的数据类型是int32,也就是Go语言中rune对应的类型;它的同义词rune符文正是这个意思。 我们可以将一个符文序列表示为一个int32序列。这种编码方式叫UTF-32或UCS-4,每个Unicode码点都使用同样大小的32bit来表示。这种方式比较简单统一,但是它会浪费很多存储空间,因为大多数计算机可读的文本是ASCII字符,本来每个ASCII字符只需要8bit或1字节就能表示。而且即使是常用的字符也远少于65,536个,也就是说用16bit编码方式就能表达常用字符。但是,还有其它更好的编码方法吗?","breadcrumbs":"基础数据类型 » 字符串 » 3.5.2. Unicode","id":"39","title":"3.5.2. Unicode"},"4":{"body":"我们假设你已经有一种或多种其他编程语言的使用经历,不管是类似C、C++或Java的编译型语言,还是类似Python、Ruby、JavaScript的脚本语言,因此我们不会像对完全的编程语言初学者那样解释所有的细节。因为,Go语言的变量、常量、表达式、控制流和函数等基本语法也是类似的。 第一章包含了本教程的基本结构,通过十几个程序介绍了用Go语言如何实现类似读写文件、文本格式化、创建图像、网络客户端和服务器通讯等日常工作。 第二章描述了Go语言程序的基本元素结构、变量、新类型定义、包和文件、以及作用域等概念。第三章讨论了数字、布尔值、字符串和常量,并演示了如何显示和处理Unicode字符。第四章描述了复合类型,从简单的数组、字典、切片到动态列表。第五章涵盖了函数,并讨论了错误处理、panic和recover,还有defer语句。 第一章到第五章是基础部分,主流命令式编程语言这部分都类似。个别之处,Go语言有自己特色的语法和风格,但是大多数程序员能很快适应。其余章节是Go语言特有的:方法、接口、并发、包、测试和反射等语言特性。 Go语言的面向对象机制与一般语言不同。它没有类层次结构,甚至可以说没有类;仅仅通过组合(而不是继承)简单的对象来构建复杂的对象。方法不仅可以定义在结构体上,而且,可以定义在任何用户自定义的类型上;并且,具体类型和抽象类型(接口)之间的关系是隐式的,所以很多类型的设计者可能并不知道该类型到底实现了哪些接口。方法在第六章讨论,接口在第七章讨论。 第八章讨论了基于顺序通信进程(CSP)概念的并发编程,使用goroutines和channels处理并发编程。第九章则讨论了传统的基于共享变量的并发编程。 第十章描述了包机制和包的组织结构。这一章还展示了如何有效地利用Go自带的工具,使用单个命令完成编译、测试、基准测试、代码格式化、文档以及其他诸多任务。 第十一章讨论了单元测试,Go语言的工具和标准库中集成了轻量级的测试功能,避免了强大但复杂的测试框架。测试库提供了一些基本构件,必要时可以用来构建复杂的测试构件。 第十二章讨论了反射,一种程序在运行期间审视自己的能力。反射是一个强大的编程工具,不过要谨慎地使用;这一章利用反射机制实现一些重要的Go语言库函数,展示了反射的强大用法。第十三章解释了底层编程的细节,在必要时,可以使用unsafe包绕过Go语言安全的类型系统。 每一章都有一些练习题,你可以用来测试你对Go的理解,你也可以探讨书中这些例子的扩展和替代。 书中所有的代码都可以从 http://gopl.io 上的Git仓库下载。go get命令根据每个例子的导入路径智能地获取、构建并安装。只需要选择一个目录作为工作空间,然后将GOPATH环境变量设置为该路径。 必要时,Go语言工具会创建目录。例如: $ export GOPATH=$HOME/gobook # 选择工作目录\n$ go get gopl.io/ch1/helloworld # 获取/编译/安装\n$ $GOPATH/bin/helloworld # 运行程序\nHello, 世界 # 这是中文 运行这些例子需要安装Go1.5以上的版本。 $ go version\ngo version go1.5 linux/amd64 如果使用其他的操作系统,请参考 https://golang.org/doc/install 提供的说明安装。","breadcrumbs":"前言 » 本书的组织","id":"4","title":"本书的组织"},"40":{"body":"UTF8是一个将Unicode码点编码为字节序列的变长编码。UTF8编码是由Go语言之父Ken Thompson和Rob Pike共同发明的,现在已经是Unicode的标准。UTF8编码使用1到4个字节来表示每个Unicode码点,ASCII部分字符只使用1个字节,常用字符部分使用2或3个字节表示。每个符号编码后第一个字节的高端bit位用于表示编码总共有多少个字节。如果第一个字节的高端bit为0,则表示对应7bit的ASCII字符,ASCII字符每个字符依然是一个字节,和传统的ASCII编码兼容。如果第一个字节的高端bit是110,则说明需要2个字节;后续的每个高端bit都以10开头。更大的Unicode码点也是采用类似的策略处理。 0xxxxxxx runes 0-127 (ASCII)\n110xxxxx 10xxxxxx 128-2047 (values <128 unused)\n1110xxxx 10xxxxxx 10xxxxxx 2048-65535 (values <2048 unused)\n11110xxx 10xxxxxx 10xxxxxx 10xxxxxx 65536-0x10ffff (other values unused) 变长的编码无法直接通过索引来访问第n个字符,但是UTF8编码获得了很多额外的优点。首先UTF8编码比较紧凑,完全兼容ASCII码,并且可以自动同步:它可以通过向前回朔最多3个字节就能确定当前字符编码的开始字节的位置。它也是一个前缀编码,所以当从左向右解码时不会有任何歧义也并不需要向前查看(译注:像GBK之类的编码,如果不知道起点位置则可能会出现歧义)。没有任何字符的编码是其它字符编码的子串,或是其它编码序列的字串,因此搜索一个字符时只要搜索它的字节编码序列即可,不用担心前后的上下文会对搜索结果产生干扰。同时UTF8编码的顺序和Unicode码点的顺序一致,因此可以直接排序UTF8编码序列。同时因为没有嵌入的NUL(0)字节,可以很好地兼容那些使用NUL作为字符串结尾的编程语言。 Go语言的源文件采用UTF8编码,并且Go语言处理UTF8编码的文本也很出色。unicode包提供了诸多处理rune字符相关功能的函数(比如区分字母和数字,或者是字母的大写和小写转换等),unicode/utf8包则提供了用于rune字符序列的UTF8编码和解码的功能。 有很多Unicode字符很难直接从键盘输入,并且还有很多字符有着相似的结构;有一些甚至是不可见的字符(译注:中文和日文就有很多相似但不同的字)。Go语言字符串面值中的Unicode转义字符让我们可以通过Unicode码点输入特殊的字符。有两种形式:\\uhhhh对应16bit的码点值,\\Uhhhhhhhh对应32bit的码点值,其中h是一个十六进制数字;一般很少需要使用32bit的形式。每一个对应码点的UTF8编码。例如:下面的字母串面值都表示相同的值: \"世界\"\n\"\\xe4\\xb8\\x96\\xe7\\x95\\x8c\"\n\"\\u4e16\\u754c\"\n\"\\U00004e16\\U0000754c\" 上面三个转义序列都为第一个字符串提供替代写法,但是它们的值都是相同的。 Unicode转义也可以使用在rune字符中。下面三个字符是等价的: '世' '\\u4e16' '\\U00004e16' 对于小于256的码点值可以写在一个十六进制转义字节中,例如\\x41对应字符'A',但是对于更大的码点则必须使用\\u或\\U转义形式。因此,\\xe4\\xb8\\x96并不是一个合法的rune字符,虽然这三个字节对应一个有效的UTF8编码的码点。 得益于UTF8编码优良的设计,诸多字符串操作都不需要解码操作。我们可以不用解码直接测试一个字符串是否是另一个字符串的前缀: func HasPrefix(s, prefix string) bool { return len(s) >= len(prefix) && s[:len(prefix)] == prefix\n} 或者是后缀测试: func HasSuffix(s, suffix string) bool { return len(s) >= len(suffix) && s[len(s)-len(suffix):] == suffix\n} 或者是包含子串测试: func Contains(s, substr string) bool { for i := 0; i < len(s); i++ { if HasPrefix(s[i:], substr) { return true } } return false\n} 对于UTF8编码后文本的处理和原始的字节处理逻辑是一样的。但是对应很多其它编码则并不是这样的。(上面的函数都来自strings字符串处理包,真实的代码包含了一个用哈希技术优化的Contains 实现。) 另一方面,如果我们真的关心每个Unicode字符,我们可以使用其它处理方式。考虑前面的第一个例子中的字符串,它混合了中西两种字符。图3.5展示了它的内存表示形式。字符串包含13个字节,以UTF8形式编码,但是只对应9个Unicode字符: import \"unicode/utf8\" s := \"Hello, 世界\"\nfmt.Println(len(s)) // \"13\"\nfmt.Println(utf8.RuneCountInString(s)) // \"9\" 为了处理这些真实的字符,我们需要一个UTF8解码器。unicode/utf8包提供了该功能,我们可以这样使用: for i := 0; i < len(s); { r, size := utf8.DecodeRuneInString(s[i:]) fmt.Printf(\"%d\\t%c\\n\", i, r) i += size\n} 每一次调用DecodeRuneInString函数都返回一个r和长度,r对应字符本身,长度对应r采用UTF8编码后的编码字节数目。长度可以用于更新第i个字符在字符串中的字节索引位置。但是这种编码方式是笨拙的,我们需要更简洁的语法。幸运的是,Go语言的range循环在处理字符串的时候,会自动隐式解码UTF8字符串。下面的循环运行如图3.5所示;需要注意的是对于非ASCII,索引更新的步长将超过1个字节。 for i, r := range \"Hello, 世界\" { fmt.Printf(\"%d\\t%q\\t%d\\n\", i, r, r)\n} 我们可以使用一个简单的循环来统计字符串中字符的数目,像这样: n := 0\nfor _, _ = range s { n++\n} 像其它形式的循环那样,我们也可以忽略不需要的变量: n := 0\nfor range s { n++\n} 或者我们可以直接调用utf8.RuneCountInString(s)函数。 正如我们前面提到的,文本字符串采用UTF8编码只是一种惯例,但是对于循环的真正字符串并不是一个惯例,这是正确的。如果用于循环的字符串只是一个普通的二进制数据,或者是含有错误编码的UTF8数据,将会发生什么呢? 每一个UTF8字符解码,不管是显式地调用utf8.DecodeRuneInString解码或是在range循环中隐式地解码,如果遇到一个错误的UTF8编码输入,将生成一个特别的Unicode字符\\uFFFD,在印刷中这个符号通常是一个黑色六角或钻石形状,里面包含一个白色的问号\"?\"。当程序遇到这样的一个字符,通常是一个危险信号,说明输入并不是一个完美没有错误的UTF8字符串。 UTF8字符串作为交换格式是非常方便的,但是在程序内部采用rune序列可能更方便,因为rune大小一致,支持数组索引和方便切割。 将[]rune类型转换应用到UTF8编码的字符串,将返回字符串编码的Unicode码点序列: // \"program\" in Japanese katakana\ns := \"プログラム\"\nfmt.Printf(\"% x\\n\", s) // \"e3 83 97 e3 83 ad e3 82 b0 e3 83 a9 e3 83 a0\"\nr := []rune(s)\nfmt.Printf(\"%x\\n\", r) // \"[30d7 30ed 30b0 30e9 30e0]\" (在第一个Printf中的% x参数用于在每个十六进制数字前插入一个空格。) 如果是将一个[]rune类型的Unicode字符slice或数组转为string,则对它们进行UTF8编码: fmt.Println(string(r)) // \"プログラム\" 将一个整数转型为字符串意思是生成以只包含对应Unicode码点字符的UTF8字符串: fmt.Println(string(65)) // \"A\", not \"65\"\nfmt.Println(string(0x4eac)) // \"京\" 如果对应码点的字符是无效的,则用\\uFFFD无效字符作为替换: fmt.Println(string(1234567)) // \"?\"","breadcrumbs":"基础数据类型 » 字符串 » 3.5.3. UTF-8","id":"40","title":"3.5.3. UTF-8"},"41":{"body":"标准库中有四个包对字符串处理尤为重要:bytes、strings、strconv和unicode包。strings包提供了许多如字符串的查询、替换、比较、截断、拆分和合并等功能。 bytes包也提供了很多类似功能的函数,但是针对和字符串有着相同结构的[]byte类型。因为字符串是只读的,因此逐步构建字符串会导致很多分配和复制。在这种情况下,使用bytes.Buffer类型将会更有效,稍后我们将展示。 strconv包提供了布尔型、整型数、浮点数和对应字符串的相互转换,还提供了双引号转义相关的转换。 unicode包提供了IsDigit、IsLetter、IsUpper和IsLower等类似功能,它们用于给字符分类。每个函数有一个单一的rune类型的参数,然后返回一个布尔值。而像ToUpper和ToLower之类的转换函数将用于rune字符的大小写转换。所有的这些函数都是遵循Unicode标准定义的字母、数字等分类规范。strings包也有类似的函数,它们是ToUpper和ToLower,将原始字符串的每个字符都做相应的转换,然后返回新的字符串。 下面例子的basename函数灵感源于Unix shell的同名工具。在我们实现的版本中,basename(s)将看起来像是系统路径的前缀删除,同时将看似文件类型的后缀名部分删除: fmt.Println(basename(\"a/b/c.go\")) // \"c\"\nfmt.Println(basename(\"c.d.go\")) // \"c.d\"\nfmt.Println(basename(\"abc\")) // \"abc\" 第一个版本并没有使用任何库,全部手工硬编码实现: gopl.io/ch3/basename1 // basename removes directory components and a .suffix.\n// e.g., a => a, a.go => a, a/b/c.go => c, a/b.c.go => b.c\nfunc basename(s string) string { // Discard last '/' and everything before. for i := len(s) - 1; i >= 0; i-- { if s[i] == '/' { s = s[i+1:] break } } // Preserve everything before last '.'. for i := len(s) - 1; i >= 0; i-- { if s[i] == '.' { s = s[:i] break } } return s\n} 这个简化版本使用了strings.LastIndex库函数: gopl.io/ch3/basename2 func basename(s string) string { slash := strings.LastIndex(s, \"/\") // -1 if \"/\" not found s = s[slash+1:] if dot := strings.LastIndex(s, \".\"); dot >= 0 { s = s[:dot] } return s\n} path和path/filepath包提供了关于文件路径名更一般的函数操作。使用斜杠分隔路径可以在任何操作系统上工作。斜杠本身不应该用于文件名,但是在其他一些领域可能会用于文件名,例如URL路径组件。相比之下,path/filepath包则使用操作系统本身的路径规则,例如POSIX系统使用/foo/bar,而Microsoft Windows使用c:\\foo\\bar等。 让我们继续另一个字符串的例子。函数的功能是将一个表示整数值的字符串,每隔三个字符插入一个逗号分隔符,例如“12345”处理后成为“12,345”。这个版本只适用于整数类型;支持浮点数类型的留作练习。 gopl.io/ch3/comma // comma inserts commas in a non-negative decimal integer string.\nfunc comma(s string) string { n := len(s) if n <= 3 { return s } return comma(s[:n-3]) + \",\" + s[n-3:]\n} 输入comma函数的参数是一个字符串。如果输入字符串的长度小于或等于3的话,则不需要插入逗号分隔符。否则,comma函数将在最后三个字符前的位置将字符串切割为两个子串并插入逗号分隔符,然后通过递归调用自身来得出前面的子串。 一个字符串是包含只读字节的数组,一旦创建,是不可变的。相比之下,一个字节slice的元素则可以自由地修改。 字符串和字节slice之间可以相互转换: s := \"abc\"\nb := []byte(s)\ns2 := string(b) 从概念上讲,一个[]byte(s)转换是分配了一个新的字节数组用于保存字符串数据的拷贝,然后引用这个底层的字节数组。编译器的优化可以避免在一些场景下分配和复制字符串数据,但总的来说需要确保在变量b被修改的情况下,原始的s字符串也不会改变。将一个字节slice转换到字符串的string(b)操作则是构造一个字符串拷贝,以确保s2字符串是只读的。 为了避免转换中不必要的内存分配,bytes包和strings同时提供了许多实用函数。下面是strings包中的六个函数: func Contains(s, substr string) bool\nfunc Count(s, sep string) int\nfunc Fields(s string) []string\nfunc HasPrefix(s, prefix string) bool\nfunc Index(s, sep string) int\nfunc Join(a []string, sep string) string bytes包中也对应的六个函数: func Contains(b, subslice []byte) bool\nfunc Count(s, sep []byte) int\nfunc Fields(s []byte) [][]byte\nfunc HasPrefix(s, prefix []byte) bool\nfunc Index(s, sep []byte) int\nfunc Join(s [][]byte, sep []byte) []byte 它们之间唯一的区别是字符串类型参数被替换成了字节slice类型的参数。 bytes包还提供了Buffer类型用于字节slice的缓存。一个Buffer开始是空的,但是随着string、byte或[]byte等类型数据的写入可以动态增长,一个bytes.Buffer变量并不需要初始化,因为零值也是有效的: gopl.io/ch3/printints // intsToString is like fmt.Sprint(values) but adds commas.\nfunc intsToString(values []int) string { var buf bytes.Buffer buf.WriteByte('[') for i, v := range values { if i > 0 { buf.WriteString(\", \") } fmt.Fprintf(&buf, \"%d\", v) } buf.WriteByte(']') return buf.String()\n} func main() { fmt.Println(intsToString([]int{1, 2, 3})) // \"[1, 2, 3]\"\n} 当向bytes.Buffer添加任意字符的UTF8编码时,最好使用bytes.Buffer的WriteRune方法,但是WriteByte方法对于写入类似'['和']'等ASCII字符则会更加有效。 bytes.Buffer类型有着很多实用的功能,我们在第七章讨论接口时将会涉及到,我们将看看如何将它用作一个I/O的输入和输出对象,例如当做Fprintf的io.Writer输出对象,或者当作io.Reader类型的输入源对象。 练习 3.10: 编写一个非递归版本的comma函数,使用bytes.Buffer代替字符串链接操作。 练习 3.11: 完善comma函数,以支持浮点数处理和一个可选的正负号的处理。 练习 3.12: 编写一个函数,判断两个字符串是否是相互打乱的,也就是说它们有着相同的字符,但是对应不同的顺序。","breadcrumbs":"基础数据类型 » 字符串 » 3.5.4. 字符串和Byte切片","id":"41","title":"3.5.4. 字符串和Byte切片"},"42":{"body":"除了字符串、字符、字节之间的转换,字符串和数值之间的转换也比较常见。由strconv包提供这类转换功能。 将一个整数转为字符串,一种方法是用fmt.Sprintf返回一个格式化的字符串;另一个方法是用strconv.Itoa(“整数到ASCII”): x := 123\ny := fmt.Sprintf(\"%d\", x)\nfmt.Println(y, strconv.Itoa(x)) // \"123 123\" FormatInt和FormatUint函数可以用不同的进制来格式化数字: fmt.Println(strconv.FormatInt(int64(x), 2)) // \"1111011\" fmt.Printf函数的%b、%d、%o和%x等参数提供功能往往比strconv包的Format函数方便很多,特别是在需要包含有附加额外信息的时候: s := fmt.Sprintf(\"x=%b\", x) // \"x=1111011\" 如果要将一个字符串解析为整数,可以使用strconv包的Atoi或ParseInt函数,还有用于解析无符号整数的ParseUint函数: x, err := strconv.Atoi(\"123\") // x is an int\ny, err := strconv.ParseInt(\"123\", 10, 64) // base 10, up to 64 bits ParseInt函数的第三个参数是用于指定整型数的大小;例如16表示int16,0则表示int。在任何情况下,返回的结果y总是int64类型,你可以通过强制类型转换将它转为更小的整数类型。 有时候也会使用fmt.Scanf来解析输入的字符串和数字,特别是当字符串和数字混合在一行的时候,它可以灵活处理不完整或不规则的输入。","breadcrumbs":"基础数据类型 » 字符串 » 3.5.5. 字符串和数字的转换","id":"42","title":"3.5.5. 字符串和数字的转换"},"43":{"body":"常量表达式的值在编译期计算,而不是在运行期。每种常量的潜在类型都是基础类型:boolean、string或数字。 一个常量的声明语句定义了常量的名字,和变量的声明语法类似,常量的值不可修改,这样可以防止在运行期被意外或恶意的修改。例如,常量比变量更适合用于表达像π之类的数学常数,因为它们的值不会发生变化: const pi = 3.14159 // approximately; math.Pi is a better approximation 和变量声明一样,可以批量声明多个常量;这比较适合声明一组相关的常量: const ( e = 2.71828182845904523536028747135266249775724709369995957496696763 pi = 3.14159265358979323846264338327950288419716939937510582097494459\n) 所有常量的运算都可以在编译期完成,这样可以减少运行时的工作,也方便其他编译优化。当操作数是常量时,一些运行时的错误也可以在编译时被发现,例如整数除零、字符串索引越界、任何导致无效浮点数的操作等。 常量间的所有算术运算、逻辑运算和比较运算的结果也是常量,对常量的类型转换操作或以下函数调用都是返回常量结果:len、cap、real、imag、complex和unsafe.Sizeof(§13.1)。 因为它们的值是在编译期就确定的,因此常量可以是构成类型的一部分,例如用于指定数组类型的长度: const IPv4Len = 4 // parseIPv4 parses an IPv4 address (d.d.d.d).\nfunc parseIPv4(s string) IP { var p [IPv4Len]byte // ...\n} 一个常量的声明也可以包含一个类型和一个值,但是如果没有显式指明类型,那么将从右边的表达式推断类型。在下面的代码中,time.Duration是一个命名类型,底层类型是int64,time.Minute是对应类型的常量。下面声明的两个常量都是time.Duration类型,可以通过%T参数打印类型信息: const noDelay time.Duration = 0\nconst timeout = 5 * time.Minute\nfmt.Printf(\"%T %[1]v\\n\", noDelay) // \"time.Duration 0\"\nfmt.Printf(\"%T %[1]v\\n\", timeout) // \"time.Duration 5m0s\"\nfmt.Printf(\"%T %[1]v\\n\", time.Minute) // \"time.Duration 1m0s\" 如果是批量声明的常量,除了第一个外其它的常量右边的初始化表达式都可以省略,如果省略初始化表达式则表示使用前面常量的初始化表达式写法,对应的常量类型也一样的。例如: const ( a = 1 b c = 2 d\n) fmt.Println(a, b, c, d) // \"1 1 2 2\" 如果只是简单地复制右边的常量表达式,其实并没有太实用的价值。但是它可以带来其它的特性,那就是iota常量生成器语法。","breadcrumbs":"基础数据类型 » 常量 » 3.6. 常量","id":"43","title":"3.6. 常量"},"44":{"body":"常量声明可以使用iota常量生成器初始化,它用于生成一组以相似规则初始化的常量,但是不用每行都写一遍初始化表达式。在一个const声明语句中,在第一个声明的常量所在的行,iota将会被置为0,然后在每一个有常量声明的行加一。 下面是来自time包的例子,它首先定义了一个Weekday命名类型,然后为一周的每天定义了一个常量,从周日0开始。在其它编程语言中,这种类型一般被称为枚举类型。 type Weekday int const ( Sunday Weekday = iota Monday Tuesday Wednesday Thursday Friday Saturday\n) 周日将对应0,周一为1,如此等等。 我们也可以在复杂的常量表达式中使用iota,下面是来自net包的例子,用于给一个无符号整数的最低5bit的每个bit指定一个名字: type Flags uint const ( FlagUp Flags = 1 << iota // is up FlagBroadcast // supports broadcast access capability FlagLoopback // is a loopback interface FlagPointToPoint // belongs to a point-to-point link FlagMulticast // supports multicast access capability\n) 随着iota的递增,每个常量对应表达式1 << iota,是连续的2的幂,分别对应一个bit位置。使用这些常量可以用于测试、设置或清除对应的bit位的值: gopl.io/ch3/netflag func IsUp(v Flags) bool { return v&FlagUp == FlagUp }\nfunc TurnDown(v *Flags) { *v &^= FlagUp }\nfunc SetBroadcast(v *Flags) { *v |= FlagBroadcast }\nfunc IsCast(v Flags) bool { return v&(FlagBroadcast|FlagMulticast) != 0 } func main() { var v Flags = FlagMulticast | FlagUp fmt.Printf(\"%b %t\\n\", v, IsUp(v)) // \"10001 true\" TurnDown(&v) fmt.Printf(\"%b %t\\n\", v, IsUp(v)) // \"10000 false\" SetBroadcast(&v) fmt.Printf(\"%b %t\\n\", v, IsUp(v)) // \"10010 false\" fmt.Printf(\"%b %t\\n\", v, IsCast(v)) // \"10010 true\"\n} 下面是一个更复杂的例子,每个常量都是1024的幂: const ( _ = 1 << (10 * iota) KiB // 1024 MiB // 1048576 GiB // 1073741824 TiB // 1099511627776 (exceeds 1 << 32) PiB // 1125899906842624 EiB // 1152921504606846976 ZiB // 1180591620717411303424 (exceeds 1 << 64) YiB // 1208925819614629174706176\n) 不过iota常量生成规则也有其局限性。例如,它并不能用于产生1000的幂(KB、MB等),因为Go语言并没有计算幂的运算符。 练习 3.13: 编写KB、MB的常量声明,然后扩展到YB。","breadcrumbs":"基础数据类型 » 常量 » 3.6.1. iota 常量生成器","id":"44","title":"3.6.1. iota 常量生成器"},"45":{"body":"Go语言的常量有个不同寻常之处。虽然一个常量可以有任意一个确定的基础类型,例如int或float64,或者是类似time.Duration这样命名的基础类型,但是许多常量并没有一个明确的基础类型。编译器为这些没有明确基础类型的数字常量提供比基础类型更高精度的算术运算;你可以认为至少有256bit的运算精度。这里有六种未明确类型的常量类型,分别是无类型的布尔型、无类型的整数、无类型的字符、无类型的浮点数、无类型的复数、无类型的字符串。 通过延迟明确常量的具体类型,无类型的常量不仅可以提供更高的运算精度,而且可以直接用于更多的表达式而不需要显式的类型转换。例如,例子中的ZiB和YiB的值已经超出任何Go语言中整数类型能表达的范围,但是它们依然是合法的常量,而且像下面的常量表达式依然有效(译注:YiB/ZiB是在编译期计算出来的,并且结果常量是1024,是Go语言int变量能有效表示的): fmt.Println(YiB/ZiB) // \"1024\" 另一个例子,math.Pi无类型的浮点数常量,可以直接用于任意需要浮点数或复数的地方: var x float32 = math.Pi\nvar y float64 = math.Pi\nvar z complex128 = math.Pi 如果math.Pi被确定为特定类型,比如float64,那么结果精度可能会不一样,同时对于需要float32或complex128类型值的地方则会强制需要一个明确的类型转换: const Pi64 float64 = math.Pi var x float32 = float32(Pi64)\nvar y float64 = Pi64\nvar z complex128 = complex128(Pi64) 对于常量面值,不同的写法可能会对应不同的类型。例如0、0.0、0i和\\u0000虽然有着相同的常量值,但是它们分别对应无类型的整数、无类型的浮点数、无类型的复数和无类型的字符等不同的常量类型。同样,true和false也是无类型的布尔类型,字符串面值常量是无类型的字符串类型。 前面说过除法运算符/会根据操作数的类型生成对应类型的结果。因此,不同写法的常量除法表达式可能对应不同的结果: var f float64 = 212\nfmt.Println((f - 32) * 5 / 9) // \"100\"; (f - 32) * 5 is a float64\nfmt.Println(5 / 9 * (f - 32)) // \"0\"; 5/9 is an untyped integer, 0\nfmt.Println(5.0 / 9.0 * (f - 32)) // \"100\"; 5.0/9.0 is an untyped float 只有常量可以是无类型的。当一个无类型的常量被赋值给一个变量的时候,就像下面的第一行语句,或者出现在有明确类型的变量声明的右边,如下面的其余三行语句,无类型的常量将会被隐式转换为对应的类型,如果转换合法的话。 var f float64 = 3 + 0i // untyped complex -> float64\nf = 2 // untyped integer -> float64\nf = 1e123 // untyped floating-point -> float64\nf = 'a' // untyped rune -> float64 上面的语句相当于: var f float64 = float64(3 + 0i)\nf = float64(2)\nf = float64(1e123)\nf = float64('a') 无论是隐式或显式转换,将一种类型转换为另一种类型都要求目标可以表示原始值。对于浮点数和复数,可能会有舍入处理: const ( deadbeef = 0xdeadbeef // untyped int with value 3735928559 a = uint32(deadbeef) // uint32 with value 3735928559 b = float32(deadbeef) // float32 with value 3735928576 (rounded up) c = float64(deadbeef) // float64 with value 3735928559 (exact) d = int32(deadbeef) // compile error: constant overflows int32 e = float64(1e309) // compile error: constant overflows float64 f = uint(-1) // compile error: constant underflows uint\n) 对于一个没有显式类型的变量声明(包括简短变量声明),常量的形式将隐式决定变量的默认类型,就像下面的例子: i := 0 // untyped integer; implicit int(0)\nr := '\\000' // untyped rune; implicit rune('\\000')\nf := 0.0 // untyped floating-point; implicit float64(0.0)\nc := 0i // untyped complex; implicit complex128(0i) 注意有一点不同:无类型整数常量转换为int,它的内存大小是不确定的,但是无类型浮点数和复数常量则转换为内存大小明确的float64和complex128。 如果不知道浮点数类型的内存大小是很难写出正确的数值算法的,因此Go语言不存在整型类似的不确定内存大小的浮点数和复数类型。 如果要给变量一个不同的类型,我们必须显式地将无类型的常量转化为所需的类型,或给声明的变量指定明确的类型,像下面例子这样: var i = int8(0)\nvar i int8 = 0 当尝试将这些无类型的常量转为一个接口值时(见第7章),这些默认类型将显得尤为重要,因为要靠它们明确接口对应的动态类型。 fmt.Printf(\"%T\\n\", 0) // \"int\"\nfmt.Printf(\"%T\\n\", 0.0) // \"float64\"\nfmt.Printf(\"%T\\n\", 0i) // \"complex128\"\nfmt.Printf(\"%T\\n\", '\\000') // \"int32\" (rune) 现在我们已经讲述了Go语言中全部的基础数据类型。下一步将演示如何用基础数据类型组合成数组或结构体等复杂数据类型,然后构建用于解决实际编程问题的数据结构,这将是第四章的讨论主题。","breadcrumbs":"基础数据类型 » 常量 » 3.6.2. 无类型常量","id":"45","title":"3.6.2. 无类型常量"},"46":{"body":"在第三章我们讨论了基本数据类型,它们可以用于构建程序中数据的结构,是Go语言世界的原子。在本章,我们将讨论复合数据类型,它是以不同的方式组合基本类型而构造出来的复合数据类型。我们主要讨论四种类型——数组、slice、map和结构体——同时在本章的最后,我们将演示如何使用结构体来解码和编码到对应JSON格式的数据,并且通过结合使用模板来生成HTML页面。 数组和结构体是聚合类型;它们的值由许多元素或成员字段的值组成。数组是由同构的元素组成——每个数组元素都是完全相同的类型——结构体则是由异构的元素组成的。数组和结构体都是有固定内存大小的数据结构。相比之下,slice和map则是动态的数据结构,它们将根据需要动态增长。","breadcrumbs":"复合数据类型 » 第4章 复合数据类型","id":"46","title":"第4章 复合数据类型"},"47":{"body":"数组是一个由固定长度的特定类型元素组成的序列,一个数组可以由零个或多个元素组成。因为数组的长度是固定的,因此在Go语言中很少直接使用数组。和数组对应的类型是Slice(切片),它是可以增长和收缩的动态序列,slice功能也更灵活,但是要理解slice工作原理的话需要先理解数组。 数组的每个元素可以通过索引下标来访问,索引下标的范围是从0开始到数组长度减1的位置。内置的len函数将返回数组中元素的个数。 var a [3]int // array of 3 integers\nfmt.Println(a[0]) // print the first element\nfmt.Println(a[len(a)-1]) // print the last element, a[2] // Print the indices and elements.\nfor i, v := range a { fmt.Printf(\"%d %d\\n\", i, v)\n} // Print the elements only.\nfor _, v := range a { fmt.Printf(\"%d\\n\", v)\n} 默认情况下,数组的每个元素都被初始化为元素类型对应的零值,对于数字类型来说就是0。我们也可以使用数组字面值语法用一组值来初始化数组: var q [3]int = [3]int{1, 2, 3}\nvar r [3]int = [3]int{1, 2}\nfmt.Println(r[2]) // \"0\" 在数组字面值中,如果在数组的长度位置出现的是“...”省略号,则表示数组的长度是根据初始化值的个数来计算。因此,上面q数组的定义可以简化为 q := [...]int{1, 2, 3}\nfmt.Printf(\"%T\\n\", q) // \"[3]int\" 数组的长度是数组类型的一个组成部分,因此[3]int和[4]int是两种不同的数组类型。数组的长度必须是常量表达式,因为数组的长度需要在编译阶段确定。 q := [3]int{1, 2, 3}\nq = [4]int{1, 2, 3, 4} // compile error: cannot assign [4]int to [3]int 我们将会发现,数组、slice、map和结构体字面值的写法都很相似。上面的形式是直接提供顺序初始化值序列,但是也可以指定一个索引和对应值列表的方式初始化,就像下面这样: type Currency int const ( USD Currency = iota // 美元 EUR // 欧元 GBP // 英镑 RMB // 人民币\n) symbol := [...]string{USD: \"$\", EUR: \"€\", GBP: \"£\", RMB: \"¥\"} fmt.Println(RMB, symbol[RMB]) // \"3 ¥\" 在这种形式的数组字面值形式中,初始化索引的顺序是无关紧要的,而且没用到的索引可以省略,和前面提到的规则一样,未指定初始值的元素将用零值初始化。例如, r := [...]int{99: -1} 定义了一个含有100个元素的数组r,最后一个元素被初始化为-1,其它元素都是用0初始化。 如果一个数组的元素类型是可以相互比较的,那么数组类型也是可以相互比较的,这时候我们可以直接通过==比较运算符来比较两个数组,只有当两个数组的所有元素都是相等的时候数组才是相等的。不相等比较运算符!=遵循同样的规则。 a := [2]int{1, 2}\nb := [...]int{1, 2}\nc := [2]int{1, 3}\nfmt.Println(a == b, a == c, b == c) // \"true false false\"\nd := [3]int{1, 2}\nfmt.Println(a == d) // compile error: cannot compare [2]int == [3]int 作为一个真实的例子,crypto/sha256包的Sum256函数对一个任意的字节slice类型的数据生成一个对应的消息摘要。消息摘要有256bit大小,因此对应[32]byte数组类型。如果两个消息摘要是相同的,那么可以认为两个消息本身也是相同(译注:理论上有HASH码碰撞的情况,但是实际应用可以基本忽略);如果消息摘要不同,那么消息本身必然也是不同的。下面的例子用SHA256算法分别生成“x”和“X”两个信息的摘要: gopl.io/ch4/sha256 import \"crypto/sha256\" func main() { c1 := sha256.Sum256([]byte(\"x\")) c2 := sha256.Sum256([]byte(\"X\")) fmt.Printf(\"%x\\n%x\\n%t\\n%T\\n\", c1, c2, c1 == c2, c1) // Output: // 2d711642b726b04401627ca9fbac32f5c8530fb1903cc4db02258717921a4881 // 4b68ab3847feda7d6c62c1fbcbeebfa35eab7351ed5e78f4ddadea5df64b8015 // false // [32]uint8\n} 上面例子中,两个消息虽然只有一个字符的差异,但是生成的消息摘要则几乎有一半的bit位是不相同的。需要注意Printf函数的%x副词参数,它用于指定以十六进制的格式打印数组或slice全部的元素,%t副词参数是用于打印布尔型数据,%T副词参数是用于显示一个值对应的数据类型。 当调用一个函数的时候,函数的每个调用参数将会被赋值给函数内部的参数变量,所以函数参数变量接收的是一个复制的副本,并不是原始调用的变量。因为函数参数传递的机制导致传递大的数组类型将是低效的,并且对数组参数的任何的修改都是发生在复制的数组上,并不能直接修改调用时原始的数组变量。在这个方面,Go语言对待数组的方式和其它很多编程语言不同,其它编程语言可能会隐式地将数组作为引用或指针对象传入被调用的函数。 当然,我们可以显式地传入一个数组指针,那样的话函数通过指针对数组的任何修改都可以直接反馈到调用者。下面的函数用于给[32]byte类型的数组清零: func zero(ptr *[32]byte) { for i := range ptr { ptr[i] = 0 }\n} 其实数组字面值[32]byte{}就可以生成一个32字节的数组。而且每个数组的元素都是零值初始化,也就是0。因此,我们可以将上面的zero函数写的更简洁一点: func zero(ptr *[32]byte) { *ptr = [32]byte{}\n} 虽然通过指针来传递数组参数是高效的,而且也允许在函数内部修改数组的值,但是数组依然是僵化的类型,因为数组的类型包含了僵化的长度信息。上面的zero函数并不能接收指向[16]byte类型数组的指针,而且也没有任何添加或删除数组元素的方法。由于这些原因,除了像SHA256这类需要处理特定大小数组的特例外,数组依然很少用作函数参数;相反,我们一般使用slice来替代数组。 练习 4.1: 编写一个函数,计算两个SHA256哈希码中不同bit的数目。(参考2.6.2节的PopCount函数。) 练习 4.2: 编写一个程序,默认情况下打印标准输入的SHA256编码,并支持通过命令行flag定制,输出SHA384或SHA512哈希算法。","breadcrumbs":"复合数据类型 » 数组 » 4.1. 数组","id":"47","title":"4.1. 数组"},"48":{"body":"Slice(切片)代表变长的序列,序列中每个元素都有相同的类型。一个slice类型一般写作[]T,其中T代表slice中元素的类型;slice的语法和数组很像,只是没有固定长度而已。 数组和slice之间有着紧密的联系。一个slice是一个轻量级的数据结构,提供了访问数组子序列(或者全部)元素的功能,而且slice的底层确实引用一个数组对象。一个slice由三个部分构成:指针、长度和容量。指针指向第一个slice元素对应的底层数组元素的地址,要注意的是slice的第一个元素并不一定就是数组的第一个元素。长度对应slice中元素的数目;长度不能超过容量,容量一般是从slice的开始位置到底层数据的结尾位置。内置的len和cap函数分别返回slice的长度和容量。 多个slice之间可以共享底层的数据,并且引用的数组部分区间可能重叠。图4.1显示了表示一年中每个月份名字的字符串数组,还有重叠引用了该数组的两个slice。数组这样定义 months := [...]string{1: \"January\", /* ... */, 12: \"December\"} 因此一月份是months[1],十二月份是months[12]。通常,数组的第一个元素从索引0开始,但是月份一般是从1开始的,因此我们声明数组时直接跳过第0个元素,第0个元素会被自动初始化为空字符串。 slice的切片操作s[i:j],其中0 ≤ i≤ j≤ cap(s),用于创建一个新的slice,引用s的从第i个元素开始到第j-1个元素的子序列。新的slice将只有j-i个元素。如果i位置的索引被省略的话将使用0代替,如果j位置的索引被省略的话将使用len(s)代替。因此,months[1:13]切片操作将引用全部有效的月份,和months[1:]操作等价;months[:]切片操作则是引用整个数组。让我们分别定义表示第二季度和北方夏天月份的slice,它们有重叠部分: Q2 := months[4:7]\nsummer := months[6:9]\nfmt.Println(Q2) // [\"April\" \"May\" \"June\"]\nfmt.Println(summer) // [\"June\" \"July\" \"August\"] 两个slice都包含了六月份,下面的代码是一个包含相同月份的测试(性能较低): for _, s := range summer { for _, q := range Q2 { if s == q { fmt.Printf(\"%s appears in both\\n\", s) } }\n} 如果切片操作超出cap(s)的上限将导致一个panic异常,但是超出len(s)则是意味着扩展了slice,因为新slice的长度会变大: fmt.Println(summer[:20]) // panic: out of range endlessSummer := summer[:5] // extend a slice (within capacity)\nfmt.Println(endlessSummer) // \"[June July August September October]\" 另外,字符串的切片操作和[]byte字节类型切片的切片操作是类似的。都写作x[m:n],并且都是返回一个原始字节序列的子序列,底层都是共享之前的底层数组,因此这种操作都是常量时间复杂度。x[m:n]切片操作对于字符串则生成一个新字符串,如果x是[]byte的话则生成一个新的[]byte。 因为slice值包含指向第一个slice元素的指针,因此向函数传递slice将允许在函数内部修改底层数组的元素。换句话说,复制一个slice只是对底层的数组创建了一个新的slice别名(§2.3.2)。下面的reverse函数在原内存空间将[]int类型的slice反转,而且它可以用于任意长度的slice。 gopl.io/ch4/rev // reverse reverses a slice of ints in place.\nfunc reverse(s []int) { for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 { s[i], s[j] = s[j], s[i] }\n} 这里我们反转数组的应用: a := [...]int{0, 1, 2, 3, 4, 5}\nreverse(a[:])\nfmt.Println(a) // \"[5 4 3 2 1 0]\" 一种将slice元素循环向左旋转n个元素的方法是三次调用reverse反转函数,第一次是反转开头的n个元素,然后是反转剩下的元素,最后是反转整个slice的元素。(如果是向右循环旋转,则将第三个函数调用移到第一个调用位置就可以了。) s := []int{0, 1, 2, 3, 4, 5}\n// Rotate s left by two positions.\nreverse(s[:2])\nreverse(s[2:])\nreverse(s)\nfmt.Println(s) // \"[2 3 4 5 0 1]\" 要注意的是slice类型的变量s和数组类型的变量a的初始化语法的差异。slice和数组的字面值语法很类似,它们都是用花括弧包含一系列的初始化元素,但是对于slice并没有指明序列的长度。这会隐式地创建一个合适大小的数组,然后slice的指针指向底层的数组。就像数组字面值一样,slice的字面值也可以按顺序指定初始化值序列,或者是通过索引和元素值指定,或者用两种风格的混合语法初始化。 和数组不同的是,slice之间不能比较,因此我们不能使用==操作符来判断两个slice是否含有全部相等元素。不过标准库提供了高度优化的bytes.Equal函数来判断两个字节型slice是否相等([]byte),但是对于其他类型的slice,我们必须自己展开每个元素进行比较: func equal(x, y []string) bool { if len(x) != len(y) { return false } for i := range x { if x[i] != y[i] { return false } } return true\n} 上面关于两个slice的深度相等测试,运行的时间并不比支持==操作的数组或字符串更多,但是为何slice不直接支持比较运算符呢?这方面有两个原因。第一个原因,一个slice的元素是间接引用的,一个slice甚至可以包含自身(译注:当slice声明为[]interface{}时,slice的元素可以是自身)。虽然有很多办法处理这种情形,但是没有一个是简单有效的。 第二个原因,因为slice的元素是间接引用的,一个固定的slice值(译注:指slice本身的值,不是元素的值)在不同的时刻可能包含不同的元素,因为底层数组的元素可能会被修改。而例如Go语言中map的key只做简单的浅拷贝,它要求key在整个生命周期内保持不变性(译注:例如slice扩容,就会导致其本身的值/地址变化)。而用深度相等判断的话,显然在map的key这种场合不合适。对于像指针或chan之类的引用类型,==相等测试可以判断两个是否是引用相同的对象。一个针对slice的浅相等测试的==操作符可能是有一定用处的,也能临时解决map类型的key问题,但是slice和数组不同的相等测试行为会让人困惑。因此,安全的做法是直接禁止slice之间的比较操作。 slice唯一合法的比较操作是和nil比较,例如: if summer == nil { /* ... */ } 一个零值的slice等于nil。一个nil值的slice并没有底层数组。一个nil值的slice的长度和容量都是0,但是也有非nil值的slice的长度和容量也是0的,例如[]int{}或make([]int, 3)[3:]。与任意类型的nil值一样,我们可以用[]int(nil)类型转换表达式来生成一个对应类型slice的nil值。 var s []int // len(s) == 0, s == nil\ns = nil // len(s) == 0, s == nil\ns = []int(nil) // len(s) == 0, s == nil\ns = []int{} // len(s) == 0, s != nil 如果你需要测试一个slice是否是空的,使用len(s) == 0来判断,而不应该用s == nil来判断。除了和nil相等比较外,一个nil值的slice的行为和其它任意0长度的slice一样;例如reverse(nil)也是安全的。除了文档已经明确说明的地方,所有的Go语言函数应该以相同的方式对待nil值的slice和0长度的slice。 内置的make函数创建一个指定元素类型、长度和容量的slice。容量部分可以省略,在这种情况下,容量将等于长度。 make([]T, len)\nmake([]T, len, cap) // same as make([]T, cap)[:len] 在底层,make创建了一个匿名的数组变量,然后返回一个slice;只有通过返回的slice才能引用底层匿名的数组变量。在第一种语句中,slice是整个数组的view。在第二个语句中,slice只引用了底层数组的前len个元素,但是容量将包含整个的数组。额外的元素是留给未来的增长用的。","breadcrumbs":"复合数据类型 » Slice » 4.2. Slice","id":"48","title":"4.2. Slice"},"49":{"body":"内置的append函数用于向slice追加元素: var runes []rune\nfor _, r := range \"Hello, 世界\" { runes = append(runes, r)\n}\nfmt.Printf(\"%q\\n\", runes) // \"['H' 'e' 'l' 'l' 'o' ',' ' ' '世' '界']\" 在循环中使用append函数构建一个由九个rune字符构成的slice,当然对应这个特殊的问题我们可以通过Go语言内置的[]rune(\"Hello, 世界\")转换操作完成。 append函数对于理解slice底层是如何工作的非常重要,所以让我们仔细查看究竟是发生了什么。下面是第一个版本的appendInt函数,专门用于处理[]int类型的slice: gopl.io/ch4/append func appendInt(x []int, y int) []int { var z []int zlen := len(x) + 1 if zlen <= cap(x) { // There is room to grow. Extend the slice. z = x[:zlen] } else { // There is insufficient space. Allocate a new array. // Grow by doubling, for amortized linear complexity. zcap := zlen if zcap < 2*len(x) { zcap = 2 * len(x) } z = make([]int, zlen, zcap) copy(z, x) // a built-in function; see text } z[len(x)] = y return z\n} 每次调用appendInt函数,必须先检测slice底层数组是否有足够的容量来保存新添加的元素。如果有足够空间的话,直接扩展slice(依然在原有的底层数组之上),将新添加的y元素复制到新扩展的空间,并返回slice。因此,输入的x和输出的z共享相同的底层数组。 如果没有足够的增长空间的话,appendInt函数则会先分配一个足够大的slice用于保存新的结果,先将输入的x复制到新的空间,然后添加y元素。结果z和输入的x引用的将是不同的底层数组。 虽然通过循环复制元素更直接,不过内置的copy函数可以方便地将一个slice复制另一个相同类型的slice。copy函数的第一个参数是要复制的目标slice,第二个参数是源slice,目标和源的位置顺序和dst = src赋值语句是一致的。两个slice可以共享同一个底层数组,甚至有重叠也没有问题。copy函数将返回成功复制的元素的个数(我们这里没有用到),等于两个slice中较小的长度,所以我们不用担心覆盖会超出目标slice的范围。 为了提高内存使用效率,新分配的数组一般略大于保存x和y所需要的最低大小。通过在每次扩展数组时直接将长度翻倍从而避免了多次内存分配,也确保了添加单个元素操作的平均时间是一个常数时间。这个程序演示了效果: func main() { var x, y []int for i := 0; i < 10; i++ { y = appendInt(x, i) fmt.Printf(\"%d cap=%d\\t%v\\n\", i, cap(y), y) x = y }\n} 每一次容量的变化都会导致重新分配内存和copy操作: 0 cap=1 [0]\n1 cap=2 [0 1]\n2 cap=4 [0 1 2]\n3 cap=4 [0 1 2 3]\n4 cap=8 [0 1 2 3 4]\n5 cap=8 [0 1 2 3 4 5]\n6 cap=8 [0 1 2 3 4 5 6]\n7 cap=8 [0 1 2 3 4 5 6 7]\n8 cap=16 [0 1 2 3 4 5 6 7 8]\n9 cap=16 [0 1 2 3 4 5 6 7 8 9] 让我们仔细查看i=3次的迭代。当时x包含了[0 1 2]三个元素,但是容量是4,因此可以简单将新的元素添加到末尾,不需要新的内存分配。然后新的y的长度和容量都是4,并且和x引用着相同的底层数组,如图4.2所示。 在下一次迭代时i=4,现在没有新的空余的空间了,因此appendInt函数分配一个容量为8的底层数组,将x的4个元素[0 1 2 3]复制到新空间的开头,然后添加新的元素i,新元素的值是4。新的y的长度是5,容量是8;后面有3个空闲的位置,三次迭代都不需要分配新的空间。当前迭代中,y和x是对应不同底层数组的view。这次操作如图4.3所示。 内置的append函数可能使用比appendInt更复杂的内存扩展策略。因此,通常我们并不知道append调用是否导致了内存的重新分配,因此我们也不能确认新的slice和原始的slice是否引用的是相同的底层数组空间。同样,我们不能确认在原先的slice上的操作是否会影响到新的slice。因此,通常是将append返回的结果直接赋值给输入的slice变量: runes = append(runes, r) 更新slice变量不仅对调用append函数是必要的,实际上对应任何可能导致长度、容量或底层数组变化的操作都是必要的。要正确地使用slice,需要记住尽管底层数组的元素是间接访问的,但是slice对应结构体本身的指针、长度和容量部分是直接访问的。要更新这些信息需要像上面例子那样一个显式的赋值操作。从这个角度看,slice并不是一个纯粹的引用类型,它实际上是一个类似下面结构体的聚合类型: type IntSlice struct { ptr *int len, cap int\n} 我们的appendInt函数每次只能向slice追加一个元素,但是内置的append函数则可以追加多个元素,甚至追加一个slice。 var x []int\nx = append(x, 1)\nx = append(x, 2, 3)\nx = append(x, 4, 5, 6)\nx = append(x, x...) // append the slice x\nfmt.Println(x) // \"[1 2 3 4 5 6 1 2 3 4 5 6]\" 通过下面的小修改,我们可以达到append函数类似的功能。其中在appendInt函数参数中的最后的“...”省略号表示接收变长的参数为slice。我们将在5.7节详细解释这个特性。 func appendInt(x []int, y ...int) []int { var z []int zlen := len(x) + len(y) // ...expand z to at least zlen... copy(z[len(x):], y) return z\n} 为了避免重复,和前面相同的代码并没有显示。","breadcrumbs":"复合数据类型 » Slice » 4.2.1. append函数","id":"49","title":"4.2.1. append函数"},"5":{"body":"最佳的帮助信息来自Go语言的官方网站,https://golang.org ,它提供了完善的参考文档,包括编程语言规范和标准库等诸多权威的帮助信息。同时也包含了如何编写更地道的Go程序的基本教程,还有各种各样的在线文本资源和视频资源,它们是本书最有价值的补充。Go语言的官方博客 https://blog.golang.org 会不定期发布一些Go语言最好的实践文章,包括当前语言的发展状态、未来的计划、会议报告和Go语言相关的各种会议的主题等信息(译注: http://talks.golang.org/ 包含了官方收录的各种报告的讲稿)。 在线访问的一个有价值的地方是可以从web页面运行Go语言的程序(而纸质书则没有这么便利了)。这个功能由来自 https://play.golang.org 的 Go Playground 提供,并且可以方便地嵌入到其他页面中,例如 https://golang.org 的主页,或 godoc 提供的文档页面中。 Playground可以简单的通过执行一个小程序来测试对语法、语义和对程序库的理解,类似其他很多语言提供的REPL即时运行的工具。同时它可以生成对应的url,非常适合共享Go语言代码片段,汇报bug或提供反馈意见等。 基于 Playground 构建的 Go Tour,https://tour.golang.org ,是一个系列的Go语言入门教程,它包含了诸多基本概念和结构相关的并可在线运行的互动小程序。 当然,Playground 和 Tour 也有一些限制,它们只能导入标准库,而且因为安全的原因对一些网络库做了限制。如果要在编译和运行时需要访问互联网,对于一些更复杂的实验,你可能需要在自己的电脑上构建并运行程序。幸运的是下载Go语言的过程很简单,从 https://golang.org 下载安装包应该不超过几分钟(译注:感谢伟大的长城,让大陆的Gopher们都学会了自己打洞的基本生活技能,下载时间可能会因为洞的大小等因素从几分钟到几天或更久),然后就可以在自己电脑上编写和运行Go程序了。 Go语言是一个开源项目,你可以在 https://golang.org/pkg 阅读标准库中任意函数和类型的实现代码,和下载安装包的代码完全一致。这样,你可以知道很多函数是如何工作的, 通过挖掘找出一些答案的细节,或者仅仅是出于欣赏专业级Go代码。","breadcrumbs":"前言 » 更多的信息","id":"5","title":"更多的信息"},"50":{"body":"让我们看看更多的例子,比如旋转slice、反转slice或在slice原有内存空间修改元素。给定一个字符串列表,下面的nonempty函数将在原有slice内存空间之上返回不包含空字符串的列表: gopl.io/ch4/nonempty // Nonempty is an example of an in-place slice algorithm.\npackage main import \"fmt\" // nonempty returns a slice holding only the non-empty strings.\n// The underlying array is modified during the call.\nfunc nonempty(strings []string) []string { i := 0 for _, s := range strings { if s != \"\" { strings[i] = s i++ } } return strings[:i]\n} 比较微妙的地方是,输入的slice和输出的slice共享一个底层数组。这可以避免分配另一个数组,不过原来的数据将可能会被覆盖,正如下面两个打印语句看到的那样: data := []string{\"one\", \"\", \"three\"}\nfmt.Printf(\"%q\\n\", nonempty(data)) // `[\"one\" \"three\"]`\nfmt.Printf(\"%q\\n\", data) // `[\"one\" \"three\" \"three\"]` 因此我们通常会这样使用nonempty函数:data = nonempty(data)。 nonempty函数也可以使用append函数实现: func nonempty2(strings []string) []string { out := strings[:0] // zero-length slice of original for _, s := range strings { if s != \"\" { out = append(out, s) } } return out\n} 无论如何实现,以这种方式重用一个slice一般都要求最多为每个输入值产生一个输出值,事实上很多这类算法都是用来过滤或合并序列中相邻的元素。这种slice用法是比较复杂的技巧,虽然使用到了slice的一些技巧,但是对于某些场合是比较清晰和有效的。 一个slice可以用来模拟一个stack。最初给定的空slice对应一个空的stack,然后可以使用append函数将新的值压入stack: stack = append(stack, v) // push v stack的顶部位置对应slice的最后一个元素: top := stack[len(stack)-1] // top of stack 通过收缩stack可以弹出栈顶的元素 stack = stack[:len(stack)-1] // pop 要删除slice中间的某个元素并保存原有的元素顺序,可以通过内置的copy函数将后面的子slice向前依次移动一位完成: func remove(slice []int, i int) []int { copy(slice[i:], slice[i+1:]) return slice[:len(slice)-1]\n} func main() { s := []int{5, 6, 7, 8, 9} fmt.Println(remove(s, 2)) // \"[5 6 8 9]\"\n} 如果删除元素后不用保持原来顺序的话,我们可以简单的用最后一个元素覆盖被删除的元素: func remove(slice []int, i int) []int { slice[i] = slice[len(slice)-1] return slice[:len(slice)-1]\n} func main() { s := []int{5, 6, 7, 8, 9} fmt.Println(remove(s, 2)) // \"[5 6 9 8]\n} 练习 4.3: 重写reverse函数,使用数组指针代替slice。 练习 4.4: 编写一个rotate函数,通过一次循环完成旋转。 练习 4.5: 写一个函数在原地完成消除[]string中相邻重复的字符串的操作。 练习 4.6: 编写一个函数,原地将一个UTF-8编码的[]byte类型的slice中相邻的空格(参考unicode.IsSpace)替换成一个空格返回 练习 4.7: 修改reverse函数用于原地反转UTF-8编码的[]byte。是否可以不用分配额外的内存?","breadcrumbs":"复合数据类型 » Slice » 4.2.2. Slice内存技巧","id":"50","title":"4.2.2. Slice内存技巧"},"51":{"body":"哈希表是一种巧妙并且实用的数据结构。它是一个无序的key/value对的集合,其中所有的key都是不同的,然后通过给定的key可以在常数时间复杂度内检索、更新或删除对应的value。 在Go语言中,一个map就是一个哈希表的引用,map类型可以写为map[K]V,其中K和V分别对应key和value。map中所有的key都有相同的类型,所有的value也有着相同的类型,但是key和value之间可以是不同的数据类型。其中K对应的key必须是支持==比较运算符的数据类型,所以map可以通过测试key是否相等来判断是否已经存在。虽然浮点数类型也是支持相等运算符比较的,但是将浮点数用做key类型则是一个坏的想法,正如第三章提到的,最坏的情况是可能出现的NaN和任何浮点数都不相等。对于V对应的value数据类型则没有任何的限制。 内置的make函数可以创建一个map: ages := make(map[string]int) // mapping from strings to ints 我们也可以用map字面值的语法创建map,同时还可以指定一些最初的key/value: ages := map[string]int{ \"alice\": 31, \"charlie\": 34,\n} 这相当于 ages := make(map[string]int)\nages[\"alice\"] = 31\nages[\"charlie\"] = 34 因此,另一种创建空的map的表达式是map[string]int{}。 Map中的元素通过key对应的下标语法访问: ages[\"alice\"] = 32\nfmt.Println(ages[\"alice\"]) // \"32\" 使用内置的delete函数可以删除元素: delete(ages, \"alice\") // remove element ages[\"alice\"] 所有这些操作是安全的,即使这些元素不在map中也没有关系;如果一个查找失败将返回value类型对应的零值,例如,即使map中不存在“bob”下面的代码也可以正常工作,因为ages[\"bob\"]失败时将返回0。 ages[\"bob\"] = ages[\"bob\"] + 1 // happy birthday! 而且x += y和x++等简短赋值语法也可以用在map上,所以上面的代码可以改写成 ages[\"bob\"] += 1 更简单的写法 ages[\"bob\"]++ 但是map中的元素并不是一个变量,因此我们不能对map的元素进行取址操作: _ = &ages[\"bob\"] // compile error: cannot take address of map element 禁止对map元素取址的原因是map可能随着元素数量的增长而重新分配更大的内存空间,从而可能导致之前的地址无效。 要想遍历map中全部的key/value对的话,可以使用range风格的for循环实现,和之前的slice遍历语法类似。下面的迭代语句将在每次迭代时设置name和age变量,它们对应下一个键/值对: for name, age := range ages { fmt.Printf(\"%s\\t%d\\n\", name, age)\n} Map的迭代顺序是不确定的,并且不同的哈希函数实现可能导致不同的遍历顺序。在实践中,遍历的顺序是随机的,每一次遍历的顺序都不相同。这是故意的,每次都使用随机的遍历顺序可以强制要求程序不会依赖具体的哈希函数实现。如果要按顺序遍历key/value对,我们必须显式地对key进行排序,可以使用sort包的Strings函数对字符串slice进行排序。下面是常见的处理方式: import \"sort\" var names []string\nfor name := range ages { names = append(names, name)\n}\nsort.Strings(names)\nfor _, name := range names { fmt.Printf(\"%s\\t%d\\n\", name, ages[name])\n} 因为我们一开始就知道names的最终大小,因此给slice分配一个合适的大小将会更有效。下面的代码创建了一个空的slice,但是slice的容量刚好可以放下map中全部的key: names := make([]string, 0, len(ages)) 在上面的第一个range循环中,我们只关心map中的key,所以我们忽略了第二个循环变量。在第二个循环中,我们只关心names中的名字,所以我们使用“_”空白标识符来忽略第一个循环变量,也就是迭代slice时的索引。 map类型的零值是nil,也就是没有引用任何哈希表。 var ages map[string]int\nfmt.Println(ages == nil) // \"true\"\nfmt.Println(len(ages) == 0) // \"true\" map上的大部分操作,包括查找、删除、len和range循环都可以安全工作在nil值的map上,它们的行为和一个空的map类似。但是向一个nil值的map存入元素将导致一个panic异常: ages[\"carol\"] = 21 // panic: assignment to entry in nil map 在向map存数据前必须先创建map。 通过key作为索引下标来访问map将产生一个value。如果key在map中是存在的,那么将得到与key对应的value;如果key不存在,那么将得到value对应类型的零值,正如我们前面看到的ages[\"bob\"]那样。这个规则很实用,但是有时候可能需要知道对应的元素是否真的是在map之中。例如,如果元素类型是一个数字,你可能需要区分一个已经存在的0,和不存在而返回零值的0,可以像下面这样测试: age, ok := ages[\"bob\"]\nif !ok { /* \"bob\" is not a key in this map; age == 0. */ } 你会经常看到将这两个结合起来使用,像这样: if age, ok := ages[\"bob\"]; !ok { /* ... */ } 在这种场景下,map的下标语法将产生两个值;第二个是一个布尔值,用于报告元素是否真的存在。布尔变量一般命名为ok,特别适合马上用于if条件判断部分。 和slice一样,map之间也不能进行相等比较;唯一的例外是和nil进行比较。要判断两个map是否包含相同的key和value,我们必须通过一个循环实现: func equal(x, y map[string]int) bool { if len(x) != len(y) { return false } for k, xv := range x { if yv, ok := y[k]; !ok || yv != xv { return false } } return true\n} 从例子中可以看到如何用!ok来区分元素不存在,与元素存在但为0的。我们不能简单地用xv != y[k]判断,那样会导致在判断下面两个map时产生错误的结果: // True if equal is written incorrectly.\nequal(map[string]int{\"A\": 0}, map[string]int{\"B\": 42}) Go语言中并没有提供一个set类型,但是map中的key也是不相同的,可以用map实现类似set的功能。为了说明这一点,下面的dedup程序读取多行输入,但是只打印第一次出现的行。(它是1.3节中出现的dup程序的变体。)dedup程序通过map来表示所有的输入行所对应的set集合,以确保已经在集合存在的行不会被重复打印。 gopl.io/ch4/dedup func main() { seen := make(map[string]bool) // a set of strings input := bufio.NewScanner(os.Stdin) for input.Scan() { line := input.Text() if !seen[line] { seen[line] = true fmt.Println(line) } } if err := input.Err(); err != nil { fmt.Fprintf(os.Stderr, \"dedup: %v\\n\", err) os.Exit(1) }\n} Go程序员将这种忽略value的map当作一个字符串集合,并非所有map[string]bool类型value都是无关紧要的;有一些则可能会同时包含true和false的值。 有时候我们需要一个map或set的key是slice类型,但是map的key必须是可比较的类型,但是slice并不满足这个条件。不过,我们可以通过两个步骤绕过这个限制。第一步,定义一个辅助函数k,将slice转为map对应的string类型的key,确保只有x和y相等时k(x) == k(y)才成立。然后创建一个key为string类型的map,在每次对map操作时先用k辅助函数将slice转化为string类型。 下面的例子演示了如何使用map来记录提交相同的字符串列表的次数。它使用了fmt.Sprintf函数将字符串列表转换为一个字符串以用于map的key,通过%q参数忠实地记录每个字符串元素的信息: var m = make(map[string]int) func k(list []string) string { return fmt.Sprintf(\"%q\", list) } func Add(list []string) { m[k(list)]++ }\nfunc Count(list []string) int { return m[k(list)] } 使用同样的技术可以处理任何不可比较的key类型,而不仅仅是slice类型。这种技术对于想使用自定义key比较函数的时候也很有用,例如在比较字符串的时候忽略大小写。同时,辅助函数k(x)也不一定是字符串类型,它可以返回任何可比较的类型,例如整数、数组或结构体等。 这是map的另一个例子,下面的程序用于统计输入中每个Unicode码点出现的次数。虽然Unicode全部码点的数量巨大,但是出现在特定文档中的字符种类并没有多少,使用map可以用比较自然的方式来跟踪那些出现过的字符的次数。 gopl.io/ch4/charcount // Charcount computes counts of Unicode characters.\npackage main import ( \"bufio\" \"fmt\" \"io\" \"os\" \"unicode\" \"unicode/utf8\"\n) func main() { counts := make(map[rune]int) // counts of Unicode characters var utflen [utf8.UTFMax + 1]int // count of lengths of UTF-8 encodings invalid := 0 // count of invalid UTF-8 characters in := bufio.NewReader(os.Stdin) for { r, n, err := in.ReadRune() // returns rune, nbytes, error if err == io.EOF { break } if err != nil { fmt.Fprintf(os.Stderr, \"charcount: %v\\n\", err) os.Exit(1) } if r == unicode.ReplacementChar && n == 1 { invalid++ continue } counts[r]++ utflen[n]++ } fmt.Printf(\"rune\\tcount\\n\") for c, n := range counts { fmt.Printf(\"%q\\t%d\\n\", c, n) } fmt.Print(\"\\nlen\\tcount\\n\") for i, n := range utflen { if i > 0 { fmt.Printf(\"%d\\t%d\\n\", i, n) } } if invalid > 0 { fmt.Printf(\"\\n%d invalid UTF-8 characters\\n\", invalid) }\n} ReadRune方法执行UTF-8解码并返回三个值:解码的rune字符的值,字符UTF-8编码后的长度,和一个错误值。我们可预期的错误值只有对应文件结尾的io.EOF。如果输入的是无效的UTF-8编码的字符,返回的将是unicode.ReplacementChar表示无效字符,并且编码长度是1。 charcount程序同时打印不同UTF-8编码长度的字符数目。对此,map并不是一个合适的数据结构;因为UTF-8编码的长度总是从1到utf8.UTFMax(最大是4个字节),使用数组将更有效。 作为一个实验,我们用charcount程序对英文版原稿的字符进行了统计。虽然大部分是英语,但是也有一些非ASCII字符。下面是排名前10的非ASCII字符: 下面是不同UTF-8编码长度的字符的数目: len count\n1 765391\n2 60\n3 70\n4 0 Map的value类型也可以是一个聚合类型,比如是一个map或slice。在下面的代码中,图graph的key类型是一个字符串,value类型map[string]bool代表一个字符串集合。从概念上讲,graph将一个字符串类型的key映射到一组相关的字符串集合,它们指向新的graph的key。 gopl.io/ch4/graph var graph = make(map[string]map[string]bool) func addEdge(from, to string) { edges := graph[from] if edges == nil { edges = make(map[string]bool) graph[from] = edges } edges[to] = true\n} func hasEdge(from, to string) bool { return graph[from][to]\n} 其中addEdge函数惰性初始化map是一个惯用方式,也就是说在每个值首次作为key时才初始化。hasEdge函数显示了如何让map的零值也能正常工作;即使from到to的边不存在,graph[from][to]依然可以返回一个有意义的结果。 练习 4.8: 修改charcount程序,使用unicode.IsLetter等相关的函数,统计字母、数字等Unicode中不同的字符类别。 练习 4.9: 编写一个程序wordfreq程序,报告输入文本中每个单词出现的频率。在第一次调用Scan前先调用input.Split(bufio.ScanWords)函数,这样可以按单词而不是按行输入。","breadcrumbs":"复合数据类型 » Map » 4.3. Map","id":"51","title":"4.3. Map"},"52":{"body":"结构体是一种聚合的数据类型,是由零个或多个任意类型的值聚合成的实体。每个值称为结构体的成员。用结构体的经典案例是处理公司的员工信息,每个员工信息包含一个唯一的员工编号、员工的名字、家庭住址、出生日期、工作岗位、薪资、上级领导等等。所有的这些信息都需要绑定到一个实体中,可以作为一个整体单元被复制,作为函数的参数或返回值,或者是被存储到数组中,等等。 下面两个语句声明了一个叫Employee的命名的结构体类型,并且声明了一个Employee类型的变量dilbert: type Employee struct { ID int Name string Address string DoB time.Time Position string Salary int ManagerID int\n} var dilbert Employee dilbert结构体变量的成员可以通过点操作符访问,比如dilbert.Name和dilbert.DoB。因为dilbert是一个变量,它所有的成员也同样是变量,我们可以直接对每个成员赋值: dilbert.Salary -= 5000 // demoted, for writing too few lines of code 或者是对成员取地址,然后通过指针访问: position := &dilbert.Position\n*position = \"Senior \" + *position // promoted, for outsourcing to Elbonia 点操作符也可以和指向结构体的指针一起工作: var employeeOfTheMonth *Employee = &dilbert\nemployeeOfTheMonth.Position += \" (proactive team player)\" 相当于下面语句 (*employeeOfTheMonth).Position += \" (proactive team player)\" 下面的EmployeeByID函数将根据给定的员工ID返回对应的员工信息结构体的指针。我们可以使用点操作符来访问它里面的成员: func EmployeeByID(id int) *Employee { /* ... */ } fmt.Println(EmployeeByID(dilbert.ManagerID).Position) // \"Pointy-haired boss\" id := dilbert.ID\nEmployeeByID(id).Salary = 0 // fired for... no real reason 后面的语句通过EmployeeByID返回的结构体指针更新了Employee结构体的成员。如果将EmployeeByID函数的返回值从*Employee指针类型改为Employee值类型,那么更新语句将不能编译通过,因为在赋值语句的左边并不确定是一个变量(译注:调用函数返回的是值,并不是一个可取地址的变量)。 通常一行对应一个结构体成员,成员的名字在前类型在后,不过如果相邻的成员类型如果相同的话可以被合并到一行,就像下面的Name和Address成员那样: type Employee struct { ID int Name, Address string DoB time.Time Position string Salary int ManagerID int\n} 结构体成员的输入顺序也有重要的意义。我们也可以将Position成员合并(因为也是字符串类型),或者是交换Name和Address出现的先后顺序,那样的话就是定义了不同的结构体类型。通常,我们只是将相关的成员写到一起。 如果结构体成员名字是以大写字母开头的,那么该成员就是导出的;这是Go语言导出规则决定的。一个结构体可能同时包含导出和未导出的成员。 结构体类型往往是冗长的,因为它的每个成员可能都会占一行。虽然我们每次都可以重写整个结构体成员,但是重复会令人厌烦。因此,完整的结构体写法通常只在类型声明语句的地方出现,就像Employee类型声明语句那样。 一个命名为S的结构体类型将不能再包含S类型的成员:因为一个聚合的值不能包含它自身。(该限制同样适用于数组。)但是S类型的结构体可以包含*S指针类型的成员,这可以让我们创建递归的数据结构,比如链表和树结构等。在下面的代码中,我们使用一个二叉树来实现一个插入排序: gopl.io/ch4/treesort type tree struct { value int left, right *tree\n} // Sort sorts values in place.\nfunc Sort(values []int) { var root *tree for _, v := range values { root = add(root, v) } appendValues(values[:0], root)\n} // appendValues appends the elements of t to values in order\n// and returns the resulting slice.\nfunc appendValues(values []int, t *tree) []int { if t != nil { values = appendValues(values, t.left) values = append(values, t.value) values = appendValues(values, t.right) } return values\n} func add(t *tree, value int) *tree { if t == nil { // Equivalent to return &tree{value: value}. t = new(tree) t.value = value return t } if value < t.value { t.left = add(t.left, value) } else { t.right = add(t.right, value) } return t\n} 结构体类型的零值是每个成员都是零值。通常会将零值作为最合理的默认值。例如,对于bytes.Buffer类型,结构体初始值就是一个随时可用的空缓存,还有在第9章将会讲到的sync.Mutex的零值也是有效的未锁定状态。有时候这种零值可用的特性是自然获得的,但是也有些类型需要一些额外的工作。 如果结构体没有任何成员的话就是空结构体,写作struct{}。它的大小为0,也不包含任何信息,但是有时候依然是有价值的。有些Go语言程序员用map来模拟set数据结构时,用它来代替map中布尔类型的value,只是强调key的重要性,但是因为节约的空间有限,而且语法比较复杂,所以我们通常会避免这样的用法。 seen := make(map[string]struct{}) // set of strings\n// ...\nif _, ok := seen[s]; !ok { seen[s] = struct{}{} // ...first time seeing s...\n}","breadcrumbs":"复合数据类型 » 结构体 » 4.4. 结构体","id":"52","title":"4.4. 结构体"},"53":{"body":"结构体值也可以用结构体字面值表示,结构体字面值可以指定每个成员的值。 type Point struct{ X, Y int } p := Point{1, 2} 这里有两种形式的结构体字面值语法,上面的是第一种写法,要求以结构体成员定义的顺序为每个结构体成员指定一个字面值。它要求写代码和读代码的人要记住结构体的每个成员的类型和顺序,不过结构体成员有细微的调整就可能导致上述代码不能编译。因此,上述的语法一般只在定义结构体的包内部使用,或者是在较小的结构体中使用,这些结构体的成员排列比较规则,比如image.Point{x, y}或color.RGBA{red, green, blue, alpha}。 其实更常用的是第二种写法,以成员名字和相应的值来初始化,可以包含部分或全部的成员,如1.4节的Lissajous程序的写法: anim := gif.GIF{LoopCount: nframes} 在这种形式的结构体字面值写法中,如果成员被忽略的话将默认用零值。因为提供了成员的名字,所以成员出现的顺序并不重要。 两种不同形式的写法不能混合使用。而且,你不能企图在外部包中用第一种顺序赋值的技巧来偷偷地初始化结构体中未导出的成员。 package p\ntype T struct{ a, b int } // a and b are not exported package q\nimport \"p\"\nvar _ = p.T{a: 1, b: 2} // compile error: can't reference a, b\nvar _ = p.T{1, 2} // compile error: can't reference a, b 虽然上面最后一行代码的编译错误信息中并没有显式提到未导出的成员,但是这样企图隐式使用未导出成员的行为也是不允许的。 结构体可以作为函数的参数和返回值。例如,这个Scale函数将Point类型的值缩放后返回: func Scale(p Point, factor int) Point { return Point{p.X * factor, p.Y * factor}\n} fmt.Println(Scale(Point{1, 2}, 5)) // \"{5 10}\" 如果考虑效率的话,较大的结构体通常会用指针的方式传入和返回, func Bonus(e *Employee, percent int) int { return e.Salary * percent / 100\n} 如果要在函数内部修改结构体成员的话,用指针传入是必须的;因为在Go语言中,所有的函数参数都是值拷贝传入的,函数参数将不再是函数调用时的原始变量。 func AwardAnnualRaise(e *Employee) { e.Salary = e.Salary * 105 / 100\n} 因为结构体通常通过指针处理,可以用下面的写法来创建并初始化一个结构体变量,并返回结构体的地址: pp := &Point{1, 2} 它和下面的语句是等价的 pp := new(Point)\n*pp = Point{1, 2} 不过&Point{1, 2}写法可以直接在表达式中使用,比如一个函数调用。","breadcrumbs":"复合数据类型 » 结构体 » 4.4.1. 结构体字面值","id":"53","title":"4.4.1. 结构体字面值"},"54":{"body":"如果结构体的全部成员都是可以比较的,那么结构体也是可以比较的,那样的话两个结构体将可以使用==或!=运算符进行比较。相等比较运算符==将比较两个结构体的每个成员,因此下面两个比较的表达式是等价的: type Point struct{ X, Y int } p := Point{1, 2}\nq := Point{2, 1}\nfmt.Println(p.X == q.X && p.Y == q.Y) // \"false\"\nfmt.Println(p == q) // \"false\" 可比较的结构体类型和其他可比较的类型一样,可以用于map的key类型。 type address struct { hostname string port int\n} hits := make(map[address]int)\nhits[address{\"golang.org\", 443}]++","breadcrumbs":"复合数据类型 » 结构体 » 4.4.2. 结构体比较","id":"54","title":"4.4.2. 结构体比较"},"55":{"body":"在本节中,我们将看到如何使用Go语言提供的不同寻常的结构体嵌入机制让一个命名的结构体包含另一个结构体类型的匿名成员,这样就可以通过简单的点运算符x.f来访问匿名成员链中嵌套的x.d.e.f成员。 考虑一个二维的绘图程序,提供了一个各种图形的库,例如矩形、椭圆形、星形和轮形等几何形状。这里是其中两个的定义: type Circle struct { X, Y, Radius int\n} type Wheel struct { X, Y, Radius, Spokes int\n} 一个Circle代表的圆形类型包含了标准圆心的X和Y坐标信息,和一个Radius表示的半径信息。一个Wheel轮形除了包含Circle类型所有的全部成员外,还增加了Spokes表示径向辐条的数量。我们可以这样创建一个wheel变量: var w Wheel\nw.X = 8\nw.Y = 8\nw.Radius = 5\nw.Spokes = 20 随着库中几何形状数量的增多,我们一定会注意到它们之间的相似和重复之处,所以我们可能为了便于维护而将相同的属性独立出来: type Point struct { X, Y int\n} type Circle struct { Center Point Radius int\n} type Wheel struct { Circle Circle Spokes int\n} 这样改动之后结构体类型变的清晰了,但是这种修改同时也导致了访问每个成员变得繁琐: var w Wheel\nw.Circle.Center.X = 8\nw.Circle.Center.Y = 8\nw.Circle.Radius = 5\nw.Spokes = 20 Go语言有一个特性让我们只声明一个成员对应的数据类型而不指名成员的名字;这类成员就叫匿名成员。匿名成员的数据类型必须是命名的类型或指向一个命名的类型的指针。下面的代码中,Circle和Wheel各自都有一个匿名成员。我们可以说Point类型被嵌入到了Circle结构体,同时Circle类型被嵌入到了Wheel结构体。 type Circle struct { Point Radius int\n} type Wheel struct { Circle Spokes int\n} 得益于匿名嵌入的特性,我们可以直接访问叶子属性而不需要给出完整的路径: var w Wheel\nw.X = 8 // equivalent to w.Circle.Point.X = 8\nw.Y = 8 // equivalent to w.Circle.Point.Y = 8\nw.Radius = 5 // equivalent to w.Circle.Radius = 5\nw.Spokes = 20 在右边的注释中给出的显式形式访问这些叶子成员的语法依然有效,因此匿名成员并不是真的无法访问了。其中匿名成员Circle和Point都有自己的名字——就是命名的类型名字——但是这些名字在点操作符中是可选的。我们在访问子成员的时候可以忽略任何匿名成员部分。 不幸的是,结构体字面值并没有简短表示匿名成员的语法, 因此下面的语句都不能编译通过: w = Wheel{8, 8, 5, 20} // compile error: unknown fields\nw = Wheel{X: 8, Y: 8, Radius: 5, Spokes: 20} // compile error: unknown fields 结构体字面值必须遵循形状类型声明时的结构,所以我们只能用下面的两种语法,它们彼此是等价的: gopl.io/ch4/embed w = Wheel{Circle{Point{8, 8}, 5}, 20} w = Wheel{ Circle: Circle{ Point: Point{X: 8, Y: 8}, Radius: 5, }, Spokes: 20, // NOTE: trailing comma necessary here (and at Radius)\n} fmt.Printf(\"%#v\\n\", w)\n// Output:\n// Wheel{Circle:Circle{Point:Point{X:8, Y:8}, Radius:5}, Spokes:20} w.X = 42 fmt.Printf(\"%#v\\n\", w)\n// Output:\n// Wheel{Circle:Circle{Point:Point{X:42, Y:8}, Radius:5}, Spokes:20} 需要注意的是Printf函数中%v参数包含的#副词,它表示用和Go语言类似的语法打印值。对于结构体类型来说,将包含每个成员的名字。 因为匿名成员也有一个隐式的名字,因此不能同时包含两个类型相同的匿名成员,这会导致名字冲突。同时,因为成员的名字是由其类型隐式地决定的,所以匿名成员也有可见性的规则约束。在上面的例子中,Point和Circle匿名成员都是导出的。即使它们不导出(比如改成小写字母开头的point和circle),我们依然可以用简短形式访问匿名成员嵌套的成员 w.X = 8 // equivalent to w.circle.point.X = 8 但是在包外部,因为circle和point没有导出,不能访问它们的成员,因此简短的匿名成员访问语法也是禁止的。 到目前为止,我们看到匿名成员特性只是对访问嵌套成员的点运算符提供了简短的语法糖。稍后,我们将会看到匿名成员并不要求是结构体类型;其实任何命名的类型都可以作为结构体的匿名成员。但是为什么要嵌入一个没有任何子成员类型的匿名成员类型呢? 答案是匿名类型的方法集。简短的点运算符语法可以用于选择匿名成员嵌套的成员,也可以用于访问它们的方法。实际上,外层的结构体不仅仅是获得了匿名成员类型的所有成员,而且也获得了该类型导出的全部的方法。这个机制可以用于将一些有简单行为的对象组合成有复杂行为的对象。组合是Go语言中面向对象编程的核心,我们将在6.3节中专门讨论。","breadcrumbs":"复合数据类型 » 结构体 » 4.4.3. 结构体嵌入和匿名成员","id":"55","title":"4.4.3. 结构体嵌入和匿名成员"},"56":{"body":"JavaScript对象表示法(JSON)是一种用于发送和接收结构化信息的标准协议。在类似的协议中,JSON并不是唯一的一个标准协议。 XML(§7.14)、ASN.1和Google的Protocol Buffers都是类似的协议,并且有各自的特色,但是由于简洁性、可读性和流行程度等原因,JSON是应用最广泛的一个。 Go语言对于这些标准格式的编码和解码都有良好的支持,由标准库中的encoding/json、encoding/xml、encoding/asn1等包提供支持(译注:Protocol Buffers的支持由 github.com/golang/protobuf 包提供),并且这类包都有着相似的API接口。本节,我们将对重要的encoding/json包的用法做个概述。 JSON是对JavaScript中各种类型的值——字符串、数字、布尔值和对象——Unicode本文编码。它可以用有效可读的方式表示第三章的基础数据类型和本章的数组、slice、结构体和map等聚合数据类型。 基本的JSON类型有数字(十进制或科学记数法)、布尔值(true或false)、字符串,其中字符串是以双引号包含的Unicode字符序列,支持和Go语言类似的反斜杠转义特性,不过JSON使用的是\\Uhhhh转义数字来表示一个UTF-16编码(译注:UTF-16和UTF-8一样是一种变长的编码,有些Unicode码点较大的字符需要用4个字节表示;而且UTF-16还有大端和小端的问题),而不是Go语言的rune类型。 这些基础类型可以通过JSON的数组和对象类型进行递归组合。一个JSON数组是一个有序的值序列,写在一个方括号中并以逗号分隔;一个JSON数组可以用于编码Go语言的数组和slice。一个JSON对象是一个字符串到值的映射,写成一系列的name:value对形式,用花括号包含并以逗号分隔;JSON的对象类型可以用于编码Go语言的map类型(key类型是字符串)和结构体。例如: boolean true\nnumber -273.15\nstring \"She said \\\"Hello, BF\\\"\"\narray [\"gold\", \"silver\", \"bronze\"]\nobject {\"year\": 1980, \"event\": \"archery\", \"medals\": [\"gold\", \"silver\", \"bronze\"]} 考虑一个应用程序,该程序负责收集各种电影评论并提供反馈功能。它的Movie数据类型和一个典型的表示电影的值列表如下所示。(在结构体声明中,Year和Color成员后面的字符串面值是结构体成员Tag;我们稍后会解释它的作用。) gopl.io/ch4/movie type Movie struct { Title string Year int `json:\"released\"` Color bool `json:\"color,omitempty\"` Actors []string\n} var movies = []Movie{ {Title: \"Casablanca\", Year: 1942, Color: false, Actors: []string{\"Humphrey Bogart\", \"Ingrid Bergman\"}}, {Title: \"Cool Hand Luke\", Year: 1967, Color: true, Actors: []string{\"Paul Newman\"}}, {Title: \"Bullitt\", Year: 1968, Color: true, Actors: []string{\"Steve McQueen\", \"Jacqueline Bisset\"}}, // ...\n} 这样的数据结构特别适合JSON格式,并且在两者之间相互转换也很容易。将一个Go语言中类似movies的结构体slice转为JSON的过程叫编组(marshaling)。编组通过调用json.Marshal函数完成: data, err := json.Marshal(movies)\nif err != nil { log.Fatalf(\"JSON marshaling failed: %s\", err)\n}\nfmt.Printf(\"%s\\n\", data) Marshal函数返回一个编码后的字节slice,包含很长的字符串,并且没有空白缩进;我们将它折行以便于显示: [{\"Title\":\"Casablanca\",\"released\":1942,\"Actors\":[\"Humphrey Bogart\",\"Ingr\nid Bergman\"]},{\"Title\":\"Cool Hand Luke\",\"released\":1967,\"color\":true,\"Ac\ntors\":[\"Paul Newman\"]},{\"Title\":\"Bullitt\",\"released\":1968,\"color\":true,\"\nActors\":[\"Steve McQueen\",\"Jacqueline Bisset\"]}] 这种紧凑的表示形式虽然包含了全部的信息,但是很难阅读。为了生成便于阅读的格式,另一个json.MarshalIndent函数将产生整齐缩进的输出。该函数有两个额外的字符串参数用于表示每一行输出的前缀和每一个层级的缩进: data, err := json.MarshalIndent(movies, \"\", \" \")\nif err != nil { log.Fatalf(\"JSON marshaling failed: %s\", err)\n}\nfmt.Printf(\"%s\\n\", data) 上面的代码将产生这样的输出(译注:在最后一个成员或元素后面并没有逗号分隔符): [ { \"Title\": \"Casablanca\", \"released\": 1942, \"Actors\": [ \"Humphrey Bogart\", \"Ingrid Bergman\" ] }, { \"Title\": \"Cool Hand Luke\", \"released\": 1967, \"color\": true, \"Actors\": [ \"Paul Newman\" ] }, { \"Title\": \"Bullitt\", \"released\": 1968, \"color\": true, \"Actors\": [ \"Steve McQueen\", \"Jacqueline Bisset\" ] }\n] 在编码时,默认使用Go语言结构体的成员名字作为JSON的对象(通过reflect反射技术,我们将在12.6节讨论)。只有导出的结构体成员才会被编码,这也就是我们为什么选择用大写字母开头的成员名称。 细心的读者可能已经注意到,其中Year名字的成员在编码后变成了released,还有Color成员编码后变成了小写字母开头的color。这是因为结构体成员Tag所导致的。一个结构体成员Tag是和在编译阶段关联到该成员的元信息字符串: Year int `json:\"released\"`\nColor bool `json:\"color,omitempty\"` 结构体的成员Tag可以是任意的字符串面值,但是通常是一系列用空格分隔的key:\"value\"键值对序列;因为值中含有双引号字符,因此成员Tag一般用原生字符串面值的形式书写。json开头键名对应的值用于控制encoding/json包的编码和解码的行为,并且encoding/...下面其它的包也遵循这个约定。成员Tag中json对应值的第一部分用于指定JSON对象的名字,比如将Go语言中的TotalCount成员对应到JSON中的total_count对象。Color成员的Tag还带了一个额外的omitempty选项,表示当Go语言结构体成员为空或零值时不生成该JSON对象(这里false为零值)。果然,Casablanca是一个黑白电影,并没有输出Color成员。 编码的逆操作是解码,对应将JSON数据解码为Go语言的数据结构,Go语言中一般叫unmarshaling,通过json.Unmarshal函数完成。下面的代码将JSON格式的电影数据解码为一个结构体slice,结构体中只有Title成员。通过定义合适的Go语言数据结构,我们可以选择性地解码JSON中感兴趣的成员。当Unmarshal函数调用返回,slice将被只含有Title信息的值填充,其它JSON成员将被忽略。 var titles []struct{ Title string }\nif err := json.Unmarshal(data, &titles); err != nil { log.Fatalf(\"JSON unmarshaling failed: %s\", err)\n}\nfmt.Println(titles) // \"[{Casablanca} {Cool Hand Luke} {Bullitt}]\" 许多web服务都提供JSON接口,通过HTTP接口发送JSON格式请求并返回JSON格式的信息。为了说明这一点,我们通过Github的issue查询服务来演示类似的用法。首先,我们要定义合适的类型和常量: gopl.io/ch4/github // Package github provides a Go API for the GitHub issue tracker.\n// See https://developer.github.com/v3/search/#search-issues.\npackage github import \"time\" const IssuesURL = \"https://api.github.com/search/issues\" type IssuesSearchResult struct { TotalCount int `json:\"total_count\"` Items []*Issue\n} type Issue struct { Number int HTMLURL string `json:\"html_url\"` Title string State string User *User CreatedAt time.Time `json:\"created_at\"` Body string // in Markdown format\n} type User struct { Login string HTMLURL string `json:\"html_url\"`\n} 和前面一样,即使对应的JSON对象名是小写字母,每个结构体的成员名也是声明为大写字母开头的。因为有些JSON成员名字和Go结构体成员名字并不相同,因此需要Go语言结构体成员Tag来指定对应的JSON名字。同样,在解码的时候也需要做同样的处理,GitHub服务返回的信息比我们定义的要多很多。 SearchIssues函数发出一个HTTP请求,然后解码返回的JSON格式的结果。因为用户提供的查询条件可能包含类似?和&之类的特殊字符,为了避免对URL造成冲突,我们用url.QueryEscape来对查询中的特殊字符进行转义操作。 gopl.io/ch4/github package github import ( \"encoding/json\" \"fmt\" \"net/http\" \"net/url\" \"strings\"\n) // SearchIssues queries the GitHub issue tracker.\nfunc SearchIssues(terms []string) (*IssuesSearchResult, error) { q := url.QueryEscape(strings.Join(terms, \" \")) resp, err := http.Get(IssuesURL + \"?q=\" + q) if err != nil { return nil, err } // We must close resp.Body on all execution paths. // (Chapter 5 presents 'defer', which makes this simpler.) if resp.StatusCode != http.StatusOK { resp.Body.Close() return nil, fmt.Errorf(\"search query failed: %s\", resp.Status) } var result IssuesSearchResult if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { resp.Body.Close() return nil, err } resp.Body.Close() return &result, nil\n} 在早些的例子中,我们使用了json.Unmarshal函数来将JSON格式的字符串解码为字节slice。但是这个例子中,我们使用了基于流式的解码器json.Decoder,它可以从一个输入流解码JSON数据,尽管这不是必须的。如您所料,还有一个针对输出流的json.Encoder编码对象。 我们调用Decode方法来填充变量。这里有多种方法可以格式化结构。下面是最简单的一种,以一个固定宽度打印每个issue,但是在下一节我们将看到如何利用模板来输出复杂的格式。 gopl.io/ch4/issues // Issues prints a table of GitHub issues matching the search terms.\npackage main import ( \"fmt\" \"log\" \"os\" \"gopl.io/ch4/github\"\n) func main() { result, err := github.SearchIssues(os.Args[1:]) if err != nil { log.Fatal(err) } fmt.Printf(\"%d issues:\\n\", result.TotalCount) for _, item := range result.Items { fmt.Printf(\"#%-5d %9.9s %.55s\\n\", item.Number, item.User.Login, item.Title) }\n} 通过命令行参数指定检索条件。下面的命令是查询Go语言项目中和JSON解码相关的问题,还有查询返回的结果: $ go build gopl.io/ch4/issues\n$ ./issues repo:golang/go is:open json decoder\n13 issues:\n#5680 eaigner encoding/json: set key converter on en/decoder\n#6050 gopherbot encoding/json: provide tokenizer\n#8658 gopherbot encoding/json: use bufio\n#8462 kortschak encoding/json: UnmarshalText confuses json.Unmarshal\n#5901 rsc encoding/json: allow override type marshaling\n#9812 klauspost encoding/json: string tag not symmetric\n#7872 extempora encoding/json: Encoder internally buffers full output\n#9650 cespare encoding/json: Decoding gives errPhase when unmarshalin\n#6716 gopherbot encoding/json: include field name in unmarshal error me\n#6901 lukescott encoding/json, encoding/xml: option to treat unknown fi\n#6384 joeshaw encoding/json: encode precise floating point integers u\n#6647 btracey x/tools/cmd/godoc: display type kind of each named type\n#4237 gjemiller encoding/base64: URLEncoding padding is optional GitHub的Web服务接口 https://developer.github.com/v3/ 包含了更多的特性。 练习 4.10: 修改issues程序,根据问题的时间进行分类,比如不到一个月的、不到一年的、超过一年。 练习 4.11: 编写一个工具,允许用户在命令行创建、读取、更新和关闭GitHub上的issue,当必要的时候自动打开用户默认的编辑器用于输入文本信息。 练习 4.12: 流行的web漫画服务xkcd也提供了JSON接口。例如,一个 https://xkcd.com/571/info.0.json 请求将返回一个很多人喜爱的571编号的详细描述。下载每个链接(只下载一次)然后创建一个离线索引。编写一个xkcd工具,使用这些离线索引,打印和命令行输入的检索词相匹配的漫画的URL。 练习 4.13: 使用开放电影数据库的JSON服务接口,允许你检索和下载 https://omdbapi.com/ 上电影的名字和对应的海报图像。编写一个poster工具,通过命令行输入的电影名字,下载对应的海报。","breadcrumbs":"复合数据类型 » JSON » 4.5. JSON","id":"56","title":"4.5. JSON"},"57":{"body":"前面的例子,只是最简单的格式化,使用Printf是完全足够的。但是有时候会需要复杂的打印格式,这时候一般需要将格式化代码分离出来以便更安全地修改。这些功能是由text/template和html/template等模板包提供的,它们提供了一个将变量值填充到一个文本或HTML格式的模板的机制。 一个模板是一个字符串或一个文件,里面包含了一个或多个由双花括号包含的{{action}}对象。大部分的字符串只是按字面值打印,但是对于actions部分将触发其它的行为。每个actions都包含了一个用模板语言书写的表达式,一个action虽然简短但是可以输出复杂的打印值,模板语言包含通过选择结构体的成员、调用函数或方法、表达式控制流if-else语句和range循环语句,还有其它实例化模板等诸多特性。下面是一个简单的模板字符串: gopl.io/ch4/issuesreport const templ = `{{.TotalCount}} issues:\n{{range .Items}}----------------------------------------\nNumber: {{.Number}}\nUser: {{.User.Login}}\nTitle: {{.Title | printf \"%.64s\"}}\nAge: {{.CreatedAt | daysAgo}} days\n{{end}}` {% endraw %} 这个模板先打印匹配到的issue总数,然后打印每个issue的编号、创建用户、标题还有存在的时间。对于每一个action,都有一个当前值的概念,对应点操作符,写作“.”。当前值“.”最初被初始化为调用模板时的参数,在当前例子中对应github.IssuesSearchResult类型的变量。模板中{{.TotalCount}}对应action将展开为结构体中TotalCount成员以默认的方式打印的值。模板中{{range .Items}}和{{end}}对应一个循环action,因此它们之间的内容可能会被展开多次,循环每次迭代的当前值对应当前的Items元素的值。 {% endraw %} 在一个action中,|操作符表示将前一个表达式的结果作为后一个函数的输入,类似于UNIX中管道的概念。在Title这一行的action中,第二个操作是一个printf函数,是一个基于fmt.Sprintf实现的内置函数,所有模板都可以直接使用。对于Age部分,第二个动作是一个叫daysAgo的函数,通过time.Since函数将CreatedAt成员转换为过去的时间长度: func daysAgo(t time.Time) int { return int(time.Since(t).Hours() / 24)\n} 需要注意的是CreatedAt的参数类型是time.Time,并不是字符串。以同样的方式,我们可以通过定义一些方法来控制字符串的格式化(§2.5),一个类型同样可以定制自己的JSON编码和解码行为。time.Time类型对应的JSON值是一个标准时间格式的字符串。 生成模板的输出需要两个处理步骤。第一步是要分析模板并转为内部表示,然后基于指定的输入执行模板。分析模板部分一般只需要执行一次。下面的代码创建并分析上面定义的模板templ。注意方法调用链的顺序:template.New先创建并返回一个模板;Funcs方法将daysAgo等自定义函数注册到模板中,并返回模板;最后调用Parse函数分析模板。 report, err := template.New(\"report\"). Funcs(template.FuncMap{\"daysAgo\": daysAgo}). Parse(templ)\nif err != nil { log.Fatal(err)\n} 因为模板通常在编译时就测试好了,如果模板解析失败将是一个致命的错误。template.Must辅助函数可以简化这个致命错误的处理:它接受一个模板和一个error类型的参数,检测error是否为nil(如果不是nil则发出panic异常),然后返回传入的模板。我们将在5.9节再讨论这个话题。 一旦模板已经创建、注册了daysAgo函数、并通过分析和检测,我们就可以使用github.IssuesSearchResult作为输入源、os.Stdout作为输出源来执行模板: var report = template.Must(template.New(\"issuelist\"). Funcs(template.FuncMap{\"daysAgo\": daysAgo}). Parse(templ)) func main() { result, err := github.SearchIssues(os.Args[1:]) if err != nil { log.Fatal(err) } if err := report.Execute(os.Stdout, result); err != nil { log.Fatal(err) }\n} 程序输出一个纯文本报告: $ go build gopl.io/ch4/issuesreport\n$ ./issuesreport repo:golang/go is:open json decoder\n13 issues:\n----------------------------------------\nNumber: 5680\nUser: eaigner\nTitle: encoding/json: set key converter on en/decoder\nAge: 750 days\n----------------------------------------\nNumber: 6050\nUser: gopherbot\nTitle: encoding/json: provide tokenizer\nAge: 695 days\n----------------------------------------\n... 现在让我们转到html/template模板包。它使用和text/template包相同的API和模板语言,但是增加了一个将字符串自动转义特性,这可以避免输入字符串和HTML、JavaScript、CSS或URL语法产生冲突的问题。这个特性还可以避免一些长期存在的安全问题,比如通过生成HTML注入攻击,通过构造一个含有恶意代码的问题标题,这些都可能让模板输出错误的输出,从而让他们控制页面。 下面的模板以HTML格式输出issue列表。注意import语句的不同: gopl.io/ch4/issueshtml import \"html/template\" var issueList = template.Must(template.New(\"issuelist\").Parse(`\n

{{.TotalCount}} issues

\n\n\n\n{{range .Items}}\n\n\n{{end}}\n
# State User Title
{{.Number}} {{.State}} {{.User.Login}} {{.Title}}
\n`)) {% endraw %} 下面的命令将在新的模板上执行一个稍微不同的查询: $ go build gopl.io/ch4/issueshtml\n$ ./issueshtml repo:golang/go commenter:gopherbot json encoder >issues.html 图4.4显示了在web浏览器中的效果图。每个issue包含到Github对应页面的链接。 图4.4中issue没有包含会对HTML格式产生冲突的特殊字符,但是我们马上将看到标题中含有&和<字符的issue。下面的命令选择了两个这样的issue: $ ./issueshtml repo:golang/go 3133 10535 >issues2.html 图4.5显示了该查询的结果。注意,html/template包已经自动将特殊字符转义,因此我们依然可以看到正确的字面值。如果我们使用text/template包的话,这2个issue将会产生错误,其中“<”四个字符将会被当作小于字符“<”处理,同时“”字符串将会被当作一个链接元素处理,它们都会导致HTML文档结构的改变,从而导致有未知的风险。 我们也可以通过对信任的HTML字符串使用template.HTML类型来抑制这种自动转义的行为。还有很多采用类型命名的字符串类型分别对应信任的JavaScript、CSS和URL。下面的程序演示了两个使用不同类型的相同字符串产生的不同结果:A是一个普通字符串,B是一个信任的template.HTML字符串类型。 gopl.io/ch4/autoescape func main() { const templ = `

A: {{.A}}

B: {{.B}}

` t := template.Must(template.New(\"escape\").Parse(templ)) var data struct { A string // untrusted plain text B template.HTML // trusted HTML } data.A = \"Hello!\" data.B = \"Hello!\" if err := t.Execute(os.Stdout, data); err != nil { log.Fatal(err) }\n} {% endraw %} 图4.6显示了出现在浏览器中的模板输出。我们看到A的黑体标记被转义失效了,但是B没有。 我们这里只讲述了模板系统中最基本的特性。一如既往,如果想了解更多的信息,请自己查看包文档: $ go doc text/template\n$ go doc html/template 练习 4.14: 创建一个web服务器,查询一次GitHub,然后生成BUG报告、里程碑和对应的用户信息。","breadcrumbs":"复合数据类型 » 文本和HTML模板 » 4.6. 文本和HTML模板","id":"57","title":"4.6. 文本和HTML模板"},"58":{"body":"函数可以让我们将一个语句序列打包为一个单元,然后可以从程序中其它地方多次调用。函数的机制可以让我们将一个大的工作分解为小的任务,这样的小任务可以让不同程序员在不同时间、不同地方独立完成。一个函数同时对用户隐藏了其实现细节。由于这些因素,对于任何编程语言来说,函数都是一个至关重要的部分。 我们已经见过许多函数了。现在,让我们多花一点时间来彻底地讨论函数特性。本章的运行示例是一个网络蜘蛛,也就是web搜索引擎中负责抓取网页部分的组件,它们根据抓取网页中的链接继续抓取链接指向的页面。一个网络蜘蛛的例子给我们足够的机会去探索递归函数、匿名函数、错误处理和函数其它的很多特性。","breadcrumbs":"函数 » 第5章 函数","id":"58","title":"第5章 函数"},"59":{"body":"函数声明包括函数名、形式参数列表、返回值列表(可省略)以及函数体。 func name(parameter-list) (result-list) { body\n} 形式参数列表描述了函数的参数名以及参数类型。这些参数作为局部变量,其值由参数调用者提供。返回值列表描述了函数返回值的变量名以及类型。如果函数返回一个无名变量或者没有返回值,返回值列表的括号是可以省略的。如果一个函数声明不包括返回值列表,那么函数体执行完毕后,不会返回任何值。在hypot函数中: func hypot(x, y float64) float64 { return math.Sqrt(x*x + y*y)\n}\nfmt.Println(hypot(3,4)) // \"5\" x和y是形参名,3和4是调用时的传入的实参,函数返回了一个float64类型的值。 返回值也可以像形式参数一样被命名。在这种情况下,每个返回值被声明成一个局部变量,并根据该返回值的类型,将其初始化为该类型的零值。 如果一个函数在声明时,包含返回值列表,该函数必须以 return语句结尾,除非函数明显无法运行到结尾处。例如函数在结尾时调用了panic异常或函数中存在无限循环。 正如hypot一样,如果一组形参或返回值有相同的类型,我们不必为每个形参都写出参数类型。下面2个声明是等价的: func f(i, j, k int, s, t string) { /* ... */ }\nfunc f(i int, j int, k int, s string, t string) { /* ... */ } 下面,我们给出4种方法声明拥有2个int型参数和1个int型返回值的函数.blank identifier(译者注:即下文的_符号)可以强调某个参数未被使用。 func add(x int, y int) int {return x + y}\nfunc sub(x, y int) (z int) { z = x - y; return}\nfunc first(x int, _ int) int { return x }\nfunc zero(int, int) int { return 0 } fmt.Printf(\"%T\\n\", add) // \"func(int, int) int\"\nfmt.Printf(\"%T\\n\", sub) // \"func(int, int) int\"\nfmt.Printf(\"%T\\n\", first) // \"func(int, int) int\"\nfmt.Printf(\"%T\\n\", zero) // \"func(int, int) int\" 函数的类型被称为函数的签名。如果两个函数形式参数列表和返回值列表中的变量类型一一对应,那么这两个函数被认为有相同的类型或签名。形参和返回值的变量名不影响函数签名,也不影响它们是否可以以省略参数类型的形式表示。 每一次函数调用都必须按照声明顺序为所有参数提供实参(参数值)。在函数调用时,Go语言没有默认参数值,也没有任何方法可以通过参数名指定形参,因此形参和返回值的变量名对于函数调用者而言没有意义。 在函数体中,函数的形参作为局部变量,被初始化为调用者提供的值。函数的形参和有名返回值作为函数最外层的局部变量,被存储在相同的词法块中。 实参通过值的方式传递,因此函数的形参是实参的拷贝。对形参进行修改不会影响实参。但是,如果实参包括引用类型,如指针,slice(切片)、map、function、channel等类型,实参可能会由于函数的间接引用被修改。 你可能会偶尔遇到没有函数体的函数声明,这表示该函数不是以Go实现的。这样的声明定义了函数签名。 package math func Sin(x float64) float //implemented in assembly language","breadcrumbs":"函数 » 函数声明 » 5.1. 函数声明","id":"59","title":"5.1. 函数声明"},"6":{"body":"Rob Pike 和 Russ Cox ,以及很多其他Go团队的核心成员多次仔细阅读了本书的手稿,他们对本书的组织结构和表述用词等给出了很多宝贵的建议。在准备日文版翻译的时候,Yoshiki Shibata更是仔细地审阅了本书的每个部分,及时发现了诸多英文和代码的错误。我们非常感谢本书的每一位审阅者,并感谢对本书给出了重要的建议的Brian Goetz、Corey Kosak、Arnold Robbins、Josh Bleecher Snyder和Peter Weinberger等人。 我们还感谢Sameer Ajmani、Ittai Balaban、David Crawshaw、Billy Donohue、Jonathan Feinberg、Andrew Gerrand、Robert Griesemer、John Linderman、Minux Ma(译注:中国人,Go团队成员。)、Bryan Mills、Bala Natarajan、Cosmos Nicolaou、Paul Staniforth、Nigel Tao(译注:好像是陶哲轩的兄弟)以及Howard Trickey给出的许多有价值的建议。我们还要感谢David Brailsford和Raph Levien关于类型设置的建议。 我们从来自Addison-Wesley的编辑Greg Doench收到了很多帮助,从最开始就得到了越来越多的帮助。来自AW生产团队的John Fuller、Dayna Isley、Julie Nahil、Chuti Prasertsith到Barbara Wood,感谢你们的热心帮助。 Alan Donovan 特别感谢:Sameer Ajmani、Chris Demetriou、Walt Drummond和Google公司的Reid Tatge允许他有充裕的时间去写本书;感谢Stephen Donovan的建议和始终如一的鼓励,以及他的妻子Leila Kazemi并没有让他为了家庭琐事而分心,并热情坚定地支持这个项目。 Brian Kernighan 特别感谢:朋友和同事对他的耐心和宽容,让他慢慢地梳理本书的写作思路。同时感谢他的妻子Meg和其他很多朋友对他写作事业的支持。 2015年 10月 于 纽约","breadcrumbs":"前言 » 致谢","id":"6","title":"致谢"},"60":{"body":"函数可以是递归的,这意味着函数可以直接或间接的调用自身。对许多问题而言,递归是一种强有力的技术,例如处理递归的数据结构。在4.4节,我们通过遍历二叉树来实现简单的插入排序,在本章节,我们再次使用它来处理HTML文件。 下文的示例代码使用了非标准包 golang.org/x/net/html ,解析HTML。golang.org/x/... 目录下存储了一些由Go团队设计、维护,对网络编程、国际化文件处理、移动平台、图像处理、加密解密、开发者工具提供支持的扩展包。未将这些扩展包加入到标准库原因有二,一是部分包仍在开发中,二是对大多数Go语言的开发者而言,扩展包提供的功能很少被使用。 例子中调用golang.org/x/net/html的部分api如下所示。html.Parse函数读入一组bytes解析后,返回html.Node类型的HTML页面树状结构根节点。HTML拥有很多类型的结点如text(文本)、commnets(注释)类型,在下面的例子中,我们 只关注< name key='value' >形式的结点。 golang.org/x/net/html package html type Node struct { Type NodeType Data string Attr []Attribute FirstChild, NextSibling *Node\n} type NodeType int32 const ( ErrorNode NodeType = iota TextNode DocumentNode ElementNode CommentNode DoctypeNode\n) type Attribute struct { Key, Val string\n} func Parse(r io.Reader) (*Node, error) main函数解析HTML标准输入,通过递归函数visit获得links(链接),并打印出这些links: gopl.io/ch5/findlinks1 // Findlinks1 prints the links in an HTML document read from standard input.\npackage main import ( \"fmt\" \"os\" \"golang.org/x/net/html\"\n) func main() { doc, err := html.Parse(os.Stdin) if err != nil { fmt.Fprintf(os.Stderr, \"findlinks1: %v\\n\", err) os.Exit(1) } for _, link := range visit(nil, doc) { fmt.Println(link) }\n} visit函数遍历HTML的节点树,从每一个anchor元素的href属性获得link,将这些links存入字符串数组中,并返回这个字符串数组。 // visit appends to links each link found in n and returns the result.\nfunc visit(links []string, n *html.Node) []string { if n.Type == html.ElementNode && n.Data == \"a\" { for _, a := range n.Attr { if a.Key == \"href\" { links = append(links, a.Val) } } } for c := n.FirstChild; c != nil; c = c.NextSibling { links = visit(links, c) } return links\n} 为了遍历结点n的所有后代结点,每次遇到n的孩子结点时,visit递归的调用自身。这些孩子结点存放在FirstChild链表中。 让我们以Go的主页(golang.org)作为目标,运行findlinks。我们以fetch(1.5章)的输出作为findlinks的输入。下面的输出做了简化处理。 $ go build gopl.io/ch1/fetch\n$ go build gopl.io/ch5/findlinks1\n$ ./fetch https://golang.org | ./findlinks1\n#\n/doc/\n/pkg/\n/help/\n/blog/\nhttp://play.golang.org/\n//tour.golang.org/\nhttps://golang.org/dl/\n//blog.golang.org/\n/LICENSE\n/doc/tos.html\nhttp://www.google.com/intl/en/policies/privacy/ 注意在页面中出现的链接格式,在之后我们会介绍如何将这些链接,根据根路径( https://golang.org )生成可以直接访问的url。 在函数outline中,我们通过递归的方式遍历整个HTML结点树,并输出树的结构。在outline内部,每遇到一个HTML元素标签,就将其入栈,并输出。 gopl.io/ch5/outline func main() { doc, err := html.Parse(os.Stdin) if err != nil { fmt.Fprintf(os.Stderr, \"outline: %v\\n\", err) os.Exit(1) } outline(nil, doc)\n}\nfunc outline(stack []string, n *html.Node) { if n.Type == html.ElementNode { stack = append(stack, n.Data) // push tag fmt.Println(stack) } for c := n.FirstChild; c != nil; c = c.NextSibling { outline(stack, c) }\n} 有一点值得注意:outline有入栈操作,但没有相对应的出栈操作。当outline调用自身时,被调用者接收的是stack的拷贝。被调用者对stack的元素追加操作,修改的是stack的拷贝,其可能会修改slice底层的数组甚至是申请一块新的内存空间进行扩容;但这个过程并不会修改调用方的stack。因此当函数返回时,调用方的stack与其调用自身之前完全一致。 下面是 https://golang.org 页面的简要结构: $ go build gopl.io/ch5/outline\n$ ./fetch https://golang.org | ./outline\n[html]\n[html head]\n[html head meta]\n[html head title]\n[html head link]\n[html body]\n[html body div]\n[html body div]\n[html body div div]\n[html body div div form]\n[html body div div form div]\n[html body div div form div a]\n... 正如你在上面实验中所见,大部分HTML页面只需几层递归就能被处理,但仍然有些页面需要深层次的递归。 大部分编程语言使用固定大小的函数调用栈,常见的大小从64KB到2MB不等。固定大小栈会限制递归的深度,当你用递归处理大量数据时,需要避免栈溢出;除此之外,还会导致安全性问题。与此相反,Go语言使用可变栈,栈的大小按需增加(初始时很小)。这使得我们使用递归时不必考虑溢出和安全问题。 练习 5.1: 修改findlinks代码中遍历n.FirstChild链表的部分,将循环调用visit,改成递归调用。 练习 5.2: 编写函数,记录在HTML树中出现的同名元素的次数。 练习 5.3: 编写函数输出所有text结点的内容。注意不要访问 + {{/if}} + + + + + + + + + + + + + + + + + +
+ +
+ {{> header}} + + + + {{#if search_enabled}} + + {{/if}} + + + + +
+ +
+
+ + +
+ + {{{ content }}} + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + {{#if livereload}} + + + {{/if}} + + {{#if google_analytics}} + + + {{/if}} + + {{#if playground_line_numbers}} + + {{/if}} + + {{#if playground_copyable}} + + {{/if}} + + {{#if playground_js}} + + + + + + {{/if}} + + {{#if search_js}} + + + + {{/if}} + + + + + + + + + {{#each additional_js}} + + {{/each}} + + {{#if is_print}} + {{#if mathjax_support}} + + {{else}} + + {{/if}} + {{/if}} + + + diff --git a/tomorrow-night.css b/tomorrow-night.css new file mode 100644 index 0000000..f719792 --- /dev/null +++ b/tomorrow-night.css @@ -0,0 +1,104 @@ +/* Tomorrow Night Theme */ +/* http://jmblog.github.com/color-themes-for-google-code-highlightjs */ +/* Original theme - https://github.com/chriskempson/tomorrow-theme */ +/* http://jmblog.github.com/color-themes-for-google-code-highlightjs */ + +/* Tomorrow Comment */ +.hljs-comment { + color: #969896; +} + +/* Tomorrow Red */ +.hljs-variable, +.hljs-attribute, +.hljs-tag, +.hljs-regexp, +.ruby .hljs-constant, +.xml .hljs-tag .hljs-title, +.xml .hljs-pi, +.xml .hljs-doctype, +.html .hljs-doctype, +.css .hljs-id, +.css .hljs-class, +.css .hljs-pseudo { + color: #cc6666; +} + +/* Tomorrow Orange */ +.hljs-number, +.hljs-preprocessor, +.hljs-pragma, +.hljs-built_in, +.hljs-literal, +.hljs-params, +.hljs-constant { + color: #de935f; +} + +/* Tomorrow Yellow */ +.ruby .hljs-class .hljs-title, +.css .hljs-rule .hljs-attribute { + color: #f0c674; +} + +/* Tomorrow Green */ +.hljs-string, +.hljs-value, +.hljs-inheritance, +.hljs-header, +.hljs-name, +.ruby .hljs-symbol, +.xml .hljs-cdata { + color: #b5bd68; +} + +/* Tomorrow Aqua */ +.hljs-title, +.css .hljs-hexcolor { + color: #8abeb7; +} + +/* Tomorrow Blue */ +.hljs-function, +.python .hljs-decorator, +.python .hljs-title, +.ruby .hljs-function .hljs-title, +.ruby .hljs-title .hljs-keyword, +.perl .hljs-sub, +.javascript .hljs-title, +.coffeescript .hljs-title { + color: #81a2be; +} + +/* Tomorrow Purple */ +.hljs-keyword, +.javascript .hljs-function { + color: #b294bb; +} + +.hljs { + display: block; + overflow-x: auto; + background: #1d1f21; + color: #c5c8c6; + padding: 0.5em; + -webkit-text-size-adjust: none; +} + +.coffeescript .javascript, +.javascript .xml, +.tex .hljs-formula, +.xml .javascript, +.xml .vbscript, +.xml .css, +.xml .hljs-cdata { + opacity: 0.5; +} + +.hljs-addition { + color: #718c00; +} + +.hljs-deletion { + color: #c82829; +} diff --git a/tools/STPhrases.txt b/tools/STPhrases.txt new file mode 100644 index 0000000..11be47c --- /dev/null +++ b/tools/STPhrases.txt @@ -0,0 +1,48933 @@ +㓦划 㓦劃 +一丝不挂 一絲不掛 +一了心愿 一了心願 +一了百了 一了百了 +一了百当 一了百當 +一争两丑 一爭兩醜 +一伙 一夥 +一伙人 一夥人 +一伙头 一夥頭 +一偿宿愿 一償宿願 +一元复始 一元復始 +一克 一克 +一党 一黨 +一冲性子 一沖性子 +一准 一準 +一出剧 一齣劇 +一出去 一出去 +一出场 一出場 +一出子 一齣子 +一出戏 一齣戲 +一出来 一出來 +一出生 一出生 +一出祁山 一出祁山 +一分收获 一分收穫 +一分耕耘 一分耕耘 +一分钟 一分鐘 +一划 一劃 +一别 一別 +一别多年 一別多年 +一别头 一彆頭 +一刻千金 一刻千金 +一前一后 一前一後 +一力承当 一力承當 +一卷 一卷 +一厘一毫 一釐一毫 +一厢情愿 一廂情願 +一去不回 一去不回 +一去不复 一去不復 +一去不复返 一去不復返 +一发 一發 +一发之差 一髮之差 +一发之间 一髮之間 +一发千钧 一髮千鈞 +一口钟 一口鐘 +一只 一隻 +一台 一臺 +一台台 一臺臺 +一叶 一葉 +一叶兰 一葉蘭 +一叶扁舟 一葉扁舟 +一叶知秋 一葉知秋 +一号木杆 一號木桿 +一吊 一吊 +一吊钱 一吊錢 +一同 一同 +一向 一向 +一周 一週 +一周天 一週天 +一周年 一週年 +一周遭 一周遭 +一哄 一鬨 +一哄而上 一哄而上 +一哄而散 一鬨而散 +一哄而起 一哄而起 +一哄而集 一哄而集 +一喊出 一喊出 +一回 一回 +一回事 一回事 +一团 一團 +一团和气 一團和氣 +一团团 一團團 +一团漆黑 一團漆黑 +一团火 一團火 +一团糟 一團糟 +一国两制 一國兩制 +一地胡拿 一地胡拿 +一地里 一地裏 +一块面 一塊麪 +一坛 一罈 +一坛坛 一罈罈 +一坛死水 一壇死水 +一塌糊涂 一塌糊塗 +一壶千金 一壺千金 +一夜致富 一夜致富 +一大伙 一大夥 +一天后 一天後 +一天星斗 一天星斗 +一天钟 一天鐘 +一夫一妻 一夫一妻 +一夫当关 一夫當關 +一妻制 一妻制 +一妻多夫 一妻多夫 +一孔出气 一孔出氣 +一字一板 一字一板 +一字千金 一字千金 +一寸秋波 一寸秋波 +一将功成 一將功成 +一展长才 一展長才 +一干 一干 +一干二净 一乾二淨 +一干人 一干人 +一干家中 一干家中 +一干弟兄 一干弟兄 +一干弟子 一干弟子 +一干而尽 一乾而盡 +一干部下 一干部下 +一并 一併 +一开出来 一開出來 +一弦 一弦 +一当 一當 +一心向上 一心向上 +一念 一念 +一念三千 一念三千 +一扎 一紮 +一托头 一托頭 +一托气 一托氣 +一扣 一扣 +一折 一折 +一折一磨 一折一磨 +一折两段 一折兩段 +一折八扣 一折八扣 +一拍即合 一拍即合 +一挂 一掛 +一挂之下 一掛之下 +一掷千金 一擲千金 +一掷百万 一擲百萬 +一搜 一搜 +一摇一摆 一搖一擺 +一摇三摆 一搖三擺 +一播出 一播出 +一斗 一斗 +一斗再斗 一鬥再鬥 +一斗斗 一斗斗 +一方面 一方面 +一无所获 一無所獲 +一日万机 一日萬機 +一日三秋 一日三秋 +一日千里 一日千里 +一日叫娘 一日叫孃 +一早起了 一早起了 +一时糊涂 一時糊塗 +一曲 一曲 +一曲千金 一曲千金 +一曲阳关 一曲陽關 +一本万利 一本萬利 +一杆 一杆 +一杆进洞 一桿進洞 +一杠 一槓 +一杯 一杯 +一杯杯 一杯杯 +一杯羹 一杯羹 +一松 一鬆 +一板 一板 +一板一眼 一板一眼 +一板三眼 一板三眼 +一柜 一櫃 +一树百获 一樹百穫 +一根烟 一根菸 +一死了之 一死了之 +一毫一发 一毫一髮 +一池秋水 一池秋水 +一沐三捉发 一沐三捉髮 +一沐三握发 一沐三握髮 +一波三折 一波三折 +一泻千里 一瀉千里 +一派胡言 一派胡言 +一流人才 一流人才 +一涂 一塗 +一游 一遊 +一溜烟 一溜煙 +一点钟 一點鐘 +一物一制 一物一制 +一物克一物 一物剋一物 +一百多万 一百多萬 +一百廿万 一百廿萬 +一目了然 一目瞭然 +一相情愿 一相情願 +一看出 一看出 +一碗面 一碗麪 +一碧万顷 一碧萬頃 +一禾九穗 一禾九穗 +一种 一種 +一秒钟 一秒鐘 +一穗三秀 一穗三秀 +一笑了之 一笑了之 +一笑千金 一笑千金 +一笔划 一筆劃 +一答一合 一答一合 +一签 一簽 +一箭之仇 一箭之仇 +一箭双雕 一箭雙鵰 +一系 一系 +一系列 一系列 +一统志 一統志 +一网打尽 一網打盡 +一翻出 一翻出 +一胎制 一胎制 +一至于此 一至於此 +一致 一致 +一致字 一致字 +一致性 一致性 +一致百虑 一致百慮 +一般等价 一般等價 +一落千丈 一落千丈 +一表 一表 +一表人才 一表人才 +一表人材 一表人材 +一表人物 一表人物 +一表非俗 一表非俗 +一表非凡 一表非凡 +一见钟情 一見鍾情 +一见面 一見面 +一视同仁 一視同仁 +一览表 一覽表 +一触即发 一觸即發 +一言不发 一言不發 +一言不合 一言不合 +一言千金 一言千金 +一言已定千金不移 一言已定千金不移 +一言既出 一言既出 +一言既出驷马难追 一言既出駟馬難追 +一言难尽 一言難盡 +一讲出 一講出 +一语不发 一語不發 +一说出 一說出 +一诺值千金 一諾值千金 +一诺千金 一諾千金 +一败涂地 一敗塗地 +一赞 一讚 +一走了之 一走了之 +一起干 一起幹 +一蹴可几 一蹴可幾 +一身作事一身当 一身作事一身當 +一轨同风 一軌同風 +一辞莫赞 一辭莫贊 +一递里 一遞裏 +一逞兽欲 一逞獸慾 +一道烟 一道煙 +一醉解千愁 一醉解千愁 +一里 一里 +一里一外 一裏一外 +一针 一針 +一针见血 一針見血 +一锅面 一鍋麪 +一锹掘个井 一鍬掘個井 +一院制 一院制 +一雨成秋 一雨成秋 +一面 一面 +一面之交 一面之交 +一面之缘 一面之緣 +一面之识 一面之識 +一面之词 一面之詞 +一面之辞 一面之辭 +一面之雅 一面之雅 +一面倒 一面倒 +一面儿官司 一面兒官司 +一面如旧 一面如舊 +一面点 一面點 +一飞冲天 一飛沖天 +一食万钱 一食萬錢 +一饭千金 一飯千金 +一饮而尽 一飲而盡 +一马当先 一馬當先 +一麾出守 一麾出守 +一鼓一板 一鼓一板 +一鼻孔出气 一鼻孔出氣 +一龙生九种种种各别 一龍生九種種種各別 +丁一确二 丁一確二 +丁丁冬冬 丁丁冬冬 +丁丁当当 丁丁當當 +丁丁炒面 丁丁炒面 +丁丑 丁丑 +丁伯升 丁伯升 +丁克 丁克 +丁冬 丁冬 +丁固生松 丁固生松 +丁娘十索 丁娘十索 +丁字梁 丁字梁 +丁当 丁當 +丁柏升 丁柏升 +丁种 丁種 +丁种维生素 丁種維生素 +丁铃当啷 丁鈴噹啷 +丁零当啷 丁零當啷 +丁鸿志 丁鴻志 +七万 七萬 +七万三千 七萬三千 +七万五千 七萬五千 +七万八千 七萬八千 +七万六千 七萬六千 +七万四千 七萬四千 +七个 七個 +七个八个 七個八個 +七了八当 七了八當 +七余 七餘 +七八下里 七八下裏 +七出 七出 +七出祁山 七出祁山 +七分钟 七分鐘 +七划 七劃 +七十七万 七十七萬 +七十七国集团 七十七國集團 +七十五万 七十五萬 +七千 七千 +七千两百 七千兩百 +七发 七發 +七只 七隻 +七台 七臺 +七台河 七臺河 +七台河市 七臺河市 +七叶胆 七葉膽 +七周 七週 +七回 七回 +七团 七團 +七国集团 七國集團 +七坛 七罈 +七天后 七天後 +七娘 七娘 +七娘妈 七孃媽 +七孔生烟 七孔生煙 +七巧板 七巧板 +七弦 七絃 +七彩 七彩 +七彩缤纷 七彩繽紛 +七彩虹 七彩虹 +七情六欲 七情六慾 +七扎 七紮 +七折 七折 +七折八扣 七折八扣 +七政四余 七政四餘 +七星坛 七星壇 +七星岩 七星巖 +七星板 七星板 +七星瓢虫 七星瓢蟲 +七杯 七杯 +七步之才 七步之才 +七步奇才 七步奇才 +七点钟 七點鐘 +七百万 七百萬 +七百多万 七百多萬 +七种 七種 +七秒钟 七秒鐘 +七窍冒烟 七竅冒煙 +七窍生烟 七竅生煙 +七色板 七色板 +七里河 七里河 +七里河区 七里河區 +七里香 七里香 +万一 萬一 +万一只 萬一只 +万万 萬萬 +万万不可 萬萬不可 +万万千千 萬萬千千 +万万岁 萬萬歲 +万丈 萬丈 +万丈光芒 萬丈光芒 +万丈深渊 萬丈深淵 +万丈竿头 萬丈竿頭 +万丈红尘 萬丈紅塵 +万丈高楼平地起 萬丈高樓平地起 +万不及一 萬不及一 +万不可失 萬不可失 +万不失一 萬不失一 +万不得已 萬不得已 +万世 萬世 +万世一时 萬世一時 +万世师表 萬世師表 +万个 萬個 +万丰 萬豐 +万丹 萬丹 +万丹乡 萬丹鄉 +万乘 萬乘 +万乘之国 萬乘之國 +万乘之尊 萬乘之尊 +万事 萬事 +万事亨通 萬事亨通 +万事俱备只欠东风 萬事俱備只欠東風 +万事具备 萬事具備 +万事大吉 萬事大吉 +万事如意 萬事如意 +万事得 萬事得 +万事皆从急中错 萬事皆從急中錯 +万事皆休 萬事皆休 +万事皆备 萬事皆備 +万事起头难 萬事起頭難 +万事达 萬事達 +万事达卡 萬事達卡 +万事通 萬事通 +万人 萬人 +万人之敌 萬人之敵 +万人之敵 萬人之敵 +万人坑 萬人坑 +万人敌 萬人敵 +万人空巷 萬人空巷 +万人迷 萬人迷 +万仞 萬仞 +万代 萬代 +万代一时 萬代一時 +万代兰 萬代蘭 +万代千秋 萬代千秋 +万份 萬份 +万众 萬衆 +万众一心 萬衆一心 +万众欢腾 萬衆歡騰 +万众瞩目 萬衆矚目 +万位 萬位 +万余 萬餘 +万余只 萬餘隻 +万余里 萬餘里 +万俟 万俟 +万倍 萬倍 +万儿 萬兒 +万儿八千 萬兒八千 +万元 萬元 +万元户 萬元戶 +万全 萬全 +万全之策 萬全之策 +万全之计 萬全之計 +万全县 萬全縣 +万全街 萬全街 +万兽之王 萬獸之王 +万几 萬幾 +万分 萬分 +万分之一 萬分之一 +万分感激 萬分感激 +万分痛苦 萬分痛苦 +万别千差 萬別千差 +万剐千刀 萬剮千刀 +万劫 萬劫 +万劫不复 萬劫不復 +万千 萬千 +万华 萬華 +万华区 萬華區 +万华站 萬華站 +万博宣伟 萬博宣偉 +万博省 萬博省 +万卷 萬卷 +万卷书 萬卷書 +万历 萬曆 +万县 萬縣 +万县地区 萬縣地區 +万县市 萬縣市 +万县港 萬縣港 +万变不离其宗 萬變不離其宗 +万古 萬古 +万古不灭 萬古不滅 +万古千秋 萬古千秋 +万古流芳 萬古流芳 +万古留芳 萬古留芳 +万古长新 萬古長新 +万古长春 萬古長春 +万古长青 萬古長青 +万只 萬隻 +万叶 萬葉 +万名 萬名 +万向节 萬向節 +万国 萬國 +万国公报 萬國公報 +万国公法 萬國公法 +万国宫 萬國宮 +万国旗 萬國旗 +万国码 萬國碼 +万国邮联 萬國郵聯 +万国音标 萬國音標 +万圆 萬圓 +万圣节 萬聖節 +万坛 萬罈 +万境归空 萬境歸空 +万壑 萬壑 +万壑争流 萬壑爭流 +万天后 萬天後 +万夫 萬夫 +万夫不当 萬夫不當 +万夫莫当 萬夫莫當 +万夫莫敌 萬夫莫敵 +万头攒动 萬頭攢動 +万姓 萬姓 +万姓统谱 萬姓統譜 +万字 萬字 +万宁市 萬寧市 +万安 萬安 +万安县 萬安縣 +万宝华 萬寶華 +万宝囊 萬寶囊 +万宝常 萬寶常 +万宝路 萬寶路 +万家 萬家 +万家乐 萬家樂 +万家灯火 萬家燈火 +万家生佛 萬家生佛 +万家香 萬家香 +万寿 萬壽 +万寿千秋 萬壽千秋 +万寿山 萬壽山 +万寿无疆 萬壽無疆 +万寿果 萬壽果 +万寿菊 萬壽菊 +万山 萬山 +万山特区 萬山特區 +万山镇 萬山鎮 +万岁 萬歲 +万岁千秋 萬歲千秋 +万岁爷 萬歲爺 +万峦 萬巒 +万峦乡 萬巒鄉 +万州区 萬州區 +万年 萬年 +万年历 萬年曆 +万年历表 萬年曆錶 +万年县 萬年縣 +万年青 萬年青 +万幸 萬幸 +万念 萬念 +万念俱灰 萬念俱灰 +万急 萬急 +万恩市 萬恩市 +万恶 萬惡 +万恶之源 萬惡之源 +万恶之首 萬惡之首 +万恶淫为首 萬惡淫爲首 +万恶滔天 萬惡滔天 +万户 萬戶 +万户侯 萬戶侯 +万户千门 萬戶千門 +万扎 萬紮 +万把 萬把 +万把块 萬把塊 +万斛泉源 萬斛泉源 +万斤 萬斤 +万斯同 萬斯同 +万方 萬方 +万旗 万旗 +万无 萬無 +万无一失 萬無一失 +万无失一 萬無失一 +万智牌 萬智牌 +万有 萬有 +万有引力 萬有引力 +万有引力定律 萬有引力定律 +万望 萬望 +万机 萬機 +万杞良 萬杞良 +万柏林 萬柏林 +万柏林区 萬柏林區 +万桶 萬桶 +万步 萬步 +万步表 萬步表 +万死 萬死 +万死一生 萬死一生 +万死不辞 萬死不辭 +万段 萬段 +万毒枯 萬毒枯 +万民伞 萬民傘 +万水千山 萬水千山 +万沙浪 萬沙浪 +万泉河 萬泉河 +万法唯识 萬法唯識 +万泰银 萬泰銀 +万洋山 萬洋山 +万源市 萬源市 +万灵丹 萬靈丹 +万灵节 萬靈節 +万灵药 萬靈藥 +万点 萬點 +万点大关 萬點大關 +万物 萬物 +万物之灵 萬物之靈 +万状 萬狀 +万用 萬用 +万用字元 萬用字元 +万用手冊 萬用手冊 +万用电表 萬用電表 +万用表 萬用表 +万盛 萬盛 +万盛区 萬盛區 +万盛溪 萬盛溪 +万目睽睽 萬目睽睽 +万石 萬石 +万福 萬福 +万福玛丽亚 萬福瑪麗亞 +万秀区 萬秀區 +万种 萬種 +万种风情 萬種風情 +万窍 萬竅 +万端 萬端 +万签插架 萬籤插架 +万箭攒心 萬箭攢心 +万箭穿心 萬箭穿心 +万箭穿身 萬箭穿身 +万箱 萬箱 +万籁 萬籟 +万籁俱寂 萬籟俱寂 +万籁无声 萬籟無聲 +万籤插架 萬籤插架 +万米 萬米 +万米长跑 萬米長跑 +万紫千红 萬紫千紅 +万红千紫 萬紅千紫 +万绪千头 萬緒千頭 +万绪千端 萬緒千端 +万维网 萬維網 +万绿丛中 萬綠叢中 +万缕千丝 萬縷千絲 +万缘 萬緣 +万能 萬能 +万能工专 萬能工專 +万能曲尺 萬能曲尺 +万能梗 萬能梗 +万能梗犬 萬能梗犬 +万能胶 萬能膠 +万能选手 萬能選手 +万能钥匙 萬能鑰匙 +万能锅 萬能鍋 +万般 萬般 +万般皆下品 萬般皆下品 +万般皆是命 萬般皆是命 +万艾可 萬艾可 +万花争艳 萬花爭豔 +万花筒 萬花筒 +万苦千辛 萬苦千辛 +万荣乡 萬榮鄉 +万荣县 萬榮縣 +万虑俱清 萬慮俱清 +万言 萬言 +万言书 萬言書 +万语千言 萬語千言 +万象 萬象 +万象包罗 萬象包羅 +万象更新 萬象更新 +万象森罗 萬象森羅 +万贯 萬貫 +万贯家产 萬貫家產 +万贯家私 萬貫家私 +万贯家财 萬貫家財 +万载千秋 萬載千秋 +万载县 萬載縣 +万道 萬道 +万那杜 萬那杜 +万邦 萬邦 +万部 萬部 +万里 萬里 +万里之望 萬里之望 +万里乡 萬里鄉 +万里侯 萬里侯 +万里同风 萬里同風 +万里封侯 萬里封侯 +万里无云 萬里無雲 +万里春愁直 萬裏春愁直 +万里晴空 萬里晴空 +万里江山 萬里江山 +万里迢迢 萬里迢迢 +万里追踪 萬里追蹤 +万里长城 萬里長城 +万里长征 萬里長征 +万里长江 萬里長江 +万里长空 萬里長空 +万里鹏程 萬里鵬程 +万里鹏翼 萬里鵬翼 +万重 萬重 +万重山 萬重山 +万金 萬金 +万金不换 萬金不換 +万金之躯 萬金之軀 +万金油 萬金油 +万钧 萬鈞 +万钧之力 萬鈞之力 +万锺 萬鍾 +万难 萬難 +万顷 萬頃 +万顷琉璃 萬頃琉璃 +万顷碧波 萬頃碧波 +万馀 萬餘 +万马 萬馬 +万马千军 萬馬千軍 +万马奔腾 萬馬奔騰 +万马皆瘖 萬馬皆瘖 +万马齐喑 萬馬齊喑 +万马齐瘖 萬馬齊瘖 +万鸦老 萬鴉老 +万齐融 萬齊融 +丈余 丈餘 +丈八灯台 丈八燈臺 +丈母娘 丈母孃 +三万 三萬 +三个 三個 +三个鼻子管 三個鼻子管 +三丰 三豐 +三仙台 三仙臺 +三代同堂 三代同堂 +三余 三餘 +三元合金 三元合金 +三元里 三元里 +三克 三克 +三党 三黨 +三八制 三八制 +三冬 三冬 +三冬两夏 三冬兩夏 +三准 三準 +三出祁山 三出祁山 +三分钟 三分鐘 +三只 三隻 +三只手 三隻手 +三台 三臺 +三台县 三臺縣 +三台联播 三臺聯播 +三叶期 三葉期 +三叶松 三葉松 +三叶草 三葉草 +三叶虫 三葉蟲 +三叹 三嘆 +三合会 三合會 +三合土 三合土 +三合房 三合房 +三合星 三合星 +三合板 三合板 +三合院 三合院 +三同 三同 +三向 三向 +三周 三週 +三周年 三週年 +三呼万岁 三呼萬歲 +三回两转 三回兩轉 +三回九曲 三回九曲 +三回九转 三回九轉 +三回五次 三回五次 +三回五转 三回五轉 +三国志 三國志 +三坛大戒 三壇大戒 +三复 三複 +三复斯言 三復斯言 +三复白圭 三復白圭 +三大发明 三大發明 +三大差别 三大差別 +三天后 三天後 +三头两面 三頭兩面 +三头马车制 三頭馬車制 +三夹板 三夾板 +三娘教子 三孃教子 +三对三斗牛 三對三鬥牛 +三对六面 三對六面 +三尸 三尸 +三尸神 三尸神 +三年制 三年制 +三弦 三絃 +三征七辟 三徵七辟 +三思台 三思臺 +三思而后 三思而後 +三思而后行 三思而後行 +三恶道 三惡道 +三战两胜 三戰兩勝 +三才 三才 +三才图会 三才圖會 +三扎 三紮 +三折肱 三折肱 +三折肱为良医 三折肱爲良醫 +三振出局 三振出局 +三方面 三方面 +三日不读书面目可憎 三日不讀書面目可憎 +三星集团 三星集團 +三月里的桃花 三月裏的桃花 +三杆 三杆 +三杠 三槓 +三杯 三杯 +三杯和万事 三杯和萬事 +三杯鸡 三杯雞 +三板 三板 +三极 三極 +三极真空 三極真空 +三极管 三極管 +三次曲线 三次曲線 +三段制 三段制 +三江并流 三江並流 +三浴三熏 三浴三熏 +三涂 三塗 +三点钟 三點鐘 +三熏三沐 三熏三沐 +三班制 三班制 +三瓦两舍 三瓦兩舍 +三瓦四舍 三瓦四舍 +三生有幸 三生有幸 +三用表 三用表 +三番四复 三番四復 +三百万 三百萬 +三百个 三百個 +三百千千 三百千千 +三百多万 三百多萬 +三百馀万 三百餘萬 +三秋 三秋 +三秒钟 三秒鐘 +三穗 三穗 +三穗县 三穗縣 +三级三审制 三級三審制 +三结合 三結合 +三统历 三統曆 +三统历史 三統歷史 +三胜制 三勝制 +三脚采茶戏 三腳採茶戲 +三舍 三舍 +三苏 三蘇 +三表 三表 +三角关系 三角關係 +三角套汇 三角套匯 +三角巾包扎法 三角巾包紮法 +三角板 三角板 +三角表 三角表 +三辟 三辟 +三连胜 三連勝 +三部合唱 三部合唱 +三部曲 三部曲 +三里屯 三里屯 +三里河 三里河 +三针 三針 +三长制 三長制 +三门干部 三門幹部 +三青团 三青團 +三面 三面 +三面亚当 三面亞當 +三面体 三面體 +三面夏娃 三面夏娃 +三面红旗 三面紅旗 +三面网 三面網 +三面角 三面角 +三马同槽 三馬同槽 +三鹿集团 三鹿集團 +上一个 上一個 +上万 上萬 +上上个月 上上個月 +上下五千年 上下五千年 +上下交困 上下交困 +上下同心 上下同心 +上下游 上下游 +上不了 上不了 +上不了台面 上不了檯面 +上不得台盘 上不得檯盤 +上个星期 上個星期 +上个月 上個月 +上中下游 上中下游 +上了 上了 +上了岁数 上了歲數 +上了年纪 上了年紀 +上党 上黨 +上党梆子 上黨梆子 +上冲 上衝 +上冲下洗 上沖下洗 +上千 上千 +上千万 上千萬 +上千人 上千人 +上升 上升 +上升为 上升爲 +上升趋势 上升趨勢 +上半叶 上半葉 +上发条 上發條 +上台 上臺 +上台演唱 上臺演唱 +上台演奏 上臺演奏 +上台演讲 上臺演講 +上叶 上葉 +上合屋 上閤屋 +上合组织 上合組織 +上吊 上吊 +上吊自杀 上吊自殺 +上同调 上同調 +上周 上週 +上回 上回 +上复 上覆 +上夸克 上夸克 +上层建筑 上層建築 +上市柜 上市櫃 +上弦 上弦 +上当 上當 +上彩 上彩 +上搜 上搜 +上杠 上槓 +上柜 上櫃 +上栗县 上栗縣 +上梁 上樑 +上梁不正 上樑不正 +上梁不正下梁歪 上樑不正下樑歪 +上梁山 上梁山 +上梁文 上梁文 +上游 上游 +上游工业 上游工業 +上百万 上百萬 +上确界 上確界 +上签 上籤 +上签写 上簽寫 +上签名 上簽名 +上签字 上簽字 +上签收 上簽收 +上舍 上舍 +上花台 上花臺 +上药 上藥 +上药膏 上藥膏 +上蜡 上蠟 +上表 上表 +上课钟 上課鐘 +上野树里 上野樹里 +上链 上鍊 +上面 上面 +上马杯 上馬杯 +下一个 下一個 +下三面 下三面 +下下个月 下下個月 +下不了 下不了 +下不了台 下不了臺 +下不来台 下不來臺 +下个星期 下個星期 +下个月 下個月 +下了 下了 +下于 下於 +下仑路 下崙路 +下冲 下衝 +下出 下出 +下划线 下劃線 +下厂 下廠 +下发 下發 +下台 下臺 +下台阶 下臺階 +下叶 下葉 +下同 下同 +下向 下向 +下周 下週 +下咽 下嚥 +下回 下回 +下城里 下城裏 +下夸克 下夸克 +下弦 下弦 +下彩 下彩 +下得了 下得了 +下手干 下手幹 +下才 下才 +下摆 下襬 +下有苏杭 下有蘇杭 +下板儿 下板兒 +下梁 下樑 +下水道系统 下水道系統 +下注 下注 +下注解 下註解 +下游 下游 +下游工业 下游工業 +下确界 下确界 +下种 下種 +下笔千言 下筆千言 +下签 下籤 +下签写 下簽寫 +下签名 下簽名 +下签字 下簽字 +下签收 下簽收 +下药 下藥 +下表 下表 +下课后 下課後 +下课钟 下課鐘 +下采 下采 +下里 下里 +下里巴人 下里巴人 +下面 下面 下麪 +下面请看 下面請看 +下风方向 下風方向 +下马杯 下馬杯 +不一致 不一致 +不一致字 不一致字 +不上台盘 不上檯盤 +不下于 不下於 +不丑 不醜 +不世出 不世出 +不中于款 不中於款 +不丰不杀 不豐不殺 +不为牛后 不爲牛後 +不为米折腰 不爲米折腰 +不乐于 不樂於 +不了 不了 +不了之局 不了之局 +不了了之 不了了之 +不了了当 不了了當 +不了情 不了情 +不了汉 不了漢 +不了解 不瞭解 +不以词害志 不以詞害志 +不以辞害志 不以辭害志 +不体面 不體面 +不作准 不作準 +不侮暗室 不侮暗室 +不修 不修 +不修小节 不修小節 +不修帷薄 不修帷薄 +不修边幅 不修邊幅 +不借 不借 +不值当 不值當 +不假外出 不假外出 +不偏极 不偏極 +不停当 不停當 +不光彩 不光彩 +不克 不克 +不克制 不剋制 +不克自制 不克自制 +不全症 不全症 +不再出版 不再出版 +不准 不準 不准 +不准他 不准他 +不准你 不准你 +不准备 不準備 +不准她 不准她 +不准它 不准它 +不准我 不准我 +不准没 不准沒 +不准确 不準確 +不准翻印 不准翻印 +不准许 不准許 +不准谁 不准誰 +不准问 不准問 +不减当年 不減當年 +不出 不出 +不出去 不出去 +不出所料 不出所料 +不出材 不出材 +不出来 不出來 +不分胜负 不分勝負 +不分胜败 不分勝敗 +不分青红皂白 不分青紅皁白 +不切合实际 不切合實際 +不划算 不划算 +不利于 不利於 +不前不后 不前不後 +不加修饰 不加修飾 +不加区别 不加區別 +不加自制 不加自制 +不劳无获 不勞無獲 +不劳而获 不勞而獲 +不升 不升 +不升反降 不升反降 +不单只是 不單只是 +不卜可知 不卜可知 +不占 不佔 +不占凶吉 不占凶吉 +不占卜 不占卜 +不占吉凶 不占吉凶 +不占算 不占算 +不发心 不發心 +不变价格 不變價格 +不只 不只 +不只是 不只是 +不可以道里计 不可以道里計 +不可同年而语 不可同年而語 +不可同日 不可同日 +不可同日而语 不可同日而語 +不可向迩 不可向邇 +不可当 不可當 +不可战胜 不可戰勝 +不可挽回 不可挽回 +不可救药 不可救藥 +不可胜书 不可勝書 +不可胜原 不可勝原 +不可胜数 不可勝數 +不可胜纪 不可勝紀 +不可胜言 不可勝言 +不可胜计 不可勝計 +不可胜记 不可勝記 +不吃烟火食 不吃煙火食 +不合 不合 +不合体统 不合體統 +不合作 不合作 +不合作运动 不合作運動 +不合式 不合式 +不合时宜 不合時宜 +不合标准 不合標準 +不合格 不合格 +不合法 不合法 +不合理 不合理 +不合算 不合算 +不合群 不合羣 +不合节 不合節 +不合规定 不合規定 +不合适 不合適 +不合逻辑 不合邏輯 +不吊 不弔 +不同 不同 +不同于 不同於 +不同人 不同人 +不同以往 不同以往 +不同凡响 不同凡響 +不同印本 不同印本 +不同处 不同處 +不同意 不同意 +不同日月 不同日月 +不同点 不同點 +不同调 不同調 +不向 不向 +不告而别 不告而別 +不周 不周 +不周到 不周到 +不周山 不周山 +不周延 不周延 +不周风 不周風 +不善于 不善於 +不图打点只图混水 不圖打點只圖混水 +不在了 不在了 +不堪回首 不堪回首 +不复 不復 +不复存在 不復存在 +不外借 不外借 +不多于 不多於 +不多几日 不多幾日 +不大合时 不大合時 +不大精采 不大精采 +不好了 不好了 +不好干涉 不好干涉 +不好干預 不好干預 +不好干预 不好干預 +不嫌母丑 不嫌母醜 +不孕症 不孕症 +不孝有三无后为大 不孝有三無後爲大 +不学亡术 不學亡術 +不学无术 不學無術 +不安于位 不安於位 +不安于室 不安於室 +不完全叶 不完全葉 +不寒而栗 不寒而慄 +不对腔板 不對腔板 +不小于 不小於 +不少于 不少於 +不尽 不盡 +不尽年 不盡年 +不尽心 不盡心 +不尽木 不盡木 +不尽根 不盡根 +不尽然 不盡然 +不尽相同 不盡相同 +不尽道理 不盡道理 +不屑于 不屑於 +不属于 不屬於 +不差毫厘 不差毫釐 +不差毫发 不差毫髮 +不干 不幹 不乾 +不干不净 不乾不淨 +不干不淨吃了没病 不乾不淨吃了沒病 +不干了 不幹了 +不干事 不幹事 +不干他 不干他 +不干休 不干休 +不干你 不干你 +不干净 不乾淨 +不干她 不干她 +不干它 不干它 +不干己事 不干己事 +不干性 不乾性 +不干我 不干我 +不干扰 不干擾 +不干杯 不乾杯 +不干涉 不干涉 +不干涉主义 不干涉主義 +不干渴 不乾渴 +不干犯 不干犯 +不干着急 不乾着急 +不干胶 不乾膠 +不干脆 不乾脆 +不干裂 不乾裂 +不干预 不干預 +不并 不併 +不幸 不幸 +不幸之事 不幸之事 +不幸之幸 不幸之幸 +不幸受害 不幸受害 +不幸的是 不幸的是 +不当 不當 +不当一回事 不當一回事 +不当不对 不當不對 +不当不正 不當不正 +不当事 不當事 +不当人 不當人 +不当人化化 不當人化化 +不当人子 不當人子 +不当党 不當黨 +不当党产 不當黨產 +不当准 不當準 +不当官 不當官 +不当家 不當家 +不当家化化 不當家化化 +不当家花拉 不當家花拉 +不当家花花 不當家花花 +不当家豁拉 不當家豁拉 +不当得利 不當得利 +不当数 不當數 +不当稳便 不當穩便 +不当紧 不當緊 +不当耍处 不當耍處 +不当道 不當道 +不形于色 不形於色 +不彩 不彩 +不徇颜面 不徇顏面 +不得了 不得了 +不得台盘 不得檯盤 +不得志 不得志 +不得闲 不得閒 +不念 不念 +不念旧恶 不念舊惡 +不恶而严 不惡而嚴 +不情不愿 不情不願 +不情愿 不情願 +不惮强御 不憚強禦 +不愿 不願 +不愿意 不願意 +不成才 不成才 +不战而胜 不戰而勝 +不才 不才 +不打不成才 不打不成才 +不托 不託 +不扣 不扣 +不折 不折 +不折不扣 不折不扣 +不挂 不掛 +不挂眼 不掛眼 +不放松 不放鬆 +不敢出声 不敢出聲 +不敢出气 不敢出氣 +不敢后人 不敢後人 +不敢当 不敢當 +不整合 不整合 +不斗 不鬥 +不断电系统 不斷電系統 +不早了 不早了 +不时之须 不時之須 +不明就里 不明就裏 +不明确 不明確 +不是个儿 不是個兒 +不是了处 不是了處 +不是别人 不是別人 +不松下 不鬆下 +不极 不極 +不极不反 不極不反 +不染纤尘 不染纖塵 +不标准 不標準 +不欲 不欲 +不欺暗室 不欺暗室 +不止于此 不止於此 +不正当 不正當 +不正当关系 不正當關係 +不正当竞争 不正當競爭 +不正确 不正確 +不死药 不死藥 +不毒不发 不毒不發 +不气干 不氣干 +不求收获 不求收獲 +不治之症 不治之症 +不注意 不注意 +不测风云 不測風雲 +不涂 不塗 +不消几日 不消幾日 +不甘于 不甘於 +不甘后人 不甘後人 +不甚了了 不甚了了 +不甚合意 不甚合意 +不用干 不用幹 +不用斗了 不用鬥了 +不畏强御 不畏強禦 +不畏彊御 不畏彊禦 +不留情面 不留情面 +不相同 不相同 +不相干 不相干 +不知凡几 不知凡幾 +不知去向 不知去向 +不知所云 不知所云 +不确 不確 +不确定 不確定 +不确定性 不確定性 +不确定性原理 不確定性原理 +不确定感 不確定感 +不确定故意 不確定故意 +不确实 不確實 +不符合 不符合 +不等于 不等於 +不等价交换 不等價交換 +不管闲事 不管閒事 +不系 不繫 +不系舟 不繫舟 +不约而合 不約而合 +不约而同 不約而同 +不织布 不織布 +不结汇进口 不結匯進口 +不绝于耳 不絕於耳 +不老药 不老藥 +不肯干休 不肯干休 +不育症 不育症 +不胜 不勝 +不胜之态 不勝之態 +不胜其扰 不勝其擾 +不胜其烦 不勝其煩 +不胜其苦 不勝其苦 +不胜唏嘘 不勝唏噓 +不胜感喟 不勝感喟 +不胜感激 不勝感激 +不胜春 不勝春 +不胜枚举 不勝枚舉 +不胜衣 不勝衣 +不胜负荷 不勝負荷 +不胜酒力 不勝酒力 +不能出口 不能出口 +不能尽数 不能盡數 +不能胜数 不能勝數 +不能自制 不能自制 +不能赞一辞 不能贊一辭 +不至于 不至於 +不致 不致 +不致于 不致於 +不舍 不捨 +不舍得 不捨得 +不舍昼夜 不捨晝夜 +不舒适 不舒適 +不良于行 不良於行 +不良倾向 不良傾向 +不良才 不良才 +不良适应 不良適應 +不药而愈 不藥而癒 +不药而癒 不藥而癒 +不获 不獲 +不落人后 不落人後 +不虚发 不虛發 +不行了 不行了 +不表 不表 +不见世面 不見世面 +不见了 不見了 +不见识面 不見識面 +不见面 不見面 +不规范 不規範 +不解之仇 不解之仇 +不讨采 不討采 +不让须眉 不讓鬚眉 +不讲情面 不講情面 +不识局面 不識局面 +不识闲 不識閒 +不说价 不說價 +不说出 不說出 +不谈别的 不談別的 +不谋而合 不謀而合 +不谋而同 不謀而同 +不谐当 不諧當 +不谷 不穀 +不负所托 不負所托 +不赞 不讚 +不赞一词 不讚一詞 +不赞一辞 不讚一辭 +不赞同 不贊同 +不赞成 不贊成 +不足为据 不足爲據 +不足之症 不足之症 +不足回旋 不足回旋 +不足挂齿 不足掛齒 +不足采信 不足採信 +不辞而别 不辭而別 +不远万里 不遠萬里 +不远千里 不遠千里 +不连续面 不連續面 +不适 不適 +不适合 不適合 +不适当 不適當 +不适感 不適感 +不适用 不適用 +不逊于 不遜於 +不透明水彩画 不透明水彩畫 +不通吊庆 不通弔慶 +不遗余力 不遺餘力 +不避彊御 不避彊禦 +不采 不採 +不采声 不采聲 +不锈钢 不鏽鋼 +不锈钢板 不鏽鋼板 +不问前因后果 不問前因後果 +不问是非曲直 不問是非曲直 +不降反升 不降反升 +不限于 不限於 +不须 不須 +不顾前后 不顧前後 +不顾后果 不顧後果 +不顾曲直 不顧曲直 +不顾闲野 不顧閒野 +不食人间烟火 不食人間煙火 +不食周粟 不食周粟 +不食干腊 不食乾腊 +不食烟火 不食煙火 +不龟手药 不龜手藥 +不龟药 不龜藥 +与他一斗 與他一鬥 +与众不同 與衆不同 +与克制 與剋制 +与党 與黨 +与君一席话胜读十年书 與君一席話勝讀十年書 +与国同休 與國同休 +与她一斗 與她一鬥 +与子同袍 與子同袍 +与日同辉 與日同輝 +与此同时 與此同時 +与民同乐 與民同樂 +与民同忧 與民同憂 +丑三 丑三 +丑丑 醜醜 +丑丫头 醜丫頭 +丑事 醜事 +丑于 醜於 +丑人 醜人 +丑人多作怪 醜人多作怪 +丑侪 醜儕 +丑八怪 醜八怪 +丑剌剌 醜剌剌 +丑剧 醜劇 +丑化 醜化 +丑史 醜史 +丑名 醜名 +丑咤 醜吒 +丑地 醜地 +丑声 醜聲 +丑声四溢 醜聲四溢 +丑声远播 醜聲遠播 +丑头怪脸 醜頭怪臉 +丑夷 醜夷 +丑女 醜女 +丑女人 醜女人 +丑女效颦 醜女效顰 +丑奴儿 醜奴兒 +丑妇 醜婦 +丑媳 醜媳 +丑媳妇 醜媳婦 +丑媳妇总得要见公婆 醜媳婦總得要見公婆 +丑小鸭 醜小鴨 +丑巴怪 醜巴怪 +丑年 丑年 +丑徒 醜徒 +丑态 醜態 +丑态毕露 醜態畢露 +丑态百出 醜態百出 +丑怪 醜怪 +丑恶 醜惡 +丑日 丑日 +丑旦 丑旦 +丑时 丑時 +丑月 丑月 +丑末 醜末 +丑杂 醜雜 +丑样 醜樣 +丑死 醜死 +丑比 醜比 +丑毙了 醜斃了 +丑沮 醜沮 +丑牛 丑牛 +丑生 醜生 +丑男 醜男 +丑相 醜相 +丑类 醜類 +丑类恶物 醜類惡物 +丑脸 醜臉 +丑虏 醜虜 +丑行 醜行 +丑角 丑角 +丑言 醜言 +丑诋 醜詆 +丑话 醜話 +丑语 醜語 +丑贼生 醜賊生 +丑辞 醜辭 +丑辱 醜辱 +丑逆 醜逆 +丑闻 醜聞 +丑闻案 醜聞案 +丑陋 醜陋 +专业人才 專業人才 +专修 專修 +专修班 專修班 +专修科 專修科 +专修科目 專修科目 +专利药 專利藥 +专利药品 專利藥品 +专制 專制 +专制主义 專制主義 +专制制度 專制制度 +专制君主制 專制君主制 +专制政体 專制政體 +专制政府 專制政府 +专制政治 專制政治 +专制权 專制權 +专制起来 專制起來 +专勤制度 專勤制度 +专向 專向 +专家系统 專家系統 +专家评价 專家評價 +专干 專幹 +专征 專征 +专心一志 專心一志 +专心致志 專心致志 +专才 專才 +专摆 專擺 +专柜 專櫃 +专柜小姐 專櫃小姐 +专欲难成 專欲難成 +专注 專注 +专注力 專注力 +专精于 專精於 +专美于前 專美於前 +专辑里 專輯裏 +专鉴 專鑒 +专门人才 專門人才 +专门术语 專門術語 +且于 且於 +且听下回分解 且聽下回分解 +世上无难事只怕有心人 世上無難事只怕有心人 +世仇 世仇 +世出世 世出世 +世台 世臺 +世台会 世臺會 +世彩堂 世綵堂 +世彩堂帖 世綵堂帖 +世恩录 世恩錄 +世情看冷暖人面逐高低 世情看冷暖人面逐高低 +世所周知 世所周知 +世田谷 世田谷 +世界大同 世界大同 +世界旅游组织 世界旅遊組織 +世界杯 世界盃 +世界杯室 世界盃室 +世界杯赛 世界盃賽 +世界纪录 世界紀錄 +世界范围 世界範圍 +世界里 世界裏 +世系 世系 +世纪钟 世紀鐘 +世纪钟表 世紀鐘錶 +世胄 世胄 +世表 世表 +世阿弥 世阿彌 +世面 世面 +丘克 丘克 +丙种 丙種 +丙种射线 丙種射線 +丙舍 丙舍 +业余 業餘 +业余大学 業餘大學 +业余教育 業餘教育 +业余爱好 業餘愛好 +业余者 業餘者 +业务范围 業務範圍 +业界标准 業界標準 +业种 業種 +业精于勤 業精於勤 +业荒于嬉 業荒於嬉 +业馀电台 業餘電臺 +丛台区 叢臺區 +丛生叶 叢生葉 +东丰 東豐 +东丰县 東豐縣 +东丰阁 東豐閣 +东京柜 東京櫃 +东仓里 東倉里 +东伙 東夥 +东冲西突 東衝西突 +东加里曼丹 東加里曼丹 +东北向 東北向 +东北面 東北面 +东升 東昇 +东南向 東南向 +东南面 東南面 +东厂 東廠 +东台 東臺 +东台市 東臺市 +东台湾 東臺灣 +东向 東向 +东周 東周 +东周时 東周時 +东周时代 東周時代 +东周时期 東周時期 +东周钟 東周鐘 +东奔西向 東奔西向 +东学党 東學黨 +东山之志 東山之志 +东山里 東山里 +东山里站 東山里站 +东岳 東嶽 +东干 東干 +东征 東征 +东征西怨 東征西怨 +东征西讨 東征西討 +东扬西荡 東揚西蕩 +东折西绕 東折西繞 +东挨西撞 東挨西撞 +东挪西借 東挪西借 +东摇西摆 東搖西擺 +东方发白 東方發白 +东方汇理 東方匯理 +东林党 東林黨 +东欧集团 東歐集團 +东海捞针 東海撈針 +东涂西抹 東塗西抹 +东淨里的砖儿 東淨裏的磚兒 +东游 東遊 +东窗事发 東窗事發 +东胜区 東勝區 +东胡 東胡 +东芝医疗系 東芝醫療繫 +东荡西除 東蕩西除 +东蒙 東蒙 +东西向 東西向 +东西周 東西周 +东讨西征 東討西征 +东谷拉瓦 東谷拉瓦 +东邻西舍 東鄰西舍 +东里 東里 +东量西折 東量西折 +东面 東面 +东飘西荡 東飄西蕩 +丝发 絲髮 +丝发之功 絲髮之功 +丝布 絲布 +丝弦 絲絃 +丝恩发怨 絲恩髮怨 +丝托索 絲托索 +丝挂子 絲掛子 +丝杆 絲桿 +丝杠 絲槓 +丝来线去 絲來線去 +丝板 絲板 +丝瓜布 絲瓜布 +丝盘虫 絲盤蟲 +丝线 絲線 +丝织厂 絲織廠 +丝绒布 絲絨布 +丝虫 絲蟲 +丝虫病 絲蟲病 +丢丑 丟醜 +丢了 丟了 +丢体面 丟體面 +丢入爪哇国里 丟入爪哇國裏 +丢出 丟出 +丢出去 丟出去 +丢包术 丟包術 +丢在脑后 丟在腦後 +丢巧针 丟巧針 +丢面子 丟面子 +两万 兩萬 +两个 兩個 +两个中国 兩個中國 +两个或两个以上 兩個或兩個以上 +两个或更多 兩個或更多 +两个肩膀扛张嘴 兩個肩膀扛張嘴 +两人同心 兩人同心 +两余 兩餘 +两克 兩克 +两党 兩黨 +两千 兩千 +两厢情愿 兩廂情願 +两只 兩隻 +两只手 兩隻手 +两只脚赶不上一张嘴 兩隻腳趕不上一張嘴 +两台 兩臺 +两叶 兩葉 +两叶掩目 兩葉掩目 +两合公司 兩合公司 +两周 兩週 +两周年 兩週年 +两回 兩回 +两回事 兩回事 +两国关系 兩國關係 +两天后 兩天後 +两天晒网 兩天曬網 +两头三面 兩頭三面 +两头白面 兩頭白面 +两娘女 兩娘女 +两岸关系 兩岸關係 +两当 兩當 +两当一 兩當一 +两当县 兩當縣 +两性关系 兩性關係 +两情两愿 兩情兩願 +两扎 兩紮 +两撇胡 兩撇鬍 +两方面 兩方面 +两杆 兩杆 +两杠 兩槓 +两杯 兩杯 +两板 兩板 +两极 兩極 +两极分化 兩極分化 +两极化 兩極化 +两极管 兩極管 +两极观点 兩極觀點 +两段制 兩段制 +两点钟 兩點鐘 +两班制 兩班制 +两百万 兩百萬 +两百个 兩百個 +两百多万 兩百多萬 +两相情愿 兩相情願 +两眼发直 兩眼發直 +两种 兩種 +两种人 兩種人 +两秒钟 兩秒鐘 +两腿发软 兩腿發軟 +两虎共斗 兩虎共鬥 +两虎相斗 兩虎相鬥 +两西西里王国 兩西西里王國 +两院制 兩院制 +两面 兩面 +两面三刀 兩面三刀 +两面不是人 兩面不是人 +两面不讨好 兩面不討好 +两面二舌 兩面二舌 +两面作战 兩面作戰 +两面倒 兩面倒 +两面光 兩面光 +两面刀 兩面刀 +两面夹攻 兩面夾攻 +两面性 兩面性 +两面手法 兩面手法 +两面派 兩面派 +两面讨好 兩面討好 +两面转圜 兩面轉圜 +两面锯 兩面鋸 +两面顾全 兩面顧全 +两鼠斗穴 兩鼠鬥穴 +严丝合缝 嚴絲合縫 +严了眼儿 嚴了眼兒 +严于 嚴於 +严于律己 嚴於律己 +严云农 嚴云農 +严冬 嚴冬 +严制 嚴制 +严加防范 嚴加防範 +严复 嚴復 +严恶 嚴惡 +严禁吸烟 嚴禁吸菸 +严禁烟火 嚴禁煙火 +严重后果 嚴重後果 +丧失殆尽 喪失殆盡 +丧尸 喪屍 +丧尽 喪盡 +丧尽天良 喪盡天良 +丧志 喪志 +丧荡游魂 喪蕩游魂 +丧钟 喪鐘 +个个 個個 +个个称羡 個個稱羨 +个中 箇中 +个中三昧 箇中三昧 +个中人 箇中人 +个中原因 箇中原因 +个中奥妙 箇中奧妙 +个中奥秘 箇中奧祕 +个中好手 箇中好手 +个中强手 箇中強手 +个中消息 箇中消息 +个中滋味 箇中滋味 +个中玄机 箇中玄機 +个中理由 箇中理由 +个中讯息 箇中訊息 +个中资讯 箇中資訊 +个中道理 箇中道理 +个中高手 箇中高手 +个事 個事 +个人 個人 +个人主义 個人主義 +个人伤害 個人傷害 +个人储蓄 個人儲蓄 +个人利益 個人利益 +个人化 個人化 +个人单季 個人單季 +个人卫生 個人衛生 +个人外交 個人外交 +个人奖 個人獎 +个人崇拜 個人崇拜 +个人工作室 個人工作室 +个人得失 個人得失 +个人性 個人性 +个人所得 個人所得 +个人所得税 個人所得稅 +个人护理 個人護理 +个人拨接 個人撥接 +个人本位 個人本位 +个人消费 個人消費 +个人用 個人用 +个人用户 個人用戶 +个人电脑 個人電腦 +个人组 個人組 +个人网站 個人網站 +个人资料 個人資料 +个人赛 個人賽 +个人隐私 個人隱私 +个人风 個人風 +个人首页 個人首頁 +个位 個位 +个位数 個位數 +个体 個體 +个体发育 個體發育 +个体户 個體戶 +个体所有 個體所有 +个体经济 個體經濟 +个例 個例 +个儿 個兒 +个别 個別 +个别差异 個別差異 +个别性 個別性 +个别情况 個別情況 +个别指导 個別指導 +个别教学 個別教學 +个别测验 個別測驗 +个别现象 個別現象 +个别生产 個別生產 +个别谈话 個別談話 +个别辅导 個別輔導 +个头 個頭 +个头儿 個頭兒 +个子 個子 +个屁阿 個屁阿 +个展 個展 +个性 個性 +个性化 個性化 +个性难改 個性難改 +个把 個把 +个把月 個把月 +个数 個數 +个旧 箇舊 +个旧县 箇舊縣 +个旧市 箇舊市 +个样 個樣 +个核 個核 +个案 個案 +个案分析 個案分析 +个案研究 個案研究 +个案纪录 個案紀錄 +个股为 個股爲 +个过客 個過客 +个钟 個鐘 +个钟表 個鐘錶 +中上游 中上游 +中下游 中下游 +中书舍人 中書舍人 +中了暑 中了暑 +中了标 中了標 +中了毒 中了毒 +中于 中於 +中云 中雲 +中介社团 中介社團 +中仑 中崙 +中仑站 中崙站 +中价位 中價位 +中低价位 中低價位 +中俄关系 中俄關系 +中千世界 中千世界 +中华台北 中華臺北 +中华电视台 中華電視臺 +中华肝吸虫病 中華肝吸蟲病 +中华苏维埃共和国 中華蘇維埃共和國 +中华革命党 中華革命黨 +中原板荡 中原板蕩 +中原标准时间 中原標準時間 +中台 中臺 中颱 +中台医专 中臺醫專 +中台湾 中臺灣 +中台禅寺 中臺禪寺 +中台科技大学 中臺科技大學 +中叶 中葉 +中国共产党 中國共產黨 +中国制 中國製 +中国制造 中國製造 +中国剩余定理 中國剩餘定理 +中国医药 中國醫藥 +中国历史 中國歷史 +中国同盟会 中國同盟會 +中国国民党 中國國民黨 +中型钟 中型鐘 +中型钟表 中型鐘錶 +中型钟表面 中型鐘表面 +中型钟面 中型鐘面 +中央专制集权 中央專制集權 +中央党 中央黨 +中央党部 中央黨部 +中央台 中央臺 +中央广播电台 中央廣播電臺 +中央标准局 中央標準局 +中央汇金 中央匯金 +中央电视台 中央電視臺 +中央监控系统 中央監控系統 +中央空调系统 中央空調系統 +中央面 中央面 +中子俘获 中子俘獲 +中岳 中嶽 +中度台风 中度颱風 +中彩 中彩 +中心汇率 中心匯率 +中恶 中惡 +中懑之症 中懣之症 +中才 中才 +中控台 中控臺 +中控面板 中控面板 +中搜 中搜 +中文台 中文臺 +中文系 中文系 +中文里 中文裏 +中日关系 中日關係 +中暗箭 中暗箭 +中板 中板 +中极 中極 +中枢系统 中樞系統 +中核 中核 +中止症 中止症 +中比关系 中比關係 +中注模样 中注模樣 +中涂 中塗 +中港台 中港臺 +中游 中游 +中环杯 中環盃 +中盘胜 中盤勝 +中秋 中秋 +中秋佳节 中秋佳節 +中秋帖 中秋帖 +中秋月饼 中秋月餅 +中秋节 中秋節 +中程计划 中程計劃 +中筋面粉 中筋麪粉 +中签 中籤 +中美发表 中美發表 +中胡 中胡 +中草药 中草藥 +中药 中藥 +中药偏方 中藥偏方 +中药学 中藥學 +中药店 中藥店 +中药房 中藥房 +中药材 中藥材 +中药铺 中藥鋪 +中表 中表 +中西合并 中西合併 +中西合璧 中西合璧 +中谷 中谷 +中谷有蓷 中谷有蓷 +中转柜台 中轉櫃檯 +中远集团 中遠集團 +中远香港集团 中遠香港集團 +中间纤维 中間纖維 +中风后 中風後 +丰上锐下 豐上銳下 +丰下 豐下 +丰丘明 豐丘明 +丰业 豐業 +丰乐 豐樂 +丰乐亭 豐樂亭 +丰产 豐產 +丰产田 豐產田 +丰亨豫大 豐亨豫大 +丰仪 丰儀 +丰伟 豐偉 +丰俭由人 豐儉由人 +丰儀 丰儀 +丰功 豐功 +丰功伟业 豐功偉業 +丰功伟绩 豐功偉績 +丰功厚利 豐功厚利 +丰功大业 豐功大業 +丰功懋烈 豐功懋烈 +丰功盛烈 豐功盛烈 +丰华 豐華 +丰南 豐南 +丰南区 豐南區 +丰厚 豐厚 +丰原 豐原 +丰原市 豐原市 +丰县 豐縣 +丰取刻与 豐取刻與 +丰台 豐臺 +丰台区 豐臺區 +丰城 豐城 +丰城剑气 豐城劍氣 +丰城市 豐城市 +丰城贯斗 豐城貫斗 +丰壤 豐壤 +丰奢 豐奢 +丰妍 豐妍 +丰姿 丰姿 +丰姿冶丽 丰姿冶麗 +丰姿绰约 丰姿綽約 +丰宁 豐寧 +丰宁县 豐寧縣 +丰实 豐實 +丰容 丰容 +丰容靓饰 豐容靚飾 +丰富 豐富 +丰富多彩 豐富多彩 +丰富性 豐富性 +丰川悦司 豐川悅司 +丰年 豐年 +丰年玉 豐年玉 +丰年祭 豐年祭 +丰年稔岁 豐年稔歲 +丰年节 豐年節 +丰年虾 豐年蝦 +丰度 丰度 豐度 +丰悴 豐悴 +丰情 丰情 +丰收 豐收 +丰收年 豐收年 +丰本 豐本 +丰杀 豐殺 +丰标 丰標 +丰标不凡 丰標不凡 +丰歉 豐歉 +丰殖 豐殖 +丰水 豐水 +丰水期 豐水期 +丰沛 豐沛 +丰注 豐注 +丰泽 豐澤 +丰泽区 豐澤區 +丰润 豐潤 +丰润区 豐潤區 +丰渔桥 豐漁橋 +丰溪里 豐溪里 +丰满 豐滿 +丰满区 豐滿區 +丰滨 豐濱 +丰滨乡 豐濱鄉 +丰炽 豐熾 +丰熟 豐熟 +丰田 豐田 +丰登 豐登 +丰盈 豐盈 +丰盛 豐盛 +丰硕 豐碩 +丰碑 豐碑 +丰神 丰神 +丰神俊美 丰神俊美 +丰神异彩 豐神異彩 +丰神绰约 丰神綽約 +丰神飘洒 丰神飄灑 +丰稔 豐稔 +丰穰 豐穰 +丰美 豐美 +丰耗 豐耗 +丰肌 豐肌 +丰胸 豐胸 +丰腆 豐腆 +丰腴 豐腴 +丰臣秀吉 豐臣秀吉 +丰若晖 豐若暉 +丰若有肌柔若无骨 丰若有肌柔若無骨 +丰茂 豐茂 +丰茸 丰茸 +丰草 豐草 +丰蔚 豐蔚 +丰衍 豐衍 +丰衣足食 豐衣足食 +丰裕 豐裕 +丰赡 豐贍 +丰足 豐足 +丰都 豐都 +丰都县 豐都縣 +丰采 丰采 +丰镇 豐鎮 +丰镇市 豐鎮市 +丰镐 豐鎬 +丰隆 豐隆 +丰韵 丰韻 +丰韻 丰韻 +丰顺 豐順 +丰顺县 豐順縣 +丰颊 豐頰 +丰饶 豐饒 +丰饶丰足 豐饒豐足 +串亲戚 串親戚 +串出来 串出來 +串同 串同 +串哄 串哄 +串游 串游 +临别 臨別 +临别依依 臨別依依 +临别时 臨別時 +临别赠言 臨別贈言 +临制 臨制 +临危致命 臨危致命 +临去秋波 臨去秋波 +临夏回族自治州 臨夏回族自治州 +临幸 臨幸 +临床特征 臨牀特徵 +临机制变 臨機制變 +临机制胜 臨機制勝 +临江折轴 臨江折軸 +临海水土志 臨海水土誌 +临潼斗宝 臨潼鬥寶 +丸药 丸藥 +丸药盒 丸藥盒 +丹参 丹蔘 +丹台 丹臺 +丹布朗 丹布朗 +丹干 丹干 +丹徒布衣 丹徒布衣 +丹朱 丹朱 +丹药 丹藥 +为中台 爲中颱 +为了 爲了 +为人师表 爲人師表 +为准 爲準 +为念 爲念 +为恶不悛 爲惡不悛 +为所欲为 爲所欲爲 +为模范 爲模範 +为着 爲着 +为规范 爲規範 +为鉴 爲鑑 +主仆 主僕 +主修 主修 +主修科目 主修科目 +主席台 主席臺 +主席团 主席團 +主干 主幹 +主干家庭 主幹家庭 +主干线 主幹線 +主干网络 主幹網絡 +主干网路 主幹網路 +主控台 主控臺 +主播台 主播臺 +主教团 主教團 +主普坛 主普壇 +主机板 主機板 +主板 主板 +主梁 主樑 +主法向量 主法向量 +主画面 主畫面 +主要树种 主要樹種 +主钟差 主鐘差 +主钟曲线 主鐘曲線 +主题曲 主題曲 +主题论坛区 主題論壇區 +丽于 麗於 +丽舍 麗舍 +举不胜举 舉不勝舉 +举了 舉了 +举例发凡 舉例發凡 +举出 舉出 +举出来 舉出來 +举发 舉發 +举尾虫 舉尾蟲 +举手可采 舉手可采 +举手表 舉手表 +举手表决 舉手表決 +举措失当 舉措失當 +举杯 舉杯 舉盃 +举杯畅饮 舉杯暢飲 +举止闲冶 舉止閒冶 +举目千里 舉目千里 +举荐征辟 舉薦征辟 +乃至于 乃至於 +乃里 乃里 +久仰山斗 久仰山斗 +久别 久別 +久别相逢 久別相逢 +久别重逢 久別重逢 +久违颜范 久違顏範 +么娘 麼娘 +义义合合 義義合合 +义仆 義僕 +义占 義佔 +义同生死 義同生死 +义和团 義和團 +义大利杯 義大利杯 +义大利面 義大利麪 +义庄 義莊 +义形于色 義形於色 +义气干霄 義氣干霄 +义薄云天 義薄雲天 +之于 之於 +之余 之餘 +之八九只 之八九只 +之叹 之嘆 +之后 之後 +之征 之徵 +之念 之念 +之托 之託 +之杯 之杯 +之核 之核 +之欲 之慾 +之游 之遊 +之秋 之秋 +之鉴 之鑑 +之钟 之鐘 +乌东察克 烏東察克 +乌东査克 烏東查克 +乌云 烏雲 +乌云密布 烏雲密佈 +乌云蔽日 烏雲蔽日 +乌什塔拉回族乡 烏什塔拉回族鄉 +乌克丽丽 烏克麗麗 +乌克兰 烏克蘭 +乌克兰人 烏克蘭人 +乌克兰籍 烏克蘭籍 +乌克兰队 烏克蘭隊 +乌兰察布 烏蘭察布 +乌兰察布市 烏蘭察布市 +乌兰巴托 烏蘭巴托 +乌兹冲锋枪 烏茲衝鋒槍 +乌兹别克 烏茲別克 +乌兹别克人 烏茲別克人 +乌兹别克共和国 烏茲別克共和國 +乌兹别克斯坦 烏茲別克斯坦 +乌兹别克族 烏茲別克族 +乌冬面 烏冬面 +乌发 烏髮 +乌合 烏合 +乌合之众 烏合之衆 +乌合之卒 烏合之卒 +乌孜别克 烏孜別克 +乌孜别克族 烏孜別克族 +乌孜别克语 烏孜別克語 +乌干达 烏干達 +乌干达共和国 烏干達共和國 +乌当 烏當 +乌当区 烏當區 +乌托邦 烏托邦 +乌拉特后旗 烏拉特後旗 +乌松 烏松 +乌梁海 烏梁海 +乌梁海盆地 烏梁海盆地 +乌沈沈 烏沈沈 +乌洛托品 烏洛托品 +乌涂 烏塗 +乌涂水 烏塗水 +乌滋别克 烏滋別克 +乌滋别克斯坦 烏滋別克斯坦 +乌烟瘴气 烏煙瘴氣 +乌狗吃食白狗当灾 烏狗吃食白狗當災 +乌苏 烏蘇 +乌苏市 烏蘇市 +乌苏拉 烏蘇拉 +乌苏里斯克 烏蘇里斯克 +乌苏里江 烏蘇里江 +乌药 烏藥 +乌获 烏獲 +乌菲兹美术馆 烏菲茲美術館 +乌里 烏里 +乌里雅苏台 烏里雅蘇臺 +乌鲁克恰提 烏魯克恰提 +乌鲁克恰提县 烏魯克恰提縣 +乌鲁克穆河 烏魯克穆河 +乌龙面 烏龍麪 +乏困 乏困 +乐不可极 樂不可極 +乐于 樂於 +乐于助人 樂於助人 +乐合彩 樂合彩 +乐器钟 樂器鐘 +乐团 樂團 +乐坏了 樂壞了 +乐坛 樂壇 +乐府杂录 樂府雜錄 +乐意于 樂意於 +乐曲 樂曲 +乐极则悲 樂極則悲 +乐极忘形 樂極忘形 +乐极悲来 樂極悲來 +乐极悲生 樂極悲生 +乐极生悲 樂極生悲 +乐游原 樂遊原 +乐祸幸灾 樂禍幸災 +乐透彩 樂透彩 +乐颠了馅 樂顛了餡 +乒乓球台 乒乓球檯 +乔修亚 喬修亞 +乔修亚.吉尔菲艾斯 喬修亞.吉爾菲艾斯 +乔做胡为 喬做胡爲 +乔岳 喬嶽 +乔布斯 喬布斯 +乔德里 喬德里 +乔志 喬志 +乔才 喬才 +乔杰立 喬傑立 +乔松 喬松 +乔松之寿 喬松之壽 +乔治克隆尼 喬治克隆尼 +乔治克鲁尼 喬治克魯尼 +乔治.布希 喬治.布希 +乖别 乖別 +乘了 乘了 +乘云 乘雲 +乘凶完配 乘凶完配 +乘奔御风 乘奔御風 +乘法表 乘法表 +乘胜 乘勝 +乘胜追击 乘勝追擊 +乘胜逐北 乘勝逐北 +乘鹤驾云 乘鶴駕雲 +乙丑 乙丑 +乙种 乙種 +乙种射线 乙種射線 +乙种粒子 乙種粒子 +乙种维生素 乙種維生素 +九世之仇 九世之仇 +九个 九個 +九个人 九個人 +九个月 九個月 +九九乘法表 九九乘法表 +九九表 九九表 +九亿五千万 九億五千萬 +九余 九餘 +九冬 九冬 +九出祁山 九出祁山 +九分钟 九分鐘 +九划 九劃 +九千点 九千點 +九只 九隻 +九台 九臺 +九台市 九臺市 +九合 九合 +九合一匡 九合一匡 +九吊六的 九吊六的 +九回肠 九迴腸 +九天后 九天後 +九宵云 九宵雲 +九宵云外 九宵雲外 +九扎 九紮 +九折 九折 +九折优待 九折優待 +九折臂 九折臂 +九曲 九曲 +九曲桥 九曲橋 +九曲洞 九曲洞 +九杯 九杯 +九点钟 九點鐘 +九炼成钢 九鍊成鋼 +九百万 九百萬 +九百个 九百個 +九秋 九秋 +九秒钟 九秒鐘 +九章算术 九章算術 +九谷 九穀 +九里 九里 +九里余 九里餘 +九里区 九里區 +九针 九針 +九零后 九零後 +九霄云外 九霄雲外 +九面体 九面體 +九香虫 九香蟲 +九龙杯 九龍杯 +九龙表行 九龍表行 +乞力马扎罗山 乞力馬扎羅山 +乞留恶滥 乞留惡濫 +乞留曲律 乞留曲律 +也不尽然 也不盡然 +也克制 也剋制 +也向 也向 +也对于 也對於 +也念 也念 +也斗了胆 也斗了膽 +也舍下 也舍下 +也须 也須 +习于 習於 +习惯于 習慣於 +习非胜是 習非勝是 +乡党 鄉黨 +乡党尚齿 鄉黨尚齒 +乡团 鄉團 +乡土艺术 鄉土藝術 +乡愿 鄉愿 +乡曲 鄉曲 +乡曲之誉 鄉曲之譽 +乡民代表 鄉民代表 +乡民代表会 鄉民代表會 +乡里 鄉里 +乡里小人 鄉里小人 +乡镇代表 鄉鎮代表 +乡镇民代表 鄉鎮民代表 +乡间别墅 鄉間別墅 +乡面 鄉面 +书不尽 書不盡 +书不尽言 書不盡言 +书中自有千钟粟 書中自有千鍾粟 +书写不能症 書寫不能症 +书刊杂志 書刊雜誌 +书卷 書卷 +书卷奖 書卷獎 +书卷气 書卷氣 +书台 書檯 +书后 書後 +书呆子 書呆子 +书坛 書壇 +书录 書錄 +书报杂志 書報雜誌 +书柜 書櫃 +书种 書種 +书签 書籤 +书系 書系 +书虫 書蟲 +书虫子 書蟲子 +书面 書面 +书面上 書面上 +书面报告 書面報告 +书面纸 書面紙 +书面许可 書面許可 +书面语 書面語 +书面资料 書面資料 +乩坛 乩壇 +买一个饶一个 買一個饒一個 +买了 買了 +买价 買價 +买凶 買兇 +买咸鱼放生 買鹹魚放生 +买回 買回 +买回去 買回去 +买回家 買回家 +买回来 買回來 +买大彩 買大彩 +买汇 買匯 +买烟 買菸 +买物历 買物歷 +买臣复水 買臣覆水 +买进对冲 買進對沖 +买闲钱 買閒錢 +买面子 買面子 +买风云雷雨 買風雲雷雨 +乱世凶年 亂世凶年 +乱了 亂了 +乱了手脚 亂了手腳 +乱作一团 亂作一團 +乱党 亂黨 +乱冲 亂衝 +乱发 亂髮 +乱哄 亂鬨 +乱哄不过来 亂鬨不過來 +乱哄哄 亂哄哄 +乱成一团 亂成一團 +乱扣 亂扣 +乱搞男女关系 亂搞男女關係 +乱松松 亂鬆鬆 +乱极则平 亂極則平 +乱棒胡敲 亂棒胡敲 +乱涂 亂塗 +乱针绣 亂針繡 +乱集团 亂集團 +乱首垢面 亂首垢面 +乳制品 乳製品 +乳娘 乳孃 +乳液聚合 乳液聚合 +乳糖不耐症 乳糖不耐症 +乳臭未干 乳臭未乾 +乳药求死 乳藥求死 +乾元 乾元 +乾兌 乾兌 +乾卦 乾卦 +乾坤一掷 乾坤一擲 +乾坤再造 乾坤再造 +乾坤大挪移 乾坤大挪移 +乾尽午中 乾盡午中 +乾象历 乾象曆 +乾隆 乾隆 +乾隆年间 乾隆年間 +乾隆皇帝 乾隆皇帝 +了不得 了不得 +了不成 了不成 +了不起 了不起 +了了 了了 +了事 了事 +了事环 了事環 +了债 了債 +了儿 了兒 +了决 了決 +了劣 了劣 +了却 了卻 +了却此生 了卻此生 +了去 了去 +了如 瞭如 +了如指掌 瞭如指掌 +了局 了局 +了帐 了帳 +了当 了當 +了当不得 了當不得 +了得 了得 +了得了 了得了 +了悟 了悟 +了愿 了願 +了手 了手 +了收 了收 +了断 了斷 +了无 了無 +了无挂碍 了無掛礙 +了无新意 了無新意 +了无牵挂 了無牽掛 +了无生机 了無生機 +了无生趣 了無生趣 +了无痕迹 了無痕跡 +了无罣碍 了無罣礙 +了无长进 了無長進 +了望 瞭望 +了望台 瞭望臺 +了望塔 瞭望塔 +了望山 瞭望山 +了望所 瞭望所 +了案 了案 +了此残生 了此殘生 +了毕 了畢 +了清 了清 +了然 瞭然 +了然不惑 瞭然不惑 +了然于心 瞭然於心 +了纳 了納 +了结 了結 +了若指掌 瞭若指掌 +了落 了落 +了解 瞭解 +了解到 瞭解到 +了讫 了訖 +了话 了話 +了语 了語 +了账 了賬 +了身达命 了身達命 +了鸟 了鳥 +争了 爭了 +争先发言 爭先發言 +争先后 爭先後 +争先恐后 爭先恐後 +争出 爭出 +争奇斗妍 爭奇鬥妍 +争奇斗异 爭奇鬥異 +争奇斗艳 爭奇鬥豔 +争妍斗奇 爭妍鬥奇 +争妍斗胜 爭妍鬥勝 +争妍斗艳 爭妍鬥豔 +争强好胜 爭強好勝 +争强斗胜 爭強鬥勝 +争强显胜 爭強顯勝 +争强赌胜 爭強賭勝 +争斗 爭鬥 +争斗不休 爭鬥不休 +争相罗致 爭相羅致 +争红斗紫 爭紅鬥紫 +争胜 爭勝 +争胜要强 爭勝要強 +争闲气 爭閒氣 +争面子 爭面子 +事与愿违 事與願違 +事也干 事也幹 +事出 事出 +事出不意 事出不意 +事出不测 事出不測 +事出有因 事出有因 +事前事后 事前事後 +事发 事發 +事发地点 事發地點 +事发时 事發時 +事可干 事可幹 +事后 事後 +事后聪明 事後聰明 +事后诸葛亮 事後諸葛亮 +事好干 事好幹 +事实胜于雄辩 事實勝於雄辯 +事干 事幹 +事态发展 事態發展 +事情可干 事情可幹 +事情好干 事情好幹 +事情干脆 事情干脆 +事成之后 事成之後 +事有斗巧 事有鬥巧 +事迹 事蹟 +二丁挂 二丁掛 +二仑 二崙 +二仑乡 二崙鄉 +二价 二價 +二余 二餘 +二党制 二黨制 +二六板 二六板 +二冲程引擎 二衝程引擎 +二出祁山 二出祁山 +二分钟 二分鐘 +二划 二劃 +二叠系 二疊系 +二口虫 二口蟲 +二只 二隻 +二台 二臺 +二叶松 二葉松 +二合一 二合一 +二吊六 二吊六 +二周 二週 +二和药 二和藥 +二回 二回 +二回熟 二回熟 +二堂舍子 二堂舍子 +二天后 二天後 +二姑娘顽老雕 二姑娘頑老雕 +二娘 二孃 二娘 +二娘子 二娘子 +二屋里 二屋裏 +二年制 二年制 +二弦 二絃 +二志 二志 +二恶英 二噁英 +二手烟 二手菸 +二拉八当 二拉八當 +二撇胡 二撇鬍 +二斗 二斗 +二杆子 二桿子 +二杯 二杯 +二板 二板 +二极 二極 +二极体 二極體 +二极管 二極管 +二柜 二櫃 +二次曲 二次曲 +二次曲线 二次曲線 +二次曲面 二次曲面 +二段制 二段制 +二流人才 二流人才 +二点钟 二點鐘 +二班制 二班制 +二田制 二田制 +二百万 二百萬 +二百个 二百個 +二百多万 二百多萬 +二种 二種 +二秒钟 二秒鐘 +二缶钟惑 二缶鐘惑 +二老板 二老闆 +二胡 二胡 +二致 二致 +二苏 二蘇 +二虎相斗 二虎相鬥 +二表哥 二表哥 +二表妹 二表妹 +二表姊 二表姊 +二表姊夫 二表姊夫 +二表姐 二表姐 +二表嫂 二表嫂 +二表弟 二表弟 +二进位制 二進位制 +二进制 二進制 +二部制 二部制 +二部合唱 二部合唱 +二部曲 二部曲 +二里 二里 +二里头 二里頭 +二里头文化 二里頭文化 +二重分类表 二重分類表 +二门不出 二門不出 +二面角 二面角 +二项式系数 二項式係數 +于一 於一 +于一役 於一役 +于七 於七 +于三 於三 +于丑 於醜 +于世 於世 +于丹 于丹 +于之 於之 +于乎 於乎 +于乐 於樂 +于九 於九 +于事 於事 +于事无济 於事無濟 +于事无补 於事無補 +于二 於二 +于于 于于 +于五 於五 +于人 於人 +于仁泰 于仁泰 +于今 於今 +于今犹烈 於今猶烈 +于从濂 于從濂 +于他 於他 +于伏 於伏 +于会泳 于會泳 +于伟国 于偉國 +于何 於何 +于余曲折 于餘曲折 +于你 於你 +于佳卉 于佳卉 +于光远 于光遠 +于克勒 于克勒 +于八 於八 +于六 於六 +于冕 于冕 +于军 于軍 +于农 於農 +于凌奎 于凌奎 +于凤桐 于鳳桐 +于凤至 于鳳至 +于前 於前 +于劣 於劣 +于勒 于勒 +于勤 於勤 +于化虎 于化虎 +于十 於十 +于半 於半 +于占元 于占元 +于双戈 于雙戈 +于台烟 于臺煙 +于右任 于右任 +于吉 于吉 +于后 於後 +于呼哀哉 於呼哀哉 +于品海 于品海 +于嗟 于嗟 +于四 於四 +于国 於國 +于国桢 于國楨 +于坚 于堅 +于垂 於垂 +于堅 于堅 +于墙 於牆 +于大宝 于大寶 +于天仁 于天仁 +于夫罗 於夫羅 +于奇库杜克 于奇庫杜克 +于她 於她 +于好 於好 +于始 於始 +于姓 于姓 +于娜 于娜 +于娟 于娟 +于子千 于子千 +于孔兼 于孔兼 +于学忠 于學忠 +于它 於它 +于家 於家 +于家为国 於家爲國 +于家堡 于家堡 +于密 於密 +于寘 于寘 +于小伟 于小偉 +于小彤 于小彤 +于尔岑 于爾岑 +于尔根 于爾根 +于尔里克 于爾里克 +于尽 於盡 +于山 于山 +于山国 于山國 +于左 於左 +于差 於差 +于己 於己 +于市 於市 +于帅 于帥 +于帥 于帥 +于幕 於幕 +于幼军 于幼軍 +于幼华 於幼華 +于广洲 于廣洲 +于康震 于康震 +于式枚 于式枚 +于弱 於弱 +于强 於強 +于归 于歸 +于征 於徵 +于徐 于徐 +于從濂 于從濂 +于德海 于德海 +于心 於心 +于心不安 於心不安 +于心不忍 於心不忍 +于心何忍 於心何忍 +于心无愧 於心無愧 +于志宁 于志寧 +于怀 於懷 +于思 于思 +于慎行 于慎行 +于慧 于慧 +于戏 於戲 +于成龍 于成龍 +于成龙 于成龍 +于我 於我 +于振 于振 +于振武 于振武 +于敏 于敏 +于敏中 于敏中 +于敝 於敝 +于斌 于斌 +于斯 於斯 +于斯塔德 于斯塔德 +于斯納爾斯貝里 于斯納爾斯貝里 +于斯纳尔斯贝里 于斯納爾斯貝里 +于斯达尔 于斯達爾 +于斯達爾 于斯達爾 +于时 於時 +于明涛 于明濤 +于是 於是 +于是之 於是之 +于是乎 於是乎 +于是就 於是就 +于晨楠 于晨楠 +于晴 于晴 +于杰 于傑 +于树洁 于樹潔 +于根伟 于根偉 +于格 于格 +于梨华 於梨華 +于樂 于樂 +于欣源 于欣源 +于正升 于正昇 +于正昌 于正昌 +于此 於此 +于毕 於畢 +于民 於民 +于民润国 於民潤國 +于水 於水 +于永波 于永波 +于汉超 于漢超 +于江震 于江震 +于法 於法 +于法无据 於法無據 +于波 于波 +于泽尔 于澤爾 +于洪区 于洪區 +于浩威 于浩威 +于海洋 于海洋 +于涛 于濤 +于湘兰 于湘蘭 +于潜县 於潛縣 +于濤 于濤 +于火 於火 +于焉 於焉 +于爾里克 于爾里克 +于物 於物 +于特森 于特森 +于玉立 于玉立 +于田 于田 +于田县 于田縣 +于盲 於盲 +于祂 於祂 +于禁 于禁 +于秀敏 于秀敏 +于穆 於穆 +于素秋 于素秋 +于终 於終 +于美 於美 +于美人 於美人 +于色 於色 +于若木 于若木 +于荫霖 于蔭霖 +于菟 於菟 +于蓝 於藍 +于行 於行 +于衡 于衡 +于衷 於衷 +于西翰 于西翰 +于謙 于謙 +于该 於該 +于谦 于謙 +于贈 于贈 +于贝尔 于貝爾 +于赠 于贈 +于越 于越 +于軍 于軍 +于过 於過 +于远伟 于遠偉 +于途 於途 +于道泉 于道泉 +于邑 於邑 +于都县 於都縣 +于里察 于里察 +于野 於野 +于阗 于闐 +于陆 於陸 +于雾霭之中 於霧靄之中 +于震寰 于震寰 +于震环 于震環 +于靖 于靖 +于韋斯屈萊 于韋斯屈萊 +于韦斯屈莱 于韋斯屈萊 +于风政 于風政 +于飞 于飛 +于飞之乐 于飛之樂 +于馀曲折 于餘曲折 +于默奥 于默奧 +亏不尽 虧不盡 +亏了 虧了 +亏折 虧折 +亏本出售 虧本出售 +云中 雲中 +云中君 雲中君 +云中白鹤 雲中白鶴 +云为 云爲 +云乎 云乎 +云云 云云 +云从龙风从虎 雲從龍風從虎 +云仙杂记 雲仙雜記 +云何 云何 +云儿 雲兒 +云兴霞蔚 雲興霞蔚 +云冈 雲岡 +云冈石佛 雲岡石佛 +云冈石窟 雲岡石窟 +云出无心 雲出無心 +云华 雲華 +云南 雲南 +云南回变 雲南回變 +云南白药 雲南白藥 +云南省 雲南省 +云南起义 雲南起義 +云南黄馨 雲南黃馨 +云县 雲縣 +云台 雲臺 +云台山 雲臺山 +云合雾集 雲合霧集 +云吞 雲吞 +云吞面 雲吞麪 +云和 雲和 +云和县 雲和縣 +云嘉南 雲嘉南 +云团 雲團 +云图 雲圖 +云城区 云城區 +云天 雲天 +云天昊 雲天昊 +云天高谊 雲天高誼 +云头 雲頭 +云头儿 雲頭兒 +云子 雲子 +云孙 雲孫 +云安 雲安 +云安县 雲安縣 +云实 雲實 +云室 雲室 +云小啾 雲小啾 +云尔 云爾 +云层 雲層 +云屏 雲屏 +云屯雨集 雲屯雨集 +云山 雲山 +云山雾罩 雲山霧罩 +云岩区 雲巖區 +云岭 雲嶺 +云帆 雲帆 +云师 雲師 +云帚 雲帚 +云开见日 雲開見日 +云形定规 雲形定規 +云彩 雲彩 +云影 雲影 +云情雨意 雲情雨意 +云房 雲房 +云手 雲手 +云扰 雲擾 +云收雨散 雲收雨散 +云收雾散 雲收霧散 +云散 雲散 +云散风流 雲散風流 +云景 雲景 +云朵 雲朵 +云杉 雲杉 +云板 雲板 +云林 雲林 +云林人 雲林人 +云林县 雲林縣 +云林寺 雲林寺 +云林工专 雲林工專 +云林科技大学 雲林科技大學 +云林讯 雲林訊 +云栈 雲棧 +云根 雲根 +云梢 雲梢 +云梦 雲夢 +云梦县 雲夢縣 +云梦大泽 雲夢大澤 +云梯 雲梯 +云梯车 雲梯車 +云步 雲步 +云母 雲母 +云母片 雲母片 +云母石 雲母石 +云气 雲氣 +云水 雲水 +云水僧 雲水僧 +云水道人 雲水道人 +云汉 雲漢 +云河 雲河 +云泥 雲泥 +云泥之别 雲泥之別 +云泥异路 雲泥異路 +云浮 雲浮 +云浮市 雲浮市 +云海 雲海 +云消雨散 雲消雨散 +云消雾散 雲消霧散 +云涌 雲涌 +云涛 雲濤 +云液 雲液 +云淡风轻 雲淡風輕 +云游 雲遊 +云游四方 雲遊四方 +云溪 云溪 +云溪区 云溪區 +云烟 雲煙 +云烟过眼 雲煙過眼 +云烟过眼录 雲煙過眼錄 +云然 云然 +云爲 云爲 +云片糕 雲片糕 +云版 雲版 +云物 雲物 +云石 雲石 +云科大 雲科大 +云程发轫 雲程發軔 +云窗雾槛 雲窗霧檻 +云章 雲章 +云端 雲端 +云端里看厮杀 雲端裏看廝殺 +云端里老鼠 雲端裏老鼠 +云笈七签 雲笈七籤 +云笈七籤 雲笈七籤 +云篦 雲篦 +云精 雲精 +云系 雲系 +云网 雲網 +云翳 雲翳 +云翻雨复 雲翻雨覆 +云肩 雲肩 +云腿 雲腿 +云芝 雲芝 +云英 雲英 +云英未嫁 雲英未嫁 +云莽莽 雲莽莽 +云蒸霞蔚 雲蒸霞蔚 +云行雨施 雲行雨施 +云表 雲表 +云西市 雲西市 +云谲波诡 雲譎波詭 +云谷 雲谷 +云豆 雲豆 +云豹 雲豹 +云贵 雲貴 +云贵川 雲貴川 +云贵高原 雲貴高原 +云起龙骧 雲起龍驤 +云路 雲路 +云车 雲車 +云车风马 雲車風馬 +云遮月 雲遮月 +云量 雲量 +云锣 雲鑼 +云锦 雲錦 +云门 雲門 +云门舞集 雲門舞集 +云阳 雲陽 +云阳县 雲陽縣 +云阳市 雲陽市 +云阶月地 雲階月地 +云际 雲際 +云雀 雲雀 +云集 雲集 +云雨 雲雨 +云雨之欢 雲雨之歡 +云雨巫山 雲雨巫山 +云雨高唐 雲雨高唐 +云雾 雲霧 +云雾径迹 雲霧徑跡 +云霄 雲霄 +云霄县 雲霄縣 +云霄飞车 雲霄飛車 +云霓 雲霓 +云霓之望 雲霓之望 +云霞 雲霞 +云霭 雲靄 +云顶 雲頂 +云须 雲鬚 +云髻 雲髻 +云鬓 雲鬢 +云鬟 雲鬟 +云麾勋章 雲麾勳章 +云麾将军碑 雲麾將軍碑 +云龙 雲龍 +云龙区 雲龍區 +云龙县 雲龍縣 +云龙风虎 雲龍風虎 +互不干涉 互不干涉 +互不干涉內政 互不干涉內政 +互于 互於 +互别苗头 互別苗頭 +互制 互制 +互动关系 互動關係 +互动台 互動臺 +互助合作 互助合作 +互发 互發 +互向 互向 +互有胜负 互有勝負 +互生叶 互生葉 +互相联系 互相聯繫 +互致问候 互致問候 +互蒙其利 互蒙其利 +互鉴 互鑑 +五七干校 五七幹校 +五七干部学校 五七幹部學校 +五万 五萬 +五万三千 五萬三千 +五万两千 五萬兩千 +五万五千 五萬五千 +五万八千 五萬八千 +五世同堂 五世同堂 +五个 五個 +五代同堂 五代同堂 +五余 五餘 +五倍子虫 五倍子蟲 +五克 五克 +五党 五黨 +五公里 五公里 +五出戏 五齣戲 +五出祁山 五出祁山 +五分钟 五分鐘 +五划 五劃 +五十五万 五十五萬 +五十六万 五十六萬 +五十多万 五十多萬 +五千 五千 +五千多万 五千多萬 +五发 五發 +五只 五隻 +五台 五臺 +五台县 五臺縣 +五台山 五臺山 +五台市 五臺市 +五叶 五葉 +五周 五週 +五周年 五週年 +五天后 五天後 +五岳 五嶽 +五年计划 五年計劃 +五度制调值标记法 五度制調值標記法 +五弦 五絃 +五彩 五彩 +五彩夺目 五彩奪目 +五彩宾纷 五彩賓紛 +五彩缤纷 五彩繽紛 +五扎 五紮 +五折 五折 +五斗 五斗 +五斗折腰 五斗折腰 +五斗柜 五斗櫃 +五斗橱 五斗櫥 +五斗米 五斗米 +五斗米道 五斗米道 +五斗解酲 五斗解酲 +五杯 五杯 +五板 五板 +五棵松 五棵松 +五浊恶世 五濁惡世 +五点钟 五點鐘 +五百万 五百萬 +五百个 五百個 +五百多万 五百多萬 +五百姻缘天注定 五百姻緣天註定 +五种 五種 +五种性 五種性 +五秒钟 五秒鐘 +五笔划 五筆劃 +五育并重 五育並重 +五胡 五胡 +五胡之乱 五胡之亂 +五胡乱华 五胡亂華 +五胡十六国 五胡十六國 +五脏 五臟 +五脏俱全 五臟俱全 +五脏六腑 五臟六腑 +五脏庙 五臟廟 +五脏神 五臟神 +五色祥云 五色祥雲 +五花腌猪肉 五花醃豬肉 +五虫 五蟲 +五行并下 五行並下 +五行生克 五行生剋 +五谷 五穀 +五谷不分 五穀不分 +五谷不升 五穀不升 +五谷丰收 五穀豐收 +五谷丰登 五穀豐登 +五谷丰稔 五穀豐稔 +五谷杂粮 五穀雜糧 +五谷王北街 五谷王北街 +五谷王南街 五谷王南街 +五辟 五辟 +五采 五采 +五里 五里 +五里雾 五里霧 +五里雾中 五里霧中 +五面 五面 +五面体 五面體 +五马分尸 五馬分屍 +井台 井臺 +井干 井榦 +井干摧败 井榦摧敗 +井田制 井田制 +井田制度 井田制度 +井里 井裏 +亘古 亙古 +亚东关系 亞東關係 +亚于 亞於 +亚克力 亞克力 +亚克朗 亞克朗 +亚利叶 亞利葉 +亚力克 亞力克 +亚历 亞歷 +亚历山大 亞歷山大 +亚历桑 亞歷桑 +亚当 亞當 +亚当山德勒 亞當山德勒 +亚当斯 亞當斯 +亚当斯密 亞當斯密 +亚得里亚海 亞得里亞海 +亚德安布洛迪 亞德安布洛迪 +亚松森 亞松森 +亚核 亞核 +亚洲周刊 亞洲週刊 +亚洲杯 亞洲盃 +亚穆苏克罗 亞穆蘇克羅 +亚美利加人种 亞美利加人種 +亚美尼亚历 亞美尼亞曆 +亚词汇单元 亞詞彙單元 +亚里 亞里 +亚里士多德 亞里士多德 +亚里斯多德 亞里斯多德 +亚里斯提 亞里斯提 +亚雷克斯 亞雷克斯 +亚青杯 亞青盃 +亚麻布 亞麻布 +亟欲 亟欲 +亟须 亟須 +亡了 亡了 +亡国灭种 亡國滅種 +交个 交個 +交了 交了 +交于 交於 +交出 交出 +交出去 交出去 +交出来 交出來 +交割价 交割價 +交卷 交卷 +交叉耐药性 交叉耐藥性 +交发 交發 +交口称赞 交口稱讚 +交合 交合 +交哄 交鬨 +交响乐团 交響樂團 +交响曲 交響曲 +交回 交回 +交回去 交回去 +交回来 交回來 +交困 交困 +交差了事 交差了事 +交并 交併 +交恶 交惡 +交战团体 交戰團體 +交托 交託 +交换价值 交換價值 +交换技术 交換技術 +交易价 交易價 +交易台 交易臺 +交杯 交杯 +交杯盏 交杯盞 +交杯酒 交杯酒 +交汇 交匯 +交汇处 交匯處 +交流团 交流團 +交游 交遊 +交游广阔 交遊廣闊 +交白卷 交白卷 +交货价 交貨價 +交通号志 交通號誌 +交通标志 交通標誌 +交通管制 交通管制 +交面 交面 +亦云 亦云 +亦同 亦同 +亦对于 亦對於 +亦庄亦谐 亦莊亦諧 +亦当 亦當 +亦舍下 亦捨下 +亦须 亦須 +产业别 產業別 +产业后备 產業後備 +产业链 產業鏈 +产出 產出 +产制 產製 +产卵洄游 產卵洄游 +产后 產後 +产后检査 產後檢查 +产品组合 產品組合 +产地价格 產地價格 +产婆术 產婆術 +产学合作 產學合作 +产生出 產生出 +产销合一 產銷合一 +产销合同 產銷合同 +亨利克森 亨利克森 +亨塞克 亨塞克 +享尽 享盡 +享御 享御 +京二胡 京二胡 +京胡 京胡 +亭台 亭臺 +亭台楼榭 亭臺樓榭 +亭台楼阁 亭臺樓閣 +亭彩 亭彩 +亭彩店 亭彩店 +亮丑 亮醜 +亮光蜡 亮光蠟 +亮彩 亮彩 +亮钟 亮鐘 +亲历 親歷 +亲历其境 親歷其境 +亲友团 親友團 +亲台 親臺 +亲娘 親孃 +亲子关系 親子關係 +亲子台 親子臺 +亲子鉴定 親子鑑定 +亲密关系 親密關係 +亲属关系 親屬關係 +亲幸 親倖 +亲征 親征 +亲戚 親戚 +亲戚关系 親戚關係 +亲极反疏 親極反疏 +亲民党 親民黨 +亲痛仇快 親痛仇快 +亲笔签名 親筆簽名 +亲系 親系 +亲缘关系 親緣關係 +亲自出马 親自出馬 +亲自干 親自幹 +亲自挂帅 親自掛帥 +亲身经历 親身經歷 +亹亹不卷 亹亹不卷 +人中豪杰 人中豪傑 +人为万物之灵 人爲萬物之靈 +人云 人云 +人云亦云 人云亦云 +人众胜天 人衆勝天 +人体彩绘 人體彩繪 +人克莱门斯 人克萊門斯 +人出现 人出現 +人别哭 人別哭 +人到中年万事休 人到中年萬事休 +人制 人制 +人前人后 人前人後 +人力回天 人力回天 +人千人万 人千人萬 +人参 人蔘 +人参果 人蔘果 +人参精 人蔘精 +人口分布 人口分佈 +人各有志 人各有志 +人同此心 人同此心 +人同此心心同此理 人同此心心同此理 +人后 人後 +人团法 人團法 +人困马乏 人困馬乏 +人如其面 人如其面 +人如风后入江云 人如風後入江雲 +人定胜天 人定勝天 +人寿年丰 人壽年豐 +人尽其才 人盡其才 +人尽可夫 人盡可夫 +人尽皆知 人盡皆知 +人工冬眠 人工冬眠 +人工心脏 人工心臟 +人工阅卷 人工閱卷 +人心不同 人心不同 +人心向背 人心向背 +人心如面 人心如面 +人心所向 人心所向 +人心涣漓 人心渙漓 +人性本恶 人性本惡 +人才 人才 +人才出众 人才出衆 +人才外流 人才外流 +人才库 人才庫 +人才流失 人才流失 +人才济济 人才濟濟 +人才辈出 人才輩出 +人才难得 人才難得 +人无千日 人無千日 +人机介面 人機介面 +人机界面 人機界面 +人权斗士 人權鬥士 +人杰 人傑 +人杰地灵 人傑地靈 +人极 人極 +人欲 人慾 +人欲横流 人慾橫流 +人民代表 人民代表 +人民党 人民黨 +人民团体 人民團體 +人民意志 人民意志 +人流手术 人流手術 +人浮于事 人浮於事 +人浮于食 人浮於食 +人海战术 人海戰術 +人烟 人煙 +人烟凑集 人煙湊集 +人烟浩穰 人煙浩穰 +人烟稀少 人煙稀少 +人烟稠密 人煙稠密 +人烟辐辏 人煙輻輳 +人物志 人物誌 +人生价值 人生價值 +人生舞台 人生舞臺 +人神同愤 人神同憤 +人种 人種 +人种学 人種學 +人种差别 人種差別 +人穷志不穷 人窮志不窮 +人穷志短 人窮志短 +人老精姜老辣 人老精薑老辣 +人胜节 人勝節 +人赃俱获 人贓俱獲 +人造板 人造板 +人造纤维 人造纖維 +人链 人鏈 +人间烟火 人間煙火 +人际关系 人際關係 +人面 人面 +人面上 人面上 +人面兽心 人面獸心 +人面桃花 人面桃花 +人面狮身 人面獅身 +人面逐高低 人面逐高低 +人饥己饥 人飢己飢 +亿万 億萬 +亿万富翁 億萬富翁 +亿万富豪 億萬富豪 +亿万斯年 億萬斯年 +亿个 億個 +亿余 億餘 +亿只 億隻 +亿多只 億多隻 +亿天后 億天後 +什不闲 什不閒 +什么 什麼 +什叶派 什葉派 +什锦炒面 什錦炒麪 +什锦面 什錦麪 +什面 什面 +仁人志士 仁人志士 +仁布 仁布 +仁布县 仁布縣 +仁心仁术 仁心仁術 +仁术 仁術 +仁杰 仁杰 +仁武厂 仁武廠 +仁至义尽 仁至義盡 +仅余 僅餘 +仅作参考 僅作參考 +仅供参考 僅供參考 +仅占 僅佔 +仅只 僅只 +仅次于 僅次於 +仆人 僕人 +仆仆 僕僕 +仆仆风尘 僕僕風塵 +仆从 僕從 +仆使 僕使 +仆倒 仆倒 +仆僮 僕僮 +仆僮成群 僕僮成羣 +仆吏 僕吏 +仆固怀恩 僕固懷恩 +仆地 仆地 +仆夫 僕伕 +仆妇 僕婦 +仆姑 僕姑 +仆婢 僕婢 +仆射 僕射 +仆射姑 僕射姑 +仆少 僕少 +仆役 僕役 +仆憎 僕憎 +仆欧 僕歐 +仆然 仆然 +仆程 僕程 +仆虽罢驽 僕雖罷駑 +仆街 仆街 +仇人 仇人 +仇仇 仇讎 +仇偶 仇偶 +仇口儿 仇口兒 +仇国论 仇國論 +仇外心理 仇外心理 +仇家 仇家 +仇怨 仇怨 +仇恨 仇恨 +仇恨罪 仇恨罪 +仇恨罪行 仇恨罪行 +仇敌 仇敵 +仇杀 仇殺 +仇梓鸣 仇梓鳴 +仇疙瘩 仇疙瘩 +仇英 仇英 +仇视 仇視 +仇隙 仇隙 +仇香 仇香 +今儿个 今兒個 +今冬 今冬 +今后 今後 +今秋 今秋 +介于 介於 +介于两难 介於兩難 +介壳虫 介殼蟲 +介系词 介係詞 +介绍出来 介紹出來 +介胄 介冑 +介虫 介蟲 +介面 介面 +介面卡 介面卡 +介面板 介面板 +仍复 仍復 +从上向下 從上向下 +从下向上 從下向上 +从事于 從事於 +从于 從於 +从今以后 從今以後 +从价税 從價稅 +从外向內 從外向內 +从宽发落 從寬發落 +从属于 從屬於 +从属关系 從屬關係 +从心所欲 從心所欲 +从此以后 從此以後 +从此往后 從此往後 +从轻发落 從輕發落 +从里到外 從裏到外 +从里向外 從裏向外 +仑丰村 崙豐村 +仑背 崙背 +仑背乡 崙背鄉 +仓皇出逃 倉皇出逃 +他了 他了 +他克制 他剋制 +他出 他出 +他出去 他出去 +他出来 他出來 +他志 他志 +他念 他念 +他种 他種 +他钟 他鐘 +仗托 仗托 +付了 付了 +付出 付出 +付出去 付出去 +付出型 付出型 +付合 付合 +付托 付託 +仙台 仙台 +仙后 仙后 +仙后座 仙后座 +仙坛 仙壇 +仙岩 仙岩 +仙才 仙才 +仙术 仙術 +仙游 仙遊 +仙游县 仙遊縣 +仙药 仙藥 +仙迹 仙蹟 +仟克 仟克 +仡栗 仡栗 +代价 代價 +代出 代出 +代工厂 代工廠 +代录 代錄 +代扣 代扣 +代数几何 代數幾何 +代数曲线 代數曲線 +代数曲面 代數曲面 +代码表 代碼表 +代签 代簽 +代签人 代簽人 +代表 代表 +代表人 代表人 +代表人物 代表人物 +代表会 代表會 +代表作 代表作 +代表团 代表團 +代表处 代表處 +代表大会 代表大會 +代表性 代表性 +代表权 代表權 +代表署 代表署 +代表色 代表色 +代表金 代表金 +代表队 代表隊 +代议制 代議制 +令人发指 令人髮指 +令人注目 令人注目 +令出如山 令出如山 +令出必行 令出必行 +令出惟行 令出惟行 +令岳 令岳 +令狐冲 令狐沖 +以一当十 以一當十 +以一持万 以一持萬 +以一知万 以一知萬 +以一驭万 以一馭萬 +以人为鉴 以人爲鑑 +以价制量 以價制量 +以众克寡 以衆克寡 +以免借口 以免藉口 +以冰致蝇 以冰致蠅 +以利于 以利於 +以功复过 以功覆過 +以升量石 以升量石 +以华制华 以華制華 +以古为鉴 以古爲鑑 +以后 以後 +以售其奸 以售其奸 +以多胜少 以多勝少 +以夷制夷 以夷制夷 +以少克众 以少克衆 +以少胜多 以少勝多 +以弱制强 以弱制強 +以弱胜强 以弱勝強 +以往鉴来 以往鑑來 +以意逆志 以意逆志 +以智取胜 以智取勝 +以暴制暴 以暴制暴 +以柔克刚 以柔克剛 +以柔制刚 以柔制剛 +以泪洗面 以淚洗面 +以点带面 以點帶面 +以自制 以自制 +以至于 以至於 +以致 以致 +以致于 以致於 +以药养医 以藥養醫 +以莛叩钟 以莛叩鐘 +以莛撞钟 以莛撞鐘 +以蜡代薪 以蠟代薪 +以观后效 以觀後效 +以貍致鼠 以貍致鼠 +以防万一 以防萬一 +以静制动 以靜制動 +仪制 儀制 +仪器表 儀器表 +仪征 儀徵 +仪征市 儀徵市 +仪态万千 儀態萬千 +仪态万方 儀態萬方 +仪注 儀注 +仪范 儀範 +仪表 儀表 儀錶 +仪表出众 儀表出衆 +仪表堂堂 儀表堂堂 +仪表板 儀表板 +仪表版 儀表版 +仪表盘 儀表盤 +仰之弥高 仰之彌高 +仰叹 仰嘆 +仰给于人 仰給於人 +仰药 仰藥 +仰面 仰面 +仲冬 仲冬 +仲秋 仲秋 +件钟 件鐘 +价位 價位 +价值 價值 +价值冲突 價值衝突 +价值判断 價值判斷 +价值增殖 價值增殖 +价值尺度 價值尺度 +价值工程 價值工程 +价值形式 價值形式 +价值感 價值感 +价值指标 價值指標 +价值标准 價值標準 +价值观 價值觀 +价值观念 價值觀念 +价值规律 價值規律 +价值论 價值論 +价值连城 價值連城 +价值逻辑 價值邏輯 +价值量 價值量 +价单 價單 +价差 價差 +价廉物美 價廉物美 +价格 價格 +价格冻结 價格凍結 +价格战 價格戰 +价格标 價格標 +价格标签 價格標籤 +价格范围 價格範圍 +价格表 價格表 +价款 價款 +价比 價比 +价电子 價電子 +价目 價目 +价目单 價目單 +价目表 價目表 +价码 價碼 +价钱 價錢 +任一个 任一個 +任于 任於 +任人摆布 任人擺佈 +任务团 任務團 +任教于 任教於 +任期制 任期制 +任由摆布 任由擺佈 +任笔沈诗 任筆沈詩 +任重致远 任重致遠 +仿佛 彷彿 +仿制 仿製 +仿制品 仿製品 +仿制药 仿製藥 +企业团 企業團 +企业系 企業系 +企业集团 企業集團 +企划 企劃 +企划书 企劃書 +企划人 企劃人 +企划厅 企劃廳 +企划处 企劃處 +企划案 企劃案 +企划组 企劃組 +企划部 企劃部 +企管系 企管系 +伊万诺 伊萬諾 +伊于湖底 伊于湖底 +伊于胡底 伊于胡底 +伊吉克 伊吉克 +伊周 伊周 +伊塞克湖 伊塞克湖 +伊布拉欣 伊布拉欣 +伊府面 伊府麪 +伊拉克 伊拉克 +伊拉克人 伊拉克人 +伊拉克籍 伊拉克籍 +伊拉克队 伊拉克隊 +伊斯兰党 伊斯蘭黨 +伊斯兰历 伊斯蘭曆 +伊核 伊核 +伊犁纵谷 伊犁縱谷 +伊莱克斯 伊萊克斯 +伊郁 伊鬱 +伊里奇 伊里奇 +伊里布 伊里布 +伊里格瑞 伊裏格瑞 +伊面 伊麪 +伍员鞭尸 伍員鞭屍 +伍德合金 伍德合金 +伍瑞克 伍瑞克 +伍采克 伍采克 +伏几 伏几 +伏尸 伏屍 +伏尸流血 伏屍流血 +伏尸遍野 伏屍遍野 +伏愿 伏願 +伏胜 伏勝 +伐异党同 伐異黨同 +伐罪吊民 伐罪弔民 +休仑湖 休崙湖 +休克 休克 +休征 休徵 +休戚 休慼 +休戚与共 休慼與共 +休戚相关 休慼相關 +休杰克曼 休傑克曼 +休致 休致 +休闲区 休閒區 +休闲服 休閒服 +休闲活动 休閒活動 +休闲组 休閒組 +休闲裤 休閒褲 +休闲鞋 休閒鞋 +休闲风 休閒風 +休闲馆 休閒館 +众口同声 衆口同聲 +众口熏天 衆口熏天 +众志成城 衆志成城 +众所周知 衆所周知 +众曲不容直 衆曲不容直 +优于 優於 +优先发展 優先發展 +优势种 優勢種 +优哉游哉 優哉遊哉 +优惠价 優惠價 +优惠价格 優惠價格 +优游 優遊 +优游不迫 優遊不迫 +优游自在 優遊自在 +优游自得 優遊自得 +优胜 優勝 +优胜劣败 優勝劣敗 +优胜奖 優勝獎 +优胜者 優勝者 +优胜队 優勝隊 +伙人 夥人 +伙众 夥衆 +伙伴 夥伴 +伙伴国 夥伴國 +伙伴龙 夥伴龍 +伙办 夥辦 +伙友 夥友 +伙同 夥同 +伙够 夥夠 +伙夫 伙伕 +伙头 伙頭 +伙房 伙房 +伙穿 夥穿 +伙计 夥計 +伙食 伙食 +伙食团 伙食團 +伙食费 伙食費 +会上签署 會上簽署 +会上签订 會上簽訂 +会个 會個 +会了 會了 +会于 會於 +会党 會黨 +会出 會出 +会出去 會出去 +会出来 會出來 +会升 會升 +会占 會佔 +会占卜 會占卜 +会发 會發 +会合 會合 +会合周期 會合周期 +会合处 會合處 +会合点 會合點 +会吊 會弔 +会同 會同 +会同县 會同縣 +会后 會後 +会员价 會員價 +会员制 會員制 +会员团 會員團 +会场价 會場價 +会尽 會盡 +会干 會幹 +会干净 會乾淨 +会干扰 會干擾 +会干政 會干政 +会干杯 會乾杯 +会干枯 會乾枯 +会干涉 會干涉 +会干涸 會乾涸 +会干脆 會乾脆 +会干裂 會乾裂 +会干预 會干預 +会当 會當 +会念 會念 +会扣 會扣 +会杯 會杯 +会种 會種 +会签制度 會簽制度 +会计分录 會計分錄 +会计制度 會計制度 +会计报表 會計報表 +会计标准 會計標準 +会计系 會計系 +会议记录 會議記錄 +会逢其适 會逢其適 +会里 會裏 +会里县 會裏縣 +会长团 會長團 +会面 會面 +会面处 會面處 +会须 會須 +伞面 傘面 +伟克适 偉克適 +伟晶岩 偉晶岩 +传习录 傳習錄 +传于 傳於 +传位于四太子 傳位于四太子 +传出 傳出 +传出去 傳出去 +传出来 傳出來 +传出神经 傳出神經 +传制权 傳制權 +传动系统 傳動系統 +传动链 傳動鏈 +传发 傳發 +传回 傳回 +传回来 傳回來 +传布 傳佈 +传心术 傳心術 +传播出去 傳播出去 +传播学系 傳播學系 +传教团 傳教團 +传杯弄盏 傳杯弄盞 +传杯换盏 傳杯換盞 +传杯送盏 傳杯送盞 +传板 傳板 +传灯录 傳燈錄 +传热系数 傳熱係數 +传真发送 傳真發送 +传种 傳種 +传统中国医药 傳統中國醫藥 +传统医药 傳統醫藥 +传舍 傳舍 +传译出 傳譯出 +传赞 傳贊 +传输技术 傳輸技術 +传输控制 傳輸控制 +传输控制协定 傳輸控制協定 +传达出 傳達出 +传达出来 傳達出來 +传递出去 傳遞出去 +传闻证据 傳聞證據 +传颂千古 傳頌千古 +伤了 傷了 +伤亡枕借 傷亡枕藉 +伤别 傷別 +伤口发炎 傷口發炎 +伤寒杆菌 傷寒桿菌 +伤寒症 傷寒症 +伤心欲绝 傷心欲絕 +伤心致死 傷心致死 +伤痕累累 傷痕累累 +伤药 傷藥 +伤风克 傷風克 +伦理规范 倫理規範 +伪托 僞託 +伪药 僞藥 +伯余 伯余 +伯克 伯克 +伯克利 伯克利 +伯克制度 伯克制度 +伯克来 伯克來 +伯南克 伯南克 +伯娘 伯孃 +伯尔发斯特 伯爾發斯特 +伯杰 伯傑 +伯纳克 伯納克 +伯罗奔尼撒同盟 伯羅奔尼撒同盟 +伯里克利 伯里克利 +伯雍种玉 伯雍種玉 +估价 估價 +估价单 估價單 +估价行 估價行 +估算出 估算出 +伴同 伴同 +伴娘 伴娘 +伴游 伴遊 +伸出 伸出 +伸出去 伸出去 +伸出援手 伸出援手 +伸出来 伸出來 +伸向 伸向 +伸回 伸回 +伸回去 伸回去 +伸回来 伸回來 +伸展出 伸展出 +伸展出去 伸展出去 +伸志 伸志 +伸手不打笑面人 伸手不打笑面人 +似于 似於 +似松实紧 似鬆實緊 +佃租制度 佃租制度 +但云 但云 +但得一片橘皮吃且莫忘了洞庭湖 但得一片橘皮吃且莫忘了洞庭湖 +但愿 但願 +但愿如此 但願如此 +但曲 但曲 +但求如愿 但求如願 +但须 但須 +位于 位於 +位准 位準 +位极人臣 位極人臣 +低了 低了 +低于 低於 +低价 低價 +低价买进 低價買進 +低价位 低價位 +低价卖出 低價賣出 +低价团 低價團 +低价格 低價格 +低价高报 低價高報 +低卡路里 低卡路里 +低合金钢 低合金鋼 +低回 低迴 +低回不已 低迴不已 +低度发展国家 低度發展國家 +低标准 低標準 +低洼 低窪 +低筋面粉 低筋麪粉 +低荡 低盪 +低谷 低谷 +住个 住個 +住宅凶方 住宅兇方 +住扎 住紮 +佐饔得尝 佐饔得嘗 +体会出 體會出 +体制 體制 +体坛 體壇 +体坛人士 體壇人士 +体察出 體察出 +体干班 體幹班 +体彩 體彩 +体征 體徵 +体念 體念 +体温表 體溫表 +体物写志 體物寫志 +体现出 體現出 +体系 體系 +体系化 體系化 +体育台 體育臺 +体育系 體育系 +体育锻炼 體育鍛煉 +体胀系数 體脹係數 +体范 體範 +体表 體表 +体重表 體重表 +体面 體面 +体面起来 體面起來 +体验出 體驗出 +何以克当 何以克當 +何只 何只 +何小升 何小昇 +何尝 何嘗 +何干 何干 +何当 何當 +何必当初 何必當初 +何志 何志 +何志钦 何志欽 +何杰金氏病 何杰金氏病 +何极 何極 +何济于事 何濟於事 +何秋美 何秋美 +何种 何種 +何胜雄 何勝雄 +何至于 何至於 +何至于此 何至於此 +何豪杰 何豪傑 +何足挂齿 何足掛齒 +何须 何須 +余〇 餘〇 +余一 餘一 +余七 餘七 +余三 餘三 +余三勝 余三勝 +余三胜 余三勝 +余上沅 余上沅 +余下 餘下 +余业 餘業 +余个 餘個 +余九 餘九 +余事 餘事 +余二 餘二 +余五 餘五 +余亩 餘畝 +余人 餘人 +余件 餘件 +余众 餘衆 +余位 餘位 +余余 余余 +余俗 餘俗 +余倍 餘倍 +余僇 餘僇 +余元 餘元 +余光 餘光 +余光中 余光中 +余光生 余光生 +余党 餘黨 +余八 餘八 +余六 餘六 +余兰香 余蘭香 +余兴 餘興 +余刃 餘刃 +余切 餘切 +余利 餘利 +余剩 餘剩 +余割 餘割 +余力 餘力 +余勇 餘勇 +余勇可贾 餘勇可賈 +余十 餘十 +余压 餘壓 +余发扬 余發揚 +余只 餘隻 +余名 餘名 +余吾镇 余吾鎮 +余味 餘味 +余响 餘響 +余响绕梁 餘響繞梁 +余喘 餘喘 +余四 餘四 +余地 餘地 +余墨 餘墨 +余声 餘聲 +余外 餘外 +余天 余天 +余头 餘頭 +余妙 餘妙 +余姓 余姓 +余姚 餘姚 +余姚市 餘姚市 +余威 餘威 +余威德 余威德 +余子 餘子 +余子明 余子明 +余子碌碌 餘子碌碌 +余字 餘字 +余存 餘存 +余孽 餘孽 +余宪宗 余憲宗 +余岁 餘歲 +余干 餘干 +余干县 餘干縣 +余年 餘年 +余庆 餘慶 +余庆县 餘慶縣 +余座 餘座 +余弦 餘弦 +余思 餘思 +余思敏 余思敏 +余悸 餘悸 +余情 餘情 +余情未了 餘情未了 +余户 餘戶 +余政宪 余政憲 +余数 餘數 +余数定理 餘數定理 +余文 余文 +余文彬 余文彬 +余日 餘日 +余明 餘明 +余映 餘映 +余晖 餘暉 +余暇 餘暇 +余月 余月 +余本 餘本 +余杭 餘杭 +余杭区 餘杭區 +余杯 餘杯 +余枝 餘枝 +余桃 餘桃 +余桶 餘桶 +余次 餘次 +余款 餘款 +余歌沧 余歌滄 +余步 餘步 +余殃 餘殃 +余毒 餘毒 +余气 餘氣 +余氯 餘氯 +余江 餘江 +余江县 餘江縣 +余沥 餘瀝 +余波 餘波 +余波荡漾 餘波盪漾 +余泽 餘澤 +余派 餘派 +余温 餘溫 +余火 餘火 +余灿荣 余燦榮 +余炳贤 余炳賢 +余点 餘點 +余烈 餘烈 +余烬 餘燼 +余热 餘熱 +余珍 餘珍 +余珮琳 余珮琳 +余生 餘生 +余男 余男 +余留 餘留 +余留事务 餘留事務 +余留无符号数 餘留無符號數 +余码 餘碼 +余碧芬 余碧芬 +余秀菁 余秀菁 +余秉谚 余秉諺 +余种 餘種 +余窍 餘竅 +余筱萍 余筱萍 +余篇 餘篇 +余粮 餘糧 +余绪 餘緒 +余缺 餘缺 +余罪 餘罪 +余羡 餘羨 +余脉 餘脈 +余膏 餘膏 +余苑绮 余苑綺 +余英时 余英時 +余荫 餘蔭 +余蓄 餘蓄 +余裕 餘裕 +余角 餘角 +余论 餘論 +余貾 餘貾 +余责 餘責 +余贤明 余賢明 +余车 余車 +余载 餘載 +余辉 餘輝 +余辜 餘辜 +余部 餘部 +余酲 餘酲 +余里 餘里 +余量 餘量 +余钱 餘錢 +余闰 餘閏 +余闲 餘閒 +余集 餘集 +余雪兰 余雪蘭 +余雪明 余雪明 +余零 餘零 +余震 餘震 +余霞 餘霞 +余音 餘音 +余音绕梁 餘音繞樑 +余韵 餘韻 +余项 餘項 +余额 餘額 +余风 餘風 +余食 餘食 +余香 餘香 +佚游 佚遊 +佚荡 佚蕩 +佛修根 佛修根 +佛克斯 佛克斯 +佛克纳 佛克納 +佛兰克林 佛蘭克林 +佛前佛后 佛前佛後 +佛历 佛曆 +佛尼亚克 佛尼亞克 +佛布兹 佛布茲 +佛曲 佛曲 +佛瑞克 佛瑞克 +佛瑞斯特怀特克 佛瑞斯特懷特克 +佛科摆 佛科擺 +佛罗棱萨 佛羅棱薩 +佛罗里达 佛羅里達 +佛罗里达州 佛羅里達州 +佛舍利 佛舍利 +佛蒙特 佛蒙特 +佛蒙特州 佛蒙特州 +佛里特 佛里特 +佛里特曼 佛里特曼 +佛钟 佛鐘 +佛雷克 佛雷克 +佛面上刮金 佛面上刮金 +作业平台 作業平臺 +作业系统 作業系統 +作了 作了 +作价 作價 +作倒了行市 作倒了行市 +作准 作準 +作出 作出 +作出来 作出來 +作出让步 作出讓步 +作别 作別 +作品里 作品裏 +作奸犯科 作奸犯科 +作好准备 作好準備 +作幸 作倖 +作庄 作莊 +作废后 作廢後 +作息时间表 作息時間表 +作恶 作惡 +作恶多端 作惡多端 +作曲 作曲 +作曲人 作曲人 +作曲家 作曲家 +作曲者 作曲者 +作育英才 作育英才 +作舍道旁 作舍道旁 +作舍道边 作舍道邊 +佞幸 佞幸 +你克制 你剋制 +你夸我逞 你誇我逞 +你干一杯 你乾一杯 +你干那杯 你乾那杯 +你念 你念 +你才子发昏 你纔子發昏 +你搜 你搜 +你斗了胆 你斗了膽 +你是为了 你是爲了 +你有千条妙计我有一定之规 你有千條妙計我有一定之規 +你系 你係 +佣中佼佼 傭中佼佼 +佣书 傭書 +佣人 傭人 +佣仆 傭僕 +佣作 傭作 +佣保 傭保 +佣兵 傭兵 +佣工 傭工 +佣懒 傭懶 +佣给 傭給 +佣耕 傭耕 +佣金 佣金 +佣金收益 佣金收益 +佣金费用 佣金費用 +佣钱 佣錢 +佣钿 佣鈿 +佥同 僉同 +佩挂 佩掛 +佩斯托瑞斯 佩斯托瑞斯 +佩脱拉克 佩脫拉克 +佳人才子 佳人才子 +佳冬 佳冬 +佳冬乡 佳冬鄉 +佳肴 佳餚 +佳致 佳致 +佳里 佳里 +佳里鎮 佳里鎮 +佳里镇 佳里鎮 +使其斗 使其鬥 +使出 使出 +使出来 使出來 +使团 使團 +使困扰 使困擾 +使困窘 使困窘 +使尽 使盡 +使心作幸 使心作倖 +使用价值 使用價值 +使用借贷 使用借貸 +使用者介面 使用者介面 +使用范围 使用範圍 +使节团 使節團 +使转向 使轉向 +侍仆 侍僕 +侍御 侍御 +侏儒症 侏儒症 +侏罗系 侏羅系 +侔德复载 侔德覆載 +供出 供出 +供制 供製 +供大于求 供大於求 +供应链 供應鏈 +供暖系统 供暖系統 +供水系统 供水系統 +供油系统 供油系統 +供电系统 供電系統 +供给制 供給制 +供给面 供給面 +供过于求 供過於求 +供销合作 供銷合作 +供销合作社 供銷合作社 +依个人 依個人 +依从关系 依從關係 +依依不舍 依依不捨 +依依难舍 依依難捨 +依头缕当 依頭縷當 +依存关系 依存關係 +依托 依託 +依据 依據 +依法炮制 依法炮製 +依然范特西 依然范特西 +依赞 依贊 +依附于 依附於 +侠气干云 俠氣干雲 +侥天之幸 僥天之倖 +侥幸 僥倖 +侥幸取胜 僥倖取勝 +侥幸获胜 僥倖獲勝 +侦听台 偵聽臺 +侦讯笔录 偵訊筆錄 +侧冲 側衝 +侧向 側向 +侧录 側錄 +侧撞防护系统 側撞防護系統 +侧链 側鏈 +侧面 側面 +侧面图 側面圖 +侧面性 側面性 +侨团 僑團 +侨汇 僑匯 +侨选代表 僑選代表 +侮蔑 侮蔑 +侯万户 侯萬戶 +侯彩凤 侯彩鳳 +侯胜茂 侯勝茂 +侵入岩 侵入岩 +侵占 侵佔 +侵占到 侵佔到 +侵占罪 侵佔罪 +侵哄 侵哄 +侵并 侵併 +侵蚀基准 侵蝕基準 +便了 便了 +便于 便於 +便吃干 便吃乾 +便宜不过当家 便宜不過當家 +便当 便當 +便当店 便當店 +便当盒 便當盒 +便益不失当家 便益不失當家 +便签 便籤 +便药 便藥 +便辟 便辟 +便面 便面 +便须 便須 +促发 促發 +促销价 促銷價 +俄克拉何马 俄克拉何馬 +俄克拉何马城 俄克拉何馬城 +俄克拉何马州 俄克拉何馬州 +俄克拉荷马州 俄克拉荷馬州 +俄制 俄製 +俄占 俄佔 +俄国共产党 俄國共產黨 +俄文系 俄文系 +俄罗斯党 俄羅斯黨 +俄语系 俄語系 +俊刮 俊刮 +俊杰 俊傑 +俊游 俊遊 +俊眼修眉 俊眼修眉 +俏丽短发 俏麗短髮 +俗念 俗念 +俘获 俘獲 +俚曲 俚曲 +保不准 保不準 +保丽龙板 保麗龍板 +保修期 保修期 +保养厂 保養廠 +保准 保準 +保发 保發 +保守党 保守黨 +保安团 保安團 +保安责任制 保安責任制 +保护状制 保護狀制 +保护范围 保護範圍 +保持克制 保持克制 +保持联系 保持聯繫 +保暖杯 保暖杯 +保温杯 保溫杯 +保甲制度 保甲制度 +保留价格 保留價格 +保留征收 保留徵收 +保留曲线 保留曲線 +保皇党 保皇黨 +保税工厂 保稅工廠 +保结制度 保結制度 +保证价格 保證價格 +保险杆 保險桿 +保险柜 保險櫃 +保险范围 保險範圍 +保险解开系统 保險解開系統 +保障范围 保障範圍 +信个 信個 +信丰 信豐 +信丰县 信豐縣 +信义计划 信義計劃 +信人调丢了瓢 信人調丟了瓢 +信口开合 信口開合 +信口胡说 信口胡說 +信号台 信號臺 +信号系统 信號系統 +信合社 信合社 +信嘴胡说 信嘴胡說 +信噪 信噪 +信天游 信天游 +信念 信念 +信息技术 信息技術 +信息系统 信息系統 +信托 信託 +信托公司 信託公司 +信托贸易 信托貿易 +信据 信據 +信步闲游 信步閒遊 +信汇 信匯 +信用合作 信用合作 +信笔涂鸦 信筆塗鴉 +信箱里 信箱裏 +信而有征 信而有徵 +信马游缰 信馬游繮 +俪采 儷采 +俭仆 儉僕 +俭朴 儉樸 +俭确之教 儉确之教 +修业 修業 +修业年限 修業年限 +修业期满 修業期滿 +修习 修習 +修书 修書 +修五脏庙 修五臟廟 +修仙 修仙 +修伊特 修伊特 +修修 修修 +修修补补 修修補補 +修养 修養 +修养成 修養成 +修函 修函 +修到 修到 +修剪 修剪 +修史 修史 +修名 脩名 +修和 修和 +修墓 修墓 +修士 修士 +修复 修復 +修女 修女 +修好 修好 +修威特 修威特 +修学 修學 +修定 修定 +修宪 修憲 +修宪案 修憲案 +修容 修容 +修己 修己 +修建 修建 +修得 修得 +修心养性 修心養性 +修成 修成 +修护 修護 +修护站 修護站 +修护队 修護隊 +修持 修持 +修指甲 修指甲 +修撰 修撰 +修改 修改 +修改为 修改爲 +修改后 修改後 +修改成 修改成 +修敬 脩敬 +修整 修整 +修文 修文 +修文偃武 修文偃武 +修文县 修文縣 +修斋 修齋 +修旧利废 修舊利廢 +修明 修明 +修曼德 修曼德 +修杰楷 修杰楷 +修枝 修枝 +修桥舖路 修橋舖路 +修桥补路 修橋補路 +修樾 脩樾 +修正 修正 +修正为 修正爲 +修正主义 修正主義 +修正期 修正期 +修正案 修正案 +修正档 修正檔 +修正法 修正法 +修正液 修正液 +修武县 修武縣 +修母画荻 修母畫荻 +修水 修水 +修水利 修水利 +修水县 修水縣 +修治 修治 +修润 脩潤 +修濬 修濬 +修炼 修煉 +修炼成仙 修煉成仙 +修版 修版 +修理 修理 +修理匠 修理匠 +修理厂 修理廠 +修理好 修理好 +修理店 修理店 +修理站 修理站 +修理费 修理費 +修理起来 修理起來 +修理部 修理部 +修的 修的 +修盖 修蓋 +修省 修省 +修眉 修眉 +修睦 修睦 +修短 修短 +修竹 修竹 +修筑 修築 +修筑公路 修築公路 +修筑工事 修築工事 +修筑工程 修築工程 +修筑道路 修築道路 +修练 修練 +修缮 修繕 +修缮费 修繕費 +修罗 修羅 +修置产室 修置產室 +修耕 修耕 +修胡刀 修鬍刀 +修脚 修腳 +修脯 脩脯 +修葺 修葺 +修行 修行 +修行人 修行人 +修补 修補 +修补匠 修補匠 +修褉 修褉 +修订 修訂 +修订历史 修訂歷史 +修订本 修訂本 +修订案 修訂案 +修订版 修訂版 +修词学 修詞學 +修课 修課 +修谨以俟 修謹以俟 +修谱 修譜 +修起 修起 +修起来 修起來 +修路 修路 +修蹄 修蹄 +修身 修身 +修身养性 修身養性 +修身齐家 修身齊家 +修车厂 修車廠 +修辞 修辭 +修辞学 修辭學 +修辞格 修辭格 +修边 修邊 +修边幅 修邊幅 +修造 修造 +修造厂 修造廠 +修道 修道 +修道人 修道人 +修道会 修道會 +修道士 修道士 +修道张 修道張 +修道院 修道院 +修配 修配 +修配厂 修配廠 +修金 脩金 +修长 修長 +修阻 修阻 +修面 修面 +修鞋匠 修鞋匠 +修饰 修飾 +修饰句 修飾句 +修饰字 修飾字 +修饰词 修飾詞 +修饰话 修飾話 +修饰语 修飾語 +修饰边幅 修飾邊幅 +修齐 修齊 +修龄 修齡 +俯冲 俯衝 +俯冲点 俯衝點 +俯冲角 俯衝角 +俯曲 俯曲 +俯首就范 俯首就範 +俱发 俱發 +俱收并蓄 俱收並蓄 +俱杯 俱杯 +俱舍师 俱舍師 +俱舍论 俱舍論 +俾资挹注 俾資挹注 +倍日并行 倍日並行 +倒了 倒了 +倒了八辈子楣 倒了八輩子楣 +倒了架 倒了架 +倒了架子 倒了架子 +倒了柴 倒了柴 +倒了核桃车子 倒了核桃車子 +倒价 倒價 +倒八字须 倒八字鬚 +倒出 倒出 +倒出去 倒出去 +倒出来 倒出來 +倒前倒后 倒前倒後 +倒台 倒臺 +倒吊 倒吊 +倒吊蜡烛 倒吊蠟燭 +倒向 倒向 +倒回 倒回 +倒回去 倒回去 +倒回来 倒回來 +倒屣奔出 倒屣奔出 +倒彩 倒彩 +倒念 倒唸 +倒悬挨命 倒懸捱命 +倒扣 倒扣 +倒扣针儿 倒扣針兒 +倒抽了一口气 倒抽了一口氣 +倒持干戈 倒持干戈 +倒挂 倒掛 +倒挂金钩 倒掛金鉤 +倒杯 倒杯 +倒杯水 倒杯水 +倒杯茶 倒杯茶 +倒板 倒板 +倒竖虎须 倒豎虎鬚 +倒绷孩儿 倒繃孩兒 +倒置干戈 倒置干戈 +倒载干戈 倒載干戈 +倒钟摆效应 倒鐘擺效應 +候虫 候蟲 +倚儿不当 倚兒不當 +倚多为胜 倚多爲勝 +倚托 倚托 +倚晴楼七种 倚晴樓七種 +倚闲 倚閑 +倚马千言 倚馬千言 +倛丑 倛醜 +借一步 借一步 +借不到 借不到 +借东风 借東風 +借个 借個 +借个火 借個火 +借主 借主 +借书 借書 +借书单 借書單 +借书证 借書證 +借了 借了 +借交报仇 借交報仇 +借人 借人 +借代 借代 +借令 借令 +借以 藉以 +借位 借位 +借住 借住 +借作 借作 +借使 借使 +借借 藉藉 +借借看 借借看 +借债 借債 +借债人 借債人 +借债度日 借債度日 +借光 借光 +借入方 借入方 +借入款 借入款 +借典 借典 +借出 借出 +借出去 借出去 +借出来 借出來 +借刀杀人 借刀殺人 +借到 借到 +借券 借券 +借剑杀人 借劍殺人 +借力 借力 +借助 藉助 +借助于 藉助於 +借势 借勢 +借卉 藉卉 +借单 借單 +借单儿 借單兒 +借去 借去 +借取 借取 +借口 藉口 +借古喻今 借古喻今 +借古讽今 借古諷今 +借名 借名 +借听于聋 借聽於聾 +借唱 借唱 +借喻 借喻 +借回 借回 +借回去 借回去 +借回来 借回來 +借地 借地 +借坐 借坐 +借契 借契 +借好 借好 +借妻 借妻 +借字 借字 +借字儿 借字兒 +借完 借完 +借客报仇 借客報仇 +借宿 借宿 +借宿一夜 借宿一夜 +借宿一晚 借宿一晚 +借寇兵 藉寇兵 +借寇兵赍盗粮 藉寇兵齎盜糧 +借寇恂 借寇恂 +借对 借對 +借寿 借壽 +借尸还魂 借屍還魂 +借弹 借彈 +借得 借得 +借手 藉手 +借手除敌 借手除敵 +借托 借托 +借抄 借抄 +借抽 借抽 +借招 借招 +借据 借據 +借提 借提 +借支 借支 +借放 借放 +借故 藉故 +借故推辞 藉故推辭 +借方 借方 +借方差额 借方差額 +借有 借有 +借机 藉機 +借条 借條 +借来 借來 +借来借去 借來借去 +借梯子下楼 借梯子下樓 +借槁 藉槁 +借款 借款 +借款人 借款人 +借此 藉此 +借此机会 藉此機會 +借水推船 借水推船 +借水行舟 借水行舟 +借满 借滿 +借火 借火 +借点 借點 +借球 借球 +借甚 藉甚 +借用 借用 +借由 藉由 +借的 借的 +借看 借看 +借看一下 借看一下 +借着 藉着 +借穿 借穿 +借端 藉端 +借端生事 藉端生事 +借箸 借箸 +借箸代筹 藉箸代籌 +借箸代谋 借箸代謀 +借米下得锅讨米下不得锅 借米下得鍋討米下不得鍋 +借约 借約 +借给 借給 +借腹生子 借腹生子 +借花献佛 借花獻佛 +借茶活捉 借茶活捉 +借草枕块 藉草枕塊 +借让 借讓 +借讬 借託 +借记卡 借記卡 +借词 藉詞 借詞 +借读 借讀 +借调 借調 +借贷 借貸 +借贷无门 借貸無門 +借贷资本 借貸資本 +借资 藉資 +借资挹注 借資挹注 +借走 借走 +借过 借過 +借过一下 借過一下 +借道 借道 +借酒三分醉 借酒三分醉 +借酒浇愁 借酒澆愁 +借酒装疯 借酒裝瘋 +借重 借重 +借鉴 借鑑 +借鑑 借鑑 +借钱 借錢 +借镜 借鏡 +借问 借問 +借阅 借閱 +借阅率 借閱率 +借韵 借韻 +借题 借題 +借题发挥 借題發揮 +借风使船 借風使船 +借齿牙 借齒牙 +倡条冶叶 倡條冶葉 +倦游 倦遊 +倦鸟余花 倦鳥餘花 +倪云林 倪雲林 +倪嗣冲 倪嗣沖 +倮虫 倮蟲 +债台高筑 債臺高築 +债权团 債權團 +值回票价 值回票價 +值得一干 值得一幹 +值得干 值得幹 +值得庆幸 值得慶幸 +值得注意 值得注意 +值得注意的是 值得注意的是 +值得称赞 值得稱讚 +值日表 值日表 +倾出 傾出 +倾向 傾向 +倾向于 傾向於 +倾向性 傾向性 +倾复重器 傾覆重器 +倾家尽产 傾家盡產 +倾家荡产 傾家蕩產 +倾尽 傾盡 +倾巢出动 傾巢出動 +倾巢而出 傾巢而出 +倾斜面 傾斜面 +倾杯 傾杯 +倾注 傾注 +倾箱倒柜 傾箱倒櫃 +倾耳注目 傾耳注目 +倾诉衷曲 傾訴衷曲 +偃仆 偃仆 +偃松 偃松 +偃武修文 偃武修文 +偃蹇困穷 偃蹇困窮 +假借 假借 +假借义 假借義 +假借字 假借字 +假借法 假借法 +假力于人 假力於人 +假发 假髮 +假叶 假葉 +假意周旋 假意周旋 +假托 假託 +假期忧郁症候群 假期憂鬱症候羣 +假药 假藥 +假面 假面 +假面具 假面具 +假面剧 假面劇 +偎干 偎乾 +偎干就湿 偎乾就溼 +偏了 偏了 +偏于 偏於 +偏信则暗 偏信則闇 +偏出 偏出 +偏后 偏後 +偏向 偏向 +偏回 偏回 +偏回去 偏回去 +偏回来 偏回來 +偏幸 偏倖 +偏才 偏才 +偏执症 偏執症 +偏暗 偏暗 +偏极光 偏極光 +偏极光镜 偏極光鏡 +偏极化 偏極化 +偏极滤光镜 偏極濾光鏡 +偏极镜 偏極鏡 +偏正式合成词 偏正式合成詞 +偏相关系数 偏相關係數 +偏重于 偏重於 +偕同 偕同 +做一天和尚撞一天钟 做一天和尚撞一天鐘 +做不了 做不了 +做了 做了 +做准备工作 做準備工作 +做出 做出 +做出事来 做出事來 +做出场 做出場 +做出好戏 做齣好戲 +做出来 做出來 +做好做恶 做好做惡 +做尽 做盡 +做庄 做莊 +做张做致 做張做致 +做得了 做得了 +做针线 做針線 +停了 停了 +停云 停雲 +停云慢步 停雲慢步 +停云落月 停雲落月 +停停当当 停停當當 +停制 停製 +停尸 停屍 +停尸房 停屍房 +停尸间 停屍間 +停当 停當 +停征 停徵 +停摆 停擺 +停板 停板 +停板制度 停板制度 +停表 停表 +偢采 偢采 +健康胜于财富 健康勝於財富 +健忘症 健忘症 +偶发 偶發 +偶发事件 偶發事件 +偶发性 偶發性 +偶合 偶合 +偷了 偷了 +偷出 偷出 +偷出去 偷出去 +偷出来 偷出來 +偷发 偷發 +偷合取容 偷合取容 +偷合苟容 偷合苟容 +偷回 偷回 +偷回去 偷回去 +偷回来 偷回來 +偷婆娘 偷婆娘 +偷尝禁果 偷嚐禁果 +偷得浮生半日闲 偷得浮生半日閒 +偷期暗会 偷期暗會 +偷梁换柱 偷樑換柱 +偷种 偷種 +偷鸡不着 偷雞不着 +偷鸡吊狗 偷雞吊狗 +偿回 償回 +偿回去 償回去 +偿回来 償回來 +偿得夙愿 償得夙願 +偿愿 償願 +傅克斯 傅克斯 +傅利叶 傅利葉 +傅匀余 傅勻余 +傅岩 傅巖 +傅彩 傅彩 +傅科摆 傅科擺 +傅立叶 傅立葉 +傅立叶变换 傅立葉變換 +傅粉施朱 傅粉施朱 +傅说版筑 傅說版築 +傅里叶 傅里葉 +傍个影儿 傍個影兒 +傍系 傍系 +傒幸 傒倖 +傢伙 傢伙 +傢伙座儿 傢伙座兒 +傥荡 儻蕩 +储备干部 儲備幹部 +储训人才 儲訓人才 +储训干部 儲訓幹部 +催并 催併 +催眠曲 催眠曲 +催眠术 催眠術 +催眠药 催眠藥 +催谷 催谷 +傲世轻才 傲世輕才 +傲睨万物 傲睨萬物 +傲霜斗雪 傲霜鬥雪 +傻大个 傻大個 +傻大个儿 傻大個兒 +傻里傻气 傻里傻氣 +像杯 像杯 +像赞 像贊 +僦舍 僦舍 +僮仆 僮僕 +僮御 僮御 +僮手指千 僮手指千 +僵事 僵事 +僵仆 僵仆 +僵住 僵住 +僵冷 僵冷 +僵化 僵化 +僵卧 僵臥 +僵固 僵固 +僵固性 僵固性 +僵尸 殭屍 +僵尸网络 僵屍網絡 +僵局 僵局 +僵持 僵持 +僵持不下 僵持不下 +僵掉 僵掉 +僵李代桃 僵李代桃 +僵死 僵死 +僵直 僵直 +僵直性 僵直性 +僵直性脊椎炎 僵直性脊椎炎 +僵硬 僵硬 +僵立 僵立 +僵臥 僵臥 +僵蚕 殭蠶 +儌幸 儌倖 +儒术 儒術 +儒略历 儒略曆 +儒略历史 儒略歷史 +儒略改革历 儒略改革曆 +儒略改革历史 儒略改革歷史 +儿不嫌母丑犬不怨主贫 兒不嫌母醜犬不怨主貧 +儿童台 兒童臺 +儿童团 兒童團 +兀术 兀朮 +允准 允准 +允当 允當 +元凶 元兇 +元后 元后 +元培医事技术学校 元培醫事技術學校 +元恶 元惡 +元恶大奸 元惡大奸 +元恶大憝 元惡大憝 +元曲 元曲 +元曲四大家 元曲四大家 +元秋 元秋 +元素周期表 元素週期表 +兄台 兄臺 +兄弟党 兄弟黨 +充发 充發 +充场面 充場面 +充当 充當 +充斥市面 充斥市面 +充满了 充滿了 +充类至尽 充類至盡 +充能干 充能幹 +充门面 充門面 +充饥 充飢 +充饥止渴 充飢止渴 +充饥画饼 充飢畫餅 +兆个 兆個 +兆丰 兆豐 +兆丰金 兆豐金 +兆丰银 兆豐銀 +兆余 兆餘 +先义后利 先義後利 +先了 先了 +先了一步 先了一步 +先于 先於 +先人后己 先人後己 +先修班 先修班 +先借 先借 +先公后私 先公後私 +先出 先出 +先出去 先出去 +先出来 先出來 +先占 先佔 +先发 先發 +先发制人 先發制人 +先发投手 先發投手 +先发投手群 先發投手羣 +先后 先後 先后 +先后倒置 先後倒置 +先后顺序 先後順序 +先向 先向 +先回 先回 +先回到 先回到 +先回去 先回去 +先回来 先回來 +先声后实 先聲後實 +先天下之忧而忧后天下之乐而乐 先天下之憂而憂后天下之樂而樂 +先天不足后天失调 先天不足後天失調 +先守后攻 先守後攻 +先小人后君子 先小人後君子 +先尝 先嚐 +先干为敬 先乾爲敬 +先忧后乐 先憂後樂 +先念 先念 +先意承志 先意承志 +先攻后守 先攻後守 +先斩后奏 先斬後奏 +先斩后闻 先斬後聞 +先期录音 先期錄音 +先来后上 先來後上 +先来后下 先來後下 +先来后到 先來後到 +先盛后衰 先盛後衰 +先礼后兵 先禮後兵 +先签 先簽 +先缺后空 先缺後空 +先花后果 先花後果 +先苦后甘 先苦後甘 +先行后闻 先行後聞 +先赢后输 先贏後輸 +先进先出 先進先出 +先进后出 先進後出 +先采 先採 +先锋模范作用 先鋒模範作用 +先难后获 先難後獲 +先驱新党 先驅新黨 +光了 光了 +光了了 光了了 +光二极管 光二極管 +光价 光價 +光光荡荡 光光蕩蕩 +光冲量 光衝量 +光出律 光出律 +光出溜 光出溜 +光前绝后 光前絕後 +光前耀后 光前耀後 +光前裕后 光前裕後 +光卤石 光鹵石 +光发送器 光發送器 +光可鉴人 光可鑑人 +光合 光合 +光合作用 光合作用 +光合细菌 光合細菌 +光向 光向 +光周期 光周期 +光圈范围 光圈範圍 +光复 光復 +光复乡 光復鄉 +光复会 光復會 +光复南路 光復南路 +光复国土 光復國土 +光复国小 光復國小 +光复旧京 光復舊京 +光复旧物 光復舊物 +光复节 光復節 +光复路 光復路 +光学字符识别 光學字符識別 +光学录音 光學錄音 +光学系统 光學系統 +光导纤维 光導纖維 +光度表 光度表 +光彩 光彩 +光彩夺目 光彩奪目 +光彩耀眼 光彩耀眼 +光彩起来 光彩起來 +光念 光念 +光效应艺术 光效應藝術 +光敏症 光敏症 +光明云 光明雲 +光明党 光明黨 +光明面 光明面 +光杆 光桿 +光杆儿 光桿兒 +光杆司令 光桿司令 +光杠 光槓 +光板儿 光板兒 +光烟雾 光煙霧 +光焰万丈 光焰萬丈 +光电二极 光電二極 +光电二极体 光電二極體 +光电二极管 光電二極管 +光碟杂志 光碟雜誌 +光碟柜 光碟櫃 +光纤 光纖 +光纤分布式数据介面 光纖分佈式數據介面 +光纤分布数据接口 光纖分佈數據接口 +光纤分散式资料介面 光纖分散式資料介面 +光纤接口 光纖接口 +光纤电缆 光纖電纜 +光纤维 光纖維 +光纤衰减 光纖衰減 +光纤通信 光纖通信 +光纤通信系统 光纖通信系統 +光纤通讯 光纖通訊 +光纤通讯干道网路系统 光纖通訊幹道網路系統 +光纤飞弹 光纖飛彈 +光能合成 光能合成 +光脊梁 光脊樑 +光致致 光緻緻 +光艺术 光藝術 +光芒万丈 光芒萬丈 +光范围 光範圍 +光表 光表 +光辉典范 光輝典範 +光采 光采 +光面 光面 +光面内质网 光面內質網 +光面子 光面子 +克东 克東 +克东县 克東縣 +克丝钳子 克絲鉗子 +克丽丝 克麗絲 +克亚 克亞 +克什 克什 +克什克腾 克什克騰 +克什克腾旗 克什克騰旗 +克什米尔 克什米爾 +克仑特罗 克侖特羅 +克伦克 克倫克 +克伦奇 克倫奇 +克伦威尔 克倫威爾 +克佩罗 克佩羅 +克俭 克儉 +克俭克勤 克儉克勤 +克克 剋剋 +克克尔 克克爾 +克兰诗 克蘭詩 +克兰达尔 克蘭達爾 +克分子 克分子 +克利 克利 +克利斯 克利斯 +克利斯提 克利斯提 +克利福洛 克利福洛 +克制 剋制 +克制不了 剋制不了 +克制不住 剋制不住 +克剥 剋剝 +克劳 克勞 +克劳佛 克勞佛 +克劳修斯 克勞修斯 +克劳可 克勞可 +克劳契 克勞契 +克劳德 克勞德 +克劳斯 克勞斯 +克劳福 克勞福 +克劳福德 克勞福德 +克劳芙特 克勞芙特 +克勒 克勒 +克勒拉省 克勒拉省 +克勒特 克勒特 +克勤 克勤 +克勤克俭 克勤克儉 +克卜勒 克卜勒 +克卜勒定律 克卜勒定律 +克原子 克原子 +克啬 剋嗇 +克基拉岛 克基拉島 +克复 克復 +克夫 剋夫 +克娄巴特拉 克婁巴特拉 +克孜勒苏 克孜勒蘇 +克孜勒苏地区 克孜勒蘇地區 +克孜勒苏柯尔克孜自治州 克孜勒蘇柯爾克孜自治州 +克孜勒苏河 克孜勒蘇河 +克孜尔千佛洞 克孜爾千佛洞 +克孜尔尕哈 克孜爾尕哈 +克孜尔尕哈烽火台 克孜爾尕哈烽火臺 +克宁 克寧 +克定 克定 +克家 克家 +克家子 克家子 +克尔 克爾 +克尔白 克爾白 +克尽 克盡 +克尽厥职 克盡厥職 +克尽夫道 克盡夫道 +克尽妇道 克盡婦道 +克尽己职 克盡己職 +克山 克山 +克山县 克山縣 +克己 克己 +克己主义 克己主義 +克己复礼 克己復禮 +克己奉公 克己奉公 +克强 克強 +克当一面 克當一面 +克当量 克當量 +克意 剋意 +克扣 剋扣 +克拉 克拉 +克拉克 克拉克 +克拉兹 克拉茲 +克拉夫特 克拉夫特 +克拉姆 克拉姆 +克拉本 克拉本 +克拉玛依 克拉瑪依 +克捷 克捷 +克敌 克敵 +克敌制胜 克敵制勝 +克敦孝行 克敦孝行 +克文 克文 +克斯 克斯 +克日 剋日 +克明 克明 +克星 剋星 +克服 克服 +克服不了 克服不了 +克服困难 克服困難 +克朗 克朗 +克朗代克 克朗代克 +克期 剋期 +克来汀症 克來汀症 +克林伊斯威特 克林伊斯威特 +克林德 克林德 +克林斯曼 克林斯曼 +克林霉素 克林黴素 +克林顿 克林頓 +克柔 克柔 +克核 剋核 +克格勃 克格勃 +克死 剋死 +克汀病 克汀病 +克汗 克汗 +克沙奇病毒 克沙奇病毒 +克洛 克洛 +克洛尔 克洛爾 +克洛斯 克洛斯 +克洛格 克洛格 +克流感 克流感 +克瑞强 克瑞強 +克瑞斯波 克瑞斯波 +克瑞格 克瑞格 +克绍箕裘 克紹箕裘 +克绳祖武 克繩祖武 +克罗地亚 克羅地亞 +克罗地亚共和国 克羅地亞共和國 +克罗地亚语 克羅地亞語 +克罗埃 克羅埃 +克罗埃西亚 克羅埃西亞 +克罗德 克羅德 +克罗恩科 克羅恩科 +克罗诺斯 克羅諾斯 +克罗齐 克羅齊 +克耳文 克耳文 +克耶族 克耶族 +克耶邦 克耶邦 +克苦耐劳 克苦耐勞 +克药 克藥 +克莉 克莉 +克莉丝 克莉絲 +克莉丝汀 克莉絲汀 +克莉丝汀娜 克莉絲汀娜 +克莉丝特丝 克莉絲特絲 +克莉兰柯 克莉蘭柯 +克莉奥佩特拉 克莉奧佩特拉 +克莉斯蒂纳 克莉斯蒂納 +克莉芭丝 克莉芭絲 +克莱 克萊 +克莱伦斯宫 克萊倫斯宮 +克莱儿 克萊兒 +克莱因 克萊因 +克莱恩 克萊恩 +克莱斯勒 克萊斯勒 +克莱斯勒汽车公司 克萊斯勒汽車公司 +克莱曼 克萊曼 +克莱柏 克萊柏 +克莱査克 克萊查克 +克莱格 克萊格 +克莱蒙特 克萊蒙特 +克莱门斯 克萊門斯 +克莱顿 克萊頓 +克萨斯州 克薩斯州 +克落 剋落 +克蕾儿 克蕾兒 +克薄 剋薄 +克虏伯 克虜伯 +克西 克西 +克让 克讓 +克谐 克諧 +克赖斯特彻奇 克賴斯特徹奇 +克郎 克郎 +克郡 克郡 +克里 克里 +克里丝蒂娃 克里絲蒂娃 +克里契科 克里契科 +克里奥尔语 克里奧爾語 +克里姆林 克里姆林 +克里姆林宫 克里姆林宮 +克里姆林杯 克里姆林杯 +克里岛 克里島 +克里斯 克里斯 +克里斯伊凡 克里斯伊凡 +克里斯塔基斯 克里斯塔基斯 +克里斯托 克里斯托 +克里斯托弗 克里斯托弗 +克里斯普 克里斯普 +克里斯汀 克里斯汀 +克里斯汀贝尔 克里斯汀貝爾 +克里斯蒂安 克里斯蒂安 +克里斯蒂安松 克里斯蒂安松 +克里普斯 克里普斯 +克里木 克里木 +克里木半岛 克里木半島 +克里木战争 克里木戰爭 +克里梅 克里梅 +克里特 克里特 +克里特克 克里特克 +克里特克里岛 克里特克里島 +克里特岛 克里特島 +克里米亚 克里米亞 +克里米亚半岛 克里米亞半島 +克里米亚战争 克里米亞戰爭 +克里蒙梭 克里蒙梭 +克里门 克里門 +克里门特 克里門特 +克队 克隊 +克隆 克隆 +克隆人 克隆人 +克隆尼 克隆尼 +克隆技术 克隆技術 +克隆斯台 克隆斯臺 +克隆氏病 克隆氏病 +克难 克難 +克难街 克難街 +克难运动 克難運動 +克雅氏症 克雅氏症 +克雷伯氏菌属 克雷伯氏菌屬 +克雷因 克雷因 +克雷姆凯 克雷姆凱 +克雷尼尔 克雷尼爾 +克雷文 克雷文 +克雷斯 克雷斯 +克雷斯吉 克雷斯吉 +克雷格 克雷格 +克雷门斯 克雷門斯 +克雷默 克雷默 +克霉唑 克黴唑 +克顺克卑 克順克卑 +克食 克食 +克鲁 克魯 +克鲁伦河 克魯倫河 +克鲁克斯 克魯克斯 +克鲁兹 克魯茲 +克鲁利 克魯利 +克鲁尼 克魯尼 +克鲁斯 克魯斯 +克鲁斯州 克魯斯州 +克鲁格 克魯格 +克麦洛伏 克麥洛伏 +免不了 免不了 +免于 免於 +免参 免參 +免征 免徵 +免疫系统 免疫系統 +免胄 免冑 +免试升学 免試升學 +免试升高中班 免試升高中班 +兔尽狗烹 兔盡狗烹 +党三役 黨三役 +党中央 黨中央 +党主席 黨主席 +党义 黨義 +党争 黨爭 +党产 黨產 +党人 黨人 +党代会 黨代會 +党代表 黨代表 +党伍 黨伍 +党內 黨內 +党內人士 黨內人士 +党內初选 黨內初選 +党內斗争 黨內鬥爭 +党八股 黨八股 +党公职 黨公職 +党军 黨軍 +党刊 黨刊 +党务 黨務 +党参 黨蔘 +党友 黨友 +党史 黨史 +党同伐异 黨同伐異 +党名 黨名 +党员 黨員 +党员大会 黨員大會 +党员证 黨員證 +党团 黨團 +党团员 黨團員 +党国 黨國 +党国元老 黨國元老 +党外 黨外 +党外人士 黨外人士 +党太尉 党太尉 +党太尉吃匾食 党太尉吃匾食 +党委 黨委 +党委书记 黨委書記 +党委会 黨委會 +党小组 黨小組 +党工 黨工 +党徒 黨徒 +党徽 黨徽 +党怀英 党懷英 +党性 黨性 +党总支 黨總支 +党报 黨報 +党支书 党支書 +党政 黨政 +党政军 黨政軍 +党政机关 黨政機關 +党旗 黨旗 +党校 黨校 +党格 黨格 +党棍 黨棍 +党歌 黨歌 +党法 黨法 +党派 黨派 +党派集会 黨派集會 +党的基本 黨的基本 +党祸 黨禍 +党禁 黨禁 +党章 黨章 +党籍 黨籍 +党籍碑 黨籍碑 +党纪 黨紀 +党纪国法 黨紀國法 +党纲 黨綱 +党组 黨組 +党羽 黨羽 +党职 黨職 +党营 黨營 +党见 黨見 +党言 黨言 +党论 黨論 +党证 黨證 +党课 黨課 +党费 黨費 +党进 党進 +党部 黨部 +党锢 黨錮 +党锢之祸 黨錮之禍 +党阀 黨閥 +党鞭 黨鞭 +党項 党項 +党项 党項 +党项族 党項族 +党风 黨風 +党魁 黨魁 +党龄 黨齡 +兜肚断了带子 兜肚斷了帶子 +入不支出 入不支出 +入不敷出 入不敷出 +入主出奴 入主出奴 +入伍须知 入伍須知 +入伙 入夥 +入党 入黨 +入冬 入冬 +入出境 入出境 +入出境管理局 入出境管理局 +入口匝道号志管制 入口匝道號誌管制 +入团 入團 +入境签证 入境簽證 +入夜后 入夜後 +入室升堂 入室升堂 +入帘 入簾 +入托 入托 +入秋 入秋 +入舍 入舍 +入药 入藥 +內出血 內出血 +內分泌系统 內分泌系統 +內制作 內製作 +內务柜 內務櫃 +內外交困 內外交困 +內外向包办 內外向包辦 +內外姻党 內外姻黨 +內外并重 內外並重 +內婚制 內婚制 +內布拉斯 內布拉斯 +內布拉斯加 內布拉斯加 +內心里 內心裏 +內掌柜的 內掌櫃的 +內服药 內服藥 +內科手术 內科手術 +內脏器官移植 內臟器官移植 +內蒙古 內蒙古 +內蒙古自治区 內蒙古自治區 +內部联系 內部聯繫 +內阁制 內閣制 +全世界无产者联合起来 全世界無產者聯合起來 +全体同仁 全體同仁 +全党 全黨 +全党全军 全黨全軍 +全党同志 全黨同志 +全军复没 全軍覆沒 +全军复灭 全軍覆滅 +全出 全出 +全出去 全出去 +全出来 全出來 +全台 全臺 +全台湾 全臺灣 +全向 全向 +全国不分区代表 全國不分區代表 +全国人民代表大会 全國人民代表大會 +全国人民代表大会常务委员会 全國人民代表大會常務委員會 +全国代表大会 全國代表大會 +全国劳动模范 全國勞動模範 +全国同胞 全國同胞 +全国大会党 全國大會黨 +全国教育资讯服务系统 全國教育資訊服務系統 +全国纪录 全國紀錄 +全国范围 全國範圍 +全场一致 全場一致 +全尸 全屍 +全干 全乾 +全当 全當 +全录 全錄 +全彩 全綵 +全彩干式印表机 全彩乾式印表機 +全所同仁 全所同仁 +全才 全才 +全托 全託 +全挂子 全掛子 +全斗焕 全斗煥 +全无准备 全無準備 +全日制 全日制 +全权代表 全權代表 +全校同学 全校同學 +全然不同 全然不同 +全班同学 全班同學 +全球位置测定系统 全球位置測定系統 +全球卫星导航系统 全球衛星導航系統 +全球发展中心 全球發展中心 +全球定位系统 全球定位系統 +全球定位系统卫星测量 全球定位系統衛星測量 +全球气候升温 全球氣候升溫 +全球环境变迁资讯系统 全球環境變遷資訊系統 +全球范围 全球範圍 +全盘托出 全盤托出 +全神灌注 全神灌注 +全神贯注 全神貫注 +全票价 全票價 +全程参加 全程參加 +全系列 全系列 +全系统 全系統 +全线出击 全線出擊 +全缘叶 全緣葉 +全胜 全勝 +全胜记录 全勝記錄 +全范围 全範圍 +全裂叶 全裂葉 +全谷物 全穀物 +全面 全面 +全面包围 全面包圍 +全面包裹 全面包裹 +全面发展 全面發展 +全面实施 全面實施 +全面性 全面性 +全面战争 全面戰爭 +全面禁止 全面禁止 +全面禁止核试验条约 全面禁止核試驗條約 +全面规划 全面規劃 +全面进行 全面進行 +八万 八萬 +八万一千 八萬一千 +八万四千法门 八萬四千法門 +八万多 八萬多 +八万大藏经 八萬大藏經 +八个 八個 +八仙桌上摆夜壶 八仙桌上擺夜壺 +八位元个人电脑 八位元個人電腦 +八余 八餘 +八克 八克 +八军团 八軍團 +八出祁山 八出祁山 +八十天环游地球 八十天環遊地球 +八十种好 八十種好 +八千里 八千里 +八厂 八廠 +八只 八隻 +八台 八臺 +八周 八週 +八周年 八週年 +八大胡同 八大胡同 +八天后 八天後 +八字方针 八字方針 +八字胡 八字鬍 +八字胡须 八字鬍鬚 +八小时制 八小時制 +八小时工作制 八小時工作制 +八扎 八紮 +八折 八折 +八斗 八斗 +八斗之才 八斗之才 +八斗子 八斗子 +八斗才 八斗才 +八斗陈思 八斗陳思 +八旗制度 八旗制度 +八极 八極 +八极拳 八極拳 +八步周行 八步周行 +八点钟 八點鐘 +八珍咸粥 八珍鹹粥 +八百万 八百萬 +八百多万 八百多萬 +八秒钟 八秒鐘 +八级工资制 八級工資制 +八蜡 八蜡 +八表 八表 +八辟 八辟 +八进制 八進制 +八里 八里 +八里乡 八里鄉 +八面 八面 +八面体 八面體 +八面光 八面光 +八面受敌 八面受敵 +八面圆通 八面圓通 +八面威风 八面威風 +八面玲珑 八面玲瓏 +八面见光 八面見光 +八面锋 八面鋒 +公之于众 公之於衆 +公了 公了 +公仆 公僕 +公仔面 公仔麪 +公价 公價 +公众参与 公衆參與 +公余 公餘 +公使团 公使團 +公克 公克 +公公向孙子磕头 公公向孫子磕頭 +公共休闲环境 公共休閒環境 +公共关系 公共關係 +公共团体 公共團體 +公共支出 公共支出 +公出 公出 +公切面 公切面 +公制 公制 +公制单位 公制單位 +公升 公升 +公历 公曆 +公厘 公釐 +公合 公合 +公同 公同 +公同共有 公同共有 +公听并观 公聽並觀 +公告价格 公告價格 +公告地价 公告地價 +公墓里放鞭炮 公墓裏放鞭炮 +公孙丑 公孫丑 +公孙大娘 公孫大娘 +公定价 公定價 +公定价格 公定價格 +公布 公佈 +公布于众 公佈於衆 +公布出来 公佈出來 +公布栏 公佈欄 +公干 公幹 +公平合理 公平合理 +公广集团 公廣集團 +公开出来 公開出來 +公开场合 公開場合 +公才公望 公才公望 +公报私仇 公報私仇 +公据 公據 +公斗 公斗 +公方代表 公方代表 +公明党 公明黨 +公有制 公有制 +公民表决 公民表決 +公然表示 公然表示 +公用征收 公用徵收 +公用限制 公用限制 +公益彩券 公益彩券 +公私两尽 公私兩盡 +公私合营 公私合營 +公羊春秋 公羊春秋 +公认会计准 公認會計准 +公诸于世 公諸於世 +公诸同好 公諸同好 +公路个人赛 公路個人賽 +公里 公里 +公里数 公里數 +公里时 公里時 +公门之中好修行 公門之中好修行 +公门修行 公門修行 +公门里好修行 公門裏好修行 +六万 六萬 +六个 六個 +六余 六餘 +六冲 六沖 +六出奇计 六出奇計 +六出祁山 六出祁山 +六厂 六廠 +六发 六發 +六只 六隻 +六台 六臺 +六合 六合 +六合区 六合區 +六合彩 六合彩 +六合拳 六合拳 +六合路 六合路 +六周 六週 +六周年 六週年 +六回 六回 +六天后 六天後 +六度万行 六度萬行 +六弦 六絃 +六彩 六彩 +六才子书 六才子書 +六扎 六紮 +六折 六折 +六星彩 六星彩 +六曲 六曲 +六极 六極 +六欲 六慾 +六点钟 六點鐘 +六百万 六百萬 +六百个 六百個 +六百多万 六百多萬 +六祖坛经 六祖壇經 +六种 六種 +六谷 六穀 +六通四辟 六通四辟 +六道轮回 六道輪迴 +六面 六面 +六面体 六面體 +六须鲇 六鬚鮎 +六须鲶 六鬚鮎 +兰克 蘭克 +兰台 蘭臺 +兰台令史 蘭臺令史 +兰台公子 蘭臺公子 +兰台石室 蘭臺石室 +兰叶描 蘭葉描 +兰叶撇 蘭葉撇 +兰摧玉折 蘭摧玉折 +兰摧蕙折 蘭摧蕙折 +兰秋 蘭秋 +兰艾同烬 蘭艾同燼 +兰艾同焚 蘭艾同焚 +兰花烟 蘭花煙 +兰里老太太 蘭里老太太 +共产主义青年团 共產主義青年團 +共产党 共產黨 +共产党人 共產黨人 +共产党员 共產黨員 +共产党宣言 共產黨宣言 +共产党政府 共產黨政府 +共产党部队 共產黨部隊 +共产制 共產制 +共产集团 共產集團 +共享计划 共享計劃 +共价 共價 +共价键 共價鍵 +共党 共黨 +共出 共出 +共发射极 共發射極 +共同 共同 +共同交际语 共同交際語 +共同代理 共同代理 +共同代表 共同代表 +共同企业 共同企業 +共同体 共同體 +共同保证 共同保證 +共同决议案 共同決議案 +共同利益 共同利益 +共同努力 共同努力 +共同基金 共同基金 +共同市场 共同市場 +共同性 共同性 +共同感觉 共同感覺 +共同正犯 共同正犯 +共同海损 共同海損 +共同点 共同點 +共同社 共同社 +共同科目 共同科目 +共同筛选 共同篩選 +共同管道 共同管道 +共同纲领 共同綱領 +共同经营 共同經營 +共同继承 共同繼承 +共同被告人 共同被告人 +共同诉讼 共同訴訟 +共同语 共同語 +共同财产 共同財產 +共同财产制 共同財產制 +共同趋向 共同趨向 +共同运销 共同運銷 +共同通讯社 共同通訊社 +共同闸道介面 共同閘道介面 +共和党 共和黨 +共和党人 共和黨人 +共和党籍 共和黨籍 +共和制 共和制 +共和历 共和曆 +共和历史 共和歷史 +共基极 共基極 +共审制度 共審制度 +共御外侮 共禦外侮 +共挽鹿车 共挽鹿車 +共摆 共擺 +共游 共遊 +共获 共獲 +共轭不尽根 共軛不盡根 +共轭双曲线 共軛雙曲線 +共轭复数 共軛複數 +共通意志 共通意志 +共铲 共剷 +共青团 共青團 +共餐青年团 共餐青年團 +关不了 關不了 +关个 關個 +关了 關了 +关了钉儿 關了釘兒 +关于 關於 +关云长 關雲長 +关出 關出 +关台 關臺 +关合 關合 +关同 關同 +关回 關回 +关回去 關回去 +关山万里 關山萬里 +关岭布依族苗族自治县 關嶺布依族苗族自治縣 +关岳 關岳 +关弓与我确 關弓與我确 +关征 關征 +关念 關念 +关怀面 關懷面 +关注 關注 +关注度 關注度 +关税同盟 關稅同盟 +关系 關係 +关系人 關係人 +关系代名词 關係代名詞 +关系企业 關係企業 +关系到 關係到 +关系命题 關係命題 +关系奖 關係獎 +关系密切 關係密切 +关系式 關係式 +关系户 關係戶 +关系法 關係法 +关系着 關係着 +关系融洽 關係融洽 +关系词 關係詞 +关系调 關係調 +关系运算 關係運算 +关系部 關係部 +关节面 關節面 +兴云作雨 興雲作雨 +兴云作雾 興雲作霧 +兴云吐雾 興雲吐霧 +兴云布雨 興雲佈雨 +兴云致雨 興雲致雨 +兴会淋漓 興會淋漓 +兴修 興修 +兴冲冲 興沖沖 +兴叹 興嘆 +兴复 興復 +兴复不浅 興復不淺 +兴尽 興盡 +兴尽悲来 興盡悲來 +兴尽意阑 興盡意闌 +兴旺发达 興旺發達 +兴筑 興築 +兴致 興致 +兴致勃勃 興致勃勃 +兴致勃发 興致勃發 +兴致盎然 興致盎然 +兴致索然 興致索然 +兴隆台 興隆臺 +兴隆台区 興隆臺區 +兴高彩烈 興高彩烈 +兴高采烈 興高采烈 +兵出无名 兵出無名 +兵制 兵制 +兵器术 兵器術 +兵团 兵團 +兵尽器竭 兵盡器竭 +兵尽矢穷 兵盡矢窮 +兵工厂 兵工廠 +兵工厂队 兵工廠隊 +兵疲马困 兵疲馬困 +兵种 兵種 +兵籍表 兵籍表 +兵马司倒了墙 兵馬司倒了牆 +其从如云 其從如雲 +其他支出 其他支出 +其余 其餘 +其八九只 其八九只 +其势凶凶 其勢兇兇 +其后 其後 +其核 其核 +其次辟地 其次辟地 +具体计划 具體計劃 +典借 典借 +典制 典制 +典型示范 典型示範 +典当 典當 +典据 典據 +典章制度 典章制度 +典范 典範 +典范性 典範性 +典范长存 典範長存 +兹核 茲核 +养了 養了 +养儿待老积谷防饥 養兒待老積穀防饑 +养儿防老积谷防饥 養兒防老積穀防飢 +养兵千日 養兵千日 +养兵千日用兵一时 養兵千日用兵一時 +养兵千日用在一时 養兵千日用在一時 +养兵千日用在一朝 養兵千日用在一朝 +养军千日 養軍千日 +养军千日用军一时 養軍千日用軍一時 +养军千日用在一时 養軍千日用在一時 +养军千日用在一朝 養軍千日用在一朝 +养出 養出 +养发 養髮 +养娘 養娘 +养婆娘 養婆娘 +养子防老积谷防饥 養子防老積穀防飢 +养家糊口 養家餬口 +养小防老积谷防饥 養小防老積穀防饑 +养志 養志 +养性修真 養性修真 +养老鼠咬布袋 養老鼠咬布袋 +养颜有术 養顏有術 +兼了 兼了 +兼修 兼修 +兼听则明偏信则暗 兼聽則明偏信則暗 +兼容并包 兼容幷包 +兼容并蓄 兼容幷蓄 +兼并 兼併 +兼并与收购 兼併與收購 +兼收并蓄 兼收幷蓄 +兼筹并顾 兼籌幷顧 +兽医系 獸醫系 +兽奸 獸姦 +兽心人面 獸心人面 +兽性大发 獸性大發 +兽欲 獸慾 +兽药 獸藥 +内出血 內出血 +内制 內製 +内参 內參 +内向 內向 +内哄 內鬨 +内在几何 內在幾何 +内在几何学 內在幾何學 +内塔尼亚胡 內塔尼亞胡 +内奸 內奸 +内容管理系统 內容管理系統 +内布拉斯加 內布拉斯加 +内布拉斯加州 內布拉斯加州 +内忧外困 內憂外困 +内扣 內扣 +内斗 內鬥 +内松外紧 內鬆外緊 +内核 內核 +内紧外松 內緊外鬆 +内细胞团 內細胞團 +内脏 內臟 +内蒙 內蒙 +内蒙古 內蒙古 +内蒙古大学 內蒙古大學 +内蒙古自治区 內蒙古自治區 +内部斗争 內部鬥爭 +内面包的 內面包的 +冈田准 岡田准 +冉冉上升 冉冉上升 +冉有仆 冉有僕 +冊叶 冊葉 +再于 再於 +再借 再借 +再借不难 再借不難 +再冲 再衝 +再出 再出 +再出去 再出去 +再出来 再出來 +再出现 再出現 +再制 再製 +再制品 再製品 +再制盐 再製鹽 +再制纸 再製紙 +再发 再發 +再发生 再發生 +再发见 再發見 +再向 再向 +再回 再回 +再回到 再回到 +再回去 再回去 +再回来 再回來 +再干 再幹 +再干一杯 再乾一杯 +再念 再念 +再斗一斗 再鬥一鬥 +再来一个 再來一個 +再生制动 再生制動 +再生纤维 再生纖維 +再种 再種 +再见面 再見面 +再转复 再轉復 +再造手术 再造手術 +再长爹娘 再長爹孃 +冒了 冒了 +冒儿咕冬 冒兒咕冬 +冒出 冒出 +冒出来 冒出來 +冒升 冒升 +冒烟 冒煙 +冒烟突火 冒煙突火 +冒镝当锋 冒鏑當鋒 +冗余 冗餘 +写不出 寫不出 +写个 寫個 +写了 寫了 +写出 寫出 +写出去 寫出去 +写出来 寫出來 +写回 寫回 +写回去 寫回去 +写回来 寫回來 +写字台 寫字檯 +写真合成海报 寫真合成海報 +军事管制 軍事管制 +军制 軍制 +军品出口领导小组 軍品出口領導小組 +军团 軍團 +军团杆菌 軍團桿菌 +军团菌 軍團菌 +军团菌病 軍團菌病 +军官团 軍官團 +军政当局 軍政當局 +军民合作 軍民合作 +军种 軍種 +军舰岩 軍艦岩 +军阀割据 軍閥割據 +军队克制 軍隊剋制 +农业合作 農業合作 +农业合作化 農業合作化 +农业技术 農業技術 +农业生产合作社 農業生產合作社 +农业生产技术 農業生產技術 +农产品平准基金 農產品平準基金 +农作曲 農作曲 +农作物品种 農作物品種 +农化系 農化系 +农历 農曆 +农历年 農曆年 +农历新年 農曆新年 +农地重划 農地重劃 +农复会 農復會 +农奴制 農奴制 +农奴制度 農奴制度 +农家品种 農家品種 +农庄 農莊 +农技团 農技團 +农机系 農機系 +农村合作化 農村合作化 +农村家庭联产承包责任制 農村家庭聯產承包責任制 +农民党 農民黨 +农民历 農民曆 +农民历史 農民歷史 +农民团体 農民團體 +农经系 農經系 +农舍 農舍 +农艺系 農藝系 +农药 農藥 +农药商 農藥商 +冠世之才 冠世之才 +冠军杯 冠軍盃 +冠子虫 冠子蟲 +冠状动脉旁路移植手术 冠狀動脈旁路移植手術 +冠状动脉旁通手术 冠狀動脈旁通手術 +冠状动脉硬化症 冠狀動脈硬化症 +冠盖云集 冠蓋雲集 +冠盖如云 冠蓋如雲 +冠胄 冠冑 +冤仇 冤仇 +冥凌 冥淩 +冥凌浃行 冥淩浹行 +冥子里 冥子裏 +冥蒙 冥濛 +冬不拉 冬不拉 +冬事 冬事 +冬令 冬令 +冬令救济 冬令救濟 +冬令进补 冬令進補 +冬储 冬儲 +冬冬 鼕鼕 +冬冬鼓 鼕鼕鼓 +冬凌 冬凌 +冬初 冬初 +冬夏 冬夏 +冬天 冬天 +冬天里 冬天裏 +冬奥会 冬奧會 +冬字头 冬字頭 +冬季 冬季 +冬季世界 冬季世界 +冬季作物 冬季作物 +冬季档 冬季檔 +冬季节 冬季節 +冬季赛 冬季賽 +冬季运动 冬季運動 +冬学 冬學 +冬宫 冬宮 +冬小麦 冬小麥 +冬山 冬山 +冬山乡 冬山鄉 +冬山河 冬山河 +冬心 冬心 +冬扇夏炉 冬扇夏爐 +冬日 冬日 +冬日可爱 冬日可愛 +冬日里 冬日裏 +冬暖 冬暖 +冬暖夏凉 冬暖夏涼 +冬月 冬月 +冬柴铁 冬柴鐵 +冬温夏凊 冬溫夏凊 +冬游 冬遊 +冬灌 冬灌 +冬烘 冬烘 +冬烘先生 冬烘先生 +冬狩 冬狩 +冬瓜 冬瓜 +冬瓜汤 冬瓜湯 +冬瓜茶 冬瓜茶 +冬眠 冬眠 +冬眠期 冬眠期 +冬笋 冬筍 +冬粉 冬粉 +冬耕 冬耕 +冬至 冬至 +冬至点 冬至點 +冬节 冬節 +冬菇 冬菇 +冬藏 冬藏 +冬虫夏草 冬蟲夏草 +冬衣 冬衣 +冬装 冬裝 +冬赈 冬賑 +冬运 冬運 +冬运会 冬運會 +冬闲 冬閒 +冬防 冬防 +冬雨 冬雨 +冬雪 冬雪 +冬雾 冬霧 +冬青 冬青 +冬青树 冬青樹 +冬风 冬風 +冬麦 冬麥 +冯胜贤 馮勝賢 +冯虚御风 馮虛御風 +冯骥才 馮驥才 +冰上曲棍球 冰上曲棍球 +冰上表演 冰上表演 +冰了 冰了 +冰前刮雪 冰前颳雪 +冰厂 冰廠 +冰壶秋月 冰壺秋月 +冰岩 冰岩 +冰斗 冰斗 +冰杯 冰杯 +冰柜 冰櫃 +冰核 冰核 +冰火不同炉 冰火不同爐 +冰炭不同器 冰炭不同器 +冰炭不同罏 冰炭不同罏 +冰生于水而寒于水 冰生於水而寒於水 +冰碛岩 冰磧岩 +冰雕 冰雕 +冰雪皇后 冰雪皇后 +冰面 冰面 +冲上 衝上 +冲上前 衝上前 +冲上去 衝上去 +冲上来 衝上來 +冲下 衝下 +冲下去 衝下去 +冲下来 衝下來 +冲不上 衝不上 +冲不下 衝不下 +冲不入 衝不入 +冲不出 衝不出 +冲不开 衝不開 +冲不破 衝不破 +冲不过 衝不過 +冲不进 衝不進 +冲了上 衝了上 +冲了下 衝了下 +冲了入 衝了入 +冲了出 衝了出 +冲了来 衝了來 +冲了过 衝了過 +冲了进 衝了進 +冲人 沖人 +冲他 衝他 +冲你 衝你 +冲倒 衝倒 +冲克 沖剋 +冲入 衝入 +冲冠 衝冠 +冲冠发怒 衝冠髮怒 +冲冲 沖沖 +冲冲水 沖沖水 +冲决 沖決 +冲决堤防 沖決堤防 +冲凉 沖涼 +冲出 衝出 +冲出去 衝出去 +冲出来 衝出來 +冲出重围 衝出重圍 +冲击 衝擊 +冲击力 衝擊力 +冲击性 衝擊性 +冲击波 衝擊波 +冲击韧性 衝擊韌性 +冲到 衝到 +冲刷 沖刷 +冲刺 衝刺 +冲刺班 衝刺班 +冲剂 沖劑 +冲力 衝力 +冲动 衝動 +冲动型 衝動型 +冲劲 衝勁 +冲劲十足 衝勁十足 +冲势 衝勢 +冲印 沖印 +冲压 衝壓 +冲压机 衝壓機 +冲厕所 沖廁所 +冲去 衝去 +冲口而出 衝口而出 +冲口而发 衝口而發 +冲向 衝向 +冲向前 衝向前 +冲和 沖和 +冲喜 沖喜 +冲回 衝回 +冲回去 衝回去 +冲在下 衝在下 +冲在前 衝在前 +冲在最前 衝在最前 +冲坏 沖壞 +冲坚陷阵 衝堅陷陣 +冲垮 沖垮 衝垮 +冲堂 衝堂 +冲塌 沖塌 +冲天 沖天 +冲天之怒 沖天之怒 +冲天炉 沖天爐 +冲天炮 沖天炮 +冲头阵 衝頭陣 +冲她 衝她 +冲子 衝子 +冲州撞府 衝州撞府 +冲帐 沖帳 +冲年 沖年 +冲床 衝牀 +冲开 衝開 +冲弱 沖弱 +冲得入 衝得入 +冲得出 衝得出 +冲得过 衝得過 +冲得进 衝得進 +冲心 衝心 +冲怀 沖懷 +冲我 衝我 +冲扩 沖擴 +冲掉 沖掉 +冲撞 衝撞 +冲撞力 衝撞力 +冲散 衝散 +冲断 沖斷 +冲断层 衝斷層 +冲昏 衝昏 +冲昏头脑 衝昏頭腦 +冲昧 沖昧 +冲服 沖服 +冲服剂 沖服劑 +冲末 沖末 +冲杀 衝殺 +冲来 衝來 +冲来冲去 衝來衝去 +冲模 沖模 +冲毁 沖毀 +冲水 沖水 +冲沟 沖溝 +冲泡 沖泡 +冲泡式 沖泡式 +冲波 衝波 +冲波激浪 衝波激浪 +冲波逆折 衝波逆折 +冲泻 沖瀉 +冲洗 沖洗 +冲洗照片 沖洗照片 +冲流 沖流 +冲浪 衝浪 +冲浪客 衝浪客 +冲浪板 衝浪板 +冲浪者 衝浪者 +冲浪赛 衝浪賽 +冲涤 沖滌 +冲淋浴 沖淋浴 +冲淡 沖淡 +冲澡 沖澡 +冲然 衝然 +冲牀工 沖牀工 +冲犯 衝犯 +冲田 沖田 +冲盹 衝盹 +冲盹儿 衝盹兒 +冲着 衝着 +冲破 衝破 +冲积 沖積 +冲积土 沖積土 +冲积堤 沖積堤 +冲积层 沖積層 +冲积岛 沖積島 +冲积平原 沖積平原 +冲积扇 沖積扇 +冲积物 沖積物 +冲程 衝程 +冲税 沖稅 +冲穴 衝穴 +冲空机 沖空機 +冲突 衝突 +冲突区 衝突區 +冲突性 衝突性 +冲突点 衝突點 +冲线 衝線 +冲绳 沖繩 +冲绳县 沖繩縣 +冲绳岛 沖繩島 +冲绳群岛 沖繩羣島 +冲脉 衝脈 +冲自己 衝自己 +冲至 衝至 +冲茶 沖茶 +冲虚 沖虛 +冲虚真人 沖虛真人 +冲虚真经 沖虛真經 +冲蚀 沖蝕 +冲襟 沖襟 +冲要 衝要 +冲走 沖走 +冲起 衝起 +冲起来 衝起來 +冲车 衝車 +冲过 衝過 +冲过去 衝過去 +冲过来 衝過來 +冲进 衝進 +冲进去 衝進去 +冲进来 衝進來 +冲退 衝退 +冲量 衝量 +冲销 沖銷 +冲锋 衝鋒 +冲锋号 衝鋒號 +冲锋枪 衝鋒槍 +冲锋鎗 衝鋒鎗 +冲锋陷阵 衝鋒陷陣 +冲门 衝門 +冲陷 衝陷 +冲霄 沖霄 +冲霄汉外 沖霄漢外 +冲风 衝風 +冲高 衝高 +冲默 沖默 +冲鼻 沖鼻 +冲龄 沖齡 +决了 決了 +决定出 決定出 +决心干 決心幹 +决志 決志 +决斗 決鬥 +决斗者 決鬥者 +决策千里 決策千里 +决策当局 決策當局 +决胜 決勝 +决胜千里 決勝千里 +决胜局 決勝局 +决胜期 決勝期 +决胜点 決勝點 +决胜盘 決勝盤 +决胜节 決勝節 +决胜负 決勝負 +冶叶倡条 冶葉倡條 +冶游 冶遊 +冶炼 冶煉 +冶炼厂 冶煉廠 +冶炼炉 冶煉爐 +冶荡 冶蕩 +冷冻柜 冷凍櫃 +冷地里 冷地里 +冷布 冷布 +冷心冷面 冷心冷面 +冷感症 冷感症 +冷战以后 冷戰以後 +冷板凳 冷板凳 +冷板曲 冷板曲 +冷气团 冷氣團 +冷淡关系 冷淡關係 +冷灰里爆出火来 冷灰裏爆出火來 +冷腌法 冷醃法 +冷藏柜 冷藏櫃 +冷读术 冷讀術 +冷轧钢板 冷軋鋼板 +冷酒儿后犯 冷酒兒後犯 +冷锅里爆豆 冷鍋裏爆豆 +冷面 冷麪 冷面 +冷面冷心 冷面冷心 +冷面相 冷面相 +冻僵 凍僵 +净余 淨餘 +净发 淨髮 +净尽 淨盡 +净心修身 淨心修身 +凄丽 悽麗 +凄冷 淒冷 +凄凄 悽悽 +凄凉 淒涼 +凄切 悽切 +凄历 悽歷 +凄厉 淒厲 +凄咽 悽咽 +凄婉 悽婉 +凄寒 淒寒 +凄怆 悽愴 +凄怨 悽怨 +凄恻 悽惻 +凄惨 悽慘 +凄惶 悽惶 +凄楚 悽楚 +凄沧 淒滄 +凄清 悽清 +凄然 悽然 +凄紧 悽緊 +凄绝 悽絕 +凄美 悽美 +凄艳 悽豔 +凄苦 悽苦 +凄迷 悽迷 +凄酸 悽酸 +凄雨 淒雨 +凄风 悽風 +准三后 准三后 +准不准 準不準 +准不准他 准不准他 +准不准你 准不准你 +准不准备 準不準備 +准不准她 准不准她 +准不准它 准不准它 +准不准我 准不准我 +准不准确 準不準確 +准不准许 准不准許 +准不准谁 准不准誰 +准予 准予 +准以 准以 +准伏 准伏 +准会 準會 +准例 準例 +准保 準保 +准保护 准保護 +准保释 准保釋 +准信 準信 +准假 准假 +准儿 準兒 +准入 准入 +准决斗 准決鬥 +准决赛 準決賽 +准分子 準分子 +准分子雷射仪 準分子雷射儀 +准则 準則 +准噶尔 準噶爾 +准噶尔盆地 準噶爾盆地 +准噶尔翼龙 準噶爾翼龍 +准备 準備 +准备下 準備下 +准备充分 準備充分 +准备准备 準備準備 +准备好 準備好 +准备好了 準備好了 +准备好的 準備好的 +准备活动 準備活動 +准备率 準備率 +准备给 準備給 +准备金 準備金 +准备金率 準備金率 +准头 準頭 +准奏 准奏 +准妈妈 準媽媽 +准定 準定 +准将 准將 +准尉 准尉 +准平原 準平原 +准度 準度 +准式 準式 +准得 準得 +准折 准折 +准拟 準擬 +准拿督 準拿督 +准据 準據 +准新娘 準新娘 +准新郎 準新郎 +准时 準時 +准时出席 準時出席 +准时到 準時到 +准时到达 準時到達 +准星 準星 +准是 準是 +准普尔 准普爾 +准格尔 準格爾 +准格尔盆地 準格爾盆地 +准此 准此 +准決賽 準決賽 +准点 準點 +准点率 準點率 +准用 準用 +准的 準的 +准确 準確 +准确度 準確度 +准确性 準確性 +准确无误 準確無誤 +准确率 準確率 +准稳旋涡结构 準穩旋渦結構 +准算 准算 +准线 準線 +准绳 準繩 +准考证 准考證 +准葛尔盆地 準葛爾盆地 +准许 准許 +准话 準話 +准谱 準譜 +准谱儿 準譜兒 +准货币 準貨幣 +准错不了 準錯不了 +凉了 涼了 +凉了半截 涼了半截 +凉台 涼臺 +凉席 涼蓆 +凉药 涼藥 +凉面 涼麪 +凌云 凌雲 +凌云健笔 凌雲健筆 +凌云县 凌雲縣 +凌云壮志 凌雲壯志 +凌云翰 淩云翰 +凌借 凌藉 +凌十八 淩十八 +凌如焕 淩如焕 +凌姓 淩姓 +凌小姐 淩小姐 +凌志 凌志 +凌志美 淩志美 +凌惠平 淩惠平 +凌昌焕 淩昌焕 +凌氏 淩氏 +凌氏惠平 淩氏惠平 +凌水 淩水 +凌河 淩河 +凌烟阁 凌煙閣 +凌策 淩策 +凌统 淩統 +凌蒙初 淩濛初 +凌退思 淩退思 +凌驰 淩馳 +减个 減個 +减了 減了 +减价 減價 +减价出售 減價出售 +减价时间 減價時間 +减压时间表 減壓時間表 +减压症 減壓症 +减压表 減壓表 +减噪 減噪 +减征 減徵 +减肥药 減肥藥 +凑不出来 湊不出來 +凑出 湊出 +凑合 湊合 +凑合着 湊合着 +凑四合六 湊四合六 +凛栗 凜慄 +凝合 凝合 +凝合力 凝合力 +凝咽 凝咽 +凝灰岩 凝灰岩 +凝炼 凝鍊 +几丁质 幾丁質 +几万 幾萬 +几万个 幾萬個 +几万人 幾萬人 +几万元 幾萬元 +几万块 幾萬塊 +几上 几上 +几下 幾下 +几世 幾世 +几世纪 幾世紀 +几丝 几絲 +几两 幾兩 +几个 幾個 +几个人 幾個人 +几个月 幾個月 +几为所害 幾爲所害 +几乎 幾乎 +几乎不 幾乎不 +几乎不可能 幾乎不可能 +几乎在 幾乎在 +几乎完全 幾乎完全 +几乎是 幾乎是 +几乎没有 幾乎沒有 +几事 幾事 +几于 幾於 +几人 幾人 +几人份 幾人份 +几亿 幾億 +几付 幾付 +几代 幾代 +几令 幾令 +几件 幾件 +几件事 幾件事 +几任 幾任 +几份 幾份 +几伍 幾伍 +几众 幾衆 +几位 幾位 +几位数 幾位數 +几何 幾何 +几何体 幾何體 +几何光学 幾何光學 +几何原本 幾何原本 +几何图形 幾何圖形 +几何图案 幾何圖案 +几何学 幾何學 +几何拓扑 幾何拓撲 +几何拓扑学 幾何拓撲學 +几何级数 幾何級數 +几何线 幾何線 +几何量 幾何量 +几倍 幾倍 +几儿 幾兒 +几儿个 幾兒個 +几元 幾元 +几克 幾克 +几党 幾黨 +几內亚 幾內亞 +几內亚共和国 幾內亞共和國 +几內亚比索 幾內亞比索 +几內亚比索共和国 幾內亞比索共和國 +几內亚比绍 幾內亞比紹 +几內亚湾 幾內亞灣 +几关 幾關 +几具 幾具 +几内亚 幾內亞 +几内亚比绍 幾內亞比紹 +几内亚湾 幾內亞灣 +几冊 幾冊 +几净窗明 几淨窗明 +几几 几几 +几几乎乎 幾幾乎乎 +几凳 几凳 +几出 幾齣 +几刀 幾刀 +几分 幾分 +几分之几 幾分之幾 +几分收获 幾分收穫 +几分钟 幾分鐘 +几划 幾劃 +几列 幾列 +几副 幾副 +几动 幾動 +几化 幾化 +几匹 幾匹 +几匹马 幾匹馬 +几区 幾區 +几十 幾十 +几十万 幾十萬 +几十个 幾十個 +几十人 幾十人 +几十亿 幾十億 +几十年 幾十年 +几千 幾千 +几千万 幾千萬 +几千个 幾千個 +几千人 幾千人 +几千元 幾千元 +几千块 幾千塊 +几千年 幾千年 +几发 幾發 +几句 幾句 +几句话 幾句話 +几只 幾隻 +几可乱真 幾可亂真 +几台 幾臺 +几号 幾號 +几吋 幾吋 +几名 幾名 +几员 幾員 +几回 幾回 +几回价 幾回價 +几因 幾因 +几团 幾團 +几国 幾國 +几圆 幾圓 +几圈 幾圈 +几场 幾場 +几块 幾塊 +几块钱 幾塊錢 +几垒 幾壘 +几声 幾聲 +几处 幾處 +几多 幾多 +几大 幾大 +几大块 幾大塊 +几大片 幾大片 +几大类 幾大類 +几天 幾天 +几天后 幾天後 +几天来 幾天來 +几头 幾頭 +几子 几子 +几孔 幾孔 +几字 幾字 +几季 幾季 +几客 幾客 +几家 幾家 +几家欢乐 幾家歡樂 +几家欢乐几家愁 幾家歡樂幾家愁 +几寸 幾寸 +几封 幾封 +几封信 幾封信 +几小时 幾小時 +几尾 幾尾 +几局 幾局 +几层 幾層 +几层楼 幾層樓 +几届 幾屆 +几岁 幾歲 +几巷 幾巷 +几师 幾師 +几希 幾希 +几席 几席 +几幅 幾幅 +几年 幾年 +几年几班 幾年幾班 +几年来 幾年來 +几年生 幾年生 +几年级 幾年級 +几床 幾牀 +几度 幾度 +几度春风 幾度春風 +几座 幾座 +几开 幾開 +几弄 幾弄 +几张 幾張 +几弹 幾彈 +几微 幾微 +几成 幾成 +几成新 幾成新 +几截 幾截 +几户 幾戶 +几所 幾所 +几手 幾手 +几打 幾打 +几批 幾批 +几拳 幾拳 +几支 幾支 +几文钱 幾文錢 +几斤 幾斤 +几斤几两 幾斤幾兩 +几旁 几旁 +几旅 幾旅 +几日 幾日 +几日份 幾日份 +几时 幾時 +几星 幾星 +几星期 幾星期 +几晚 幾晚 +几曾 幾曾 +几月 幾月 +几月份 幾月份 +几期 幾期 +几本 幾本 +几本书 幾本書 +几杆 幾桿 +几杖 几杖 +几杯 幾杯 +几板 幾板 +几枚 幾枚 +几枝 幾枝 +几枪 幾槍 +几架 幾架 +几栋 幾棟 +几株 幾株 +几样 幾樣 +几格 幾格 +几案 几案 +几案之才 几案之才 +几档 幾檔 +几桶 幾桶 +几梯次 幾梯次 +几棵 幾棵 +几椅 几椅 +几楼 幾樓 +几榻 几榻 +几次 幾次 +几次三番 幾次三番 +几欲 幾欲 +几步 幾步 +几殆 幾殆 +几段 幾段 +几比几 幾比幾 +几毛 幾毛 +几毛钱 幾毛錢 +几洞 幾洞 +几滴 幾滴 +几滴水 幾滴水 +几炷香 幾炷香 +几点 幾點 +几点了 幾點了 +几点几 幾點幾 +几点钟 幾點鐘 +几版 幾版 +几率 機率 +几环 幾環 +几班 幾班 +几番 幾番 +几番家 幾番家 +几百 幾百 +几百万 幾百萬 +几百个 幾百個 +几百人 幾百人 +几百亿 幾百億 +几百元 幾百元 +几百块 幾百塊 +几百年 幾百年 +几盏 幾盞 +几盒 幾盒 +几盒装 幾盒裝 +几眼 幾眼 +几碗 幾碗 +几碗饭 幾碗飯 +几社 幾社 +几票 幾票 +几种 幾種 +几科 幾科 +几秒 幾秒 +几秒钟 幾秒鐘 +几稀 幾稀 +几窝 幾窩 +几站 幾站 +几章 幾章 +几竿 幾竿 +几笔 幾筆 +几笼 幾籠 +几筒 幾筒 +几筵 几筵 +几箱 幾箱 +几米 幾米 +几类 幾類 +几粒 幾粒 +几级 幾級 +几线 幾線 +几组 幾組 +几经 幾經 +几缕 幾縷 +几罐 幾罐 +几股 幾股 +几胎 幾胎 +几能 幾能 +几能勾 幾能勾 +几脚 幾腳 +几至 幾至 +几般 幾般 +几节 幾節 +几节课 幾節課 +几街 幾街 +几袋 幾袋 +几角 幾角 +几角形 幾角形 +几许 幾許 +几课 幾課 +几谏 幾諫 +几起 幾起 +几趟 幾趟 +几趟路 幾趟路 +几路 幾路 +几车 幾車 +几转 幾轉 +几辆 幾輛 +几辆车 幾輛車 +几近 幾近 +几近于 幾近於 +几通 幾通 +几道 幾道 +几道菜 幾道菜 +几部 幾部 +几针 幾針 +几门 幾門 +几间 幾間 +几集 幾集 +几面上 几面上 +几页 幾頁 +几顶 幾頂 +几项 幾項 +几顿 幾頓 +几颗 幾顆 +几题 幾題 +几首 幾首 +几首歌 幾首歌 +凡事总有一个开头 凡事總有一個開頭 +凡于 凡於 +凡才 凡才 +凡此种种 凡此種種 +凡须 凡須 +凤凰于蜚 鳳凰于蜚 +凤凰于飞 鳳凰于飛 +凤凰台 鳳凰臺 +凤占 鳳占 +凤去台空 鳳去臺空 +凤台 鳳台 +凤台县 鳳臺縣 +凤尾松 鳳尾松 +凤梨干 鳳梨乾 +凤皇于蜚 鳳皇于蜚 +凭借 憑藉 +凭借着 憑藉着 +凭准 憑準 +凭几 憑几 +凭吊 憑弔 +凭媒说合 憑媒說合 +凭折 憑摺 +凭据 憑據 +凭空出现 憑空出現 +凭虚御风 憑虛御風 +凭闲 憑閑 +凯复 凱復 +凯特布兰琪 凱特布蘭琪 +凯迪拉克 凱迪拉克 +凯里 凱里 +凯里市 凱裏市 +凶事 凶事 +凶人 兇人 +凶仪 兇儀 +凶侠 兇俠 +凶信 凶信 +凶兆 凶兆 +凶党 兇黨 +凶具 兇具 +凶凶 兇兇 +凶凶恶恶 兇兇惡惡 +凶凶狠狠 兇兇狠狠 +凶刀 兇刀 +凶器 兇器 +凶地 凶地 +凶多吉少 凶多吉少 +凶嫌 兇嫌 +凶宅 凶宅 +凶岁 凶歲 +凶巴巴 兇巴巴 +凶年 凶年 +凶年饥岁 凶年饑歲 +凶徒 兇徒 +凶得 兇得 +凶得狠 兇得狠 +凶德 凶德 +凶怪 凶怪 +凶恶 兇惡 +凶悍 兇悍 +凶惧 兇懼 +凶手 兇手 +凶日 凶日 +凶暴 兇暴 +凶服 凶服 +凶杀 兇殺 +凶杀案 兇殺案 +凶枪 兇槍 +凶案 兇案 +凶横 兇橫 +凶死 凶死 +凶残 兇殘 +凶殘 兇殘 +凶殴 兇毆 +凶殺 兇殺 +凶气 凶氣 +凶焰 兇焰 +凶煞 凶煞 +凶燄 凶燄 +凶犯 兇犯 +凶狂 兇狂 +凶狠 兇狠 +凶猛 兇猛 +凶疑 兇疑 +凶相 兇相 +凶相毕露 兇相畢露 +凶礼 凶禮 +凶神 凶神 +凶神恶煞 凶神惡煞 +凶神附体 凶神附體 +凶竖 凶豎 +凶终隙末 凶終隙末 +凶耗 凶耗 +凶肆 凶肆 +凶荒 凶荒 +凶虐 兇虐 +凶讯 凶訊 +凶起来 兇起來 +凶身 凶身 +凶逆 凶逆 +凶门 凶門 +凶险 兇險 +凶顽 兇頑 +凸出 凸出 +凸出去 凸出去 +凸出成 凸出成 +凸出来 凸出來 +凸多面体 凸多面體 +凸折线 凸折線 +凸显出 凸顯出 +凸显出来 凸顯出來 +凸板印刷 凸板印刷 +凸面 凸面 +凸面体 凸面體 +凸面部分 凸面部分 +凸面镜 凸面鏡 +凹凸有致 凹凸有致 +凹板 凹板 +凹洞里 凹洞裏 +凹雕 凹雕 +凹面 凹面 +凹面镜 凹面鏡 +出一回神 出一回神 +出上 出上 +出下 出下 +出不起 出不起 +出丑 出醜 +出丑扬疾 出醜揚疾 +出丑狼借 出醜狼藉 +出世 出世 +出世作 出世作 +出世法 出世法 +出丧 出喪 +出个 出個 +出个价 出個價 +出主意 出主意 +出乎 出乎 +出乎寻常 出乎尋常 +出乎意外 出乎意外 +出乎意料 出乎意料 +出乎预料 出乎預料 +出乖弄丑 出乖弄醜 +出乖露丑 出乖露醜 +出乘 出乘 +出书 出書 +出乱子 出亂子 +出了 出了 +出了事 出了事 +出了月 出了月 +出事 出事 +出事情 出事情 +出于 出於 +出云 出雲 +出亡 出亡 +出产 出產 +出产地 出產地 +出人 出人 +出人命 出人命 +出人头地 出人頭地 +出人意外 出人意外 +出人意料 出人意料 +出人意料之外 出人意料之外 +出人意表 出人意表 +出仕 出仕 +出价 出價 +出任 出任 +出份子 出份子 +出伏 出伏 +出众 出衆 +出伦之才 出倫之才 +出使 出使 +出倒 出倒 +出借 出借 +出借书 出借書 +出儿 齣兒 +出入 出入 +出入口 出入口 +出入境 出入境 +出入将相 出入將相 +出入平安 出入平安 +出入相随 出入相隨 +出入証 出入証 +出入证 出入證 +出入门 出入門 +出公差 出公差 +出关 出關 +出兵 出兵 +出其不备 出其不備 +出其不意 出其不意 +出其不意攻其不备 出其不意攻其不備 +出其东门 出其東門 +出其右 出其右 +出具 出具 +出典 出典 +出军 出軍 +出冷门 出冷門 +出出 出出 +出出气 出出氣 +出出进进 出出進進 +出击 出擊 +出分子 出分子 +出刊 出刊 +出列 出列 +出到 出到 +出力 出力 +出动 出動 +出勤 出勤 +出勤率 出勤率 +出卖 出賣 +出卖灵魂 出賣靈魂 +出厂 出廠 +出厂价 出廠價 +出厂价格 出廠價格 +出去 出去 +出去会 出去會 +出去时 出去時 +出去玩 出去玩 +出双入对 出雙入對 +出发 出發 +出发到 出發到 +出发地 出發地 +出发日 出發日 +出发点 出發點 +出口 出口 +出口产品 出口產品 +出口伤人 出口傷人 +出口值 出口值 +出口入耳 出口入耳 +出口到 出口到 +出口区 出口區 +出口商 出口商 +出口商品 出口商品 +出口国 出口國 +出口处 出口處 +出口导向 出口導向 +出口成章 出口成章 +出口税 出口稅 +出口调查 出口調查 +出口货 出口貨 +出口贸易 出口貿易 +出口量 出口量 +出口额 出口額 +出台 出臺 +出号 出號 +出名 出名 +出品 出品 +出品人 出品人 +出品国 出品國 +出售 出售 +出售一空 出售一空 +出售给 出售給 +出唱片 出唱片 +出喽子 出嘍子 +出团 出團 +出国 出國 +出国前 出國前 +出国时 出國時 +出国者 出國者 +出圈 出圈 +出圈儿 出圈兒 +出土 出土 +出土文物 出土文物 +出土物 出土物 +出在 出在 +出场 出場 +出场费 出場費 +出埃及记 出埃及記 +出城 出城 +出堂 出堂 +出塞 出塞 +出境 出境 +出境检查 出境檢查 +出境签证 出境簽證 +出境証 出境証 +出境证 出境證 +出声 出聲 +出处 出處 +出处不如聚处 出處不如聚處 +出外 出外 +出外人 出外人 +出外景 出外景 +出大差 出大差 +出大恭 出大恭 +出太阳 出太陽 +出头 出頭 +出头之日 出頭之日 +出头天 出頭天 +出头日子 出頭日子 +出头棍 出頭棍 +出头椽儿先朽烂 出頭椽兒先朽爛 +出头露角 出頭露角 +出头露面 出頭露面 +出头鸟 出頭鳥 +出奇 出奇 +出奇不意 出奇不意 +出奇制胜 出奇制勝 +出奔 出奔 +出好 出好 +出妇 出婦 +出妻 出妻 +出姓 出姓 +出娄子 出婁子 +出嫁 出嫁 +出完 出完 +出官 出官 +出定 出定 +出宰 出宰 +出家 出家 +出家人 出家人 +出家人吃八方 出家人吃八方 +出将入相 出將入相 +出小恭 出小恭 +出尔反尔 出爾反爾 +出尖 出尖 +出尖儿 出尖兒 +出尘 出塵 +出尽 出盡 +出局 出局 +出局数 出局數 +出山 出山 +出岔 出岔 +出岔儿 出岔兒 +出岔子 出岔子 +出巡 出巡 +出工 出工 +出差 出差 +出差费 出差費 +出差错 出差錯 +出师 出師 +出师不利 出師不利 +出师表 出師表 +出席 出席 +出席率 出席率 +出席者 出席者 +出席表决比例 出席表決比例 +出席费 出席費 +出幼 出幼 +出店 出店 +出庭 出庭 +出庭作证 出庭作證 +出庭应讯 出庭應訊 +出彩 出彩 +出征 出征 +出征收 出徵收 +出得 出得 +出心 出心 +出恭 出恭 +出息 出息 +出意外 出意外 +出戏 齣戲 +出战 出戰 +出户 出戶 +出手 出手 +出手得卢 出手得盧 +出手见高低 出手見高低 +出把戏 出把戲 +出招 出招 +出拳 出拳 +出挑 出挑 +出掌 出掌 +出操 出操 +出操课 出操課 +出故典 出故典 +出教 出教 +出数儿 出數兒 +出文 出文 +出斩 出斬 +出新 出新 +出景 出景 +出月 出月 +出月子 出月子 +出有 出有 +出有入无 出有入無 +出材 出材 +出条子 出條子 +出来 出來 +出来时 出來時 +出校 出校 +出格 出格 +出梅 出梅 +出楼子 出樓子 +出榜 出榜 +出橐 出橐 +出此下策 出此下策 +出死入生 出死入生 +出殃 出殃 +出殡 出殯 +出殡日 出殯日 +出毛病 出毛病 +出气 出氣 +出气口 出氣口 +出气多进气少 出氣多進氣少 +出气筒 出氣筒 +出水 出水 +出水伙计 出水夥計 +出水口 出水口 +出水管 出水管 +出水芙蓉 出水芙蓉 +出汗 出汗 +出汙泥而不染 出污泥而不染 +出江 出江 +出没 出沒 +出没不定 出沒不定 +出没无常 出沒無常 +出油 出油 +出注 出注 +出洋 出洋 +出洋相 出洋相 +出活 出活 +出浴 出浴 +出海 出海 +出海口 出海口 +出海打鱼 出海打魚 +出海捕鱼 出海捕魚 +出涕 出涕 +出淤泥而不染 出淤泥而不染 +出清 出清 +出港 出港 +出港大厅 出港大廳 +出港证 出港證 +出游 出遊 +出溜 出溜 +出溜儿 出溜兒 +出漏子 出漏子 +出演 出演 +出火 出火 +出火炕 出火炕 +出炉 出爐 +出点 出點 +出点子 出點子 +出热 出熱 +出片 出片 +出版 出版 +出版业 出版業 +出版业务 出版業務 +出版人 出版人 +出版前编目 出版前編目 +出版品 出版品 +出版品奖 出版品獎 +出版商 出版商 +出版地缺 出版地缺 +出版所 出版所 +出版日 出版日 +出版日期 出版日期 +出版期缺 出版期缺 +出版法 出版法 +出版物 出版物 +出版界 出版界 +出版社 出版社 +出版社不详 出版社不詳 +出版社丛集 出版社叢集 +出版社清样 出版社清樣 +出版社目录卡 出版社目錄卡 +出版社装祯 出版社裝禎 +出版者 出版者 +出版自由 出版自由 +出版节 出版節 +出版项 出版項 +出牌 出牌 +出状况 出狀況 +出狱 出獄 +出猎 出獵 +出现 出現 +出现意外 出現意外 +出现数 出現數 +出球 出球 +出生 出生 +出生入死 出生入死 +出生别 出生別 +出生地 出生地 +出生地点 出生地點 +出生年 出生年 +出生日 出生日 +出生日期 出生日期 +出生牙 出生牙 +出生率 出生率 +出生纸 出生紙 +出生缺陷 出生缺陷 +出生证 出生證 +出界 出界 +出疹子 出疹子 +出的 出的 +出监 出監 +出盘 出盤 +出示 出示 +出示证件 出示證件 +出社会 出社會 +出神 出神 +出神入化 出神入化 +出票 出票 +出科 出科 +出租 出租 +出租人 出租人 +出租店 出租店 +出租汽车 出租汽車 +出租率 出租率 +出租给 出租給 +出租车 出租車 +出稿 出稿 +出窍 出竅 +出窝老 出窩老 +出站 出站 +出笏 出笏 +出笼 出籠 +出笼鸟 出籠鳥 +出籍 出籍 +出类拔群 出類拔羣 +出类拔萃 出類拔萃 +出类超群 出類超羣 +出粗 出粗 +出粜 出糶 +出粮 出糧 +出糗 出糗 +出红差 出紅差 +出纳 出納 +出纳台 出納臺 +出纳员 出納員 +出纳处 出納處 +出纳科 出納科 +出纳系统 出納系統 +出纳组 出納組 +出线 出線 +出结 出結 +出给 出給 +出继 出繼 +出缺 出缺 +出群 出羣 +出群拔萃 出羣拔萃 +出老千 出老千 +出脱 出脫 +出自 出自 +出自于 出自於 +出自娘胎 出自孃胎 +出自肺腑 出自肺腑 +出臭子儿 出臭子兒 +出航 出航 +出色 出色 +出花儿 出花兒 +出花样 出花樣 +出芽 出芽 +出芽法 出芽法 +出芽生殖 出芽生殖 +出苗 出苗 +出苗率 出苗率 +出草 出草 +出落 出落 +出虚恭 出虛恭 +出蛰 出蟄 +出血 出血 +出血性 出血性 +出血性登革热 出血性登革熱 +出血热 出血熱 +出血病 出血病 +出血筒子 出血筒子 +出血量 出血量 +出行 出行 +出言 出言 +出言不逊 出言不遜 +出言成章 出言成章 +出言无状 出言無狀 +出言有序 出言有序 +出警入跸 出警入蹕 +出让 出讓 +出记 出記 +出访 出訪 +出诉 出訴 +出诊 出診 +出调 出調 +出谋划策 出謀劃策 +出谋献策 出謀獻策 +出谷迁乔 出谷遷喬 +出豁 出豁 +出货 出貨 +出货单 出貨單 +出货量 出貨量 +出费 出費 +出资 出資 +出资人 出資人 +出赘 出贅 +出赛 出賽 +出走 出走 +出起 出起 +出起来 出起來 +出超 出超 +出超国 出超國 +出超额 出超額 +出路 出路 +出跳 出跳 +出身 出身 +出车 出車 +出车祸 出車禍 +出轨 出軌 +出轨行为 出軌行爲 +出过 出過 +出迎 出迎 +出进口 出進口 +出远门 出遠門 +出逃 出逃 +出道 出道 +出道时 出道時 +出量 出量 +出金 出金 +出钱 出錢 +出钱出力 出錢出力 +出铁 出鐵 +出锋头 出鋒頭 +出错 出錯 +出错信息 出錯信息 +出锤 出錘 +出镜 出鏡 +出镜头 出鏡頭 +出门 出門 +出门在外 出門在外 +出门子 出門子 +出门时 出門時 +出问题 出問題 +出闸 出閘 +出阁 出閣 +出阁之喜 出閣之喜 +出阵 出陣 +出陈布新 出陳佈新 +出院 出院 +出险 出險 +出难题 出難題 +出面 出面 +出鞘 出鞘 +出韵 出韻 +出顶 出頂 +出项 出項 +出题 出題 +出风口 出風口 +出风头 出風頭 +出饭 出飯 +出首 出首 +出马 出馬 +出马上场 出馬上場 +出马上阵 出馬上陣 +出高价 出高價 +出鬼入神 出鬼入神 +出齐 出齊 +击出 擊出 +击发 擊發 +击向 擊向 +击板 擊板 +击沈 擊沈 +击筑 擊築 +击钟 擊鐘 +击钟陈鼎 擊鐘陳鼎 +击钟鼎食 擊鐘鼎食 +函复 函覆 +函谷关 函谷關 +凿出 鑿出 +凿出去 鑿出去 +凿出来 鑿出來 +凿凿有据 鑿鑿有據 +凿壁悬梁 鑿壁懸梁 +凿岩 鑿巖 +凿岩机 鑿岩機 +凿船虫 鑿船蟲 +刀创药 刀創藥 +刀削面 刀削麪 +刀割针扎 刀割針扎 +刀布 刀布 +刀板 刀板 +刀耕火种 刀耕火種 +刀面 刀面 +刁奸 刁姦 +刁恶 刁惡 +刁斗 刁斗 +分一杯羹 分一杯羹 +分不出 分不出 +分个 分個 +分久必合 分久必合 +分久必合合久必分 分久必合合久必分 +分了 分了 +分伙 分夥 +分克 分克 +分党 分黨 +分出 分出 +分出去 分出去 +分出来 分出來 +分分合合 分分合合 +分别 分別 +分别出 分別出 +分别财产制 分別財產制 +分别部居 分別部居 +分半钟 分半鐘 +分占 分佔 +分厂 分廠 +分厘卡 分釐卡 +分厘毫丝 分釐毫絲 +分发 分發 +分发到 分發到 +分发区 分發區 +分发给 分發給 +分合 分合 +分向岛 分向島 +分多钟 分多鐘 +分天之仇 分天之仇 +分子化合物 分子化合物 +分子钟 分子鐘 +分封制 分封制 +分尸 分屍 +分尸案 分屍案 +分工合作 分工合作 +分工整合 分工整合 +分布 分佈 +分布于 分佈於 +分布区 分佈區 +分布图 分佈圖 +分布学习 分佈學習 +分布式 分佈式 +分布式发展模型 分佈式發展模型 +分布式拒绝服务 分佈式拒絕服務 +分布式环境 分佈式環境 +分布式结构 分佈式結構 +分布式网络 分佈式網絡 +分布控制 分佈控制 +分布范围 分佈範圍 +分布连结网络 分佈連結網絡 +分当 分當 +分录 分錄 +分形几何 分形幾何 +分形几何学 分形幾何學 +分形同气 分形同氣 +分得出 分得出 +分我杯羹 分我杯羹 +分摆 分擺 +分散于 分散於 +分散注意 分散注意 +分数挂帅 分數掛帥 +分时系统 分時系統 +分权制衡 分權制衡 +分杯羹 分杯羹 +分析出 分析出 +分析出来 分析出來 +分泌出 分泌出 +分泌系统 分泌系統 +分离出 分離出 +分离出来 分離出來 +分离术 分離術 +分种 分種 +分筋术 分筋術 +分类目录 分類目錄 +分系 分系 +分系统 分系統 +分级制 分級制 +分获 分獲 +分行布白 分行布白 +分裂症 分裂症 +分身乏术 分身乏術 +分身术 分身術 +分辨出 分辨出 +分辨出来 分辨出來 +分辨善恶 分辨善惡 +分进合 分進合 +分进合击 分進合擊 +分针 分針 +分钟 分鐘 +分门别类 分門別類 +切个 切個 +切云 切雲 +切出 切出 +切出去 切出去 +切出来 切出來 +切合 切合 +切合实际 切合實際 +切合需要 切合需要 +切向 切向 +切向力 切向力 +切向速度 切向速度 +切向量 切向量 +切平面 切平面 +切开术 切開術 +切当 切當 +切菜板 切菜板 +切除术 切除術 +切面 切面 +切骨之仇 切骨之仇 +刊了 刊了 +刊出 刊出 +刊出来 刊出來 +刊布 刊佈 +刊板 刊板 +刊误表 刊誤表 +刑于 刑于 +刑余 刑餘 +刑克 刑剋 +刑法志 刑法志 +刑辟 刑辟 +划一 劃一 +划一不二 劃一不二 +划一桨 划一槳 +划上 劃上 +划下 劃下 +划下道来 劃下道來 +划不来 划不來 +划为 劃爲 +划了 劃了 +划了一会 划了一會 +划价 劃價 +划伤 劃傷 +划位 劃位 +划入 劃入 +划具 划具 +划出 劃出 +划分 劃分 +划分为 劃分爲 +划分成 劃分成 +划分法 劃分法 +划划 劃劃 +划到 劃到 +划到岸 划到岸 +划到江心 划到江心 +划动 划動 +划单人艇 划單人艇 +划去 劃去 +划双人 划雙人 +划向 划向 +划圆防守 劃圓防守 +划在 劃在 +划地 劃地 +划地为王 劃地爲王 +划地自限 劃地自限 +划子 划子 +划定 劃定 +划定为 劃定爲 +划座位 劃座位 +划开 劃開 +划归 劃歸 +划得 劃得 +划得来 划得來 +划成 劃成 +划拉 劃拉 +划拨 劃撥 +划拨帐号 劃撥帳號 +划拳 划拳 +划掉 劃掉 +划时代 劃時代 +划来 划來 +划来划去 劃來劃去 +划桨 划槳 +划款 劃款 +划水 划水 +划法 劃法 +划清 劃清 +划清界线 劃清界線 +划清界限 劃清界限 +划然 劃然 +划界 劃界 +划痕 劃痕 +划着 划着 +划着走 划着走 +划破 劃破 +划策 劃策 +划算 划算 +划纹症 劃紋症 +划线 劃線 +划船 划船 +划艇 划艇 +划花 劃花 +划行 划行 +划设 劃設 +划走 划走 +划起 划起 +划起来 划起來 +划足 劃足 +划过 劃過 +划过去 划過去 +划过来 划過來 +划进 划進 +划进去 划進去 +划进来 划進來 +划龙舟 划龍舟 +刓团 刓團 +刖足适屦 刖足適屨 +刖趾适屦 刖趾適屨 +列举出 列舉出 +列了 列了 +列克星顿 列克星頓 +列出 列出 +列出来 列出來 +列别杰夫 列別傑夫 +列印出来 列印出來 +列夫托尔斯泰 列夫托爾斯泰 +列宁威权体系 列寧威權體系 +列布 列布 +列席代表 列席代表 +列当 列當 +列御寇 列禦寇 +列氏寒暑表 列氏寒暑表 +列表 列表 +列表机 列表機 +刘云山 劉雲山 +刘任杰 劉任傑 +刘伟杰 劉偉杰 +刘克庄 劉克莊 +刘克襄 劉克襄 +刘占吉 劉占吉 +刘向 劉向 +刘嘉发 劉嘉發 +刘复 劉復 +刘宗周 劉宗周 +刘峰松 劉峯松 +刘幸义 劉幸義 +刘幸如 劉倖如 +刘志勤 劉志勤 +刘志升 劉志昇 +刘志威 劉志威 +刘念 劉念 +刘松仁 劉松仁 +刘松年 劉松年 +刘松藩 劉松藩 +刘知几 劉知幾 +刘秋凤 劉秋鳳 +刘胡兰 劉胡蘭 +刘表 劉表 +刘鉴 劉鑑 +刘鉴庭 劉鑑庭 +刘长发 劉長發 +刘阮上天台 劉阮上天臺 +刘青云 劉青雲 +则个 則個 +刚出 剛出 +刚出去 剛出去 +刚出来 剛出來 +刚出道 剛出道 +刚回 剛回 +刚回到 剛回到 +刚回去 剛回去 +刚回来 剛回來 +刚干 剛乾 +刚才 剛纔 +刚才一载 剛纔一載 +刚才在 剛纔在 +刚才是 剛纔是 +刚板硬正 剛板硬正 +刚柔并济 剛柔並濟 +创业板 創業板 +创业板上市 創業板上市 +创价学会 創價學會 +创伤后 創傷後 +创伤后压力 創傷後壓力 +创伤后压力紊乱 創傷後壓力紊亂 +创作出 創作出 +创作曲 創作曲 +创出 創出 +创制 創制 創製 +创制权 創制權 +创历年 創歷年 +创巨 創鉅 +创意曲 創意曲 +创意杯 創意盃 +创汇 創匯 +创立出 創立出 +创纪录 創紀錄 +创获 創穫 +创记录 創記錄 +创造出 創造出 +创造出来 創造出來 +创面 創面 +初冬 初冬 +初出茅庐 初出茅廬 +初升 初升 +初发 初發 +初发芙蓉 初發芙蓉 +初唐四杰 初唐四傑 +初回 初回 +初征 初征 +初志 初志 +初愿 初願 +初次见面 初次見面 +初登板 初登板 +初秋 初秋 +初秋图 初秋圖 +初级关系 初級關係 +初级团体 初級團體 +初选制 初選制 +初露才华 初露才華 +判别 判別 +判别式 判別式 +判据 判據 +判断出 判斷出 +判然不合 判然不合 +判若云泥 判若雲泥 +別干净 別乾淨 +刨出来 刨出來 +利于 利於 +利古里亚 利古里亞 +利多于弊 利多於弊 +利多出尽 利多出盡 +利字当头 利字當頭 +利害关系 利害關係 +利害关系人 利害關係人 +利害关系方 利害關係方 +利害冲突 利害衝突 +利弊参半 利弊參半 +利得汇 利得彙 +利息支出 利息支出 +利托 利托 +利析秋毫 利析秋毫 +利欲 利慾 +利欲心 利慾心 +利欲熏心 利慾薰心 +利欲薰心 利慾薰心 +利比里亚 利比里亞 +利用价值 利用價值 +利用系数 利用係數 +利益团体 利益團體 +利益集团 利益集團 +利空出尽 利空出盡 +利纳克斯 利納克斯 +利默里克 利默里克 +别上 別上 +别上去 別上去 +别上来 別上來 +别下 別下 +别下去 別下去 +别下来 別下來 +别业 別業 +别个 別個 +别义 別義 +别乡 別鄉 +别乱 別亂 +别了 別了 +别于 別於 +别人 別人 +别人的 別人的 +别人的孩子死不完 別人的孩子死不完 +别人的肉偎不热 別人的肉偎不熱 +别人肉帖不在腮颊上 別人肉帖不在腮頰上 +别从 別從 +别传 別傳 +别体 別體 +别作一眼 別作一眼 +别使 別使 +别倒 別倒 +别傻 別傻 +别傻了 別傻了 +别像 別像 +别克 別克 +别党 別黨 +别关 別關 +别具 別具 +别具一格 別具一格 +别具匠心 別具匠心 +别具只眼 別具隻眼 +别具慧眼 別具慧眼 +别具炉锤 別具爐錘 +别具肺肠 別具肺腸 +别再 別再 +别出 別出 +别出去 別出去 +别出心裁 別出心裁 +别出新裁 別出新裁 +别出机杼 別出機杼 +别出来 別出來 +别创新格 別創新格 +别别扭扭 彆彆扭扭 +别到 別到 +别刻 別刻 +别办 別辦 +别加 別加 +别动 別動 +别动队 別動隊 +别劲 別勁 +别区 別區 +别去 別去 +别县 別縣 +别又 別又 +别受 別受 +别变 別變 +别口气 彆口氣 +别句 別句 +别只 別隻 +别叫 別叫 +别史 別史 +别号 別號 +别名 別名 +别后 別後 +别后寒温 別後寒溫 +别向 別向 +别吓 別嚇 +别吓人 別嚇人 +别哭 別哭 +别嘴 彆嘴 +别国 別國 +别在 別在 +别墅 別墅 +别墅区 別墅區 +别墨 別墨 +别处 別處 +别太客气 別太客氣 +别套 別套 +别子 別子 +别字 別字 +别守 別守 +别客气 別客氣 +别室 別室 +别宫祭江 別宮祭江 +别害羞 別害羞 +别家 別家 +别寄 別寄 +别对 別對 +别将 別將 +别局 別局 +别岁 別歲 +别巷 別巷 +别师 別師 +别庄 別莊 +别开 別開 +别开生面 別開生面 +别开蹊径 別開蹊徑 +别异 別異 +别弹 別彈 +别强 彆強 +别当 別當 +别得 別得 +别忙 別忙 +别念 別念 +别急 別急 +别怪 別怪 +别情 別情 +别情依依 別情依依 +别想 別想 +别意 別意 +别成 別成 +别房 別房 +别手 別手 +别扭 彆扭 +别把 別把 +别拍 別拍 +别拔 別拔 +别拗 彆拗 +别拿 別拿 +别挤 別擠 +别排 別排 +别描头 別描頭 +别提 別提 +别提了 別提了 +别搬 別搬 +别支 別支 +别收 別收 +别教 別教 +别数 別數 +别族 別族 +别无 別無 +别无他法 別無他法 +别无他物 別無他物 +别无他用 別無他用 +别无分号 別無分號 +别无选择 別無選擇 +别无长物 別無長物 +别日南鸿才北去 別日南鴻纔北去 +别是 別是 +别替 別替 +别有 別有 +别有天地 別有天地 +别有居心 別有居心 +别有所指 別有所指 +别有洞天 別有洞天 +别有用心 別有用心 +别有用意 別有用意 +别有肺肠 別有肺腸 +别有韵味 別有韻味 +别有风味 別有風味 +别本 別本 +别杀 別殺 +别材 別材 +别束 別束 +别来 別來 +别来无恙 別來無恙 +别枝 別枝 +别架 別架 +别栋 別棟 +别树一帜 別樹一幟 +别树一旗 別樹一旗 +别树一格 別樹一格 +别校 別校 +别样 別樣 +别棵 別棵 +别殿 別殿 +别气 彆氣 +别法 別法 +别派 別派 +别爲 別爲 +别现 別現 +别班 別班 +别理 別理 +别生 別生 +别生枝节 別生枝節 +别生气 別生氣 +别用 別用 +别由 別由 +别白 別白 +别的 別的 +别的人 別的人 +别省 別省 +别看 別看 +别着 彆着 +别离 別離 +别种 別種 +别种的 別種的 +别科 別科 +别称 別稱 +别笑 別笑 +别第 別第 +别答 別答 +别筵 別筵 +别管 別管 +别箱 別箱 +别线 別線 +别组 別組 +别给 別給 +别绪 別緒 +别罐 別罐 +别脚 別腳 +别致 別緻 +别致有趣 別致有趣 +别苗头 別苗頭 +别被 別被 +别裁 別裁 +别裁伪体 別裁僞體 +别解 別解 +别讨 別討 +别让 別讓 +别记 別記 +别讲 別講 +别论 別論 +别识别见 別識別見 +别试 別試 +别话 別話 +别说 別說 +别说出 別說出 +别说到 別說到 +别说起 別說起 +别请 別請 +别课 別課 +别调 別調 +别谈 別談 +别赋 別賦 +别赏 別賞 +别走 別走 +别起 別起 +别起来 別起來 +别趣 別趣 +别跑 別跑 +别踢 別踢 +别转 別轉 +别输 別輸 +别辟 別闢 +别辟蹊径 別闢蹊徑 +别辟门户 別闢門戶 +别过 別過 +别过去 別過去 +别过头 別過頭 +别过来 別過來 +别逃 別逃 +别针 別針 +别队 別隊 +别院 別院 +别除权 別除權 +别集 別集 +别项 別項 +别颏腮 別頦腮 +别颗 別顆 +别题 別題 +别风淮雨 別風淮雨 +别馆 別館 +别首 別首 +别驾 別駕 +别骑 別騎 +别鹤 別鶴 +别鹤孤鸾 別鶴孤鸞 +别鹤操 別鶴操 +别鹤离鸾 別鶴離鸞 +刬恶除奸 剗惡除奸 +刮上 刮上 +刮下 刮下 +刮下去 刮下去 +刮下来 刮下來 +刮书背 刮書背 +刮了 颳了 +刮伤 刮傷 +刮倒 颳倒 +刮冷风 刮冷風 +刮出 刮出 +刮刀 刮刀 +刮刮 刮刮 +刮刮乐 刮刮樂 +刮刮匝匝 刮刮匝匝 +刮刮卡 刮刮卡 +刮刮叫 刮刮叫 +刮刮杂杂 刮刮雜雜 +刮刮而谈 刮刮而談 +刮到 刮到 +刮削 刮削 +刮剌 刮剌 +刮剌剌 刮剌剌 +刮去 颳去 +刮取 刮取 +刮喇 刮喇 +刮在 刮在 +刮地器 刮地器 +刮地皮 刮地皮 +刮垢 刮垢 +刮垢磨光 刮垢磨光 +刮大风 颳大風 +刮头 刮頭 +刮好 刮好 +刮子 刮子 +刮宫术 刮宮術 +刮得 颳得 +刮打 刮打 +刮拉 刮拉 +刮掉 刮掉 +刮搭板儿 刮搭板兒 +刮摩 刮摩 +刮来刮去 刮來刮去 +刮毒 刮毒 +刮涎 刮涎 +刮痕 刮痕 +刮痧 刮痧 +刮的 刮的 +刮皮刀 刮皮刀 +刮目相待 刮目相待 +刮目相看 刮目相看 +刮着 颳着 +刮研 刮研 +刮破 刮破 +刮肠洗胃 刮腸洗胃 +刮胡 刮鬍 +刮胡刀 刮鬍刀 +刮胡子 刮鬍子 +刮脸 刮臉 +刮脸刀 刮臉刀 +刮脸皮 刮臉皮 +刮舌 刮舌 +刮舌子 刮舌子 +刮补 刮補 +刮言 刮言 +刮走 颳走 +刮起 颳起 +刮起来 刮起來 +刮躁 刮躁 +刮过 刮過 +刮过去 刮過去 +刮过来 刮過來 +刮铲 刮鏟 +刮除 刮除 +刮雪 颳雪 +刮须 刮鬚 +刮风 颳風 +刮风下雪倒便宜 刮風下雪倒便宜 +刮风后 颳風後 +刮骨 刮骨 +刮骨去毒 刮骨去毒 +刮骨疗毒 刮骨療毒 +到不了 到不了 +到了 到了 +到岸价 到岸價 +到岸价格 到岸價格 +到此一游 到此一遊 +到达签证 到達簽證 +到那个时候 到那個時候 +到那里 到那裏 +制中 制中 +制为 製爲 +制举 制舉 +制义 制義 +制书 制書 +制于 制於 +制件 製件 +制伏 制伏 +制住 制住 +制作 製作 +制作业 製作業 +制作人 製作人 +制作出 製作出 +制作出来 製作出來 +制作商 製作商 +制作好 製作好 +制作成 製作成 +制作群 製作羣 +制作者 製作者 +制作费 製作費 +制假 製假 +制做 製做 +制军 制軍 +制冰 製冰 +制冰机 製冰機 +制冷 製冷 +制冷剂 製冷劑 +制冷机 製冷機 +制出 製出 +制剂 製劑 +制动 制動 +制动器 制動器 +制动因子 制動因子 +制动火箭 制動火箭 +制动缸 制動缸 +制动踏板 制動踏板 +制勘 制勘 +制压射击 制壓射擊 +制取 製取 +制变 制變 +制台 制臺 +制命 制命 +制品 製品 +制售 製售 +制图 製圖 +制图员 製圖員 +制图学 製圖學 +制图室 製圖室 +制图尺 製圖尺 +制图师 製圖師 +制图板 製圖板 +制图样 製圖樣 +制图桌 製圖桌 +制图者 製圖者 +制图车 製圖車 +制坯 製坯 +制备 製備 +制定 制定 +制定出 制定出 +制定出来 制定出來 +制宪 制憲 +制宪会议 制憲會議 +制宪权 制憲權 +制导 制導 +制币 制幣 +制度 制度 +制度化 制度化 +制度性 制度性 +制度法 制度法 +制度面 制度面 +制式 制式 +制式化 制式化 +制得 製得 +制成 製成 +制成品 製成品 +制播 製播 +制敌机先 制敵機先 +制新宪 制新憲 +制服 制服 +制服呢 制服呢 +制服警察 制服警察 +制材 製材 +制梃 制梃 +制止 制止 +制毒 製毒 +制氧 製氧 +制水阀 制水閥 +制法 製法 +制浆 製漿 +制海 制海 +制海权 制海權 +制爲 製爲 +制片 製片 +制片人 製片人 +制片厂 製片廠 +制片商 製片商 +制片家 製片家 +制版 製版 +制版术 製版術 +制盐 製鹽 +制礼 制禮 +制礼作乐 制禮作樂 +制科 制科 +制程 製程 +制空 制空 +制空权 制空權 +制策 制策 +制签 制籤 +制糖 製糖 +制糖厂 製糖廠 +制约 制約 +制约刺激 制約刺激 +制约反应 制約反應 +制纸 製紙 +制胜 制勝 +制胜之道 制勝之道 +制艺 制藝 +制茶 製茶 +制药 製藥 +制药业 製藥業 +制药企业 製藥企業 +制药厂 製藥廠 +制衡 制衡 +制衡作用 制衡作用 +制衣 製衣 +制衣厂 製衣廠 +制表 製表 +制裁 制裁 +制裁案 制裁案 +制订 制訂 +制诰 制誥 +制造 製造 +制造业 製造業 +制造业者 製造業者 +制造出 製造出 +制造出来 製造出來 +制造厂 製造廠 +制造厂商 製造廠商 +制造品 製造品 +制造商 製造商 +制造器 製造器 +制造场 製造場 +制造成 製造成 +制造术 製造術 +制造者 製造者 +制造费用 製造費用 +制酸剂 制酸劑 +制酸性 製酸性 +制醣 制醣 +制钟 制鐘 +制钱 制錢 +制限 制限 +制限选举 制限選舉 +制陶 製陶 +制陶工人 製陶工人 +制面 制面 +制面具 製面具 +制革 製革 +制革厂 製革廠 +制革工厂 製革工廠 +制鞋 製鞋 +制鞋业 製鞋業 +制鞋匠 製鞋匠 +制鞋工人 製鞋工人 +制音器 制音器 +制高点 制高點 +刷卷 刷卷 +刷新纪录 刷新紀錄 +刺不准 刺不準 +刺出 刺出 +刺出去 刺出去 +刺出来 刺出來 +刺参 刺蔘 +刺头泥里陷 刺頭泥裏陷 +刺干 刺干 +刺探出 刺探出 +刺杀出局 刺殺出局 +刺枪术 刺槍術 +刺激启动不同步 刺激啓動不同步 +刺绣 刺繡 +刺股悬梁 刺股懸梁 +刺胳针 刺胳針 +刺针 刺針 +刺骨悬梁 刺骨懸梁 +刻个 刻個 +刻了 刻了 +刻于 刻於 +刻出 刻出 +刻划 刻劃 +刻划入微 刻劃入微 +刻划出 刻劃出 +刻半钟 刻半鐘 +刻多钟 刻多鐘 +刻录机 刻錄機 +刻板 刻板 +刻板印象 刻板印象 +刻蜡纸 刻蠟紙 +刻足适屦 刻足適屨 +刻钟 刻鐘 +剂量当量 劑量當量 +剃了 剃了 +剃发 剃髮 +剃发为尼 剃髮爲尼 +剃发令 剃髮令 +剃发留辫 剃髮留辮 +剃发铺 剃髮鋪 +剃头发 剃頭髮 +剃胡 剃鬍 +剃须 剃鬚 +剃须刀 剃鬚刀 +剉折 剉折 +削了 削了 +削价 削價 +削发 削髮 +削发为僧 削髮爲僧 +削发为尼 削髮爲尼 +削发披缁 削髮披緇 +削苹果 削蘋果 +削足适履 削足適履 +削趾适屦 削趾適屨 +削面 削麪 +前不巴村后不巴店 前不巴村後不巴店 +前事不忘后事之师 前事不忘後事之師 +前亲晚后 前親晚後 +前人失脚后人把滑 前人失腳後人把滑 +前人撒土迷了后人的眼 前人撒土迷了後人的眼 +前人栽树后人乘凉 前人栽樹後人乘涼 +前人种树 前人種樹 +前仆后继 前仆後繼 +前仆后起 前仆後起 +前仇 前仇 +前仰后合 前仰後合 +前修 前修 +前俯后仰 前俯後仰 +前倨后卑 前倨後卑 +前倨后恭 前倨後恭 +前儿个 前兒個 +前几天 前幾天 +前出后空 前出後空 +前前后后 前前後後 +前功尽废 前功盡廢 +前功尽弃 前功盡棄 +前功尽灭 前功盡滅 +前厂 前廠 +前台 前臺 +前叶 前葉 +前合后仰 前合後仰 +前合后偃 前合後偃 +前后 前後 +前后任 前後任 +前后夹攻 前後夾攻 +前后左右 前後左右 +前后文 前後文 +前后相悖 前後相悖 +前后相随 前後相隨 +前后矛盾 前後矛盾 +前后脚儿 前後腳兒 +前向拥塞通知 前向擁塞通知 +前呼后应 前呼後應 +前呼后拥 前呼後擁 +前回 前回 +前因后果 前因後果 +前奏曲 前奏曲 +前婚后嫁 前婚後嫁 +前家后继 前家後繼 +前志 前志 +前怕狼后怕虎 前怕狼後怕虎 +前思后想 前思後想 +前扑后继 前撲後繼 +前挽后推 前挽後推 +前推后拥 前推後擁 +前新系 前新系 +前无古人后无来者 前無古人後無來者 +前核 前核 +前歌后舞 前歌後舞 +前甲板 前甲板 +前短后长 前短後長 +前程万里 前程萬里 +前缘未了 前緣未了 +前苏联 前蘇聯 +前街后巷 前街後巷 +前言不对后语 前言不對後語 +前言不答后语 前言不答後語 +前词汇加工 前詞彙加工 +前词汇语音加工 前詞彙語音加工 +前词汇阶段 前詞彙階段 +前赴后继 前赴後繼 +前车之复 前車之覆 +前车之覆后车之鉴 前車之覆後車之鑑 +前车之鉴 前車之鑑 +前车可鉴 前車可鑑 +前车复后车戒 前車覆後車戒 +前进党 前進黨 +前途未卜 前途未卜 +前遮后拥 前遮後擁 +前郭尔罗斯蒙古族自治县 前郭爾羅斯蒙古族自治縣 +前长后短 前長後短 +前门不进师姑后门不进和尚 前門不進師姑後門不進和尚 +前门打虎后门打狼 前門打虎後門打狼 +前门拒虎后门进狼 前門拒虎後門進狼 +前面 前面 +剑叶 劍葉 +剑合珠还 劍合珠還 +剑术 劍術 +剑杆 劍桿 +剔了 剔了 +剔出 剔出 +剔团圆 剔團圓 +剔团𪢮 剔團圞 +剔庄货 剔莊貨 +剔齿纤 剔齒纖 +剖别 剖別 +剖宫产手术 剖宮產手術 +剖腹产手术 剖腹產手術 +剖腹手术 剖腹手術 +剖面 剖面 +剖面图 剖面圖 +剥制 剝製 +剥尽 剝盡 +剥挽 剝挽 +剥极必复 剝極必復 +剥离症 剝離症 +剥采比 剝採比 +剥面皮 剝面皮 +剧力万钧 劇力萬鈞 +剧团 劇團 +剧坛 劇壇 +剧曲 劇曲 +剧种 劇種 +剧药 劇藥 +剩个 剩個 +剩了 剩了 +剩余 剩餘 +剩余价值 剩餘價值 +剩余定理 剩餘定理 +剩余放射性 剩餘放射性 +剩余辐射 剩餘輻射 +剩馀价值 剩餘價值 +剩馀价值率 剩餘價值率 +剪个 剪個 +剪了 剪了 +剪其发 剪其髮 +剪出 剪出 +剪发 剪髮 +剪发披缁 剪髮披緇 +剪头发 剪頭髮 +剪彩 剪綵 +剪彩仪式 剪彩儀式 +剪恶除奸 剪惡除奸 +剪牡丹喂牛 剪牡丹喂牛 +剪秋罗 剪秋羅 +剪贴出 剪貼出 +副厂长 副廠長 +副团长 副團長 +副性征 副性徵 +副总干事 副總幹事 +副曲 副曲 +副法向量 副法向量 +割了 割了 +割出 割出 +割回 割回 +割回去 割回去 +割回来 割回來 +割圆术 割圓術 +割据 割據 +割舍 割捨 +割舍不下 割捨不下 +剺面 剺面 +剿获 剿獲 +劈划 劈劃 +劈地价来 劈地價來 +劈尸万段 劈屍萬段 +劈心里 劈心裏 +劈挂拳 劈掛拳 +劈脑后 劈腦後 +劈里 劈里 +劈里啪啦 劈里啪啦 +劈面 劈面 +力不从愿 力不從願 +力不同科 力不同科 +力不胜任 力不勝任 +力不自胜 力不自勝 +力争上游 力爭上游 +力克 力克 +力克制 力剋制 +力回馈 力回饋 +力困筋乏 力困筋乏 +力尽 力盡 +力尽神危 力盡神危 +力尽筋疲 力盡筋疲 +力尽筋舒 力盡筋舒 +力征 力征 +力拼众敌 力拼衆敵 +力挽 力挽 +力挽狂澜 力挽狂瀾 +力敌万人 力敵萬人 +力敌万夫 力敵萬夫 +力系 力系 +力胜 力勝 +力薄才疏 力薄才疏 +力量党 力量黨 +劝出 勸出 +劝出去 勸出去 +劝出来 勸出來 +劝善惩恶 勸善懲惡 +劝善戒恶 勸善戒惡 +劝善黜恶 勸善黜惡 +劝回 勸回 +劝回去 勸回去 +劝回来 勸回來 +办伙 辦伙 +办公台 辦公檯 +办公室里 辦公室裏 +办后事 辦後事 +功同良相 功同良相 +功同赏异 功同賞異 +功大于过 功大於過 +功布 功布 +功流万世 功流萬世 +功率恶化 功率惡化 +功率表 功率表 +功率输出 功率輸出 +功绩制 功績制 +功能团 功能團 +功能团体 功能團體 +功能磁共振成像术 功能磁共振成像術 +功能表 功能表 +功致 功緻 +功课表 功課表 +功过参半 功過參半 +加个 加個 +加了 加了 +加于 加於 +加仑 加侖 +加价 加價 +加出 加出 +加利波里 加利波里 +加勒比海共同体 加勒比海共同體 +加勒比海共同市场 加勒比海共同市場 +加升 加升 +加卷 加捲 +加发 加發 +加回 加回 +加回去 加回去 +加回来 加回來 +加害于 加害於 +加密后的 加密後的 +加尔克汉德省 加爾克漢德省 +加州技术学院 加州技術學院 +加工出口 加工出口 +加工出口区 加工出口區 +加工厂 加工廠 +加巴里雅 加巴里雅 +加布 加布 +加强控制 加強控制 +加强管制 加強管制 +加当 加當 +加拉干达 加拉干達 +加拿大铝业集团 加拿大鋁業集團 +加挂 加掛 +加挂车厢 加掛車廂 +加杯 加杯 +加杯水 加杯水 +加氢精制 加氫精制 +加油团 加油團 +加注 加註 加注 +加涂 加塗 +加签 加簽 +加签证 加簽證 +加荣耀于 加榮耀於 +加药 加藥 +加解密系统 加解密系統 +加达里 加達裏 +加速踏板 加速踏板 +加里 加里 +加里宁 加裏寧 +加里宁格勒 加里寧格勒 +加里宁格勒州 加裏寧格勒州 +加里曼丹 加裏曼丹 +加里曼丹岛 加裏曼丹島 +加里波的 加里波的 +加里波第 加里波第 +加里肋亚 加里肋亞 +加里肋亚海 加里肋亞海 +务须 務須 +劣于 劣於 +劣种 劣種 +劣缺搊搜 劣缺搊搜 +劣药 劣藥 +动不了 動不了 +动了 動了 +动力系统 動力系統 +动向 動向 +动向不明 動向不明 +动如参商 動如參商 +动干戈 動干戈 +动念 動念 +动态范围 動態範圍 +动手术 動手術 +动物系 動物系 +动物纤维 動物纖維 +动物育种 動物育種 +动画艺术 動畫藝術 +动荡 動盪 +动荡不安 動盪不安 +动荡不定 動盪不定 +动配合 動配合 +助于 助於 +助恶 助惡 +助选团 助選團 +努瓦克肖特 努瓦克肖特 +劫余 劫餘 +劫制 劫制 +劫后余生 劫後餘生 +劫后英雄传 劫後英雄傳 +劫后馀烬 劫後餘燼 +劫后馀生 劫後餘生 +劬劳顾复 劬勞顧復 +励志 勵志 +励志书 勵志書 +劲度系数 勁度係數 +劲秋 勁秋 +劲舞团 勁舞團 +劲骨丰肌 勁骨豐肌 +劳军团 勞軍團 +劳力士表 勞力士錶 +劳务出口 勞務出口 +劳动党 勞動黨 +劳动合同 勞動合同 +劳动基准法 勞動基準法 +劳动模范 勞動模範 +劳发 勞發 +劳台重 勞臺重 +劳困 勞困 +劳工党 勞工黨 +劳工团体 勞工團體 +劳工退休准备金 勞工退休準備金 +劳资关系 勞資關係 +劳资合作 勞資合作 +劳逸结合 勞逸結合 +劳里斯 勞里斯 +劳雇关系 勞僱關係 +劾系 劾繫 +势不可当 勢不可當 +势不并立 勢不並立 +势力并行 勢力並行 +势力范围 勢力範圍 +势合形离 勢合形離 +势同水火 勢同水火 +势穷力极 勢窮力極 +勃发 勃發 +勃德修正案 勃德修正案 +勃极烈 勃極烈 +勃郁 勃鬱 +勇于 勇於 +勇于认错 勇於認錯 +勋章 勳章 +勒借 勒借 +勒克 勒克 +勒克司 勒克司 +勒克莱尔 勒克萊爾 +勒里勒得 勒里勒得 +勘合 勘合 +勘误表 勘誤表 +募兵制 募兵制 +勤仆 勤僕 +勤俭朴实 勤儉樸實 +勤俭朴素 勤儉樸素 +勤劳致富 勤勞致富 +勤朴 勤樸 +勺药 勺藥 +勾了 勾了 +勾出 勾出 +勾划 勾劃 +勾勒出 勾勒出 +勾干 勾幹 +勾当 勾當 +勾心斗角 勾心鬥角 +勾画出 勾畫出 +勾魂荡魄 勾魂蕩魄 +勿念 勿念 +勿施于人 勿施於人 +勿药 勿藥 +勿药有喜 勿藥有喜 +勿药而愈 勿藥而愈 +勿里洞岛 勿里洞島 +匀出 勻出 +匀出来 勻出來 +匀面 勻面 +包乘制 包乘制 +包产到户制 包產到戶制 +包价旅游 包價旅遊 +包伙 包伙 +包准 包準 +包利克莱荳斯 包利克萊荳斯 +包占 包占 +包干 包乾 +包干儿 包乾兒 +包干制 包乾制 +包庄 包莊 +包待制 包待制 +包扎 包紮 +包扎法 包紮法 +包承制 包承制 +包揽闲事 包攬閒事 +包班制 包班制 +包皮环切术 包皮環切術 +包种茶 包種茶 +包罗万象 包羅萬象 +包衣种子 包衣種子 +包裹表决 包裹表決 +包谷 包穀 +包谷花 包谷花 +包里斯 包里斯 +包髻团衫 包髻團衫 +匋斋吉金录 匋齋吉金錄 +匏有苦叶 匏有苦葉 +匏系 匏繫 +化了 化了 +化出 化出 +化出化入 化出化入 +化合 化合 +化合价 化合價 +化合反应 化合反應 +化合态 化合態 +化合性 化合性 +化合物 化合物 +化合量 化合量 +化妆台 化妝臺 +化学合成 化學合成 +化学工厂 化學工廠 +化学弹药 化學彈藥 +化学当量 化學當量 +化学战斗部 化學戰斗部 +化学系 化學系 +化学纤维 化學纖維 +化工厂 化工廠 +化工系 化工系 +化干戈为玉帛 化干戈爲玉帛 +化暗为明 化暗爲明 +化纤 化纖 +化纤厂 化纖廠 +化肥厂 化肥廠 +化隆回族自治县 化隆回族自治縣 +化验台 化驗臺 +北京中医药大学 北京中醫藥大學 +北京周报 北京週報 +北京国家游泳中心 北京國家游泳中心 +北京师范大学 北京師範大學 +北京汽车制造厂有限公司 北京汽車製造廠有限公司 +北京艺术学院 北京藝術學院 +北仑 北侖 +北仑区 北侖區 +北叶门 北葉門 +北向 北向 +北周 北周 +北回 北迴 +北回归线 北迴歸線 +北回线 北迴線 +北回铁路 北迴鐵路 +北岳 北嶽 +北征 北征 +北斗 北斗 +北斗七星 北斗七星 +北斗星 北斗星 +北斗镇 北斗鎮 +北曲 北曲 +北极 北極 +北极光 北極光 +北极冻原 北極凍原 +北极区 北極區 +北极圈 北極圈 +北极地区 北極地區 +北极星 北極星 +北极星飞弹 北極星飛彈 +北极海 北極海 +北极熊 北極熊 +北极犬 北極犬 +北极狐 北極狐 +北极锋 北極鋒 +北极鲸 北極鯨 +北柴胡 北柴胡 +北洋系 北洋系 +北瓦兹里斯坦 北瓦茲里斯坦 +北辕适楚 北轅適楚 +北里 北里 +北面 北面 +北面称臣 北面稱臣 +北马里亚纳 北馬裏亞納 +北马里亚纳群岛 北馬裏亞納羣島 +匙扣 匙扣 +匡合 匡合 +匡复 匡復 +匡当 匡當 +匡济之才 匡濟之才 +匣里龙吟 匣裏龍吟 +匪党 匪黨 +匪干 匪幹 +匪徒集团 匪徒集團 +匹似闲 匹似閒 +匹头里 匹頭裏 +匹面 匹面 +匹马只轮 匹馬隻輪 +区分出 區分出 +区划 區劃 +区别 區別 +区别性 區別性 +区别词 區別詞 +区域网路技术 區域網路技術 +区域范围 區域範圍 +区段征收 區段徵收 +医卜星相 醫卜星相 +医学系 醫學系 +医宗金鉴 醫宗金鑑 +医托 醫托 +医术 醫術 +医疗团 醫療團 +医药 醫藥 +医药业 醫藥業 +医药分业 醫藥分業 +医药分离 醫藥分離 +医药品 醫藥品 +医药商店 醫藥商店 +医药学 醫藥學 +医药气候学 醫藥氣候學 +医药界 醫藥界 +医药罔效 醫藥罔效 +医药费 醫藥費 +医院里 醫院裏 +匾扎 匾扎 +匿于 匿於 +十万 十萬 +十三经注疏 十三經注疏 +十个 十個 +十个月 十個月 +十九万 十九萬 +十九个 十九個 +十二万 十二萬 +十二万分 十二萬分 +十二个 十二個 +十二指肠虫 十二指腸蟲 +十二木表法 十二木表法 +十二面体 十二面體 +十五万 十五萬 +十五个 十五個 +十五个吊桶打水 十五個吊桶打水 +十余 十餘 +十余只 十餘隻 +十余里 十餘里 +十克 十克 +十克拉 十克拉 +十八万 十八萬 +十八个 十八個 +十八姑娘 十八姑娘 +十公克 十公克 +十公升 十公升 +十公里 十公里 +十六万 十六萬 +十六个 十六個 +十六国春秋 十六國春秋 +十六进制 十六進制 +十冬腊月 十冬臘月 +十几 十幾 +十几万 十幾萬 +十几个 十幾個 +十几个月 十幾個月 +十几二十 十幾二十 +十几人 十幾人 +十几元 十幾元 +十几分 十幾分 +十几号 十幾號 +十几块 十幾塊 +十几天 十幾天 +十几岁 十幾歲 +十几年 十幾年 +十几次 十幾次 +十出戏 十齣戲 +十出祁山 十出祁山 +十分干 十分乾 +十分钟 十分鐘 +十划 十劃 +十加仑 十加侖 +十卷 十卷 +十发 十發 +十只 十隻 +十台 十臺 +十叶 十葉 +十周 十週 +十周年 十週年 +十四万 十四萬 +十四个 十四個 +十回 十回 +十团 十團 +十国春秋 十國春秋 +十多万 十多萬 +十多个 十多個 +十多只 十多隻 +十天半个月 十天半個月 +十天后 十天後 +十天干 十天干 +十字军东征 十字軍東征 +十字军远征 十字軍遠征 +十干 十干 +十年后 十年後 +十恶 十惡 +十恶不赦 十惡不赦 +十扎 十紮 +十方地面 十方地面 +十曲 十曲 +十来个 十來個 +十杯 十杯 +十杯水 十杯水 +十杯酒 十杯酒 +十点钟 十點鐘 +十种 十種 +十秒钟 十秒鐘 +十荡十决 十蕩十決 +十谒朱门九不开 十謁朱門九不開 +十足虫 十足蟲 +十进位制 十進位制 +十进制 十進制 +十进算术 十進算術 +十郎八当 十郎八當 +十里 十里 +十里余 十里餘 +十里洋场 十里洋場 +十里长亭 十里長亭 +十里长亭无客走 十里長亭無客走 +十针 十針 +十面 十面 +十面体 十面體 +十面埋伏 十面埋伏 +十鼠同穴 十鼠同穴 +千万 千萬 +千万买邻 千萬買鄰 +千万分之一 千萬分之一 +千万千万 千萬千萬 +千万富翁 千萬富翁 +千万年 千萬年 +千不幸万不幸 千不幸萬不幸 +千不是万不是 千不是萬不是 +千丝万缕 千絲萬縷 +千个 千個 +千乘 千乘 +千乘万骑 千乘萬騎 +千乞 千乞 +千了百当 千了百當 +千亩 千畝 +千人所指 千人所指 +千亿 千億 +千仇万恨 千仇萬恨 +千仓万箱 千倉萬箱 +千仞 千仞 +千伏 千伏 +千位 千位 +千位元 千位元 +千余 千餘 +千余只 千餘隻 +千余里 千餘里 +千佛塔 千佛塔 +千佛山 千佛山 +千佛洞 千佛洞 +千依万顺 千依萬順 +千依百顺 千依百順 +千兆 千兆 +千克 千克 +千兵万马 千兵萬馬 +千军万马 千軍萬馬 +千军易得一将难求 千軍易得一將難求 +千刀万剁 千刀萬剁 +千刀万剐 千刀萬剮 +千刁万恶 千刁萬惡 +千分 千分 +千分之 千分之 +千分之一 千分之一 +千分之三 千分之三 +千分之二 千分之二 +千分表 千分表 +千千 千千 +千千万万 千千萬萬 +千卡 千卡 +千变万化 千變萬化 +千变万状 千變萬狀 +千变万轸 千變萬軫 +千古 千古 +千古事 千古事 +千古传诵 千古傳誦 +千古恨 千古恨 +千古未闻 千古未聞 +千古流传 千古流傳 +千古独步 千古獨步 +千古绝唱 千古絕唱 +千古罪人 千古罪人 +千古遗恨 千古遺恨 +千只 千隻 +千只足够 千只足夠 +千叮万嘱 千叮萬囑 +千叶 千葉 +千叶县 千葉縣 +千吨 千噸 +千吨级核武器 千噸級核武器 +千周 千周 +千呼万唤 千呼萬喚 +千咒万骂 千咒萬罵 +千唤万唤 千喚萬喚 +千回百折 千迴百折 +千回百转 千迴百轉 +千多只 千多隻 +千天后 千天後 +千夫 千夫 +千夫所指 千夫所指 +千夫长 千夫長 +千头万绪 千頭萬緒 +千头橘奴 千頭橘奴 +千头百绪 千頭百緒 +千奇百怪 千奇百怪 +千妥万妥 千妥萬妥 +千妥万当 千妥萬當 +千姿百态 千姿百態 +千娇百媚 千嬌百媚 +千娇百态 千嬌百態 +千孔百疮 千孔百瘡 +千字文 千字文 +千字节 千字節 +千家 千家 +千家万户 千家萬戶 +千家姓 千家姓 +千家诗 千家詩 +千寻 千尋 +千层糕 千層糕 +千层面 千層麪 +千山万壑 千山萬壑 +千山万水 千山萬水 +千山区 千山區 +千山山脉 千山山脈 +千岁 千歲 +千岁一时 千歲一時 +千岐万辙 千岐萬轍 +千岛列岛 千島列島 +千岛湖 千島湖 +千岛湖事件 千島湖事件 +千岛群岛 千島羣島 +千岛酱 千島醬 +千岩万壑 千巖萬壑 +千岩万谷 千巖萬谷 +千岩竞秀 千巖競秀 +千嶂 千嶂 +千差万别 千差萬別 +千差万差来人不差 千差萬差來人不差 +千帆 千帆 +千年 千年 +千年万载 千年萬載 +千年前 千年前 +千年怪兽 千年怪獸 +千年田地八百主 千年田地八百主 +千年艾 千年艾 +千态万状 千態萬狀 +千思万想 千思萬想 +千总 千總 +千恩万谢 千恩萬謝 +千愁万恨 千愁萬恨 +千愁万绪 千愁萬緒 +千愁万虑 千愁萬慮 +千户 千戶 +千手千眼观音 千手千眼觀音 +千扎 千紮 +千挑万选 千挑萬選 +千挑百选 千挑百選 +千推万阻 千推萬阻 +千支万派 千支萬派 +千斤 千斤 +千斤重担 千斤重擔 +千斤顶 千斤頂 +千方万计 千方萬計 +千方百计 千方百計 +千日 千日 +千日红 千日紅 +千日菊 千日菊 +千日酒 千日酒 +千村万落 千村萬落 +千条万端 千條萬端 +千条万绪 千條萬緒 +千杯 千杯 +千欢万喜 千歡萬喜 +千步 千步 +千湖国 千湖國 +千点 千點 +千牛 千牛 +千牛刀 千牛刀 +千状万态 千狀萬態 +千状万端 千狀萬端 +千瓦 千瓦 +千瓦小时 千瓦小時 +千瓦时 千瓦時 +千生万死 千生萬死 +千疮百孔 千瘡百孔 +千百万 千百萬 +千百年 千百年 +千百成群 千百成羣 +千皓宣 千皓宣 +千真万真 千真萬真 +千真万确 千真萬確 +千石 千石 +千碱基对 千鹼基對 +千禧 千禧 +千禧年 千禧年 +千秋 千秋 +千秋万世 千秋萬世 +千秋万代 千秋萬代 +千秋万古 千秋萬古 +千秋万岁 千秋萬歲 +千秋佳城 千秋佳城 +千秋大业 千秋大業 +千秋庙 千秋廟 +千秋节 千秋節 +千章 千章 +千端万绪 千端萬緒 +千算万算不值天一划 千算萬算不值天一劃 +千篇一律 千篇一律 +千米 千米 +千粒重 千粒重 +千红万紫 千紅萬紫 +千经万卷 千經萬卷 +千绪万端 千緒萬端 +千羊之皮不如一狐之腋 千羊之皮不如一狐之腋 +千般 千般 +千般万样 千般萬樣 +千英宇 千英宇 +千虑一失 千慮一失 +千虑一得 千慮一得 +千言 千言 +千言万语 千言萬語 +千赫 千赫 +千赫兹 千赫茲 +千足港条 千足港條 +千足虫 千足蟲 +千载 千載 +千载一会 千載一會 +千载一合 千載一合 +千载一时 千載一時 +千载一逢 千載一逢 +千载一遇 千載一遇 +千载扬名 千載揚名 +千载独步 千載獨步 +千载难逢 千載難逢 +千辛万苦 千辛萬苦 +千辛百苦 千辛百苦 +千里 千里 +千里一曲 千里一曲 +千里之堤 千里之堤 +千里之外 千里之外 +千里之行 千里之行 +千里命驾 千里命駕 +千里始足下 千里始足下 +千里姻缘一线牵 千裏姻緣一線牽 +千里寄鹅毛 千里寄鵝毛 +千里搭长棚 千里搭長棚 +千里犹面 千里猶面 +千里目 千里目 +千里眼 千里眼 +千里移檄 千里移檄 +千里足 千里足 +千里达 千里達 +千里迢迢 千里迢迢 +千里迢遥 千里迢遙 +千里送鹅毛 千里送鵝毛 +千里镜 千里鏡 +千里馈粮 千里饋糧 +千里马 千里馬 +千里驹 千里駒 +千里鹅毛 千里鵝毛 +千金 千金 +千金一刻 千金一刻 +千金一掷 千金一擲 +千金一笑 千金一笑 +千金一诺 千金一諾 +千金之子 千金之子 +千金之家 千金之家 +千金买笑 千金買笑 +千金买骨 千金買骨 +千金小姐 千金小姐 +千金市骨 千金市骨 +千金敝帚 千金敝帚 +千金方 千金方 +千金裘 千金裘 +千金要方 千金要方 +千金记 千金記 +千金躯 千金軀 +千金难买 千金難買 +千钧 千鈞 +千钧一发 千鈞一髮 +千钧重负 千鈞重負 +千锤百炼 千錘百煉 +千锺粟 千鍾粟 +千镒之裘非一狐之白 千鎰之裘非一狐之白 +千门 千門 +千门万户 千門萬戶 +千闻不如一见 千聞不如一見 +千阳 千陽 +千阳县 千陽縣 +千难万险 千難萬險 +千难万难 千難萬難 +千面人 千面人 +千页群岛 千頁羣島 +千顷陂 千頃陂 +千鬼百怪 千鬼百怪 +千鸟 千鳥 +千鸟渊国家公墓 千鳥淵國家公墓 +升上 升上 +升上去 升上去 +升上来 升上來 +升为 升爲 +升了 升了 +升仙 昇仙 +升任 升任 +升任为 升任爲 +升值 升值 +升入 升入 +升出 升出 +升出来 升出來 +升到 升到 +升力 升力 +升势 升勢 +升势受阻 升勢受阻 +升升 升升 +升华 昇華 +升华作用 昇華作用 +升压 升壓 +升号 升號 +升回 升回 +升回去 升回去 +升回来 升回來 +升在 升在 +升坐 升坐 +升堂 升堂 +升堂入室 升堂入室 +升堂拜母 升堂拜母 +升天 昇天 +升好 升好 +升学 升學 +升学主义 升學主義 +升学压力 升學壓力 +升学率 升學率 +升学班 升學班 +升学考试 升學考試 +升官 升官 +升官发财 升官發財 +升官图 升官圖 +升帐 升帳 +升幂 升冪 +升幂级数 升冪級數 +升幅 升幅 +升平 昇平 +升引 升引 +升得 升得 +升息 升息 +升成 升成 +升斗 升斗 +升斗之禄 升斗之祿 +升斗小民 升斗小民 +升旗 升旗 +升旗仪式 升旗儀式 +升旗典礼 升旗典禮 +升来 升來 +升来升去 升來升去 +升格 升格 +升格为 升格爲 +升格成 升格成 +升水 升水 +升汞 昇汞 +升沉 升沉 +升温 升溫 +升火 升火 +升点 升點 +升爲 升爲 +升班 升班 +升班考试 升班考試 +升的 升的 +升破 升破 +升科 升科 +升空 升空 +升等 升等 +升等考试 升等考試 +升级 升級 +升级性 升級性 +升级成 升級成 +升级换代 升級換代 +升级版 升級版 +升结肠 升結腸 +升职 升職 +升腾 升騰 +升至 升至 +升记号 升記號 +升调 升調 +升起 升起 +升起来 升起來 +升迁 升遷 +升迁制度 升遷制度 +升迁管道 升遷管道 +升过 升過 +升速 升速 +升遐 升遐 +升阳 昇陽 +升阶 升階 +升降 升降 +升降机 升降機 +升降梯 升降梯 +升降舵 升降舵 +升降记号 升降記號 +升限 升限 +升高 升高 +升高为 升高爲 +升高自下 升高自下 +午后 午後 +半个 半個 +半个世纪 半個世紀 +半乳糖血症 半乳糖血症 +半于 半於 +半价 半價 +半价优待 半價優待 +半价倍息 半價倍息 +半保留复制 半保留複製 +半分钟 半分鐘 +半制品 半製品 +半只 半隻 +半吊子 半吊子 +半夜里 半夜裏 +半导体厂 半導體廠 +半岛电视台 半島電視臺 +半干 半乾 +半径范围 半徑範圍 +半托 半托 +半排出期 半排出期 +半日制 半日制 +半日制学校 半日制學校 +半杯 半杯 +半板 半板 +半涂而罢 半塗而罷 +半点钟 半點鐘 +半票价 半票價 +半秒钟 半秒鐘 +半老徐娘 半老徐娘 +半路出家 半路出家 +半里 半里 +半面 半面 +半面之交 半面之交 +半面之旧 半面之舊 +华东师范 華東師範 +华东师范大学 華東師範大學 +华严钟 華嚴鐘 +华中师范大学 華中師範大學 +华冈艺术学校 華岡藝術學校 +华南师范大学 華南師範大學 +华发 華髮 +华尔滋舞曲 華爾滋舞曲 +华彩 華彩 +华志 華志 +华星秋月之章 華星秋月之章 +华核 華覈 +华氏寒暑表 華氏寒暑表 +华润万家 華潤萬家 +华特里德 華特里德 +华纳音乐集团 華納音樂集團 +华胄 華胄 +华表 華表 +华表鹤归 華表鶴歸 +华里 華里 +华阳国志 華陽國志 +协力同心 協力同心 +协合 協合 +协同 協同 +协同作战 協同作戰 +协同作用 協同作用 +协同动作 協同動作 +协奏曲 協奏曲 +协议范本 協議範本 +协议规范 協議規範 +协调出 協調出 +卑梁之衅 卑梁之釁 +卓别林 卓別林 +卓尔出群 卓爾出羣 +卓柏卡布拉 卓柏卡布拉 +单一价 單一價 +单一合体字 單一合體字 +单个儿 單個兒 +单于 單于 +单交种 單交種 +单价 單價 +单位价格 單位價格 +单位信托 單位信托 +单位切向量 單位切向量 +单位制 單位制 +单位向量 單位向量 +单位面积 單位面積 +单克隆 單克隆 +单克隆抗体 單克隆抗體 +单单于 單單於 +单只 單隻 +单只是 單只是 +单叶双曲面 單葉雙曲面 +单向 單向 +单向天线 單向天線 +单向电流 單向電流 +单向行驶 單向行駛 +单向通车 單向通車 +单向道 單向道 +单向阀 單向閥 +单周 單週 +单味药 單味藥 +单夫只妇 單夫隻婦 +单婚制 單婚制 +单子叶 單子葉 +单子叶植物 單子葉植物 +单字表 單字表 +单干 單幹 +单干户 單幹戶 +单张汇票 單張匯票 +单弦 單絃 +单循环赛制 單循環賽制 +单打独斗 單打獨鬥 +单挂号 單掛號 +单据 單據 +单摆 單擺 +单方制剂 單方製劑 +单方向 單方向 +单方面 單方面 +单曲 單曲 +单曲榜 單曲榜 +单杠 單槓 +单杯 單杯 +单板 單板 +单板心合板 單板心合板 +单板机 單板機 +单核 單核 +单核细胞增多症 單核細胞增多症 +单模光纤 單模光纖 +单法货制 單法貨制 +单端孢霉烯类毒素中毒症 單端孢黴烯類毒素中毒症 +单系 單系 +单链 單鏈 +单面 單面 +单面山 單面山 +卖不出去 賣不出去 +卖个破绽 賣個破綻 +卖了 賣了 +卖了儿子招女婿 賣了兒子招女婿 +卖价 賣價 +卖俏营奸 賣俏營奸 +卖俏行奸 賣俏行奸 +卖俏迎奸 賣俏迎奸 +卖光了 賣光了 +卖出 賣出 +卖出去 賣出去 +卖卜 賣卜 +卖卜测字 賣卜測字 +卖卦口没量斗 賣卦口沒量斗 +卖呆 賣呆 +卖奸 賣姦 +卖恶 賣惡 +卖拐 賣柺 +卖油娘子水梳头 賣油娘子水梳頭 +卖炭的掉在面缸里 賣炭的掉在麪缸裏 +卖盐的做雕銮匠 賣鹽的做雕鑾匠 +卖红萝卜 賣紅蘿蔔 +卖膏药的 賣膏藥的 +卖良姜 賣良姜 +卖药 賣藥 +卖药人 賣藥人 +卖荳腐点了河滩地 賣荳腐點了河灘地 +卖金须向识金家 賣金須向識金家 +卖面子 賣面子 +南丰 南豐 +南丰县 南豐縣 +南京钟 南京鐘 +南京钟表 南京鐘錶 +南出 南出 +南北向 南北向 +南北极 南北極 +南台 南臺 +南台工专 南臺工專 +南台湾 南臺灣 +南叶门 南葉門 +南向 南向 +南回 南迴 +南回公路 南迴公路 +南回归线 南迴歸線 +南回线 南迴線 +南回铁路 南迴鐵路 +南宫适 南宮适 +南屏晚钟 南屏晚鐘 +南山并寿 南山並壽 +南山有台 南山有臺 +南山杯 南山盃 +南岳 南嶽 +南征 南征 +南征北伐 南征北伐 +南征北战 南征北戰 +南征北讨 南征北討 +南方周末 南方週末 +南无阿弥陀佛 南無阿彌陀佛 +南曲 南曲 +南朝梁 南朝梁 +南极 南極 +南极仙翁 南極仙翁 +南极光 南極光 +南极呈祥 南極呈祥 +南极圈 南極圈 +南极地区 南極地區 +南极大陆 南極大陸 +南极座 南極座 +南极星沉 南極星沉 +南极星辉 南極星輝 +南极洲 南極洲 +南极洲半岛 南極洲半島 +南极老人星 南極老人星 +南极腾辉 南極騰輝 +南枝向火北枝寒 南枝向火北枝寒 +南汇 南匯 +南汇区 南匯區 +南洋模范 南洋模範 +南游 南遊 +南特杰克 南特傑克 +南筑 南筑 +南箕北斗 南箕北斗 +南胡 南胡 +南里 南里 +南针 南針 +南面 南面 +南面为王 南面爲王 +南面百城 南面百城 +南面称伯 南面稱伯 +南面称孤 南面稱孤 +南面称王 南面稱王 +博克达山 博克達山 +博古图录 博古圖錄 +博士后 博士後 +博学多才 博學多才 +博客里 博客裏 +博尔塔拉蒙古自治州 博爾塔拉蒙古自治州 +博彩 博彩 +博彩业 博彩業 +博文反应系列 博文反應系列 +博斗 博鬥 +博汇 博彙 +博物志 博物志 +博莱克 博萊克 +博蒙特 博蒙特 +博采 博採 +博采众长 博採衆長 +博闻彊志 博聞彊志 +博鳌亚洲论坛 博鰲亞洲論壇 +卜人 卜人 +卜儿 卜兒 +卜冠文 卜冠文 +卜卜 卜卜 +卜卜米 卜卜米 +卜卦 卜卦 +卜卦家 卜卦家 +卜吉 卜吉 +卜商 卜商 +卜夜卜昼 卜夜卜晝 +卜威廉 卜威廉 +卜婿 卜婿 +卜学亮 卜學亮 +卜宅 卜宅 +卜居 卜居 +卜年 卜年 +卜度 卜度 +卜式 卜式 +卜征 卜征 +卜日 卜日 +卜易 卜易 +卜昼卜夜 卜晝卜夜 +卜珓 卜珓 +卜睿哲 卜睿哲 +卜祝 卜祝 +卜窀穸 卜窀穸 +卜筑 卜築 +卜筮 卜筮 +卜筮官 卜筮官 +卜算 卜算 +卜老 卜老 +卜者 卜者 +卜舫济 卜舫濟 +卜蜂 卜蜂 +卜课 卜課 +卜辞 卜辭 +卜邻 卜鄰 +卜骨 卜骨 +卞庄 卞莊 +卞庄子 卞莊子 +卞梁 卞梁 +占〇 佔〇 +占一 佔一 +占七 佔七 +占万 佔万 +占三 佔三 +占上 占上 +占上游 佔上游 +占上风 佔上風 +占下 佔下 +占下风 佔下風 +占不占 佔不佔 +占不足 佔不足 +占世界 佔世界 +占东 佔東 +占两 佔兩 +占个 佔個 +占个位 佔個位 +占中 佔中 +占为 佔爲 +占为己有 佔爲己有 +占主 佔主 +占主导地位 占主導地位 +占九 佔九 +占了 佔了 +占了卜 占了卜 +占二 佔二 +占五 佔五 +占亲 占親 +占人 占人 +占人便宜 佔人便宜 +占亿 佔億 +占优 佔優 +占优势 佔優勢 +占位 佔位 +占住 佔住 +占便宜 佔便宜 +占便宜的是呆 佔便宜的是呆 +占俄 佔俄 +占候 占候 +占停车 佔停車 +占先 佔先 +占光 佔光 +占全 佔全 +占八 佔八 +占六 佔六 +占凤 占鳳 +占分 佔分 +占到 佔到 +占加 佔加 +占劣 佔劣 +占北 佔北 +占十 佔十 +占千 佔千 +占半 佔半 +占南 佔南 +占卜 占卜 +占卜师 占卜師 +占卜术 占卜術 +占占 佔佔 +占卦 占卦 +占印 佔印 +占压 佔壓 +占去 佔去 +占取 佔取 +占台 佔臺 +占后 佔後 +占哺乳 佔哺乳 +占嗫 佔囁 +占四 佔四 +占国内 佔國內 +占在 佔在 +占地 佔地 +占地盘 佔地盤 +占场 佔場 +占场儿 佔場兒 +占城 占城 佔城 +占多 佔多 +占多数 佔多數 +占大 佔大 +占头 佔頭 +占头筹 佔頭籌 +占好 佔好 +占射 占射 +占小 佔小 +占少 佔少 +占尽 佔盡 +占尽便宜 佔盡便宜 +占局部 佔局部 +占居 佔居 +占屋 佔屋 +占山 佔山 +占市场 佔市場 +占平均 佔平均 +占床 佔牀 +占座 佔座 +占强 占強 +占得 佔得 +占德 佔德 +占总 佔總 +占房 占房 +占拜 占拜 +占据 佔據 +占掉 佔掉 +占整体 佔整體 +占断 占斷 +占新 佔新 +占星 占星 +占星学 占星學 +占星家 占星家 +占星师 占星師 +占星术 占星術 +占有 佔有 +占有五不验 占有五不驗 +占有权 佔有權 +占有欲 佔有慾 +占有率 佔有率 +占查 佔查 +占梦 占夢 +占次 佔次 +占比 佔比 +占毕 佔畢 +占法 佔法 +占满 佔滿 +占澳 佔澳 +占爲 佔爲 +占率 佔率 +占用 佔用 +占百 佔百 +占着 佔着 +占稳 佔穩 +占筮 占筮 +占线 佔線 +占缺 佔缺 +占网 佔網 +占美 佔美 +占耕 佔耕 +占至多 佔至多 +占至少 佔至少 +占花魁 佔花魁 +占苏 佔蘇 +占英 佔英 +占葡 佔葡 +占西 佔西 +占课 占課 佔課 +占资源 佔資源 +占起 佔起 +占起来 佔起來 +占超过 佔超過 +占身 占身 +占过 佔過 +占过去 佔過去 +占过来 佔過來 +占道 佔道 +占零 佔零 +占領 佔領 +占领 佔領 +占领军 佔領軍 +占领区 佔領區 +占领地 佔領地 +占领者 佔領者 +占风使帆 占風使帆 +占饭 佔飯 +占香 佔香 +占马 佔馬 +占验 占驗 +占高枝儿 佔高枝兒 +占鳌头 佔鰲頭 +卡其布 卡其布 +卡卡布 卡卡布 +卡尔加里 卡爾加里 +卡尔扎伊 卡爾扎伊 +卡尔文克莱因 卡爾文克萊因 +卡布 卡布 +卡布其诺 卡布其諾 +卡布其诺咖啡 卡布其諾咖啡 +卡布列拉 卡布列拉 +卡布奇诺 卡布奇諾 +卡布瑞 卡布瑞 +卡布瑞拉 卡布瑞拉 +卡布雷拉 卡布雷拉 +卡带柜 卡帶櫃 +卡式录音带 卡式錄音帶 +卡式录音机 卡式錄音機 +卡扎菲 卡扎菲 +卡扣 卡扣 +卡拉布里亚 卡拉布里亞 +卡拉曼里斯 卡拉曼里斯 +卡洛里 卡洛里 +卡片柜 卡片櫃 +卡片目录 卡片目錄 +卡特里娜 卡特里娜 +卡纳塔克邦 卡納塔克邦 +卡耶里 卡耶里 +卡芬雅克 卡芬雅克 +卡苏里 卡蘇里 +卡萨布兰加 卡薩布蘭加 +卡萨布兰卡 卡薩布蘭卡 +卡路里 卡路里 +卡迪拉克 卡迪拉克 +卡里 卡里 +卡里扎德 卡里紮德 +卡面 卡面 +卢仁杰 盧仁傑 +卢克索 盧克索 +卢千惠 盧千惠 +卢同 盧同 +卢布 盧布 +卢布尔雅那 盧布爾雅那 +卢布里雅纳 盧布里雅納 +卢棱伽 盧棱伽 +卢比西克 盧比西克 +卢胡 盧胡 +卢贝松 盧貝松 +卢郁佳 盧郁佳 +卤人 鹵人 +卤代烃 鹵代烴 +卤制 滷製 +卤化 鹵化 +卤化物 鹵化物 +卤化银 鹵化銀 +卤味 滷味 +卤地 鹵地 +卤族 鹵族 +卤梅水 滷梅水 +卤水 滷水 +卤汁 滷汁 +卤湖 滷湖 +卤煮 滷煮 +卤牛肉 滷牛肉 +卤簿 鹵簿 +卤素 鹵素 +卤素灯 鹵素燈 +卤肉 滷肉 +卤肉饭 滷肉飯 +卤莽 鹵莽 +卤莽灭裂 鹵莽滅裂 +卤菜 滷菜 +卤虾 滷蝦 +卤虾油 滷蝦油 +卤蛋 滷蛋 +卤钝 鹵鈍 +卤面 滷麪 +卤鸡 滷雞 +卤鸡肉 滷雞肉 +卦千纸 卦千紙 +卧室里 臥室裏 +卧游 臥遊 +卧薪尝胆 臥薪嚐膽 +卫教系 衛教系 +卫星云图 衛星雲圖 +卫星定位系统 衛星定位系統 +卫星导航系统 衛星導航系統 +卫星钟 衛星鐘 +卫生标准 衛生標準 +卫生系 衛生系 +卯上干 卯上幹 +卯后酒 卯後酒 +卯尽全力 卯盡全力 +卯足了劲 卯足了勁 +卯酉参辰 卯酉參辰 +印不出 印不出 +印佣 印傭 +印出 印出 +印出去 印出去 +印出来 印出來 +印制 印製 +印制厂 印製廠 +印制电路 印製電路 +印制电路板 印製電路板 +印刷厂 印刷廠 +印刷术 印刷術 +印刷电路板 印刷電路板 +印发 印發 +印台 印臺 +印台区 印臺區 +印合 印合 +印堂发黑 印堂發黑 +印度人民党 印度人民黨 +印度国大党 印度國大黨 +印度法系 印度法系 +印板儿 印板兒 +印染厂 印染廠 +印核 印核 +印欧语系 印歐語系 +印累绶若 印累綬若 +印花布 印花布 +印表 印表 +印表机 印表機 +印鉴 印鑑 +印鉴鉴定 印鑑鑑定 +危于 危於 +危于累卵 危於累卵 +危亡之秋 危亡之秋 +危害评价 危害評價 +即兴发挥 即興發揮 +即兴曲 即興曲 +即兴表演 即興表演 +即期外汇交易 即期外匯交易 +即期汇票 即期匯票 +即食面 即食麪 +却后 卻後 +却回去 卻回去 +却回来 卻回來 +却才 卻纔 +卵与石斗 卵與石鬥 +卵核 卵核 +卷一卷 捲一捲 +卷上 捲上 +卷不起 捲不起 +卷了 捲了 +卷云 捲雲 +卷住 捲住 +卷儿 卷兒 +卷入 捲入 +卷入漩涡 捲入漩渦 +卷刃 捲刃 +卷到 捲到 +卷动 捲動 +卷动门 捲動門 +卷包 捲包 +卷卷 卷卷 +卷去 捲去 +卷发 捲髮 +卷发器 捲髮器 +卷取 卷取 +卷叶蛾 卷葉蛾 +卷吸作用 捲吸作用 +卷回 捲回 +卷图 捲圖 +卷土 捲土 +卷土重来 捲土重來 +卷地皮 卷地皮 +卷娄 卷婁 +卷子 卷子 +卷子本 卷子本 +卷宗 卷宗 +卷尺 捲尺 +卷尾猴 捲尾猴 +卷层云 卷層雲 +卷層云 捲層雲 +卷帘 捲簾 +卷帘格 捲簾格 +卷帘门 捲簾門 +卷帙 卷帙 +卷帙浩繁 卷帙浩繁 +卷帻 卷幘 +卷开 捲開 +卷心 捲心 +卷心菜 捲心菜 +卷怀 卷懷 +卷成 捲成 +卷扬 捲揚 +卷扬机 捲揚機 +卷拢 捲攏 +卷旋 捲旋 +卷旗息鼓 卷旗息鼓 +卷曲 捲曲 +卷来 捲來 +卷来卷去 捲來捲去 +卷柏 卷柏 +卷染 卷染 +卷棚 捲棚 +卷款 捲款 +卷款潜逃 捲款潛逃 +卷款逃走 捲款逃走 +卷毛 捲毛 +卷浪 捲浪 +卷浪翻波 捲浪翻波 +卷烟 捲菸 +卷烟画片 捲煙畫片 +卷烟盒 捲菸盒 +卷甲 卷甲 +卷甲重来 捲甲重來 +卷积云 卷積雲 +卷笔刀 卷筆刀 +卷筒 捲筒 +卷筒纸 捲筒紙 +卷繖花序 卷繖花序 +卷纬 卷緯 +卷纸 捲紙 +卷线器 捲線器 +卷绕 卷繞 +卷缠 捲纏 +卷缩 捲縮 +卷翘 捲翹 +卷耳 卷耳 +卷腿裤 捲腿褲 +卷舌 捲舌 +卷舌元音 捲舌元音 +卷舌音 捲舌音 +卷舒 卷舒 +卷舖盖 捲舖蓋 +卷菸 捲菸 +卷落叶 捲落葉 +卷衣袖 捲衣袖 +卷袖 捲袖 +卷装 卷裝 +卷走 捲走 +卷起 捲起 +卷起來 捲起來 +卷起来 捲起來 +卷轴 卷軸 +卷轴式 卷軸式 +卷轴装 卷軸裝 +卷过 捲過 +卷进 捲進 +卷逃 捲逃 +卷钢 捲鋼 +卷铺盖 捲鋪蓋 +卷阿 卷阿 +卷须 卷鬚 +卷领 卷領 +卷风 捲風 +卷饼 捲餅 +卸了 卸了 +卸尸宴 卸屍宴 +卸扣 卸扣 +卿云 卿雲 +厂主 廠主 +厂卫 廠衛 +厂名 廠名 +厂商 廠商 +厂地 廠地 +厂址 廠址 +厂外 廠外 +厂子 廠子 +厂字旁 廠字旁 +厂家 廠家 +厂容 廠容 +厂房 廠房 +厂方 廠方 +厂棚 廠棚 +厂牌 廠牌 +厂用电 廠用電 +厂矿 廠礦 +厂礼拜 廠禮拜 +厂税 廠稅 +厂衣 廠衣 +厂规 廠規 +厂部 厂部 +厂长 廠長 +厄台 厄臺 +厄尔布鲁士 厄爾布魯士 +厄立特里亚 厄立特里亞 +厅舍 廳舍 +历下 歷下 +历下区 歷下區 +历久 歷久 +历久不衰 歷久不衰 +历久常新 歷久常新 +历久弥坚 歷久彌堅 +历久弥新 歷久彌新 +历书 曆書 +历乱 歷亂 +历代 歷代 +历代先皇 歷代先皇 +历代名画记 歷代名畫記 +历代志上 歷代志上 +历代志下 歷代志下 +历代祖先 歷代祖先 +历任 歷任 +历元 曆元 +历劫 歷劫 +历劫归来 歷劫歸來 +历历 歷歷 +历历可数 歷歷可數 +历历可纪 歷歷可紀 +历历可考 歷歷可考 +历历可见 歷歷可見 +历历可辨 歷歷可辨 +历历在目 歷歷在目 +历历如绘 歷歷如繪 +历历落落 歷歷落落 +历史 歷史 +历史上 歷史上 +历史久远 歷史久遠 +历史事件 歷史事件 +历史人物 歷史人物 +历史剧 歷史劇 +历史博物馆 歷史博物館 +历史名词 歷史名詞 +历史学 歷史學 +历史学家 歷史學家 +历史学者 歷史學者 +历史家 歷史家 +历史小说 歷史小說 +历史年表 歷史年表 +历史性 歷史性 +历史总在重演 歷史總在重演 +历史悠久 歷史悠久 +历史意义 歷史意義 +历史文学 歷史文學 +历史新高 歷史新高 +历史时代 歷史時代 +历史时期 歷史時期 +历史比较语言学 歷史比較語言學 +历史沿革 歷史沿革 +历史潮流 歷史潮流 +历史版本 歷史版本 +历史社会学 歷史社會學 +历史系 歷史系 +历史线图 歷史線圖 +历史背景 歷史背景 +历史观 歷史觀 +历史观点 歷史觀點 +历史语言学 歷史語言學 +历史语词 歷史語詞 +历史课 歷史課 +历史遗产 歷史遺產 +历史遗迹 歷史遺蹟 +历史里 歷史裏 +历史重演 歷史重演 +历命 曆命 +历城 歷城 +历城区 歷城區 +历城县 歷城縣 +历夏经秋 歷夏經秋 +历头 曆頭 +历始 曆始 +历审 歷審 +历室 曆室 +历尽 歷盡 +历尽沧桑 歷盡滄桑 +历尽艰辛 歷盡艱辛 +历尽艰险 歷盡艱險 +历尽艰难 歷盡艱難 +历尾 曆尾 +历届 歷屆 +历山 歷山 +历年 歷年 +历年来 歷年來 +历数 歷數 +历日 歷日 +历日旷久 歷日曠久 +历时 歷時 +历月 歷月 +历有年所 歷有年所 +历朝 歷朝 +历朝历代 歷朝歷代 +历朝通俗演义 歷朝通俗演義 +历本 曆本 +历来 歷來 +历来如此 歷來如此 +历来最低点 歷來最低點 +历次 歷次 +历正 歷正 +历沴 歷沴 +历法 曆法 +历澜 歷瀾 +历物之意 歷物之意 +历狱 曆獄 +历碌 歷碌 +历程 歷程 +历纪 曆紀 +历线 歷線 +历练 歷練 +历练之才 歷練之才 +历练老成 歷練老成 +历经 歷經 +历经沧桑 歷經滄桑 +历经波折 歷經波折 +历落 歷落 +历象 曆象 +历象表 曆象表 +历遍 歷遍 +历险 歷險 +历险归来 歷險歸來 +历险记 歷險記 +历齿 歷齒 +压价 壓價 +压克力 壓克力 +压克力板 壓克力板 +压出 壓出 +压出去 壓出去 +压出来 壓出來 +压制 壓制 壓製 +压制住 壓制住 +压制性 壓制性 +压力团体 壓力團體 +压力症 壓力症 +压力表 壓力錶 +压卷 壓卷 +压回 壓回 +压回去 壓回去 +压回来 壓回來 +压杆 壓桿 +压板 壓板 +压缩饼干 壓縮餅乾 +压胄子 壓冑子 +压舌板 壓舌板 +压面棍 壓麪棍 +厌恶 厭惡 +厌恶感 厭惡感 +厌食症 厭食症 +厓谷 厓谷 +厘出 釐出 +厘升 釐升 +厘定 釐定 +厘改 釐改 +厘整 釐整 +厘正 釐正 +厘清 釐清 +厘米 釐米 +厘订 釐訂 +厘金 厘金 +厘革 釐革 +厚朴 厚朴 +厚板 厚板 +厚纸板 厚紙板 +厝薪于火 厝薪於火 +原于 原於 +原价 原價 +原厂 原廠 +原厂药 原廠藥 +原发 原發 +原发性 原發性 +原发性进行性失语 原發性進行性失語 +原叶体 原葉體 +原始公社制度 原始公社制度 +原始艺术 原始藝術 +原始记录 原始記錄 +原子云 原子雲 +原子价 原子價 +原子发电厂 原子發電廠 +原子团 原子團 +原子爆破弹药 原子爆破彈藥 +原子能发电 原子能發電 +原子能发电站 原子能發電站 +原子钟 原子鐘 +原定价 原定價 +原料价格 原料價格 +原料药 原料藥 +原曲 原曲 +原板 原板 +原核 原核 +原种 原種 +原纤维 原纖維 +原苏联 原蘇聯 +原虫 原蟲 +原野游侠 原野遊俠 +原钟 原鐘 +原须 原須 +厥后 厥後 +厨余 廚餘 +厨娘 廚娘 +厨柜 廚櫃 +厮斗 廝鬥 +厮舍 廝舍 +去不了 去不了 +去了 去了 +去借 去借 +去台人员 去臺人員 +去后 去後 +去向 去向 +去向不明 去向不明 +去干 去幹 +去念 去念 +去恶从善 去惡從善 +去搜 去搜 +去暗投明 去暗投明 +去杀胜残 去殺勝殘 +去核 去核 +县党部 縣黨部 +县志 縣誌 +县里 縣裏 +参与 參與 +参与人员 參與人員 +参与制 參與制 +参与度 參與度 +参与感 參與感 +参与权 參與權 +参与率 參與率 +参与者 參與者 +参两院 參兩院 +参予 參予 +参事 參事 +参伍 參伍 +参众两院 參衆兩院 +参会 參會 +参佐 參佐 +参假 參假 +参军 參軍 +参前落后 參前落後 +参加 參加 +参加为 參加爲 +参加人 參加人 +参加国 參加國 +参加奖 參加獎 +参加完 參加完 +参加者 參加者 +参劾 參劾 +参半 參半 +参合 參合 +参同契 參同契 +参商 參商 +参团 參團 +参场 參場 +参堂 參堂 +参天 參天 +参头 參頭 +参奏 參奏 +参孙 參孫 +参宿 參宿 +参宿七 參宿七 +参将 參將 +参展 參展 +参展商 參展商 +参展团 參展團 +参差 參差 +参差不齐 參差不齊 +参差错落 參差錯落 +参度 參度 +参悟 參悟 +参战 參戰 +参战国 參戰國 +参拜 參拜 +参拾壹 參拾壹 +参拾陆 參拾陸 +参政 參政 +参政权 參政權 +参数 參數 +参朝 參朝 +参本 參本 +参杂 參雜 +参校 參校 +参汤 蔘湯 +参演 參演 +参灵 參靈 +参照 參照 +参照卡 參照卡 +参照物 參照物 +参照系 參照系 +参看 參看 +参知政事 參知政事 +参破 參破 +参禅 參禪 +参绥 蔘綏 +参综 參綜 +参考 參考 +参考书 參考書 +参考书目 參考書目 +参考价 參考價 +参考价值 參考價值 +参考值 參考值 +参考参考 參考參考 +参考座标 參考座標 +参考性 參考性 +参考手冊 參考手冊 +参考文献 參考文獻 +参考材料 參考材料 +参考法 參考法 +参考消息 參考消息 +参考特藏 參考特藏 +参考系 參考系 +参考资料 參考資料 +参股 參股 +参茸 蔘茸 +参见 參見 +参见互照 參見互照 +参见注 參見注 +参观 參觀 +参观券 參觀券 +参观参观 參觀參觀 +参观团 參觀團 +参观团体 參觀團體 +参观完 參觀完 +参观者 參觀者 +参订 參訂 +参训 參訓 +参议 參議 +参议会 參議會 +参议员 參議員 +参议院 參議院 +参访 參訪 +参访团 參訪團 +参评 參評 +参话头 參話頭 +参请 參請 +参谋 參謀 +参谋总部 參謀總部 +参谋总长 參謀總長 +参谋长 參謀長 +参谒 參謁 +参谭 參譚 +参赛 參賽 +参赛国 參賽國 +参赛权 參賽權 +参赛片 參賽片 +参赛者 參賽者 +参赞 參贊 +参辰 參辰 +参辰卯酉 參辰卯酉 +参辰日月 參辰日月 +参选 參選 +参选人 參選人 +参透 參透 +参道 參道 +参酌 參酌 +参量 參量 +参量空间 參量空間 +参错 參錯 +参阅 參閱 +参院 參院 +参革 參革 +参预 參預 +参验 參驗 +又云 又云 +又咸 又鹹 +又咽 又咽 +又干 又幹 +又干又硬 又乾又硬 +又弱一个 又弱一個 +又当别论 又當別論 +又摇又摆 又搖又擺 +又来了 又來了 +又饥又渴 又飢又渴 +及于 及於 +及早准备 及早準備 +友于 友于 +友党 友黨 +友台 友臺 +友好代表 友好代表 +友好关系 友好關係 +友情价 友情價 +友民党 友民黨 +友谊万岁 友誼萬歲 +双丰 雙豐 +双主修 雙主修 +双凸面 雙凸面 +双台子 雙臺子 +双台子区 雙臺子區 +双叶 雙葉 +双后前兵开局 雙后前兵開局 +双向 雙向 +双向交通 雙向交通 +双向沟通 雙向溝通 +双向讨论区 雙向討論區 +双周 雙週 +双周刊 雙週刊 +双周期性 雙週期性 +双回门 雙回門 +双复磷 雙復磷 +双子叶 雙子葉 +双子叶植物 雙子葉植物 +双手万能 雙手萬能 +双手赞成 雙手贊成 +双扣 雙扣 +双折 雙摺 +双折射 雙折射 +双拐 雙柺 +双挂号 雙掛號 +双捻布 雙捻布 +双方同意 雙方同意 +双方面 雙方面 +双曲 雙曲 +双曲余割 雙曲餘割 +双曲余弦 雙曲餘弦 +双曲抛物面 雙曲拋物面 +双曲拱桥 雙曲拱橋 +双曲正弦 雙曲正弦 +双曲线 雙曲線 +双曲线正弦 雙曲線正弦 +双杠 雙槓 +双杯 雙杯 +双柑斗酒 雙柑斗酒 +双核 雙核 +双江拉祜族佤族布朗族傣族自治县 雙江拉祜族佤族布朗族傣族自治縣 +双湖特别区 雙湖特別區 +双画面电视 雙畫面電視 +双胜类 雙胜類 +双语立体声系统 雙語立體聲系統 +双轨制 雙軌制 +双进双出 雙進雙出 +双重标准 雙重標準 +双链 雙鏈 +双链核酸 雙鏈核酸 +双雕 雙鵰 +双面 雙面 +双面人 雙面人 +双面娇娃 雙面嬌娃 +双面谍 雙面諜 +双龙大裂谷 雙龍大裂谷 +反个 反個 +反乱并 反亂併 +反了 反了 +反于 反於 +反修 反修 +反光板 反光板 +反光面 反光面 +反冲 反衝 +反冲力 反衝力 +反制 反制 +反升 反升 +反卷 反捲 +反反复复 反反覆覆 +反叛党 反叛黨 +反右派斗争 反右派鬥爭 +反向 反向 +反向而行 反向而行 +反回头 反回頭 +反坦克 反坦克 +反坦克炮 反坦克炮 +反复 反覆 反復 +反复不一 反覆不一 +反复不定 反覆不定 +反复不常 反覆不常 +反复制 反複製 +反复思维 反覆思維 +反复思量 反覆思量 +反复性 反覆性 +反复无常 反覆無常 +反对党 反對黨 +反导向 反導向 +反导向飞弹 反導向飛彈 +反导系统 反導系統 +反射面 反射面 +反录病毒 反錄病毒 +反托拉斯 反托拉斯 +反托拉斯法案 反托拉斯法案 +反扣 反扣 反釦 +反掌折枝 反掌折枝 +反攻复国 反攻復國 +反攻复国大业 反攻復國大業 +反攻复国战争 反攻復國戰爭 +反斗 反斗 +反斗城 反斗城 +反方向 反方向 +反时针 反時針 +反时钟 反時鐘 +反时钟方向 反時鐘方向 +反映出 反映出 +反映出来 反映出來 +反曲弓 反曲弓 +反朴 反樸 +反核 反核 +反清复明 反清復明 +反烟 反煙 +反烟运动 反煙運動 +反目成仇 反目成仇 +反经合义 反經合義 +反袁斗争 反袁鬥爭 +反败为胜 反敗爲勝 +反转录 反轉錄 +反转录病毒 反轉錄病毒 +反酷刑折磨公约 反酷刑折磨公約 +反铲 反剷 +反阴复阴 反陰復陰 +反面 反面 +反面人物 反面人物 +反面儿 反面兒 +反面教员 反面教員 +反面教材 反面教材 +反面无情 反面無情 +反面角色 反面角色 +反颜相向 反顏相向 +反馈表 反饋表 +发上 發上 +发上冲冠 髮上衝冠 +发上去 發上去 +发上指冠 髮上指冠 +发上来 發上來 +发下 發下 +发下去 發下去 +发下来 發下來 +发不出 發不出 +发不到 發不到 +发不起 發不起 +发业 發業 +发丝 髮絲 +发丧 發喪 +发个 發個 +发个儿 發個兒 +发为血之本 髮爲血之本 +发乎情 發乎情 +发乎情止乎礼义 發乎情止乎禮義 +发乔 發喬 +发书 發書 +发乱钗横 髮亂釵橫 +发乳 髮乳 +发了 發了 +发予 發予 +发于 發於 +发些 發些 +发交 發交 +发亮 發亮 +发人 發人 +发人深省 發人深省 +发人省思 發人省思 +发付 發付 +发令 發令 +发令枪 發令槍 +发件人 發件人 +发作 發作 +发俊科 發俊科 +发信 發信 +发信号 發信號 +发像管 發像管 +发僵 發僵 +发兆 發兆 +发光 發光 +发光二极体 發光二極體 +发光二极管 發光二極管 +发光体 發光體 +发光可鉴 髮光可鑑 +发光器 發光器 +发光屏 發光屏 +发光度 發光度 +发光强度 發光強度 +发光颜料 發光顏料 +发兵 發兵 +发冢 發冢 +发冷 發冷 +发凡 發凡 +发凡举例 發凡舉例 +发出 發出 +发出去 發出去 +发出指示 發出指示 +发出来 發出來 +发函 發函 +发刊 發刊 +发刊词 發刊詞 +发利市 發利市 +发到 發到 +发功 發功 +发动 發動 +发动力 發動力 +发动期 發動期 +发动机 發動機 +发包 發包 +发包给 發包給 +发匪 髮匪 +发匮 發匱 +发单 發單 +发卖 發賣 +发卡 髮卡 發卡 +发卷 髮捲 +发去 發去 +发发 發發 +发叶 發葉 +发号 發號 +发号令 發號令 +发号出令 發號出令 +发号布令 發號佈令 +发号施令 發號施令 +发向 發向 +发呆 發呆 +发呕 發嘔 +发咒 發咒 +发响 發響 +发哑 發啞 +发哥 發哥 +发售 發售 +发售量 發售量 +发喉急 發喉急 +发喊连天 發喊連天 +发喘 發喘 +发嘎嘎声 發嘎嘎聲 +发嘘声 發噓聲 +发噱 發噱 +发回 發回 +发回去 發回去 +发回来 發回來 +发困 發睏 +发圈 髮圈 +发型 髮型 +发型师 髮型師 +发墨 發墨 +发声 發聲 +发声器 發聲器 +发声器官 發聲器官 +发声法 發聲法 +发复 發覆 +发天阴 發天陰 +发头 發頭 +发头风 發頭風 +发夹 髮夾 +发奉 發奉 +发奋 發奮 +发奋图强 發奮圖強 +发奋有为 發奮有爲 +发奖 發獎 +发套 髮套 +发奸摘隐 發奸摘隱 +发奸擿伏 發奸擿伏 +发好 發好 +发如飞蓬 髮如飛蓬 +发妻 髮妻 +发姐 髮姐 +发威 發威 +发威动怒 發威動怒 +发嫁 發嫁 +发完 發完 +发家 發家 +发富发贵 發富發貴 +发封 發封 +发射 發射 +发射井 發射井 +发射台 發射臺 +发射器 發射器 +发射场 發射場 +发射控制 發射控制 +发射机 發射機 +发射机应答器 發射機應答器 +发射极 發射極 +发射点 發射點 +发射站 發射站 +发尾 髮尾 +发屋 髮屋 +发屋求狸 發屋求狸 +发展 發展 +发展下去 發展下去 +发展不平均 發展不平均 +发展中 發展中 +发展中国 發展中國 +发展中国家 發展中國家 +发展为 發展爲 +发展出 發展出 +发展出来 發展出來 +发展到 發展到 +发展区 發展區 +发展史 發展史 +发展局 發展局 +发展心理 發展心理 +发展性 發展性 +发展成 發展成 +发展核武器 發展核武器 +发展潜力 發展潛力 +发展生产 發展生產 +发展的国家 發展的國家 +发展研究中心 發展研究中心 +发展观 發展觀 +发展起来 發展起來 +发展趋势 發展趨勢 +发展速度 發展速度 +发展部 發展部 +发岁 發歲 +发已霜白 髮已霜白 +发市 發市 +发布 發佈 +发布会 發佈會 +发带 髮帶 +发干 發乾 +发庄 發莊 +发廊 髮廊 +发廪 發廩 +发开 發開 +发式 髮式 +发引 發引 +发引千钧 髮引千鈞 +发往 發往 +发得 發得 +发心 發心 +发忿 發忿 +发怒 發怒 +发怔 發怔 +发急 發急 +发性 發性 +发怨言 發怨言 +发怵 發怵 +发恨 發恨 +发息 發息 +发恼 發惱 +发悲 發悲 +发悸 發悸 +发情 發情 +发情周期 發情周期 +发情期 發情期 +发想 發想 +发愁 發愁 +发意 發意 +发愣 發愣 +发愤 發憤 +发愤努力 發憤努力 +发愤向上 發憤向上 +发愤图强 發憤圖強 +发愤忘食 發憤忘食 +发愿 發願 +发慌 發慌 +发憷 發憷 +发懒 發懶 +发成 發成 +发扬 發揚 +发扬光大 發揚光大 +发扬踔厉 發揚踔厲 +发扬蹈厉 發揚蹈厲 +发抒 發抒 +发抖 發抖 +发报 發報 +发报人 發報人 +发报员 發報員 +发报器 發報器 +发报机 發報機 +发指 髮指 +发指令 發指令 +发指眦裂 髮指眥裂 +发挥 發揮 +发挥不了 發揮不了 +发挥作用 發揮作用 +发挥出 發揮出 +发挥出来 發揮出來 +发挥到 發揮到 +发挥性 發揮性 +发挥所长 發揮所長 +发挽双髻 髮挽雙髻 +发掉 發掉 +发排 發排 +发掘 發掘 +发掘出 發掘出 +发掘出来 發掘出來 +发掘到 發掘到 +发摃 發摃 +发摆子 發擺子 +发擂 發擂 +发擿 發擿 +发擿奸伏 發擿奸伏 +发收 發收 +发改委 發改委 +发放 發放 +发政施仁 發政施仁 +发散 發散 +发文 發文 +发文者 發文者 +发明 發明 +发明人 發明人 +发明创造 發明創造 +发明到 發明到 +发明奖 發明獎 +发明家 發明家 +发明展 發明展 +发明者 發明者 +发昏 發昏 +发昏章第十一 發昏章第十一 +发春 發春 +发晕 發暈 +发暗 發暗 +发有 發有 +发木 發木 +发村 發村 +发束 髮束 +发条 發條 +发来 發來 +发来发去 發來發去 +发松 發鬆 +发极 發極 +发标 發標 +发样 發樣 +发根 髮根 +发案 發案 +发梢 髮梢 +发梳 髮梳 +发棠 發棠 +发棵 發棵 +发楞 發楞 +发榜 發榜 +发横 發橫 +发横财 發橫財 +发款 發款 +发毛 發毛 +发气 發氣 +发水 發水 +发汗 發汗 +发汗剂 發汗劑 +发汗药 發汗藥 +发油 髮油 +发泄 發泄 +发泄出来 發泄出來 +发泡 發泡 +发泡体 發泡體 +发泡剂 發泡劑 +发泡成形 發泡成形 +发泡胶 發泡膠 +发洋财 發洋財 +发洪 發洪 +发派 發派 +发淡科 發淡科 +发源 發源 +发源地 發源地 +发漂 髮漂 +发潮 發潮 +发火 發火 +发火器 發火器 +发火点 發火點 +发炎 發炎 +发炎性 發炎性 +发炮 發炮 +发点 發點 +发烛 發燭 +发烟 發煙 +发烟剂 發煙劑 +发烦 發煩 +发烧 發燒 +发烧友 發燒友 +发烧客 發燒客 +发烧度 發燒度 +发烧时 發燒時 +发烧片 發燒片 +发烫 發燙 +发热 發熱 +发热时 發熱時 +发热量 發熱量 +发爲 發爲 +发片 發片 +发片日 發片日 +发牌 發牌 +发牒 發牒 +发牙豆 發牙豆 +发牢骚 發牢騷 +发物 發物 +发状 髮狀 +发狂 發狂 +发狠 發狠 +发现 發現 +发现体 發現體 +发现出 發現出 +发现到 發現到 +发现号 發現號 +发现学习 發現學習 +发现数 發現數 +发现物 發現物 +发球 發球 +发球区 發球區 +发球员 發球員 +发球局 發球局 +发球权 發球權 +发球点 發球點 +发生 發生 +发生关系 發生關係 +发生器 發生器 +发生地 發生地 +发生学 發生學 +发生学分类法 發生學分類法 +发生率 發生率 +发甲 發甲 +发电 發電 +发电厂 發電廠 +发电场 發電場 +发电所 發電所 +发电机 發電機 +发电机组 發電機組 +发电板 發電板 +发电站 發電站 +发电量 發電量 +发疯 發瘋 +发疹 發疹 +发疹子 發疹子 +发病 發病 +发病率 發病率 +发痒 發癢 +发痛 發痛 +发痧 發痧 +发痴 發癡 +发癡 發癡 +发癣 髮癬 +发癫 發癲 +发白 發白 +发白时 發白時 +发的 發的 +发皇 發皇 +发皓齿 發皓齒 +发监 發監 +发直 發直 +发眩 發眩 +发矢 發矢 +发短信 發短信 +发短心长 髮短心長 +发石车 發石車 +发砲 發砲 +发硎 發硎 +发硎新试 發硎新試 +发硬 發硬 +发硬时 發硬時 +发神经 發神經 +发神经病 發神經病 +发祥 發祥 +发祥地 發祥地 +发票 發票 +发禁 髮禁 +发福 發福 +发科 發科 +发科打诨 發科打諢 +发积 發積 +发稿 發稿 +发稿时 發稿時 +发窘 發窘 +发端 發端 +发端词 發端詞 +发笑 發笑 +发笔 發筆 +发笺 髮箋 +发策 發策 +发策决科 發策決科 +发签 發籤 +发箍 髮箍 +发箧 發篋 +发箭 發箭 +发簪 髮簪 +发籤 發籤 +发粉 發粉 +发糕 發糕 +发紫 發紫 +发红 發紅 +发纱 髮紗 +发绀 發紺 +发结 髮結 +发给 發給 +发绿 發綠 +发缨 髮纓 +发网 髮網 +发老狠 發老狠 +发聋振聩 發聾振聵 +发肤 髮膚 +发育 發育 +发育不良 發育不良 +发育成 發育成 +发育期 發育期 +发育条件 發育條件 +发育生物学 發育生物學 +发肿 發腫 +发胀 發脹 +发胀感 發脹感 +发胖 發胖 +发胡 發胡 +发胶 髮膠 +发脚 髮腳 +发脱 發脫 +发脱口齿 發脫口齒 +发脾寒 發脾寒 +发脾气 發脾氣 +发腊 髮臘 +发自 發自 +发自內心 發自內心 +发自肺腑 發自肺腑 +发臭 發臭 +发船 發船 +发色 髮色 +发色勒 髮色勒 +发花 發花 +发芽 發芽 +发芽势 發芽勢 +发芽率 發芽率 +发草 發草 +发草帖 發草帖 +发菜 髮菜 +发落 發落 +发蒙 發矇 +发蒙振落 發蒙振落 +发蓝 發藍 +发蔫 發蔫 +发薪 發薪 +发薪水 發薪水 +发虚 發虛 +发蜡 髮蠟 +发蜡条 髮蠟條 +发行 發行 +发行人 發行人 +发行商 發行商 +发行备忘录 發行備忘錄 +发行日 發行日 +发行权 發行權 +发行红利股 發行紅利股 +发行者 發行者 +发行量 發行量 +发行量加权股价指数 發行量加權股價指數 +发行额 發行額 +发行首日 發行首日 +发表 發表 +发表会 發表會 +发表出来 發表出來 +发表声明 發表聲明 +发表意见 發表意見 +发表欲 發表慾 +发表演讲 發表演講 +发见 發見 +发觉 發覺 +发觉出 發覺出 +发觉到 發覺到 +发解 發解 +发言 發言 +发言中肯 發言中肯 +发言人 發言人 +发言人卡 發言人卡 +发言人室 發言人室 +发言发语 發言發語 +发言台 發言臺 +发言权 發言權 +发言条 發言條 +发言者 發言者 +发言遣辞 發言遣辭 +发誓 發誓 +发讪 發訕 +发议论 發議論 +发话 發話 +发话器 發話器 +发诨子 發諢子 +发语词 發語詞 +发语辞 發語辭 +发课 發課 +发财 發財 +发财票 發財票 +发财致富 發財致富 +发财金 發財金 +发货 發貨 +发货单 發貨單 +发质 髮質 +发贴 發貼 +发起 發起 +发起人 發起人 +发起来 發起來 +发起烧来 發起燒來 +发越 發越 +发足 發足 +发跡 發跡 +发踊冲冠 髮踊沖冠 +发踪指示 發蹤指示 +发蹙 發蹙 +发躁 發躁 +发身 發身 +发车 發車 +发轫 發軔 +发软 發軟 +发轿 發轎 +发辫 髮辮 +发达 發達 +发达为 發達爲 +发达到 發達到 +发达国 發達國 +发达国家 發達國家 +发达地区 發達地區 +发达起来 發達起來 +发过 發過 +发过去 發過去 +发过来 發過來 +发运 發運 +发还 發還 +发进 發進 +发进去 發進去 +发进来 發進來 +发迹 發跡 +发迹变泰 發跡變泰 +发送 發送 +发送功率 發送功率 +发送器 發送器 +发送机 發送機 +发遣 發遣 +发配 發配 +发酒疯 發酒瘋 +发酒风 發酒風 +发酵 發酵 +发酵乳 發酵乳 +发酵乳酸 發酵乳酸 +发酵出来 發酵出來 +发酵粉 發酵粉 +发酵饲料 發酵飼料 +发酸 發酸 +发采扬明 發采揚明 +发野 發野 +发量 髮量 +发针 髮針 +发钗 髮釵 +发长 髮長 +发问 發問 +发问者 發問者 +发闷 發悶 +发闹 發鬧 +发阴天 發陰天 +发际 髮際 +发隐擿伏 發隱擿伏 +发难 發難 +发雕 髮雕 +发霉 發黴 +发霜 髮霜 +发露 發露 +发青 發青 +发面 發麪 +发鞍 發鞍 +发音 發音 +发音体 發音體 +发音合作人 發音合作人 +发音器 發音器 +发音器官 發音器官 +发音学 發音學 +发音方法 發音方法 +发音部位 發音部位 +发音障碍 發音障礙 +发须俱 髮鬚俱 +发须已 髮鬚已 +发须斑 髮鬚斑 +发须皆 髮鬚皆 +发须都 髮鬚都 +发颤 發顫 +发风 發風 +发风骂坐 發風罵坐 +发飘 發飄 +发飙 發飆 +发飚 發飈 +发饰 髮飾 +发饷 發餉 +发香 髮香 +发马 發馬 +发骚 發騷 +发高烧 發高燒 +发高热 發高熱 +发髻 髮髻 +发鬓 髮鬢 +发麻 發麻 +发黄 發黃 +发黏 發黏 +发黑 發黑 +叔于田 叔于田 +叔梁纥 叔梁紇 +取之不尽 取之不盡 +取之不尽用之不竭 取之不盡用之不竭 +取了 取了 +取信于 取信於 +取信于人 取信於人 +取值范围 取值範圍 +取决于 取決於 +取出 取出 +取出来 取出來 +取向 取向 +取回 取回 +取回去 取回去 +取回来 取回來 +取得一致 取得一致 +取得胜利 取得勝利 +取才 取才 +取材于 取材於 +取法于 取法於 +取胜 取勝 +取舍 取捨 +取舍不定 取捨不定 +取舍之间 取捨之間 +取舍难定 取捨難定 +取药 取藥 +受不了 受不了 +受了 受了 +受人之托 受人之託 +受制 受制 +受制于 受制於 +受制于人 受制於人 +受命于天 受命于天 +受困 受困 +受夠了 受夠了 +受尽 受盡 +受尽压迫 受盡壓迫 +受尽折磨 受盡折磨 +受得了 受得了 +受托 受託 +受托人 受託人 +受托者 受託者 +受折磨 受折磨 +受用不尽 受用不盡 +受聘于 受聘于 +受阻于 受阻於 +受限于 受限於 +受难曲 受難曲 +受骗上当 受騙上當 +变不了 變不了 +变不出花样 變不出花樣 +变丑 變醜 +变个 變個 +变了 變了 +变了又变 變了又變 +变价 變價 +变修 變修 +变出 變出 +变出来 變出來 +变动汇率 變動匯率 +变化万端 變化萬端 +变化范围 變化範圍 +变回 變回 +变回去 變回去 +变回来 變回來 +变奏曲 變奏曲 +变尽方法 變盡方法 +变异型克雅氏症 變異型克雅氏症 +变异系数 變異係數 +变形虫 變形蟲 +变征 變徵 +变征之声 變徵之聲 +变征之音 變徵之音 +变性手术 變性手術 +变性气团 變性氣團 +变暗 變暗 +变松 變鬆 +变相加价 變相加價 +变种 變種 +变种人 變種人 +变脏 變髒 +变质岩 變質岩 +变速杆 變速桿 +变造出 變造出 +变髒 變髒 +变魔术 變魔術 +叙事曲 敘事曲 +叙别 敘別 +叙录 敘錄 +叙述出来 敘述出來 +叛党 叛黨 +叠了 疊了 +叠出 疊出 +叠出去 疊出去 +叠出来 疊出來 +叠合 疊合 +叠层岩 疊層岩 +叠彩区 疊彩區 +口仇 口仇 +口出 口出 +口出不逊 口出不遜 +口出恶言 口出惡言 +口出秽言 口出穢言 +口占 口占 +口布 口布 +口干 口乾 +口干舌燥 口乾舌燥 +口彩 口彩 +口念 口唸 +口愿 口願 +口才 口才 +口才好 口才好 +口才辨给 口才辨給 +口技表演者 口技表演者 +口服药 口服藥 +口杯 口杯 +口燥唇干 口燥脣乾 +口燥脣干 口燥脣乾 +口腹之欲 口腹之慾 +口血未干 口血未乾 +口语字词识别 口語字詞識別 +口述出来 口述出來 +口述历史 口述歷史 +口里 口裏 +口钟 口鐘 +古书云 古書云 +古云 古云 +古今注 古今注 +古典艺术 古典藝術 +古切里 古切里 +古制 古制 +古巴共产党 古巴共產黨 +古弦 古絃 +古書云 古書云 +古朴 古樸 +古杰拉尔 古傑拉爾 +古杰拉特邦 古傑拉特邦 +古板 古板 +古柯叶 古柯葉 +古柯咸 古柯鹹 +古游 古遊 +古腾堡计划 古騰堡計劃 +古語云 古語云 +古语云 古語云 +古迹 古蹟 +古里古怪 古里古怪 +古钟 古鐘 +古钟表 古鐘錶 +句践复国 句踐復國 +另一回 另一回 +另一回事 另一回事 +另一方面 另一方面 +另一种 另一種 +另一面 另一面 +另于 另於 +另借 另借 +另开生面 另開生面 +另当别论 另當別論 +另方面 另方面 +另谋出路 另謀出路 +另谋发展 另謀發展 +另起烟爨 另起煙爨 +另辟 另闢 +另辟新径 另闢新徑 +另辟蹊径 另闢蹊徑 +叨念 叨唸 +叩出 叩出 +叩头虫 叩頭蟲 +叩钟 叩鐘 +只不过 只不過 +只不过几年前 只不過幾年前 +只不过是 只不過是 +只为了 只爲了 +只买 只買 +只买到 只買到 +只争旦夕 只爭旦夕 +只于 只於 +只产 只產 +只亮 只亮 +只亮出 只亮出 +只亮到 只亮到 +只今 只今 +只从 只從 +只会 只會 +只会到 只會到 +只会在 只會在 +只传 只傳 +只传出 只傳出 +只住 只住 +只余 只餘 +只作 只作 +只借 只借 +只借不还 只借不還 +只借到 只借到 +只做 只做 +只停 只停 +只养 只養 +只再 只再 +只写 只寫 +只写出 只寫出 +只写到 只寫到 +只写在 只寫在 +只冲 只衝 +只准 只准 +只凭 只憑 +只分 只分 +只刊 只刊 +只判 只判 +只剩 只剩 +只剩下 只剩下 +只办 只辦 +只加 只加 +只加到 只加到 +只动 只動 +只包 只包 +只卖 只賣 +只占 只佔 +只占到 只佔到 +只占卜 只占卜 +只占吉 只占吉 +只占神问卜 只占神問卜 +只占算 只占算 +只印 只印 +只去 只去 +只发 只發 +只受 只受 +只受到 只受到 +只变 只變 +只可 只可 +只可以 只可以 +只可在 只可在 +只可意会不可言传 只可意會不可言傳 +只叹 只嘆 +只吃 只吃 +只合 只合 +只含 只含 +只听 只聽 +只听到 只聽到 +只和 只和 +只唱 只唱 +只唱出 只唱出 +只唱到 只唱到 +只喊 只喊 +只喝 只喝 +只喝到 只喝到 +只回 只回 +只回到 只回到 +只回去 只回去 +只回来 只回來 +只因 只因 +只因为 只因爲 +只图 只圖 +只在 只在 +只坐 只坐 +只声不出 隻聲不出 +只多 只多 +只多不少 只多不少 +只够 只夠 +只夠到 只夠到 +只夠在 只夠在 +只套 只套 +只好 只好 +只好去 只好去 +只好在 只好在 +只好来 只好來 +只字 隻字 +只字不提 隻字不提 +只字片纸 隻字片紙 +只字片言 隻字片言 +只字片语 隻字片語 +只学 只學 +只学会 只學會 +只学到 只學到 +只守 只守 +只守不攻 只守不攻 +只守到 只守到 +只定 只定 +只害 只害 +只对 只對 +只封 只封 +只射 只射 +只射到 只射到 +只将 只將 +只尽 只盡 +只尽到 只盡到 +只差 只差 +只带 只帶 +只帮 只幫 +只帮忙 只幫忙 +只应 只應 +只延 只延 +只开 只開 +只开出 只開出 +只开到 只開到 +只开去 只開去 +只开回 只開回 +只开来 只開來 +只当 只當 +只录 只錄 +只录到 只錄到 +只影 隻影 +只影全无 隻影全無 +只待 只待 +只得 只得 +只得到 只得到 +只念 只念 +只怕 只怕 +只怕会 只怕會 +只怕是 只怕是 +只怪 只怪 +只想 只想 +只想出 只想出 +只想到 只想到 +只想去 只想去 +只想来 只想來 +只愿 只願 +只愿意 只願意 +只懂 只懂 +只懂得 只懂得 +只成 只成 +只戴 只戴 +只手 隻手 +只手单拳 隻手單拳 +只手擎天 隻手擎天 +只手空拳 隻手空拳 +只手遮天 隻手遮天 +只打 只打 +只找 只找 +只找出 只找出 +只找到 只找到 +只把 只把 +只报 只報 +只抱 只抱 +只拜 只拜 +只拼 只拼 +只挑 只挑 +只挡 只擋 +只捉 只捉 +只捉到 只捉到 +只排 只排 +只接 只接 +只接到 只接到 +只提 只提 +只插 只插 +只插到 只插到 +只搬 只搬 +只摆 只擺 +只撞 只撞 +只收 只收 +只收到 只收到 +只放 只放 +只教 只教 +只敢 只敢 +只数 只數 +只数到 只數到 +只日 隻日 +只是 只是 +只是为了 只是爲了 +只是会 只是會 +只是在 只是在 +只是有 只是有 +只替 只替 +只有 只有 +只有出的气没有进的气 只有出的氣沒有進的氣 +只有在 只有在 +只来 只來 +只来到 只來到 +只査 只查 +只査出 只查出 +只査到 只查到 +只欠东风 只欠東風 +只此一家别无分店 只此一家別無分店 +只比 只比 +只求 只求 +只求无过 只求無過 +只求自保 只求自保 +只派 只派 +只流 只流 +只流出 只流出 +只流到 只流到 +只涂 只塗 +只消 只消 +只漏 只漏 +只漏出 只漏出 +只漏到 只漏到 +只演 只演 +只点 只點 +只照 只照 +只照到 只照到 +只煮 只煮 +只爲 只爲 +只猜 只猜 +只猜出 只猜出 +只猜到 只猜到 +只玩 只玩 +只甚 只甚 +只用 只用 +只用到 只用到 +只画 只畫 +只留 只留 +只留下 只留下 +只留到 只留到 +只看 只看 +只看出 只看出 +只看到 只看到 +只眼 隻眼 +只眼独具 隻眼獨具 +只睡 只睡 +只睡到 只睡到 +只知其一不知其二 只知其一不知其二 +只知有己不知有人 只知有己不知有人 +只砍 只砍 +只种 只種 +只租 只租 +只租不买 只租不買 +只租不卖 只租不賣 +只称 只稱 +只穿 只穿 +只立 隻立 +只站 只站 +只站到 只站到 +只等 只等 +只答 只答 +只管 只管 +只管出 只管出 +只管到 只管到 +只管去 只管去 +只管在 只管在 +只管来 只管來 +只索 只索 +只红 只紅 +只红到 只紅到 +只约 只約 +只经 只經 +只经过 只經過 +只编 只編 +只缝 只縫 +只罚 只罰 +只考 只考 +只考出 只考出 +只考到 只考到 +只聘 只聘 +只肯 只肯 +只能 只能 +只能在 只能在 +只能有 只能有 +只花 只花 +只补 只補 +只被 只被 +只装 只裝 +只要 只要 +只要会 只要會 +只要在 只要在 +只要是 只要是 +只要有 只要有 +只要能 只要能 +只见 只見 +只见树木 只見樹木 +只言片字 隻言片字 +只言片语 隻言片語 +只订 只訂 +只认钱不认人 只認錢不認人 +只让 只讓 +只让出 只讓出 +只让到 只讓到 +只记 只記 +只记到 只記到 +只记在 只記在 +只记得 只記得 +只讲 只講 +只讲出 只講出 +只讲到 只講到 +只许 只許 +只许州官放火不许百姓点灯 只許州官放火不許百姓點燈 +只许成功 只許成功 +只设 只設 +只设到 只設到 +只设在 只設在 +只说 只說 +只说不做 只說不做 +只说出 只說出 +只说到 只說到 +只说是 只說是 +只读 只讀 +只调 只調 +只费 只費 +只赌 只賭 +只赔 只賠 +只赚 只賺 +只赚到 只賺到 +只赢 只贏 +只赢不输 只贏不輸 +只走 只走 +只走到 只走到 +只赶 只趕 +只赶出 只趕出 +只赶到 只趕到 +只赶去 只趕去 +只赶回 只趕回 +只赶来 只趕來 +只跌 只跌 +只跟 只跟 +只跟到 只跟到 +只跟去 只跟去 +只跟在 只跟在 +只跟来 只跟來 +只跳 只跳 +只踢 只踢 +只踩 只踩 +只踩到 只踩到 +只身 隻身 +只身一人 隻身一人 +只身上已 只身上已 +只身上无 只身上無 +只身上有 只身上有 +只身上没 只身上沒 +只身上的 只身上的 +只身孤影 隻身孤影 +只转 只轉 +只转到 只轉到 +只轮不反 隻輪不反 +只轮不返 隻輪不返 +只轰 只轟 +只载 只載 +只输 只輸 +只输到 只輸到 +只达 只達 +只过 只過 +只过去 只過去 +只过来 只過來 +只进 只進 +只进到 只進到 +只追 只追 +只追到 只追到 +只追回 只追回 +只送 只送 +只送不卖 只送不賣 +只通 只通 +只通到 只通到 +只道 只道 +只配 只配 +只采 只採 +只采到 只採到 +只采声 只採聲 +只重衣衫不重人 只重衣衫不重人 +只量 只量 +只量出 只量出 +只量到 只量到 +只铺 只鋪 +只销 只銷 +只镀 只鍍 +只问 只問 +只闯 只闖 +只闯出 只闖出 +只闯到 只闖到 +只闻 只聞 +只防 只防 +只降 只降 +只降到 只降到 +只限 只限 +只限于 只限於 +只限到 只限到 +只限在 只限在 +只陪 只陪 +只需 只需 +只需要 只需要 +只露 只露 +只露出 只露出 +只露到 只露到 +只靠 只靠 +只须 只須 +只顾 只顧 +只顾到 只顧到 +只顾在 只顧在 +只领 只領 +只领到 只領到 +只飞 只飛 +只鸡斗酒 只雞斗酒 +只鸡絮酒 隻雞絮酒 +叫个 叫個 +叫了 叫了 +叫价 叫價 +叫出 叫出 +叫出去 叫出去 +叫出声 叫出聲 +叫出来 叫出來 +叫回 叫回 +叫回去 叫回去 +叫回来 叫回來 +叫板 叫板 +召回 召回 +召幸 召幸 +叮人战术 叮人戰術 +叮叮当当 叮叮噹噹 +叮叮当当的婆娘 叮叮噹噹的婆娘 +叮当 叮噹 +叮当作响 叮噹作響 +叮当响 叮噹響 +叮当声 叮噹聲 +可了不得 可了不得 +可于 可於 +可仑坡 可侖坡 +可以克制 可以剋制 +可供参考 可供參考 +可免于 可免於 +可别 可別 +可变化合价 可變化合價 +可可托海 可可托海 +可可托海镇 可可托海鎮 +可叹 可嘆 +可向 可向 +可周 可週 +可喜娘 可喜娘 +可回收 可回收 +可好了 可好了 +可少不了 可少不了 +可展曲面 可展曲面 +可干制 可乾製 +可干拭 可乾拭 +可干饮 可乾飲 +可当 可當 +可怜虫 可憐蟲 +可恶 可惡 +可愿 可願 +可愿意 可願意 +可憎娘 可憎娘 +可憎才 可憎才 +可持续发展 可持續發展 +可操作的艺术 可操作的藝術 +可擦写可编程只读存储器 可擦寫可編程只讀存儲器 +可望取胜者 可望取勝者 +可欲 可欲 +可紧可松 可緊可鬆 +可结合性 可結合性 +可转换同位素 可轉換同位素 +可采 可採 +可鉴 可鑑 +台上 臺上 +台下 臺下 +台东 臺東 +台东人 臺東人 +台东区 臺東區 +台东县 臺東縣 +台东县长 臺東縣長 +台东大学 臺東大學 +台东市 臺東市 +台东线 臺東線 +台两 臺兩 +台中 臺中 +台中人 臺中人 +台中区 臺中區 +台中县 臺中縣 +台中县市 臺中縣市 +台中商专 臺中商專 +台中场 臺中場 +台中市 臺中市 +台中市人 臺中市人 +台中市文 臺中市文 +台中师范 臺中師範 +台中师范学院 臺中師範學院 +台中店 臺中店 +台中敎育大学 臺中教育大學 +台中港 臺中港 +台中港路 臺中港路 +台中盆地 臺中盆地 +台中站 臺中站 +台中讯 臺中訊 +台企联 臺企聯 +台企银 臺企銀 +台佟 臺佟 +台候 臺候 +台儿庄 臺兒莊 +台光 臺光 +台军 臺軍 +台凤 臺鳳 +台凳 檯凳 +台函 臺函 +台制 臺製 +台制品 臺製品 +台前 臺前 +台前县 臺前縣 +台前台后 臺前臺後 +台办 臺辦 +台北 臺北 +台北人 臺北人 +台北区 臺北區 +台北县 臺北縣 +台北地院 臺北地院 +台北场 臺北場 +台北城 臺北城 +台北大学 臺北大學 +台北客 臺北客 +台北市 臺北市 +台北市市 臺北市市 +台北市立 臺北市立 +台北市长 臺北市長 +台北市队 臺北市隊 +台北师院 臺北師院 +台北护专 臺北護專 +台北捷运 臺北捷運 +台北桥 臺北橋 +台北港 臺北港 +台北盆地 臺北盆地 +台北站 臺北站 +台北讯 臺北訊 +台北队 臺北隊 +台华轮 臺華輪 +台南 臺南 +台南人 臺南人 +台南区 臺南區 +台南县 臺南縣 +台南县市 臺南縣市 +台南大学 臺南大學 +台南孔庙 臺南孔廟 +台南市 臺南市 +台南府 臺南府 +台南站 臺南站 +台历 檯曆 +台压版 臺壓版 +台台 臺臺 +台号 臺號 +台司 臺司 +台后 臺後 +台启 臺啓 +台命 臺命 +台和 臺和 +台商 臺商 +台商圈 臺商圈 +台啤队 臺啤隊 +台地 臺地 +台场 臺場 +台型 臺型 +台城 臺城 +台基 臺基 +台塑 臺塑 +台墩 臺墩 +台大 臺大 +台大医院 臺大醫院 +台大学生 臺大學生 +台女 臺女 +台妹 臺妹 +台委 臺委 +台媒 臺媒 +台子 臺子 檯子 +台子女 臺子女 +台子孙 臺子孫 +台孙 臺孫 +台孩 臺孩 +台安 檯安 +台安县 檯安縣 +台客 臺客 +台客呛辣 臺客嗆辣 +台客舞 臺客舞 +台尺 臺尺 +台山 台山 +台山市 臺山市 +台峪 臺峪 +台州 台州 +台州地区 台州地區 +台州市 台州市 +台币 臺幣 +台布 檯布 +台布下 檯布下 +台师大 臺師大 +台帘 臺簾 +台席 臺席 +台庆 臺慶 +台座 臺座 +台开 臺開 +台式 臺式 +台式机 臺式機 +台意 臺意 +台意怒 臺意怒 +台成 臺成 +台扇 檯扇 +台拉维夫 臺拉維夫 +台指期 臺指期 +台收 臺收 +台数 臺數 +台文 臺文 +台斤 臺斤 +台新 臺新 +台新金 臺新金 +台新银 臺新銀 +台日 臺日 +台旨 臺旨 +台期指 臺期指 +台本 臺本 +台机 臺機 +台柱 臺柱 +台榭 臺榭 +台槐 臺槐 +台步 臺步 +台水澎运 臺水澎運 +台江 臺江 +台江区 臺江區 +台江县 臺江縣 +台汽 臺汽 +台汽客运 臺汽客運 +台海 臺海 +台海两岸 臺海兩岸 +台港 臺港 +台港澳侨 臺港澳僑 +台湾 臺灣 +台湾世界展望会 臺灣世界展望會 +台湾人 臺灣人 +台湾光复 臺灣光復 +台湾光复节 臺灣光復節 +台湾共和国 臺灣共和國 +台湾关系法 臺灣關係法 +台湾凤蝶 臺灣鳳蝶 +台湾化 臺灣化 +台湾区 臺灣區 +台湾台 臺灣台 +台湾史 臺灣史 +台湾叶鼻蝠 臺灣葉鼻蝠 +台湾味 臺灣味 +台湾团 臺灣團 +台湾国 臺灣國 +台湾国语 臺灣國語 +台湾土狗 臺灣土狗 +台湾地区 臺灣地區 +台湾大学 臺灣大學 +台湾大学附属医学院 臺灣大學附屬醫學院 +台湾女 臺灣女 +台湾学术网路 臺灣學術網路 +台湾岛 臺灣島 +台湾工业技术学院 臺灣工業技術學院 +台湾师范大学 臺灣師範大學 +台湾心 臺灣心 +台湾敎育学院 臺灣教育學院 +台湾散打王 臺灣散打王 +台湾新美术运动 臺灣新美術運動 +台湾日 臺灣日 +台湾日日新报 臺灣日日新報 +台湾杜鹃 臺灣杜鵑 +台湾歌 臺灣歌 +台湾民主自治同盟 臺灣民主自治同盟 +台湾汽车客运公司 臺灣汽車客運公司 +台湾海峡 臺灣海峽 +台湾海洋大学 臺灣海洋大學 +台湾海洋学院 臺灣海洋學院 +台湾版 臺灣版 +台湾犬 臺灣犬 +台湾猕猴桃 臺灣獼猴桃 +台湾玉 臺灣玉 +台湾环颈雉 臺灣環頸雉 +台湾电力 臺灣電力 +台湾电力公司 臺灣電力公司 +台湾电视公司 臺灣電視公司 +台湾疗养院 臺灣療養院 +台湾省 臺灣省 +台湾省政府教育厅 臺灣省政府教育廳 +台湾省政府林业试验所 臺灣省政府林業試驗所 +台湾省政府消防处 臺灣省政府消防處 +台湾省政府警政厅 臺灣省政府警政廳 +台湾省政府财政厅 臺灣省政府財政廳 +台湾省立 臺灣省立 +台湾省议会 臺灣省議會 +台湾社 臺灣社 +台湾神学院 臺灣神學院 +台湾科技大学 臺灣科技大學 +台湾籍 臺灣籍 +台湾米 臺灣米 +台湾粉蝶 臺灣粉蝶 +台湾精品 臺灣精品 +台湾糖业公司 臺灣糖業公司 +台湾红 臺灣紅 +台湾经验 臺灣經驗 +台湾肥料公司 臺灣肥料公司 +台湾航业公司 臺灣航業公司 +台湾艺术专科学校 臺灣藝術專科學校 +台湾茶 臺灣茶 +台湾菜 臺灣菜 +台湾蓝鹊 臺灣藍鵲 +台湾行 臺灣行 +台湾警察专科学校 臺灣警察專科學校 +台湾证券集中保管公司 臺灣證券集中保管公司 +台湾话 臺灣話 +台湾铝业 臺灣鋁業 +台湾铝业公司 臺灣鋁業公司 +台湾银行 臺灣銀行 +台湾队 臺灣隊 +台湾青年 臺灣青年 +台湾馆 臺灣館 +台湾高山族 臺灣高山族 +台澎 臺澎 +台澎金马 臺澎金馬 +台灯 檯燈 +台照 臺照 +台版 臺版 +台独 臺獨 +台独运动 臺獨運動 +台玻 臺玻 +台球 檯球 +台球桌 檯球桌 +台甫 臺甫 +台电 臺電 +台电公司 臺電公司 +台盆 臺盆 +台盏 臺盞 +台盘 檯盤 +台省 臺省 +台矶 臺磯 +台科大 臺科大 +台秤 檯秤 +台积电 臺積電 +台站 臺站 +台端 臺端 +台笔 檯筆 +台籍 臺籍 +台糖 臺糖 +台糖公司 臺糖公司 +台经院 臺經院 +台维斯杯 臺維斯杯 +台网 臺網 +台美 臺美 +台联 臺聯 +台联会 臺聯會 +台联党 臺聯黨 +台股 臺股 +台肥 臺肥 +台胜科 臺勝科 +台胞 臺胞 +台胞证 臺胞證 +台航 臺航 +台艺 臺藝 +台艺大 臺藝大 +台菜 臺菜 +台菜餐厅 臺菜餐廳 +台衔 臺銜 +台装 臺裝 +台装货 臺裝貨 +台西 臺西 +台西乡 臺西鄉 +台视 臺視 +台视公司 臺視公司 +台讳 臺諱 +台词 臺詞 +台语 臺語 +台语歌 臺語歌 +台账 臺賬 +台资 臺資 +台车 臺車 +台车道 臺車道 +台辅 臺輔 +台达 臺達 +台达电 臺達電 +台金 臺金 +台鉴 臺鑒 +台钟 檯鐘 +台钱 臺錢 +台钻 臺鑽 +台铁 臺鐵 +台铁局 臺鐵局 +台铝 臺鋁 +台银 臺銀 +台长 臺長 +台阁 臺閣 +台阁生风 臺閣生風 +台阶 臺階 +台面 檯面 +台面上 檯面上 +台面前 檯面前 +台音 臺音 +台风 颱風 +台风后 颱風後 +台风夜 颱風夜 +台风天 颱風天 +台风季 颱風季 +台风尾 颱風尾 +台风眼 颱風眼 +台风稳健 臺風穩健 +台风草 颱風草 +台风警报 颱風警報 +台风险 颱風險 +台风雨 颱風雨 +台马轮 臺馬輪 +台驾 臺駕 +叱吒风云 叱吒風雲 +叱咤乐坛 叱吒樂壇 +叱咤叱叱咤 叱吒叱叱吒 +叱咤叱咤叱咤咤 叱吒叱吒叱吒吒 +叱咤风云 叱吒風雲 +叱嗟风云 叱嗟風雲 +史克里亚宾 史克里亞賓 +史冬鹏 史冬鵬 +史勒德克 史勒德克 +史坦尼克 史坦尼克 +史坦布律克 史坦布律克 +史坦布瑞纳 史坦布瑞納 +史坦布莱纳 史坦布萊納 +史垂克 史垂克 +史塔克 史塔克 +史塔克豪斯 史塔克豪斯 +史崔克 史崔克 +史托克 史托克 +史托姆 史托姆 +史托威 史托威 +史托瑟 史托瑟 +史托腾柏格 史托騰柏格 +史托苏儿 史托蘇兒 +史査克 史查克 +史査克队 史查克隊 +史派克李 史派克李 +史派克特 史派克特 +史游 史游 +史瑞克 史瑞克 +史瑞波妮克 史瑞波妮克 +史瓦布 史瓦布 +史翠克 史翠克 +史莱克 史萊克 +史达克 史達克 +史迹 史蹟 +右分枝关系从句 右分枝關係從句 +右后 右後 +右后卫 右後衛 +右后方 右後方 +右戚 右戚 +右拐 右拐 +右面 右面 +叶一茜 葉一茜 +叶丁仁 葉丁仁 +叶下珠 葉下珠 +叶丛 葉叢 +叶两传 葉兩傳 +叶亮清 葉亮清 +叶代芝 葉代芝 +叶伟志 葉偉志 +叶佐禹 葉佐禹 +叶俊凯 葉俊凱 +叶俊荣 葉俊榮 +叶儿 葉兒 +叶全真 葉全真 +叶公好龙 葉公好龍 +叶公超 葉公超 +叶兰 葉蘭 +叶利钦 葉利欽 +叶南铭 葉南銘 +叶卡捷琳堡 葉卡捷琳堡 +叶卡捷琳娜 葉卡捷琳娜 +叶卡特琳娜堡 葉卡特琳娜堡 +叶原 葉原 +叶县 葉縣 +叶口蝠科 葉口蝠科 +叶可欣 葉可欣 +叶可蓁 葉可蓁 +叶叶 葉葉 +叶叶琹 葉叶琹 +叶名琛 葉名琛 +叶君璋 葉君璋 +叶咏捷 葉詠捷 +叶国兴 葉國興 +叶圣陶 葉聖陶 +叶块繁殖 葉塊繁殖 +叶城县 葉城縣 +叶堂 葉堂 +叶天士 葉天士 +叶子 葉子 +叶子列 葉子列 +叶子戏 葉子戲 +叶子杰 葉子杰 +叶子格 葉子格 +叶子烟 葉子菸 +叶子纲 葉子綱 +叶宗留 葉宗留 +叶宗贤 葉宗賢 +叶宜津 葉宜津 +叶宪祖 葉憲祖 +叶尔勤 葉爾勤 +叶尔羌 葉爾羌 +叶尔羌河 葉爾羌河 +叶尔辛 葉爾辛 +叶尖 葉尖 +叶尼基河 葉尼基河 +叶尼塞河 葉尼塞河 +叶庆元 葉慶元 +叶序 葉序 +叶形 葉形 +叶彦伯 葉彥伯 +叶彰廷 葉彰廷 +叶德辉 葉德輝 +叶志仙 葉志仙 +叶志远 葉志遠 +叶恭弘 叶恭弘 +叶惠青 葉惠青 +叶慈 葉慈 +叶成忠 葉成忠 +叶拱 葉拱 +叶挺 葉挺 +叶政彦 葉政彥 +叶文贵 葉文貴 +叶斑 葉斑 +叶斑病 葉斑病 +叶斯沛森 葉斯沛森 +叶昌桐 葉昌桐 +叶明顺 葉明順 +叶景栋 葉景棟 +叶杰生 葉傑生 +叶枝 葉枝 +叶柄 葉柄 +叶树 葉樹 +叶树姗 葉樹姍 +叶根吉 葉根吉 +叶正玲 葉正玲 +叶步梁 葉步樑 +叶永烈 葉永烈 +叶永骞 葉永騫 +叶江川 葉江川 +叶泰兴 葉泰興 +叶泽山 葉澤山 +叶清照 葉清照 +叶湘伦 葉湘倫 +叶炳强 葉炳強 +叶片 葉片 +叶片状 葉片狀 +叶状 葉狀 +叶状体 葉狀體 +叶状植物 葉狀植物 +叶王 葉王 +叶甜菜 葉甜菜 +叶盛兰 葉盛蘭 +叶硕堂 葉碩堂 +叶礼庭 葉禮庭 +叶紫华 葉紫華 +叶红素 葉紅素 +叶绍锜 葉紹錡 +叶绿体 葉綠體 +叶绿体基质 葉綠體基質 +叶绿素 葉綠素 +叶缘 葉緣 +叶美君 葉美君 +叶羿君 葉羿君 +叶耳 葉耳 +叶肉 葉肉 +叶肥 葉肥 +叶脉 葉脈 +叶脉序 葉脈序 +叶脤 葉脤 +叶腋 葉腋 +叶舌 葉舌 +叶芽 葉芽 +叶茎 葉莖 +叶菊兰 葉菊蘭 +叶菜类 葉菜類 +叶落归根 葉落歸根 +叶落归秋 葉落歸秋 +叶落知秋 葉落知秋 +叶落粪本 葉落糞本 +叶蕴仪 葉蘊儀 +叶蜂 葉蜂 +叶蜡石 葉蠟石 +叶蝉 葉蟬 +叶语 葉語 +叶赛莺 葉賽鶯 +叶赤铁路 葉赤鐵路 +叶跡 葉跡 +叶轮 葉輪 +叶轮机械 葉輪機械 +叶轴 葉軸 +叶适 葉適 +叶选平 葉選平 +叶酸 葉酸 +叶金川 葉金川 +叶针 葉針 +叶锈病 葉鏽病 +叶长龙 葉長龍 +叶门 葉門 +叶门共和国 葉門共和國 +叶闲花 葉閒花 +叶面 葉面 +叶面对称 葉面對稱 +叶面施肥 葉面施肥 +叶鞘 葉鞘 +叶音 叶音 +叶韵 叶韻 +叶黄素 葉黃素 +叶鼻蝠 葉鼻蝠 +号寒啼饥 號寒啼飢 +号志 號誌 +号志机 號誌機 +号志灯 號誌燈 +号挂子 號掛子 +号板 號板 +号码布 號碼布 +号舍 號舍 +司令台 司令臺 +司天台 司天臺 +司法制度 司法制度 +叹为 嘆爲 +叹为观止 歎爲觀止 +叹之 嘆之 +叹了 嘆了 +叹号 歎號 +叹喟 嘆喟 +叹声 嘆聲 +叹息 嘆息 +叹惋 嘆惋 +叹惜 嘆惜 +叹曰 嘆曰 +叹服 歎服 +叹气 嘆氣 +叹绝 歎絕 +叹羡 歎羨 +叹老 嘆老 +叹词 嘆詞 +叹赏 歎賞 +叹道 嘆道 +吁了 吁了 +吁俞 吁俞 +吁叹 吁嘆 +吁吁 吁吁 +吁咈 吁咈 +吁咈都俞 吁咈都俞 +吁嗟 吁嗟 +吁嘘 吁噓 +吁天 籲天 +吁气 吁氣 +吁求 籲求 +吁请 籲請 +吃一顿挨一顿 吃一頓挨一頓 +吃不了 吃不了 +吃不出 吃不出 +吃不出来 吃不出來 +吃了 吃了 +吃了定心丸 吃了定心丸 +吃了秤砣 吃了秤砣 +吃了蜜蜂儿屎似的 吃了蜜蜂兒屎似的 +吃了饭 吃了飯 +吃亏上当 吃虧上當 +吃亏就是占便宜 吃虧就是佔便宜 +吃人一个蛋恩情无法断 吃人一個蛋恩情無法斷 +吃人虫 吃人蟲 +吃伤了 吃傷了 +吃几碗干饭 吃幾碗乾飯 +吃出 吃出 +吃合家欢 吃合家歡 +吃后悔药 吃後悔藥 +吃回头草 吃回頭草 +吃地面 吃地面 +吃姜 吃薑 +吃完面 吃完麪 +吃尽 吃盡 +吃尽当光 吃盡當光 +吃干了 吃乾了 +吃干醋 吃乾醋 +吃得了 吃得了 +吃得出 吃得出 +吃得出来 吃得出來 +吃挂络儿 吃掛絡兒 +吃敲才 吃敲才 +吃板刀面 吃板刀麪 +吃枪药 吃槍藥 +吃烟 吃煙 +吃药 吃藥 +吃药前 吃藥前 +吃药后 吃藥後 +吃药时 吃藥時 +吃豆干 吃豆乾 +吃辣面 吃辣麪 +吃过面 吃過麪 +吃里扒外 吃裏扒外 +吃里爬外 吃裏爬外 +吃钉板 吃釘板 +吃错药 吃錯藥 +吃闲话 吃閒話 +吃闲饭 吃閒飯 +吃面 吃麪 +吃饭傢伙 吃飯傢伙 +吃饭别忘了种谷人 吃飯別忘了種穀人 +吃饭家伙 吃飯家伙 +吃饱了饭撑的 吃飽了飯撐的 +吃饱没事干 吃飽沒事幹 +各不相同 各不相同 +各个 各個 +各个击破 各個擊破 +各人儿洗面儿各人儿光 各人兒洗面兒各人兒光 +各从其志 各從其志 +各借 各借 +各党 各黨 +各党派 各黨派 +各出 各出 +各别 各別 +各升 各升 +各厂 各廠 +各发 各發 +各吊 各吊 +各向 各向 +各向异性 各向異性 +各回 各回 +各回合 各回合 +各团 各團 +各团体 各團體 +各如其面 各如其面 +各尽 各盡 +各尽其用 各盡其用 +各尽所能 各盡所能 +各干各 各幹各 +各当 各當 +各念 各念 +各打五十板 各打五十板 +各方面 各方面 +各有千秋 各有千秋 +各有胜负 各有勝負 +各版面 各版面 +各种 各種 +各种事 各種事 +各种人 各種人 +各种各样 各種各樣 +各种颜色 各種顏色 +各签 各簽 +各类钟 各類鐘 +各系 各系 +各系所 各系所 +各级党委 各級黨委 +各胜 各勝 +各行其志 各行其志 +各谋出路 各謀出路 +各辟 各闢 +各辟蹊径 各闢蹊徑 +各里 各里 +各院系 各院系 +各须 各須 +合上 合上 +合下 合下 +合不合 合不合 +合不拢嘴 合不攏嘴 +合不来 合不來 +合中 閤中 +合久必分 合久必分 +合义复词 合義複詞 +合乎 合乎 +合乎逻辑 合乎邏輯 +合于 合於 +合于时宜 合于時宜 +合从 合從 +合付 合付 +合众 合衆 +合众为一 合衆爲一 +合众国 合衆國 +合众国际社 合衆國際社 +合众社 合衆社 +合众银行 合衆銀行 +合伙 合夥 +合伙人 合夥人 +合会 合會 +合传 合傳 +合住 合住 +合体 合體 +合体字 合體字 +合作 合作 +合作主义 合作主義 +合作伙伴 合作伙伴 +合作农场 合作農場 +合作化 合作化 +合作协议 合作協議 +合作商店 合作商店 +合作学 合作學 +合作市 合作市 +合作所 合作所 +合作无懈 合作無懈 +合作无间 合作無間 +合作案 合作案 +合作社 合作社 +合作署 合作署 +合作者 合作者 +合作节 合作節 +合作金库 合作金庫 +合借 合借 +合做 合做 +合儿 閤兒 +合八字 合八字 +合养 合養 +合准 合準 +合出 合出 +合击 合擊 +合刃 合刃 +合则两利 合則兩利 +合到 合到 +合券 合券 +合刻 合刻 +合剂 合劑 +合力 合力 +合办 合辦 +合十 合十 +合卺 合巹 +合历 合曆 +合发 合發 +合变 合變 +合口 合口 +合口元音 合口元音 +合口味 合口味 +合口呼 合口呼 +合叶 合葉 +合吃 合吃 +合吃族 合吃族 +合合 合合 +合同 合同 +合同各方 合同各方 +合同文字 合同文字 +合同法 合同法 +合后 合後 +合唱 合唱 +合唱团 合唱團 +合唱曲 合唱曲 +合唱队 合唱隊 +合嘴合舌 合嘴合舌 +合四乙尺工 合四乙尺工 +合围 合圍 +合在 合在 +合在一起 合在一起 +合声 合聲 +合夥人 合夥人 +合头 合頭 +合奏 合奏 +合奏团 合奏團 +合奏曲 合奏曲 +合契若神 合契若神 +合好 合好 +合婚 合婚 +合子 合子 +合子钱 合子錢 +合子饼 合子餅 +合宅 合宅 +合定 合定 +合宜 合宜 +合家 閤家 +合家大小 合家大小 +合家子 閤家子 +合家欢 閤家歡 +合局 合局 +合山市 合山市 +合川区 合川區 +合并 合併 +合并为 合併爲 +合并在 合併在 +合并成 合併成 +合并有 合併有 +合并案 合併案 +合并症 合併症 +合府 閤府 +合府上 閤府上 +合度 合度 +合式 合式 +合弦 合弦 +合弹 合彈 +合当 合當 +合当有事 合當有事 +合彩 合彩 +合影 合影 +合影留念 合影留念 +合得 合得 +合得来 合得來 +合心 合心 +合心合意 合心合意 +合恩角 合恩角 +合情 合情 +合情合理 合情合理 +合情理 合情理 +合意 合意 +合意儿 合意兒 +合成 合成 +合成乐器 合成樂器 +合成乳 合成乳 +合成代谢 合成代謝 +合成体 合成體 +合成作用 合成作用 +合成器 合成器 +合成数 合成數 +合成染料 合成染料 +合成树脂 合成樹脂 +合成橡胶 合成橡膠 +合成氨 合成氨 +合成法 合成法 +合成洗涤 合成洗滌 +合成洗涤剂 合成洗滌劑 +合成清洁剂 合成清潔劑 +合成物 合成物 +合成皮 合成皮 +合成石油 合成石油 +合成类固醇 合成類固醇 +合成纤维 合成纖維 +合成色素 合成色素 +合成词 合成詞 +合成语境 合成語境 +合成语音 合成語音 +合战 合戰 +合手 合手 +合扑 合撲 +合折 合折 +合报 合報 +合抱 合抱 +合拍 合拍 +合拢 合攏 +合指症 合指症 +合掌 合掌 +合掌瓜 合掌瓜 +合数 合數 +合族 合族 +合时 合時 +合时宜 合時宜 +合昏 合昏 +合是 合是 +合朔 合朔 +合本 合本 +合机 合機 +合杀 合殺 +合板 合板 +合板眼 合板眼 +合校 合校 +合格 合格 +合格率 合格率 +合格者 合格者 +合格证 合格證 +合格赛 合格賽 +合欢 合歡 +合欢山 合歡山 +合欢扇 合歡扇 +合欢结 合歡結 +合款 合款 +合气 合氣 +合气道 合氣道 +合水县 合水縣 +合江县 合江縣 +合江省 合江省 +合沓 合沓 +合法 合法 +合法化 合法化 +合法性 合法性 +合法斗争 合法鬥爭 +合注 合注 +合洗 合洗 +合派 合派 +合流 合流 +合流处 合流處 +合浦县 合浦縣 +合浦珠还 合浦珠還 +合浦还珠 合浦還珠 +合溜 合溜 +合演 合演 +合火 合火 +合照 合照 +合照留念 合照留念 +合爲 合爲 +合牵 合牽 +合独 合獨 +合班 合班 +合球 合球 +合理 合理 +合理化 合理化 +合理化作用 合理化作用 +合理合情 合理合情 +合理密植 合理密植 +合理布局 合理佈局 +合理性 合理性 +合理标 合理標 +合理错误 合理錯誤 +合璧 合璧 +合璧连珠 合璧連珠 +合瓣 合瓣 +合瓣花冠 合瓣花冠 +合生 合生 +合用 合用 +合盖 合蓋 +合眼 閤眼 +合眼摸象 合眼摸象 +合着 合着 +合租 合租 +合稀释 合稀釋 +合窆 合窆 +合站 合站 +合符 合符 +合签 合簽 +合算 合算 +合纤 合纖 +合约 合約 +合约书 合約書 +合约人 合約人 +合约价 合約價 +合约法 合約法 +合纵 合縱 +合纵连横 合縱連橫 +合组 合組 +合编 合編 +合缝 合縫 +合群 合羣 +合群性 合羣性 +合而为一 合而爲一 +合股 合股 +合肥 合肥 +合肥人 合肥人 +合肥县 合肥縣 +合肥工业大学 合肥工業大學 +合肥市 合肥市 +合胃口 合胃口 +合色鞋 合色鞋 +合苦 合苦 +合菜戴帽 合菜戴帽 +合营 合營 +合著 合著 +合著者 合著者 +合葬 合葬 +合补 合補 +合计 合計 +合计为 合計爲 +合订本 合訂本 +合议制 合議制 +合议庭 合議庭 +合该 合該 +合该有事 合該有事 +合请 合請 +合读 合讀 +合调 合調 +合谋 合謀 +合谐 合諧 +合谷 合谷 +合谷穴 合谷穴 +合购 合購 +合资 合資 +合资案 合資案 +合起 合起 +合起来 合起來 +合踢 合踢 +合踩 合踩 +合身 合身 +合辙 合轍 +合辙儿 合轍兒 +合辙押韵 合轍押韻 +合适 合適 +合通 合通 +合造 合造 +合逻辑 合邏輯 +合遝 合遝 +合醵 合醵 +合采 合採 +合金 合金 +合金元素 合金元素 +合金钢 合金鋼 +合钓 合釣 +合锯 合鋸 +合镜 合鏡 +合闸 合閘 +合阳县 合陽縣 +合零为整 合零爲整 +合面 合面 +合音 合音 +合领 合領 +合骑 合騎 +合髻 合髻 +合龙 合龍 +合龙门 合龍門 +吉丁当 吉丁當 +吉伯特氏症候群 吉伯特氏症候羣 +吉凶 吉凶 +吉凶庆吊 吉凶慶弔 +吉凶悔吝 吉凶悔吝 +吉凶未卜 吉凶未卜 +吉卜力工作室 吉卜力工作室 +吉卜赛 吉卜賽 +吉卜赛人 吉卜賽人 +吉卜龄 吉卜齡 +吉占 吉占 +吉尔库克 吉爾庫克 +吉尼普里 吉尼普里 +吉布兹 吉布茲 +吉布地 吉布地 +吉布地共和国 吉布地共和國 +吉布提 吉布提 +吉布森 吉布森 +吉普斯夸 吉普斯夸 +吉林师范大学 吉林師範大學 +吉田松阴 吉田松陰 +吉蔑 吉蔑 +吉蔑族 吉蔑族 +吉诺布里 吉諾布里 +吉里 吉里 +吉里巴斯 吉里巴斯 +吉里巴斯共和国 吉里巴斯共和國 +吊上 吊上 +吊上去 吊上去 +吊上来 吊上來 +吊下 吊下 +吊下去 吊下去 +吊下来 吊下來 +吊丧 弔喪 +吊丧问疾 弔喪問疾 +吊个 吊個 +吊书 弔書 +吊了 吊了 +吊伐 吊伐 +吊儿郎当 吊兒郎當 +吊兰 吊蘭 +吊具 吊具 +吊刑 吊刑 +吊到 吊到 +吊卷 吊卷 +吊去 吊去 +吊取 吊取 +吊古 弔古 +吊古寻幽 弔古尋幽 +吊吊 吊吊 +吊名 吊名 +吊唁 弔唁 +吊喉 弔喉 +吊喭 弔喭 +吊嗓 吊嗓 +吊嗓子 吊嗓子 +吊嘴 吊嘴 +吊场 吊場 +吊坠 吊墜 +吊塔 吊塔 +吊头 弔頭 +吊奠 弔奠 +吊好 吊好 +吊子 吊子 +吊子曰儿 吊子曰兒 +吊孝 弔孝 +吊客 弔客 +吊客眉 弔客眉 +吊宴 弔宴 +吊带 吊帶 +吊带衫 吊帶衫 +吊带裤 吊帶褲 +吊床 吊牀 +吊影 弔影 +吊得 吊得 +吊慰 弔慰 +吊扇 吊扇 +吊打 吊打 +吊扣 吊扣 吊釦 +吊拷 弔拷 +吊拷絣把 吊拷絣把 +吊拷绷扒 吊拷繃扒 +吊挂 吊掛 +吊挂在 吊掛在 +吊挂着 吊掛着 +吊撒 弔撒 +吊放声纳 吊放聲納 +吊文 弔文 +吊斗 吊斗 +吊旗 弔旗 +吊杆 吊杆 +吊杠 吊槓 +吊来吊去 吊來吊去 +吊架 吊架 +吊档裤 吊檔褲 +吊桥 吊橋 +吊桶 吊桶 +吊桶落在井里 吊桶落在井裏 +吊楼 吊樓 +吊死 吊死 +吊死问孤 弔死問孤 +吊死问疾 弔死問疾 +吊死鬼 吊死鬼 +吊死鬼搽粉 吊死鬼搽粉 +吊死鬼的裹脚布 吊死鬼的裹腳布 +吊毛 吊毛 +吊民 弔民 +吊民伐罪 弔民伐罪 +吊水 吊水 +吊灯 吊燈 +吊环 吊環 +吊球 吊球 +吊瓶族 吊瓶族 +吊盘 吊盤 +吊着 吊着 +吊祭 弔祭 +吊稍 吊稍 +吊窗 吊窗 +吊篮 弔籃 +吊索 吊索 +吊纸 弔紙 +吊线 吊線 +吊绳 吊繩 +吊者大悦 弔者大悅 +吊胃口 吊胃口 +吊脚 吊腳 +吊脚儿 吊腳兒 +吊脚儿事 弔腳兒事 +吊腰撒跨 弔腰撒跨 +吊膀子 吊膀子 +吊臂 弔臂 +吊衣架 吊衣架 +吊袜 吊襪 +吊袜带 吊襪帶 +吊装 吊裝 +吊裤 吊褲 +吊裤带 吊褲帶 +吊誉沽名 吊譽沽名 +吊词 弔詞 +吊诡 弔詭 +吊诡矜奇 弔詭矜奇 +吊谎 弔謊 +吊贺迎送 弔賀迎送 +吊走 吊走 +吊起 吊起 +吊起来 吊起來 +吊车 吊車 +吊车尾 吊車尾 +吊运 吊運 +吊钟 吊鐘 +吊钢丝 吊鋼絲 +吊钩 吊鉤 +吊铺 吊鋪 +吊销 吊銷 +吊销执照 吊銷執照 +吊门 吊門 +吊问 弔問 +吊颈 吊頸 +吊饰 吊飾 +吊鹤 弔鶴 +同一 同一 +同一个 同一個 +同一个世界同一个梦想 同一個世界同一個夢想 +同一人 同一人 +同一场 同一場 +同一型 同一型 +同一处 同一處 +同一家 同一家 +同一年 同一年 +同一座 同一座 +同一性 同一性 +同一所 同一所 +同一片 同一片 +同一班 同一班 +同一组 同一組 +同上 同上 +同下 同下 +同不同意 同不同意 +同业 同業 +同业公会 同業公會 +同业拆借 同業拆借 +同个 同個 +同中有异 同中有異 +同为 同爲 +同义 同義 +同义反复 同義反復 +同义字 同義字 +同义词 同義詞 +同义语 同義語 +同乐 同樂 +同乐会 同樂會 +同乡 同鄉 +同乡亲故 同鄉親故 +同乡会 同鄉會 +同事 同事 +同事家 同事家 +同于 同於 +同产 同產 +同享 同享 +同人 同人 +同人女 同人女 +同人志 同人誌 +同仁 同仁 +同仁县 同仁縣 +同仁堂 同仁堂 +同仇 同仇 +同仇敌忾 同仇敵愾 +同休共戚 同休共慼 +同伙 同夥 +同传耳麦 同傳耳麥 +同伴 同伴 +同位 同位 +同位素 同位素 +同位素分离 同位素分離 +同位素扫描 同位素掃描 +同位角 同位角 +同位语 同位語 +同住 同住 +同体 同體 +同余 同餘 +同余式 同餘式 +同余类 同餘類 +同侪 同儕 +同侪压力 同儕壓力 +同侪团体 同儕團體 +同侪审查 同儕審查 +同侪扶持 同儕扶持 +同侪检视 同儕檢視 +同侪谘商 同儕諮商 +同修 同修 +同做 同做 +同僚 同僚 +同光 同光 +同党 同黨 +同出 同出 +同出一源 同出一源 +同出一脉 同出一脈 +同出同进 同出同進 +同分 同分 +同分异构体 同分異構體 +同分异构物 同分異構物 +同分数 同分數 +同列 同列 +同到 同到 +同功一体 同功一體 +同动词 同動詞 +同化 同化 +同化作用 同化作用 +同区 同區 +同升 同升 +同卵 同卵 +同卵双生 同卵雙生 +同卵双胞胎 同卵雙胞胎 +同去 同去 +同县 同縣 +同参 同參 +同右 同右 +同号 同號 +同吃 同吃 +同吃同住 同吃同住 +同名 同名 +同名之累 同名之累 +同名同姓 同名同姓 +同名数 同名數 +同向 同向 +同命 同命 +同命鸟 同命鳥 +同命鸳鸯 同命鴛鴦 +同唱 同唱 +同喜 同喜 +同团 同團 +同国 同國 +同在 同在 +同地 同地 +同坐 同坐 +同型 同型 +同型性 同型性 +同型机 同型機 +同型配子 同型配子 +同堂 同堂 +同声 同聲 +同声一哭 同聲一哭 +同声之谊 同聲之誼 +同声传译 同聲傳譯 +同声异俗 同聲異俗 +同声相应 同聲相應 +同声翻译 同聲翻譯 +同天 同天 +同好 同好 +同姓 同姓 +同姓同名 同姓同名 +同字框 同字框 +同学 同學 +同学们 同學們 +同学会 同學會 +同学家 同學家 +同学录 同學錄 +同学路 同學路 +同安 同安 +同安区 同安區 +同安县 同安縣 +同安街 同安街 +同宗 同宗 +同宗同气 同宗同氣 +同定 同定 +同室 同室 +同室操戈 同室操戈 +同宿 同宿 +同寅 同寅 +同对 同對 +同尘 同塵 +同居 同居 +同居人 同居人 +同居各爨 同居各爨 +同屋 同屋 +同属 同屬 +同岁 同歲 +同州梆子 同州梆子 +同工 同工 +同工同酬 同工同酬 +同工异曲 同工異曲 +同工异调 同工異調 +同左 同左 +同席 同席 +同年 同年 +同年代 同年代 +同年同月 同年同月 +同年级 同年級 +同年而语 同年而語 +同庆 同慶 +同床 同牀 +同床异梦 同牀異夢 +同庚 同庚 +同度 同度 +同座 同座 +同异 同異 +同张 同張 +同归 同歸 +同归于尽 同歸於盡 +同归殊途 同歸殊途 +同当 同當 +同形词 同形詞 +同往 同往 +同德 同德 +同德县 同德縣 +同德同心 同德同心 +同心 同心 +同心僇力 同心僇力 +同心协力 同心協力 +同心县 同心縣 +同心合力 同心合力 +同心合意 同心合意 +同心同德 同心同德 +同心圆 同心圓 +同心圆理论 同心圓理論 +同心并力 同心並力 +同心戮力 同心戮力 +同心断金 同心斷金 +同心方胜儿 同心方勝兒 +同心结 同心結 +同心髻 同心髻 +同志 同志 +同志合道 同志合道 +同志天地 同志天地 +同志酒吧 同志酒吧 +同忧相救 同憂相救 +同态 同態 +同性 同性 +同性恋 同性戀 +同性恋恐惧症 同性戀恐懼症 +同性恋者 同性戀者 +同性恋酒吧 同性戀酒吧 +同性爱 同性愛 +同性相斥 同性相斥 +同恩 同恩 +同恶相助 同惡相助 +同恶相救 同惡相救 +同恶相求 同惡相求 +同恶相济 同惡相濟 +同情 同情 +同情心 同情心 +同情者 同情者 +同意 同意 +同意书 同意書 +同意权 同意權 +同意案 同意案 +同意票 同意票 +同感 同感 +同愿 同願 +同房 同房 +同房兄弟 同房兄弟 +同手同脚 同手同腳 +同打 同打 +同指 同指 +同排 同排 +同支 同支 +同改 同改 +同文 同文 +同文同种 同文同種 +同文算指 同文算指 +同文韵统 同文韻統 +同文馆 同文館 +同斜层 同斜層 +同方 同方 +同旁內角 同旁內角 +同族 同族 +同日 同日 +同日生 同日生 +同日而言 同日而言 +同日而语 同日而語 +同时 同時 +同时代 同時代 +同时候 同時候 +同时期 同時期 +同时语言学 同時語言學 +同是 同是 +同是天涯沦落人 同是天涯淪落人 +同月 同月 +同月同日 同月同日 +同有 同有 +同期 同期 +同期录音 同期錄音 +同机 同機 +同村 同村 +同条共贯 同條共貫 +同来 同來 +同来同往 同來同往 +同构 同構 +同枕共眠 同枕共眠 +同林鸟 同林鳥 +同校 同校 +同样 同樣 +同样会 同樣會 +同样在 同樣在 +同样地 同樣地 +同样是 同樣是 +同根 同根 +同案 同案 +同案犯 同案犯 +同桌 同桌 +同梦 同夢 +同梯 同梯 +同榜 同榜 +同榻 同榻 +同欢 同歡 +同欢同赏 同歡同賞 +同款 同款 +同正 同正 +同步 同步 +同步传输 同步傳輸 +同步加速器 同步加速器 +同步卫星 同步衛星 +同步录音 同步錄音 +同步数位阶层 同步數位階層 +同步电动 同步電動 +同步辐射仪 同步輻射儀 +同步辐射加速器 同步輻射加速器 +同步辐射研究中心 同步輻射研究中心 +同步进行 同步進行 +同母 同母 +同母异父 同母異父 +同气 同氣 +同气之光 同氣之光 +同气之情 同氣之情 +同气相求 同氣相求 +同气连枝 同氣連枝 +同江 同江 +同江市 同江市 +同治 同治 +同派 同派 +同流合污 同流合污 +同济 同濟 +同济会 同濟會 +同济医科大学 同濟醫科大學 +同济大学 同濟大學 +同渡 同渡 +同温 同溫 +同温同压 同溫同壓 +同温层 同溫層 +同游 同遊 +同源 同源 +同源词 同源詞 +同点 同點 +同爨 同爨 +同爲 同爲 +同父 同父 +同父异母 同父異母 +同牀各梦 同牀各夢 +同牀异梦 同牀異夢 +同班 同班 +同班同学 同班同學 +同理 同理 +同理可证 同理可證 +同理心 同理心 +同甘共苦 同甘共苦 +同甘同苦 同甘同苦 +同甘苦 同甘苦 +同生 同生 +同生共死 同生共死 +同生死 同生死 +同用 同用 +同甲 同甲 +同病相怜 同病相憐 +同盖 同蓋 +同盟 同盟 +同盟会 同盟會 +同盟会宣言 同盟會宣言 +同盟党 同盟黨 +同盟军 同盟軍 +同盟国 同盟國 +同盟条约 同盟條約 +同盟罢工 同盟罷工 +同盟者 同盟者 +同省 同省 +同知 同知 +同砚 同硯 +同种 同種 +同科 同科 +同穴 同穴 +同窗 同窗 +同窗夜语 同窗夜語 +同窗契友 同窗契友 +同站 同站 +同章 同章 +同符合契 同符合契 +同等 同等 +同等学力 同等學力 +同等学历 同等學歷 +同篇 同篇 +同类 同類 +同类产品 同類產品 +同类意识 同類意識 +同类相吸 同類相吸 +同类相呼 同類相呼 +同类相残 同類相殘 +同类相求 同類相求 +同类相聚 同類相聚 +同类相食 同類相食 +同类色 同類色 +同类项 同類項 +同系 同系 +同素异形 同素異形 +同素异形体 同素異形體 +同素异性 同素異性 +同素异性体 同素異性體 +同约 同約 +同级 同級 +同级评审 同級評審 +同级车 同級車 +同线 同線 +同组 同組 +同缘同相 同緣同相 +同罗杯 同羅杯 +同罪 同罪 +同翅目 同翅目 +同考官 同考官 +同职 同職 +同胞 同胞 +同胞兄妹 同胞兄妹 +同胞兄弟 同胞兄弟 +同胞共乳 同胞共乳 +同胞双生 同胞雙生 +同胞爱 同胞愛 +同致 同致 +同舍生 同舍生 +同舟之谊 同舟之誼 +同舟共济 同舟共濟 +同舟而济 同舟而濟 +同船济水 同船濟水 +同色 同色 +同行 同行 +同行同业 同行同業 +同行相忌 同行相忌 +同行语 同行語 +同衾共枕 同衾共枕 +同衾帱 同衾幬 +同袍 同袍 +同袍同泽 同袍同澤 +同被 同被 +同襟 同襟 +同见同知 同見同知 +同言线 同言線 +同订 同訂 +同论 同論 +同语线 同語線 +同调 同調 +同谋 同謀 +同谋者 同謀者 +同谱 同譜 +同质 同質 +同质性 同質性 +同走 同走 +同起同坐 同起同坐 +同跑 同跑 +同路 同路 +同路人 同路人 +同车 同車 +同轨 同軌 +同轨同文 同軌同文 +同轴 同軸 +同轴圆 同軸圓 +同轴圆弧 同軸圓弧 +同轴电缆 同軸電纜 +同辈 同輩 +同边 同邊 +同达 同達 +同过 同過 +同进 同進 +同进同出 同進同出 +同进士出身 同進士出身 +同道 同道 +同道者 同道者 +同配生殖 同配生殖 +同酬 同酬 +同重 同重 +同量 同量 +同量异位素 同量異位素 +同长 同長 +同门 同門 +同门友 同門友 +同门异户 同門異戶 +同队 同隊 +同音 同音 +同音字 同音字 +同音词 同音詞 +同韵词 同韻詞 +同项 同項 +同题 同題 +同高 同高 +同龄 同齡 +同龄人 同齡人 +同龄林 同齡林 +名不当实 名不當實 +名义价值 名義價值 +名人录 名人錄 +名噪 名噪 +名坛 名壇 +名垂后世 名垂後世 +名垂罔极 名垂罔極 +名复金瓯 名覆金甌 +名山胜境 名山勝境 +名师出高徒 名師出高徒 +名录 名錄 +名录服务 名錄服務 +名曲 名曲 +名望体面 名望體面 +名次表 名次表 +名种 名種 +名称标签 名稱標籤 +名系 名系 +名胄 名胄 +名胜 名勝 +名胜古迹 名勝古蹟 +名臣言行录 名臣言行錄 +名表 名錶 +名言录 名言錄 +名重识暗 名重識暗 +名鉴 名鑑 +名门之后 名門之後 +名闻于世 名聞於世 +后七子 後七子 +后上 後上 +后上去 後上去 +后上来 後上來 +后下 後下 +后下去 後下去 +后下来 後下來 +后不为例 後不爲例 +后世 後世 +后两者 後兩者 +后丰 后豐 +后主 後主 +后事 後事 +后事之师 後事之師 +后于 後於 +后人 後人 +后人乘凉 後人乘涼 +后代 後代 +后代子孙 後代子孫 +后仰 後仰 +后仰前合 後仰前合 +后件 後件 +后任 後任 +后会 後會 +后会可期 後會可期 +后会无期 後會無期 +后会有期 後會有期 +后会难期 後會難期 +后传 後傳 +后作 後作 +后侧 後側 +后信号灯 後信號燈 +后偏 後偏 +后像 後像 +后儿 後兒 +后元音 後元音 +后先辉映 後先輝映 +后冠 后冠 +后冷战 後冷戰 +后冷战时代 後冷戰時代 +后凉 後涼 +后凋 後凋 +后分 後分 +后到 後到 +后制 後製 +后力不继 後力不繼 +后加 後加 +后加成分 後加成分 +后劲 後勁 +后劲溪 後勁溪 +后势 後勢 +后勤 後勤 +后勤人员 後勤人員 +后勤区 後勤區 +后勤学 後勤學 +后勤部 後勤部 +后北街 后北街 +后区 後區 +后医系 後醫系 +后半 後半 +后半世 後半世 +后半叶 後半葉 +后半场 後半場 +后半夜 後半夜 +后半天 後半天 +后半季 後半季 +后半晌 後半晌 +后半期 後半期 +后半段 後半段 +后半部 後半部 +后卫 後衛 +后印 後印 +后印象主义 後印象主義 +后去 後去 +后发先至 後發先至 +后发制人 後發制人 +后发座 后髮座 +后台 後臺 +后台老板 後臺老闆 +后叶 後葉 +后合前仰 後合前仰 +后后 後後 +后向 後向 +后周 後周 +后味 後味 +后命 後命 +后哨 後哨 +后唐 後唐 +后嗣 後嗣 +后园 後園 +后图 後圖 +后土 后土 +后场 後場 +后坐 後坐 +后坐力 後坐力 +后埔 後埔 +后堂 後堂 +后壁 後壁 +后壁乡 後壁鄉 +后壁湖 後壁湖 +后备 後備 +后备军 後備軍 +后备军人 後備軍人 +后备部 後備部 +后天 後天 +后天免疫 後天免疫 +后天免疫缺乏症候群 後天免疫缺乏症候羣 +后天性 後天性 +后夫 後夫 +后头 後頭 +后奏 後奏 +后奏曲 後奏曲 +后妃 后妃 +后妈 後媽 +后妻 後妻 +后娘 後孃 +后婚 後婚 +后婚儿 後婚兒 +后嫁 後嫁 +后学 後學 +后学儿 後學兒 +后安路 后安路 +后实先声 後實先聲 +后宫 後宮 +后尘 後塵 +后尧婆 後堯婆 +后尾 後尾 +后尾儿 後尾兒 +后山 後山 +后巷 後巷 +后巷前街 後巷前街 +后市 後市 +后帝 后帝 +后平路 后平路 +后年 後年 +后广告纪元 後廣告紀元 +后庄 後莊 +后序 後序 +后座 後座 +后座力 後座力 +后座系 後座繫 +后庭 後庭 +后庭花 後庭花 +后弦 後弦 +后影 後影 +后心 後心 +后怕 後怕 +后恭前倨 後恭前倨 +后悔 後悔 +后悔不及 後悔不及 +后悔不来 後悔不來 +后悔不迭 後悔不迭 +后悔何及 後悔何及 +后悔无及 後悔無及 +后悔药儿 後悔藥兒 +后悔莫及 後悔莫及 +后患 後患 +后患无穷 後患無窮 +后感 後感 +后感觉 後感覺 +后房 後房 +后手 後手 +后手不上 後手不上 +后手不接 後手不接 +后手儿 後手兒 +后手钱 後手錢 +后拥前呼 後擁前呼 +后拥前推 後擁前推 +后拥前遮 後擁前遮 +后拥前驱 後擁前驅 +后挡板 後擋板 +后排 後排 +后掠翼 後掠翼 +后掠角 後掠角 +后接 後接 +后掩蔽 後掩蔽 +后援 後援 +后援会 後援會 +后援军 後援軍 +后摆 後襬 +后撤 後撤 +后攻 後攻 +后放 後放 +后效 後效 +后文 後文 +后方 後方 +后方区 後方區 +后无来者 後無來者 +后日 後日 +后昆 後昆 +后晋 後晉 +后晌 後晌 +后晚 後晚 +后景 後景 +后有 後有 +后望镜 後望鏡 +后期 後期 +后期印象派 後期印象派 +后来 後來 +后来之秀 後來之秀 +后来人 後來人 +后来居上 後來居上 +后果 後果 +后果前因 後果前因 +后果堪忧 後果堪憂 +后果堪虞 後果堪虞 +后桅 後桅 +后桥 後橋 +后梁 後梁 +后梁太祖 後梁太祖 +后槽 後槽 +后步 後步 +后段 後段 +后段班 後段班 +后殿 後殿 +后母 後母 +后汉 後漢 +后汉书 後漢書 +后汉纪 後漢紀 +后派 後派 +后浪 後浪 +后浪推前浪 後浪推前浪 +后海湾 后海灣 +后海灣 后海灣 +后港 後港 +后滚翻 後滾翻 +后灯 後燈 +后点 後點 +后照镜 後照鏡 +后燕 後燕 +后爲 後爲 +后父 後父 +后爹 後爹 +后王 后王 +后现代 後現代 +后现代主义 後現代主義 +后现代剧场 後現代劇場 +后生 後生 +后生动物 後生動物 +后生可畏 後生可畏 +后生小子 後生小子 +后生小辈 後生小輩 +后生晚学 後生晚學 +后用 後用 +后由 後由 +后甲板 後甲板 +后皇 后皇 +后盖 後蓋 +后盾 後盾 +后知 後知 +后知后觉 後知後覺 +后福 後福 +后秃 後禿 +后秦 後秦 +后程 後程 +后稷 后稷 +后空翻 後空翻 +后窗 後窗 +后站 後站 +后端 後端 +后竹围 後竹圍 +后篇 後篇 +后级扩大机 後級擴大機 +后继 後繼 +后继乏人 後繼乏人 +后继乏力 後繼乏力 +后继无人 後繼無人 +后继无力 後繼無力 +后继有人 後繼有人 +后续 後續 +后续力 後續力 +后续的解释过程 後續的解釋過程 +后缀 後綴 +后缘 後緣 +后罩房 後罩房 +后置 後置 +后置词 後置詞 +后羿 后羿 +后羿射日 后羿射日 +后翅 後翅 +后翻筋斗 後翻筋斗 +后者 後者 +后肢 後肢 +后背 後背 +后脑 後腦 +后脑勺 後腦勺 +后脑杓 後腦杓 +后脚 後腳 +后脸儿 後臉兒 +后腰 後腰 +后腿 後腿 +后腿肉 後腿肉 +后膛 後膛 +后舱 後艙 +后舱门 後艙門 +后节 後節 +后花园 後花園 +后菜园 後菜園 +后藏 後藏 +后藤 後藤 +后虑 後慮 +后蜀 後蜀 +后行 後行 +后街 后街 +后裔 後裔 +后襟 後襟 +后西游记 後西遊記 +后视镜 後視鏡 +后觉 後覺 +后角 后角 +后言 後言 +后计 後計 +后记 後記 +后设 後設 +后词汇加工 後詞彙加工 +后话 後話 +后读 後讀 +后豐 后豐 +后账 後賬 +后赤壁赋 後赤壁賦 +后走 後走 +后赵 後趙 +后起 後起 +后起之秀 後起之秀 +后起字 後起字 +后足 後足 +后跟 後跟 +后路 後路 +后身 後身 +后车 後車 +后车之戒 後車之戒 +后车之鉴 後車之鑑 +后车站 後車站 +后车轴 後車軸 +后转 後轉 +后轮 後輪 +后辈 後輩 +后辈小子 後輩小子 +后辍 後輟 +后辛 后辛 +后辟 后辟 +后边 後邊 +后边儿 後邊兒 +后过 後過 +后进 後進 +后进先出 後進先出 +后述 後述 +后退 後退 +后退色 後退色 +后送 後送 +后送医院 後送醫院 +后遗 後遺 +后遗症 後遺症 +后部 後部 +后里 后里 +后里乡 后里鄉 +后重 後重 +后金 後金 +后钩儿 後鉤兒 +后镜 後鏡 +后门 後門 +后防 後防 +后院 後院 +后院子 後院子 +后院起火 後院起火 +后集 後集 +后面 後面 +后项 後項 +后顾 後顧 +后顾之忧 後顧之憂 +后顾之患 後顧之患 +后顾之虑 後顧之慮 +后顾之虞 後顧之虞 +后颈 後頸 +后首 後首 +后魏 後魏 +后鼻音 後鼻音 +后龙 後龍 +后龙溪 後龍溪 +后龙镇 後龍鎮 +吐出 吐出 +吐出来 吐出來 +吐司面包 吐司麪包 +吐哺捉发 吐哺捉髮 +吐哺握发 吐哺握髮 +吐穗 吐穗 +吐谷浑 吐谷渾 +吐露出 吐露出 +向上 向上 +向上地 向上地 +向下 向下 +向东 向東 +向东看 向東看 +向东走 向東走 +向人 向人 +向何处 向何處 +向使 向使 +向例 向例 +向光 向光 +向光性 向光性 +向內 向內 +向內走 向內走 +向前 向前 +向前看 向前看 +向前看齐 向前看齊 +向前翻腾 向前翻騰 +向前走 向前走 +向前进 向前進 +向化 向化 +向北 向北 +向北地 向北地 +向北看 向北看 +向北走 向北走 +向午 向午 +向南 向南 +向南看 向南看 +向南走 向南走 +向右 向右 +向右拐 向右拐 +向右看 向右看 +向右看齐 向右看齊 +向右转 向右轉 +向右转走 向右轉走 +向后 向後 +向后冲 向後衝 +向后看 向後看 +向后翻腾 向後翻騰 +向后走 向後走 +向后转 向後轉 +向后转走 向後轉走 +向善 向善 +向地 向地 +向地性 向地性 +向培良 向培良 +向壁虚构 向壁虛構 +向壁虚造 向壁虛造 +向声背实 向聲背實 +向外 向外 +向外冲 向外衝 +向外看 向外看 +向外走 向外走 +向天 向天 +向学 向學 +向宠 向寵 +向导 嚮導 +向导公司 嚮導公司 +向导员 嚮導員 +向导犬 嚮導犬 +向左 向左 +向左拐 向左拐 +向左看 向左看 +向左看齐 向左看齊 +向左转 向左轉 +向左转走 向左轉走 +向巴平措 向巴平措 +向平之愿 向平之願 +向年 向年 +向应 嚮應 +向度 向度 +向当 向當 +向往 嚮往 +向心 向心 +向心力 向心力 +向心店 向心店 +向心花序 向心花序 +向性 向性 +向慕 嚮慕 +向戌 向戌 +向斜 向斜 +向斜层 向斜層 +向日 向日 +向日性 向日性 +向日葵 向日葵 +向时 向時 +向明 嚮明 +向晓 向曉 +向晚 向晚 +向晦 嚮晦 +向暮 向暮 +向来 向來 +向来是 向來是 +向来都是 向來都是 +向流星雨 向流星雨 +向海 向海 +向溼性 向溼性 +向火 向火 +向火乞儿 向火乞兒 +向用 向用 +向电性 向電性 +向盘 向盤 +向着 向着 +向秀 向秀 +向秀丽 向秀麗 +向者 曏者 +向背 向背 +向荣 向榮 +向西 向西 +向西南 向西南 +向触 向觸 +向迩 嚮邇 +向里面冲 向裏面衝 +向量 向量 +向量代数 向量代數 +向量分析 向量分析 +向量图形 向量圖形 +向钱看 向錢看 +向阳 向陽 +向阳信 向陽信 +向阳区 向陽區 +向阳大道 向陽大道 +向隅 向隅 +向隅独泣 向隅獨泣 +向隅而泣 向隅而泣 +向非 向非 +向顺 向順 +向风 向風 +向风针 向風針 +吓不了 嚇不了 +吓了 嚇了 +吓了一跳 嚇了一跳 +吓出 嚇出 +吓出病来 嚇出病來 +吓得发抖 嚇得發抖 +吕后 呂后 +吕太后的筵席 呂太后的筵席 +吕宋烟 呂宋菸 +吕岩 呂岩 +吕布 呂布 +吕布戟 呂布戟 +吕梁 呂梁 +吕梁山 呂梁山 +吕梁市 呂梁市 +吕氏春秋 呂氏春秋 +吕芳烟 呂芳煙 +吕蒙 呂蒙 +吕蒙正 呂蒙正 +吕贝克 呂貝克 +吗啡针 嗎啡針 +君主专制 君主專制 +君主专制制 君主專制制 +君主制 君主制 +君主立宪制 君主立憲制 +君合国 君合國 +君子不念旧恶 君子不念舊惡 +君子于役 君子于役 +君子交绝不出恶声 君子交絕不出惡聲 +君子坦荡荡小人长戚戚 君子坦蕩蕩小人長慼慼 +君子报仇十年不晚 君子報仇十年不晚 +君子言先不言后 君子言先不言後 +吞了 吞了 +吞了下去 吞了下去 +吞云吐雾 吞雲吐霧 +吞刀刮肠 吞刀刮腸 +吞咽 吞嚥 +吞并 吞併 +吞烟 吞煙 +吞米桑布札 吞米桑布札 +吟叹 吟歎 +吟坛 吟壇 +吟游 吟遊 +吟游诗人 吟遊詩人 +吠舍 吠舍 +否极必泰 否極必泰 +否极泰来 否極泰來 +否极生泰 否極生泰 +吧台 吧檯 +吧托女 吧托女 +吨公里 噸公里 +含了 含了 +含商咀征 含商咀徵 +含宫咀征 含宮咀徵 +含油岩 含油岩 +含烟笼雾 含煙籠霧 +含苞欲放 含苞欲放 +含蜡 含蠟 +含齿戴发 含齒戴髮 +听不出 聽不出 +听不出来 聽不出來 +听不得一面之词 聽不得一面之詞 +听了 聽了 +听了风就是雨 聽了風就是雨 +听于 聽於 +听候发落 聽候發落 +听出 聽出 +听出来 聽出來 +听墙面 聽牆面 +听弦 聽絃 +听得出 聽得出 +听得出来 聽得出來 +听腻了 聽膩了 +听见了 聽見了 +听觉范围 聽覺範圍 +听证制度 聽證制度 +听骨链 聽骨鏈 +启动技术 啓動技術 +启发 啓發 +启发式 啓發式 +启发式敎学法 啓發式教學法 +启发性 啓發性 +启发性程式 啓發性程式 +启发法 啓發法 +启示录 啓示錄 +启蒙 啓蒙 +启蒙专制君主 啓蒙專制君主 +启蒙哲学 啓蒙哲學 +启蒙时代 啓蒙時代 +启蒙运动 啓蒙運動 +吴下阿蒙 吳下阿蒙 +吴俊杰 吳俊傑 +吴克群 吳克羣 +吴嘉种 吳嘉種 +吴复连 吳復連 +吴嶽修 吳嶽修 +吴幸桦 吳幸樺 +吴志 吳志 +吴志伟 吳志偉 +吴志扬 吳志揚 +吴志祺 吳志祺 +吴志远 吳志遠 +吴皓升 吳皓昇 +吴育升 吳育昇 +吴荣杰 吳榮杰 +吴蒙惠 吳蒙惠 +吴越同舟 吳越同舟 +吴越春秋 吳越春秋 +吴越曲 吳越曲 +吴采璋 吳采璋 +吴里克 吳里克 +吴隆杰 吳隆傑 +吸了 吸了 +吸出 吸出 +吸出去 吸出去 +吸出来 吸出來 +吸回 吸回 +吸回去 吸回去 +吸回来 吸回來 +吸地板 吸地板 +吸尽 吸盡 +吸干 吸乾 +吸引不了 吸引不了 +吸得干干 吸得乾乾 +吸杯 吸杯 +吸油烟机 吸油煙機 +吸烟 吸菸 +吸烟区 吸菸區 +吸烟客 吸菸客 +吸烟室 吸菸室 +吸烟族 吸菸族 +吸烟率 吸菸率 +吸烟者 吸菸者 +吸管虫 吸管蟲 +吸虫 吸蟲 +吸虫纲 吸蟲綱 +吸血虫 吸血蟲 +吹了 吹了 +吹云 吹雲 +吹出 吹出 +吹出去 吹出去 +吹出来 吹出來 +吹发 吹髮 +吹台 吹臺 +吹叶机 吹葉機 +吹向 吹向 +吹头发 吹頭髮 +吹干 吹乾 +吹气胜兰 吹氣勝蘭 +吹炼 吹煉 +吹绵介壳虫 吹綿介殼蟲 +吹胡 吹鬍 +吹胡子 吹鬍子 +吹胡子瞪眼睛 吹鬍子瞪眼睛 +吹风胡哨 吹風胡哨 +吻别 吻別 +吻别时 吻別時 +吻合 吻合 +吼出 吼出 +吾为之范我驰驱 吾爲之範我馳驅 +吾党 吾黨 +吾当 吾當 +呂后 呂后 +呆串了皮 呆串了皮 +呆了 呆了 +呆了一呆 呆了一呆 +呆呆兽 呆呆獸 +呆呆挣挣 呆呆掙掙 +呆头 呆頭 +呆小症 呆小症 +呆板 呆板 +呆气 呆氣 +呆滞 呆滯 +呆痴 呆癡 +呆脑 呆腦 +呆致致 呆緻緻 +呆话 呆話 +呆里呆气 呆裏呆氣 +呆里撒奸 呆裏撒奸 +呈准 呈准 +呈现出 呈現出 +呈现出来 呈現出來 +呈逆价差 呈逆價差 +告别 告別 +告别式 告別式 +告发 告發 +告示板 告示板 +呕出物 嘔出物 +呗赞 唄讚 +员山庄 員山莊 +呛了 嗆了 +呜咽 嗚咽 +周一 週一 +周一岳 周一嶽 +周三 週三 +周三径一 周三徑一 +周上 週上 +周世惠 周世惠 +周东昱 周東昱 +周中 週中 +周丽淇 周麗淇 +周书 周書 +周事 周事 +周二 週二 +周五 週五 +周亚夫 周亞夫 +周人 周人 +周人之急 賙人之急 +周仓 周倉 +周代 周代 +周休 週休 +周休二日 週休二日 +周会 週會 +周伟 周偉 +周传瑛 周傳瑛 +周传英 周傳英 +周传雄 周傳雄 +周作人 周作人 +周佳佑 周佳佑 +周佳琦 周佳琦 +周俊三 周俊三 +周俊勳 周俊勳 +周內 周內 +周全 周全 +周全方便 周全方便 +周公 周公 +周公吐哺 周公吐哺 +周公瑾 周公瑾 +周六 週六 +周六日 週六日 +周典论 周典論 +周刊 週刊 +周到 周到 +周助 周助 +周勃 周勃 +周匝 周匝 +周华健 周華健 +周南 周南 +周卫 周衛 +周历 周曆 +周厉王 周厲王 +周原 周原 +周口 周口 +周口地区 周口地區 +周口市 周口市 +周口店 周口店 +周口店文化 周口店文化 +周召 周召 +周召共和 周召共和 +周台竹 周臺竹 +周告 周告 +周周 週週 +周四 週四 +周回 週迴 +周围 周圍 +周围人 周圍人 +周围性眩晕 周圍性眩暈 +周围环境 周圍環境 +周培蕾 周培蕾 +周堂 周堂 +周士榆 周士榆 +周士渊 周士淵 +周处 周處 +周备 周備 +周大福 周大福 +周天 周天 +周奕成 周奕成 +周妙音 周妙音 +周姓 周姓 +周孟晔 周孟曄 +周宁 周寧 +周宁县 周寧縣 +周守训 周守訓 +周宏哲 周宏哲 +周宏室 周宏室 +周官 周官 +周定纬 周定緯 +周宣王 周宣王 +周室 周室 +周家 周家 +周密 周密 +周小 周小 +周小川 周小川 +周岁 週歲 +周平王 周平王 +周年 週年 +周年庆 週年慶 +周年纪念 週年紀念 +周年视差 周年視差 +周幼婷 周幼婷 +周幽王 周幽王 +周庄 周莊 +周庄王 周莊王 +周庄镇 周莊鎮 +周康王 周康王 +周延 周延 +周弘宪 周弘憲 +周忌 周忌 +周志全 周志全 +周志浩 周志浩 +周志诚 周志誠 +周思源 周思源 +周思齐 周思齊 +周急 周急 +周急继乏 周急繼乏 +周总理 周總理 +周恒毅 周恆毅 +周恤 周恤 +周恩来 周恩來 +周情孔思 周情孔思 +周慧敏 周慧敏 +周成王 周成王 +周承玮 周承瑋 +周折 周折 +周报 週報 +周敏鸿 周敏鴻 +周敦颐 周敦頤 +周数 週數 +周文 周文 +周文王 周文王 +周方 周方 +周旋 周旋 +周旋到底 周旋到底 +周日 週日 +周日版 週日版 +周昉 周昉 +周易 周易 +周星驰 周星馳 +周春秀 周春秀 +周晓涵 周曉涵 +周晬 周晬 +周朝 周朝 +周期 週期 +周期函数 周期函數 +周期彗星 周期彗星 +周期律 週期律 +周期性 週期性 +周期数 週期數 +周期系 週期系 +周期表 週期表 +周期解 週期解 +周末 週末 +周末愉快 週末愉快 +周末效应 週末效應 +周村 周村 +周村区 周村區 +周杰 周杰 +周杰伦 周杰倫 +周柏臣 周柏臣 +周某 周某 +周树人 周樹人 +周梁淑怡 周梁淑怡 +周梦瑶 周夢瑤 +周正 周正 +周武王 周武王 +周武王姬发 周武王姬發 +周武雄 周武雄 +周氏 周氏 +周永康 周永康 +周永明 周永明 +周治平 周治平 +周泓谕 周泓諭 +周波 周波 +周流 周流 +周浃 周浹 +周济 賙濟 +周海媚 周海媚 +周润发 周潤發 +周渝民 周渝民 +周游 周遊 +周游世界 周遊世界 +周游列国 周遊列國 +周游券 周遊券 +周灿德 周燦德 +周率 周率 +周王朝 周王朝 +周瑜 周瑜 +周瑜打黄盖 周瑜打黃蓋 +周璇 周璇 +周盈成 周盈成 +周盈文 周盈文 +周盛渊 周盛淵 +周相 周相 +周知 周知 +周礼 周禮 +周礼良 周禮良 +周秀霞 周秀霞 +周程张朱 周程張朱 +周穆王 周穆王 +周立昌 周立昌 +周立波 周立波 +周章 周章 +周章狼狈 周章狼狽 +周筱涵 周筱涵 +周经凯 周經凱 +周给 周給 +周缘 周緣 +周置 周置 +周美里 周美里 +周美青 周美青 +周考 週考 +周而不比 周而不比 +周而复始 周而復始 +周至 周至 +周至县 周至縣 +周董 周董 +周蓓姬 周蓓姬 +周蕙 周蕙 +周薪 週薪 +周行 周行 +周览 周覽 +周角 周角 +周记 週記 +周详 周詳 +周诰 周誥 +周赐海 周賜海 +周赧王 周赧王 +周走秀 週走秀 +周身 周身 +周转 週轉 +周边 周邊 +周边设备 周邊設備 +周迅 周迅 +周近 週近 +周遍 周遍 +周道 周道 +周遭 周遭 +周遭事物 週遭事物 +周遮 周遮 +周邦彦 周邦彥 +周郎 周郎 +周郎癖 周郎癖 +周郎顾曲 周郎顧曲 +周采诗 周采詩 +周锡玮 周錫瑋 +周锦贵 周錦貴 +周长 周長 +周雅淑 周雅淑 +周韫维 周韞維 +周顗 周顗 +周颂 周頌 +周颙 周顒 +周髀 周髀 +周髀算经 周髀算經 +周龙 周龍 +呱嗒板儿 呱嗒板兒 +味同嚼蜡 味同嚼蠟 +味胜易牙 味勝易牙 +呼之欲出 呼之欲出 +呼出 呼出 +呼出来 呼出來 +呼吁 呼籲 +呼吸困难 呼吸困難 +呼吸系统 呼吸系統 +呼图克图 呼圖克圖 +命世之才 命世之才 +命世才 命世才 +命中注定 命中註定 +命名系统 命名系統 +命运注定 命運註定 +命题范围 命題範圍 +咀咽 咀嚥 +咀嚼出 咀嚼出 +和丰 和豐 +和了 和了 +和光同尘 和光同塵 +和克制 和剋制 +和合 和合 +和合二仙 和合二仙 +和合僧 和合僧 +和合日 和合日 +和合汤 和合湯 +和同 和同 +和哄 和哄 +和奸 和姦 +和布克赛尔县 和布克賽爾縣 +和布克赛尔蒙古自治县 和布克賽爾蒙古自治縣 +和平工作团 和平工作團 +和平里 和平里 +和弦 和絃 +和杯 和杯 +和核 和核 +和气致祥 和氣致祥 +和盘托出 和盤托出 +和而不同 和而不同 +和胜 和勝 +和谐一致 和諧一致 +和面 和麪 +和风拂面 和風拂面 +咍台 咍臺 +咎征 咎徵 +咏叹 詠歎 +咏赞 詠贊 +咏雪之才 詠雪之才 +咒印术 咒印術 +咒愿 咒愿 +咒术 咒術 +咕咕钟 咕咕鐘 +咖啡杯 咖啡杯 +咖啡色系 咖啡色系 +咙胡 嚨胡 +咣当 咣噹 +咨询 諮詢 +咫尺万里 咫尺萬里 +咫尺千里 咫尺千里 +咬了 咬了 +咬了一口 咬了一口 +咬出 咬出 +咬合 咬合 +咬合不正 咬合不正 +咬合调整 咬合調整 +咬姜呷醋 咬薑呷醋 +咬字不准 咬字不準 +咬屁虫 咬屁蟲 +咬弦 咬弦 +咬折丁子的老婆 咬折丁子的老婆 +咬秋 咬秋 +咬舌自尽 咬舌自盡 +咭叮当 咭叮噹 +咭当当 咭噹噹 +咯当 咯噹 +咳嗽药 咳嗽藥 +咸丝丝 鹹絲絲 +咸丰 咸豐 +咸丰县 咸豐縣 +咸丰草 咸豐草 +咸五登三 咸五登三 +咸亨 咸亨 +咸亨酒店 咸亨酒店 +咸信 咸信 +咸兴 咸興 +咸兴市 咸興市 +咸卤 鹹鹵 +咸味 鹹味 +咸和 咸和 +咸咸 鹹鹹 +咸嘴淡舌 鹹嘴淡舌 +咸土 鹹土 +咸宁 咸寧 +咸宁地区 咸寧地區 +咸宁市 咸寧市 +咸安区 咸安區 +咸宜 咸宜 +咸度 鹹度 +咸得 鹹得 +咸批 鹹批 +咸水 鹹水 +咸水妹 鹹水妹 +咸水湖 鹹水湖 +咸水鱼 鹹水魚 +咸池 咸池 +咸汤 鹹湯 +咸津津 鹹津津 +咸津津儿 鹹津津兒 +咸派 鹹派 +咸海 鹹海 +咸淡 鹹淡 +咸淡适中 鹹淡適中 +咸湖 鹹湖 +咸湿 鹹溼 +咸潟 鹹潟 +咸猪手 鹹豬手 +咸猪肉 鹹豬肉 +咸的 鹹的 +咸盐 鹹鹽 +咸类 鹹類 +咸粥 鹹粥 +咸肉 鹹肉 +咸菜 鹹菜 +咸菜干 鹹菜乾 +咸蛋 鹹蛋 +咸认为 咸認爲 +咸酥鸡 鹹酥雞 +咸镜 咸鏡 +咸镜北道 咸鏡北道 +咸镜南道 咸鏡南道 +咸镜道 咸鏡道 +咸阳 咸陽 +咸阳地区 咸陽地區 +咸阳宫 咸陽宮 +咸阳市 咸陽市 +咸阳桥 咸陽橋 +咸阳火 咸陽火 +咸食 鹹食 +咸鱼 鹹魚 +咸鱼翻身 鹹魚翻身 +咸鸭蛋 鹹鴨蛋 +咽下 嚥下 +咽不了 嚥不了 +咽了 嚥了 +咽住 嚥住 +咽到 嚥到 +咽哽 咽哽 +咽唾 嚥唾 +咽喉 咽喉 +咽峡 咽峽 +咽干 咽乾 +咽气 嚥氣 +咽炎 咽炎 +咽病 咽病 +咽痛 咽痛 +咽着 嚥着 +咽肌 嚥肌 +咽苦吞甘 嚥苦吞甘 +咽进 嚥進 +咽部 咽部 +咽镜 咽鏡 +哀兵必胜 哀兵必勝 +哀凄 哀悽 +哀叹 哀嘆 +哀吊 哀弔 +哀哀欲绝 哀哀欲絕 +哀戚 哀慼 +哀挽 哀輓 +哀莫大于心死 哀莫大於心死 +品尝 品嚐 +品尝会 品嚐會 +品尝到 品嚐到 +品汇 品彙 +品种 品種 +品种改良 品種改良 +品系 品系 +品花宝鉴 品花寶鑑 +品莲台 品蓮臺 +品貌出众 品貌出衆 +品质管制 品質管制 +品鉴 品鑑 +哄乱 哄亂 +哄了 哄了 +哄人 哄人 +哄伙 鬨夥 +哄传 哄傳 +哄传一时 哄傳一時 +哄劝 哄勸 +哄动 鬨動 +哄动一时 哄動一時 +哄吓骗诈 哄嚇騙詐 +哄哄 哄哄 +哄哄翕翕 哄哄翕翕 +哄堂 鬨堂 +哄堂大笑 鬨堂大笑 +哄弄 哄弄 +哄得 哄得 +哄抢 哄搶 +哄抬 哄擡 +哄抬物价 哄擡物價 +哄来哄去 哄來哄去 +哄然 鬨然 +哄然大笑 鬨然大笑 +哄笑 鬨笑 +哄诱 哄誘 +哄赚 哄賺 +哄走 哄走 +哄闹 鬨鬧 +哄骗 哄騙 +哈丰角 哈豐角 +哈克 哈克 +哈克莉洛 哈克莉洛 +哈克贝利芬历险记 哈克貝利芬歷險記 +哈利发 哈利發 +哈利法克斯 哈利法克斯 +哈卡里 哈卡里 +哈发林 哈發林 +哈塞布苏 哈塞布蘇 +哈巴罗夫斯克 哈巴羅夫斯克 +哈巴谷书 哈巴谷書 +哈布斯堡 哈布斯堡 +哈比亚里马纳 哈比亞里馬納 +哈特谢普苏 哈特謝普蘇 +哈玛斯派系 哈瑪斯派系 +哈玛斯集团 哈瑪斯集團 +哈萨克 哈薩克 +哈萨克人 哈薩克人 +哈萨克共和国 哈薩克共和國 +哈萨克文 哈薩克文 +哈萨克斯坦 哈薩克斯坦 +哈萨克族 哈薩克族 +哈萨克语 哈薩克語 +哈里 哈里 +哈里伯顿 哈里伯頓 +哈里发 哈里發 +哈里发塔 哈里發塔 +哈里发帝国 哈里發帝國 +哈里尔 哈里爾 +哈里斯 哈里斯 +哈里斯堡 哈里斯堡 +哈里札德 哈里札德 +哈里森史密特 哈裏森史密特 +哈里路亚 哈里路亞 +哈里逊 哈里遜 +哈里逊福特 哈里遜福特 +哈里里 哈里里 +响了 響了 +响卜 響卜 +响叮当 響叮噹 +响弦 響絃 +响当当 響噹噹 +响彻云汉 響徹雲漢 +响彻云际 響徹雲際 +响彻云霄 響徹雲霄 +响板 響板 +响遏行云 響遏行雲 +响钟 響鐘 +响马党羽 響馬黨羽 +哑子托梦 啞子托夢 +哑板 啞板 +哗众 譁衆 +哗变 譁變 +哗哗 嘩嘩 +哗啦 嘩啦 +哗地 嘩地 +哗然 譁然 +哗的 嘩的 +哗笑 譁笑 +哥伦布 哥倫布 +哥伦布市 哥倫布市 +哥伦布纪 哥倫布紀 +哥林多后书 哥林多後書 +哥萨克 哥薩克 +哥里 哥里 +哪一个 哪一個 +哪一出 哪一齣 +哪个 哪個 +哪个人 哪個人 +哪个月 哪個月 +哪几 哪幾 +哪几个 哪幾個 +哪几天 哪幾天 +哪几次 哪幾次 +哪只 哪隻 +哪台 哪臺 +哪回 哪回 +哪方面 哪方面 +哪种 哪種 +哪种人 哪種人 +哪里 哪裏 +哪里买 哪裏買 +哪里人 哪裏人 +哪里哪里 哪里哪里 +哪里摔倒哪里爬 哪裏摔倒哪裏爬 +哭个 哭個 +哭个夠 哭個夠 +哭个痛快 哭個痛快 +哭了 哭了 +哭出 哭出 +哭出来 哭出來 +哭尽 哭盡 +哭成一团 哭成一團 +哭秋风 哭秋風 +哭脏 哭髒 +哲学系 哲學系 +哲学范畴 哲學範疇 +哲布尊丹巴 哲布尊丹巴 +哺喂 哺餵 +哼个 哼個 +哼出 哼出 +哽咽 哽咽 +唁吊 唁弔 +唇干 脣乾 +唇彩 脣彩 +唇燥舌干 脣燥舌乾 +唉叹 唉嘆 +唐三彩 唐三彩 +唐志中 唐志中 +唐才常 唐才常 +唐氏症 唐氏症 +唐氏综合症 唐氏綜合症 +唐王游地府 唐王遊地府 +唐美云 唐美雲 +唐胖子吊在醋缸里 唐胖子吊在醋缸裏 +唤出 喚出 +唤回 喚回 +售价 售價 +售价为 售價爲 +售出 售出 +售台 售臺 +售后 售後 +售后服务 售後服務 +售后部 售後部 +售货台 售貨臺 +售货合约 售貨合約 +唯意志论 唯意志論 +唱个 唱個 +唱了 唱了 +唱出 唱出 +唱出来 唱出來 +唱叹 唱嘆 +唱回 唱回 +唱回去 唱回去 +唱回来 唱回來 +唱对台戏 唱對臺戲 +唱念 唱唸 +唱曲 唱曲 +唱游 唱遊 +唱片目录 唱片目錄 +唱针 唱針 +唸了 唸了 +唾余 唾餘 +唾沫直咽 唾沫直嚥 +唾面 唾面 +唾面自干 唾面自乾 +啃书虫 啃書蟲 +啃出 啃出 +啄针儿 啄針兒 +商业发票 商業發票 +商业计划 商業計劃 +商借 商借 +商务代表 商務代表 +商历 商曆 +商周 商周 +商品价值 商品價值 +商品价格 商品價格 +商品目录 商品目錄 +商品输出 商品輸出 +商团 商團 +商学系 商學系 +商秋 商秋 +商科集团 商科集團 +啜哄 啜哄 +啤酒厂 啤酒廠 +啧啧称赞 嘖嘖稱讚 +啧啧赞叹 嘖嘖讚歎 +啮合 齧合 +啷当 啷噹 +啼饥号寒 啼飢號寒 +喀喇崑仑山 喀喇崑崙山 +喀喇昆仑公路 喀喇崑崙公路 +喀喇昆仑山 喀喇崑崙山 +喀喇昆仑山脉 喀喇崑崙山脈 +喀喇沁左翼蒙古族自治县 喀喇沁左翼蒙古族自治縣 +喀布尔 喀布爾 +喀布尔河 喀布爾河 +喀拉喀托火山 喀拉喀托火山 +喀拉昆仑山 喀拉崑崙山 +喂乳 餵乳 +喂了 餵了 +喂了一声 喂了一聲 +喂你 餵你 +喂偏食 喂偏食 +喂养 餵養 +喂动物 餵動物 +喂哺 餵哺 +喂喂 喂喂 +喂奶 餵奶 +喂奶时 餵奶時 +喂它 餵它 +喂我 餵我 +喂母乳 餵母乳 +喂猪 餵豬 +喂眼 喂眼 +喂给 餵給 +喂羊 餵羊 +喂过 餵過 +喂食 餵食 +喂饭 餵飯 +喂饱 餵飽 +喂马 餵馬 +喂驴 餵驢 +喂鱼 餵魚 +喂鸡 餵雞 +喂鸭 餵鴨 +喂鹅 餵鵝 +善了 善了 +善于 善於 +善于词令 善於詞令 +善于辞令 善於辭令 +善价 善價 +善后 善後 +善后事宜 善後事宜 +善后借款 善後借款 +善善恶恶 善善惡惡 +善尽 善盡 +善念 善念 +善恶 善惡 +善恶不分 善惡不分 +善意回应 善意回應 +善才 善才 +善才童子 善才童子 +善有善报恶有恶报 善有善報惡有惡報 +善有善报恶有恶报若然不报时晨未到 善有善報惡有惡報若然不報時晨未到 +善罢干休 善罷干休 +善财难舍 善財難捨 +喇叭虫 喇叭蟲 +喉咽 喉咽 +喉头发干 喉頭發乾 +喉干舌燥 喉乾舌燥 +喊了 喊了 +喊价 喊價 +喊出 喊出 +喊出去 喊出去 +喊出来 喊出來 +喊回 喊回 +喊回去 喊回去 +喊回来 喊回來 +喑恶叱咤 喑惡叱吒 +喘出 喘出 +喘吁吁 喘吁吁 +喜不自胜 喜不自勝 +喜冲冲 喜衝衝 +喜出望外 喜出望外 +喜娘 喜娘 +喜形于色 喜形於色 +喜忧参半 喜憂參半 +喜怒不形于色 喜怒不形於色 +喜恶 喜惡 +喜极而泣 喜極而泣 +喜欢表 喜歡錶 +喜欢钟 喜歡鐘 +喜欢钟表 喜歡鐘錶 +喜获 喜獲 +喜虫儿 喜蟲兒 +喝个 喝個 +喝了 喝了 +喝交杯 喝交杯 +喝倒彩 喝倒彩 +喝倒采 喝倒采 +喝出 喝出 +喝参 喝參 +喝尽 喝盡 +喝干 喝乾 +喝彩 喝彩 +喝彩声 喝彩聲 +喝杯 喝杯 +喝采 喝采 +喝风呵烟 喝風呵煙 +喝风疴烟 喝風痾煙 +喟叹 喟嘆 +喧哄 喧鬨 +喧哗 喧譁 +喧噪 喧噪 +喫亏的是乖占便宜的是呆 喫虧的是乖占便宜的是呆 +喷云吐雾 噴雲吐霧 +喷云嗳雾 噴雲噯霧 +喷出 噴出 +喷出去 噴出去 +喷出口 噴出口 +喷出来 噴出來 +喷发 噴發 +喷墨印表机 噴墨印表機 +喷射发动机 噴射發動機 +喷射战斗机 噴射戰鬥機 +喷气发动 噴氣發動 +喷气发动机 噴氣發動機 +喷洒 噴灑 +喷薄欲出 噴薄欲出 +嗅出 嗅出 +嗑药 嗑藥 +嗔拳不打笑面 嗔拳不打笑面 +嗛志 嗛志 +嗜欲 嗜慾 +嗜眠症 嗜眠症 +嗜睡症 嗜睡症 +嗜血杆菌 嗜血桿菌 +嗜酸乳干菌 嗜酸乳干菌 +嗝症 嗝症 +嗟叹 嗟嘆 +嗟吁 嗟吁 +嗣后 嗣後 +嗣适 嗣適 +嘀嗒的表 嘀嗒的錶 +嘀里嘟噜 嘀裏嘟嚕 +嘉义师范 嘉義師範 +嘉义师范学院 嘉義師範學院 +嘉南药专 嘉南藥專 +嘉南药理科技大学 嘉南藥理科技大學 +嘉布瑞尔 嘉布瑞爾 +嘉柏隆里 嘉柏隆里 +嘉肴 嘉餚 +嘉言录 嘉言錄 +嘉谷 嘉穀 +嘉里 嘉裏 +嘘下台 噓下臺 +嘘下台去 噓下臺去 +嘘下台来 噓下臺來 +嘱托 囑託 +嘴松 嘴鬆 +嘴答谷 嘴答谷 +嘴里 嘴裏 +噎饥 噎饑 +噙齿戴发 噙齒戴髮 +噜噜苏苏 嚕嚕囌囌 +噜苏 嚕囌 +器物录 器物錄 +噪动 譟動 +噪声 噪聲 +噪杂 噪雜 +噪点 噪點 +噪诈 譟詐 +噪音 噪音 +噪音控制 噪音控制 +噪音管制 噪音管制 +噪音管制法 噪音管制法 +噪鹛 噪鶥 +噫吁戏 噫吁戲 +噶大克 噶大克 +噶布伦 噶布倫 +噶当派 噶當派 +噶拉多杰仁波切 噶拉多傑仁波切 +嚚暗 嚚闇 +嚷出 嚷出 +嚷出去 嚷出去 +嚷出来 嚷出來 +嚷刮 嚷刮 +嚼不了 嚼不了 +嚼舌自尽 嚼舌自盡 +嚼蜡 嚼蠟 +嚼谷 嚼穀 +囉囉苏苏 囉囉囌囌 +囉苏 囉囌 +囊橐丰盈 囊橐豐盈 +囊温郎当 囊溫郎當 +囓合 囓合 +囚系 囚繫 +囚首丧面 囚首喪面 +囚首垢面 囚首垢面 +四万 四萬 +四万一千 四萬一千 +四万七千 四萬七千 +四万三千 四萬三千 +四万两千 四萬兩千 +四万五千 四萬五千 +四万八千 四萬八千 +四万六千 四萬六千 +四万四千 四萬四千 +四下里 四下裏 +四世同堂 四世同堂 +四两拨千斤 四兩撥千斤 +四个 四個 +四个坚持 四個堅持 +四个现代化 四個現代化 +四代同堂 四代同堂 +四体不勤五谷不分 四體不勤五穀不分 +四余 四餘 +四停八当 四停八當 +四克 四克 +四党 四黨 +四六面体 四六面體 +四凶 四凶 +四出征收 四出徵收 +四出戏 四齣戲 +四出祁山 四出祁山 +四分历 四分曆 +四分历史 四分歷史 +四分钟 四分鐘 +四十一万 四十一萬 +四十五万 四十五萬 +四十四万 四十四萬 +四十多万 四十多萬 +四千 四千 +四千两百 四千兩百 +四千多万 四千多萬 +四只 四隻 +四台 四臺 +四号台 四號臺 +四合 四合 +四合一 四合一 +四合房 四合房 +四合院 四合院 +四向 四向 +四周 四周 四週 +四周围 四周圍 +四周年 四週年 +四大发明 四大發明 +四大须生 四大鬚生 +四天后 四天後 +四川师范大学 四川師範大學 +四川师范学院 四川師範學院 +四年制 四年制 +四年制的大学 四年制的大學 +四库禁毁书丛刋 四庫禁燬書叢刋 +四弘誓愿 四弘誓願 +四才子 四才子 +四扎 四紮 +四方八面 四方八面 +四方台 四方臺 +四方台区 四方臺區 +四星彩 四星彩 +四杯 四杯 +四杰 四傑 +四板 四板 +四极 四極 +四极管 四極管 +四柜 四櫃 +四核 四核 +四洲志 四洲志 +四海升平 四海昇平 +四海皆准 四海皆准 +四点钟 四點鐘 +四百万 四百萬 +四百个 四百個 +四百多万 四百多萬 +四秒钟 四秒鐘 +四级三审制 四級三審制 +四胡 四胡 +四舍五入 四捨五入 +四舍六入 四捨六入 +四表 四表 +四邻八舍 四鄰八舍 +四部合唱 四部合唱 +四部曲 四部曲 +四里 四里 +四面 四面 +四面体 四面體 +四面佛 四面佛 +四面八方 四面八方 +四面受困 四面受困 +四面受敌 四面受敵 +四面楚歌 四面楚歌 +四面玲珑 四面玲瓏 +四面见光 四面見光 +四面钟 四面鐘 +回九 回九 +回乡 回鄉 +回书 回書 +回买 回買 +回了 回了 +回事 回事 +回交 回交 +回京 回京 +回人 回人 +回亿 回億 +回传 回傳 +回佣 回佣 +回信 回信 +回信地址 回信地址 +回候 回候 +回光返照 迴光返照 +回光镜 回光鏡 +回冲 回沖 +回击 回擊 +回函 回函 +回到 回到 +回力棒 回力棒 +回力球 回力球 +回动 回動 +回匝 迴匝 +回升 回升 +回单 回單 +回单儿 回單兒 +回卷 回捲 +回厂 回廠 +回历 回曆 +回去 回去 +回口 回口 +回台 回臺 +回合 回合 +回吐 回吐 +回向 迴向 +回告 回告 +回味 回味 +回味无穷 回味無窮 +回和 回和 +回咬 回咬 +回响 迴響 +回嗔作喜 回嗔作喜 +回嘴 回嘴 +回回 回回 +回回历 回回曆 +回回青 回回青 +回回鼻子 回回鼻子 +回国 回國 +回圈 迴圈 +回塘 回塘 +回填 回填 +回墨印 回墨印 +回声 回聲 +回声定位 回聲定位 +回声探测 迴聲探測 +回复 回覆 回復 +回复青春 回復青春 +回天 迴天 +回天之力 回天之力 +回天乏术 回天乏術 +回头 回頭 +回头一看 回頭一看 +回头书 回頭書 +回头人 回頭人 +回头儿 回頭兒 +回头客 回頭客 +回头掣脑 回頭掣腦 +回头是岸 回頭是岸 +回头草 回頭草 +回头见 回頭見 +回头路 回頭路 +回头车子 回頭車子 +回奉 回奉 +回娘家 回孃家 +回子 回子 +回字 回字 +回定 回定 +回家 回家 +回家乐 回家樂 +回家路 回家路 +回家路上 回家路上 +回家途中 回家途中 +回容 回容 +回山倒海 回山倒海 +回师 回師 +回帖 回帖 +回带 迴帶 +回席 回席 +回应 迴應 +回府 回府 +回廊 迴廊 +回弹 回彈 +回归 迴歸 +回归年 迴歸年 +回归潮 迴歸潮 +回归热 迴歸熱 +回归线 迴歸線 +回形夹 迴形夾 +回得 回得 +回得去 回得去 +回得来 回得來 +回心 迴心 +回心转意 回心轉意 +回忆 回憶 +回忆录 回憶錄 +回忆说 回憶說 +回忆起 回憶起 +回忌 回忌 +回念 回念 +回思 回思 +回想 回想 +回想到 回想到 +回想起 回想起 +回想起来 回想起來 +回惶 回惶 +回戏 回戲 +回手 回手 +回扣 回扣 +回执 回執 +回折格子 回折格子 +回护 迴護 +回报 回報 +回报率 回報率 +回拜 回拜 +回拨 回撥 +回掉 回掉 +回援 回援 +回收 回收 +回收价值 回收價值 +回收商 回收商 +回收场 回收場 +回收物 回收物 +回收率 回收率 +回收站 回收站 +回收量 回收量 +回放 回放 +回敎会议组织 回教會議組織 +回教 回教 +回教世界 回教世界 +回教人 回教人 +回教信徒 回教信徒 +回教党 回教黨 +回教国 回教國 +回教圣战 回教聖戰 +回教帝国 回教帝國 +回教开斋节 回教開齋節 +回教徒 回教徒 +回教忠孝节 回教忠孝節 +回敬 回敬 +回数票 回數票 +回文 迴文 +回文织锦 迴文織錦 +回斡 迴斡 +回旋 迴旋 +回族 回族 +回旨 回旨 +回易 回易 +回映 回映 +回春 回春 +回暖 回暖 +回望 回望 +回朝 回朝 +回本 回本 +回条 回條 +回来 回來 +回档 回檔 +回梦 迴夢 +回棋 回棋 +回椎 回椎 +回民 回民 +回民区 回民區 +回水 回水 +回波 回波 +回流 迴流 +回清倒影 迴清倒影 +回温 回溫 +回港 回港 +回游 迴游 +回溯 回溯 +回溯到 回溯到 +回滩 回灘 +回潮 回潮 +回潮率 回潮率 +回火 回火 +回灯 回燈 +回炉 回爐 +回炉复帐 回爐復帳 +回点 回點 +回煞 回煞 +回片 回片 +回状 回狀 +回环 迴環 +回环转折 迴環轉折 +回班 回班 +回球 回球 +回甘 回甘 +回生 回生 +回生乏术 回生乏術 +回生起死 回生起死 +回用 回用 +回电 回電 +回疆 回疆 +回盘 回盤 +回目 回目 +回盲瓣 迴盲瓣 +回看 回看 +回眸 回眸 +回眸一笑 回眸一笑 +回着 回着 +回示 回示 +回礼 回禮 +回神 回神 +回票面 回票面 +回禀 回稟 +回禄 回祿 +回禄之灾 回祿之災 +回程 回程 +回稳 回穩 +回穴 迴穴 +回空 回空 +回笼 回籠 +回答 回答 +回答出来 回答出來 +回答到 回答到 +回答说 回答說 +回籍 回籍 +回纥 回紇 +回纥兵 回紇兵 +回纥军 回紇軍 +回纳 回納 +回纹针 迴紋針 +回绕 迴繞 +回绝 回絕 +回翔 迴翔 +回老家 回老家 +回肠 迴腸 +回肠九回 回腸九回 +回肠九转 迴腸九轉 +回肠伤气 迴腸傷氣 +回肠寸断 迴腸寸斷 +回肠荡气 迴腸蕩氣 +回背 回背 +回腕 迴腕 +回航 回航 +回航途中 回航途中 +回船转舵 回船轉舵 +回良玉 回良玉 +回荡 迴盪 +回落 回落 +回补 回補 +回西流 回西流 +回见 回見 +回访 回訪 +回诊 回診 +回话 回話 +回诵 迴誦 +回请 回請 +回购 回購 +回购额 回購額 +回赎 回贖 +回赠 回贈 +回起 回起 +回跌 回跌 +回路 迴路 +回身 回身 +回车 回車 +回车键 回車鍵 +回转 迴轉 +回转仪 迴轉儀 +回转半径 回轉半徑 +回转寿司 回轉壽司 +回软 回軟 +回过 回過 +回过来 回過來 +回过神 回過神 +回返 回返 +回还 回還 +回送 回送 +回递性 迴遞性 +回途 回途 +回遑 回遑 +回道 回道 +回避 迴避 +回避学习 迴避學習 +回邪 回邪 +回邮 回郵 +回邮信封 回郵信封 +回部 回部 +回采 回採 +回銮 迴鑾 +回销 回銷 +回锅 回鍋 +回锅油 回鍋油 +回锅肉 回鍋肉 +回锋 回鋒 +回门 回門 +回阑 回闌 +回防 回防 +回阳荡气 迴陽蕩氣 +回雪 迴雪 +回青 回青 +回音 迴音 +回顾 回顧 +回风 迴風 +回飙 迴飆 +回飞棒 回飛棒 +回馈 回饋 +回首 回首 +回马 回馬 +回马枪 回馬槍 +回驳 回駁 +回魂仙梦 迴魂仙夢 +回鱼箸 回魚箸 +回鹘 回鶻 +回鹘文 回鶻文 +回黄倒皂 回黃倒皂 +回黄转绿 回黃轉綠 +因事制宜 因事制宜 +因于 因於 +因地制宜 因地制宜 +因奸成孕 因姦成孕 +因斯布鲁克 因斯布魯克 +因时制宜 因時制宜 +因果关系 因果關係 +因缘巧合 因緣巧合 +团丁 團丁 +团主 團主 +团书 團書 +团伙 團伙 +团体 團體 +团体冠军 團體冠軍 +团体协约 團體協約 +团体奖 團體獎 +团体性 團體性 +团体意识 團體意識 +团体操 團體操 +团体治疗 團體治療 +团体活动 團體活動 +团体票 團體票 +团体组 團體組 +团体行 團體行 +团体行动 團體行動 +团体规范 團體規範 +团体赛 團體賽 +团体辅导 團體輔導 +团保 團保 +团剥 團剝 +团勇 團勇 +团匪 團匪 +团名 團名 +团员 團員 +团团 團團 +团团围住 團團圍住 +团团转 團團轉 +团围 團圍 +团圆 團圓 +团圆夜 團圓夜 +团圆日 團圓日 +团圆节 團圓節 +团圆饭 團圓飯 +团坐 團坐 +团头 團頭 +团头团脸 團頭團臉 +团头聚面 團頭聚面 +团契 團契 +团委 團委 +团子 糰子 +团年 團年 +团康 團康 +团弄 團弄 +团扇 團扇 +团扇妾 團扇妾 +团拜 團拜 +团掿 團掿 +团旗 團旗 +团服 團服 +团栾 團欒 +团案 團案 +团沙群岛 團沙羣島 +团牌 團牌 +团状 團狀 +团瓢 團瓢 +团矿 團礦 +团社 團社 +团空 團空 +团管区 團管區 +团粉 糰粉 +团粒 團粒 +团练 團練 +团结 團結 +团结一心 團結一心 +团结一致 團結一致 +团结就是力量 團結就是力量 +团结工会 團結工會 +团聚 團聚 +团脐 團臍 +团脸 團臉 +团花 團花 +团茶 團茶 +团荷 團荷 +团藻 團藻 +团行 團行 +团购 團購 +团费 團費 +团部 團部 +团长 團長 +团队 團隊 +团队奖 團隊獎 +团队报 團隊報 +团队精神 團隊精神 +团队赛 團隊賽 +团音 團音 +团风 團風 +团风县 團風縣 +团香扇 團香扇 +团鱼 團魚 +团龙 團龍 +团龙儿 團龍兒 +团𪢮 團圞 +园游会 園遊會 +园游券 園遊券 +园里 園裏 +困乏 睏乏 +困了 困了 +困于 困於 +困人 困人 +困住 困住 +困倦 睏倦 +困倦起来 困倦起來 +困兽 困獸 +困兽之斗 困獸之鬥 +困兽犹斗 困獸猶鬥 +困厄 困厄 +困围 困圍 +困在 困在 +困境 困境 +困处 困處 +困处泥涂 困處泥塗 +困学 困學 +困守 困守 +困局 困局 +困心衡虑 困心衡慮 +困惑 困惑 +困惑不解 困惑不解 +困惫 困憊 +困意 睏意 +困扰 困擾 +困斗 困鬥 +困歇 困歇 +困滞 困滯 +困畏 困畏 +困知勉行 困知勉行 +困穷 困窮 +困窘 困窘 +困竭 困竭 +困腾腾 困騰騰 +困苦 困苦 +困觉 睏覺 +困话 困話 +困踬 困躓 +困蹙 困蹙 +困阨 困阨 +困阻 困阻 +困隘 困隘 +困难 困難 +困难在于 困難在於 +困难度 困難度 +困难点 困難點 +困难重重 困難重重 +困顿 困頓 +围了 圍了 +围困 圍困 +围困在 圍困在 +围场满族蒙古族自治县 圍場滿族蒙古族自治縣 +围岩 圍巖 +固于 固於 +固定制 固定制 +固定术 固定術 +固定汇率 固定匯率 +固征 固徵 +国一制 國一制 +国之桢干 國之楨榦 +国乐团 國樂團 +国于 國於 +国仇 國仇 +国会制 國會制 +国共两党 國共兩黨 +国共合作 國共合作 +国别 國別 +国别史 國別史 +国剧团 國劇團 +国务院台湾事务办公室 國務院臺灣事務辦公室 +国务院法制局 國務院法制局 +国历 國曆 +国历年 國曆年 +国发 國發 +国发院 國發院 +国台办 國臺辦 +国台语 國臺語 +国合会 國合會 +国困民艰 國困民艱 +国土规划 國土規劃 +国土资讯系统 國土資訊系統 +国外汇兑 國外匯兌 +国大主席团 國大主席團 +国大代表 國大代表 +国大代表部 國大代表部 +国大党 國大黨 +国家党 國家黨 +国家制度 國家制度 +国家发展和改革委员会 國家發展和改革委員會 +国家发展改革委 國家發展改革委 +国家发展计划委员会 國家發展計劃委員會 +国家外汇管理局 國家外匯管理局 +国家开发银行 國家開發銀行 +国家文物鉴定委员会 國家文物鑑定委員會 +国家旅游度假区 國家旅遊度假區 +国家杯 國家杯 +国家标准 國家標準 +国家标准中文交换码 國家標準中文交換碼 +国家标准化管理委员会 國家標準化管理委員會 +国家标准码 國家標準碼 +国家栋梁 國家棟梁 +国家永续发展论坛 國家永續發展論壇 +国家空气品质标准 國家空氣品質標準 +国家规范 國家規範 +国家计划委员会 國家計劃委員會 +国家食品药品监督管理局 國家食品藥品監督管理局 +国建学术联谊会 國建學術聯誼會 +国建计划 國建計劃 +国戚 國戚 +国戚皇亲 國戚皇親 +国术 國術 +国术团 國術團 +国术社 國術社 +国术馆 國術館 +国梁 國樑 +国民党 國民黨 +国民党军队 國民黨軍隊 +国民党员 國民黨員 +国民党版 國民黨版 +国民党籍 國民黨籍 +国民常用字标准字体表 國民常用字標準字體表 +国民生活须知 國民生活須知 +国民礼仪范例 國民禮儀範例 +国民裁判制 國民裁判制 +国民革命党 國民革命黨 +国泰综合医院 國泰綜合醫院 +国王杯 國王杯 +国社党 國社黨 +国科会同步幅射研究中心 國科會同步幅射研究中心 +国科会晶片设计制作中心 國科會晶片設計製作中心 +国科会精密仪器发展中心 國科會精密儀器發展中心 +国穷民困 國窮民困 +国立历史博物馆 國立歷史博物館 +国立台北科技大学 國立臺北科技大學 +国立台湾博物馆 國立臺灣博物館 +国立台湾图书馆 國立臺灣圖書館 +国立台湾技术大学 國立臺灣技術大學 +国立教育广播电台 國立教育廣播電臺 +国胄 國胄 +国药 國藥 +国语注音符号第一式 國語注音符號第一式 +国语注音符号第二式 國語注音符號第二式 +国贸系 國貿系 +国防体制 國防體制 +国际人权标准 國際人權標準 +国际体操联合会 國際體操聯合會 +国际先驱论坛报 國際先驅論壇報 +国际公制 國際公制 +国际关系 國際關係 +国际关系学院 國際關係學院 +国际准备 國際準備 +国际分工体制 國際分工體制 +国际单位制 國際單位制 +国际台 國際臺 +国际合作 國際合作 +国际合作节 國際合作節 +国际奥林匹克委员会 國際奧林匹克委員會 +国际学术网路 國際學術網路 +国际学舍 國際學舍 +国际开发总会 國際開發總會 +国际柜 國際櫃 +国际标准 國際標準 +国际标准书码 國際標準書碼 +国际标准化组织 國際標準化組織 +国际标准舞 國際標準舞 +国际水准 國際水準 +国际漫游 國際漫遊 +国际漫游拨接服务 國際漫遊撥接服務 +国际电影制片人协会联盟 國際電影製片人協會聯盟 +国际羽毛球联合会 國際羽毛球聯合會 +国际舞台 國際舞臺 +国际航空联合会 國際航空聯合會 +国际讬管制度 國際託管制度 +国际贷借 國際貸借 +国际足球联合会 國際足球聯合會 +图书巡回车 圖書巡迴車 +图书目录 圖書目錄 +图书餐饮复合式餐厅 圖書餐飲複合式餐廳 +图书馆周 圖書館週 +图亚克拜 圖亞克拜 +图像用户介面 圖像用戶介面 +图克 圖克 +图录 圖錄 +图形用户界面 圖形用戶界面 +图形界面 圖形界面 +图文并茂 圖文並茂 +图木舒克 圖木舒克 +图木舒克市 圖木舒克市 +图板 圖板 +图表 圖表 +图表说明 圖表說明 +图财致命 圖財致命 +图资系统 圖資系統 +图里 圖裏 +图里亚夫 圖里亞夫 +图鉴 圖鑑 +图面 圖面 +囿于 囿於 +囿于一时 囿於一時 +囿于成见 囿於成見 +圆台 圓臺 +圆周 圓周 +圆周率 圓周率 +圆周角 圓周角 +圆周运动 圓周運動 +圆形面包 圓形麪包 +圆板 圓板 +圆板状 圓板狀 +圆柱面 圓柱面 +圆舞曲 圓舞曲 +圆锥台 圓錐臺 +圆锥曲线 圓錐曲線 +圆雕 圓雕 +圆面大耳 圓面大耳 +圆面积 圓面積 +圆面饼 圓麪餅 +圈占 圈佔 +圈子里 圈子裏 +圈扣 圈釦 +圈杯 圈杯 +圈梁 圈樑 +圈里 圈裏 +圈里人 圈裏人 +土中曲蟮 土中曲蟮 +土制 土製 +土制品 土製品 +土参 土參 +土司面包 土司麪包 +土地利用规划 土地利用規劃 +土地征收 土地徵收 +土地重划 土地重劃 +土壤冲蚀 土壤沖蝕 +土布 土布 +土托鱼 土托魚 +土托鱼羹 土托魚羹 +土木工程学系 土木工程學系 +土木系 土木系 +土法炼钢 土法煉鋼 +土洋并举 土洋並舉 +土洋结合 土洋結合 +土种 土種 +土耳其进行曲 土耳其進行曲 +土药 土藥 +土谷祠 土穀祠 +土里 土裏 +土里土气 土裏土氣 +土霉素 土黴素 +圣修伯里 聖修伯里 +圣克里斯多福 聖克里斯多福 +圣克里斯托巴 聖克里斯托巴 +圣克鲁斯 聖克魯斯 +圣克鲁斯岛 聖克魯斯島 +圣哈辛托 聖哈辛托 +圣坛 聖壇 +圣塔蒙尼加 聖塔蒙尼加 +圣帕台风 聖帕颱風 +圣帕强台 聖帕強颱 +圣帕特里克 聖帕特里克 +圣帕袭台 聖帕襲臺 +圣德克旭贝里 聖德克旭貝里 +圣拉蒙 聖拉蒙 +圣杯 聖盃 +圣杰门队 聖傑門隊 +圣母升天节 聖母升天節 +圣潘克勒斯站 聖潘克勒斯站 +圣火台 聖火臺 +圣皮埃尔和密克隆 聖皮埃爾和密克隆 +圣神降临周 聖神降臨週 +圣胡安 聖胡安 +圣药 聖藥 +圣诞叶 聖誕葉 +圣诞百合 聖誕百合 +圣迹 聖蹟 +圣餐台 聖餐檯 +圣餐布 聖餐布 +在一定范围内 在一定範圍內 +在下面 在下面 +在世界范围内 在世界範圍內 +在于 在於 +在克制 在剋制 +在全国范围 在全國範圍 +在全国范围内 在全國範圍內 +在全省范围内 在全省範圍內 +在出 在出 +在台 在臺 +在台协会 在臺協會 +在后 在後 +在坛子胡同 在罈子胡同 +在天愿做比翼鸟在地愿做连理枝 在天願做比翼鳥在地願做連理枝 +在家修 在家修 +在家出家 在家出家 +在家千日好出门一时难 在家千日好出門一時難 +在家靠父母出外靠朋友 在家靠父母出外靠朋友 +在密切注意 在密切注意 +在念 在念 +在某种程度上 在某種程度上 +在核 在覈 +在桥梁工地上 在橋梁工地上 +在此之后 在此之後 +在此后 在此後 +在泛 在泛 +在眼里 在眼裏 +在种 在種 +在职干部 在職幹部 +在职进修 在職進修 +在范围内 在範圍內 +在规范 在規範 +在野党 在野黨 +圭表 圭表 +地一卷 地一捲 +地下修文 地下修文 +地下修文郎 地下修文郎 +地下害虫 地下害蟲 +地下工厂 地下工廠 +地下开采 地下開採 +地下水面 地下水面 +地下电台 地下電臺 +地下签赌 地下簽賭 +地下通汇 地下通匯 +地丑德齐 地醜德齊 +地价 地價 +地价公告 地價公告 +地价税 地價稅 +地克制 地剋制 +地冲 地衝 +地制法 地制法 +地区党 地區黨 +地区冲突 地區衝突 +地区差价 地區差價 +地占 地佔 +地台 地臺 +地坛 地壇 +地塞米松 地塞米松 +地复天翻 地覆天翻 +地大物丰 地大物豐 +地尽其利 地盡其利 +地干吧 地幹吧 +地平面 地平面 +地当 地當 +地志 地誌 +地念 地念 +地方志 地方誌 +地方戏曲 地方戲曲 +地方色彩 地方色彩 +地无三里平 地無三里平 +地板 地板 +地板操 地板操 +地板运动 地板運動 +地极 地極 +地核 地核 +地灵人杰 地靈人傑 +地热发电厂 地熱發電廠 +地牛发威 地牛發威 +地狱谷 地獄谷 +地球同步轨道 地球同步軌道 +地理极 地理極 +地理资讯系统 地理資訊系統 +地瓜叶 地瓜葉 +地瓜签 地瓜籤 +地瓜面 地瓜面 +地缘关系 地緣關係 +地缝里 地縫裏 +地老天荒不了情 地老天荒不了情 +地胄 地胄 +地蜡 地蠟 +地表 地表 +地表水 地表水 +地质年代表 地質年代表 +地质年表 地質年表 +地质编录 地質編錄 +地里 地裏 +地里鬼 地裏鬼 +地面 地面 +地面上 地面上 +地面下 地面下 +地面层 地面層 +地面控制 地面控制 +地面核爆炸 地面核爆炸 +地面气压 地面氣壓 +地面水 地面水 +地面温度 地面溫度 +地面灌溉 地面灌溉 +地面站 地面站 +地面部队 地面部隊 +地面零点 地面零點 +场合 場合 +场记板 場記板 +场面 場面 +场面阔绰 場面闊綽 +均一价 均一價 +均权制度 均權制度 +均田制 均田制 +均田制度 均田制度 +坊曲 坊曲 +坍台 坍臺 +坎塔布连 坎塔布連 +坎塔布连山脉 坎塔布連山脈 +坎塔布连海 坎塔布連海 +坏了 壞了 +坏了性命 壞了性命 +坏于 壞於 +坏家伙 壞傢伙 +坏恶 壞惡 +坐了 坐了 +坐冷板凳 坐冷板凳 +坐台 坐檯 +坐台子 坐檯子 +坐台小姐 坐檯小姐 +坐回 坐回 +坐困 坐困 +坐困愁城 坐困愁城 +坐如钟 坐如鐘 +坐山观虎斗 坐山觀虎鬥 +坐庄 坐莊 +坐板疮 坐板瘡 +坐标 座標 +坐标系 座標系 +坐致 坐致 +坐药 坐藥 +坐萝卜 坐蘿蔔 +坐薪尝胆 坐薪嘗膽 +坐蜡 坐蠟 +坐钟 坐鐘 +坐领干薪 坐領乾薪 +坑蒙 坑蒙 +坑里 坑裏 +块肉馀生录 塊肉餘生錄 +坚白同异 堅白同異 +坚臥烟霞 堅臥煙霞 +坚致 堅緻 +坛佳酿 罈佳釀 +坛兆 壇兆 +坛台 壇臺 +坛场 壇場 +坛坛罐罐 罈罈罐罐 +坛坫 壇坫 +坛坫周旋 壇坫週旋 +坛城 壇城 +坛女儿红 罈女兒紅 +坛好酒 罈好酒 +坛子 罈子 +坛宇 壇宇 +坛烧刀子 罈燒刀子 +坛燒刀子 罈燒刀子 +坛白干 罈白干 +坛美酒 罈美酒 +坛老酒 罈老酒 +坛陈年 罈陳年 +坛騞 罈騞 +坛高粱 罈高粱 +坡布莱 坡布萊 +坤极 坤極 +坤范 坤範 +坤表 坤錶 +坦克 坦克 +坦克兵 坦克兵 +坦克型 坦克型 +坦克车 坦克車 +坦坦荡荡 坦坦蕩蕩 +坦尚尼亚联合共和国 坦尚尼亞聯合共和國 +坦涂 坦塗 +坦荡 坦蕩 +坦荡荡 坦蕩蕩 +坨里 坨里 +坯布 坯布 +坱郁 坱鬱 +垂了 垂了 +垂了下去 垂了下去 +垂了下来 垂了下來 +垂于 垂於 +垂于将来 垂於將來 +垂发 垂髮 +垂吊 垂吊 +垂帘 垂簾 +垂帘听政 垂簾聽政 +垂念 垂念 +垂死挣扎 垂死掙扎 +垂注 垂注 +垂涎欲滴 垂涎欲滴 +垂直于 垂直於 +垂直搜索 垂直搜索 +垂直面 垂直面 +垂范 垂範 +垂范百世 垂範百世 +垂裕后昆 垂裕後昆 +垂面 垂面 +垂馨千祀 垂馨千祀 +垃圾回收 垃圾回收 +垃圾虫 垃圾蟲 +垄断价格 壟斷價格 +垄断资产 壟斷資產 +垄断集团 壟斷集團 +型别 型別 +型别转换 型別轉換 +型录 型錄 +型录式广告 型錄式廣告 +型板 型板 +型范 型範 +垢面 垢面 +垣曲 垣曲 +垣曲县 垣曲縣 +垦丁杯 墾丁盃 +垦复 墾複 +垦辟 墾闢 +垫个 墊個 +垫了 墊了 +垫出 墊出 +垫出去 墊出去 +垫出来 墊出來 +垫发 墊發 +垫回 墊回 +垫回去 墊回去 +垫回来 墊回來 +垫板 墊板 +垮了 垮了 +垮台 垮臺 +垮向 垮向 +埃克托 埃克托 +埃克托柏辽兹 埃克托柏遼茲 +埃及历 埃及曆 +埃及历史 埃及歷史 +埃及艳后 埃及豔后 +埃夫伯里 埃夫伯里 +埃布罗 埃布羅 +埃布罗河 埃布羅河 +埃德蒙顿 埃德蒙頓 +埃拉托塞尼斯 埃拉托塞尼斯 +埃格尔松 埃格爾松 +埃荣冲 埃榮衝 +埃里温 埃裏溫 +埋了 埋了 +埋头寻表 埋頭尋錶 +埋头寻钟 埋頭尋鐘 +埋头寻钟表 埋頭尋鐘錶 +埋头苦干 埋頭苦幹 +埋尸 埋屍 +埋布 埋佈 +埋杆竖柱 埋杆豎柱 +埋没人才 埋沒人才 +城市依赖症 城市依賴症 +城市规划 城市規劃 +城里 城裏 +城雕 城雕 +埔里 埔里 +埔里社抚垦局 埔裏社撫墾局 +埔里镇 埔里鎮 +域名抢注 域名搶注 +域名注册 域名註冊 +域多利皇后 域多利皇后 +埤塘里 埤塘里 +培养出 培養出 +培养出来 培養出來 +培尔松 培爾松 +培植出 培植出 +培特曼赫鲁威克 培特曼赫魯威克 +培美曲塞 培美曲塞 +培育出 培育出 +培育出来 培育出來 +培里克利斯 培裏克利斯 +培里克里斯 培里克里斯 +基于 基於 +基克维特 基克維特 +基准 基準 +基准兵 基準兵 +基准日 基準日 +基准法 基準法 +基准点 基準點 +基准线 基準線 +基准面 基準面 +基因修改 基因修改 +基因技术 基因技術 +基因表达 基因表達 +基团 基團 +基地台 基地臺 +基尔库克 基爾庫克 +基岩 基岩 +基布兹 基布茲 +基干 基幹 +基度山恩仇记 基度山恩仇記 +基性岩石 基性岩石 +基本多文种平面 基本多文種平面 +基本词汇 基本詞彙 +基本面 基本面 +基板 基板 +基极 基極 +基民党 基民黨 +基里兰柯 基里蘭柯 +基里巴斯 基里巴斯 +基里巴斯共和国 基裏巴斯共和國 +基面 基面 +堂分姑娘 堂分姑娘 +堂后官 堂後官 +堂屋里挂草荐 堂屋裏掛草薦 +堂布 堂布 +堆了 堆了 +堆案盈几 堆案盈几 +堕云雾中 墮雲霧中 +堕胎药 墮胎藥 +堙淀 堙澱 +堪为表率 堪爲表率 +堪布 堪布 +堪当 堪當 +堪当重任 堪當重任 +堪称典范 堪稱典範 +堪舆术 堪輿術 +塌台 塌臺 +塑炼 塑煉 +塑炼机 塑煉機 +塑胶制 塑膠製 +塑胶布 塑膠布 +塑胶杯 塑膠杯 +塑胶板 塑膠板 +塑造出 塑造出 +塔什干 塔什干 +塔什库尔干乡 塔什庫爾干鄉 +塔什库尔干塔吉克自治县 塔什庫爾干塔吉克自治縣 +塔什库尔干自治县 塔什庫爾干自治縣 +塔克 塔克 +塔克拉玛干 塔克拉瑪干 +塔克拉玛干沙漠 塔克拉瑪干沙漠 +塔克拉马干 塔克拉馬干 +塔克辛 塔克辛 +塔台 塔臺 +塔吉克 塔吉克 +塔吉克人 塔吉克人 +塔吉克共和国 塔吉克共和國 +塔吉克斯坦 塔吉克斯坦 +塔吉克族 塔吉克族 +塔吊 塔吊 +塔娜苏冈 塔娜蘇岡 +塔娜苏甘 塔娜蘇甘 +塔布 塔布 +塔波兰尼克 塔波蘭尼克 +塔罗维克 塔羅維克 +塔里契亚努 塔里契亞努 +塔里木 塔里木 +塔里木河 塔里木河 +塔里木盆地 塔里木盆地 +塔里班 塔里班 +塔钟 塔鐘 +塞上曲 塞上曲 +塞下曲 塞下曲 +塞克 塞克 +塞克森 塞克森 +塞回 塞回 +塞尔提克 塞爾提克 +塞尔提克队 塞爾提克隊 +塞尔维亚克罗地亚语 塞爾維亞克羅地亞語 +塞尔维亚民主党 塞爾維亞民主黨 +塞尔蒂克 塞爾蒂克 +塞尔蒂克队 塞爾蒂克隊 +塞瓦斯托波尔 塞瓦斯托波爾 +塞耳盗钟 塞耳盜鐘 +塞药 塞藥 +塞莉佛维克 塞莉佛維克 +塞车症候群 塞車症候羣 +塞韦里诺 塞韋裏諾 +填个 填個 +填了 填了 +填发 填發 +填字游戏 填字遊戲 +填表 填表 +填表格 填表格 +墓志 墓誌 +墓志铭 墓誌銘 +墓表 墓表 +墙里 牆裏 +墙面 牆面 +墙面而立 牆面而立 +增修 增修 +增辟 增闢 +增量参数 增量參數 +墟里 墟里 +墨卷 墨卷 +墨斗 墨斗 +墨斗鱼 墨斗魚 +墨沈 墨沈 +墨沈未干 墨瀋未乾 +墨沈沈 墨沈沈 +墨索里尼 墨索里尼 +墨荡子 墨盪子 +墨西哥合众国 墨西哥合衆國 +墨迹未干 墨跡未乾 +墩台 墩臺 +墩布 墩布 +壁志 壁誌 +壁立千仞 壁立千仞 +壁钟 壁鐘 +壑谷 壑谷 +士别三日 士別三日 +士别三日刮目相待 士別三日刮目相待 +士别多日 士別多日 +士胄 士胄 +壮室之秋 壯室之秋 +壮志 壯志 +壮志凌云 壯志凌雲 +壮志凌霄 壯志凌霄 +壮志未酬 壯志未酬 +壮游 壯遊 +壮阳药 壯陽藥 +壮面 壯麪 +声价 聲價 +声价不凡 聲價不凡 +声价十倍 聲價十倍 +声如洪钟 聲如洪鐘 +声彻云霄 聲徹雲霄 +声情并茂 聲情並茂 +声类系统 聲類系統 +壳里 殼裏 +壶口瀑布 壺口瀑布 +壶范 壺範 +壶里 壺裏 +壶里乾坤 壺裏乾坤 +壹个人 壹個人 +壹周刊 壹週刊 +壹败涂地 壹敗塗地 +壹郁 壹鬱 +处于 處於 +处女表演 處女表演 +处方药 處方藥 +处理厂 處理廠 +处理表 處理表 +备尝 備嘗 +备尝忧患 備嘗憂患 +备尝艰苦 備嘗艱苦 +备尝艰辛 備嘗艱辛 +备尝辛苦 備嘗辛苦 +备御 備禦 +备忘录 備忘錄 +备抵折旧 備抵折舊 +备注 備註 +备注栏 備註欄 +复三 復三 +复上 覆上 +复业 復業 +复习 複習 +复习考 複習考 +复书 復書 +复交 復交 +复亩珍 複畝珍 +复仇 復仇 +复仇者 復仇者 +复仇记 復仇記 +复仇雪耻 復仇雪恥 +复仞年如 複仞年如 +复以百万 複以百萬 +复任 復任 +复会 復會 +复位 復位 +复住 覆住 +复信 覆信 +复健 復健 +复健中心 復健中心 +复健医学 復健醫學 +复健科 復健科 +复健赛 復健賽 +复元 復元 +复元音 複元音 +复共轭 複共軛 +复兴 復興 +复兴中学 復興中學 +复兴乡 復興鄉 +复兴党 復興黨 +复兴剧校 復興劇校 +复兴剧艺学校 復興劇藝學校 +复兴北路 復興北路 +复兴区 復興區 +复兴南路 復興南路 +复兴号 復興號 +复兴基地 復興基地 +复兴岗 復興崗 +复兴岛 復興島 +复兴工商 復興工商 +复兴广播 復興廣播 +复兴广播电台 復興廣播電臺 +复兴时代 復興時代 +复兴社 復興社 +复兴路 復興路 +复兴门 復興門 +复兴馆 復興館 +复兴高中 復興高中 +复冒 覆冒 +复写 複寫 +复写纸 複寫紙 +复军 覆軍 +复军杀将 覆軍殺將 +复冰 復冰 +复决 複決 +复决权 複決權 +复出 復出 +复函 覆函 +复函数 複函數 +复分数 複分數 +复分析 複分析 +复分解 複分解 +复分解反应 複分解反應 +复刊 復刊 +复列 複列 +复利 複利 +复利法 複利法 +复利率 複利率 +复利计算 複利計算 +复制 複製 +复制下来 複製下來 +复制出 複製出 +复制品 複製品 +复刻 復刻 +复华 復華 +复华金 復華金 +复卒 復卒 +复印 複印 +复印品 複印品 +复印机 複印機 +复印纸 複印紙 +复原 復原 +复原期 復原期 +复去翻来 覆去翻來 +复发 復發 +复发性 複發性 +复发率 複發率 +复变函数 複變函數 +复变函数论 複變函數論 +复古 復古 +复古会 復古會 +复古风 復古風 +复句 複句 +复叶 複葉 +复合 複合 +复合企业 複合企業 +复合传动 複合傳動 +复合体 複合體 +复合元音 複合元音 +复合光 複合光 +复合包装 複合包裝 +复合句 複合句 +复合命题 複合命題 +复合国 複合國 +复合型 複合型 +复合增长 複合增長 +复合字 複合字 +复合年 複合年 +复合式 複合式 +复合性 複合性 +复合技 複合技 +复合摄影 複合攝影 +复合机 複合機 +复合材料 複合材料 +复合板 複合板 +复合架 複合架 +复合样式 複合樣式 +复合概念 複合概念 +复合模 複合模 +复合母音 複合母音 +复合民族国家 複合民族國家 +复合物 複合物 +复合管 複合管 +复合肥料 複合肥料 +复合膜 複合膜 +复合药 複合藥 +复合蛋白质 複合蛋白質 +复合装甲 複合裝甲 +复合词 複合詞 +复合词素词 複合詞素詞 +复合量词 複合量詞 +复合金属 複合金屬 +复合韵母 複合韻母 +复名 複名 +复名数 複名數 +复名词 複名詞 +复吸 復吸 +复呈 覆呈 +复员 復員 +复员令 復員令 +复员军人 複員軍人 +复命 覆命 +复品牌 複品牌 +复回 復回 +复国 復國 +复圆 復圓 +复土 復土 +复圣 復聖 +复在 覆在 +复基因 複基因 +复墓 覆墓 +复壁 複壁 +复壮 復壯 +复复 複復 +复始 復始 +复姓 複姓 +复婚 復婚 +复婚制 複婚制 +复子明辟 復子明辟 +复字键 複字鍵 +复学 復學 +复学生 復學生 +复宗 覆宗 +复审 複審 +复对数 複對數 +复工 復工 +复帐 覆帳 +复帱 覆幬 +复平面 複平面 +复庇之恩 覆庇之恩 +复康巴士 復康巴士 +复建 復建 +复建中心 復建中心 +复式 複式 +复式关税 複式關稅 +复式教学 複式教學 +复式路面 複式路面 +复归 復歸 +复役 復役 +复循环发电 複循環發電 +复思 復思 +复意 複意 +复成 覆成 +复拍子 複拍子 +复按 覆按 +复政 復政 +复数 複數 +复数域 複數域 +复数平面 複數平面 +复数形 複數形 +复数形式 複數形式 +复文 覆文 +复新 復新 +复方 複方 +复旦 復旦 +复旦大学 復旦大學 +复旦桥 復旦橋 +复旧 復舊 +复旧如新 復舊如新 +复明 復明 +复本 複本 +复本位制度 複本位制度 +复杂 複雜 +复杂劳动 複雜勞動 +复杂化 複雜化 +复杂度 複雜度 +复杂度理论 複雜度理論 +复杂性 複雜性 +复杂生产 複雜生產 +复杂系统 複雜系統 +复权 復權 +复杯 覆杯 +复果 複果 +复查 複查 +复査 複查 +复校 覆校 +复核 複覈 +复案 復案 +复检 複檢 +复次 複次 +复殖吸虫 複殖吸蟲 +复殖目 複殖目 +复比 複比 +复比例 複比例 +复氧能力 復氧能力 +复水 覆水 +复没 覆沒 +复活 復活 +复活岛 復活島 +复活日 復活日 +复活的军团 復活的軍團 +复活节 復活節 +复活节岛 復活節島 +复活赛 復活賽 +复流 複流 +复测 複測 +复海移山 覆海移山 +复灭 覆滅 +复燃 復燃 +复现 復現 +复瓿 覆瓿 +复生 復生 +复用 複用 +复电 覆電 +复盂 覆盂 +复盂之固 覆盂之固 +复盂之安 覆盂之安 +复盆 覆盆 +复盆之冤 覆盆之冤 +复盆子 覆盆子 +复盆难照 覆盆難照 +复盐 複鹽 +复盖 覆蓋 +复盖住 覆蓋住 +复盖率 覆蓋率 +复盖面 覆蓋面 +复盘 覆盤 +复盘难照 覆盤難照 +复目 複目 +复相关 複相關 +复眼 複眼 +复礼 復禮 +复礼克己 復禮克己 +复种 複種 +复种指数 複種指數 +复称 複稱 +复穴 複穴 +复籍 復籍 +复线 複線 +复综语 複綜語 +复耕 復耕 +复职 復職 +复肥 複肥 +复育 覆育 +复舟 覆舟 +复舟载舟 覆舟載舟 +复色 複色 +复色光 複色光 +复苏 復甦 +复苏期 復甦期 +复苏术 復甦術 +复萌 復萌 +复蔽 覆蔽 +复蕉寻鹿 覆蕉尋鹿 +复被 覆被 +复襦 複襦 +复视 複視 +复训 複訓 +复议 複議 +复评 複評 +复诊 複診 +复词 複詞 +复试 複試 +复诵 複誦 +复读 復讀 +复读生 復讀生 +复课 復課 +复败 覆敗 +复赛 複賽 +复蹈其辙 復蹈其轍 +复蹈前辙 復蹈前轍 +复身 復身 +复车 覆車 +复车之戒 覆車之戒 +复车之轨 覆車之軌 +复车之辙 覆車之轍 +复车之鉴 覆車之鑑 +复车当戒 覆車當戒 +复车继轨 覆車繼軌 +复转 復轉 +复载 覆載 +复辅音 複輔音 +复辙 覆轍 +复辙重蹈 覆轍重蹈 +复辟 復辟 +复辟事件 復辟事件 +复返 復返 +复述 複述 +复逆 覆逆 +复选 複選 +复选题 複選題 +复道 複道 +复酱瓿 覆醬瓿 +复醒 復醒 +复醢 覆醢 +复钱 複錢 +复阁 複閣 +复阅 複閱 +复除 復除 +复雠 復讎 +复雨翻云 覆雨翻雲 +复露 覆露 +复音 複音 覆音 +复音形 複音形 +复音词 複音詞 +复韵 複韻 +复韵母 複韻母 +复频 複頻 +复验 複驗 +复魄 復魄 +复鹿寻蕉 覆鹿尋蕉 +复鹿遗蕉 覆鹿遺蕉 +复鼎 覆鼎 +复𫗧 覆餗 +复𫗧之忧 覆餗之憂 +复𫗧之患 覆餗之患 +复𫗧之衅 覆餗之釁 +夏于乔 夏于喬 +夏于喬 夏于喬 +夏历 夏曆 +夏后氏 夏后氏 +夏商周 夏商周 +夏天里 夏天裏 +夏姆锡克 夏姆錫克 +夏川里美 夏川里美 +夏布 夏布 +夏日里 夏日裏 +夏时制 夏時制 +夏洛克 夏洛克 +夏游 夏遊 +夏炉冬扇 夏爐冬扇 +夏秋 夏秋 +夏种 夏種 +夏虫 夏蟲 +夏虫不可以语冰 夏蟲不可以語冰 +夏虫不可语冰 夏蟲不可語冰 +夏虫朝菌 夏蟲朝菌 +夏里夫 夏里夫 +夏鑪冬扇 夏鑪冬扇 +夕烟 夕煙 +外交代表 外交代表 +外交关系 外交關係 +外交关系理事会 外交關係理事會 +外交团 外交團 +外交系 外交系 +外佣 外傭 +外侧裂周区 外側裂周區 +外借 外借 +外出 外出 +外出服 外出服 +外出血 外出血 +外出装 外出裝 +外出访问 外出訪問 +外制 外製 +外厂 外廠 +外发 外發 +外合里差 外合裏差 +外合里应 外合裏應 +外后日 外後日 +外向 外向 +外向型 外向型 +外国旅游者 外國旅遊者 +外屋里的灶君爷 外屋裏的竈君爺 +外弦 外弦 +外强中干 外強中乾 +外御其侮 外禦其侮 +外戚 外戚 +外才 外才 +外挂 外掛 +外挂式 外掛式 +外文系 外文系 +外明不知里暗 外明不知裏暗 +外来物种 外來物種 +外来种 外來種 +外松内紧 外鬆內緊 +外核 外核 +外欲 外慾 +外汇 外匯 +外汇储备 外匯儲備 +外汇兑换 外匯兌換 +外汇存底 外匯存底 +外汇定期存单 外匯定期存單 +外汇局 外匯局 +外汇市场 外匯市場 +外汇收入 外匯收入 +外汇汇率 外匯匯率 +外汇管制 外匯管制 +外汇银行 外匯銀行 +外涂 外塗 +外烟 外菸 +外用药 外用藥 +外科学系 外科學系 +外科手术 外科手術 +外聘制 外聘制 +外舍 外舍 +外蒙 外蒙 +外蒙古 外蒙古 +外表 外表 +外表上 外表上 +外语系 外語系 +外调制 外調制 +外部链接 外部鏈接 +外面 外面 +外面儿光 外面兒光 +外面情 外面情 +夙仇 夙仇 +夙心往志 夙心往志 +夙愿 夙願 +夙愿以偿 夙願以償 +多不胜数 多不勝數 +多丑 多醜 +多么 多麼 +多义关系 多義關係 +多事之秋 多事之秋 +多事逞才 多事逞才 +多于 多於 +多云 多雲 +多云偶 多雲偶 +多云偶阵雨 多雲偶陣雨 +多云偶雨 多雲偶雨 +多云有雨 多雲有雨 +多人聊天系统 多人聊天系統 +多余 多餘 +多侧面 多側面 +多借 多借 +多党 多黨 +多党制 多黨制 +多党选举 多黨選舉 +多冲 多衝 +多凶少吉 多凶少吉 +多出 多出 +多出来 多出來 +多功能表 多功能表 +多动症 多動症 +多占 多佔 +多原作艺术 多原作藝術 +多发性 多發性 +多发病 多發病 +多只 多隻 +多台 多臺 +多吃多占 多吃多佔 +多向 多向 +多哈回合 多哈回合 +多回 多回 +多天后 多天後 +多妻制 多妻制 +多姿多彩 多姿多彩 +多姿多采 多姿多采 +多媒体杂志 多媒體雜誌 +多媒体简报系统 多媒體簡報系統 +多少只 多少隻 +多层复 多層複 +多层复迭 多層複迭 +多干 多幹 +多平台 多平臺 +多平台环境 多平臺環境 +多幸 多幸 +多式综合语 多式綜合語 +多当 多當 +多彩 多彩 +多彩多姿 多彩多姿 +多情种子 多情種子 +多才 多才 +多才多艺 多才多藝 +多拉尔蒙 多拉爾蒙 +多指症 多指症 +多数党 多數黨 +多方面 多方面 +多极化 多極化 +多核 多核 +多模光纤 多模光纖 +多欲 多欲 +多汗症 多汗症 +多特蒙德 多特蒙德 +多种 多種 +多种多样 多種多樣 +多种经营 多種經營 +多种语言 多種語言 +多种语言支持 多種語言支持 +多管闲事 多管閒事 +多米尼克 多米尼克 +多粒子系统 多粒子系統 +多蒙寄声 多蒙寄聲 +多蒙推毂 多蒙推轂 +多蒙药石 多蒙藥石 +多表 多表 +多轨录音 多軌錄音 +多边合作 多邊合作 +多采 多采 +多采多姿 多采多姿 +多面 多面 +多面体 多面體 +多面性 多面性 +多面手 多面手 +多面角 多面角 +夜儿个 夜兒個 +夜光云 夜光雲 +夜光杯 夜光杯 +夜光虫 夜光蟲 +夜光表 夜光錶 +夜半钟声 夜半鐘聲 +夜台 夜臺 +夜合 夜合 +夜合花 夜合花 +夜合钱 夜合錢 +夜尿症 夜尿症 +夜度娘 夜度娘 +夜惊症 夜驚症 +夜暗 夜暗 +夜暗风高 夜暗風高 +夜曲 夜曲 +夜游 夜遊 +夜游子 夜遊子 +夜游神 夜遊神 +夜游队 夜遊隊 +夜盲症 夜盲症 +夜色迷蒙 夜色迷濛 +夜谭随录 夜譚隨錄 +夜里 夜裏 +夜雨秋灯录 夜雨秋燈錄 +够了 夠了 +够克制 夠剋制 +够凶 夠兇 +够出 夠出 +够涂 夠塗 +够种 夠種 +够钟情 夠鍾情 +够钟爱 夠鍾愛 +夠面子 夠面子 +大一统志 大一統誌 +大不了 大不了 +大不相同 大不相同 +大不里士 大不里士 +大丑 大丑 +大专同学 大專同學 +大专杯 大專杯 +大业千秋 大業千秋 +大个 大個 +大个儿 大個兒 +大个子 大個子 +大丰 大豐 +大丰市 大豐市 +大丰收 大豐收 +大丰簋 大豐簋 +大义觉迷录 大義覺迷錄 +大乌苏里岛 大烏蘇里島 +大事化小小事化了 大事化小小事化了 +大于 大於 +大人药 大人藥 +大仁药专 大仁藥專 +大价钱 大價錢 +大众捷运系统 大衆捷運系統 +大伙 大夥 +大伙人 大夥人 +大伙儿 大夥兒 +大余 大餘 +大余县 大餘縣 +大便干燥 大便乾燥 +大修 大修 +大修理 大修理 +大修理基金 大修理基金 +大修道院 大修道院 +大修道院长 大修道院長 +大借款 大借款 +大傢伙儿 大傢伙兒 +大元大一统志 大元大一統誌 +大党 大黨 +大兵团 大兵團 +大冲 大沖 +大减价 大減價 +大凶 大凶 +大出 大出 +大出丧 大出喪 +大出其汗 大出其汗 +大出意料之外 大出意料之外 +大出手 大出手 +大出血 大出血 +大出锋头 大出鋒頭 +大出风头 大出風頭 +大利面 大利麪 +大别 大別 +大别山 大別山 +大别山脉 大別山脈 +大制作 大製作 +大力发展 大力發展 +大动干戈 大動干戈 +大千 大千 +大千世界 大千世界 +大华技术学院 大華技術學院 +大协奏曲 大協奏曲 +大单于 大單于 +大卤面 大滷麪 +大卷 大卷 +大厂 大廠 +大厂县 大廠縣 +大厂回族自治县 大廠回族自治縣 +大历 大曆 +大历十才子 大曆十才子 +大历史 大歷史 +大厦栋梁 大廈棟梁 +大发 大發 +大发利市 大發利市 +大发慈悲 大發慈悲 +大发牢骚 大發牢騷 +大发神威 大發神威 +大发脾气 大發脾氣 +大发雷霆 大發雷霆 +大叔于田 大叔于田 +大只 大隻 +大台 大臺 +大台北 大臺北 +大台北区 大臺北區 +大叶合欢 大葉合歡 +大叶大学 大葉大學 +大叶性肺炎 大葉性肺炎 +大叶桉 大葉桉 +大叶野百合 大葉野百合 +大叹 大嘆 +大合唱 大合唱 +大同 大同 +大同世界 大同世界 +大同主义 大同主義 +大同之世 大同之世 +大同乡 大同鄉 +大同书 大同書 +大同区 大同區 +大同县 大同縣 +大同商专 大同商專 +大同国中 大同國中 +大同大学 大同大學 +大同小异 大同小異 +大同市 大同市 +大同教 大同教 +大同煤矿 大同煤礦 +大同盆地 大同盆地 +大同盟 大同盟 +大同社会 大同社會 +大同路 大同路 +大后天 大後天 +大后年 大後年 +大后方 大後方 +大周后 大周后 +大周折 大週摺 +大咸 大咸 +大哗 大譁 +大回 大回 +大回朝 大回朝 +大团 大團 +大团圆 大團圓 +大团白脸 大團白臉 +大团结 大團結 +大国家党 大國家黨 +大地回春 大地回春 +大地春回 大地春回 +大场面 大場面 +大型钟 大型鐘 +大型钟表 大型鐘錶 +大型钟表面 大型鐘表面 +大型钟面 大型鐘面 +大天后宫 大天后宮 +大夫松 大夫松 +大头症 大頭症 +大头针 大頭針 +大头面目 大頭面目 +大夸 大誇 +大奸 大奸 +大奸似忠 大奸似忠 +大姑娘 大姑娘 +大姑娘坐花轿 大姑娘坐花轎 +大娘 大娘 +大娘子 大娘子 +大家伙儿 大家夥兒 +大家风范 大家風範 +大尽 大盡 +大山谷州立大学 大山谷州立大學 +大岩桐 大巖桐 +大峡谷 大峽谷 +大布 大布 +大布棚 大布棚 +大干 大幹 +大干一场 大幹一場 +大干一票 大幹一票 +大干特干 大幹特幹 +大幸 大幸 +大廉价 大廉價 +大当 大當 +大志 大志 +大恶 大惡 +大才 大才 +大才小用 大才小用 +大才槃槃 大才槃槃 +大打出手 大打出手 +大打折扣 大打折扣 +大折儿 大摺兒 +大抚台 大撫臺 +大搜 大搜 +大摆 大擺 +大摆褶子 大擺褶子 +大摇大摆 大搖大擺 +大放异彩 大放異彩 +大放异采 大放異采 +大政方针 大政方針 +大敌当前 大敵當前 +大斗 大斗 +大斗小秤 大斗小秤 +大方向 大方向 +大旱云霓 大旱雲霓 +大旱望云霓 大旱望雲霓 +大明历 大明曆 +大明历史 大明歷史 +大明大摆 大明大擺 +大是不同 大是不同 +大曲 大麴 +大曲道 大曲道 +大曲酒 大麴酒 +大有分别 大有分別 +大有斩获 大有斬獲 +大本钟 大本鐘 +大本钟敲 大本鐘敲 +大来历 大來歷 +大杯 大杯 +大板 大板 +大板根 大板根 +大板车 大板車 +大核 大核 +大梁 大梁 大樑 +大楼监控系统 大樓監控系統 +大欲 大欲 +大武仑 大武崙 +大气团 大氣團 +大气折射 大氣折射 +大气系 大氣系 +大水冲倒龙王庙 大水沖倒龍王廟 +大水冲倒龙王殿 大水沖倒龍王殿 +大水冲溺 大水沖溺 +大汉技术学院 大漢技術學院 +大汗淋漓 大汗淋漓 +大海捞针 大海撈針 +大润发 大潤發 +大涨小回 大漲小回 +大清一统志 大清一統志 +大湄公河次区域合作 大湄公河次區域合作 +大溪豆干 大溪豆幹 +大烟 大煙 +大爱台 大愛臺 +大班制 大班制 +大理岩 大理岩 +大病初愈 大病初癒 +大症 大症 +大白日里借不出个干灯盏来 大白日裏借不出個乾燈盞來 +大目干连 大目乾連 +大目干连冥间救母变文 大目乾連冥間救母變文 +大秋 大秋 +大秋作物 大秋作物 +大秋元 大秋元 +大笨钟 大笨鐘 +大笨钟敲 大笨鐘敲 +大精彩 大精彩 +大系 大系 +大缸里翻油沿路拾芝麻 大缸裏翻油沿路拾芝麻 +大老板 大老闆 +大而无当 大而無當 +大肆搜捕 大肆搜捕 +大肠杆菌 大腸桿菌 +大肠杆菌群 大腸桿菌羣 +大胜 大勝 +大胡子 大鬍子 +大脑出血性中风 大腦出血性中風 +大脚婆娘 大腳婆娘 +大腔当 大腔當 +大致 大致 +大致上 大致上 +大致说来 大致說來 +大舍 大舍 +大花面 大花面 +大苏打 大蘇打 +大英联合王国 大英聯合王國 +大范围 大範圍 +大获 大獲 +大获全胜 大獲全勝 +大虫 大蟲 +大虫不吃伏肉 大蟲不吃伏肉 +大虫口里倒涎 大蟲口裏倒涎 +大虫吃小虫 大蟲吃小蟲 +大虫头上做窠 大蟲頭上做窠 +大蜡 大蜡 +大衍历 大衍曆 +大衍历史 大衍歷史 +大表 大表 +大表哥 大表哥 +大表妹 大表妹 +大表姊 大表姊 +大表弟 大表弟 +大表惊叹 大表驚歎 +大裂谷 大裂谷 +大角度俯冲轰炸 大角度俯衝轟炸 +大言非夸 大言非夸 +大谷 大谷 +大象口里拔生牙 大象口裏拔生牙 +大费周折 大費周折 +大费周章 大費周章 +大赞 大讚 +大辟 大辟 +大迦叶 大迦葉 +大通回族土族自治县 大通回族土族自治縣 +大通师范学堂 大通師範學堂 +大采 大采 +大里 大里 +大里市 大里市 +大里溪 大里溪 +大金发苔 大金髮薹 +大鉴 大鑒 +大钟 大鐘 +大锤 大錘 +大门不出二门不迈 大門不出二門不邁 +大陆冷气团 大陸冷氣團 +大陆同胞 大陸同胞 +大陆团 大陸團 +大陆性气团 大陸性氣團 +大陆板块 大陸板塊 +大陆气团 大陸氣團 +大陆法系 大陸法系 +大陪审团 大陪審團 +大难不死必有后禄 大難不死必有後祿 +大难不死必有后福 大難不死必有後福 +大集合 大集合 +大雨如注 大雨如注 +大面 大面 +大面儿上 大面兒上 +大面皮儿 大面皮兒 +大风后 大風後 +大风鉴 大風鑑 +大食团 大食團 +大马步挂 大馬步掛 +大麦克 大麥克 +大麦克指数 大麥克指數 +大麻烟 大麻煙 +大麻里 大麻里 +大麻里乡 大麻里鄉 +大黑松 大黑松 +天下大势分久必合合久必分 天下大勢分久必合合久必分 +天下安注意相天下危注意将 天下安注意相天下危注意將 +天下无难事只怕有心人 天下無難事只怕有心人 +天下杂志 天下雜誌 +天不从人愿 天不從人願 +天人合一 天人合一 +天人永别 天人永別 +天从人愿 天從人願 +天价 天價 +天作之合 天作之合 +天克地冲 天剋地衝 +天冬氨酸 天冬氨酸 +天冬苯丙二肽酯 天冬苯丙二肽酯 +天冬酰胺 天冬醯胺 +天历 天曆 +天发神谶碑 天發神讖碑 +天台 天台 天臺 +天台县 天台縣 +天台女 天台女 +天台宗 天台宗 +天台山 天台山 +天同 天同 +天后 天后 天後 +天后站 天后站 +天后级 天后級 +天团 天團 +天地万物 天地萬物 +天地为范 天地爲範 +天地之别 天地之別 +天地可表 天地可表 +天地志狼 天地志狼 +天坛 天壇 +天坛座 天壇座 +天壤之别 天壤之別 +天复 天覆 +天复地载 天覆地載 +天妒英才 天妒英才 +天孙娘娘 天孫娘娘 +天差地别 天差地別 +天干 天干 +天干地支 天干地支 +天干物燥 天乾物燥 +天幸 天幸 +天心和合 天心和合 +天心岩 天心岩 +天才 天才 +天才儿童 天才兒童 +天才出自勤奋 天才出自勤奮 +天才型 天才型 +天才教育 天才教育 +天才横溢 天才橫溢 +天才论 天才論 +天文台 天文臺 +天文学钟 天文學鐘 +天文钟 天文鐘 +天无三日晴地无三里平 天無三日晴地無三里平 +天日之表 天日之表 +天昏地暗 天昏地暗 +天暗 天暗 +天有不测风云 天有不測風雲 +天有不测风云人有旦夕祸福 天有不測風雲人有旦夕禍福 +天机云锦 天機雲錦 +天津师范 天津師範 +天津师范大学 天津師範大學 +天渊之别 天淵之別 +天潢贵胄 天潢貴胄 +天然纤维 天然纖維 +天生干 天生幹 +天盟誓表现 天盟誓表現 +天纳克 天納克 +天纵之才 天縱之才 +天缘凑合 天緣湊合 +天缘巧合 天緣巧合 +天缘注定 天緣注定 +天翻地复 天翻地覆 +天花板 天花板 +天表 天表 +天要下雨娘要嫁人 天要下雨孃要嫁人 +天要落雨娘要嫁人 天要落雨孃要嫁人 +天覆地载 天覆地載 +天路历程 天路歷程 +天门冬 天門冬 +天门冬科 天門冬科 +天随人愿 天隨人願 +太乙神针 太乙神針 +太仆 太僕 +太仆寺 太僕寺 +太仆寺卿 太僕寺卿 +太仆寺旗 太僕寺旗 +太公钓鱼愿者上钩 太公釣魚願者上鉤 +太冲 太沖 +太初历 太初曆 +太初历史 太初歷史 +太卜 太卜 +太原师范学院 太原師範學院 +太后 太后 +太咸 太鹹 +太好了 太好了 +太子党 太子黨 +太子舍人 太子舍人 +太干 太乾 +太平御览 太平御覽 +太平洋周边 太平洋週邊 +太平洋周邊 太平洋周邊 +太平洋联合铁路 太平洋聯合鐵路 +太扯了 太扯了 +太松 太鬆 +太极 太極 +太极剑 太極劍 +太极图 太極圖 +太极图说 太極圖說 +太极拳 太極拳 +太极门 太極門 +太皇太后 太皇太后 +太空天文台 太空天文臺 +太空游 太空遊 +太空艺术 太空藝術 +太谷 太谷 +太谷县 太谷縣 +太谷灯 太谷燈 +太阳升 太陽升 +太阳历 太陽曆 +太阳微系统公司 太陽微系統公司 +太阳照在桑干河上 太陽照在桑乾河上 +太阳电池板 太陽電池板 +太阳神计划 太陽神計劃 +太阳系 太陽系 +太阳能板 太陽能板 +太阳谷 太陽谷 +太阳黑子周 太陽黑子周 +太阴历 太陰曆 +太麻里 太麻里 +太麻里乡 太麻里鄉 +太麻里溪 太麻里溪 +夫党 夫黨 +夫力 伕力 +夫妇关系 夫婦關係 +夫妇合璧 夫婦合璧 +夫妇好合 夫婦好合 +夫妻关系 夫妻關係 +夫娘 夫娘 +夫子 夫子 伕子 +夫役 伕役 +夭折 夭折 +央浼营干 央浼營幹 +夯土建筑 夯土建築 +夯干 夯幹 +失之毫厘 失之毫釐 +失之毫厘差之千里 失之毫釐差之千里 +失之毫厘差以千里 失之毫釐差以千里 +失之毫厘谬以千里 失之毫厘謬以千里 +失于 失於 +失体面 失體面 +失信于人 失信於人 +失修 失修 +失准 失準 +失出 失出 +失张失志 失張失志 +失当 失當 +失忆症 失憶症 +失志 失志 +失据 失據 +失智症 失智症 +失眠症 失眠症 +失而复得 失而復得 +失语症 失語症 +失读症 失讀症 +失调症 失調症 +失面子 失面子 +失饥伤饱 失飢傷飽 +头一回 頭一回 +头上打一下脚底板响 頭上打一下腳底板響 +头发 頭髮 +头发上指 頭髮上指 +头发了狂 頭發了狂 +头发了疯 頭發了瘋 +头发壳子 頭髮殼子 +头发胡子一把抓 頭髮鬍子一把抓 +头向前 頭向前 +头回 頭回 +头巾吊在水里 頭巾弔在水裏 +头当 頭當 +头彩 頭彩 +头悬梁锥刺股 頭懸梁錐刺股 +头昏眼暗 頭昏眼暗 +头朝里 頭朝裏 +头板 頭板 +头柜 頭櫃 +头破血出 頭破血出 +头脑发胀 頭腦發脹 +头脑简单四肢发达 頭腦簡單四肢發達 +头花发 頭花髮 +头足愿 頭足願 +头里 頭裏 +头面 頭面 +头面人物 頭面人物 +夷坚志 夷堅志 +夸下海口 誇下海口 +夸丽 夸麗 +夸了 誇了 +夸人 夸人 +夸克 夸克 +夸克星 夸克星 +夸功 誇功 +夸口 誇口 +夸嘴 誇嘴 +夸多斗靡 誇多鬥靡 +夸大 誇大 +夸大不实 誇大不實 +夸大之词 誇大之詞 +夸大其词 誇大其詞 +夸大其辞 誇大其辭 +夸大狂 誇大狂 +夸夸 誇誇 +夸夸其谈 誇誇其談 +夸奖 誇獎 +夸姣 夸姣 +夸官 誇官 +夸容 夸容 +夸尔 夸爾 +夸张 誇張 +夸张其辞 誇張其辭 +夸张法 誇張法 +夸强说会 誇強說會 +夸得 誇得 +夸成 誇成 +夸才卖智 誇才賣智 +夸来夸去 誇來誇去 +夸毗 夸毗 +夸海口 誇海口 +夸父 夸父 +夸父逐日 夸父逐日 +夸特 夸特 +夸示 誇示 +夸耀 誇耀 +夸胜道强 誇勝道強 +夸能斗智 誇能鬥智 +夸脱 夸脫 +夸诞 夸誕 +夸诞不经 夸誕不經 +夸诩 誇詡 +夸说 誇說 +夸赞 誇讚 +夸起 誇起 +夸起来 誇起來 +夸辩 誇辯 +夸过 誇過 +夸饰 誇飾 +夹个 夾個 +夹了 夾了 +夹克 夾克 +夹布子 夾布子 +夹当儿 夾當兒 +夹彩塑 夾彩塑 +夹心饼干 夾心餅乾 +夹板 夾板 +夹板气 夾板氣 +夹板船 夾板船 +夹注 夾註 +夹注号 夾注號 +夺回 奪回 +夺回去 奪回去 +夺回来 奪回來 +夺彩 奪彩 +夺志 奪志 +夺斗 奪鬥 +夺杯 奪盃 +夺眶而出 奪眶而出 +夺门而出 奪門而出 +奇丑 奇醜 +奇丑无比 奇醜無比 +奇冤极枉 奇冤極枉 +奇努克 奇努克 +奇台 奇台 +奇台县 奇臺縣 +奇岩 奇巖 +奇异夸克 奇異夸克 +奇彩 奇彩 +奇志 奇志 +奇才 奇才 +奇才异能 奇才異能 +奇术 奇術 +奇杯 奇盃 +奇袭战术 奇襲戰術 +奇迹 奇蹟 +奇里安 奇里安 +奈向 奈向 +奉公克己 奉公剋己 +奉别 奉別 +奉复 奉復 +奉干 奉干 +奉毂后车 奉轂後車 +奉献出 奉獻出 +奉系 奉系 +奉系军阀 奉系軍閥 +奉苹蘩 奉蘋蘩 +奉药 奉藥 +奉蒸尝 奉蒸嘗 +奉表 奉表 +奉辛比克党 奉辛比克黨 +奋勇向前 奮勇向前 +奋勇当先 奮勇當先 +奋发 奮發 +奋发向上 奮發向上 +奋发图强 奮發圖強 +奋发有为 奮發有爲 +奋发自强 奮發自強 +奋发蹈厉 奮發蹈厲 +奋志 奮志 +奋斗 奮鬥 +奋斗不懈 奮鬥不懈 +奋斗到底 奮鬥到底 +奋斗目标 奮鬥目標 +奎尼匹克 奎尼匹克 +奎松市 奎松市 +奏了 奏了 +奏于 奏於 +奏出 奏出 +奏出去 奏出去 +奏出来 奏出來 +奏折 奏摺 +奏曲 奏曲 +奏表 奏表 +奏鸣曲 奏鳴曲 +奏鸣曲式 奏鳴曲式 +契努克 契努克 +契合 契合 +契合度 契合度 +契合金兰 契合金蘭 +契据 契據 +契沙比克湾 契沙比克灣 +奔出 奔出 +奔向 奔向 +奔回 奔回 +奔回来 奔回來 +奖借 獎借 +奖励制度 獎勵制度 +奖杯 獎盃 +奖赞 獎贊 +套个 套個 +套了 套了 +套出 套出 +套出来 套出來 +套曲 套曲 +套板 套板 +套汇 套匯 +套种 套種 +套装旅游 套裝旅遊 +套问出 套問出 +套马杆 套馬杆 +奢念 奢念 +奥万大 奧萬大 +奥克兰 奧克蘭 +奥克拉荷马 奧克拉荷馬 +奥克拉荷马州 奧克拉荷馬州 +奥克拉荷马市 奧克拉荷馬市 +奥克斯纳德 奧克斯納德 +奥克明 奧克明 +奥克苏斯河 奧克蘇斯河 +奥克蒙 奧克蒙 +奥兰多布鲁 奧蘭多布魯 +奥占 奧佔 +奥士力克 奧士力克 +奥妮克西亚 奧妮克西亞 +奥密克戎 奧密克戎 +奥尔布赖特 奧爾布賴特 +奥布里 奧布里 +奥德修斯 奧德修斯 +奥托 奧托 +奥托瓦兹 奧托瓦茲 +奥拜克 奧拜克 +奥杜伐峡谷 奧杜伐峽谷 +奥林匹克 奧林匹克 +奥林匹克世运 奧林匹克世運 +奥林匹克体育场 奧林匹克體育場 +奥林匹克日 奧林匹克日 +奥林匹克精神 奧林匹克精神 +奥林匹克运动会 奧林匹克運動會 +奥林匹克运动会组织委员会 奧林匹克運動會組織委員會 +奥特朗托 奧特朗托 +奥特朗托海峡 奧特朗托海峽 +奥运团 奧運團 +奥迪修斯 奧迪修斯 +奥里斯 奧里斯 +奥里萨 奧里薩 +奥里萨省 奧里薩省 +奥里萨邦 奧里薩邦 +奥里里亚 奧里里亞 +奥陶系 奧陶系 +奧克拉荷马州 奧克拉荷馬州 +女丑 女丑 +女丑剧场 女丑劇場 +女中豪杰 女中豪傑 +女乳症 女乳症 +女人心海底针 女人心海底針 +女仆 女僕 +女仆部 女僕部 +女佣 女傭 +女佣人 女傭人 +女修士 女修士 +女修道 女修道 +女修道张 女修道張 +女修道院 女修道院 +女同 女同 +女同志 女同志 +女同胞 女同胞 +女大当嫁 女大當嫁 +女大须嫁 女大須嫁 +女娘 女娘 +女子参政主义 女子參政主義 +女子参政权 女子參政權 +女宿舍 女宿舍 +女尸 女屍 +女御 女御 +女性厌恶 女性厭惡 +女性第二性征 女性第二性徵 +女性艺术 女性藝術 +女性艺术史 女性藝術史 +女杰 女傑 +女王杯 女王杯 +女生外向 女生外嚮 +女秀才 女秀才 +女系家族 女系家族 +女舍监 女舍監 +女药头 女藥頭 +女貌郎才 女貌郎才 +女长须嫁 女長須嫁 +奴仆 奴僕 +奴儿干 奴兒干 +奴儿干都司 奴兒干都司 +奴才 奴才 +奴隶制 奴隸制 +奴隶制度 奴隸制度 +奶制品 奶製品 +奶卷 奶捲 +奶妈怀里的孩子 奶媽懷裏的孩子 +奶娘 奶孃 +奸人 奸人 +奸人之雄 奸人之雄 +奸伏 姦伏 +奸佞 奸佞 +奸俏 奸俏 +奸党 奸黨 +奸凶 姦凶 +奸刁 奸刁 +奸匪 奸匪 +奸同鬼蜮 奸同鬼蜮 +奸商 奸商 +奸夫 姦夫 +奸夫淫妇 姦夫淫婦 +奸妇 姦婦 +奸宄 奸宄 +奸官污吏 奸官污吏 +奸尸 姦屍 +奸巧 奸巧 +奸徒 奸徒 +奸恶 奸惡 +奸情 姦情 +奸慝 奸慝 +奸杀 姦殺 +奸棍 奸棍 +奸毒 奸毒 +奸污 姦污 +奸淫 姦淫 +奸淫掳掠 姦淫擄掠 +奸滑 奸滑 +奸狡 奸狡 +奸猾 奸猾 +奸盗邪淫 姦盜邪淫 +奸笑 奸笑 +奸细 奸細 +奸胥猾吏 奸胥猾吏 +奸臣 奸臣 +奸言 奸言 +奸计 奸計 +奸诈 奸詐 +奸诈不级 奸詐不級 +奸谋 奸謀 +奸贼 奸賊 +奸通 姦通 +奸邪 奸邪 +奸险 奸險 +奸雄 奸雄 +奸非 姦非 +奸顽 奸頑 +奸骗 奸騙 +奸黠 奸黠 +她克制 她剋制 +她准知 她準知 +她出 她出 +她出去 她出去 +她出来 她出來 +她念 她念 +好一出 好一齣 +好不了 好不了 +好不容易才 好不容易纔 +好丑 好醜 +好个 好個 +好了 好了 +好了吗 好了嗎 +好了吧 好了吧 +好了歌 好了歌 +好于 好於 +好价 好價 +好体面 好體面 +好傢伙 好傢伙 +好兵帅克 好兵帥克 +好几 好幾 +好几万 好幾萬 +好几个 好幾個 +好几亿 好幾億 +好几十 好幾十 +好几十万 好幾十萬 +好几千 好幾千 +好几千万 好幾千萬 +好几天 好幾天 +好几年 好幾年 +好几百 好幾百 +好几百万 好幾百萬 +好凶 好凶 +好出 好出 +好出风头 好出風頭 +好勇斗狠 好勇鬥狠 +好合 好合 +好向 好向 +好咸 好鹹 +好善嫉恶 好善嫉惡 +好善恶恶 好善惡惡 +好困 好睏 +好困吧 好睏吧 +好困啊 好睏啊 +好多个 好多個 +好好地干 好好地幹 +好好学习天天向上 好好學習天天向上 +好好干 好好幹 +好家伙 好傢伙 +好干 好乾 +好当 好當 +好彩头 好彩頭 +好心倒做了驴肝肺 好心倒做了驢肝肺 +好恶 好惡 +好戏还在后头 好戲還在後頭 +好戏连台 好戲連臺 +好手如云 好手如雲 +好斗 好鬥 +好斗笠 好斗笠 +好斗篷 好斗篷 +好斗胆 好斗膽 +好时是他人恶时是家人 好時是他人惡時是家人 +好暗 好暗 +好极了 好極了 +好死不如恶活 好死不如惡活 +好气万千 好氣萬千 +好汉不怕出身低 好漢不怕出身低 +好汉不论出身低 好漢不論出身低 +好汉做事好汉当 好漢做事好漢當 +好汉只怕病来磨 好漢只怕病來磨 +好生恶杀 好生惡殺 +好立克 好立克 +好签 好籤 +好管闲事 好管閒事 +好胜 好勝 +好胜心 好勝心 +好自矜夸 好自矜誇 +好话当作耳边风 好話當作耳邊風 +好话说尽 好話說盡 +好谀恶直 好諛惡直 +好逸恶劳 好逸惡勞 +好酒贪杯 好酒貪杯 +好采头 好采頭 +好面子 好面子 +如不胜衣 如不勝衣 +如临深谷 如臨深谷 +如临渊谷 如臨淵谷 +如于 如於 +如云 如雲 +如何干 如何幹 +如入云端 如入雲端 +如入宝山空手回 如入寶山空手回 +如出一口 如出一口 +如出一辙 如出一轍 +如同 如同 +如坐云雾 如坐雲霧 +如坐针毡 如坐鍼氈 +如堕五里雾中 如墮五里霧中 +如堕烟雾 如墮煙霧 +如干 如干 +如愿 如願 +如愿以偿 如願以償 +如所周知 如所周知 +如日东升 如日東昇 +如日方升 如日方升 +如有雷同 如有雷同 +如果干 如果幹 +如法泡制 如法泡製 +如法炮制 如法炮製 +如获珍宝 如獲珍寶 +如获至宝 如獲至寶 +如隔三秋 如隔三秋 +如须 如須 +如饥似渴 如飢似渴 +如饥如渴 如飢如渴 +妄念 妄念 +妄想症 妄想症 +妆台 妝臺 +妇人生须 婦人生鬚 +妇女团体 婦女團體 +妇女杂志 婦女雜誌 +妇女联合会 婦女聯合會 +妊娠毒血症 妊娠毒血症 +妖后 妖后 +妖术 妖術 +妖术惑众 妖術惑衆 +妖术邪法 妖術邪法 +妖气冲天 妖氣沖天 +妖精谷 妖精谷 +妖里妖气 妖里妖氣 +妙发灵机 妙發靈機 +妙妙熊历险记 妙妙熊歷險記 +妙手回春 妙手回春 +妙才 妙才 +妙探寻凶 妙探尋兇 +妙曲 妙曲 +妙极 妙極 +妙极了 妙極了 +妙药 妙藥 +妢胡 妢胡 +妥当 妥當 +妥当人 妥當人 +妻党 妻黨 +始于 始於 +始愿 始願 +姌袅 姌嫋 +姑娘 姑娘 +姑娘家 姑娘家 +姑息养奸 姑息養奸 +姑恶 姑惡 +姑苏 姑蘇 +姑表 姑表 +姓岳 姓岳 +姓系 姓系 +委员制 委員制 +委托 委託 +委托书 委託書 +委托人 委託人 +委曲 委曲 +委曲成全 委曲成全 +委曲求全 委曲求全 +委派代表 委派代表 +委罪于人 委罪於人 +姘出去 姘出去 +姚俊杰 姚俊傑 +姚升志 姚昇志 +姚志源 姚志源 +姚采颖 姚采穎 +姜丝 薑絲 +姜丰年 姜豐年 +姜凤君 姜鳳君 +姜切片 薑切片 +姜夔 姜夔 +姜大宇 姜大宇 +姜太公 姜太公 +姜嫄 姜嫄 +姜子牙 姜子牙 +姜家大被 姜家大被 +姜宸英 姜宸英 +姜尚 姜尚 +姜就是老 薑就是老 +姜建铭 姜建銘 +姜愈老愈辣 薑愈老愈辣 +姜文杰 姜文杰 +姜是老 薑是老 +姜是老的辣 薑是老的辣 +姜末 薑末 +姜桂 薑桂 +姜桂老辣 薑桂老辣 +姜母 薑母 +姜母鸭 薑母鴨 +姜汁 薑汁 +姜汤 薑湯 +姜沧源 姜滄源 +姜片 薑片 +姜石年 薑石年 +姜竹祥 姜竹祥 +姜糖 薑糖 +姜维 姜維 +姜维平 姜維平 +姜老辣 薑老辣 +姜至奂 姜至奐 +姜芋 姜芋 +姜茶 薑茶 +姜蓉 薑蓉 +姜越老越辣 薑越老越辣 +姜辣 薑辣 +姜辣素 薑辣素 +姜还是老 薑還是老 +姜还是老的辣 薑還是老的辣 +姜远珍 姜遠珍 +姜郁美 姜郁美 +姜金龙 姜金龍 +姜锡柱 姜錫柱 +姜饼 薑餅 +姜麻园 薑麻園 +姜黄 薑黃 +姜黄素 薑黃素 +姜黄色 薑黃色 +姨娘 姨娘 姨孃 +姨表 姨表 +姬姜 姬姜 +姬松茸 姬松茸 +姱修 姱修 +姻党 姻黨 +姻戚 姻戚 +姿采 姿采 +威克岛 威克島 +威克斯 威克斯 +威克森 威克森 +威克菲尔 威克菲爾 +威克菲尔德 威克菲爾德 +威奇托 威奇托 +威宁彝族回族苗族自治县 威寧彝族回族苗族自治縣 +威尔生氏症 威爾生氏症 +威布里吉 威布里吉 +威廉亚历山大 威廉亞歷山大 +威比苏诺 威比蘇諾 +威氏注音法 威氏註音法 +威里斯 威里斯 +威风八面 威風八面 +娇娘 嬌娘 +娇嫩欲滴 嬌嫩欲滴 +娇艳欲滴 嬌豔欲滴 +娇袅不胜 嬌嫋不勝 +娘亲 孃親 +娘儿 孃兒 +娘儿们 娘兒們 +娘儿俩 孃兒倆 +娘姨 孃姨 +娘娘 娘娘 +娘娘庙 娘娘廟 +娘娘腔 娘娘腔 +娘婆 娘婆 +娘子 娘子 +娘子关 娘子關 +娘子军 娘子軍 +娘家 孃家 +娘家姓 孃家姓 +娘母子 娘母子 +娘的 孃的 +娘老子 孃老子 +娘胎 孃胎 +娘舅 孃舅 +娩出 娩出 +娱乐台 娛樂臺 +娴于 嫺於 +娴于辞令 嫺於辭令 +娶了 娶了 +娶回 娶回 +娶回家 娶回家 +婆娘 婆娘 +婆娘子 婆娘子 +婚前婚后 婚前婚後 +婚后 婚後 +婚姻制度 婚姻制度 +婢仆 婢僕 +婴儿猝死症候群 嬰兒猝死症候羣 +婴儿猝死综合症 嬰兒猝死綜合症 +婶娘 嬸孃 +媒人口无量斗 媒人口無量斗 +媒人婆迷了路 媒人婆迷了路 +媒体狂并潮 媒體狂併潮 +媒体访问控制 媒體訪問控制 +媒合 媒合 +媚如秋月 媚如秋月 +媮合苟容 媮合苟容 +媵御 媵御 +嫁个 嫁個 +嫁个老公 嫁個老公 +嫁了 嫁了 +嫁于 嫁於 +嫁出 嫁出 +嫁出去 嫁出去 +嫁出来 嫁出來 +嫁祸于 嫁禍於 +嫁祸于人 嫁禍於人 +嫉恶 嫉惡 +嫉恶好善 嫉惡好善 +嫉恶如仇 嫉惡如仇 +嫉恶若仇 嫉惡若仇 +嫌凶 嫌兇 +嫌好道丑 嫌好道醜 +嫌好道恶 嫌好道惡 +嫌恶 嫌惡 +嫔御 嬪御 +嫠忧宗周 嫠憂宗周 +嫡出 嫡出 +嫡系 嫡系 +嫩叶 嫩葉 +嬉游 嬉遊 +嬖幸 嬖倖 +嬴余 嬴餘 +子之丰兮 子之丰兮 +子云 子云 +子叶 子葉 +子姜炒鸡 子薑炒雞 +子孙娘娘 子孫娘娘 +子宫內膜异位症 子宮內膜異位症 +子曰诗云 子曰詩云 +子杰 子傑 +子母钟 子母鐘 +子游 子游 +子癫前症 子癲前症 +子目录 子目錄 +子系统 子系統 +子集合 子集合 +孔明借箭 孔明借箭 +孔章望斗 孔章望斗 +孔虫 孔蟲 +孕出 孕出 +孕穗 孕穗 +孕穗期 孕穗期 +孕育出 孕育出 +孖虫 孖蟲 +字据 字據 +字根合体字 字根合體字 +字根表 字根表 +字母表 字母表 +字汇 字彙 +字汇判断任务 字彙判斷任務 +字码表 字碼表 +字表 字表 +字表输入法 字表輸入法 +字里 字裏 +字里行间 字裏行間 +字面 字面 +字面上 字面上 +字面意义 字面意義 +字面意思 字面意思 +字面解释 字面解釋 +字音表 字音表 +存于 存於 +存亡之秋 存亡之秋 +存亡未卜 存亡未卜 +存十一于千百 存十一於千百 +存录 存錄 +存念 存念 +存托凭证 存託憑證 +存托股 存托股 +存扣 存扣 +存折 存摺 +存款准备率 存款準備率 +存款准备金 存款準備金 +存款准备金率 存款準備金率 +孙协志 孫協志 +孙大千 孫大千 +孙杰 孫杰 +孙胜男 孫勝男 +孝布 孝布 +孝重千斤日减一斤 孝重千斤日減一斤 +孟什维克 孟什維克 +孟冬 孟冬 +孟姜女 孟姜女 +孟小冬 孟小冬 +孟尝君 孟嘗君 +孟德尔松 孟德爾松 +孟村回族自治县 孟村回族自治縣 +孟秋 孟秋 +孟获 孟獲 +孢子叶 孢子葉 +孢子虫 孢子蟲 +季冬 季冬 +季后 季後 +季后赛 季後賽 +季咸 季咸 +季布 季布 +季布一诺 季布一諾 +季札挂剑 季札掛劍 +季瑟雅克 季瑟雅克 +季秋 季秋 +季节洄游 季節洄游 +孤云 孤雲 +孤云野鹤 孤雲野鶴 +孤儿药 孤兒藥 +孤军奋斗 孤軍奮鬥 +孤凄 孤悽 +孤寡不谷 孤寡不穀 +孤形只影 孤形隻影 +孤形吊影 孤形吊影 +孤征 孤征 +孤拐 孤拐 +孤拐面 孤拐面 +孤注 孤注 +孤注一掷 孤注一擲 +孤涂 孤塗 +孤游 孤遊 +孤灯挑尽 孤燈挑盡 +孤身只影 孤身隻影 +孤辰合注 孤辰合注 +学习团 學習團 +学习曲线 學習曲線 +学习范例 學習範例 +学习计划 學習計劃 +学了个不留 學了個不留 +学以致用 學以致用 +学优才赡 學優才贍 +学位制度 學位制度 +学分制 學分制 +学制 學制 +学力鉴定 學力鑑定 +学区制 學區制 +学历 學歷 學曆 +学台 學臺 +学名药 學名藥 +学员制 學員制 +学回 學回 +学回去 學回去 +学回来 學回來 +学士后医学系 學士後醫學系 +学徒制 學徒制 +学术 學術 +学术团体 學術團體 +学术奖 學術獎 +学术性 學術性 +学术报告 學術報告 +学术文化团体 學術文化團體 +学术水平 學術水平 +学术界 學術界 +学术研究 學術研究 +学术科 學術科 +学术组 學術組 +学术网路 學術網路 +学术自由 學術自由 +学校同学 學校同學 +学浅才疏 學淺才疏 +学然后知不足 學然後知不足 +学疏才浅 學疏才淺 +学籍表 學籍表 +学系 學系 +学经历 學經歷 +学舍 學舍 +学里 學裏 +学长制 學長制 +孩儿面 孩兒面 +孬种 孬種 +孱困 孱困 +孵出 孵出 +孵出来 孵出來 +孽党 孽黨 +孽种 孽種 +孽障种子 孽障種子 +宁个 寧個 +宁中则 甯中則 +宁夏回族 寧夏回族 +宁夏回族自治区 寧夏回族自治區 +宁庄子 甯莊子 +宁当玉碎 寧當玉碎 +宁悼子 甯悼子 +宁惠子 甯惠子 +宁愿 寧願 +宁成子 甯成子 +宁戚 甯戚 +宁折不弯 寧折不彎 +宁撞金钟一下不打破鼓三千 寧撞金鐘一下不打破鼓三千 +宁斧成 甯斧成 +宁武子 甯武子 +宁浩 甯浩 +宁猛力 甯猛力 +宁调元 甯調元 +宁越 甯越 +宁静致远 寧靜致遠 +宅舍 宅舍 +宇宙云 宇宙雲 +宇宙志 宇宙誌 +守先待后 守先待後 +守制 守制 +守御 守禦 +守正不回 守正不回 +守舍 守舍 +守节不回 守節不回 +安万特 安萬特 +安了 安了 +安于 安於 +安于一隅 安於一隅 +安于泰山 安於泰山 +安于现状 安於現狀 +安仁鬓秋 安仁鬢秋 +安克拉 安克拉 +安克拉治 安克拉治 +安克雷奇 安克雷奇 +安全系数 安全係數 +安全系统 安全系統 +安全防范 安全防範 +安可曲 安可曲 +安吉里科 安吉里科 +安地卡及巴布达 安地卡及巴布達 +安山岩 安山岩 +安布罗斯 安布羅斯 +安席克 安席克 +安扎 安扎 +安提瓜和巴布达 安提瓜和巴布達 +安杰 安傑 +安格鲁萨克逊 安格魯薩克遜 +安步当车 安步當車 +安沈铁路 安瀋鐵路 +安眠药 安眠藥 +安纳托利亚 安納托利亞 +安胎药 安胎藥 +安胡 安胡 +安营扎寨 安營紮寨 +安萨里 安薩里 +安适 安適 +安适如常 安適如常 +安钦云 安欽雲 +安闲惬意 安閒愜意 +安闲自在 安閒自在 +安闲自得 安閒自得 +安闲舒适 安閒舒適 +安闲随意 安閒隨意 +安魂弥撒 安魂彌撒 +安魂曲 安魂曲 +宋三彩 宋三彩 +宋亨欣叶纯豪 宋亨欣葉純豪 +宋克 宋克 +宋板 宋板 +完了 完了 +完全叶 完全葉 +完全愈复 完全癒復 +完全正确 完全正確 +完工后 完工後 +完成后 完成後 +完税价格 完稅價格 +完税货价 完稅貨價 +宏志 宏志 +宏愿 宏願 +宏碁集团 宏碁集團 +宗周 宗周 +宗周钟 宗周鐘 +宗教团 宗教團 +宗教团体 宗教團體 +宗法制度 宗法制度 +官不怕大只怕管 官不怕大只怕管 +官价 官價 +官兵一致 官兵一致 +官准 官准 +官制 官制 +官卷 官卷 +官历 官曆 +官台木 官臺木 +官商合办 官商合辦 +官地为采 官地爲寀 +官复原职 官復原職 +官庄 官莊 +官念 官念 +官报私仇 官報私仇 +官料药 官料藥 +官板儿 官板兒 +官私合营 官私合營 +官能团 官能團 +官能症 官能症 +官舍 官舍 +官面 官面 +官面儿 官面兒 +定义出 定義出 +定义范围 定義範圍 +定了 定了 +定于 定於 +定于一尊 定於一尊 +定价 定價 +定准 定準 +定出 定出 +定出来 定出來 +定制 定製 +定制化 定製化 +定南针 定南針 +定向 定向 +定向培育 定向培育 +定向天线 定向天線 +定向越野 定向越野 +定弦 定弦 +定当 定當 +定时号志 定時號誌 +定时钟 定時鐘 +定点厂 定點廠 +定碳杯 定碳杯 +定胜败 定勝敗 +定范围 定範圍 +定计划 定計劃 +定都于 定都於 +定风针 定風針 +宛若游龙 宛若游龍 +宛转周折 宛轉周折 +宜丰 宜豐 +宜丰县 宜豐縣 +宜于 宜於 +宜云 宜云 +宝丰 寶丰 寶豐 +宝丰县 寶豐縣 +宝卷 寶卷 +宝历 寶曆 +宝山庄 寶山莊 +宝山空回 寶山空回 +宝庄 寶莊 +宝志 寶誌 +宝胄 寶胄 +宝贝团 寶貝團 +宝里宝气 寶里寶氣 +宝鉴 寶鑑 +宝钢集团 寶鋼集團 +实业计划 實業計劃 +实价 實價 +实况录影 實況錄影 +实况录音 實況錄音 +实发 實發 +实名制 實名制 +实干 實幹 +实干家 實幹家 +实录 實錄 +实才 實才 +实据 實據 +实时技术 實時技術 +实用价值 實用價值 +实症 實症 +实质面 實質面 +实践是检验真理的唯一标准 實踐是檢驗真理的唯一標準 +实际范围 實際範圍 +宠幸 寵幸 +审囚刷卷 審囚刷卷 +审干 審幹 +审曲面势 審曲面勢 +审核 審覈 +审级制度 審級制度 +审计范围 審計範圍 +客串演出 客串演出 +客制化 客製化 +客制化服务 客製化服務 +客如云集 客如雲集 +客游 客遊 +客舍 客舍 +宣传周 宣傳週 +宣卷 宣卷 +宣室志 宣室志 +宣布 宣佈 +宣布无效 宣佈無效 +宣布独立 宣佈獨立 +宣布破产 宣佈破產 +宣誓代表 宣誓代表 +宣赞 宣贊 +室內乐团 室內樂團 +室里 室裏 +宦游 宦遊 +宪台 憲臺 +宫里 宮裏 +宫里蓝 宮里藍 +宰制 宰制 +害于 害於 +害人虫 害人蟲 +害发 害發 +害虫 害蟲 +宴游 宴遊 +宵征 宵征 +家丑 家醜 +家丑不可外传 家醜不可外傳 +家丑不可外传流言切莫轻信 家醜不可外傳流言切莫輕信 +家丑不可外扬 家醜不可外揚 +家什 傢什 +家仆 家僕 +家伙 傢伙 +家佣 家傭 +家俱 傢俱 +家具 傢俱 +家具行 傢俱行 +家制 家制 +家和万事兴 家和萬事興 +家山药 家山藥 +家庄 家莊 +家庭制度 家庭制度 +家庭计划 家庭計劃 +家当 家當 +家政系 家政系 +家族同盟 家族同盟 +家无斗储 家無斗儲 +家机布 家機布 +家私 傢俬 +家种 家種 +家系 家系 +家财万贯 家財萬貫 +家赀万贯 家貲萬貫 +家里 家裏 +家里的 家裏的 +家长制 家長制 +家长里短 家長裏短 +家门不幸 家門不幸 +家门有幸 家門有幸 +宸极 宸極 +宸游 宸遊 +容于 容於 +容光焕发 容光煥發 +容克 容克 +容后说明 容後說明 +容容多后福 容容多後福 +容幸 容幸 +容范 容範 +容表 容表 +宽了 寬了 +宽于 寬於 +宽余 寬餘 +宽宽松松 寬寬鬆鬆 +宽打周遭 寬打周遭 +宽松 寬鬆 +宽泛 寬泛 +宾主关系 賓主關係 +宾主尽欢 賓主盡歡 +宾客如云 賓客如雲 +宾语关系从句 賓語關係從句 +宿仇 宿仇 +宿志 宿志 +宿愿 宿願 +宿愿已偿 宿願已償 +宿愿得偿 宿願得償 +宿松 宿松 +宿松县 宿松縣 +宿舍 宿舍 +宿舍区 宿舍區 +宿舍楼 宿舍樓 +宿舍网路 宿舍網路 +宿舍费 宿舍費 +寂历 寂歷 +寄了 寄了 +寄于 寄於 +寄出 寄出 +寄出去 寄出去 +寄发 寄發 +寄回 寄回 +寄托 寄託 +寄托在 寄託在 +寄托着 寄託着 +寄挂号 寄掛號 +寄挂号信 寄掛號信 +寄生昆虫 寄生昆蟲 +寄生虫 寄生蟲 +寄生虫病 寄生蟲病 +寅台 寅臺 +密云 密雲 +密云不雨 密雲不雨 +密云县 密雲縣 +密仑 密侖 +密克罗尼西亚 密克羅尼西亞 +密切关系 密切關係 +密切注意 密切注意 +密切注视 密切注視 +密合 密合 +密密扎扎 密密扎扎 +密布 密佈 +密折 密摺 +密致 密緻 +密苏里 密蘇里 +密苏里州 密蘇里州 +密苏里河 密蘇里河 +寇仇 寇仇 +寇准 寇準 +富于 富於 +富于想像 富於想像 +富余 富餘 +富兰克林 富蘭克林 +富富有余 富富有餘 +富布赖特 富布賴特 +富春秋 富春秋 +富维克 富維克 +富色彩 富色彩 +富贵如浮云 富貴如浮雲 +富贵浮云 富貴浮雲 +富里 富里 +富里乡 富里鄉 +寒于 寒於 +寒假里 寒假裏 +寒冬 寒冬 +寒冬腊月 寒冬臘月 +寒号虫 寒號蟲 +寒暑表 寒暑表 +寒栗 寒慄 +寒武系 寒武系 +寒波荡漾 寒波盪漾 +寒症 寒症 +寒秋 寒秋 +寒舍 寒舍 +寓于 寓於 +寓兵于农 寓兵於農 +寓教于乐 寓教於樂 +寓禁于征 寓禁於征 +寝丘之志 寢丘之志 +察合台 察合臺 +察合台汗国 察合臺汗國 +察布查尔 察布查爾 +察布查尔县 察布查爾縣 +察核 察覈 +察觉出 察覺出 +寡占 寡佔 +寡合 寡合 +寡欲 寡慾 +寥寥无几 寥寥無幾 +寮采 寮寀 +寸丝不挂 寸絲不掛 +寸发千金 寸髮千金 +对了 對了 +对了槛儿 對了檻兒 +对于 對於 +对偶多面体 對偶多面體 +对冲 對衝 +对冲基金 對衝基金 +对准 對準 +对准目标 對準目標 +对准表 對準錶 +对准钟 對準鐘 +对准钟表 對準鐘錶 +对华发动 對華發動 +对台 對臺 +对台戏 對臺戲 +对合 對合 +对合起来 對合起來 +对外关系 對外關係 +对天发誓 對天發誓 +对妻失语症 對妻失語症 +对对胡 對對胡 +对当 對當 +对折 對摺 +对数表 對數表 +对杯 對杯 +对流云系 對流雲系 +对焦范围 對焦範圍 +对照表 對照表 +对症 對症 +对症下药 對症下藥 +对症发药 對症發藥 +对着干 對着幹 +对立面 對立面 +对等关系 對等關係 +对表 對錶 +对酒当歌 對酒當歌 +对针 對針 +对面 對面 +对面不见人 對面不見人 +寺舍 寺舍 +寺钟 寺鐘 +寻仇 尋仇 +寻出 尋出 +寻回 尋回 +寻幽探胜 尋幽探勝 +寻幽访胜 尋幽訪勝 +寻找出 尋找出 +寻找出来 尋找出來 +寻来范畴 尋來範疇 +寻求出来 尋求出來 +寻甸回族彝族自治县 尋甸回族彝族自治縣 +寻获 尋獲 +导出 導出 +导出值 導出值 +导出去 導出去 +导出来 導出來 +导向 導向 +导向型 導向型 +导向思考 導向思考 +导向飞弹 導向飛彈 +导向鱼雷 導向魚雷 +导师制 導師制 +导引之术 導引之術 +导引系统 導引系統 +导弹武器技术控制制度 導彈武器技術控制制度 +导弹系统 導彈系統 +导板 導板 +导气之术 導氣之術 +导流板 導流板 +导游 導遊 +导生制 導生制 +导致 導致 +导致死亡 導致死亡 +导航系统 導航系統 +导风板 導風板 +寿丰 壽豐 +寿丰乡 壽豐鄉 +寿数已尽 壽數已盡 +寿险责任准备金 壽險責任準備金 +寿面 壽麪 +封个 封個 +封了 封了 +封了火 封了火 +封侯万里 封侯萬里 +封台 封臺 +封后 封后 +封妻荫子 封妻廕子 +封官许愿 封官許願 +封建制度 封建制度 +封建割据 封建割據 +封弥 封彌 +封杀出局 封殺出局 +封檐板 封檐板 +封胡羯末 封胡羯末 +封胡遏末 封胡遏末 +封蜡 封蠟 +封里 封裏 +封面 封面 +封面人物 封面人物 +封面女郎 封面女郎 +封面设计 封面設計 +封面里 封面裏 +射不出 射不出 +射了 射了 +射准 射準 +射出 射出 +射出去 射出去 +射出来 射出來 +射向 射向 +射回 射回 +射回去 射回去 +射回来 射回來 +射复 射覆 +射干 射干 +射影几何 射影幾何 +射影几何学 射影幾何學 +射雕 射鵰 +射雕手 射鵰手 +射雕英雄传 射鵰英雄傳 +射频干扰 射頻干擾 +射频识别 射頻識別 +将于 將於 +将假当真 將假當真 +将出 將出 +将出去 將出去 +将出来 將出來 +将功折罪 將功折罪 +将功折过 將功折過 +将占 將佔 +将占卜 將占卜 +将回 將回 +将回到 將回到 +将回去 將回去 +将回来 將回來 +将尽 將盡 +将尽未尽 將盡未盡 +将才 將才 +将无同 將無同 +将相本无种 將相本無種 +将遇良才 將遇良才 +将门之后 將門之後 +小丑 小丑 小醜 +小丑丫鬟 小醜丫鬟 +小丑跳梁 小醜跳樑 +小丑鱼 小丑魚 +小业种 小業種 +小个 小個 +小个子 小個子 +小丰满发电厂 小豐滿發電廠 +小了 小了 +小于 小於 +小云 小云 +小人得志 小人得志 +小仆 小僕 +小价 小价 +小众艺术 小衆藝術 +小伙 小夥 +小伙子 小夥子 +小伙计 小夥計 +小余 小余 +小便斗 小便斗 +小修 小修 +小傢伙 小傢伙 +小儿麻痹症 小兒麻痹症 +小克 小克 +小冬 小冬 +小冲突 小衝突 +小几 小几 +小划子 小划子 +小别 小別 +小千 小千 +小千世界 小千世界 +小升 小升 +小卷 小卷 小捲 +小厂 小廠 +小发 小發 +小发财 小發財 +小只 小隻 +小叮当 小叮噹 +小叶 小葉 +小同乡 小同鄉 +小后生 小後生 +小周天 小週天 +小回 小回 +小场面 小場面 +小型柜橱 小型櫃櫥 +小型钟 小型鐘 +小型钟表 小型鐘錶 +小型钟表面 小型鐘表面 +小型钟面 小型鐘面 +小夜曲 小夜曲 +小天后 小天后 +小姑娘 小姑娘 +小姑娘树 小姑娘樹 +小娘 小娘 +小娘子 小娘子 +小子后生 小子後生 +小尝 小嚐 +小尽 小盡 +小岩洞 小巖洞 +小布 小布 +小庵 小庵 +小廉曲谨 小廉曲謹 +小志 小志 +小念 小念 +小恶魔 小惡魔 +小才大用 小才大用 +小才子 小才子 +小挂儿 小掛兒 +小插曲 小插曲 +小时了了 小時了了 +小曲 小曲 +小朱 小朱 +小杯 小杯 +小杰 小杰 +小松 小松 +小松糕 小鬆糕 +小板 小板 +小柜子 小櫃子 +小栗旬 小栗旬 +小步舞曲 小步舞曲 +小毛虫 小毛蟲 +小池百合子 小池百合子 +小注 小注 +小泽征尔 小澤征爾 +小游 小遊 +小瀑布 小瀑布 +小班制 小班制 +小秀才 小秀才 +小秀才学堂 小秀才學堂 +小秋 小秋 +小秋收 小秋收 +小种 小種 +小穗 小穗 +小筑 小築 +小米面 小米麪 +小系 小系 +小红萝卜 小紅蘿蔔 +小老板 小老闆 +小胜 小勝 +小胜利 小勝利 +小胡子 小鬍子 +小胡桃 小胡桃 +小脏鬼 小髒鬼 +小舍人 小舍人 +小花远志 小花遠志 +小苏 小蘇 +小苏打 小蘇打 +小苏打粉 小蘇打粉 +小苹果 小蘋果 +小范 小范 +小范围 小範圍 +小萝卜头 小蘿蔔頭 +小虫 小蟲 +小虫子 小蟲子 +小行板 小行板 +小表哥 小表哥 +小表妹 小表妹 +小表姊 小表姊 +小表嫂 小表嫂 +小表弟 小表弟 +小里小气 小裏小氣 +小针 小針 +小针美容 小針美容 +小钟 小鐘 +小铲 小鏟 +小铲子 小鏟子 +小集团 小集團 +小面包 小麪包 +小须鲸 小鬚鯨 +小鬼当家 小鬼當家 +少不了 少不了 +少了 少了 +少于 少於 +少冲 少衝 +少出 少出 +少占 少佔 +少吊 少吊 +少年才俊 少年才俊 +少惹闲事 少惹閒事 +少扣 少扣 +少掌柜 少掌櫃 +少数党 少數黨 +少杰 少傑 +少私寡欲 少私寡慾 +少管闲事 少管閒事 +少说几句 少說幾句 +少采 少採 +尔冬升 爾冬升 +尔后 爾後 +尔当 爾當 +尔本周 爾本週 +尖团 尖團 +尖团字 尖團字 +尖团音 尖團音 +尖扎 尖扎 +尖扎县 尖扎縣 +尖管面 尖管麪 +尖裂叶 尖裂葉 +尘卷风 塵捲風 +尘肺症 塵肺症 +尘芥虫 塵芥蟲 +尘表 塵表 +尘襟尽涤 塵襟盡滌 +尘饭涂羹 塵飯塗羹 +尚余 尚餘 +尚志 尚志 +尚志市 尚志市 +尚慕杰 尚慕傑 +尚须 尚須 +尝个 嚐個 +尝了 嚐了 +尝了一口 嚐了一口 +尝了尝 嚐了嚐 +尝了鲜 嚐了鮮 +尝出 嚐出 +尝到 嚐到 +尝尝 嚐嚐 +尝尝鲜 嚐嚐鮮 +尝尽 嚐盡 +尝巧 嘗巧 +尝敌 嘗敵 +尝新 嘗新 +尝来尝去 嚐來嚐去 +尝汤戏 嘗湯戲 +尝点 嚐點 +尝甜头 嘗甜頭 +尝胆 嘗膽 +尝胆臥薪 嘗膽臥薪 +尝膳 嘗膳 +尝草 嘗草 +尝药 嘗藥 +尝试 嘗試 +尝试性 嘗試性 +尝试错误学习 嘗試錯誤學習 +尝起来 嚐起來 +尝遍 嚐遍 +尝酸 嘗酸 +尝鲜 嚐鮮 +尝鼎一脔 嘗鼎一臠 +尤云殢雨 尤雲殢雨 +尤克利斯 尤克利斯 +尤克勒斯 尤克勒斯 +尤克斯 尤克斯 +尤克里斯 尤克里斯 +尤克里里琴 尤克裏裏琴 +尤基里斯 尤基里斯 +尤班克斯 尤班克斯 +尤秋兴 尤秋興 +尤里 尤里 +尤里斯伊文思 尤里斯伊文思 +尤里比底斯 尤里比底斯 +尤里西斯 尤里西斯 +尤须 尤須 +尧布 堯布 +尨眉皓发 尨眉皓髮 +就于 就於 +就克制 就剋制 +就出 就出 +就出去 就出去 +就出来 就出來 +就吃干 就吃乾 +就回 就回 +就回去 就回去 +就回来 就回來 +就干一 就幹一 +就干一杯 就乾一杯 +就干吧 就幹吧 +就干淨 就乾淨 +就当 就當 +就当作 就當作 +就念 就念 +就扣 就扣 +就拿出 就拿出 +就日瞻云 就日瞻雲 +就是了 就是了 +就晚了 就晚了 +就汤下面 就湯下麪 +就系 就係 +就范 就範 +就读于 就讀於 +尸位 尸位 +尸位素餐 尸位素餐 +尸体 屍體 +尸体剖检 屍體剖檢 +尸体袋 屍體袋 +尸体解剖 屍體解剖 +尸僵 屍僵 +尸利 尸利 +尸变 屍變 +尸块 屍塊 +尸居余气 尸居餘氣 +尸居龙见 尸居龍見 +尸山血海 屍山血海 +尸斑 屍斑 +尸格 屍格 +尸检 屍檢 +尸榇 屍櫬 +尸横遍野 屍橫遍野 +尸灵 屍靈 +尸祝 尸祝 +尸祝代庖 尸祝代庖 +尸禄 尸祿 +尸禄素餐 尸祿素餐 +尸臣 尸臣 +尸蜡 屍蠟 +尸衣 屍衣 +尸解 尸解 +尸谏 尸諫 +尸身 屍身 尸身 +尸陀林 尸陀林 +尸饔 尸饔 +尸首 屍首 +尸骨 屍骨 +尸骨无存 屍骨無存 +尸骨早寒 屍骨早寒 +尸骨未寒 屍骨未寒 +尸骸 屍骸 +尸鸠 尸鳩 +尹秋君 尹秋君 +尹邢避面 尹邢避面 +尺二秀才 尺二秀才 +尺寸千里 尺寸千里 +尺寸斗粟 尺寸斗粟 +尺布斗粟 尺布斗粟 +尺幅千里 尺幅千里 +尺板 尺板 +尺板斗食 尺板斗食 +尼亚加拉瀑布 尼亞加拉瀑布 +尼克 尼克 +尼克劳斯 尼克勞斯 +尼克松 尼克松 +尼克森 尼克森 +尼克洛 尼克洛 +尼克队 尼克隊 +尼加拉瀑布 尼加拉瀑布 +尼勒克 尼勒克 +尼勒克县 尼勒克縣 +尼坛 尼壇 +尼坦雅胡 尼坦雅胡 +尼姑庵 尼姑庵 +尼布楚条约 尼布楚條約 +尼布甲尼撒 尼布甲尼撒 +尼庵 尼庵 +尼采 尼采 +尼龙布 尼龍佈 +尽世 盡世 +尽义务 盡義務 +尽了 盡了 +尽些 盡些 +尽享 盡享 +尽人 盡人 +尽人事 盡人事 +尽人情 盡人情 +尽人皆知 盡人皆知 +尽付东流 盡付東流 +尽付阙如 盡付闕如 +尽信 盡信 +尽做 盡做 +尽做坏事 盡做壞事 +尽先 儘先 +尽全力 盡全力 +尽全心 盡全心 +尽兴 盡興 +尽兴而归 盡興而歸 +尽其 盡其 +尽其在我 盡其在我 +尽其所有 盡其所有 +尽其所能 盡其所能 +尽其所长 盡其所長 +尽出 盡出 +尽到 盡到 +尽力 盡力 +尽力去做 盡力去做 +尽力而为 盡力而爲 +尽去 盡去 +尽可 儘可 +尽可能 儘可能 +尽命 盡命 +尽善 盡善 +尽善尽美 盡善盡美 +尽在 盡在 +尽场儿 盡場兒 +尽够 儘夠 +尽失 盡失 +尽头 盡頭 +尽头话 盡頭話 +尽如 盡如 +尽如人意 盡如人意 +尽子 儘子 +尽孝 盡孝 +尽尽 儘儘 +尽展所长 盡展所長 +尽属 盡屬 +尽己 盡己 +尽带 盡帶 +尽席 盡席 +尽年 盡年 +尽底下 儘底下 +尽得 盡得 +尽心 盡心 +尽心图报 盡心圖報 +尽心尽力 盡心盡力 +尽心竭力 盡心竭力 +尽心竭诚 盡心竭誠 +尽忠 盡忠 +尽忠报国 盡忠報國 +尽忠竭力 盡忠竭力 +尽忠职守 盡忠職守 +尽快 儘快 +尽快地 儘快地 +尽态极妍 盡態極妍 +尽性 儘性 +尽情 盡情 +尽情吐露 盡情吐露 +尽情尽理 盡情盡理 +尽情欢乐 盡情歡樂 +尽情欢唱 盡情歡唱 +尽情歌唱 盡情歌唱 +尽情玩乐 盡情玩樂 +尽想 儘想 +尽意 盡意 +尽意随心 儘意隨心 +尽收 盡收 +尽收眼底 盡收眼底 +尽教 儘教 +尽散 盡散 +尽数 盡數 +尽日 盡日 +尽日穷夜 盡日窮夜 +尽早 儘早 +尽是 盡是 +尽有可能 儘有可能 +尽本分 盡本分 +尽欢 盡歡 +尽欢而散 盡歡而散 +尽然 盡然 +尽瘁 盡瘁 +尽瘁鞠躬 盡瘁鞠躬 +尽皆 盡皆 +尽盘将军 盡盤將軍 +尽礼 盡禮 +尽端 盡端 +尽管 儘管 +尽管如此 儘管如此 +尽美尽善 盡美盡善 +尽职 盡職 +尽职尽责 盡職盡責 +尽能 盡能 +尽自 儘自 +尽致 盡致 +尽节 盡節 +尽节竭诚 盡節竭誠 +尽若 盡若 +尽落尾 儘落尾 +尽要 盡要 +尽览 盡覽 +尽言 盡言 +尽让 儘讓 +尽诚竭节 盡誠竭節 +尽读 盡讀 +尽责 盡責 +尽责任 盡責任 +尽述 盡述 +尽速 儘速 +尽释前嫌 盡釋前嫌 +尽里 儘裏 +尽量 儘量 +尽铅华 盡鉛華 +尾注 尾註 +尾身幸次 尾身幸次 +尿崩症 尿崩症 +尿布 尿布 +尿布台 尿布臺 +尿布疹 尿布疹 +尿斗 尿斗 +尿杯 尿杯 +尿毒症 尿毒症 +局促 侷促 +局里 局裏 +局限 侷限 +局限于 侷限於 +局面 局面 +层云 層雲 +层出 層出 +层出不穷 層出不窮 +层出叠见 層出疊見 +层压板 層壓板 +层台 層臺 +层积云 層積雲 +层见叠出 層見疊出 +层见迭出 層見迭出 +层面 層面 +居于 居於 +居住于 居住於 +居心险恶 居心險惡 +居里 居里 +居里夫人 居里夫人 +屈一伸万 屈一伸萬 +屈万里 屈萬里 +屈就于 屈就於 +屈志 屈志 +屈才 屈才 +屈折 屈折 +屈折语 屈折語 +屈曲 屈曲 +屈服于 屈服於 +屋子里 屋子裏 +屋梁 屋樑 +屋舍 屋舍 +屋里 屋裏 +屋里人 屋裏人 +屋里的 屋裏的 +屋面 屋面 +屋面板 屋面板 +屋面瓦 屋面瓦 +屏当 屏當 +屏极 屏極 +屏风后 屏風後 +屑于 屑於 +展出 展出 +展卷 展卷 +展台 展臺 +展团 展團 +展布 展布 +展才 展才 +展现出 展現出 +展现出来 展現出來 +展示出 展示出 +展示柜 展示櫃 +展采 展采 +展露出 展露出 +展露出来 展露出來 +属于 屬於 +属于偶 屬於偶 +属意于 屬意於 +属托 屬託 +属毛离里 屬毛離裏 +屠苏 屠蘇 +屠苏酒 屠蘇酒 +屡仆屡起 屢仆屢起 +屡出新招 屢出新招 +屡出狂言 屢出狂言 +屡战屡胜 屢戰屢勝 +屡顾尔仆 屢顧爾僕 +履历 履歷 +履历片 履歷片 +履历表 履歷表 +屯扎 屯紮 +屯田制 屯田制 +屯里 屯裏 +山中无历日 山中無曆日 +山中白云 山中白雲 +山仔后 山仔后 +山前山后 山前山後 +山后 山後 +山向 山向 +山地同胞 山地同胞 +山地管制区 山地管制區 +山地管制游览区 山地管制遊覽區 +山岩 山岩 +山岳 山嶽 +山崩钟应 山崩鐘應 +山庄 山莊 +山斗 山斗 +山有扶苏 山有扶蘇 +山栖谷隐 山棲谷隱 +山栖谷饮 山棲谷飲 +山梁 山樑 +山洞里 山洞裏 +山洪暴发 山洪暴發 +山穷水尽 山窮水盡 +山系 山系 +山缪杰克森 山繆傑克森 +山羊胡 山羊鬍 +山羊胡子 山羊鬍子 +山羊须 山羊鬚 +山胡桃木 山胡桃木 +山苏 山蘇 +山药 山藥 +山药蛋 山藥蛋 +山谷 山谷 +山谷地 山谷地 +山里 山裏 +山里站 山里站 +山里红 山裏紅 +山重水复 山重水複 +山雨欲来 山雨欲來 +山雨欲来风满楼 山雨欲來風滿樓 +岁丰年稔 歲豐年稔 +岁修 歲修 +岁凶 歲凶 +岁出 歲出 +岁寒松柏 歲寒松柏 +岁稔年丰 歲稔年豐 +岁聿云暮 歲聿云暮 +岁计余绌 歲計餘絀 +岂只 豈只 +岐周 岐周 +岑参 岑參 +岔曲 岔曲 +岗台 崗臺 +岚烟波影 嵐煙波影 +岛链 島鏈 +岩仓使节团 岩倉使節團 +岩圈 岩圈 +岩土 岩土 +岩土体 岩土體 +岩基 岩基 +岩墙 巖牆 +岩墙之下 巖牆之下 +岩壁 巖壁 +岩层 岩層 +岩居 巖居 +岩居穴处 巖居穴處 +岩居谷饮 巖居谷飲 +岩屑 岩屑 +岩岩 巖巖 +岩岸 巖岸 +岩巉 巖巉 +岩床 岩牀 +岩徼 巖徼 +岩心 岩心 +岩手县 巖手縣 +岩村 巖村 +岩村明宪 岩村明憲 +岩棉 岩棉 +岩洞 巖洞 +岩流圈 巖流圈 +岩浆 岩漿 +岩浆岩 岩漿岩 +岩浆流 岩漿流 +岩溶 岩溶 +岩濑健 岩瀨健 +岩画 巖畫 +岩盐 岩鹽 +岩石 岩石 +岩石圈 岩石圈 +岩石学 岩石學 +岩石层 岩石層 +岩石循环 岩石循環 +岩礁 岩礁 +岩穴 巖穴 +岩穴之士 巖穴之士 +岩羊 岩羊 +岩脉 岩脈 +岩蔷薇 巖薔薇 +岩邑 巖邑 +岩郎 巖郎 +岩阻 巖阻 +岩陛 巖陛 +岫岩县 岫巖縣 +岭表 嶺表 +岱岳 岱嶽 +岳丈 岳丈 +岳云 岳雲 +岳坟 岳墳 +岳家 岳家 +岳家军 岳家軍 +岳岳 嶽嶽 +岳庙 岳廟 +岳母 岳母 +岳氏 岳氏 +岳父 岳父 +岳珂 岳珂 +岳阳 岳陽 +岳阳县 岳陽縣 +岳阳楼 岳陽樓 +岳阳楼记 岳陽樓記 +岳飞 岳飛 +岳麓 嶽麓 +峇峇娘惹 峇峇孃惹 +峇里岛 峇里島 +峡谷 峽谷 +峰回 峯迴 +峰回路转 峯迴路轉 +峰岩 峯巖 +峰火台 峯火臺 +峻岭 峻嶺 +峻极 峻極 +崑仑 崑崙 +崑仑奴 崑崙奴 +崑仑奴传 崑崙奴傳 +崑仑山 崑崙山 +崑仑山脉 崑崙山脈 +崑曲 崑曲 +崔京周 崔京周 +崔克索 崔克索 +崔敬邕墓志铭 崔敬邕墓誌銘 +崔涂 崔塗 +崖广 崖广 +崖谷 崖谷 +崤谷 崤谷 +崩症 崩症 +嵌岩 嵌巖 +嵚埼历落 嶔埼歷落 +嵫厘 嵫釐 +嶰谷 嶰谷 +嶽云 嶽雲 +巅崖峻谷 巔崖峻谷 +巉岩 巉巖 +巍巍荡荡 巍巍蕩蕩 +川党参 川黨蔘 +川后 川后 +川埼症 川埼症 +川杯 川杯 +川汇区 川匯區 +川谷 川穀 +州同 州同 +州里 州里 +巡回 巡迴 +巡回公演 巡迴公演 +巡回剧团 巡迴劇團 +巡回医疗 巡迴醫療 +巡回图书馆 巡迴圖書館 +巡回大使 巡迴大使 +巡回学校 巡迴學校 +巡回审判 巡迴審判 +巡回展 巡迴展 +巡回检査 巡迴檢查 +巡回法庭 巡回法庭 +巡回演出 巡迴演出 +巡回演唱 巡迴演唱 +巡回祭 巡迴祭 +巡回赛 巡迴賽 +巡回车 巡迴車 +巡幸 巡幸 +巡游 巡遊 +工业体系 工業體系 +工业团体 工業團體 +工业技术 工業技術 +工业民主制 工業民主制 +工于 工於 +工于心计 工於心計 +工人党 工人黨 +工价 工價 +工余 工餘 +工作制 工作制 +工作制度 工作制度 +工作台 工作臺 +工作团 工作團 +工作表 工作表 +工作规范 工作規範 +工作面 工作面 +工党 工黨 +工厂 工廠 +工厂卫生 工廠衛生 +工厂工业 工廠工業 +工厂布置 工廠佈置 +工厂权 工廠權 +工厂法 工廠法 +工厂自动化 工廠自動化 +工商综合区 工商綜合區 +工团 工團 +工团主义 工團主義 +工布江达 工布江達 +工布江达县 工布江達縣 +工欲善其事 工欲善其事 +工矿炸药 工礦炸藥 +工种 工種 +工程学系 工程學系 +工程系 工程系 +工致 工緻 +工艺美术 工藝美術 +左云 左雲 +左云县 左雲縣 +左光斗 左光斗 +左冲 左衝 +左冲右突 左衝右突 +左右前后 左右前後 +左右摇摆 左右搖擺 +左右采之 左右采之 +左右采获 左右採獲 +左后 左後 +左后方 左後方 +左向 左向 +左手不托右手 左手不托右手 +左拐 左拐 +左氏春秋 左氏春秋 +左邻右舍 左鄰右舍 +左邻右里 左鄰右里 +左里克 左里克 +左面 左面 +巧了 巧了 +巧克力 巧克力 +巧克力糖 巧克力糖 +巧克力脆片 巧克力脆片 +巧克力色 巧克力色 +巧克力酱 巧克力醬 +巧克莉 巧克莉 +巧历 巧曆 +巧发奇中 巧發奇中 +巧合 巧合 +巧同造化 巧同造化 +巧干 巧幹 +巧当儿 巧當兒 +巨万 鉅萬 +巨业 鉅業 +巨亏 鉅虧 +巨人症 巨人症 +巨作 鉅作 +巨债 鉅債 +巨公 鉅公 +巨制 鉅製 +巨变 鉅變 +巨商 鉅商 +巨奖 鉅獎 +巨奸 鉅奸 +巨子 鉅子 +巨富 鉅富 +巨款 鉅款 +巨献 鉅獻 +巨祥 鉅祥 +巨细 鉅細 +巨脾症 巨脾症 +巨舰 鉅艦 +巨著 鉅著 +巨贪 鉅貪 +巨野 鉅野 +巨额 鉅額 +巨鹿 鉅鹿 +巨黍 鉅黍 +巫咸 巫咸 +巫山云雨 巫山雲雨 +巫术 巫術 +差之千里 差之千里 +差之毫厘 差之毫釐 +差于 差於 +差以毫厘 差以毫釐 +差价 差價 +差别 差別 +差别待遇 差別待遇 +差别费率 差別費率 +差发 差發 +差多了 差多了 +差恶 差惡 +差若豪厘 差若豪釐 +己丑 己丑 +己出 己出 +己所不欲 己所不欲 +己饥己溺 己飢己溺 +已于 已於 +已作出保 已作出保 +已占 已佔 +已占卜 已占卜 +已占算 已占算 +已对于 已對於 +已扣 已扣 +已极 已極 +已系 已係 +巴人下里 巴人下里 +巴克 巴克 +巴克利 巴克利 +巴克南德 巴克南德 +巴克夏猪 巴克夏豬 +巴克礼 巴克禮 +巴克科思 巴克科思 +巴克莱 巴克萊 +巴克莱银行 巴克萊銀行 +巴克雷 巴克雷 +巴克霍兹 巴克霍茲 +巴别塔 巴別塔 +巴前算后 巴前算後 +巴厘岛 巴厘島 +巴尔克嫩德 巴爾克嫩德 +巴尔干 巴爾幹 +巴尔干半岛 巴爾幹半島 +巴尔干山 巴爾幹山 +巴尔干山脉 巴爾幹山脈 +巴尔扎克 巴爾扎克 +巴尔札克 巴爾札克 +巴尔舍夫斯基 巴爾舍夫斯基 +巴尔贝里尼宫殿 巴爾貝里尼宮殿 +巴尔赞 巴爾贊 +巴尔陶克 巴爾陶克 +巴尖儿好胜 巴尖兒好勝 +巴布 巴布 +巴布亚 巴布亞 +巴布亚新几内亚 巴布亞新幾內亞 +巴布亚省 巴布亞省 +巴布亚纽 巴布亞紐 +巴布亚纽几內亚 巴布亞紐幾內亞 +巴布亚纽几内亚 巴布亞紐幾內亞 +巴布尔 巴布爾 +巴布延群岛 巴布延羣島 +巴布拉族 巴布拉族 +巴布狄伦 巴布狄倫 +巴托丽 巴托麗 +巴托莉 巴托莉 +巴拉克 巴拉克 +巴拉松 巴拉松 +巴控克什米尔 巴控克什米爾 +巴斗 巴斗 +巴斯克 巴斯克 +巴斯克人 巴斯克人 +巴斯克语 巴斯克語 +巴斯德菌症 巴斯德菌症 +巴斯特纳克 巴斯特納克 +巴松管 巴松管 +巴比合金 巴比合金 +巴比布朗 巴比布朗 +巴比特合金 巴比特合金 +巴氏杆菌 巴氏桿菌 +巴洛克 巴洛克 +巴洛克式 巴洛克式 +巴洛克艺术 巴洛克藝術 +巴洛克风格 巴洛克風格 +巴洛马天文台 巴洛馬天文臺 +巴游 巴游 +巴瑞克 巴瑞克 +巴罗克 巴羅克 +巴胡提 巴胡提 +巴舍莱 巴舍萊 +巴贝克 巴貝克 +巴贝西亚原虫病 巴貝西亞原蟲病 +巴里 巴里 +巴里坤 巴里坤 +巴里坤县 巴裏坤縣 +巴里坤哈萨克自治县 巴裏坤哈薩克自治縣 +巴里坤草原 巴裏坤草原 +巴里岛 巴里島 +巴里库廷火山 巴里庫廷火山 +巴里斯 巴里斯 +巴金森氏症 巴金森氏症 +巴音布克草原 巴音布克草原 +巴马干酪 巴馬乾酪 +巷里 巷裏 +巾帼须眉 巾幗鬚眉 +币别 幣別 +币制 幣制 +市不二价 市不二價 +市不豫价 市不豫價 +市价 市價 +市党部 市黨部 +市制 市制 +市占 市佔 +市占率 市佔率 +市地重划 市地重劃 +市场价 市場價 +市场价格 市場價格 +市场准入 市場準入 +市场占有率 市場佔有率 +市无二价 市無二價 +市里 市裏 +市长杯 市長盃 +市面 市面 +市面上 市面上 +布一个 佈一個 +布丁 布丁 +布下 佈下 +布丹 布丹 +布于 佈於 +布什 布什 +布什尔 布什爾 +布什尔省 布什爾省 +布仑特 布侖特 +布会 佈會 +布伞 布傘 +布伦 布倫 +布伦尼 布倫尼 +布伦托海 布倫托海 +布伦森 布倫森 +布伦特 布倫特 +布佳迪 布佳迪 +布依 布依 +布依族 布依族 +布偶 布偶 +布偶戏 布偶戲 +布克 布克 +布克奖 布克獎 +布兰 布蘭 +布兰克 布蘭克 +布兰妮 布蘭妮 +布兰妮斯皮尔斯 布蘭妮斯皮爾斯 +布兰德 布蘭德 +布兰枯秀 布蘭枯秀 +布兰森 布蘭森 +布兰特 布蘭特 +布兰琪 布蘭琪 +布兰登堡 布蘭登堡 +布兰登堡门 布蘭登堡門 +布农 布農 +布农族 布農族 +布冯 布馮 +布划 佈劃 +布列 佈列 +布列兹涅 布列茲涅 +布列兹涅夫 布列茲涅夫 +布列兹涅夫主义 布列茲涅夫主義 +布列塔尼 布列塔尼 +布利吉 布利吉 +布利斯班 布利斯班 +布利斯班市 布利斯班市 +布加勒斯条约 布加勒斯條約 +布加勒斯特 布加勒斯特 +布劳恩 布勞恩 +布势 佈勢 +布勒斯特 布勒斯特 +布包 布包 +布匹 布匹 +布匿战争 布匿戰爭 +布卡拉 布卡拉 +布叶 布葉 +布合 布合 +布吉河 布吉河 +布吉纳法索 布吉納法索 +布告 佈告 +布告栏 布告欄 +布告牌 布告牌 +布哈拉 布哈拉 +布哈林 布哈林 +布哈林模式 布哈林模式 +布哨 佈哨 +布喀河 布喀河 +布囊 布囊 +布囊其口 佈囊其口 +布坎南 布坎南 +布基纳法索 布基納法索 +布复 布覆 +布头 布頭 +布奇 布奇 +布娃娃 布娃娃 +布婚 布婚 +布宁 布寧 +布宜若 布宜若 +布宜诺 布宜諾 +布宜诺斯艾利斯 布宜諾斯艾利斯 +布宪 布憲 +布尔 布爾 +布尔乔亚 布爾喬亞 +布尔什维克 布爾什維克 +布尔代数 布爾代數 +布尔兹 布爾茲 +布尔哥尼 布爾哥尼 +布尔津 布爾津 +布尔津县 布爾津縣 +布尔省 布爾省 +布尔诺 布爾諾 +布尼亚病毒 布尼亞病毒 +布局 佈局 +布岗 佈崗 +布巾 布巾 +布布 布布 +布帆 布帆 +布帆无恙 布帆無恙 +布希 布希 +布希号 布希號 +布希威克 布希威克 +布希总统 布希總統 +布希曼 布希曼 +布帘 布簾 +布帛 布帛 +布帛菽粟 布帛菽粟 +布幔 布幔 +布幕 布幕 +布干维尔 布干維爾 +布干维尔岛 布干維爾島 +布庄 布莊 +布店 布店 +布德 佈德 +布德尔 布德爾 +布德施仁 布德施仁 +布德施惠 布德施惠 +布慈 佈慈 +布托 布托 +布扣 佈扣 +布拉 布拉 +布拉克 布拉克 +布拉加队 布拉加隊 +布拉吉 布拉吉 +布拉姆兹 布拉姆茲 +布拉姆斯 布拉姆斯 +布拉席耶利 布拉席耶利 +布拉德 布拉德 +布拉恰诺 布拉恰諾 +布拉曼德 布拉曼德 +布拉柴维尔 布拉柴維爾 +布拉格 布拉格 +布拉格定律 布拉格定律 +布拉格队 布拉格隊 +布拉欣 布拉欣 +布拉索夫 布拉索夫 +布拉萨市 布拉薩市 +布拉谢 布拉謝 +布拉迪斯拉发 布拉迪斯拉發 +布拉马普得拉河 布拉馬普得拉河 +布拖 布拖 +布拖县 布拖縣 +布招儿 布招兒 +布掸子 布撣子 +布摆 佈擺 +布政 佈政 +布政使 布政使 +布政司 布政司 +布教 佈教 +布散 佈散 +布料 布料 +布施 佈施 +布景 佈景 +布朗 布朗 +布朗克士 布朗克士 +布朗克斯 布朗克斯 +布朗士 布朗士 +布朗夏 布朗夏 +布朗大学 布朗大學 +布朗妮 布朗妮 +布朗宁 布朗寧 +布朗德 布朗德 +布朗族 布朗族 +布朗森 布朗森 +布朗特 布朗特 +布朗贝克 布朗貝克 +布朗费德 布朗費德 +布朗运动 布朗運動 +布望 布望 +布条 布條 +布来德史崔 布來德史崔 +布林 布林 +布林代数 布林代數 +布林底希 布林底希 +布林迪西 布林迪西 +布格河 布格河 +布桩 布樁 +布梏 布梏 +布氏杆菌 布氏桿菌 +布氏杆菌病 布氏桿菌病 +布氏菌苗 布氏菌苗 +布氏非鲫 布氏非鯽 +布气 布氣 +布水 布水 +布法罗 布法羅 +布洒器 布灑器 +布洛克 布洛克 +布洛斯顿 布洛斯頓 +布洛沙德 布洛沙德 +布洛湾 布洛灣 +布洛芬 布洛芬 +布满 佈滿 +布濩 布濩 +布热津斯基 布熱津斯基 +布特 布特 +布特哈 布特哈 +布特拉加亚 布特拉加亞 +布班尼斯瓦 布班尼斯瓦 +布琼布拉 布瓊布拉 +布瑞 布瑞 +布瑞尤 布瑞尤 +布瑞斯特 布瑞斯特 +布瑞特 布瑞特 +布瑞顿 布瑞頓 +布用填 布用填 +布疋 布疋 +布疑阵 佈疑陣 +布痕瓦尔德 布痕瓦爾德 +布白 布白 +布立吞族 布立吞族 +布算 布算 +布篷 布篷 +布素 布素 +布累尔 布累爾 +布纶堡 布綸堡 +布纹纸 布紋紙 +布线 佈線 +布维岛 布維島 +布缦 布縵 +布网 佈網 +布罗德赫斯特 布羅德赫斯特 +布罗温斯文学 布羅溫斯文學 +布罗迪 布羅迪 +布置 佈置 +布署 佈署 +布耳 布耳 +布胡会 布胡會 +布臆 布臆 +布色 布色 +布荆 布荊 +布草衣服 布草衣服 +布莱 布萊 +布莱克 布萊克 +布莱克史密斯 布萊克史密斯 +布莱克曼 布萊克曼 +布莱克本 布萊克本 +布莱尔 布萊爾 +布莱尼 布萊尼 +布莱希特 布萊希特 +布莱德 布萊德 +布莱德利 布萊德利 +布莱德彼特 布萊德彼特 +布莱德福 布萊德福 +布莱德雷 布萊德雷 +布莱恩 布萊恩 +布莱恩特 布萊恩特 +布莱恩狄帕玛 布萊恩狄帕瑪 +布莱特 布萊特 +布莱特妮墨菲 布萊特妮墨菲 +布莱雅特蒙古 布萊雅特蒙古 +布莱顿 布萊頓 +布菜 佈菜 +布萨 布薩 +布衣 布衣 +布衣之交 布衣之交 +布衣之怒 布衣之怒 +布衣交 布衣交 +布衣卿相 布衣卿相 +布衣小民 布衣小民 +布衣粝食 布衣糲食 +布衣苇带 布衣葦帶 +布衣蔬食 布衣蔬食 +布衣韦带 布衣韋帶 +布衣黔首 布衣黔首 +布衫 布衫 +布袋 布袋 +布袋和尚 布袋和尚 +布袋安 布袋安 +布袋戏 布袋戲 +布袋戏偶 布袋戲偶 +布袋戏馆 布袋戲館 +布袋港 布袋港 +布袋莲 布袋蓮 +布袋装 布袋裝 +布袋镇 布袋鎮 +布袜青鞋 布襪青鞋 +布被 布被 +布被十年 布被十年 +布裙荆钗 布裙荊釵 +布褐 布褐 +布西县 布西縣 +布让 佈讓 +布设 佈設 +布谷 布穀 +布谷鸟 布穀鳥 +布谷鸟钟 布穀鳥鐘 +布货 布貨 +布边 布邊 +布达 布達 +布达佩斯 布達佩斯 +布达式 布達式 +布达拉宫 布達拉宮 +布达拉寺 布達拉寺 +布达拉山 布達拉山 +布迪亚 布迪亞 +布道 佈道 +布道大会 佈道大會 +布道所 佈道所 +布那喀 布那喀 +布里 布里 +布里亚族 布里亞族 +布里兹涅夫 布里茲涅夫 +布里坦尼 布里坦尼 +布里奇顿 布里奇頓 +布里姬沃特 布里姬沃特 +布里斯 布里斯 +布里斯托 布里斯托 +布里斯托尔 布裏斯托爾 +布里斯托尔海峡 布里斯托爾海峽 +布里斯本 布里斯本 +布里斯本市 布里斯本市 +布里斯本河 布里斯本河 +布里斯班 布里斯班 +布里迪雅 布里迪雅 +布里迪雅通 布里迪雅通 +布防 佈防 +布防迎战 布防迎戰 +布阵 佈陣 +布阵安营 佈陣安營 +布隆伯格 布隆伯格 +布隆克特 布隆克特 +布隆吉尔河 布隆吉爾河 +布隆方丹 布隆方丹 +布隆迪 布隆迪 +布雅湾 布雅灣 +布雪 佈雪 +布雷 佈雷 +布雷克 布雷克 +布雷克磨 布雷克磨 +布雷封锁 佈雷封鎖 +布雷尔 布雷爾 +布雷尔港 布雷爾港 +布雷希特 布雷希特 +布雷恩 布雷恩 +布雷拉 布雷拉 +布雷的 佈雷的 +布雷舰 佈雷艦 +布雷艇 佈雷艇 +布雷蒙 布雷蒙 +布雷迪 布雷迪 +布雷速度 佈雷速度 +布雷队 佈雷隊 +布雷顿 布雷頓 +布雷顿森林 佈雷頓森林 +布雷默 布雷默 +布面 布面 +布面相片 布面相片 +布鞋 布鞋 +布须曼人 布須曼人 +布马雪 布馬雪 +布鲁 布魯 +布鲁乐谷 布魯樂谷 +布鲁克 布魯克 +布鲁克斯 布魯克斯 +布鲁克林 布魯克林 +布鲁克林大桥 布魯克林大橋 +布鲁克纳 布魯克納 +布鲁克雪德丝 布魯克雪德絲 +布鲁勒 布魯勒 +布鲁塞尔 布魯塞爾 +布鲁奈勒斯基 布魯奈勒斯基 +布鲁姆费尔德 布魯姆費爾德 +布鲁尼 布魯尼 +布鲁托 布魯托 +布鲁斯 布魯斯 +布鲁斯史普林斯汀 布魯斯史普林斯汀 +布鲁斯威利 布魯斯威利 +布鲁斯特 布魯斯特 +布鲁日 布魯日 +布鲁氏菌 布魯氏菌 +布鲁氏菌病 布魯氏菌病 +布鲁珊 布魯珊 +布鲁诺 布魯諾 +布鲁金斯 布魯金斯 +布鼓雷门 布鼓雷門 +帅呆了 帥呆了 +帆布 帆布 +帆布包 帆布包 +帆布椅 帆布椅 +帆布牀 帆布牀 +帆布袋 帆布袋 +帆布鞋 帆布鞋 +帆板 帆板 +师云而云 師云而云 +师出无名 師出無名 +师出有名 師出有名 +师团 師團 +师娘 師孃 +师徒合同 師徒合同 +师杯 師杯 +师生杯 師生盃 +师范 師範 +师范大学 師範大學 +师范学校 師範學校 +师范学院 師範學院 +师范教育 師範教育 +师范毕业 師範畢業 +师范生 師範生 +师范类 師範類 +师范附小 師範附小 +师范院校 師範院校 +师表 師表 +希伯来历 希伯來曆 +希伯来历史 希伯來歷史 +希克斯 希克斯 +希区考克 希區考克 +希布伦市 希布倫市 +希拉克 希拉剋 +希拉里 希拉里 +希拉里克林顿 希拉裏克林頓 +希斯仑 希斯崙 +希斯莱杰 希斯萊傑 +希斯雷杰 希斯雷傑 +希格斯机制 希格斯機制 +希腊建筑 希臘建築 +帐面 帳面 +帕丝齐克 帕絲齊克 +帕修斯 帕修斯 +帕克 帕克 +帕台农 帕臺農 +帕台农神庙 帕臺農神廟 +帕拉马里博 帕拉馬裏博 +帕特里克 帕特里克 +帕特里夏 帕特里夏 +帕穆克 帕穆克 +帕索里尼 帕索里尼 +帕累托最优 帕累托最優 +帕累托法则 帕累托法則 +帕罗贝克 帕羅貝克 +帕谢克 帕謝克 +帕金森氏症 帕金森氏症 +帕金森症 帕金森症 +帘子 簾子 +帘子布 簾子布 +帘子线 簾子線 +帘官 簾官 +帘布 簾布 +帘帐 簾帳 +帘帷 簾帷 +帘幔 簾幔 +帘幕 簾幕 +帘幕式 簾幕式 +帘幽梦 簾幽夢 +帘栊 簾櫳 +帘波 簾波 +帝制 帝制 +帝制时代 帝制時代 +帝后 帝后 +帝胄 帝胄 +带丑闻 帶醜聞 +带个 帶個 +带个好 帶個好 +带了 帶了 +带凶 帶凶 +带出 帶出 +带出去 帶出去 +带出来 帶出來 +带发修行 帶髮修行 +带同 帶同 +带回 帶回 +带回到 帶回到 +带回去 帶回去 +带回来 帶回來 +带团 帶團 +带团参加 帶團參加 +带征 帶徵 +带征银 帶徵銀 +带扣 帶扣 +带罪征收 帶罪徵收 +带膆貂挂 帶膆貂掛 +席丰履厚 席豐履厚 +席卷 席捲 +席卷一空 席捲一空 +席卷亚洲 席捲亞洲 +席卷天下 席捲天下 +席卷而来 席捲而來 +席卷而逃 席捲而逃 +席哈克 席哈克 +席志成 席誌成 +席棚 蓆棚 +席湘漓 席湘漓 +席胜 席勝 +席面 席面 +帮不了 幫不了 +帮个场子 幫個場子 +帮伙 幫夥 +帮佣 幫傭 +帮冬 幫冬 +帮凶 幫兇 +帮别 幫別 +帮懒钻闲 幫懶鑽閒 +帮闲钻懒 幫閒鑽懶 +帷薄不修 帷薄不修 +常价 常價 +常任代表 常任代表 +常向 常向 +常回 常回 +常回去 常回去 +常回来 常回來 +常态分布 常態分佈 +常用参考书 常用參考書 +常绿阔叶 常綠闊葉 +常绿阔叶林 常綠闊葉林 +常胜 常勝 +常胜军 常勝軍 +常胜家 常勝家 +常胜将军 常勝將軍 +常衡制 常衡制 +常见于 常見於 +常须 常須 +帽帘 帽簾 +幅面 幅面 +幕前幕后 幕前幕後 +幕后 幕後 +幕后人物 幕後人物 +幕后新闻 幕後新聞 +幕后消息 幕後消息 +幕后花絮 幕後花絮 +幕后英雄 幕後英雄 +幕布 幕布 +幡布 幡布 +干一 幹一 +干一坛 乾一罈 +干一坛法 幹一壇法 +干一杯 乾一杯 +干一碗 乾一碗 +干上 幹上 +干下去 幹下去 +干不 幹不 +干不下 幹不下 +干不了 幹不了 +干不干 幹不幹 +干不干净 乾不乾淨 +干不干杯 乾不乾杯 +干不成 幹不成 +干与 干與 +干丝 乾絲 +干两年 幹兩年 +干两杯 乾兩杯 +干个 幹個 +干个够 幹個夠 +干么 幹麼 +干乔 乾喬 +干买卖 幹買賣 +干了 幹了 乾了 +干了杯 乾了杯 +干了这一杯 乾了這一杯 +干了这一瓶 乾了這一瓶 +干了这杯 乾了這杯 +干了这碗 乾了這碗 +干事 幹事 +干事会 幹事會 +干事长 幹事長 +干云蔽日 乾雲蔽日 +干井 乾井 +干些 幹些 +干产 乾產 +干亲 乾親 +干人 幹人 +干什 幹什 +干什么 幹什麼 +干他 幹他 干他 +干仗 幹仗 +干任何 幹任何 +干休 干休 +干休所 幹休所 +干伸舌 乾伸舌 +干何事 幹何事 +干你 幹你 干你 +干你娘 幹你孃 +干傻事 幹傻事 +干儿 乾兒 +干儿子 乾兒子 +干冒烟 乾冒煙 +干农活 幹農活 +干冰 乾冰 +干冷 乾冷 +干净 乾淨 +干净俐落 乾淨俐落 +干凉 乾涼 +干几件 幹幾件 +干几宗 幹幾宗 +干几手 乾幾手 +干几杯 乾幾杯 +干几桩 幹幾樁 +干几番 幹幾番 +干几碗 乾幾碗 +干几辈 幹幾輩 +干凯文 干凱文 +干出 幹出 +干刍 乾芻 +干別的 幹別的 +干到 幹到 +干制 乾製 +干刻版 乾刻版 +干剥剥 乾剝剝 +干办 幹辦 +干劲 幹勁 +干劲儿 幹勁兒 +干劲冲天 幹勁沖天 +干劲十足 幹勁十足 +干卦 乾卦 +干卿何事 干卿何事 +干卿底事 干卿底事 +干又热 乾又熱 +干台 乾颱 +干号 乾號 +干吊着下巴 乾吊着下巴 +干吏 幹吏 +干吗 幹嗎 +干呕 乾嘔 +干员 幹員 +干和 乾和 +干咳 乾咳 +干咽 乾嚥 +干咽唾 乾嚥唾 +干哑 乾啞 +干哕 乾噦 +干哥 乾哥 +干哥哥 乾哥哥 +干哭 乾哭 +干唱 乾唱 +干啤 乾啤 +干啥 幹啥 +干啼 乾啼 +干啼湿哭 乾啼溼哭 +干嘛 幹嘛 +干嚎 乾嚎 +干回付 乾回付 +干图 乾圖 +干圆洁净 乾圓潔淨 +干土 乾土 +干地 乾地 +干坏事 幹壞事 +干坐着 乾坐着 +干坛子 乾罈子 +干坞 乾塢 +干城 干城 +干堂婶 乾堂嬸 +干大事 幹大事 +干头 幹頭 +干女 乾女 +干女儿 乾女兒 +干女友 幹女友 +干女同事 幹女同事 +干女婿 乾女婿 +干女教师 幹女教師 +干奴才 乾奴才 +干她 幹她 干她 +干好 幹好 +干妈 乾媽 +干妹 乾妹 +干妹妹 乾妹妹 +干姊 乾姊 +干姊姊 乾姊姊 +干姐 乾姐 +干姜 乾薑 +干姬松茸 乾姬松茸 +干娘 乾孃 +干子 乾子 +干季 乾季 +干宅 乾宅 +干完 幹完 +干家 幹家 +干将 干將 +干将莫邪 干將莫邪 +干就干 幹就幹 +干尸 乾屍 +干尽 幹盡 +干尽一坛 乾盡一罈 +干尽一壺 乾盡一壺 +干尽一杯 乾盡一杯 +干尽一碗 乾盡一碗 +干屎橛 乾屎橛 +干巴 乾巴 +干巴巴 乾巴巴 +干巴巴的 乾巴巴的 +干布 乾布 +干干 乾乾 +干干儿的 乾乾兒的 +干干净净 乾乾淨淨 +干干巴巴 乾乾巴巴 +干干干干 幹幹幹幹 +干干淨淨 乾乾淨淨 +干干爽爽 乾乾爽爽 +干干瘦瘦 乾乾瘦瘦 +干干的 乾乾的 +干干脆脆 乾乾脆脆 +干式 乾式 +干弟 乾弟 +干弟弟 乾弟弟 +干强盗 幹強盜 +干当 幹當 +干得 幹得 +干得一杯 乾得一杯 +干得三杯 乾得三杯 +干得两杯 乾得兩杯 +干得了 幹得了 +干得很 乾得很 +干急 乾急 +干性 乾性 +干性油 乾性油 +干性皮肤 乾性皮膚 +干戈 干戈 +干戈扰攘 干戈擾攘 +干成 幹成 +干我 幹我 干我 +干戚 干鏚 +干扁豆角 干扁豆角 +干手净脚 乾手淨腳 +干才 幹才 +干打垒 乾打壘 +干打雷 乾打雷 +干扰 干擾 +干扰到 干擾到 +干扰力 干擾力 +干扰素 干擾素 +干折 乾折 +干拎娘 幹拎娘 +干挠 干撓 +干掉 幹掉 +干掉一杯 乾掉一杯 +干掉一瓶 乾掉一瓶 +干掉一碗 乾掉一碗 +干掉这杯 乾掉這杯 +干掉这碗 乾掉這碗 +干掉那杯 乾掉那杯 +干掉那碗 乾掉那碗 +干探 幹探 +干撂台 乾撂臺 +干撇下 乾撇下 +干擦 乾擦 +干支 干支 +干支剌 乾支剌 +干支支 乾支支 +干支沟 干支溝 +干政 干政 +干数杯 乾數杯 +干料 乾料 +干断 乾斷 +干旦 乾旦 +干旱 乾旱 +干旱地区 乾旱地區 +干时 干時 +干暖 乾暖 +干曜 乾曜 +干材 乾材 +干村沙 乾村沙 +干杯 乾杯 +干果 乾果 +干枝 乾枝 +干枯 乾枯 +干架 幹架 +干柴 乾柴 +干柴烈火 乾柴烈火 +干校 幹校 +干梅 乾梅 +干梅子 乾梅子 +干正事 幹正事 +干此坛 乾此罈 +干此杯 乾此杯 +干死 乾死 +干毛巾 乾毛巾 +干池 乾池 +干沟 乾溝 +干没 乾沒 +干洗 乾洗 +干洗店 乾洗店 +干活 幹活 +干活儿 幹活兒 +干流 幹流 +干济 幹濟 +干涉 干涉 +干涉仪 干涉儀 +干涉现象 干涉現象 +干涩 乾澀 +干涸 乾涸 +干淨 乾淨 +干淨俐落 乾淨俐落 +干渠 乾渠 +干渴 乾渴 +干湿 乾溼 +干湿发 乾溼髮 +干漆 乾漆 +干灯盏 乾燈盞 +干点 乾點 幹點 +干烧 乾燒 +干热 乾熱 +干焦 乾焦 +干煸 乾煸 +干熬 乾熬 +干熱 乾熱 +干燥 乾燥 +干燥剂 乾燥劑 +干燥器 乾燥器 +干燥机 乾燥機 +干燥箱 乾燥箱 +干父之蛊 幹父之蠱 +干爸 乾爸 +干爸爸 乾爸爸 +干爹 乾爹 +干爽 乾爽 +干片 乾片 +干犯 干犯 +干犯法 幹犯法 +干球温度 幹球溫度 +干甚 幹甚 +干甚么 幹甚麼 +干生受 乾生受 +干生子 乾生子 +干生气 乾生氣 +干田 乾田 +干电 乾電 +干电池 乾電池 +干略 幹略 +干疥 乾疥 +干瘦 乾瘦 +干瘪 乾癟 +干瘪瘪 乾癟癟 +干瘾 乾癮 +干癣 乾癬 +干癣性 乾癬性 +干白 乾白 +干白儿 乾白兒 +干的 乾的 +干的停当 幹的停當 +干眼 乾眼 +干眼病 乾眼病 +干眼症 乾眼症 +干着 幹着 乾着 +干着急 乾着急 +干瞪眼 乾瞪眼 +干硬 乾硬 +干碍 干礙 +干礼 乾禮 +干稿 乾稿 +干站着 乾站着 +干笑 乾笑 +干等 乾等 +干管 幹管 +干篾片 乾篾片 +干粉 乾粉 +干粗活 幹粗活 +干粮 乾糧 +干粮袋 乾糧袋 +干糇 乾餱 +干系 干係 +干細胞 幹細胞 +干红 乾紅 +干纲 乾綱 +干纲不振 乾綱不振 +干线 幹線 +干练 幹練 +干细胞 幹細胞 +干结 乾結 +干绷 乾繃 +干绷儿 乾繃兒 +干缺 幹缺 +干群 幹羣 +干群关系 幹羣關係 +干耗 乾耗 +干肉 乾肉 +干肉片 乾肉片 +干股 乾股 +干肥 乾肥 +干脆 乾脆 +干脆利落 乾脆利落 +干花 乾花 +干苔 乾薹 +干茨腊 乾茨臘 +干茶钱 乾茶錢 +干草 乾草 +干草叉 乾草叉 +干草堆 乾草堆 +干草机 乾草機 +干草粉 乾草粉 +干菜 乾菜 +干营生 幹營生 +干落 乾落 +干薪 乾薪 +干虔 乾虔 +干蛊 幹蠱 +干血浆 乾血漿 +干衣 乾衣 +干衣机 乾衣機 +干裂 乾裂 +干警 幹警 +干谒 干謁 +干象 乾象 +干贝 乾貝 +干货 乾貨 +干起 幹起 +干起来 幹起來 +干路 幹路 +干躁 乾躁 +干过 幹過 +干过一杯 乾過一杯 +干过杯 乾過杯 +干过瘾 乾過癮 +干这 幹這 +干这一杯 乾這一杯 +干这一行 幹這一行 +干这杯 乾這杯 +干这种事 幹這種事 +干连 干連 +干透 乾透 +干造 乾造 +干逼 乾逼 +干道 幹道 +干邑 干邑 +干那 幹那 +干那一杯 乾那一杯 +干那杯 乾那杯 +干部 幹部 +干酪 乾酪 +干酵母 乾酵母 +干醋 乾醋 +干重 乾重 +干量 乾量 +干闼婆 乾闥婆 +干阿奶 乾阿奶 +干雷 乾雷 +干霍乱 乾霍亂 +干面 乾麪 +干革命 幹革命 +干预 干預 +干颡 乾顙 +干饭 乾飯 +干馆 乾館 +干馏 乾餾 +干馏法 乾餾法 +干鱼 乾魚 +干鲜 乾鮮 +干麻 幹麻 +干麻学 幹麻學 +干麻阿 幹麻阿 +平个 平個 +平个人 平個人 +平了 平了 +平价 平價 +平价住宅 平價住宅 +平价供应中心 平價供應中心 +平价商店 平價商店 +平克佛洛伊德 平克佛洛伊德 +平准 平準 +平准基金 平準基金 +平分秋色 平分秋色 +平台 平臺 +平台式扫描器 平臺式掃描器 +平台数 平臺數 +平台阶段 平臺階段 +平地上起一个霹雳 平地上起一個霹靂 +平地松林 平地松林 +平地楼台 平地樓臺 +平地青云 平地青雲 +平均价 平均價 +平均消费倾向 平均消費傾向 +平均股价 平均股價 +平复 平復 +平复帖 平復帖 +平复起来 平復起來 +平头并进 平頭並進 +平安里 平安裏 +平定准噶尔回部得胜图 平定準噶爾回部得勝圖 +平平当当 平平當當 +平康里 平康里 +平方公里 平方公里 +平方千米 平方千米 +平易谦冲 平易謙沖 +平板 平板 +平板仪 平板儀 +平板玻璃 平板玻璃 +平板车 平板車 +平梁 平梁 +平步青云 平步青雲 +平泉庄 平泉莊 +平胡 平胡 +平行六面 平行六面 +平行六面体 平行六面體 +平行叶脉 平行葉脈 +平行表亲 平行表親 +平衡台 平衡臺 +平表 平表 +平谷 平谷 +平谷区 平谷區 +平谷县 平谷縣 +平足症 平足症 +平针缝 平針縫 +平铲 平鏟 +平雕 平雕 +平面 平面 +平面几何 平面幾何 +平面图 平面圖 +平面图形 平面圖形 +平面媒体 平面媒體 +平面描迹 平面描跡 +平面曲线 平面曲線 +平面段 平面段 +平面波 平面波 +平面测量 平面測量 +平面艺术 平面藝術 +平面角 平面角 +平面镜 平面鏡 +平面雕刻 平面雕刻 +年久失修 年久失修 +年代里 年代裏 +年余 年餘 +年几 年幾 +年历 年曆 +年台 年臺 +年同 年同 +年后 年後 +年团子 年團子 +年复一年 年復一年 +年少得志 年少得志 +年德并高 年德並高 +年表 年表 +年谷 年穀 +年里 年裏 +年金制度 年金制度 +年鉴 年鑑 +年龄特征 年齡特徵 +并一不二 併一不二 +并上 並上 +并不 並不 +并不会 並不會 +并不在 並不在 +并不在乎 並不在乎 +并不并 併不併 +并不是 並不是 +并不能 並不能 +并与 並與 +并且 並且 +并世 並世 +并世无双 並世無雙 +并为 併爲 +并举 並舉 +并于 並於 +并产 併產 +并介之人 並介之人 +并先 並先 +并入 併入 +并兴 並興 +并兼 併兼 +并再 並再 +并出 並出 +并刀 並刀 +并列 並列 +并到 併到 +并力 併力 +并包 幷包 +并卷机 併捲機 +并发 併發 +并发动 並發動 +并发展 並發展 +并发现 並發現 +并发症 併發症 +并发表 並發表 +并叠 併疊 +并口 並口 +并可 並可 +并可以 並可以 +并合 併合 +并同 並同 +并名 併名 +并吞 併吞 +并吞下 併吞下 +并回 並回 +并回去 並回去 +并回来 並回來 +并在 並在 +并坐 並坐 +并垂不朽 並垂不朽 +并处 並處 +并头 並頭 +并头之花 並頭之花 +并头莲 並頭蓮 +并存 並存 +并存不悖 並存不悖 +并存在 並存在 +并寿 並壽 +并将 並將 +并州 幷州 +并州剪 幷州剪 +并州故乡 幷州故鄉 +并当 並當 +并往 並往 +并得 並得 +并成 併成 +并把 並把 +并拢 併攏 +并排 並排 +并排而坐 並排而坐 +并无 並無 +并无不可 並無不可 +并无不当 並無不當 +并无此事 並無此事 +并日 並日 +并日而食 並日而食 +并时 並時 +并是 並是 +并曰入淀 並曰入澱 +并有 並有 +并未 並未 +并条 並條 +并案 併案 +并案处理 併案處理 +并概青云 並概青雲 +并毂 並轂 +并没 並沒 +并没有 並沒有 +并派 並派 +并流 並流 併流 +并激 並激 +并火 併火 +并然 並然 +并爲 並爲 +并用 並用 +并由 並由 +并禽 並禽 +并科 併科 +并称 並稱 +并立 並立 +并纱 併紗 +并线 併線 +并网 併網 +并置 並置 +并翼齐飞 並翼齊飛 +并联 並聯 +并肩 並肩 +并肩作战 並肩作戰 +并肩前进 並肩前進 +并肩子 併肩子 +并肩干 並肩幹 +并肩而行 並肩而行 +并能 並能 +并能夠 並能夠 +并蒂 並蒂 +并蒂莲 並蒂蓮 +并行 並行 +并行不悖 並行不悖 +并行口 並行口 +并行程序 並行程序 +并行计算 並行計算 +并要 並要 +并规范 並規範 +并论 並論 +并请 並請 +并购 併購 +并购买 併購買 +并购案 併購案 +并赃拿败 並贓拿敗 +并赃拿贼 併贓拿賊 +并赃治罪 併贓治罪 +并起 並起 +并跡 並跡 +并躺 並躺 +并躺在 並躺在 +并轨 並軌 +并辏 並輳 +并辔 並轡 +并过 並過 +并过去 並過去 +并过来 並過來 +并进 並進 +并进去 並進去 +并进来 並進來 +并迭 並迭 +并邀 並邀 +并邦 並邦 +并邻 並鄰 +并采 並採 +并重 並重 +并除 併除 +并非 並非 +并非在 並非在 +并非是 並非是 +并马 並馬 +并驰 並馳 +并驱 並驅 +并驾 並駕 +并驾齐驱 並駕齊驅 +并骛 並騖 +并骨 併骨 +幸不辱命 幸不辱命 +幸为先容 幸爲先容 +幸事 幸事 +幸于 幸於 +幸于始者怠于终 幸於始者怠於終 +幸亏 幸虧 +幸亏在 幸虧在 +幸亏是 幸虧是 +幸亏有 幸虧有 +幸会 幸會 +幸位 幸位 +幸倡 幸倡 +幸免 倖免 +幸免于难 倖免於難 +幸发亭 幸發亭 +幸喜 幸喜 +幸好 幸好 +幸好是 幸好是 +幸好有 幸好有 +幸子 幸子 +幸存 倖存 +幸存者 倖存者 +幸幸 倖幸 +幸得 幸得 +幸感歌姬 倖感歌姬 +幸未 幸未 +幸村 幸村 +幸次 幸次 +幸民 幸民 +幸灾乐祸 幸災樂禍 +幸然 幸然 +幸甚 幸甚 +幸生 幸生 +幸福 幸福 +幸福女人小公主 幸福女人小公主 +幸福学 幸福學 +幸福家庭 幸福家庭 +幸福感 幸福感 +幸福戏院 幸福戲院 +幸童 幸童 +幸而 幸而 +幸而是 幸而是 +幸能 幸能 +幸臣 倖臣 +幸获识荆 幸獲識荊 +幸蒙 幸蒙 +幸运 幸運 +幸运之星 幸運之星 +幸运之神 幸運之神 +幸运人 幸運人 +幸运儿 幸運兒 +幸运券 幸運券 +幸运抽奖 幸運抽獎 +幸运星 幸運星 +幸运物 幸運物 +幸运球 幸運球 +幸运胡 幸運鬍 +幸运色 幸運色 +幸运草 幸運草 +幸进 倖進 +幸逢 幸逢 +幺么小丑 幺麼小醜 +幺并矢 幺並矢 +幺麼小丑 幺麼小醜 +幺麽小丑 幺麼小醜 +幻出 幻出 +幻念 幻念 +幻想曲 幻想曲 +幻术 幻術 +幼发拉底 幼發拉底 +幼发拉底河 幼發拉底河 +幼发拉底河谷 幼發拉底河谷 +幼托 幼托 +幼虫 幼蟲 +幽咽 幽咽 +幽岩 幽巖 +幽幽暗暗 幽幽暗暗 +幽明录 幽明錄 +幽暗 幽暗 +幽栖岩谷 幽棲巖谷 +幽谷 幽谷 +幽门狭窄症 幽門狹窄症 +幽闭恐惧症 幽閉恐懼症 +广东住血吸虫 廣東住血吸蟲 +广东炒面 廣東炒麪 +广东药学院 廣東藥學院 +广丰 廣豐 +广丰县 廣豐縣 +广厦万间 廣廈萬間 +广发 廣發 +广告曲 廣告曲 +广告板 廣告板 +广告系 廣告系 +广场恐怖症 廣場恐怖症 +广场恐惧症 廣場恐懼症 +广布 廣佈 +广征 廣徵 +广播台 廣播臺 +广播电台 廣播電臺 +广播系统 廣播系統 +广泛 廣泛 +广舍 廣捨 +广部 广部 +广里 廣裏 +庄上 莊上 +庄严 莊嚴 +庄主 莊主 +庄农 莊農 +庄员 莊員 +庄周 莊周 +庄周梦蝶 莊周夢蝶 +庄园 莊園 +庄园制度 莊園制度 +庄士顿道 莊士頓道 +庄子 莊子 +庄客 莊客 +庄家 莊家 +庄志伟 莊志偉 +庄户 莊戶 +庄房 莊房 +庄敬 莊敬 +庄田 莊田 +庄秋南 莊秋南 +庄稼 莊稼 +庄稼人 莊稼人 +庄稼院 莊稼院 +庄胜雄 莊勝雄 +庄舄越吟 莊舄越吟 +庄语 莊語 +庄里 莊裏 +庄重 莊重 +庄院 莊院 +庄骚 莊騷 +庆丰 慶豐 +庆云 慶雲 +庆云县 慶雲縣 +庆余 慶餘 +庆历 慶曆 +庆历新政 慶曆新政 +庆吊 慶弔 +庆幸 慶幸 +庇荫 庇廕 +庇里牛斯 庇里牛斯 +庇里牛斯山 庇里牛斯山 +床头柜 牀頭櫃 +床席 牀蓆 +床板 牀板 +序升 序升 +序录 序錄 +序曲 序曲 +庐山真面目 廬山真面目 +庐山面目 廬山面目 +庐舍 廬舍 +库仑 庫侖 +库仑定律 庫侖定律 +库仑计 庫侖計 +库克 庫克 +库克山 庫克山 +库克群岛 庫克羣島 +库克船长 庫克船長 +库尔德工人党 庫爾德工人黨 +库尔斯克 庫爾斯克 +库工党 庫工黨 +库布里克 庫布裏克 +库木吐拉千佛洞 庫木吐拉千佛洞 +库瑞克 庫瑞克 +库苏古尔湖 庫蘇古爾湖 +库贝克 庫貝克 +库贾氏症 庫賈氏症 +库辛尼克 庫辛尼克 +库里尔台 庫里爾臺 +库里提巴 庫裏提巴 +应付得当 應付得當 +应付票据 應付票據 +应克制 應剋制 +应制 應制 +应占 應占 +应声虫 應聲蟲 +应当 應當 +应当会 應當會 +应当在 應當在 +应当是 應當是 +应当有 應當有 +应当能 應當能 +应征 應徵 +应征信 應徵信 +应征稿 應徵稿 +应征者 應徵者 +应急出口 應急出口 +应收帐款周转率 應收帳款週轉率 +应收票据 應收票據 +应时当令 應時當令 +应有尽有 應有盡有 +应用于 應用於 +应用平台 應用平臺 +应用技术 應用技術 +应用范例 應用範例 +应用范围 應用範圍 +应运而出 應運而出 +应钟 應鐘 +应须 應須 +底价 底價 +底夸克 底夸克 +底板 底板 +底极 底極 +底栖有孔虫 底棲有孔蟲 +底格里斯 底格里斯 +底格里斯河 底格里斯河 +底脚里人 底腳裏人 +底里 底裏 +底面 底面 +店伙 店夥 +店里 店裏 +店面 店面 +店面广告 店面廣告 +庙台 廟臺 +庙里 廟裏 +府兵制 府兵制 +府台 府臺 +府后 府後 +府干 府幹 +府谷 府谷 +府谷县 府谷縣 +庞克 龐克 +庞克头 龐克頭 +庞巴瓦克 龐巴瓦克 +庞德维克 龐德維克 +庞志龙 龐志龍 +庞眉白发 龐眉白髮 +庞眉皓发 龐眉皓髮 +废了 廢了 +废后 廢后 +度假胜地 度假勝地 +度搜 度搜 +度曲 度曲 +度身定制 度身定製 +座钟 座鐘 +庵主 庵主 +庵儿 庵兒 +庵堂 庵堂 +庵婪 菴婪 +庵寺 庵寺 +庵庐 菴廬 +庵庵 菴菴 +庵罗树园 菴羅樹園 +庵舍 菴舍 +庵蔼 菴藹 +庵观 庵觀 +庶几 庶幾 +庶几无愧 庶幾無愧 +庶出 庶出 +康乃狄克 康乃狄克 +康乃狄克州 康乃狄克州 +康回 康回 +康复 康復 +康复医学 康復醫學 +康多莉扎赖斯 康多莉扎賴斯 +康庄 康莊 +康庄大道 康莊大道 +康托尔 康托爾 +康梁 康梁 +康济录 康濟錄 +康百克 康百克 +康纳克立 康納克立 +康采恩 康采恩 +庸才 庸才 +庸暗 庸闇 +廉价 廉價 +廉价品 廉價品 +廉售价 廉售價 +廉纤 廉纖 +廖于诚 廖于誠 +廖俊杰 廖俊傑 +廖千莹 廖千瑩 +廖志坚 廖志堅 +廖本烟 廖本煙 +廖本胜 廖本勝 +廖英杰 廖英傑 +廖金钟 廖金鐘 +廛布 廛布 +廢后 廢后 +廪秋 廩秋 +廪膳秀才 廩膳秀才 +延伸出 延伸出 +延修 延修 +延厘 延釐 +延发 延發 +延后 延後 +延地里 延地裏 +延挨 延挨 +延陵挂剑 延陵掛劍 +廷争面折 廷爭面折 +廷巴克图 廷巴克圖 +廷布 廷布 +建于 建於 +建党 建黨 +建党节 建黨節 +建出 建出 +建制 建制 +建厂 建廠 +建台 建臺 +建教合作 建教合作 +建教合作班 建教合作班 +建极 建極 +建立规范 建立規範 +建筑 建築 +建筑业 建築業 +建筑商 建築商 +建筑学 建築學 +建筑容积管制 建築容積管制 +建筑工业 建築工業 +建筑工人 建築工人 +建筑工地 建築工地 +建筑工程 建築工程 +建筑师 建築師 +建筑执照 建築執照 +建筑机械 建築機械 +建筑材料 建築材料 +建筑法规 建築法規 +建筑物 建築物 +建筑界 建築界 +建筑科 建築科 +建筑系 建築系 +建筑结构 建築結構 +建筑群 建築羣 +建筑艺术 建築藝術 +建筑节 建築節 +建筑设计 建築設計 +建筑起来 建築起來 +建筑面积 建築面積 +建造出 建造出 +建都于 建都於 +廿五万 廿五萬 +开不了 開不了 +开个 開個 +开了 開了 +开云见日 開雲見日 +开价 開價 +开伙 開伙 +开元录 開元錄 +开冬 開冬 +开出 開出 +开出去 開出去 +开出来 開出來 +开刀手术 開刀手術 +开列于后 開列於後 +开创出 開創出 +开化党 開化黨 +开卷 開卷 +开卷有得 開卷有得 +开卷有益 開卷有益 +开卷考 開卷考 +开厂 開廠 +开发 開發 +开发中 開發中 +开发中国 開發中國 +开发中国家 開發中國家 +开发为 開發爲 +开发井 開發井 +开发人员 開發人員 +开发出 開發出 +开发出来 開發出來 +开发区 開發區 +开发周期 開發週期 +开发商 開發商 +开发国家 開發國家 +开发基金 開發基金 +开发局 開發局 +开发成 開發成 +开发案 開發案 +开发法 開發法 +开发环境 開發環境 +开发署 開發署 +开发者 開發者 +开发过程 開發過程 +开发金 開發金 +开发银行 開發銀行 +开台 開臺 +开台圣王 開臺聖王 +开台锣鼓 開臺鑼鼓 +开吊 開弔 +开后门 開後門 +开向 開向 +开哄 開鬨 +开回 開回 +开回去 開回去 +开回来 開回來 +开复 開復 +开天辟地 開天闢地 +开彩 開彩 +开征 開徵 +开支范围 開支範圍 +开放式系统 開放式系統 +开放性医疗制度 開放性醫療制度 +开放系统 開放系統 +开放系统互连 開放系統互連 +开放舞台 開放舞臺 +开新板 開新板 +开明专制 開明專制 +开杆 開杆 +开杠 開槓 +开棺验尸 開棺驗屍 +开疆辟土 開疆闢土 +开盘价 開盤價 +开盘汇率 開盤匯率 +开秋 開秋 +开筑 開築 +开膛手杰克 開膛手傑克 +开药 開藥 +开药方 開藥方 +开蒙 開蒙 +开诚布公 開誠佈公 +开辟 開闢 +开辟出来 開闢出來 +开辟者 開闢者 +开辟记 開闢記 +开采 開採 +开采权 開採權 +开采法 開採法 +开除党籍 開除黨籍 +开面 開面 +开齐合撮 開齊合撮 +异中求同 異中求同 +异于 異於 +异动表 異動表 +异口同声 異口同聲 +异口同辞 異口同辭 +异口同音 異口同音 +异同 異同 +异形叶 異形葉 +异彩 異彩 +异志 異志 +异念 異念 +异才 異才 +异曲同工 異曲同工 +异派同源 異派同源 +异烟碱醯酸 異菸鹼醯酸 +异种 異種 +异苔同岑 異薹同岑 +异路同归 異路同歸 +异途同归 異途同歸 +异采 異采 +弃宗弄赞 棄宗弄贊 +弃尸 棄屍 +弃尸案 棄屍案 +弃弓折箭 棄弓折箭 +弃恶从善 棄惡從善 +弃暗投明 棄暗投明 +弃核 棄核 +弃武修文 棄武修文 +弃瑕录用 棄瑕錄用 +弃舍 棄捨 +弄丑 弄醜 +弄僵 弄僵 +弄出 弄出 +弄出去 弄出去 +弄出来 弄出來 +弄坏了 弄壞了 +弄干 弄乾 +弄拧了 弄擰了 +弄松 弄鬆 +弄盏传杯 弄盞傳杯 +弄粉调朱 弄粉調朱 +弄脏 弄髒 +弄面吃 弄麪吃 +弄鬼吊猴 弄鬼弔猴 +弈秋 弈秋 +弊帚千金 弊帚千金 +弊幸 弊倖 +弊恶 弊惡 +弋获 弋獲 +弓不虚发 弓不虛發 +弓弦 弓弦 +弓影杯蛇 弓影杯蛇 +弓影浮杯 弓影浮杯 +弓折矢尽 弓折矢盡 +弓折箭尽 弓折箭盡 +弓极 弓極 +弓浆虫 弓漿蟲 +弓腰曲背 弓腰曲背 +引人入胜 引人入勝 +引人注意 引人注意 +引人注目 引人注目 +引以为鉴 引以爲鑑 +引伸出 引伸出 +引体向上 引體向上 +引信系统 引信系統 +引出 引出 +引出去 引出去 +引出来 引出來 +引发 引發 +引发出 引發出 +引发物质 引發物質 +引向 引向 +引导出 引導出 +引布 引布 +引拉出 引拉出 +引据 引據 +引斗 引鬥 +引渡回国 引渡回國 +引牵出 引牽出 +引种 引種 +引线穿针 引線穿針 +引经据典 引經據典 +引经据古 引經據古 +引而不发 引而不發 +引致 引致 +引蛇出洞 引蛇出洞 +弗兰克 弗蘭克 +弗洛里斯岛 弗洛裏斯島 +弗罗里达 弗羅裏達 +弗罗里达州 弗羅裏達州 +弗里得里希 弗裏得裏希 +弗里德里希 弗里德里希 +弗里敦 弗里敦 +弗里斯兰 弗里斯蘭 +弗里曼 弗里曼 +弗雷德里克 弗雷德裏克 +弗雷德里克顿 弗雷德裏克頓 +弘历 弘曆 +弘愿 弘願 +张三丰 張三丰 +张世杰 張世傑 +张了 張了 +张云光 張雲光 +张兆志 張兆志 +张克帆 張克帆 +张出 張出 +张勇杰 張勇傑 +张勋 張勳 +张千 張千 +张基郁 張基郁 +张堪折辕 張堪折轅 +张大千 張大千 +张志 張志 +张志和 張志和 +张志家 張誌家 +张志弘 張志弘 +张志强 張志強 +张志忠 張志忠 +张志新 張志新 +张志贤 張志賢 +张志辉 張志輝 +张志铭 張志銘 +张扬出去 張揚出去 +张扬出来 張揚出來 +张挂 張掛 +张挂在 張掛在 +张挂起 張掛起 +张挂起来 張掛起來 +张文松 張文松 +张斗辉 張斗輝 +张智杰 張智傑 +张木松 張木松 +张杰 張傑 +张栋梁 張棟樑 +张灯挂彩 張燈掛彩 +张灯结彩 張燈結綵 +张玄墓志铭 張玄墓誌銘 +张琴松 張琴松 +张秋 張秋 +张秋明 張秋明 +张罗殆尽 張羅殆盡 +张聪秋 張聰秋 +张致 張致 +张良借箸 張良借箸 +张良慕赤松 張良慕赤松 +张苙云 張苙雲 +张荣发 張榮發 +张金涂 張金塗 +张飞穿针 張飛穿針 +张黑女墓志铭 張黑女墓誌銘 +弥久 彌久 +弥习弥佳 彌習彌佳 +弥事 彌事 +弥勒 彌勒 +弥勒佛 彌勒佛 +弥勒县 彌勒縣 +弥勒菩萨 彌勒菩薩 +弥天 彌天 +弥天亙地 彌天亙地 +弥天大罪 彌天大罪 +弥天大谎 彌天大謊 +弥天案 彌天案 +弥封 彌封 +弥山遍野 瀰山遍野 +弥年 彌年 +弥弥 瀰瀰 +弥撒 彌撒 +弥撒曲 彌撒曲 +弥撒经书 彌撒經書 +弥敬 彌敬 +弥时 彌時 +弥月 彌月 +弥月之喜 彌月之喜 +弥月酒 彌月酒 +弥望 彌望 +弥渡县 彌渡縣 +弥满 彌滿 +弥漫 瀰漫 +弥漫性 瀰漫性 +弥漫着 瀰漫着 +弥猴桃 彌猴桃 +弥生文化 彌生文化 +弥留 彌留 +弥留之际 彌留之際 +弥纶 彌綸 +弥缝 彌縫 +弥罗 彌羅 +弥蒙 彌矇 +弥补 彌補 +弥赛亚 彌賽亞 +弥足珍贵 彌足珍貴 +弥迦书 彌迦書 +弥陀 彌陀 +弥陀乡 彌陀鄉 +弦上 弦上 +弦不虚发 弦不虛發 +弦乐 絃樂 +弦乐团 絃樂團 +弦动 絃動 +弦器 絃器 +弦声 絃聲 +弦子 弦子 +弦弓 弦弓 +弦拨 弦撥 +弦断 絃斷 +弦月 弦月 +弦歌 絃歌 +弦琴 絃琴 +弦索 絃索 +弦线 絃線 +弦轴 絃軸 +弦重 弦重 +弦长 弦長 +弦面板 弦面板 +弦音 絃音 +弯出去 彎出去 +弯出来 彎出來 +弯回 彎回 +弯回去 彎回去 +弯回来 彎回來 +弯弯曲曲 彎彎曲曲 +弯折 彎折 +弯拐 彎拐 +弯曲 彎曲 +弯曲度 彎曲度 +弯曲状 彎曲狀 +弯曲空间 彎曲空間 +弯管面 彎管麪 +弱不胜衣 弱不勝衣 +弱于 弱於 +弱势团体 弱勢團體 +弱智赖于涵 弱智賴于涵 +弱水三千 弱水三千 +弱音踏板 弱音踏板 +弹不出 彈不出 +弹了 彈了 +弹出 彈出 +弹出去 彈出去 +弹出来 彈出來 +弹升 彈升 +弹回 彈回 +弹回去 彈回去 +弹回来 彈回來 +弹奏出 彈奏出 +弹子台 彈子檯 +弹射出 彈射出 +弹尽援绝 彈盡援絕 +弹尽粮绝 彈盡糧絕 +弹布尔 彈布爾 +弹性制造系统 彈性製造系統 +弹性控制 彈性控制 +弹性纤维 彈性纖維 +弹无虚发 彈無虛發 +弹涂鱼 彈塗魚 +弹珠台 彈珠檯 +弹药 彈藥 +弹药兵 彈藥兵 +弹药库 彈藥庫 +弹药箱 彈藥箱 +弹药补给站 彈藥補給站 +弹跳板 彈跳板 +弹针 彈針 +强了 強了 +强于 強於 +强借 強借 +强制 強制 +强制作用 強制作用 +强制保险 強制保險 +强制力 強制力 +强制处分 強制處分 +强制性 強制性 +强制手段 強制手段 +强制执行 強制執行 +强制罪 強制罪 +强制认领 強制認領 +强制辩护 強制辯護 +强制险 強制險 +强力攻占 強力攻佔 +强加于 強加於 +强加于人 強加於人 +强占 強佔 +强占性 強佔性 +强咽 強嚥 +强奸 強姦 +强奸民意 強姦民意 +强奸犯 強姦犯 +强奸罪 強姦罪 +强干 強幹 +强干弱枝 強幹弱枝 +强征 強徵 +强心针 強心針 +强投松 強投松 +强梁 強梁 +强烈台风 強烈颱風 +强烈愿望 強烈願望 +强聒不舍 強聒不捨 +强迫性储物症 強迫性儲物症 +强迫症 強迫症 +强音踏板 強音踏板 +弼针 弼針 +彊干弱枝 彊幹弱枝 +彊御 彊禦 +彊志 彊志 +彊梁 彊梁 +归于 歸於 +归余 歸餘 +归功于 歸功於 +归向 歸向 +归向导引 歸向導引 +归咎于 歸咎於 +归回 歸回 +归因于 歸因於 +归属于 歸屬於 +归并 歸併 +归并到 歸併到 +归并在 歸併在 +归烟 歸煙 +归真反朴 歸真反樸 +归类于 歸類於 +归纳出 歸納出 +归罪于 歸罪於 +归虚谷 歸虛谷 +归诸于 歸諸於 +归里包堆 歸裏包堆 +归随于 歸隨於 +归面 歸面 +归顺于 歸順於 +当一回事 當一回事 +当一声 噹一聲 +当一当 當一當 +当上 當上 +当上去 當上去 +当上来 當上來 +当下 當下 +当下去 當下去 +当下来 當下來 +当不上 當不上 +当不了 當不了 +当不得 當不得 +当不成 當不成 +当不来 當不來 +当不起 當不起 +当不过 當不過 +当且仅当 當且僅當 +当世 當世 +当世之冠 當世之冠 +当世儒宗 當世儒宗 +当世冠 當世冠 +当世才度 當世才度 +当世无双 當世無雙 +当世无敌 當世無敵 +当个 當個 +当中 當中 +当为 當爲 +当之无愧 當之無愧 +当之有愧 當之有愧 +当了 當了 +当事 當事 +当事人 當事人 +当事国 當事國 +当事者 當事者 +当于 當於 +当仁不让 當仁不讓 +当今 當今 +当今无辈 當今無輩 +当代 當代 +当代人 當代人 +当代史 當代史 +当代大师 當代大師 +当代新儒家 當代新儒家 +当令 當令 +当众 當衆 +当众宣布 當衆宣佈 +当众表明 當衆表明 +当众表示 當衆表示 +当作 當作 +当便 當便 +当值 當值 +当做 當做 +当儿 當兒 +当元 當元 +当先 當先 +当关 當關 +当兵 當兵 +当其冲 當其衝 +当准 當準 +当初 當初 +当到 當到 +当前 當前 +当前之计 當前之計 +当前季 當前季 +当务之急 當務之急 +当劳之急 當勞之急 +当午 當午 +当卢 當盧 +当即 當即 +当原 當原 +当厨 當廚 +当口 當口 +当句对 當句對 +当可 當可 +当合 當合 +当周 當週 +当啷 噹啷 +当啷落地 噹啷落地 +当回事 當回事 +当回事儿 當回事兒 +当国 當國 +当地 當地 +当地人 當地人 +当地化 當地化 +当地居民 當地居民 +当地时间 當地時間 +当场 當場 +当场出丑 當場出醜 +当场出彩 當場出彩 +当场只手 當場隻手 +当场听到 當場聽到 +当场抓到 當場抓到 +当场献丑 當場獻醜 +当场看到 當場看到 +当坊土地 當坊土地 +当垆 當壚 +当堂 當堂 +当堵 當堵 +当夕 當夕 +当夜 當夜 +当天 當天 +当天事当天毕 當天事當天畢 +当头 當頭 +当头一棒 當頭一棒 +当头人 當頭人 +当头棒喝 當頭棒喝 +当头炮 當頭炮 +当头阵 當頭陣 +当完兵 當完兵 +当官 當官 +当官追究 當官追究 +当室 當室 +当家 當家 +当家三年狗也嫌 當家三年狗也嫌 +当家人 當家人 +当家人恶水缸 當家人惡水缸 +当家作主 當家作主 +当家和尚 當家和尚 +当家子 當家子 +当家小生 當家小生 +当家才知柴米价 當家纔知柴米價 +当家方知柴米贵 當家方知柴米貴 +当家的 當家的 +当家立事 當家立事 +当家花旦 當家花旦 +当局 當局 +当局称迷傍观必审 當局稱迷傍觀必審 +当局者迷 當局者迷 +当局者迷傍观者清 當局者迷傍觀者清 +当局者迷旁观者清 當局者迷旁觀者清 +当巡 當巡 +当差 當差 +当差的 當差的 +当年 當年 +当应 當應 +当庭 當庭 +当归 當歸 +当归鸭 當歸鴨 +当当 噹噹 +当当丁丁 當當丁丁 +当当当 噹噹噹 +当当船 噹噹船 +当当车 噹噹車 +当役 當役 +当待 當待 +当得 當得 +当得上 當得上 +当得来 當得來 +当得起 當得起 +当心 當心 +当意 當意 +当成 當成 +当手 當手 +当拦 當攔 +当掉 當掉 +当撑 當撐 +当政 當政 +当政者 當政者 +当敌 當敵 +当断不断 當斷不斷 +当断不断反受其乱 當斷不斷反受其亂 +当日 當日 +当日份 當日份 +当时 當時 +当时得令 當時得令 +当时的 當時的 +当时车 當時車 +当是 當是 +当晚 當晚 +当曲河 當曲河 +当月 當月 +当月份 當月份 +当朝 當朝 +当朝宰相 當朝宰相 +当期 當期 +当机 當機 +当机立断 當機立斷 +当权 當權 +当权派 當權派 +当权者 當權者 +当村 當村 +当来 當來 +当来当去 當來當去 +当案 當案 +当槽 當槽 +当此 當此 +当涂 當塗 +当涂县 當塗縣 +当演员 當演員 +当灾 當災 +当炉 當爐 +当然 當然 +当然会 當然會 +当然在 當然在 +当然是 當然是 +当然有 當然有 +当然继承主义 當然繼承主義 +当牢节级 當牢節級 +当班 當班 +当班人员 當班人員 +当用 當用 +当用则用 當用則用 +当番 當番 +当的一响 噹的一響 +当的一声 噹的一聲 +当直 當直 +当直巡逻 當直巡邏 +当直的 當直的 +当真 當真 +当真假 當真假 +当真的 當真的 +当眼 當眼 +当眼处 當眼處 +当票 當票 +当票子 當票子 +当空 當空 +当紧 當緊 +当红 當紅 +当罏 當罏 +当罏红袖 當罏紅袖 +当者披靡 當者披靡 +当耳边风 當耳邊風 +当舖 當舖 +当艄拿舵 當艄拿舵 +当艄顺 當艄順 +当花 當花 +当行 當行 +当行出色 當行出色 +当行家 當行家 +当街 當街 +当衣买酒喝 當衣買酒喝 +当该 當該 +当赌 當賭 +当起 當起 +当起来 當起來 +当路 當路 +当路子 當路子 +当轴 當軸 +当过 當過 +当选 當選 +当选为 當選爲 +当选人 當選人 +当选无效 當選無效 +当选者 當選者 +当道 當道 +当道者 當道者 +当量 當量 +当量剂量 當量劑量 +当量浓度 當量濃度 +当铺 當鋪 +当门对户 當門對戶 +当门户 當門戶 +当门抵户 當門抵戶 +当门牙齿 當門牙齒 +当间 當間 +当阳 當陽 +当阳市 當陽市 +当院 當院 +当雄 當雄 +当雄县 當雄縣 +当面 當面 +当面唾骂 當面唾罵 +当面对质 當面對質 +当面言明 當面言明 +当面银子对面钱 當面銀子對面錢 +当面错过 當面錯過 +当面锣对面鼓 當面鑼對面鼓 +当面鼓对面锣 當面鼓對面鑼 +当风秉烛 當風秉燭 +录上 錄上 +录上去 錄上去 +录上来 錄上來 +录下 錄下 +录下去 錄下去 +录下来 錄下來 +录个 錄個 +录了 錄了 +录事 錄事 +录供 錄供 +录像 錄像 +录像带 錄像帶 +录像机 錄像機 +录像片 錄像片 +录到 錄到 +录制 錄製 +录取 錄取 +录取人数 錄取人數 +录取分数 錄取分數 +录取名单 錄取名單 +录取名额 錄取名額 +录取率 錄取率 +录取通知书 錄取通知書 +录囚 錄囚 +录在 錄在 +录录 彔彔 +录录歌 錄錄歌 +录录音 錄錄音 +录影 錄影 +录影器 錄影器 +录影带 錄影帶 +录影带奖 錄影帶獎 +录影机 錄影機 +录影碟 錄影碟 +录影节目 錄影節目 +录得 錄得 +录成 錄成 +录放影机 錄放影機 +录放音机 錄放音機 +录歌 錄歌 +录灌 錄灌 +录点 錄點 +录用 錄用 +录的 錄的 +录科 錄科 +录起 錄起 +录起来 錄起來 +录载 錄載 +录过 錄過 +录遗 錄遺 +录音 錄音 +录音室 錄音室 +录音师 錄音師 +录音带 錄音帶 +录音机 錄音機 +录音电话 錄音電話 +录音间 錄音間 +录鬼簿 錄鬼簿 +彗核 彗核 +彗氾画涂 彗氾畫塗 +形丑心善 形醜心善 +形于 形於 +形于色 形於色 +形于言色 形於言色 +形于辞色 形於辭色 +形于颜色 形於顏色 +形制 形制 +形单影只 形單影隻 +形只影单 形隻影單 +形同 形同 +形同具文 形同具文 +形同虚设 形同虛設 +形同陌路 形同陌路 +形名参同 形名參同 +形孤影只 形孤影隻 +形容尽致 形容盡致 +形影相吊 形影相弔 +形态发生素 形態發生素 +形疲神困 形疲神困 +形胜 形勝 +形象艺术 形象藝術 +彤云 彤雲 +彩云 彩雲 +彩云国 彩雲國 +彩云易散 彩雲易散 +彩信 彩信 +彩先达 彩先達 +彩光 彩光 +彩光板 彩光板 +彩凤 綵鳳 +彩凤随鸦 彩鳳隨鴉 +彩券 彩券 +彩券局 彩券局 +彩券行 彩券行 +彩印 彩印 +彩卷 彩卷 +彩叶芋 彩葉芋 +彩叶草 彩葉草 +彩号 彩號 +彩喷 彩噴 +彩图 彩圖 +彩塑 彩塑 +彩墨 彩墨 +彩头 彩頭 +彩女 綵女 +彩妆 彩妝 +彩妆保养化 彩妝保養化 +彩妆品 彩妝品 +彩妆师 彩妝師 +彩市 彩市 +彩带 綵帶 +彩带舞 綵帶舞 +彩度 彩度 +彩弹 彩彈 +彩影 彩影 +彩扩 彩擴 +彩排 彩排 +彩旗 彩旗 +彩旦 彩旦 +彩条 彩條 +彩棚 綵棚 +彩楼 綵樓 +彩楼配 綵樓配 +彩民 彩民 +彩池 彩池 +彩灯 彩燈 +彩照 彩照 +彩牌楼 綵牌樓 +彩球 綵球 +彩瓷 彩瓷 +彩电 彩電 +彩电视 彩電視 +彩画 彩畫 +彩砖 彩磚 +彩礼 彩禮 +彩票 彩票 +彩票卡 彩票卡 +彩笔 彩筆 +彩笔生 彩筆生 +彩笔生花 綵筆生花 +彩管生花 彩管生花 +彩纸 彩紙 +彩线 綵線 +彩练 彩練 +彩绘 彩繪 +彩绸 綵綢 +彩缎 綵緞 +彩缯 綵繒 +彩胜 綵勝 +彩船 綵船 +彩色 彩色 +彩色世界 彩色世界 +彩色正片 彩色正片 +彩色照 彩色照 +彩色照片 彩色照片 +彩色片 彩色片 +彩色版 彩色版 +彩色玻璃 彩色玻璃 +彩色电影 彩色電影 +彩色电视 彩色電視 +彩色电视机 彩色電視機 +彩色笔 彩色筆 +彩色米 彩色米 +彩色缤纷 彩色繽紛 +彩虹 彩虹 +彩虹仙子 彩虹仙子 +彩虹冰舖 彩虹冰舖 +彩虹桥 彩虹橋 +彩蛋 彩蛋 +彩蝶 彩蝶 +彩蝶谷 彩蝶谷 +彩衣 綵衣 +彩衣娱亲 綵衣娛親 +彩调 彩調 +彩车 彩車 +彩轿 彩轎 +彩迷 彩迷 +彩釉 彩釉 +彩金 彩金 +彩钱 彩錢 +彩铃 彩鈴 +彩陶 彩陶 +彩陶文化 彩陶文化 +彩雕 彩雕 +彩霞 彩霞 +彩饰 彩飾 +彩饰版 彩飾版 +彩鸾 綵鸞 +彩鹢 彩鷁 +彪个子 彪個子 +彪炳千古 彪炳千古 +彭于晏 彭于晏 +彭克港 彭克港 +彭咸 彭咸 +彭志光 彭志光 +彭志华 彭志華 +彭绍升 彭紹升 +彭胜竹 彭勝竹 +彭蒙 彭蒙 +彰化师范大学 彰化師範大學 +彰善瘅恶 彰善癉惡 +彰彰可据 彰彰可據 +彰显出 彰顯出 +影像合成 影像合成 +影像处理系统 影像處理系統 +影占 影占 +影只形单 影隻形單 +影后 影后 +影响不了 影響不了 +影坛 影壇 +影坛红星 影壇紅星 +影评人周 影評人週 +役于 役於 +役于外物 役於外物 +役别 役別 +役种 役種 +彻里彻外 徹裏徹外 +彼得后书 彼得後書 +彼得里皿 彼得里皿 +彼此克制 彼此剋制 +往上面 往上面 +往下面 往下面 +往事如烟 往事如煙 +往前面 往前面 +往后 往後 +往后冲 往後衝 +往后方 往後方 +往后面 往後面 +往回 往回 +往复 往復 +往复泵 往復泵 +往复运动 往復運動 +往外冲 往外衝 +往外面 往外面 +往日無仇 往日無讎 +往来于 往來於 +往泥里踩 往泥裏踩 +往还于 往還於 +往里 往裏 +往里面 往裏面 +往里面冲 往裏面衝 +征了 徵了 +征人 徵人 +征令 徵令 +征伐 征伐 +征信 徵信 +征信录 徵信錄 +征信所 徵信所 +征信社 徵信社 +征候 徵候 +征候群 徵候羣 +征兆 徵兆 +征兵 徵兵 +征兵制 徵兵制 +征兵制度 徵兵制度 +征兵法 徵兵法 +征兵站 徵兵站 +征到 徵到 +征剿 征剿 +征募 徵募 +征占 徵佔 +征友 徵友 +征友栏 徵友欄 +征发 徵發 +征召 徵召 +征名责实 徵名責實 +征吏 徵吏 +征启 徵啓 +征咎 徵咎 +征圣 徵聖 +征地 徵地 +征士 徵士 +征夫 征夫 +征婚 徵婚 +征实 徵實 +征尘 征塵 +征帆 征帆 +征庸 徵庸 +征引 徵引 +征彸 征彸 +征得 徵得 +征怪 徵怪 +征意见 徵意見 +征戍 征戍 +征战 征戰 +征才 徵才 +征招 徵招 +征收 徵收 +征收范围 徵收範圍 +征收额 徵收額 +征效 徵效 +征敛 征斂 +征敛无度 征斂無度 +征文 徵文 +征文启事 徵文啓事 +征文比赛 徵文比賽 +征旆 征旆 +征服 征服 +征服兵 征服兵 +征服到地 征服到地 +征服者 征服者 +征求 徵求 +征求启事 徵求啓事 +征状 徵狀 +征用 徵用 +征程 征程 +征税 徵稅 +征税额 徵稅額 +征稿 徵稿 +征稿栏 徵稿欄 +征答 徵答 +征管 徵管 +征粮 徵糧 +征纳 徵納 +征结 徵結 +征缴 徵繳 +征聘 徵聘 +征聘人员 徵聘人員 +征衣 征衣 +征衫 征衫 +征讨 征討 +征训 徵訓 +征询 徵詢 +征调 徵調 +征象 徵象 +征购 徵購 +征费 徵費 +征车 徵車 +征辟 徵辟 +征迹 徵跡 +征选 徵選 +征逐 徵逐 +征途 征途 +征集 徵集 +征集令 徵集令 +征风召雨 徵風召雨 +征马 征馬 +征驾 征駕 +征验 徵驗 +征验出 徵驗出 +径向 徑向 +待了 待了 +待价而沽 待價而沽 +待价藏珠 待價藏珠 +待制 待制 +待发 待發 +待在家里 待在家裏 +待如己出 待如己出 +待核 待覈 +很丑 很醜 +很僵 很僵 +很凶 很兇 +很咸 很鹹 +很干 很乾 +很干了 很乾了 +很暗 很暗 +很松 很鬆 +律历志 律曆志 +律师团 律師團 +徐伟胜 徐偉勝 +徐余伟 徐余偉 +徐克 徐克 +徐千惠 徐千惠 +徐台荪 徐臺蓀 +徐台荪宫仲毅 徐臺蓀宮仲毅 +徐妃半面妆 徐妃半面妝 +徐娘 徐娘 +徐娘半老 徐娘半老 +徐家汇 徐家彙 +徐州师范大学 徐州師範大學 +徐干 徐幹 +徐志摩 徐志摩 +徐汇 徐匯 +徐汇区 徐彙區 +徐清云 徐清雲 +徐赞升 徐讚昇 +徐霞客游记 徐霞客遊記 +徒托空言 徒託空言 +徒步当车 徒步當車 +得了 得了 +得了些颜色就开起染房来 得了些顏色就開起染房來 +得于 得於 +得偿所愿 得償所願 +得克制 得剋制 +得克萨斯州 得克薩斯州 +得克萨斯州沃思堡电 得克薩斯州沃思堡電 +得准不准 得準不準 +得出 得出 +得失参半 得失參半 +得幸 得幸 +得当 得當 +得志 得志 +得意之余 得意之餘 +得放手时须放手 得放手時須放手 +得未尝有 得未嘗有 +得益于 得益於 +得而复失 得而復失 +得胜 得勝 +得胜之兵 得勝之兵 +得胜之师 得勝之師 +得胜口 得勝口 +得胜回朝 得勝回朝 +得胜头回 得勝頭回 +得胜的猫儿欢似虎 得勝的貓兒歡似虎 +得胜课 得勝課 +得道升天 得道昇天 +得采 得采 +得黄金百不如得季布诺 得黃金百不如得季布諾 +得黄金百斤不如得季布一诺 得黃金百斤不如得季布一諾 +徘回 徘迴 +徘徊于 徘徊於 +徜徉于 徜徉於 +御世 御世 +御书 御書 +御书房 御書房 +御人 御人 +御仗 御仗 +御侮 禦侮 +御制 御製 +御前 御前 +御前会议 御前會議 +御医 御醫 +御博表 御博表 +御厨 御廚 +御史 御史 +御史台 御史臺 +御史大夫 御史大夫 +御史雨 御史雨 +御夫 御夫 +御夫座 御夫座 +御夫有术 御夫有術 +御孙 御孫 +御宅族 御宅族 +御宇 御宇 +御守 御守 +御宝 御寶 +御容 御容 +御寇 禦寇 +御寒 禦寒 +御库 御庫 +御庙 御廟 +御府 御府 +御弟 御弟 +御所 御所 +御手 御手 +御手洗 御手洗 +御敌 禦敵 +御旨 御旨 +御札 御札 +御极 御極 +御林军 御林軍 +御案 御案 +御气 御氣 +御沟 御溝 +御沟流叶 御溝流葉 +御沟题叶 御溝題葉 +御河 御河 +御状 御狀 +御用 御用 +御碑亭 御碑亭 +御窑 御窯 +御笔 御筆 +御者 御者 +御膳 御膳 +御膳房 御膳房 +御花园 御花園 +御苑 御苑 +御览 御覽 +御赐 御賜 +御轮 御輪 +御道 御道 +御酒 御酒 +御风 御風 +御馔 御饌 +御驾 御駕 +御驾亲征 御駕親征 +御龙 御龍 +循环制 循環制 +循环反复 循環反覆 +循环往复 循環往復 +循环系统 循環系統 +循环赛制 循環賽制 +微克 微克 +微分几何 微分幾何 +微分几何学 微分幾何學 +微升 微升 +微卷 微卷 +微孔板 微孔板 +微居里 微居里 +微微的发烫 微微的發燙 +微核 微核 +微波倒送系统 微波倒送系統 +微纤 微纖 +微胶囊技术 微膠囊技術 +微雕 微雕 +徯幸 徯倖 +德佛亚克 德佛亞克 +德克萨斯 德克薩斯 +德克萨斯州 德克薩斯州 +德兰克林 德蘭克林 +德勒巴克 德勒巴克 +德占 德佔 +德国学术交流总署 德國學術交流總署 +德国杯 德國杯 +德国统一社会党 德國統一社會黨 +德国马克 德國馬克 +德垂后裔 德垂後裔 +德布勒森 德布勒森 +德布西 德布西 +德干 德干 +德干高原 德干高原 +德弗札克 德弗札克 +德意志 德意志 +德意志关税同盟 德意志關稅同盟 +德意志学术交流中心 德意志學術交流中心 +德意志民主共和国 德意志民主共和國 +德意志联邦共和国 德意志聯邦共和國 +德意志银行 德意志銀行 +德意志革命 德意志革命 +德才 德才 +德才兼备 德才兼備 +德拉克洛瓦 德拉克洛瓦 +德拉克罗瓦 德拉克羅瓦 +德文系 德文系 +德沃夏克 德沃夏克 +德法年鉴 德法年鑑 +德润佣书 德潤傭書 +德瑞克 德瑞克 +德胜头回 德勝頭迴 +德胜门 德勝門 +德薄才疏 德薄才疏 +德语系 德語系 +德谟克利泰斯 德謨克利泰斯 +德谟克拉西 德謨克拉西 +德里 德里 +德里达 德里達 +德高而毁来 德高而譭來 +徼幸 徼倖 +心不甘情不愿 心不甘情不願 +心于 心於 +心余 心餘 +心凉了半截 心涼了半截 +心口合一 心口合一 +心同此理 心同此理 +心向 心向 +心向往之 心嚮往之 +心向祖国 心向祖國 +心回意转 心回意轉 +心头小鹿撞个不住 心頭小鹿撞個不住 +心存侥幸 心存僥倖 +心安神闲 心安神閒 +心宽出少年 心寬出少年 +心弦 心絃 +心志 心志 +心念 心念 +心意回转 心意回轉 +心愿 心願 +心慈面软 心慈面軟 +心折 心折 +心折首肯 心折首肯 +心曲 心曲 +心有余力不足 心有餘力不足 +心有余悸 心有餘悸 +心有余而力不足 心有餘而力不足 +心有戚戚 心有慼慼 +心服情愿 心服情願 +心术 心術 +心术不正 心術不正 +心杯 心杯 +心欲专凿石穿 心欲專鑿石穿 +心活面软 心活面軟 +心游 心遊 +心满愿足 心滿願足 +心物合一 心物合一 +心理学系 心理學系 +心理系 心理系 +心理面 心理面 +心甘情愿 心甘情願 +心相系 心相繫 +心神专注 心神專注 +心系 心繫 +心细似发 心細似髮 +心细如发 心細如髮 +心肺复苏术 心肺復甦術 +心脏 心臟 +心脏地区 心臟地區 +心脏复苏术 心臟復甦術 +心脏学 心臟學 +心脏按摩 心臟按摩 +心脏搭桥手术 心臟搭橋手術 +心脏收缩压 心臟收縮壓 +心脏瓣 心臟瓣 +心脏疾患 心臟疾患 +心脏病 心臟病 +心脏病发 心臟病發 +心脏病史 心臟病史 +心脏痲痹 心臟痲痹 +心脏痲痺 心臟痲痺 +心脏科 心臟科 +心脏移植 心臟移植 +心脏移殖 心臟移殖 +心脏舒张压 心臟舒張壓 +心脏节律器 心臟節律器 +心脏衰竭 心臟衰竭 +心脏计 心臟計 +心脏镜 心臟鏡 +心脏麻痹 心臟麻痹 +心脏麻痺 心臟麻痺 +心花怒发 心花怒發 +心荡 心蕩 +心荡神怡 心蕩神怡 +心荡神摇 心蕩神搖 +心荡神迷 心蕩神迷 +心荡神驰 心蕩神馳 +心药 心藥 +心虔志诚 心虔志誠 +心路历程 心路歷程 +心里 心裏 +心里不安 心裏不安 +心里头 心裏頭 +心里有个谱 心裏有個譜 +心里有数 心裏有數 +心里有谱 心裏有譜 +心里有鬼 心裏有鬼 +心里痒痒 心裏癢癢 +心里美萝卜 心裏美蘿蔔 +心里话 心裏話 +心里面 心裏面 +心长发短 心長髮短 +心闲手敏 心閒手敏 +心高遮了太阳 心高遮了太陽 +必修 必修 +必修科 必修科 +必修课 必修課 +必修课程 必修課程 +必备良药 必備良藥 +必当 必當 +必死之症 必死之症 +必胜 必勝 +必胜客 必勝客 +必须 必須 +忆念 憶念 +忌烟 忌菸 +忍个 忍個 +忍了 忍了 +忍冬 忍冬 +忍冬花 忍冬花 +忍受不了 忍受不了 +忍受得了 忍受得了 +忍术 忍術 +忍饥受渴 忍饑受渴 +忍饥受饿 忍饑受餓 +忍饥挨饿 忍飢挨餓 +忏悔录 懺悔錄 +忒恶 忒惡 +忖前思后 忖前思後 +志不可夺 志不可奪 +志不可满 志不可滿 +志不在此 志不在此 +志业 志業 +志丹 志丹 +志丹县 志丹縣 +志乃 志乃 +志之不忘 誌之不忘 +志书 志書 +志事 志事 +志于 志於 +志仁 志仁 +志伟 志偉 +志保 志保 +志冲斗牛 志沖斗牛 +志分 志分 +志同心合 志同心合 +志同气合 志同氣合 +志同道合 志同道合 +志向 志向 +志哀 誌哀 +志喜 誌喜 +志在 志在 +志在千里 志在千里 +志在四方 志在四方 +志在必得 志在必得 +志坚 志堅 +志坚胆壮 志堅膽壯 +志士 志士 +志士仁人 志士仁人 +志大心高 志大心高 +志大才疏 志大才疏 +志大才短 志大才短 +志学 志學 +志尚 志尚 +志工 志工 +志工团 志工團 +志工队 志工隊 +志广才疏 志廣才疏 +志庆 誌慶 +志度 志度 +志异 誌異 +志强 志強 +志得意满 志得意滿 +志心 志心 +志怪 志怪 +志怪小说 志怪小說 +志悼 誌悼 +志意 志意 +志愿 志願 +志愿书 志願書 +志愿兵 志願兵 +志愿军 志願軍 +志愿卡 志願卡 +志愿役 志願役 +志愿者 志願者 +志成 志成 +志文 志文 +志明 志明 +志村健 志村健 +志杰 志傑 +志气 志氣 +志气凌云 志氣凌雲 +志清 志清 +志满气得 志滿氣得 +志玲 志玲 +志田 志田 +志留系 志留系 +志留纪 志留紀 +志略 志略 +志能之士 志能之士 +志航基地 志航基地 +志节 志節 +志英 志英 +志行 志行 +志诚 志誠 +志诚君子 志誠君子 +志贞 志貞 +志趣 志趣 +志趣相投 志趣相投 +志足意满 志足意滿 +志量 志量 +志骄意满 志驕意滿 +志高气扬 志高氣揚 +忘不了 忘不了 +忘了 忘了 +忘忧谷 忘憂谷 +忘生舍死 忘生捨死 +忙不择价 忙不擇價 +忙了手脚 忙了手腳 +忙于 忙於 +忙作一团 忙作一團 +忙并 忙併 +忙成一团 忙成一團 +忙昏了头 忙昏了頭 +忙进忙出 忙進忙出 +忙里 忙裏 +忙里偷闲 忙裏偷閒 +忙里忙外 忙裏忙外 +忠于 忠於 +忠于国家 忠於國家 +忠于职守 忠於職守 +忠人之托 忠人之托 +忠仆 忠僕 +忧喜参半 憂喜參半 +忧形于色 憂形於色 +忧戚 憂戚 +忧郁 憂鬱 +忧郁剂 憂鬱劑 +忧郁症 憂鬱症 +快乐幸福 快樂幸福 +快了 快了 +快借 快借 +快停了 快停了 +快克 快克 +快克制 快剋制 +快出 快出 +快出去 快出去 +快出来 快出來 +快升 快升 +快去快回 快去快回 +快吃干 快吃乾 +快向 快向 +快回 快回 +快回到 快回到 +快回去 快回去 +快回来 快回來 +快好了 快好了 +快完了 快完了 +快干 快乾 +快干了 快乾了 +快干杯 快乾杯 +快干裂 快乾裂 +快当 快當 +快快当当 快快當當 +快搜 快搜 +快松下 快鬆下 +快板 快板 +快板儿 快板兒 +快死了 快死了 +快没了 快沒了 +快满了 快滿了 +快熟了 快熟了 +快舍下 快捨下 +快赢了 快贏了 +快适 快適 +快递杯 快遞杯 +快速发展 快速發展 +快速面 快速麪 +念一 念一 +念上 念上 +念不 念不 +念中 念中 +念之 念之 +念书 唸書 +念了 唸了 +念了一声 唸了一聲 +念他 念他 +念以 念以 +念佛 唸佛 +念作 唸作 +念你 念你 +念儿 念兒 +念冰 念冰 +念出 念出 +念到 唸到 +念力 念力 +念及 念及 +念叨 唸叨 +念可 念可 +念吧 唸吧 +念和 念和 +念咒 唸咒 +念啊 唸啊 +念在 念在 +念头 念頭 +念她 念她 +念好 念好 +念完 唸完 +念对 唸對 +念得 念得 +念心 念心 +念念 念念 +念念有词 唸唸有詞 +念情 念情 +念想 念想 +念慈 念慈 +念成 念成 +念我 念我 +念日 念日 +念旧 念舊 +念是 念是 +念曰 唸曰 +念曲叫曲 念曲叫曲 +念有 念有 +念来 念來 +念此 念此 +念母 念母 +念法 念法 +念点 念點 +念珠 念珠 +念琛 念琛 +念生 念生 +念白 唸白 +念的 唸的 +念着 念着 唸着 +念经 唸經 +念给 念給 +念诗 唸詩 +念诵 唸誦 +念起 念起 +念过 念過 +念这 念這 +念道 念道 +念都 念都 +念错 唸錯 +念青 念青 +念鱼 念魚 +忽前忽后 忽前忽後 +忽明忽暗 忽明忽暗 +忽舍下 忽捨下 +忿发 忿發 +怀了 懷了 +怀宠尸位 懷寵尸位 +怀忧丧志 懷憂喪志 +怀念 懷念 +怀恶不悛 懷惡不悛 +怀才不遇 懷才不遇 +怀才抱德 懷才抱德 +怀表 懷錶 +怀里 懷裏 +怀钟 懷鐘 +怎么 怎麼 +怎么了 怎麼了 +怎么回事 怎麼回事 +怎么干 怎麼幹 +怎么得了 怎麼得了 +怎么着 怎麼着 +怒于 怒於 +怒从心上起恶向胆边生 怒從心上起惡向膽邊生 +怒冲冲 怒衝衝 +怒发冲冠 怒髮衝冠 +怒发冲天 怒髮沖天 +怒容满面 怒容滿面 +怒形于色 怒形於色 +怒恶 怒惡 +怒气冲冲 怒氣衝衝 +怒气冲发 怒氣沖發 +怒气冲天 怒氣沖天 +怒江大峡谷 怒江大峽谷 +怒火万丈 怒火萬丈 +怒火冲天 怒火沖天 +怒目相向 怒目相向 +怒臂当车 怒臂當車 +怙恶不悛 怙惡不悛 +怙恶不改 怙惡不改 +怜才 憐才 +思不出位 思不出位 +思前思后 思前思後 +思前想后 思前想後 +思前算后 思前算後 +思如泉涌 思如泉涌 +思念 思念 +思想体系 思想體系 +思想准备 思想準備 +思致 思致 +思虑周详 思慮周詳 +怠于 怠於 +急于 急於 +急于星火 急於星火 +急于求成 急於求成 +急人之困 急人之困 +急冲 急衝 +急冲而下 急衝而下 +急升 急升 +急并各邦 急並各邦 +急征重敛 急征重斂 +急松松 急鬆鬆 +急水也有回头浪 急水也有回頭浪 +急症 急症 +急进党 急進黨 +急重症 急重症 +急须 急須 +性丑闻 性醜聞 +性价比 性價比 +性伙伴 性夥伴 +性倾向 性傾向 +性冲动 性衝動 +性别 性別 +性别歧视 性別歧視 +性别比 性別比 +性别角色 性別角色 +性发 性發 +性取向 性取向 +性向 性向 +性向测验 性向測驗 +性征 性徵 +性恶 性惡 +性恶说 性惡說 +性指向 性指向 +性格不合 性格不合 +性欲 性慾 +性欲高潮 性慾高潮 +性泼凶顽 性潑凶頑 +性神经症 性神經症 +性荷尔蒙 性荷爾蒙 +性饥渴 性飢渴 +怨仇 怨仇 +怨叹 怨嘆 +怨念 怨念 +怨气冲天 怨氣沖天 +怪了 怪了 +怪杰 怪傑 +怪里怪气 怪里怪氣 +怫郁 怫鬱 +怯症 怯症 +总会杯 總會杯 +总体规划 總體規劃 +总厂 總廠 +总参谋部 總參謀部 +总参谋长 總參謀長 +总发 總髮 +总台 總檯 +总合 總合 +总后勤部 總後勤部 +总回报 總回報 +总干事 總幹事 +总批发 總批發 +总方针 總方針 +总星系 總星系 +总机厂 總機廠 +总杆数 總桿數 +总杆赛 總桿賽 +总汇 總彙 +总统制 總統制 +总统杯 總統盃 +总裁制 總裁制 +总量管制 總量管制 +总面积 總面積 +恂栗 恂慄 +恃才傲物 恃才傲物 +恃才矜己 恃才矜己 +恃才自专 恃才自專 +恋念 戀念 +恋恋不舍 戀戀不捨 +恋恋难舍 戀戀難捨 +恋曲 戀曲 +恋生恶死 戀生惡死 +恋酒贪杯 戀酒貪杯 +恐变症 恐變症 +恐同症 恐同症 +恐后争先 恐後爭先 +恐怖症 恐怖症 +恐惧症 恐懼症 +恐慌症 恐慌症 +恐旷症 恐曠症 +恐水症 恐水症 +恐法症 恐法症 +恐韩症 恐韓症 +恐高症 恐高症 +恐鸡症 恐雞症 +恒星周期 恆星週期 +恒春野百合 恆春野百合 +恒生指数 恆生指數 +恒生股价指数 恆生股價指數 +恒生银行 恆生銀行 +恒言录 恆言錄 +恕乏价催 恕乏价催 +恙虫 恙蟲 +恙虫病 恙蟲病 +恢台 恢臺 +恢复 恢復 +恢复为 恢復爲 +恢复到 恢復到 +恢复原状 恢復原狀 +恢复名誉 恢復名譽 +恢复室 恢復室 +恢复常态 恢復常態 +恢复期 恢復期 +恢复起来 恢復起來 +恢复过来 恢復過來 +恢恢有余 恢恢有餘 +恣心所欲 恣心所欲 +恣情纵欲 恣情縱欲 +恤典 卹典 +恤荒 卹荒 +恤金 卹金 +恨了 恨了 +恨意尽消 恨意盡消 +恨苦修行 恨苦修行 +恨透了 恨透了 +恩仇 恩仇 +恩仇记 恩仇記 +恩克巴雅尔 恩克巴雅爾 +恩克鲁玛 恩克魯瑪 +恩准 恩准 +恩台 恩臺 +恩同再造 恩同再造 +恩同父母 恩同父母 +恩培多克勒 恩培多克勒 +恩威克 恩威克 +恩威并施 恩威並施 +恩威并济 恩威並濟 +恩威并用 恩威並用 +恩威并行 恩威並行 +恩威并重 恩威並重 +恩将仇报 恩將仇報 +恩将仇报者 恩將仇報者 +恩幸 恩幸 +恩给制 恩給制 +恩艾斯克 恩艾斯克 +恬淡寡欲 恬淡寡欲 +恬淡无欲 恬淡無欲 +恬适 恬適 +恭喜发财 恭喜發財 +息交绝游 息交絕遊 +息谷 息穀 +恰克 恰克 +恰克图 恰克圖 +恰克图条约 恰克圖條約 +恰当 恰當 +恰才 恰纔 +恳愿 懇願 +恳托 懇託 +恶业 惡業 +恶习 惡習 +恶习不改 惡習不改 +恶事 惡事 +恶事传千里 惡事傳千里 +恶人 惡人 +恶人先告状 惡人先告狀 +恶人有恶报 惡人有惡報 +恶人自有恶人磨 惡人自有惡人磨 +恶仗 惡仗 +恶作剧 惡作劇 +恶作剧者 惡作劇者 +恶例 惡例 +恶俗 惡俗 +恶兆 惡兆 +恶党 惡黨 +恶凶凶 惡兇兇 +恶劣 惡劣 +恶劣影响 惡劣影響 +恶劣性 惡劣性 +恶势力 惡勢力 +恶化 惡化 +恶化趋势 惡化趨勢 +恶化顷向 惡化頃向 +恶叉白赖 惡叉白賴 +恶发 惡發 +恶口 惡口 +恶名 惡名 +恶名儿 惡名兒 +恶名昭彰 惡名昭彰 +恶名昭著 惡名昭著 +恶哏哏 惡哏哏 +恶唑啉 噁唑啉 +恶唑啉酮 噁唑啉酮 +恶因 惡因 +恶地 惡地 +恶声 惡聲 +恶女 惡女 +恶女阿楚 惡女阿楚 +恶妇 惡婦 +恶婆 惡婆 +恶嫌 惡嫌 +恶子 惡子 +恶孽 惡孽 +恶客 惡客 +恶寒 惡寒 +恶少 惡少 +恶岁 惡歲 +恶形 惡形 +恶形恶状 惡形惡狀 +恶徒 惡徒 +恶德 惡德 +恶心 噁心 +恶心感 噁心感 +恶念 惡念 +恶怜 惡憐 +恶性 惡性 +恶性不改 惡性不改 +恶性倒闭 惡性倒閉 +恶性循环 惡性循環 +恶性疟原虫 惡性瘧原蟲 +恶性瘤 惡性瘤 +恶性竞争 惡性競爭 +恶性肿瘤 惡性腫瘤 +恶性补习 惡性補習 +恶性贫血 惡性貧血 +恶性通货膨胀 惡性通貨膨脹 +恶恶 惡惡 +恶恶从短 惡惡從短 +恶恶实实 惡惡實實 +恶意 惡意 +恶意中伤 惡意中傷 +恶意代码 惡意代碼 +恶意毁谤 惡意譭謗 +恶感 惡感 +恶战 惡戰 +恶报 惡報 +恶搞 惡搞 +恶搞文化 惡搞文化 +恶支杀 惡支殺 +恶政 惡政 +恶斗 惡鬥 +恶曜 惡曜 +恶月 惡月 +恶有 惡有 +恶有善报 惡有善報 +恶有恶报 惡有惡報 +恶极 惡極 +恶果 惡果 +恶梦 惡夢 +恶梦探侦 惡夢探偵 +恶棍 惡棍 +恶棍歹徒 惡棍歹徒 +恶模恶样 惡模惡樣 +恶毒 惡毒 +恶气 惡氣 +恶水 惡水 +恶汉 惡漢 +恶法 惡法 +恶浊 惡濁 +恶浪 惡浪 +恶湿居下 惡溼居下 +恶灵 惡靈 +恶煞 惡煞 +恶犬 惡犬 +恶狗 惡狗 +恶狠 惡狠 +恶狠狠 惡狠狠 +恶狼 惡狼 +恶疮 惡瘡 +恶疾 惡疾 +恶病质 惡病質 +恶癖 惡癖 +恶直丑正 惡直醜正 +恶相 惡相 +恶眉恶眼 惡眉惡眼 +恶神 惡神 +恶积祸盈 惡積禍盈 +恶稔祸盈 惡稔禍盈 +恶稔罪盈 惡稔罪盈 +恶稔贯盈 惡稔貫盈 +恶紫夺朱 惡紫奪朱 +恶终 惡終 +恶缘恶业 惡緣惡業 +恶耗 惡耗 +恶臭 惡臭 +恶臭味 惡臭味 +恶臭性 惡臭性 +恶臭扑鼻 惡臭撲鼻 +恶行 惡行 +恶衣恶食 惡衣惡食 +恶衣粝食 惡衣糲食 +恶衣菲食 惡衣菲食 +恶衣蔬食 惡衣蔬食 +恶补 惡補 +恶言 惡言 +恶言伤人 惡言傷人 +恶言恶语 惡言惡語 +恶言泼语 惡言潑語 +恶言相向 惡言相向 +恶言詈辞 惡言詈辭 +恶讯 惡訊 +恶识 惡識 +恶诧 惡詫 +恶语 惡語 +恶语中伤 惡語中傷 +恶语伤人 惡語傷人 +恶语相向 惡語相向 +恶质 惡質 +恶质化 惡質化 +恶贯满盈 惡貫滿盈 +恶贯祸盈 惡貫禍盈 +恶贼 惡賊 +恶赖 惡賴 +恶趣 惡趣 +恶躁 惡躁 +恶运 惡運 +恶运当头 惡運當頭 +恶迹 惡跡 +恶逆 惡逆 +恶道 惡道 +恶醉强酒 惡醉強酒 +恶阻 惡阻 +恶限 惡限 +恶障 惡障 +恶露 惡露 +恶霸 惡霸 +恶霸地主 惡霸地主 +恶霸成性 惡霸成性 +恶风 惡風 +恶食 惡食 +恶马恶人骑 惡馬惡人騎 +恶骂 惡罵 +恶鬼 惡鬼 +恶魔 惡魔 +恶魔党 惡魔黨 +恶魔岛 惡魔島 +悍药 悍藥 +悒郁 悒鬱 +悒郁不忿 悒鬱不忿 +悒郁寡欢 悒鬱寡歡 +悔不当初 悔不當初 +悖入悖出 悖入悖出 +悚栗 悚慄 +悟出 悟出 +悟出来 悟出來 +悠哉游哉 悠哉遊哉 +悠悠荡荡 悠悠盪盪 +悠暗 悠闇 +悠活丽致 悠活麗緻 +悠游 悠遊 +悠游卡 悠遊卡 +悠游表 悠遊錶 +悠然自适 悠然自適 +悠荡 悠盪 +悠闲地 悠閒地 +悠闲自在 悠閒自在 +患难之交才是真正的朋友 患難之交纔是真正的朋友 +您克制 您剋制 +悬吊 懸吊 +悬岩 懸巖 +悬岩峭壁 懸巖峭壁 +悬心吊胆 懸心吊膽 +悬念 懸念 +悬挂 懸掛 +悬挂国旗 懸掛國旗 +悬挂在 懸掛在 +悬挂式滑翔 懸掛式滑翔 +悬挂式滑翔机 懸掛式滑翔機 +悬挂物 懸掛物 +悬旌万里 懸旌萬里 +悬梁 懸樑 +悬梁刺股 懸樑刺股 +悬梁自尽 懸樑自盡 +悬河注水 懸河注水 +悬河注火 懸河注火 +悬灯结彩 懸燈結彩 +悬肠挂肚 懸腸掛肚 +悬胄 懸冑 +悬臂梁 懸臂樑 +悬车致仕 懸車致仕 +悬针 懸針 +悬针垂露 懸針垂露 +悬钟 懸鐘 +悭吝苦克 慳吝苦剋 +悲不自胜 悲不自勝 +悲凄 悲悽 +悲叹 悲嘆 +悲咽 悲咽 +悲喜交并 悲喜交並 +悲回风 悲回風 +悲怆交响曲 悲愴交響曲 +悲悲戚戚 悲悲慼慼 +悲愿 悲願 +悲戚 悲慼 +悲欢合散 悲歡合散 +悲欢离合 悲歡離合 +悲歌当哭 悲歌當哭 +悲泗淋漓 悲泗淋漓 +悲犬咸阳 悲犬咸陽 +悲痛欲绝 悲痛欲絕 +悲秋 悲秋 +悲秋伤春 悲秋傷春 +悲筑 悲筑 +悲谷 悲谷 +悲郁 悲鬱 +悸栗 悸慄 +悼念 悼念 +惄如调饥 惄如調饑 +情不愿 情不願 +情不自胜 情不自勝 +情人眼里出西施 情人眼裏出西施 +情人眼里有西施 情人眼裏有西施 +情仇 情仇 +情僧录 情僧錄 +情况证据 情況證據 +情同一家 情同一家 +情同手足 情同手足 +情同骨肉 情同骨肉 +情同鱼水 情同魚水 +情孚意合 情孚意合 +情弦 情弦 +情志 情志 +情急了 情急了 +情感冲动 情感衝動 +情愿 情願 +情投意合 情投意合 +情报系统 情報系統 +情文并茂 情文並茂 +情有所钟 情有所鍾 +情有独钟 情有獨鍾 +情欲 情慾 +情欲戏 情慾戲 +情溢于表 情溢於表 +情种 情種 +情系 情繫 +情至意尽 情至意盡 +情致 情致 +情采 情采 +情面 情面 +情面难却 情面難卻 +惇朴 惇樸 +惊叹 驚歎 +惊恐万分 驚恐萬分 +惊恐万状 驚恐萬狀 +惊才绝艳 驚才絕豔 +惊赞 驚讚 +惊钟 驚鐘 +惊闺叶 驚閨葉 +惊险百出 驚險百出 +惋叹 惋嘆 +惏栗 惏慄 +惑志 惑志 +惑术 惑術 +惕栗 惕慄 +惜别 惜別 +惜别会 惜別會 +惟天可表 惟天可表 +惠塔克 惠塔克 +惠里香 惠里香 +惠鉴 惠鑑 +惦念 惦念 +惦挂 惦掛 +惧于 懼於 +惧高症 懼高症 +惨历 慘歷 +惨戚 慘慼 +惨栗 慘慄 +惨遭不幸 慘遭不幸 +惩前毖后 懲前毖後 +惩忿窒欲 懲忿窒欲 +惩恶劝善 懲惡勸善 +惩恶奖善 懲惡獎善 +惬当 愜當 +惯于 慣於 +惯性系 慣性系 +惯摆 慣擺 +想不出 想不出 +想不出来 想不出來 +想个 想個 +想个办法 想個辦法 +想个方法 想個方法 +想也别想 想也別想 +想了 想了 +想了又想 想了又想 +想借 想借 +想像出 想像出 +想像出来 想像出來 +想克制 想剋制 +想出 想出 +想出去 想出去 +想出来 想出來 +想前顾后 想前顧後 +想回 想回 +想回去 想回去 +想回来 想回來 +想尽 想盡 +想尽办法 想盡辦法 +想尽方法 想盡方法 +想干 想幹 +想干什么 想幹什麼 +想当然 想當然 +想当然耳 想當然耳 +想录 想錄 +想念 想念 +想说出 想說出 +想都别想 想都別想 +惴栗 惴慄 +惹人注意 惹人注意 +惹人注目 惹人注目 +惹出 惹出 +惹出来 惹出來 +惹口面 惹口面 +惺松 惺鬆 +愁云 愁雲 +愁云惨雾 愁雲慘霧 +愁容满面 愁容滿面 +愁布袋 愁布袋 +愁戚戚 愁慼慼 +愆面 愆面 +愈出愈奇 愈出愈奇 +愈发 愈發 +愈合 癒合 +意会出 意會出 +意克制 意剋制 +意出望外 意出望外 +意切言尽 意切言盡 +意切辞尽 意切辭盡 +意前笔后 意前筆後 +意占 意佔 +意合情投 意合情投 +意向 意向 +意向书 意向書 +意大利直面 意大利直麪 +意大利面 意大利麪 +意广才疏 意廣才疏 +意得志满 意得志滿 +意志 意志 +意志力 意志力 +意志消沈 意志消沈 +意志消沉 意志消沉 +意念 意念 +意思表示 意思表示 +意愿 意願 +意欲 意欲 +意气相合 意氣相合 +意气风发 意氣風發 +意满志得 意滿志得 +意犹未尽 意猶未盡 +意表 意表 +意见不合 意見不合 +意见调査表 意見調查表 +意识历程 意識歷程 +意转心回 意轉心回 +意里意思 意裏意思 +意面 意麪 +愚公谷 愚公谷 +愚暗 愚闇 +愚者千虑必有一得 愚者千慮必有一得 +愚蒙 愚蒙 +感于 感於 +感冒药 感冒藥 +感化饼干 感化餅乾 +感发 感發 +感叹 感嘆 +感同身受 感同身受 +感念 感念 +感恩不尽 感恩不盡 +感慨万千 感慨萬千 +感激万分 感激萬分 +感激不尽 感激不盡 +感觉出 感覺出 +感觉出来 感覺出來 +愤发 憤發 +愤而行凶 憤而行兇 +愧不敢当 愧不敢當 +愧于 愧於 +愿不愿 願不願 +愿不愿意 願不願意 +愿力 願力 +愿天下有 願天下有 +愿寘诚念 願寘誠念 +愿干一杯 願乾一杯 +愿干这杯 願乾這杯 +愿干那杯 願乾那杯 +愿心 願心 +愿意 願意 +愿意不愿意 願意不願意 +愿意干 願意幹 +愿景 願景 +愿望 願望 +愿朴 愿樸 +愿者上钩 願者上鉤 +愿而恭 愿而恭 +愿行 願行 +愿谨 願謹 +愿闻其详 願聞其詳 +慈云 慈雲 +慈制 慈制 +慈安太后 慈安太后 +慈悲喜舍 慈悲喜捨 +慈禧太后 慈禧太后 +慌了 慌了 +慌了手脚 慌了手腳 +慌了神儿 慌了神兒 +慌作一团 慌作一團 +慌成一团 慌成一團 +慌里慌张 慌里慌張 +慢了 慢了 +慢咽 慢嚥 +慢工出巧匠 慢工出巧匠 +慢工出细活 慢工出細活 +慢工出细货 慢工出細貨 +慢曲 慢曲 +慢板 慢板 +慢板情歌 慢板情歌 +慢游 慢遊 +慢表 慢表 +慧种 慧種 +慨叹 慨嘆 +慰借 慰藉 +慰情胜无 慰情勝無 +憎恶 憎惡 +懂了 懂了 +懈松 懈鬆 +懒于 懶於 +懒惰虫 懶惰蟲 +懒虫 懶蟲 +懔栗 懍慄 +懰栗 懰慄 +懵药 懵藥 +懵里懵懂 懵裏懵懂 +懿戚 懿戚 +懿范 懿範 +懿范长昭 懿範長昭 +戏剧台 戲劇臺 +戏剧团 戲劇團 +戏剧系 戲劇系 +戏台 戲臺 +戏团 戲團 +戏彩娱亲 戲綵娛親 +戏曲 戲曲 +戏曲馆 戲曲館 +戏法人人会变巧妙各有不同 戲法人人會變巧妙各有不同 +戏谷 戲谷 +戏里 戲裏 +成不了 成不了 +成个儿 成個兒 +成了 成了 +成于 成於 +成于思 成於思 +成交价 成交價 +成仇 成仇 +成兆才 成兆才 +成功的男人后面必有一个奉献的女人 成功的男人後面必有一個奉獻的女人 +成功范例 成功範例 +成千 成千 +成千上万 成千上萬 +成千成万 成千成萬 +成千成百 成千成百 +成千累万 成千累萬 +成千论万 成千論萬 +成合 成合 +成名曲 成名曲 +成名术 成名術 +成周 成周 +成团打块 成團打塊 +成套出售 成套出售 +成岩作用 成岩作用 +成批出售 成批出售 +成本价 成本價 +成核 成核 +成百上千 成百上千 +成群打伙 成羣打夥 +成群结伙 成羣結夥 +成群结党 成羣結黨 +成药 成藥 +成虫 成蟲 +成衣厂 成衣廠 +成都卖卜 成都賣卜 +成骨不全症 成骨不全症 +我们死后将会洪水滔天 我們死後將會洪水滔天 +我克制 我剋制 +我党 我黨 +我只 我只 +我只有 我只有 +我向 我向 +我回 我回 +我回到 我回到 +我回去 我回去 +我回来 我回來 +我干一杯 我乾一杯 +我念 我念 +我愿 我願 +我愿意 我願意 +我扣 我扣 +我搜 我搜 +我系 我係 +我醉欲眠 我醉欲眠 +戒坛 戒壇 +戒涂 戒塗 +戒烟 戒菸 +戒烟法 戒菸法 +或于 或於 +或系之牛 或繫之牛 +或采 或採 +战个 戰個 +战了 戰了 +战云 戰雲 +战云密布 戰雲密佈 +战云浓密 戰雲濃密 +战前战后 戰前戰後 +战后 戰後 +战团 戰團 +战地钟声 戰地鐘聲 +战士授田凭据 戰士授田憑據 +战天斗地 戰天鬥地 +战战栗栗 戰戰慄慄 +战斗 戰鬥 +战斗人员 戰鬥人員 +战斗任务 戰鬥任務 +战斗力 戰鬥力 +战斗区 戰鬥區 +战斗员 戰鬥員 +战斗意志 戰鬥意志 +战斗旅 戰鬥旅 +战斗机 戰鬥機 +战斗编组 戰鬥編組 +战斗群 戰鬥羣 +战斗者 戰鬥者 +战斗舰 戰鬥艦 +战斗英雄 戰鬥英雄 +战斗营 戰鬥營 +战斗行为 戰鬥行爲 +战斗车 戰鬥車 +战斗轰炸 戰鬥轟炸 +战斗队形 戰鬥隊形 +战无不克 戰無不克 +战无不胜 戰無不勝 +战无不胜攻无不克 戰無不勝攻無不克 +战无不胜攻无不取 戰無不勝攻無不取 +战术 戰術 +战术导弹 戰術導彈 +战术核武器 戰術核武器 +战术轰炸 戰術轟炸 +战栗 戰慄 +战略伙伴 戰略伙伴 +战略防御倡议 戰略防御倡議 +战胜 戰勝 +战胜国 戰勝國 +战表 戰表 +戚串 戚串 +戚党 戚黨 +戚凯罗 戚凱羅 +戚友 戚友 +戚墅堰 慼墅堰 +戚墅堰区 慼墅堰區 +戚夫人 戚夫人 +戚家军 戚家軍 +戚容 戚容 +戚属 戚屬 +戚戚 慼慼 +戚施 戚施 +戚族 戚族 +戚旧 戚舊 +戚然 戚然 +戚继光 戚繼光 +戚谊 戚誼 +戚里 戚里 +戛云 戛雲 +截发 截髮 +截发留宾 截髮留賓 +截板 截板 +截然不同 截然不同 +截获 截獲 +截趾适履 截趾適履 +截趾适屦 截趾適屨 +截面 截面 +截面图 截面圖 +戬谷 戩穀 +戮力同心 戮力同心 +戮尸 戮屍 +戳个儿 戳個兒 +戳脊梁 戳脊樑 +戳脊梁骨 戳脊梁骨 +戴个 戴個 +戴了 戴了 +戴克 戴克 +戴克拉克 戴克拉克 +戴克辛 戴克辛 +戴出 戴出 +戴出去 戴出去 +戴出来 戴出來 +戴发含齿 戴髮含齒 +戴天之仇 戴天之仇 +戴姆勒克莱斯勒 戴姆勒克萊斯勒 +戴希穆克 戴希穆克 +戴瑞克罗 戴瑞克羅 +戴维斯杯 戴維斯杯 +戴胜益 戴勝益 +戴胜通 戴勝通 +戴蒙 戴蒙 +戴蒙德 戴蒙德 +戴表 戴錶 +戴表元 戴表元 +戴诚志 戴誠志 +户口制 戶口制 +户口制度 戶口制度 +户对门当 戶對門當 +戽斗 戽斗 +房中术 房中術 +房价 房價 +房地产共同基金 房地產共同基金 +房地价 房地價 +房室结回路 房室結迴路 +房屋修护 房屋修護 +房屋修护费 房屋修護費 +房屋里 房屋裏 +房舍 房舍 +房里 房裏 +所云 所云 +所云云 所云云 +所余 所餘 +所剩无几 所剩無幾 +所占 所佔 +所发现 所發現 +所向 所向 +所向披靡 所向披靡 +所向无前 所向無前 +所向无敌 所向無敵 +所周知 所周知 +所布之 所佈之 +所布的 所佈的 +所干 所幹 +所幸 所幸 +所念 所念 +所托 所託 +所扣 所扣 +所有制 所有制 +所系 所繫 +所致 所致 +所见略同 所見略同 +所见而云 所見而云 +所讥而云 所譏而云 +所谓而云 所謂而云 +扁担压不出个屁来 扁擔壓不出個屁來 +扁拟谷盗虫 扁擬穀盜蟲 +扁铲 扁鏟 +扁锹形虫 扁鍬形蟲 +扇叶蒲葵 扇葉蒲葵 +扇面 扇面 +扇面对 扇面對 +扇面琴 扇面琴 +手一卷 手一捲 +手一松 手一鬆 +手不松 手不鬆 +手不释卷 手不釋卷 +手写识别 手寫識別 +手冢治虫 手冢治虫 +手到回春 手到回春 +手制 手製 +手制动 手制動 +手卷 手卷 +手同脚 手同腳 +手工台 手工檯 +手彩儿 手彩兒 +手心里 手心裏 +手折 手摺 +手挽手 手挽手 +手掌多汗症 手掌多汗症 +手摇杯 手搖杯 +手擀面 手擀麪 +手术 手術 +手术刀 手術刀 +手术台 手術檯 +手术室 手術室 +手术房 手術房 +手术用 手術用 +手术衣 手術衣 +手术费 手術費 +手松 手鬆 +手板 手板 +手板子 手板子 +手板葫芦 手板葫蘆 +手腕式指北针 手腕式指北針 +手表 手錶 +手表带 手錶帶 +手足口症 手足口症 +手酸 手痠 +手采 手採 +手里 手裏 +手里余 手裏餘 +手铲 手鏟 +手链 手鍊 手鏈 +手面 手面 +手面赚吃 手面賺吃 +才上到 纔上到 +才上去 纔上去 +才上来 纔上來 +才下去 纔下去 +才下来 纔下來 +才不 纔不 +才不会 纔不會 +才不是 纔不是 +才不能 纔不能 +才为世出 才爲世出 +才买 纔買 +才人 才人 +才会 纔會 +才会到 纔會到 +才会在 纔會在 +才会有 纔會有 +才俊 才俊 +才信 纔信 +才储八斗 才儲八斗 +才像 纔像 +才像是 纔像是 +才具 才具 +才兼文武 才兼文武 +才再 纔再 +才出 纔出 +才出去 纔出去 +才出来 纔出來 +才分 才分 +才则 纔則 +才刚 纔剛 +才到 纔到 +才力 才力 +才勇兼优 才勇兼優 +才华 才華 +才华出众 才華出衆 +才华横溢 才華橫溢 +才华洋溢 才華洋溢 +才华盖世 才華蓋世 +才去 纔去 +才可 纔可 +才可以 纔可以 +才可容颜十五余 纔可容顏十五餘 +才名 才名 +才器 才器 +才回 纔回 +才回到 纔回到 +才回去 纔回去 +才回来 纔回來 +才在 纔在 +才士 才士 +才多 纔多 +才多出 纔多出 +才够 纔夠 +才大难用 才大難用 +才女 才女 +才好 纔好 +才如史迁 才如史遷 +才始 纔始 +才媛 才媛 +才子 才子 +才子书 才子書 +才子佳人 才子佳人 +才学 才學 +才学兼优 才學兼優 +才守 才守 +才定 才定 +才对 纔對 +才将 纔將 +才干 才幹 +才干旱 纔乾旱 +才干杯 纔乾杯 +才干淨 纔乾淨 +才干透 纔乾透 +才广妨身 才廣妨身 +才开 纔開 +才开出 纔開出 +才开到 纔開到 +才当曹斗 才當曹斗 +才得两年 纔得兩年 +才得到 纔得到 +才微智浅 才微智淺 +才德 才德 +才德兼备 才德兼備 +才思 才思 +才思敏捷 才思敏捷 +才怪 纔怪 +才悟 才悟 +才情 才情 +才想 纔想 +才打 纔打 +才打出 纔打出 +才打到 纔打到 +才拿 纔拿 +才拿出 纔拿出 +才拿到 纔拿到 +才敢 纔敢 +才料 纔料 +才是 纔是 +才智 才智 +才有 纔有 +才望 才望 +才来 纔來 +才来到 纔來到 +才松下 纔鬆下 +才此 纔此 +才气 才氣 +才气无双 才氣無雙 +才气纵横 才氣縱橫 +才气过人 才氣過人 +才没 纔沒 +才没有 纔沒有 +才没能 纔沒能 +才派 纔派 +才派人 纔派人 +才爲 纔爲 +才用 才用 +才用到 纔用到 +才略 才略 +才略过人 才略過人 +才疏学浅 才疏學淺 +才疏德薄 才疏德薄 +才疏志大 才疏志大 +才疏意广 才疏意廣 +才疏计拙 才疏計拙 +才看 纔看 +才看出 纔看出 +才看到 纔看到 +才短气粗 才短氣粗 +才秀人微 才秀人微 +才等 纔等 +才等到 纔等到 +才算 纔算 +才算是 纔算是 +才给 纔給 +才能 才能 纔能 +才能勇敢追 纔能勇敢追 +才能夠 纔能夠 +才能干济 才能幹濟 +才能有 纔能有 +才色 才色 +才艺 才藝 +才艺卓绝 才藝卓絕 +才艺技能 才藝技能 +才艺班 才藝班 +才艺秀 才藝秀 +才蔽识浅 才蔽識淺 +才藻 才藻 +才行 才行 +才要 纔要 +才讲 纔講 +才识 才識 +才识过人 才識過人 +才语 才語 +才读 纔讀 +才读到 纔讀到 +才貌 才貌 +才貌出众 才貌出衆 +才貌双全 才貌雙全 +才贯二酉 才貫二酉 +才资 才資 +才起来 纔起來 +才跟 纔跟 +才轻德薄 才輕德薄 +才过去 纔過去 +才过子建 才過子建 +才过屈宋 才過屈宋 +才过来 纔過來 +才非玉润 才非玉潤 +才高八斗 才高八斗 +才高意广 才高意廣 +才高气傲 才高氣傲 +才高行厚 才高行厚 +才高行洁 才高行潔 +扎上 紮上 +扎上去 紮上去 +扎上来 紮上來 +扎下 紮下 +扎下去 紮下去 +扎下来 紮下來 +扎乎 扎乎 +扎了 紮了 +扎伊尔 扎伊爾 +扎住 扎住 +扎兰屯 扎蘭屯 +扎兰屯市 扎蘭屯市 +扎到 扎到 +扎卡维 扎卡維 +扎啤 扎啤 +扎嘴 扎嘴 +扎囊 扎囊 +扎囊县 扎囊縣 +扎囮 紮囮 +扎在 紮在 +扎垫 扎墊 +扎堆 扎堆 +扎好 紮好 +扎好底子 紮好底子 +扎好根 紮好根 +扎实 紮實 +扎实推进 紮實推進 +扎寨 紮寨 +扎尔达里 扎爾達里 +扎带 紮帶 +扎带子 紮帶子 +扎心 扎心 +扎成 紮成 +扎手 扎手 +扎手舞脚 扎手舞腳 +扎扎 扎扎 +扎扎实实 紮紮實實 +扎括 扎括 +扎挣 扎掙 +扎掂 扎掂 +扎撒 扎撒 +扎枪 扎槍 +扎根 紮根 +扎格罗斯 扎格羅斯 +扎格罗斯山脉 扎格羅斯山脈 +扎欧扎翁 紮歐紮翁 +扎煞 扎煞 +扎猛子 扎猛子 +扎眉扎眼 扎眉扎眼 +扎眼 扎眼 +扎空枪 扎空槍 +扎穿 扎穿 +扎窝子 扎窩子 +扎筏子 扎筏子 +扎紧 紮緊 +扎线带 紮線帶 +扎结 紮結 +扎缚 扎縛 +扎罚子 扎罰子 +扎耳朵 扎耳朵 +扎脑门儿 扎腦門兒 +扎脚 紮腳 +扎花 扎花 +扎草 扎草 +扎营 紮營 +扎裹 紮裹 +扎诈 紮詐 +扎赉特 扎賚特 +扎赉特旗 扎賚特旗 +扎起 紮起 +扎起来 紮起來 +扎针 扎針 +扎铁 紮鐵 +扎马剌丁 紮馬剌丁 +扎马鲁丁 紮馬魯丁 +扎鲁特 扎魯特 +扎鲁特旗 扎魯特旗 +扑个满怀 撲個滿懷 +扑个空 撲個空 +扑了 撲了 +扑克 撲克 +扑克牌 撲克牌 +扑克脸 撲克臉 +扑冬 撲鼕 +扑冬冬 撲鼕鼕 +扑出 撲出 +扑出去 撲出去 +扑出来 撲出來 +扑同 撲同 +扑向 撲向 +扑复 撲復 +扑灭蚊虫 撲滅蚊蟲 +扑通通冬 撲通通冬 +扑面 撲面 +扑面而来 撲面而來 +扒出 扒出 +打一个吞 打一個吞 +打个 打個 +打个前失 打個前失 +打个千儿 打個千兒 +打个沉儿 打個沉兒 +打个照会 打個照會 +打个照面 打個照面 +打个花 打個花 +打个问号 打個問號 +打中伙 打中伙 +打了一个闷雷 打了一個悶雷 +打了个千儿 打了個千兒 +打了个盹儿 打了個盹兒 +打了个落花流水 打了個落花流水 +打了偏手 打了偏手 +打了半跪 打了半跪 +打了牙肚里嚥 打了牙肚裏嚥 +打价 打價 +打伙 打夥 +打伙子穿靴 打夥子穿靴 +打冲锋 打衝鋒 +打出 打出 +打出去 打出去 +打出吊入 打出弔入 +打出头棍 打出頭棍 +打出头棒子 打出頭棒子 +打出手 打出手 +打出来 打出來 +打击报复 打擊報復 +打击板 打擊板 +打制 打製 +打制石器 打製石器 +打千 打千 +打卡钟 打卡鐘 +打卤面 打滷麪 +打印台 打印臺 +打印范围 打印範圍 +打参 打參 +打发 打發 +打发掉 打發掉 +打发时间 打發時間 +打合 打合 +打向 打向 +打听出 打聽出 +打和哄 打和哄 +打哄 打鬨 +打回 打回 +打回去 打回去 +打回来 打回來 +打回票 打回票 +打团 打團 +打墙板儿翻上下 打牆板兒翻上下 +打夜胡 打夜胡 +打太极 打太極 +打太极拳 打太極拳 +打完针 打完針 +打家劫舍 打家劫舍 +打对台 打對臺 +打干 打幹 +打干哕 打乾噦 +打干淨毬儿 打乾淨毬兒 +打并 打併 +打开后门说 打開後門說 +打开板壁讲亮话 打開板壁講亮話 +打当 打當 +打心眼里 打心眼裏 +打恶心 打噁心 +打成一团 打成一團 +打扑克 打撲克 +打折 打折 +打折扣 打折扣 +打折网 打折網 +打报台 打報臺 +打抽丰 打抽豐 +打拐 打拐 +打挨 打捱 +打摆子 打擺子 +打擂台 打擂臺 +打斗 打鬥 +打斗片 打鬥片 +打暗号 打暗號 +打来回 打來回 +打板 打板 +打板子 打板子 +打桨杆 打槳桿 +打棍出箱 打棍出箱 +打没头坛 打沒頭壇 +打淨捞干 打淨撈乾 +打游击 打游擊 +打游飞 打游飛 +打照面 打照面 +打狗不看主人面 打狗不看主人面 +打狗也要看主人面 打狗也要看主人面 +打狗看主人面 打狗看主人面 +打破纪录 打破紀錄 +打秋丰 打秋豐 +打秋千 打鞦韆 +打秋风 打秋風 +打筋斗 打筋斗 +打簧表 打簧錶 +打紧板 打緊板 +打绝板 打絕板 +打翻了的牛奶而哭泣 打翻了的牛奶而哭泣 +打老鼠伤了玉瓶 打老鼠傷了玉瓶 +打耳刮子 打耳刮子 +打胜 打勝 +打胜仗 打勝仗 +打胡哨 打胡哨 +打花胡哨 打花胡哨 +打药 打藥 +打蛇不死后患无穷 打蛇不死後患無窮 +打蜡 打蠟 +打蜡机 打蠟機 +打话不同 打話不同 +打诨发科 打諢發科 +打谷 打穀 +打谷场 打穀場 +打谷机 打穀機 +打路庄板 打路莊板 +打造出 打造出 +打道回府 打道回府 +打里打外 打裏打外 +打里照外 打裏照外 +打野胡 打野胡 +打量出 打量出 +打针 打針 +打钟 打鐘 +打靠后 打靠後 +打风后 打風後 +打饥荒 打饑荒 +扔出 扔出 +扔出去 扔出去 +扔出来 扔出來 +扔回 扔回 +扔回去 扔回去 +扔回来 扔回來 +扔在脑后 扔在腦後 +托买 託買 +托了 託了 +托事 託事 +托交 託交 +托人 託人 +托人情 託人情 +托付 託付 +托住 托住 +托儿 托兒 +托儿所 託兒所 +托克 托克 +托克托 托克托 +托克托县 托克托縣 +托克逊 托克遜 +托克逊县 托克遜縣 +托出 托出 +托利党人 托利黨人 +托利米尔 托利米爾 +托勒 托勒 +托勒密 托勒密 +托勒密王 托勒密王 +托勒尔 托勒爾 +托勒玫 托勒玫 +托卖 託賣 +托古讽今 託古諷今 +托叶 托葉 +托名 託名 +托命 託命 +托咎 託咎 +托地 托地 +托塔天王 托塔天王 +托塞洛 托塞洛 +托墨 托墨 +托大 託大 +托夫 托夫 +托子 托子 +托孤 託孤 +托实 托實 +托尔 托爾 +托尔斯泰 托爾斯泰 +托尔金 托爾金 +托幼 托幼 +托庇 託庇 +托拉 托拉 +托拉博拉 托拉博拉 +托拉斯 托拉斯 +托故 託故 +托斯卡 托斯卡 +托斯卡尼 托斯卡尼 +托斯卡尼尼 托斯卡尼尼 +托木尔 托木爾 +托木尔峰 托木爾峯 +托杯 托杯 +托架 托架 +托梦 託夢 +托比亚斯 托比亞斯 +托比麦奎尔 托比麥奎爾 +托洛斯基 托洛斯基 +托洛茨基 托洛茨基 +托派 託派 +托熟 托熟 +托特 托特 +托瑞丝 托瑞絲 +托瑞赛 托瑞賽 +托生 託生 +托疾 託疾 +托病 託病 +托登汉队 托登漢隊 +托盘 托盤 +托盘区 托盤區 +托福 託福 +托福考 托福考 +托福考试 托福考試 +托管 託管 +托管国 託管國 +托米 托米 +托米欧佳 托米歐佳 +托维 托維 +托罗斯山 托羅斯山 +托老中心 托老中心 +托老院 托老院 +托育 托育 +托胆 托膽 +托胎 托胎 +托腮 托腮 +托色 托色 +托荤咸食 托葷鹹食 +托莱多 托萊多 +托蒂 托蒂 +托言 託言 +托词 託詞 +托赖 托賴 +托起 托起 +托起来 托起來 +托足 托足 +托足无门 托足無門 +托身 託身 +托辞 託辭 +托辣斯 托辣斯 +托辣斯法 托辣斯法 +托过 託過 +托运 託運 +托运行李 托運行李 +托里 托裏 +托里县 托裏縣 +托里拆利 托裏拆利 +托鉢 托鉢 +托钵人 托鉢人 +托钵修会 托鉢修會 +托钵僧 托鉢僧 +托附 託附 +托领 托領 +托马 托馬 +托马斯 托馬斯 +托马斯阿奎纳 托馬斯阿奎納 +扛了 扛了 +扛出 扛出 +扛出去 扛出去 +扛出来 扛出來 +扛大梁 扛大樑 +扞御 扞禦 +扣一 扣一 +扣上 扣上 +扣下 扣下 +扣了 扣了 +扣人 扣人 +扣件 扣件 +扣住 扣住 +扣作 扣作 +扣你 扣你 +扣儿 扣兒 +扣光 扣光 +扣克 扣剋 +扣入 扣入 +扣减 扣減 +扣出 扣出 +扣击 扣擊 +扣分 扣分 +扣分标准 扣分標準 +扣划 扣劃 +扣到 扣到 +扣动 扣動 +扣压 扣壓 +扣去 扣去 +扣发 扣發 +扣取 扣取 +扣响 扣響 +扣啊 扣啊 +扣回 扣回 +扣回去 扣回去 +扣回来 扣回來 +扣在 扣在 +扣头 扣頭 +扣女 扣女 +扣好 扣好 +扣子 釦子 +扣完 扣完 +扣将 扣將 +扣带 扣帶 +扣开 扣開 +扣得 扣得 +扣我 扣我 +扣扣 扣扣 +扣抵 扣抵 +扣押 扣押 +扣掉 扣掉 +扣杀 扣殺 +扣板 扣板 +扣板机 扣板機 +扣查 扣查 +扣款 扣款 +扣法 扣法 +扣满 扣滿 +扣点 扣點 +扣牌 扣牌 +扣牢 扣牢 +扣率 扣率 +扣环 釦環 +扣球 扣球 +扣留 扣留 +扣的 扣的 +扣眼 釦眼 +扣着 扣着 +扣税 扣稅 +扣篮 扣籃 +扣紧 扣緊 +扣绊 扣絆 +扣缴 扣繳 +扣罚 扣罰 +扣肉 扣肉 +扣船 扣船 +扣薪 扣薪 +扣表 扣表 +扣费 扣費 +扣起 扣起 +扣车 扣車 +扣过 扣過 +扣进 扣進 +扣针 釦針 +扣钩 扣鉤 +扣钱 扣錢 +扣锁 扣鎖 +扣门 扣門 +扣问 扣問 +扣除 扣除 +扣题 扣題 +执念 執念 +执政党 執政黨 +执政团 執政團 +执板 執板 +执行面 執行面 +扩厂 擴廠 +扩厂计划 擴廠計劃 +扩大范围 擴大範圍 +扩建工厂 擴建工廠 +扩张术 擴張術 +扩散出来 擴散出來 +扩音机系统 擴音機系統 +扪参历井 捫參歷井 +扪隙发罅 捫隙發罅 +扫出 掃出 +扫出去 掃出去 +扫出来 掃出來 +扫地俱尽 掃地俱盡 +扫地出门 掃地出門 +扫干淨 掃乾淨 +扫晴娘 掃晴娘 +扫田刮地 掃田刮地 +扫眉才子 掃眉才子 +扫荡 掃蕩 +扫荡残敌 掃蕩殘敵 +扬升 揚升 +扬名后世 揚名後世 +扬名立万 揚名立萬 +扬善去恶 揚善去惡 +扬善隐恶 揚善隱惡 +扬州清曲 揚州清曲 +扬州画舫录 揚州畫舫錄 +扬己露才 揚己露才 +扬谷 揚穀 +扭出 扭出 +扭出去 扭出去 +扭出来 扭出來 +扭别 扭別 +扭头折颈 扭頭折頸 +扭成一团 扭成一團 +扭摆 扭擺 +扭摆不停 扭擺不停 +扭曲 扭曲 +扭曲作直 扭曲作直 +扭转乾坤 扭轉乾坤 +扮装皇后 扮裝皇后 +扯出 扯出 +扯出去 扯出去 +扯出来 扯出來 +扯后腿 扯後腿 +扯篷拉纤 扯篷拉縴 +扯纤 扯縴 +扯闲白 扯閒白 +扯闲盘儿 扯閒盤兒 +扯闲篇 扯閒篇 +扯面 扯麪 +扰流板 擾流板 +扳回 扳回 +扳回一城 扳回一城 +扶了 扶了 +扶余 扶余 +扶余县 扶余縣 +扶余国 扶餘國 +扶出 扶出 +扶出去 扶出去 +扶出来 扶出來 +扶危救困 扶危救困 +扶危济困 扶危濟困 +扶同 扶同 +扶同硬证 扶同硬證 +扶同诖误 扶同詿誤 +扶善惩恶 扶善懲惡 +扶幼周 扶幼週 +扶手椅里 扶手椅裏 +扶苏 扶蘇 +扶贫济困 扶貧濟困 +批价 批價 +批准 批准 +批准下来 批准下來 +批准书 批准書 +批准的 批准的 +批出 批出 +批出去 批出去 +批出来 批出來 +批发 批發 +批发业 批發業 +批发价 批發價 +批发价格 批發價格 +批发商 批發商 +批发市场 批發市場 +批发店 批發店 +批回 批迴 +批复 批覆 +批尸 批屍 +批斗 批鬥 +批核 批覈 +批汇 批匯 +批注 批註 +批荡 批蕩 +扼制 扼制 +找不准 找不準 +找不出 找不出 +找个 找個 +找了 找了 +找价 找價 +找借口 找藉口 +找出 找出 +找出去 找出去 +找出来 找出來 +找出路 找出路 +找台阶 找臺階 +找台阶下 找臺階下 +找台阶儿 找臺階兒 +找回 找回 +找回去 找回去 +找回来 找回來 +找寻出 找尋出 +找面子 找面子 +承修 承修 +承先启后 承先啓後 +承制 承製 +承前启后 承前啓後 +承受不了 承受不了 +承宣布政使司 承宣布政使司 +承平面 承平面 +承当 承當 +承当不起 承當不起 +承蒙 承蒙 +承蒙关照 承蒙關照 +承蒙指教 承蒙指教 +承销价差 承銷價差 +承销团 承銷團 +技术 技術 +技术专科 技術專科 +技术专科学校 技術專科學校 +技术人员 技術人員 +技术作物 技術作物 +技术分析 技術分析 +技术单位 技術單位 +技术发展 技術發展 +技术合作 技術合作 +技术员 技術員 +技术团 技術團 +技术士 技術士 +技术处 技術處 +技术学院 技術學院 +技术官 技術官 +技术性 技術性 +技术情报 技術情報 +技术所限 技術所限 +技术指导 技術指導 +技术援助 技術援助 +技术故障 技術故障 +技术标准 技術標準 +技术水准 技術水準 +技术水平 技術水平 +技术潜水 技術潛水 +技术犯规 技術犯規 +技术知识 技術知識 +技术装备 技術裝備 +技术规范 技術規範 +技术部 技術部 +技术部门 技術部門 +技术降落 技術降落 +技术面 技術面 +技术革命 技術革命 +技术革新 技術革新 +技艺团 技藝團 +抄发 抄發 +抄台 抄臺 +抄后路 抄後路 +抄录 抄錄 +抄录下来 抄錄下來 +抄录在 抄錄在 +抄录编目 抄錄編目 +抄手游廊 抄手遊廊 +抄扎 抄扎 +抄获 抄獲 +把上了 把上了 +把个 把個 +把了 把了 +把你干 把你幹 +把卷 把卷 +把定了心 把定了心 +把方才 把方纔 +把脸一板 把臉一板 +把饭叫饥 把飯叫饑 +抑制 抑制 +抑制作用 抑制作用 +抑制剂 抑制劑 +抑制器 抑制器 +抑制栽培 抑制栽培 +抑制酶 抑制酶 +抑恶扬善 抑惡揚善 +抑扬升降性 抑揚昇降性 +抑郁 抑鬱 +抑郁不平 抑鬱不平 +抑郁寡欢 抑鬱寡歡 +抑郁症 抑鬱症 +抒发 抒發 +抒情曲 抒情曲 +抒情歌曲 抒情歌曲 +抓不准 抓不準 +抓准 抓準 +抓出 抓出 +抓出去 抓出去 +抓出来 抓出來 +抓周 抓週 +抓回 抓回 +抓回去 抓回去 +抓回来 抓回來 +抓奸 抓姦 +抓彩 抓彩 +抓斗 抓鬥 +抓药 抓藥 +抓获 抓獲 +抔土未干 抔土未乾 +投不准 投不準 +投不出 投不出 +投了 投了 +投井自尽 投井自盡 +投出 投出 +投出去 投出去 +投出来 投出來 +投合 投合 +投向 投向 +投回 投回 +投射出 投射出 +投影几何 投影幾何 +投影几何学 投影幾何學 +投影面 投影面 +投手板 投手板 +投手防御率 投手防禦率 +投托 投托 +投梭折齿 投梭折齒 +投河自尽 投河自盡 +投注 投注 +投注站 投注站 +投潘岳果 投潘岳果 +投环自尽 投環自盡 +投票表决 投票表決 +投药 投藥 +投闲置散 投閒置散 +抖了 抖了 +抖了起来 抖了起來 +抖出 抖出 +抖出去 抖出去 +抖出来 抖出來 +抖搜精神 抖搜精神 +抗了 抗了 +抗噪 抗噪 +抗干扰性 抗干擾性 +抗御 抗禦 +抗志 抗志 +抗忧郁药 抗憂鬱藥 +抗战歌曲 抗戰歌曲 +抗战胜利 抗戰勝利 +抗拒不了 抗拒不了 +抗日救亡团体 抗日救亡團體 +抗病品种 抗病品種 +抗病毒药 抗病毒藥 +抗癌药 抗癌藥 +抗癌药物 抗癌藥物 +抗组胺药 抗組胺藥 +抗药 抗藥 +抗药性 抗藥性 +抗药能力 抗藥能力 +抗菌药 抗菌藥 +抗逆转录 抗逆轉錄 +折上 折上 +折上去 折上去 +折上来 折上來 +折下 折下 +折下去 折下去 +折下来 折下來 +折不断 折不斷 +折中 折中 +折价 折價 +折价出售 折價出售 +折价券 折價券 +折价卷 折價卷 +折伤 折傷 +折倒 折倒 +折光 折光 +折兑 折兌 +折兑率 折兌率 +折兵 折兵 +折冲 折衝 +折冲厌难 折衝厭難 +折冲尊俎 折衝尊俎 +折冲御侮 折衝禦侮 +折冲樽俎 折衝樽俎 +折刀 折刀 +折刀儿 折刀兒 +折券 折券 +折剉 折剉 +折半 折半 +折半出售 折半出售 +折卖 折賣 +折受 折受 +折变 折變 +折叠 摺疊 +折叠为 摺疊爲 +折叠式 摺疊式 +折叠扇 摺疊扇 +折叠椅 摺疊椅 +折叠牀 摺疊牀 +折叠起来 摺疊起來 +折台 折檯 +折合 摺合 +折合椅 摺合椅 +折回 折回 +折回去 折回去 +折回来 折回來 +折壁脚 折壁腳 +折头 折頭 +折奏 摺奏 +折好 摺好 +折子 摺子 +折子戏 摺子戲 +折实 折實 +折对 折對 +折寿 折壽 +折射 折射 +折射光 折射光 +折射出 折射出 +折射式望远镜 折射式望遠鏡 +折射波 折射波 +折射率 折射率 +折射线 折射線 +折射角 折射角 +折尺 摺尺 +折屐 折屐 +折帐 折帳 +折弯 折彎 +折当 折當 +折得 折得 +折成 折成 +折戟沈河 折戟沈河 +折戟沉沙 折戟沉沙 +折扇 摺扇 +折扣 折扣 +折扣价 折扣價 +折扣战 折扣戰 +折扣率 折扣率 +折抵 折抵 +折挫 折挫 +折损 折損 +折损率 折損率 +折断 折斷 +折旧 折舊 +折旧基金 折舊基金 +折旧率 折舊率 +折旧费 折舊費 +折服 折服 +折本 折本 +折杀 折殺 +折枝 折枝 +折柬 折柬 +折柳 折柳 +折柳攀花 折柳攀花 +折桂 折桂 +折桂攀蟾 折桂攀蟾 +折梯 摺梯 +折椅 摺椅 +折狱 折獄 +折现 折現 +折现率 折現率 +折痕 摺痕 +折的 折的 +折碗 折碗 +折磨 折磨 +折福 折福 +折秤 折秤 +折简 折簡 +折算 折算 +折算法 折算法 +折算率 折算率 +折箩 折籮 +折箭 折箭 +折箭为盟 折箭爲盟 +折箭为誓 折箭爲誓 +折篷 摺篷 +折纸 摺紙 +折纸工 摺紙工 +折线 折線 +折缝 折縫 +折罚 折罰 +折翼 折翼 +折耗 折耗 +折肱 折肱 +折腰 折腰 +折腰五斗 折腰五斗 +折腰升斗 折腰升斗 +折腰步 折腰步 +折腾 折騰 +折臂三公 折臂三公 +折色 折色 +折节 折節 +折节下交 折節下交 +折节下士 折節下士 +折节向学 折節向學 +折节待士 折節待士 +折节礼士 折節禮士 +折节读书 折節讀書 +折莫 折莫 +折行 折行 +折衷 折衷 +折衷主义 折衷主義 +折衷家庭 折衷家庭 +折衷方案 折衷方案 +折衷法 折衷法 +折衷鹦鹉 折衷鸚鵡 +折裙 摺裙 +折角 折角 +折让 折讓 +折证 折證 +折账 折賬 +折起 折起 +折起来 折起來 +折足复𫗧 折足覆餗 +折跟头 折跟頭 +折身 折身 +折转 折轉 +折辨 折辨 +折辩 折辯 +折辱 折辱 +折过儿 折過兒 +折返 折返 +折返点 折返點 +折进 折進 +折进去 摺進去 +折进来 摺進來 +折钱 折錢 +折长补短 折長補短 +折页 摺頁 +折鼎复𫗧 折鼎覆餗 +抚台 撫臺 +抚台街 撫臺街 +抚尸 撫屍 +抚尸恸哭 撫尸慟哭 +抚尸痛哭 撫屍痛哭 +抚恤 撫卹 +抚松 撫松 +抚松县 撫松縣 +抚梁易柱 撫梁易柱 +抚面 撫面 +抚面痛哭 撫面痛哭 +抛出 拋出 +抛出去 拋出去 +抛出来 拋出來 +抛向 拋向 +抛在脑后 拋在腦後 +抛头露面 拋頭露面 +抛妻别子 拋妻別子 +抛尸露骨 拋屍露骨 +抛摆 拋擺 +抛物面 拋物面 +抟击掀发 摶擊掀發 +抟柱乘梁 摶柱乘梁 +抟沙嚼蜡 摶沙嚼蠟 +抟砂炼汞 摶砂煉汞 +抟风板 摶風板 +抠出 摳出 +抠出来 摳出來 +抢了 搶了 +抢修 搶修 +抢修工作 搶修工作 +抢占 搶佔 +抢回 搶回 +抢尽 搶盡 +抢尽锋头 搶盡鋒頭 +抢救出来 搶救出來 +抢杠 搶槓 +抢种 搶種 +护厂 護廠 +护发 護髮 +护发乳液 護髮乳液 +护发素 護髮素 +护向 護向 +护壁板 護壁板 +护念 護念 +护板 護板 +护理系 護理系 +护面 護面 +护面具 護面具 +报仇 報仇 +报仇雪恨 報仇雪恨 +报仇雪耻 報仇雪恥 +报价 報價 +报价单 報價單 +报修 報修 +报值挂号 報值掛號 +报出 報出 +报出去 報出去 +报出来 報出來 +报刊杂志 報刊雜誌 +报台 報臺 +报名表 報名表 +报告出来 報告出來 +报团 報團 +报国尽忠 報國盡忠 +报复 報復 +报复主义 報復主義 +报复关税 報復關稅 +报复性 報復性 +报导出来 報導出來 +报录人 報錄人 +报税表 報稅表 +报穷制裁 報窮制裁 +报章杂志 報章雜誌 +报系 報系 +报表 報表 +报表纸 報表紙 +报表语言 報表語言 +披云 披雲 +披云雾睹青天 披雲霧睹青天 +披卷 披卷 +披发 披髮 +披发入山 披髮入山 +披发垢面 披髮垢面 +披发左衽 披髮左衽 +披发涂面 披髮塗面 +披发缨冠 披髮纓冠 +披发藻目 披髮藻目 +披复 披覆 +披头四乐团 披頭四樂團 +披头四合唱团 披頭四合唱團 +披头散发 披頭散髮 +披挂 披掛 +披挂上阵 披掛上陣 +披挂绰鎗 披掛綽鎗 +披榛采兰 披榛採蘭 +披红挂彩 披紅掛綵 +披肝挂胆 披肝掛膽 +披针形叶 披針形葉 +抬价 擡價 +抬出 擡出 +抬出去 擡出去 +抬出来 擡出來 +抬回 擡回 +抬回去 擡回去 +抬回来 擡回來 +抬杠 擡槓 +抬高身价 擡高身價 +抱出 抱出 +抱出去 抱出去 +抱出来 抱出來 +抱出笼 抱出籠 +抱回 抱回 +抱回家 抱回家 +抱回来 抱回來 +抱大足杆 抱大足桿 +抱布贸丝 抱布貿絲 +抱成一团 抱成一團 +抱抱团 抱抱團 +抱朴 抱朴 +抱朴子 抱朴子 +抱朴而长吟兮 抱朴而長吟兮 +抱素怀朴 抱素懷樸 +抱负水准 抱負水準 +抵了 抵了 +抵借 抵借 +抵制 抵制 +抵向 抵向 +抵当 抵當 +抵御 抵禦 +抵御外侮 抵禦外侮 +抵扣 抵扣 +抵押借款 抵押借款 +抵挡不了 抵擋不了 +抵牾 牴牾 +抵觸 牴觸 +抵针 抵針 +抹了 抹了 +抹布 抹布 +抹干 抹乾 +抹面 抹面 +抻面 抻面 +押出 押出 +押回 押回 +押回去 押回去 +押回来 押回來 +押当 押當 +押柜 押櫃 +押汇 押匯 +押注 押注 +押解回 押解回 +抽丰 抽豐 +抽公签 抽公籤 +抽出 抽出 +抽出去 抽出去 +抽出来 抽出來 +抽出空 抽出空 +抽厘 抽釐 +抽后腿 抽後腿 +抽咽 抽咽 +抽回 抽回 +抽尽 抽盡 +抽干 抽乾 +抽斗 抽斗 +抽油烟机 抽油煙機 +抽烟 抽菸 +抽烟室 抽菸室 +抽穗 抽穗 +抽穗期 抽穗期 +抽签 抽籤 +抽绎出 抽繹出 +抽蓄发电 抽蓄發電 +抿发 抿髮 +拂弦 拂弦 +拂荡 拂盪 +拂钟无声 拂鐘無聲 +拂面 拂面 +拂面而来 拂面而來 +拂须 拂鬚 +担仔面 擔仔麪 +担干系 擔干係 +担干纪 擔干紀 +担当 擔當 +担当不起 擔當不起 +担当不过 擔當不過 +担当起来 擔當起來 +担担面 擔擔麪 +担水向河头卖 擔水向河頭賣 +担饥受冻 擔飢受凍 +拆伙 拆夥 +拆借 拆借 +拆出 拆出 +拆出去 拆出去 +拆出来 拆出來 +拆台 拆臺 +拆回 拆回 +拆回去 拆回去 +拆回来 拆回來 +拆封后 拆封後 +拆扣 拆扣 +拆白党 拆白黨 +拆迁范围 拆遷範圍 +拈折 拈折 +拈花摘叶 拈花摘葉 +拈针指 拈針指 +拈须 拈鬚 +拉丁语系 拉丁語系 +拉个手 拉個手 +拉了 拉了 +拉了一把 拉了一把 +拉克施尔德钟 拉克施爾德鐘 +拉克替醇 拉克替醇 +拉克莫诺夫 拉克莫諾夫 +拉出 拉出 +拉出去 拉出去 +拉出来 拉出來 +拉制 拉制 +拉升 拉昇 +拉卜楞 拉卜楞 +拉卜楞寺 拉卜楞寺 +拉合子 拉合子 +拉合尔 拉合爾 +拉合尔市 拉合爾市 +拉后腿 拉後腿 +拉向 拉向 +拉回 拉回 +拉回去 拉回去 +拉回来 拉回來 +拉尔维克 拉爾維克 +拉布 拉布 +拉布拉 拉布拉 +拉布拉他 拉布拉他 +拉布拉他河 拉布拉他河 +拉布拉多 拉布拉多 +拉布拉多半岛 拉布拉多半島 +拉布拉多寒流 拉布拉多寒流 +拉布拉多犬 拉布拉多犬 +拉德万斯卡 拉德萬斯卡 +拉托维亚 拉托維亞 +拉文克劳 拉文克勞 +拉杆 拉桿 +拉杆子 拉桿子 +拉法叶 拉法葉 +拉法叶舰 拉法葉艦 +拉涅里 拉涅里 +拉狄克 拉狄克 +拉皮手术 拉皮手術 +拉祖里 拉祖里 +拉纤 拉縴 +拉缆子摆渡 拉纜子擺渡 +拉蒙 拉蒙 +拉贾帕克萨 拉賈帕克薩 +拉达克 拉達克 +拉里 拉里 +拉里加尼 拉里加尼 +拉里拉尼 拉里拉尼 +拉链 拉鍊 +拉链工程 拉鍊工程 +拉面 拉麪 +拉面店 拉麪店 +拌嘴斗舌 拌嘴鬥舌 +拌折 拌折 +拌种 拌種 +拌面 拌麪 +拍出 拍出 +拍出去 拍出去 +拍出好戏 拍齣好戲 +拍出来 拍出來 +拍卖价 拍賣價 +拍发 拍發 +拍台拍凳 拍檯拍凳 +拍摄出 拍攝出 +拍板 拍板 +拏云 拏雲 +拏云攫石 拏雲攫石 +拐上 拐上 +拐下 拐下 +拐了 拐了 +拐人 拐人 +拐住 拐住 +拐入 拐入 +拐出 拐出 +拐出去 拐出去 +拐出来 拐出來 +拐到 拐到 +拐卖 拐賣 +拐去 拐去 +拐回 拐回 +拐回去 拐回去 +拐回来 拐回來 +拐子 柺子 +拐带 拐帶 +拐弯 拐彎 +拐得 拐得 +拐杖 柺杖 +拐来 拐來 +拐枣 拐棗 +拐棍 柺棍 +拐棒 柺棒 +拐点 拐點 +拐着 拐着 +拐臂 拐臂 +拐角 拐角 +拐诱 拐誘 +拐走 拐走 +拐跑 拐跑 +拐过 拐過 +拐进 拐進 +拐骗 拐騙 +拑制 拑制 +拒于 拒於 +拒人于 拒人於 +拒人于千里之外 拒人於千里之外 +拒烟 拒菸 +拓朴 拓樸 +拓朴学 拓樸學 +拔不出 拔不出 +拔不出腿来 拔不出腿來 +拔个 拔個 +拔了 拔了 +拔了一个尖儿 拔了一個尖兒 +拔了萝卜地皮宽 拔了蘿蔔地皮寬 +拔出 拔出 +拔出去 拔出去 +拔出来 拔出來 +拔发 拔髮 +拔回 拔回 +拔回去 拔回去 +拔回来 拔回來 +拔围出夫 拔圍出夫 +拔地参天 拔地參天 +拔宅上升 拔宅上昇 +拔宅飞升 拔宅飛昇 +拔山志 拔山志 +拔山曲 拔山曲 +拔群出类 拔羣出類 +拔群出萃 拔羣出萃 +拔萃出类 拔萃出類 +拔萃出群 拔萃出羣 +拔萝卜 拔蘿蔔 +拔虎须 拔虎鬚 +拔须 拔鬚 +拖了 拖了 +拖出 拖出 +拖出去 拖出去 +拖出来 拖出來 +拖吊 拖吊 +拖吊车 拖吊車 +拖后腿 拖後腿 +拖回 拖回 +拖回去 拖回去 +拖回来 拖回來 +拖地板 拖地板 +拖布 拖布 +拖干淨 拖乾淨 +拖延战术 拖延戰術 +拖斗 拖斗 +拖板 拖板 +拖板车 拖板車 +拖链 拖鏈 +拖鞋柜 拖鞋櫃 +拖麻拽布 拖麻拽布 +拗别 拗彆 +拗别搅炒 拗彆攪炒 +拗曲作直 拗曲作直 +拘于 拘於 +拘出 拘出 +拘板 拘板 +拘泥于 拘泥於 +拘系 拘繫 +拙于 拙於 +拙于言词 拙於言詞 +拙于言辞 拙於言辭 +拙朴 拙樸 +拚命干 拚命幹 +拚斗 拚鬥 +拚生尽死 拚生盡死 +拚舍 拚捨 +招了 招了 +招供出来 招供出來 +招出 招出 +招出来 招出來 +招回 招回 +招回去 招回去 +招回来 招回來 +招复 招復 +招待不周 招待不週 +招术 招術 +招致 招致 +拜冬 拜冬 +拜别 拜別 +拜占庭 拜占庭 +拜占庭帝国 拜占庭帝國 +拜占庭文化 拜占庭文化 +拜台 拜臺 +拜复 拜覆 +拜岳 拜岳 +拜托 拜託 +拜斗 拜斗 +拜科努尔航天发射基地 拜科努爾航天發射基地 +拜表 拜表 +拟于 擬於 +拟于不伦 擬於不倫 +拟出 擬出 +拟制 擬製 +拟卤素 擬鹵素 +拟定出 擬定出 +拟核 擬核 +拟阿拖品药物 擬阿拖品藥物 +拣出 揀出 +拣别 揀別 +拥向 擁向 +拦前断后 攔前斷後 +拦当 攔當 +拦柜 攔櫃 +拧了 擰了 +拧干 擰乾 +拧松 擰鬆 +拧种 擰種 +拨万论千 撥萬論千 +拨万轮千 撥萬輪千 +拨乱之才 撥亂之才 +拨云撩雨 撥雲撩雨 +拨云睹日 撥雲睹日 +拨云见日 撥雲見日 +拨出 撥出 +拨出去 撥出去 +拨出来 撥出來 +拨发 撥發 +拨号系统 撥號系統 +拨回 撥回 +拨回去 撥回去 +拨回来 撥回來 +拨开云雾见青天 撥開雲霧見青天 +拨弦 撥絃 +拨接式数据通信 撥接式數據通信 +拨蜡法 撥蠟法 +拨谷 撥穀 +拨雨撩云 撥雨撩雲 +括发 括髮 +括弧里 括弧裏 +拭干 拭乾 +拮据 拮据 +拱出 拱出 +拱手而别 拱手而別 +拱手让出 拱手讓出 +拱托 拱托 +拱极星 拱極星 +拳击台 拳擊臺 +拳坛 拳壇 +拳曲 拳曲 +拳术 拳術 +拷克机 拷克機 +拼了 拼了 +拼了命 拼了命 +拼凑出 拼湊出 +拼出 拼出 +拼出去 拼出去 +拼出来 拼出來 +拼列出 拼列出 +拼合 拼合 +拼合版画 拼合版畫 +拼回 拼回 +拼回去 拼回去 +拼回来 拼回來 +拼图游戏 拼圖遊戲 +拼字游戏 拼字遊戲 +拼布 拼布 +拼布图形 拼布圖形 +拼斗 拼鬥 +拼板 拼板 +拼板游戏 拼板遊戲 +拼板玩具 拼板玩具 +拼板胶 拼板膠 +拼板舟 拼板舟 +拼缀出 拼綴出 +拼花地板 拼花地板 +拼贴艺术 拼貼藝術 +拽布披麻 拽布披麻 +拽布拖麻 拽布拖麻 +拽扎 拽扎 +拾回 拾回 +拾沈 拾瀋 +拾穗 拾穗 +拾获 拾獲 +拿下表 拿下錶 +拿下钟 拿下鐘 +拿不了 拿不了 +拿不准 拿不準 +拿不出手 拿不出手 +拿个 拿個 +拿云手 拿雲手 +拿云捉月 拿雲捉月 +拿云握雾 拿雲握霧 +拿准 拿準 +拿出 拿出 +拿出去 拿出去 +拿出手 拿出手 +拿出来 拿出來 +拿回 拿回 +拿回去 拿回去 +拿回家 拿回家 +拿回来 拿回來 +拿坡里 拿坡里 +拿坡里号 拿坡里號 +拿板弄势 拿板弄勢 +拿枪杆 拿槍桿 +拿波里 拿波里 +拿着鸡毛当令箭 拿着雞毛當令箭 +拿破仑 拿破崙 +拿破仑法典 拿破崙法典 +拿破仑波拿巴 拿破侖波拿巴 +拿笔杆 拿筆桿 +拿药 拿藥 +拿获 拿獲 +拿贼要赃拿奸要双 拿賊要贓拿姦要雙 +持不同政见 持不同政見 +持不同政见者 持不同政見者 +持胜 持勝 +挂一挂 掛一掛 +挂一漏万 掛一漏萬 +挂一漏百 掛一漏百 +挂上 掛上 +挂上去 掛上去 +挂上来 掛上來 +挂上钩 掛上鉤 +挂下 掛下 +挂下去 掛下去 +挂下来 掛下來 +挂不上 掛不上 +挂不下 掛不下 +挂不住 掛不住 +挂个 掛個 +挂了 掛了 +挂了幌子 掛了幌子 +挂住 掛住 +挂僵 掛僵 +挂入 掛入 +挂冠 掛冠 +挂冠归里 掛冠歸里 +挂冠求去 掛冠求去 +挂冠而去 掛冠而去 +挂出 掛出 +挂出去 掛出去 +挂出来 掛出來 +挂到 掛到 +挂勾 掛勾 +挂千 掛千 +挂单 掛單 +挂印悬牌 掛印懸牌 +挂印而逃 掛印而逃 +挂历 掛曆 +挂号 掛號 +挂号信 掛號信 +挂号处 掛號處 +挂号证 掛號證 +挂号费 掛號費 +挂名 掛名 +挂名夫妻 掛名夫妻 +挂味儿 掛味兒 +挂回 掛回 +挂回去 掛回去 +挂回来 掛回來 +挂图 掛圖 +挂图表 掛圖表 +挂在 掛在 +挂在口上 掛在口上 +挂在嘴上 掛在嘴上 +挂在嘴边 掛在嘴邊 +挂坠盒 掛墜盒 +挂失 掛失 +挂失止付 掛失止付 +挂头牌 掛頭牌 +挂好 掛好 +挂孝 掛孝 +挂帅 掛帥 +挂帅的社会 掛帥的社會 +挂帐 掛帳 +挂帘 掛簾 +挂幌子 掛幌子 +挂彩 掛彩 +挂得 掛得 +挂心 掛心 +挂念 掛念 +挂怀 掛懷 +挂意 掛意 +挂成 掛成 +挂招牌 掛招牌 +挂掉 掛掉 +挂搭 掛搭 +挂斗 掛斗 +挂断 掛斷 +挂有 掛有 +挂来 掛來 +挂来挂去 掛來掛去 +挂杯架 掛杯架 +挂架 掛架 +挂满 掛滿 +挂漏 掛漏 +挂火 掛火 +挂灯 掛燈 +挂灯结彩 掛燈結綵 +挂牌 掛牌 +挂牌汇率 掛牌匯率 +挂球蛋 掛球蛋 +挂的 掛的 +挂碍 掛礙 +挂篮 掛籃 +挂红 掛紅 +挂红灯 掛紅燈 +挂络儿 掛絡兒 +挂羊头 掛羊頭 +挂羊头卖狗肉 掛羊頭賣狗肉 +挂羊头煮狗肉 掛羊頭煮狗肉 +挂肚牵心 掛肚牽心 +挂肚牵肠 掛肚牽腸 +挂肠悬胆 掛腸懸膽 +挂落儿 掛落兒 +挂虑 掛慮 +挂衣 掛衣 +挂衣冠 掛衣冠 +挂衣架 掛衣架 +挂衣钩 掛衣鉤 +挂表 掛錶 +挂角读书 掛角讀書 +挂记 掛記 +挂账 掛賬 +挂货铺 掛貨鋪 +挂起 掛起 +挂起来 掛起來 +挂车 掛車 +挂轴 掛軸 +挂过 掛過 +挂过去 掛過去 +挂过来 掛過來 +挂进 掛進 +挂进去 掛進去 +挂进来 掛進來 +挂钟 掛鐘 +挂钩 掛鉤 +挂钩儿 掛鉤兒 +挂钩子 掛鉤子 +挂锁 掛鎖 +挂镜儿 掛鏡兒 +挂零 掛零 +挂面 掛麪 +挂齿 掛齒 +挂龙雨 掛龍雨 +指不胜屈 指不勝屈 +指了 指了 +指亲托故 指親托故 +指出 指出 +指北针 指北針 +指南针 指南針 +指向 指向 +指向装置 指向裝置 +指回 指回 +指回去 指回去 +指回来 指回來 +指定曲 指定曲 +指征 指徵 +指手划脚 指手劃腳 +指挥台 指揮台 +指日高升 指日高升 +指极星 指極星 +指标体系 指標體系 +指水盟松 指水盟松 +指破迷团 指破迷團 +指示板 指示板 +指纹纪录 指紋紀錄 +指纹鉴定 指紋鑑定 +指认出 指認出 +指证历历 指證歷歷 +指针 指針 +指针式 指針式 +按下不表 按下不表 +按了 按了 +按出 按出 +按出去 按出去 +按出来 按出來 +按回 按回 +按回去 按回去 +按回来 按回來 +按扣 按扣 +按时间先后 按時間先後 +按照字面 按照字面 +按照计划 按照計劃 +按理出牌 按理出牌 +按表操课 按表操課 +按计划 按計劃 +按质定价 按質定價 +按需出版 按需出版 +挌斗 挌鬥 +挑了 挑了 +挑了只 挑了隻 +挑出 挑出 +挑出去 挑出去 +挑出来 挑出來 +挑口板 挑口板 +挑台 挑臺 +挑大梁 挑大樑 +挑尽 挑盡 +挑得篮里便是菜 挑得籃裏便是菜 +挑拣出 挑揀出 +挑斗 挑鬥 +挑正梁 挑正樑 +挑选出 挑選出 +挑选出来 挑選出來 +挖出 挖出 +挖出去 挖出去 +挖出来 挖出來 +挖心搜胆 挖心搜膽 +挖掘出 挖掘出 +挖掘出来 挖掘出來 +挖耳当招 挖耳當招 +挚而有别 摯而有別 +挟制 挾制 +挟朋树党 挾朋樹黨 +挠折 撓折 +挠曲 撓曲 +挠直为曲 撓直爲曲 +挡不了 擋不了 +挡了 擋了 +挡修 擋修 +挡出 擋出 +挡回 擋回 +挡回去 擋回去 +挡回来 擋回來 +挡御 擋禦 +挡板 擋板 +挡泥板 擋泥板 +挡雨板 擋雨板 +挣出 掙出 +挣扎 掙扎 +挣扎表 掙扎表 +挤了 擠了 +挤兑出来 擠兌出來 +挤出 擠出 +挤出去 擠出去 +挤出来 擠出來 +挤占 擠佔 +挤压出 擠壓出 +挤向 擠向 +挤成一团 擠成一團 +挤昏了头 擠昏了頭 +挤身于 擠身於 +挥了 揮了 +挥出 揮出 +挥别 揮別 +挥发 揮發 +挥发性 揮發性 +挥发性存储器 揮發性存儲器 +挥发油 揮發油 +挥发物 揮發物 +挥戈回日 揮戈回日 +挥手告别 揮手告別 +挥手致意 揮手致意 +挥杆 揮杆 +挨三顶五 挨三頂五 +挨三顶四 捱三頂四 +挨上 捱上 +挨上去 挨上去 +挨不上 挨不上 +挨个 挨個 +挨个儿 挨個兒 +挨了 捱了 +挨了揍 捱了揍 +挨了过去 捱了過去 +挨了过来 捱了過來 +挨亲儿 挨親兒 +挨人儿 挨人兒 +挨光 挨光 +挨刀的 挨刀的 +挨到 捱到 +挨呲 挨呲 +挨呲儿 挨呲兒 +挨头子 挨頭子 +挨家 挨家 +挨家挨户 挨家挨戶 +挨山塞海 挨山塞海 +挨延 挨延 +挨得 捱得 +挨得住 捱得住 +挨户 挨戶 +挨打 捱打 +挨打受气 挨打受氣 +挨打受骂 挨打受罵 +挨批 挨批 +挨拿 挨拿 +挨挤 挨擠 +挨挨 挨挨 +挨挨儿 挨挨兒 +挨挨抢抢 挨挨搶搶 +挨挨蹭蹭 挨挨蹭蹭 +挨捕 挨捕 +挨排 挨排 +挨揍 捱揍 +挨整 捱整 +挨斗 挨鬥 +挨日子 捱日子 +挨时间 捱時間 +挨板子 挨板子 +挨棍子 挨棍子 +挨次 挨次 +挨满 捱滿 +挨着 挨着 +挨磨 捱磨 +挨肩 挨肩 +挨肩儿 挨肩兒 +挨肩叠背 挨肩疊背 +挨肩搭背 挨肩搭背 +挨肩擦背 挨肩擦背 +挨肩擦脸 挨肩擦臉 +挨肩擦膀 挨肩擦膀 +挨苦 捱苦 +挨身 挨身 +挨边 挨邊 +挨边儿 挨邊兒 +挨过 捱過 +挨过去 捱過去 +挨过来 捱過來 +挨近 挨近 +挨门 挨門 +挨门挨户 挨門挨戶 +挨门逐户 挨門逐戶 +挨闹 挨鬧 +挨靠 挨靠 +挨风缉缝 挨風緝縫 +挨饥抵饿 捱飢抵餓 +挨饿 捱餓 +挨饿受冻 挨餓受凍 +挨骂 捱罵 +挪借 挪借 +挪出 挪出 +挫折 挫折 +挫折感 挫折感 +振兴医疗复健中心 振興醫療復健中心 +振杰 振杰 +振聋发嘳 振聾發嘳 +振聋发聩 振聾發聵 +振荡 振盪 +振荡器 振盪器 +振荡电流 振盪電流 +振荡电路 振盪電路 +振衣千仞冈 振衣千仞岡 +挹彼注兹 挹彼注茲 +挹彼注此 挹彼注此 +挹注 挹注 +挺了 挺了 +挺出 挺出 +挺出去 挺出去 +挺出来 挺出來 +挺升 挺升 +挺尸 挺屍 +挺起腰板儿 挺起腰板兒 +挺身而出 挺身而出 +挽住 挽住 +挽具 挽具 +挽力 挽力 +挽回 挽回 +挽回不了 挽回不了 +挽夫 輓夫 +挽幛 挽幛 +挽手 挽手 +挽手儿 挽手兒 +挽救 挽救 +挽救不了 挽救不了 +挽救儿童 挽救兒童 +挽曲 輓曲 +挽歌 輓歌 +挽歌郎 輓歌郎 +挽毂 挽轂 +挽留 挽留 +挽眉毛 挽眉毛 +挽联 輓聯 +挽聯 輓聯 +挽脸师 挽臉師 +挽袖 挽袖 +挽詞 輓詞 +挽詩 輓詩 +挽词 輓詞 +挽诗 輓詩 +挽起 挽起 +挽辞 挽辭 +挽面 挽面 +挽额 輓額 +挽髻 挽髻 +挽麦子 挽麥子 +捅了 捅了 +捅出 捅出 +捆了 捆了 +捆扎 捆紮 +捉不准 捉不準 +捉不胜捉 捉不勝捉 +捉发 捉髮 +捉奸 捉姦 +捉奸党 捉奸黨 +捉奸徒 捉奸徒 +捉奸捉双 捉姦捉雙 +捉奸细 捉奸細 +捉奸见双 捉姦見雙 +捉奸见床 捉姦見牀 +捉奸贼 捉奸賊 +捉摸不出 捉摸不出 +捉获 捉獲 +捉贼见赃捉奸见双 捉賊見贓捉姦見雙 +捋虎须 捋虎鬚 +捋采 捋采 +捍御 捍禦 +捏不准 捏不準 +捏了一把冷汗 捏了一把冷汗 +捏了一把汗 捏了一把汗 +捏出 捏出 +捏出水儿来的 捏出水兒來的 +捏制 捏製 +捏合 捏合 +捏造出 捏造出 +捏造出来 捏造出來 +捏面人 捏麪人 +捐出 捐出 +捐出去 捐出去 +捐出来 捐出來 +捐益表 捐益表 +捐赠盈余 捐贈盈餘 +捕影系风 捕影繫風 +捕获 捕獲 +捕虏岩 捕虜岩 +捕虫 捕蟲 +捕虫叶 捕蟲葉 +捕虫堇菜 捕蟲堇菜 +捕虫植物 捕蟲植物 +捕虫灯 捕蟲燈 +捕虫网 捕蟲網 +捕风系影 捕風繫影 +捞出 撈出 +捞干 撈乾 +捞面 撈麪 +损于 損於 +损兵折将 損兵折將 +损军折将 損軍折將 +损将折兵 損將折兵 +损折 損折 +损益表 損益表 +捡了 撿了 +捡回 撿回 +捡回去 撿回去 +捡回来 撿回來 +换个 換個 +换个儿 換個兒 +换了 換了 +换借 換借 +换出 換出 +换出去 換出去 +换出来 換出來 +换厂 換廠 +换发 換髮 +换只 換隻 +换台 換臺 +换吊 換吊 +换向 換向 +换向器 換向器 +换回 換回 +换回去 換回去 +换回来 換回來 +换心手术 換心手術 +换房旅游 換房旅遊 +换扣 換扣 +换挡杆 換擋桿 +换板 換板 +换档杆 換檔桿 +换汇 換匯 +换汤不换药 換湯不換藥 +换签 換籤 +换算无收获面积 換算無收穫面積 +换算表 換算表 +换系 換系 +换肾手术 換腎手術 +换药 換藥 +捣针 搗針 +捣鬼吊白 搗鬼弔白 +捧出 捧出 +捧出去 捧出去 +捧出来 捧出來 +捧回 捧回 +捧杯 捧杯 +据为己有 據爲己有 +据义履方 據義履方 +据了解 據瞭解 +据云 據云 +据以 據以 +据传 據傳 +据传说 據傳說 +据估计 據估計 +据信 據信 +据守 據守 +据守天险 據守天險 +据实 據實 +据实以报 據實以報 +据实相告 據實相告 +据常 據常 +据干而窥井底 據榦而窺井底 +据悉 據悉 +据情办理 據情辦理 +据我看 據我看 +据报 據報 +据报导 據報導 +据报道 據報道 +据有 據有 +据此 據此 +据点 據點 +据牀指麾 據牀指麾 +据理 據理 +据理力争 據理力爭 +据理而争 據理而爭 +据称 據稱 +据统计 據統計 +据说 據說 +据说在 據說在 +据说是 據說是 +据说有 據說有 +据道 據道 +据闻 據聞 +据险固守 據險固守 +据鞍 據鞍 +捵面 捵麪 +捶台拍凳 捶檯拍凳 +捶炼 捶鍊 +捷众药业 捷衆藥業 +捷克 捷克 +捷克人 捷克人 +捷克共和国 捷克共和國 +捷克斯拉夫 捷克斯拉夫 +捷克斯洛伐克 捷克斯洛伐克 +捷克暨斯洛伐克联邦共和国 捷克暨斯洛伐克聯邦共和國 +捷克籍 捷克籍 +捷克语 捷克語 +捷克队 捷克隊 +捷尔任斯克 捷爾任斯克 +捷才 捷才 +捷运系统 捷運系統 +捻合 捻合 +捻针 捻鍼 +捻须 捻鬚 +掀了 掀了 +掀了开来 掀了開來 +掀出 掀出 +掀出去 掀出去 +掀出来 掀出來 +掀帘子 掀簾子 +掂折 掂折 +掂梢折本 掂梢折本 +授权范围 授權範圍 +掉个 掉個 +掉了 掉了 +掉价儿 掉價兒 +掉出 掉出 +掉出来 掉出來 +掉发 掉髮 +掉回头 掉回頭 +掊克 掊克 +掊斗折衡 掊斗折衡 +掌柜 掌櫃 +掌柜的 掌櫃的 +掌状复叶 掌狀複葉 +掏出 掏出 +掏出来 掏出來 +排兵布阵 排兵佈陣 +排出 排出 +排出体 排出體 +排出作用 排出作用 +排出去 排出去 +排出来 排出來 +排列组合 排列組合 +排名表 排名表 +排当 排當 +排扣 排扣 +排放出 排放出 +排放系统 排放系統 +排档杆 排檔桿 +排水系统 排水系統 +排泄系统 排泄系統 +排烟 排煙 +排版系统 排版系統 +排表 排表 +排门挨户 排門挨戶 +排除万难 排除萬難 +排须 排鬚 +排骨面 排骨麪 +掘出 掘出 +掘出来 掘出來 +掘墓鞭尸 掘墓鞭屍 +探个究竟 探個究竟 +探了 探了 +探出 探出 +探出去 探出去 +探出来 探出來 +探听出 探聽出 +探奇访胜 探奇訪勝 +探寻出 探尋出 +探寻胜迹 探尋勝蹟 +探幽访胜 探幽訪勝 +探知欲 探知慾 +探究出 探究出 +探获 探獲 +探询出 探詢出 +探针 探針 +探骊获珠 探驪獲珠 +掣后腿 掣後腿 +掣签 掣籤 +掤扒吊栲 掤扒吊栲 +接出 接出 +接受不了 接受不了 +接口规范 接口規範 +接合 接合 +接合上 接合上 +接合处 接合處 +接合点 接合點 +接合生殖 接合生殖 +接合菌纲 接合菌綱 +接合起来 接合起來 +接合面 接合面 +接回 接回 +接回去 接回去 +接回来 接回來 +接扣 接扣 +接生术 接生術 +接种 接種 +接种率 接種率 +接穗 接穗 +接绍香烟 接紹香煙 +接续香烟 接續香煙 +接获 接獲 +接触面 接觸面 +接近于 接近於 +接面 接面 +控制 控制 +控制不了 控制不了 +控制住 控制住 +控制力 控制力 +控制区 控制區 +控制单元 控制單元 +控制卡 控制卡 +控制台 控制檯 +控制器 控制器 +控制塔 控制塔 +控制好 控制好 +控制室 控制室 +控制性 控制性 +控制权 控制權 +控制杆 控制桿 +控制板 控制板 +控制棒 控制棒 +控制欲 控制慾 +控制码 控制碼 +控制站 控制站 +控制组 控制組 +控制范围 控制範圍 +控制裕如 控制裕如 +控制论 控制論 +控制键 控制鍵 +控卷 控捲 +控御 控御 +推倒了油瓶不扶 推倒了油瓶不扶 +推出 推出 +推出去 推出去 +推出来 推出來 +推升 推升 +推后 推後 +推向 推向 +推回 推回 +推回去 推回去 +推回来 推回來 +推定出来 推定出來 +推干淨儿 推乾淨兒 +推弦 推絃 +推心致腹 推心致腹 +推情准理 推情準理 +推想出 推想出 +推托 推託 +推托之词 推托之詞 +推挽 推輓 +推断出 推斷出 +推杆 推杆 +推杯 推杯 +推派出 推派出 +推演出来 推演出來 +推算出 推算出 +推算出来 推算出來 +推舟于陆 推舟於陸 +推诚布信 推誠佈信 +推诚布公 推誠佈公 +推辇归里 推輦歸里 +推销术 推銷術 +推陈出新 推陳出新 +推陈布新 推陳佈新 +掩卷 掩卷 +掩口卢胡 掩口盧胡 +掩口胡卢 掩口胡盧 +掩恶扬善 掩惡揚善 +掩恶溢美 掩惡溢美 +掩罪藏恶 掩罪藏惡 +掩耳盗钟 掩耳盜鐘 +掩面 掩面 +掩面大哭 掩面大哭 +掩面失色 掩面失色 +掩面而泣 掩面而泣 +掩面而过 掩面而過 +措置失当 措置失當 +措置得当 措置得當 +措词不当 措詞不當 +措辞不当 措辭不當 +掰了 掰了 +掳获 擄獲 +掷出 擲出 +掷杯 擲杯 +掺合 摻合 +揉合 揉合 +揉成一团 揉成一團 +揉面 揉麪 +揍了 揍了 +描了 描了 +描写出 描寫出 +描写出来 描寫出來 +描出 描出 +描出来 描出來 +描涂 描塗 +描绘出 描繪出 +描绘出来 描繪出來 +提不出 提不出 +提个 提個 +提了 提了 +提价 提價 +提克瑞提 提克瑞提 +提克里特 提克里特 +提出 提出 +提出去 提出去 +提出建议 提出建議 +提出异议 提出異議 +提出抗辩 提出抗辯 +提出来 提出來 +提制 提製 +提升 提升 +提升为 提升爲 +提升到 提升到 +提向 提向 +提回 提回 +提回去 提回去 +提回来 提回來 +提子干 提子乾 +提学御史 提學御史 +提干 提幹 +提心吊胆 提心吊膽 +提拉米苏 提拉米蘇 +提摩太后书 提摩太後書 +提梁 提樑 +提炼 提煉 +提炼出 提煉出 +提甕出汲 提甕出汲 +提纯复壮 提純復壯 +提舍尼 提舍尼 +插于 插於 +插回 插回 +插回去 插回去 +插回来 插回來 +插图卷 插圖卷 +插扣 插扣 +插曲 插曲 +插架万轴 插架萬軸 +插箭游营 插箭遊營 +插足于 插足於 +插针 插針 +握不准 握不準 +握云拿雾 握雲拿霧 +握云携雨 握雲攜雨 +握别 握別 +握发 握髮 +握发吐哺 握髮吐哺 +握发吐餐 握髮吐餐 +握手极欢 握手極歡 +握手道别 握手道別 +握雨携云 握雨攜雲 +握雾拿云 握霧拿雲 +揣合逢迎 揣合逢迎 +揣在怀里 揣在懷裏 +揣奸把猾 揣奸把猾 +揩台抹凳 揩檯抹凳 +揩干 揩乾 +揪出 揪出 +揪出去 揪出去 +揪出来 揪出來 +揪发 揪髮 +揪斗 揪鬥 +揪采 揪採 +揪须 揪鬚 +揭丑 揭醜 +揭出 揭出 +揭发 揭發 +揭发出 揭發出 +揭布 揭布 +揭折 揭折 +揭示板 揭示板 +揭露出 揭露出 +援据 援據 +揽胜 攬勝 +揽胜图 攬勝圖 +揽闲事 攬閒事 +搀伙 攙夥 +搁板 擱板 +搁脚板 擱腳板 +搅合 攪合 +搋在怀里 搋在懷裏 +搋面 搋麪 +搌布 搌布 +搏斗 搏鬥 +搏炼 搏煉 +搏砂炼汞 搏砂煉汞 +搓板 搓板 +搓熟的汤团 搓熟的湯團 +搓粉团朱 搓粉團朱 +搓粉抟朱 搓粉摶朱 +搜下 搜下 +搜书 搜書 +搜了 搜了 +搜人 搜人 +搜出 搜出 +搜刮 搜刮 +搜到 搜到 +搜剿 搜剿 +搜吧 搜吧 +搜听 搜聽 +搜啊 搜啊 +搜图 搜圖 +搜奇 搜奇 +搜奇抉怪 搜奇抉怪 +搜奇访古 搜奇訪古 +搜奇选妙 搜奇選妙 +搜宝 搜寶 +搜客 搜客 +搜寻 搜尋 +搜寻到 搜尋到 +搜寻引擎 搜尋引擎 +搜寻软体 搜尋軟體 +搜山 搜山 +搜店 搜店 +搜房 搜房 +搜扬仄陋 搜揚仄陋 +搜扬侧陋 搜揚側陋 +搜括 蒐括 +搜括一空 搜括一空 +搜捕 搜捕 +搜捕到 搜捕到 +搜掠 搜掠 +搜搜 搜搜 +搜搜客 搜搜客 +搜搜客网 搜搜客網 +搜救 搜救 +搜救犬 搜救犬 +搜星 搜星 +搜查 搜查 +搜査证 搜查證 +搜根剔齿 搜根剔齒 +搜根究底 搜根究底 +搜根问底 搜根問底 +搜检 搜檢 +搜歌 搜歌 +搜求 搜求 +搜游 搜遊 +搜爆 搜爆 +搜狐 搜狐 +搜狐网 搜狐網 +搜狗 搜狗 +搜的 搜的 +搜神 搜神 +搜神记 搜神記 +搜秀 搜秀 +搜章擿句 搜章擿句 +搜索 搜索 +搜索到 搜索到 +搜索前进 搜索前進 +搜索半径 搜索半徑 +搜索引擎 搜索引擎 +搜索枯肠 搜索枯腸 +搜索票 搜索票 +搜索结果 搜索結果 +搜索范围 搜索範圍 +搜索队 搜索隊 +搜缴 搜繳 +搜网 搜網 +搜罗 蒐羅 +搜肠刮肚 搜腸刮肚 +搜获 搜獲 +搜藏 蒐藏 +搜藏家 蒐藏家 +搜虎 搜虎 +搜证 蒐證 +搜豹 搜豹 +搜购 蒐購 +搜走 搜走 +搜身 搜身 +搜遍 搜遍 +搜酷 搜酷 +搜集 蒐集 搜集 +搜集到 蒐集到 +搢绅录 搢紳錄 +搪饥 搪飢 +搬出 搬出 +搬出去 搬出去 +搬出来 搬出來 +搬回 搬回 +搬回去 搬回去 +搬回来 搬回來 +搬斗 搬鬥 +搭个 搭個 +搭伙 搭夥 +搭克露 搭克露 +搭出 搭出 +搭干铺 搭乾鋪 +搭扣 搭扣 +搭机回 搭機回 +搭面 搭面 +搯出来 搯出來 +携云挈雨 攜雲挈雨 +携云握雨 攜雲握雨 +携出 攜出 +携同 攜同 +携带式卫星通信系统 攜帶式衛星通信系統 +携手合作 攜手合作 +携手同心 攜手同心 +携手并肩 攜手並肩 +搽穰卷儿 搽穰捲兒 +搽药 搽藥 +摁扣 摁釦 +摄于 攝於 +摄制 攝製 +摄制厂 攝製廠 +摄制成 攝製成 +摄录影机 攝錄影機 +摄影术 攝影術 +摄影艺术 攝影藝術 +摄氏寒暑表 攝氏寒暑表 +摆上 擺上 +摆上去 擺上去 +摆上来 擺上來 +摆下 擺下 +摆下去 擺下去 +摆下来 擺下來 +摆不下 擺不下 +摆不平 擺不平 +摆不开 擺不開 +摆乌龙 擺烏龍 +摆了一道 擺了一道 +摆事实讲道理 擺事實講道理 +摆云锣儿 擺雲鑼兒 +摆供 擺供 +摆出 擺出 +摆出去 擺出去 +摆出来 擺出來 +摆划 擺劃 +摆列 擺列 +摆列出 擺列出 +摆到 擺到 +摆制 擺制 +摆动 擺動 +摆动波 擺動波 +摆卖 擺賣 +摆回 擺回 +摆回去 擺回去 +摆回来 擺回來 +摆在 擺在 +摆在心上 擺在心上 +摆在眼前 擺在眼前 +摆地摊 擺地攤 +摆夷 擺夷 +摆好 擺好 +摆姿势 擺姿勢 +摆子 擺子 +摆官架子 擺官架子 +摆尾 擺尾 +摆尾摇头 擺尾搖頭 +摆布 擺佈 +摆席 擺席 +摆平 擺平 +摆开 擺開 +摆开阵势 擺開陣勢 +摆弄 擺弄 +摆当 擺當 +摆得 擺得 +摆忙 擺忙 +摆成 擺成 +摆手 擺手 +摆拨 擺撥 +摆拨不下 擺撥不下 +摆振 擺振 +摆搠 擺搠 +摆搭 擺搭 +摆摆 擺擺 +摆摆头 擺擺頭 +摆摆手 擺擺手 +摆摆摇摇 擺擺搖搖 +摆摊 擺攤 +摆摊儿 擺攤兒 +摆摊子 擺攤子 +摆撼 擺撼 +摆擂台 擺擂臺 +摆放 擺放 +摆放在 擺放在 +摆明 擺明 +摆来 擺來 +摆来摆去 擺來擺去 +摆架子 擺架子 +摆样 擺樣 +摆样子 擺樣子 +摆格 擺格 +摆槊 擺槊 +摆款 擺款 +摆法 擺法 +摆浪子 擺浪子 +摆渡 擺渡 +摆渡船 擺渡船 +摆满 擺滿 +摆点 擺點 +摆番 擺番 +摆空架子 擺空架子 +摆站 擺站 +摆线 擺線 +摆置 擺置 +摆脱 擺脫 +摆脱不了 擺脫不了 +摆脱到 擺脫到 +摆脱危机 擺脫危機 +摆花架子 擺花架子 +摆荡 擺盪 +摆荡吊环 擺蕩吊環 +摆荡起来 擺盪起來 +摆落 擺落 +摆行阵 擺行陣 +摆袖却金 擺袖卻金 +摆设 擺設 +摆设儿 擺設兒 +摆请儿 擺請兒 +摆谱 擺譜 +摆谱儿 擺譜兒 +摆起 擺起 +摆起来 擺起來 +摆轮 擺輪 +摆进 擺進 +摆进去 擺進去 +摆进来 擺進來 +摆酒 擺酒 +摆酒席 擺酒席 +摆针 擺針 +摆钟 擺鐘 +摆锤 擺錘 +摆长 擺長 +摆门子 擺門子 +摆门面 擺門面 +摆阔 擺闊 +摆阔气 擺闊氣 +摆阵 擺陣 +摆露 擺露 +摆饭 擺飯 +摆饰 擺飾 +摆齐 擺齊 +摆龙门阵 擺龍門陣 +摇了 搖了 +摇了一下 搖了一下 +摇头摆尾 搖頭擺尾 +摇头摆脑 搖頭擺腦 +摇摆 搖擺 +摇摆不定 搖擺不定 +摇摆乐 搖擺樂 +摇摆舞 搖擺舞 +摇摇摆摆 搖搖擺擺 +摇摇欲坠 搖搖欲墜 +摇摇荡荡 搖搖蕩蕩 +摇杆 搖桿 +摇板 搖板 +摇滚乐团 搖滾樂團 +摇滚团 搖滾團 +摇篮曲 搖籃曲 +摇荡 搖盪 +摇荡不停 搖盪不停 +摇车儿里的爷爷拄拐棍儿的孙子 搖車兒裏的爺爺拄拐棍兒的孫子 +摇针 搖針 +摊了 攤了 +摊了事 攤了事 +摊了人命 攤了人命 +摊出 攤出 +摊出去 攤出去 +摊出来 攤出來 +摊薄后每股盈利 攤薄後每股盈利 +摒出 摒出 +摔了 摔了 +摔了一跤 摔了一跤 +摔出 摔出 +摔断了 摔斷了 +摔断了腿 摔斷了腿 +摔筋斗 摔筋斗 +摔跟斗 摔跟斗 +摘伏发隐 摘伏發隱 +摘借 摘借 +摘出 摘出 +摘出去 摘出去 +摘出来 摘出來 +摘奸发伏 摘奸發伏 +摘录 摘錄 +摘录自 摘錄自 +摘绵术 摘綿術 +摛藻雕章 摛藻雕章 +摧兰折玉 摧蘭折玉 +摧坚获丑 摧堅獲醜 +摧志屈道 摧志屈道 +摧折 摧折 +摧枯折腐 摧枯折腐 +摧眉折腰 摧眉折腰 +摩厉以须 摩厲以須 +摩合罗 摩合羅 +摩托 摩托 +摩托化 摩托化 +摩托罗垃 摩托羅垃 +摩托罗拉 摩托羅拉 +摩托船 摩托船 +摩托车 摩托車 +摩托车的士 摩托車的士 +摩托车组 摩托車組 +摩擦系数 摩擦係數 +摩根费里曼 摩根費里曼 +摩肩如云 摩肩如雲 +摩苏尔 摩蘇爾 +摩苏尔市 摩蘇爾市 +摩诃迦叶 摩訶迦葉 +摩里西斯 摩里西斯 +摭采 摭採 +摸不准 摸不準 +摸了 摸了 +摸出 摸出 +摸出去 摸出去 +摸出来 摸出來 +摸彩 摸彩 +摸彩券 摸彩券 +摸彩品 摸彩品 +摸彩箱 摸彩箱 +摸秋 摸秋 +摸索出 摸索出 +摸钟 摸鐘 +摹扎特 摹紮特 +撂了 撂了 +撇吊 撇弔 +撑了 撐了 +撑出 撐出 +撑场面 撐場面 +撑头获脑 撐頭獲腦 +撑市面 撐市面 +撑杆 撐杆 +撑杆跳 撐杆跳 +撑杆跳高 撐杆跳高 +撑门面 撐門面 +撒克逊 撒克遜 +撒克逊人 撒克遜人 +撒出 撒出 +撒呓症 撒囈症 +撒布 撒佈 +撒极 撒極 +撒种 撒種 +撒马尔干 撒馬爾幹 +撕了 撕了 +撞个满怀 撞個滿懷 +撞出 撞出 +撞出去 撞出去 +撞出来 撞出來 +撞击式印表机 撞擊式印表機 +撞向 撞向 +撞尸 撞屍 +撞府冲州 撞府沖州 +撞木钟 撞木鐘 +撞球台 撞球檯 +撞球杆 撞球桿 +撞警钟 撞警鐘 +撞针 撞針 +撞钟 撞鐘 +撞钟太岁 撞鐘太歲 +撞阵冲军 撞陣衝軍 +撤出 撤出 +撤出去 撤出去 +撤出来 撤出來 +撤后 撤後 +撤回 撤回 +撤回去 撤回去 +撤回来 撤回來 +撤帘 撤簾 +撤并 撤併 +撤柜 撤櫃 +撤销记录 撤銷記錄 +撧折 撧折 +撩云拨雨 撩雲撥雨 +撩斗 撩鬥 +撩虎须 撩虎鬚 +撬了 撬了 +撬出去 撬出去 +撬出来 撬出來 +撬杠 撬槓 +播于 播於 +播出 播出 +播出去 播出去 +播出来 播出來 +播出频道 播出頻道 +播发 播發 +播恶遗臭 播惡遺臭 +播放列表 播放列表 +播放歌曲 播放歌曲 +播种 播種 +播种期 播種期 +播种机 播種機 +播种法 播種法 +播种面积 播種面積 +播荡 播蕩 +播越失据 播越失據 +撮合 撮合 +撮合山 撮合山 +撮科打哄 撮科打哄 +撮药 撮藥 +撰出来 撰出來 +撰录 撰錄 +撵出 攆出 +撵出去 攆出去 +撵出来 攆出來 +撺哄 攛哄 +撺哄鸟乱 攛哄鳥亂 +擀面 擀麪 +擀面杖 擀麪杖 +擂台 擂臺 +擂台赛 擂臺賽 +擂鼓交响曲 擂鼓交響曲 +擅于 擅於 +擅长于 擅長於 +操作台 操作檯 +操作系统 操作系統 +操作规范 操作規範 +操作钟 操作鐘 +操同室之戈 操同室之戈 +操纵台 操縱檯 +操纵杆 操縱桿 +操船术 操船術 +擎拳合掌 擎拳合掌 +擐系 擐繫 +擒奸摘伏 擒奸摘伏 +擒奸擿伏 擒奸擿伏 +擒奸讨暴 擒奸討暴 +擒获 擒獲 +擘划 擘劃 +擢升 擢升 +擢发 擢髮 +擢发抽肠 擢髮抽腸 +擢发难数 擢髮難數 +擦俊药 擦俊藥 +擦出 擦出 +擦出爱 擦出愛 +擦刮 擦刮 +擦布 擦布 +擦干 擦乾 +擦干净 擦乾淨 +擦干淨 擦乾淨 +擦碗布 擦碗布 +擦药 擦藥 +擦面子 擦面子 +擿埴索涂 擿埴索塗 +擿奸发伏 擿奸發伏 +攀云 攀雲 +攀亲引戚 攀親引戚 +攀今吊古 攀今吊古 +攀升 攀升 +攀岩 攀巖 +攀折 攀折 +攀花折柳 攀花折柳 +攀蟾折桂 攀蟾折桂 +攧攧仆仆 攧攧仆仆 +支出 支出 +支出费用 支出費用 +支划 支劃 +支努干 支努干 +支干 支幹 +支当 支當 +支撑不了 支撐不了 +支杆 支桿 +支烟 支菸 +支系 支系 +支系统 支系統 +支胄 支胄 +收了 收了 +收出 收出 +收出去 收出去 +收出来 收出來 +收发 收發 +收发器 收發器 +收发室 收發室 +收发短信 收發短信 +收发站 收發站 +收回 收回 +收回到 收回到 +收回去 收回去 +收回成命 收回成命 +收回来 收回來 +收复 收復 +收复失土 收復失土 +收复失地 收復失地 +收复河山 收復河山 +收尸 收屍 +收录 收錄 +收录两用机 收錄兩用機 +收录在 收錄在 +收录音机 收錄音機 +收拾干淨 收拾乾淨 +收据 收據 +收款台 收款臺 +收汇 收匯 +收盘价 收盤價 +收盘价格 收盤價格 +收盘汇率 收盤匯率 +收秋 收秋 +收获 收穫 +收获节 收穫節 +收获量 收穫量 +收购价 收購價 +收购价格 收購價格 +收针 收針 +收银台 收銀臺 +攸戚相关 攸慼相關 +改不了 改不了 +改个 改個 +改了 改了 +改了又改 改了又改 +改于 改於 +改修 改修 +改制 改制 改製 +改制为 改製爲 +改变不了 改變不了 +改向 改向 +改回 改回 +改头换面 改頭換面 +改当 改當 +改征 改徵 +改念 改唸 +改恶向善 改惡向善 +改扣 改扣 +改签 改簽 +改良品种 改良品種 +改良种 改良種 +攻了 攻了 +攻克 攻克 +攻击范围 攻擊範圍 +攻击面 攻擊面 +攻占 攻佔 +攻向 攻向 +攻守同盟 攻守同盟 +攻无不克 攻無不克 +攻无不克战无不胜 攻無不克戰無不勝 +放一百二十个心 放一百二十個心 +放个 放個 +放之四海皆准 放之四海皆準 +放之四海而皆准 放之四海而皆準 +放了 放了 +放了屁儿却使手掩 放了屁兒卻使手掩 +放党 放黨 +放出 放出 +放出去 放出去 +放出手眼 放出手眼 +放出来 放出來 +放参 放參 +放回 放回 +放回去 放回去 +放在心里 放在心裏 +放在眼里 放在眼裏 +放大系数 放大係數 +放学后 放學後 +放射出 放射出 +放射出来 放射出來 +放射性发光材料 放射性發光材料 +放射性同位素 放射性同位素 +放射性烟羽 放射性煙羽 +放射虫 放射蟲 +放尽 放盡 +放屁虫 放屁蟲 +放暗箭 放暗箭 +放松 放鬆 +放松管制 放松管制 +放流水标准 放流水標準 +放烟 放煙 +放烟幕 放煙幕 +放烟幕弹 放煙幕彈 +放烟火 放煙火 +放荡 放蕩 +放荡不羁 放蕩不羈 +放荡任气 放蕩任氣 +放荡弛纵 放蕩弛縱 +放蒙挣 放懞掙 +放诸四海皆准 放諸四海皆準 +放轻松 放輕鬆 +放马后炮 放馬後炮 +放马后砲 放馬後砲 +政党 政黨 +政党政治 政黨政治 +政出多门 政出多門 +政制 政制 +政团 政團 +政坛 政壇 +政工干校 政工幹校 +政府机关开放系统互连总则 政府機關開放系統互連總則 +政教合一 政教合一 +政治体制 政治體制 +政治局面 政治局面 +政治斗争 政治鬥爭 +政治系 政治系 +政治舞台 政治舞臺 +政治面 政治面 +政策面 政策面 +政见发表 政見發表 +故事里 故事裏 +故于 故於 +故云 故云 +故出 故出 +故地重游 故地重遊 +故当 故當 +故态复萌 故態復萌 +故舍 故舍 +故里 故里 +效价能 效價能 +敌党 敵黨 +敌前敌后 敵前敵後 +敌占区 敵佔區 +敌台 敵臺 +敌后 敵後 +敌后伏击 敵後伏擊 +敌后作战 敵後作戰 +敌后工作 敵後工作 +敌后工作人员 敵後工作人員 +敌后方 敵後方 +敌忾同仇 敵愾同仇 +敌百虫 敵百蟲 +敏于 敏於 +敏于事而慎于言 敏於事而慎於言 +敏于伎 敏於伎 +救人一命胜造七级浮屠 救人一命勝造七級浮屠 +救人须救彻 救人須救徹 +救出 救出 +救出去 救出去 +救出来 救出來 +救回 救回 +救回去 救回去 +救回来 救回來 +救困扶危 救困扶危 +救国团 救國團 +救恩计划 救恩計劃 +救生艇甲板 救生艇甲板 +救药 救藥 +敖游 敖遊 +敖荡 敖盪 +教个 教個 +教了 教了 +教于 教於 +教准部 教準部 +教出 教出 +教团 教團 +教坛 教壇 +教外别传 教外別傳 +教学示范 教學示範 +教学计划 教學計劃 +教学钟 教學鐘 +教猱升木 教猱升木 +教练团 教練團 +教育出来 教育出來 +教育制度 教育制度 +教育团体 教育團體 +教育方针 教育方針 +教育系 教育系 +教胄 教胄 +教范 教範 +敝帚千金 敝帚千金 +敝舍 敝舍 +敢于 敢於 +敢作敢当 敢作敢當 +敢借 敢借 +敢做敢当 敢做敢當 +敢出 敢出 +敢出去 敢出去 +敢出来 敢出來 +敢向 敢向 +敢干 敢幹 +敢当 敢當 +敢情欲 敢情欲 +敢摆 敢擺 +敢斗了胆 敢斗了膽 +散于 散於 +散伙 散夥 +散兵游勇 散兵遊勇 +散出 散出 +散发 散發 散髮 +散发传单 散發傳單 +散发出 散發出 +散发出来 散發出來 +散尽 散盡 +散布 散佈 +散布开 散佈開 +散布者 散佈者 +散曲 散曲 +散板 散板 +散荡 散蕩 +敦克尔克大撤退 敦克爾克大撤退 +敦朴 敦樸 +敦煌千佛洞 敦煌千佛洞 +敦煌曲子 敦煌曲子 +敬同 敬同 +敬姜犹绩 敬姜猶績 +敬挽 敬輓 +敬烟 敬菸 +敬鉴 敬鑒 +数万 數萬 +数万人 數萬人 +数万元 數萬元 +数不尽 數不盡 +数不胜数 數不勝數 +数与虏确 數與虜确 +数了 數了 +数以万计 數以萬計 +数以万订 數以萬訂 +数以千计 數以千計 +数以百万计 數以百萬計 +数位化录音带 數位化錄音帶 +数位快速篆刻系统 數位快速篆刻系統 +数位控制 數位控制 +数位板 數位板 +数位艺术 數位藝術 +数借 數借 +数值控制 數值控制 +数值范围 數值範圍 +数出 數出 +数十万 數十萬 +数千 數千 +数千万 數千萬 +数千亿 數千億 +数周 數週 +数天后 數天後 +数字时钟 數字時鐘 +数字系数 數字係數 +数字钟 數字鐘 +数字钟表 數字鐘錶 +数学系 數學系 +数据 數據 +数据介面 數據介面 +数据传输 數據傳輸 +数据卡 數據卡 +数据压缩 數據壓縮 +数据处理 數據處理 +数据库 數據庫 +数据总线 數據總線 +数据挖掘 數據挖掘 +数据接口 數據接口 +数据机 數據機 +数据段 數據段 +数据流 數據流 +数据源 數據源 +数据结构 數據結構 +数据网络 數據網絡 +数据调制解调器 數據調制解調器 +数据资料 數據資料 +数据通信 數據通信 +数据通讯 數據通訊 +数据链 數據鏈 +数据链路 數據鏈路 +数据链路层 數據鏈路層 +数据链路连接识别码 數據鏈路連接識別碼 +数术 數術 +数杯 數杯 +数百万 數百萬 +数米志炊 數米志炊 +数罪并罚 數罪併罰 +敲丧钟 敲喪鐘 +敲了 敲了 +敲出 敲出 +敲钟 敲鐘 +整个 整個 +整个人 整個人 +整个地球 整個地球 +整个月 整個月 +整个来说 整個來說 +整了 整了 +整人游戏 整人遊戲 +整修 整修 +整修费 整修費 +整党 整黨 +整出剧 整齣劇 +整出戏 整齣戲 +整厂输出 整廠輸出 +整发 整發 +整发用品 整髮用品 +整只 整隻 +整叶 整葉 +整合 整合 +整合出 整合出 +整合到 整合到 +整合商 整合商 +整合型 整合型 +整合式 整合式 +整合性 整合性 +整合成 整合成 +整合法 整合法 +整合管理资讯化 整合管理資訊化 +整合系统 整合系統 +整合行销传播 整合行銷傳播 +整周 整週 +整型手术 整型手術 +整型术 整型術 +整容手术 整容手術 +整容术 整容術 +整庄 整莊 +整形术 整形術 +整数集合 整數集合 +整杯 整杯 +整杯水 整杯水 +整杯茶 整杯茶 +整杯酒 整杯酒 +整柜 整櫃 +整根烟 整根菸 +整理出 整理出 +整理出来 整理出來 +整装待发 整裝待發 +整面 整面 +整风后 整風後 +整齐划一 整齊劃一 +整齐干淨 整齊乾淨 +敷了 敷了 +敷出 敷出 +敷布 敷布 +敷彩泥塑 敷彩泥塑 +敷涂 敷塗 +敷药 敷藥 +敷衍了事 敷衍了事 +敷面 敷面 +敷面膜 敷面膜 +敹一针 敹一針 +文不尽意 文不盡意 +文丑 文丑 +文书鉴定 文書鑑定 +文了 文了 +文件旅游 文件旅遊 +文件柜 文件櫃 +文修武偃 文修武偃 +文修武备 文修武備 +文创志业 文創志業 +文化体系 文化體系 +文化冲击 文化衝擊 +文化斗争 文化鬥爭 +文化水准 文化水準 +文同 文同 +文君当垆 文君當壚 +文坛 文壇 +文坛人士 文壇人士 +文复会 文復會 +文学系 文學系 +文彩 文彩 +文征明 文徵明 +文心雕龙 文心雕龍 +文思泉涌 文思泉涌 +文情并茂 文情並茂 +文才 文才 +文擅雕龙 文擅雕龍 +文斯范恩 文斯范恩 +文星和合 文星和合 +文曲 文曲 +文曲星 文曲星 +文武全才 文武全才 +文武合一 文武合一 +文汇报 文匯報 +文汇阁 文匯閣 +文炳雕龙 文炳雕龍 +文种 文種 +文章星斗 文章星斗 +文致 文致 +文艺复兴 文藝復興 +文艺演出 文藝演出 +文表 文表 +文身断发 文身斷髮 +文选烂秀才半 文選爛秀才半 +文采 文采 +文采出众 文采出衆 +文采郁郁 文采郁郁 +文采风流 文采風流 +文鉴 文鑑 +文锦复阱 文錦覆阱 +文面 文面 +斋坛 齋壇 +斋栗 齋慄 +斋舍 齋舍 +斐然向风 斐然向風 +斑岩 斑岩 +斗一斗 鬥一鬥 +斗上 鬥上 +斗上一斗 鬥上一鬥 +斗不过 鬥不過 +斗丽 鬥麗 +斗了 鬥了 +斗了一斗 鬥了一鬥 +斗了起来 鬥了起來 +斗争 鬥爭 +斗争大会 鬥爭大會 +斗争形式 鬥爭形式 +斗争性 鬥爭性 +斗争意志 鬥爭意志 +斗争斗合 鬥爭鬥合 +斗争方式 鬥爭方式 +斗他一斗 鬥他一鬥 +斗倒 鬥倒 +斗储 斗儲 +斗六 斗六 +斗六市 斗六市 +斗内力 鬥內力 +斗分子 鬥分子 +斗别气 鬥彆氣 +斗力 鬥力 +斗力不如斗智 鬥力不如鬥智 +斗劲 鬥勁 +斗十余 鬥十餘 +斗升 斗升 +斗升之水 斗升之水 +斗升之禄 斗升之祿 +斗南 斗南 +斗南一人 斗南一人 +斗南镇 斗南鎮 +斗叠 鬥疊 +斗口 鬥口 +斗口齿 鬥口齒 +斗叶儿 鬥葉兒 +斗叶子 鬥葉子 +斗合 鬥合 +斗哄 鬥鬨 +斗嘴 鬥嘴 +斗地主 鬥地主 +斗城 斗城 +斗士 鬥士 +斗大 斗大 +斗大的手卷 斗大的手卷 +斗大的馒头 斗大的饅頭 +斗头 鬥頭 +斗她一斗 鬥她一鬥 +斗子 斗子 +斗室 斗室 +斗室生辉 斗室生輝 +斗富 鬥富 +斗小马 斗小馬 +斗尾港 斗尾港 +斗居 斗居 +斗山 斗山 +斗巧 鬥巧 +斗巧争奇 鬥巧爭奇 +斗帐 斗帳 +斗幌子 鬥幌子 +斗店 斗店 +斗府 斗府 +斗弄 鬥弄 +斗引 鬥引 +斗彩 鬥彩 +斗很 鬥很 +斗心眼 鬥心眼 +斗志 鬥志 +斗志昂扬 鬥志昂揚 +斗志高昂 鬥志高昂 +斗成 鬥成 +斗打 鬥打 +斗批改 鬥批改 +斗技 鬥技 +斗折蛇行 斗折蛇行 +斗拱 斗拱 +斗数 斗數 +斗文 鬥文 +斗斋 斗齋 +斗斗 鬥鬥 斗斗 +斗斗嘴 鬥鬥嘴 +斗斛之禄 斗斛之祿 +斗方 斗方 +斗方名士 斗方名士 +斗智 鬥智 +斗智不斗力 鬥智不鬥力 +斗智斗力 鬥智鬥力 +斗暴 鬥暴 +斗杓 斗杓 +斗杓东指 斗杓東指 +斗杓转势 斗杓轉勢 +斗来斗去 鬥來鬥去 +斗极 斗極 +斗柄 斗柄 +斗栱 斗栱 +斗概 斗概 +斗武 鬥武 +斗殴 鬥毆 +斗气 鬥氣 +斗法 鬥法 +斗渠 斗渠 +斗灯 斗燈 +斗烟丝 斗菸絲 +斗然 斗然 +斗牌 鬥牌 +斗牙拌齿 鬥牙拌齒 +斗牙斗齿 鬥牙鬥齒 +斗牛 鬥牛 +斗牛㹴 鬥牛㹴 +斗牛之间 斗牛之間 +斗牛场 鬥牛場 +斗牛士 鬥牛士 +斗牛士之歌 鬥牛士之歌 +斗牛梗 鬥牛梗 +斗牛赛 鬥牛賽 +斗牛阵 鬥牛陣 +斗犀台 鬥犀臺 +斗犬 鬥犬 +斗狠 鬥狠 +斗百余 鬥百餘 +斗百草 鬥百草 +斗的 斗的 +斗真 斗真 +斗眼 鬥眼 +斗私批修 鬥私批修 +斗笠 斗笠 +斗筲 斗筲 +斗筲之人 斗筲之人 +斗筲之器 斗筲之器 +斗筲之徒 斗筲之徒 +斗筲之才 斗筲之才 +斗筲之材 斗筲之材 +斗筲之辈 斗筲之輩 +斗筲小器 斗筲小器 +斗筲役 斗筲役 +斗筲穿窬 斗筲穿窬 +斗箕 斗箕 +斗篷 斗篷 +斗粟囊金 斗粟囊金 +斗粟尺布 斗粟尺布 +斗纹 斗紋 +斗绝 斗絕 +斗绝一隅 斗絕一隅 +斗罗大陆 斗羅大陸 +斗而铸兵 鬥而鑄兵 +斗而铸锥 鬥而鑄錐 +斗胆 斗膽 +斗胜 鬥勝 +斗脚 鬥腳 +斗舰 鬥艦 +斗艳 鬥豔 +斗艳争芳 鬥豔爭芳 +斗茶 鬥茶 +斗草 鬥草 +斗蓬装 斗蓬裝 +斗薮 斗藪 +斗蟋蟀 鬥蟋蟀 +斗话 鬥話 +斗起 鬥起 +斗起来 鬥起來 +斗趣 鬥趣 +斗趣儿 鬥趣兒 +斗车 斗車 +斗转参横 斗轉參橫 +斗转星移 斗轉星移 +斗酒 斗酒 +斗酒博凉州 斗酒博涼州 +斗酒只鸡 斗酒隻雞 +斗酒学士 斗酒學士 +斗酒百篇 斗酒百篇 +斗重山齐 斗重山齊 +斗量 斗量 +斗量车载 斗量車載 +斗门 斗門 +斗门区 斗門區 +斗闲气 鬥閒氣 +斗闷 鬥悶 +斗闷子 鬥悶子 +斗阵 鬥陣 +斗雪红 鬥雪紅 +斗顿 斗頓 +斗风 鬥風 +斗食 斗食 +斗饤 鬥飣 +斗香 斗香 +斗魁 斗魁 +斗鱼 鬥魚 +斗鸡 鬥雞 +斗鸡台 鬥雞臺 +斗鸡场 鬥雞場 +斗鸡眼 鬥雞眼 +斗鸡走狗 鬥雞走狗 +斗鸡走马 鬥雞走馬 +斗鸭 鬥鴨 +斗鹌鹑 鬥鵪鶉 +料不准 料不準 +料前不能料后 料前不能料後 +料敌制胜 料敵制勝 +料斗 料斗 +斜了 斜了 +斜对面 斜對面 +斜座标系 斜座標系 +斜曲 斜曲 +斜杠 斜槓 +斜管面 斜管麪 +斜纹布 斜紋布 +斜谷 斜谷 +斜面 斜面 +斤斗 斤斗 +斥卤 斥鹵 +斩尽 斬盡 +斩尽杀绝 斬盡殺絕 +斩获 斬獲 +斫雕为朴 斫雕爲樸 +断不了 斷不了 +断了 斷了 +断仇谷 斷仇谷 +断发 斷髮 +断发文身 斷髮文身 +断后 斷後 +断后路 斷後路 +断头台 斷頭臺 +断弦 斷絃 +断念 斷念 +断根绝种 斷根絕種 +断烟 斷煙 +断种 斷種 +断纸余墨 斷紙余墨 +断钗重合 斷釵重合 +断雨残云 斷雨殘雲 +断面 斷面 +断面图 斷面圖 +斯伯丁杯 斯伯丁盃 +斯克 斯克 +斯克里亚宾 斯克里亞賓 +斯坦贝克 斯坦貝克 +斯干 斯干 +斯当东 斯當東 +斯托 斯托 +斯托肯立石圈 斯托肯立石圈 +斯摩棱斯克 斯摩棱斯克 +斯杯 斯杯 +斯洛伐克 斯洛伐克 +斯洛伐克共和国 斯洛伐克共和國 +斯洛伐克语 斯洛伐克語 +斯瓦希里 斯瓦希里 +斯瓦希里语 斯瓦希里語 +斯瓦特谷地 斯瓦特谷地 +斯科普里 斯科普里 +斯芬克士 斯芬克士 +斯芬克斯 斯芬克斯 +斯诺克 斯諾克 +斯迪里 斯迪里 +斯里 斯里 +斯里兰卡 斯里蘭卡 +斯里兰卡民主社会主义共和国 斯里蘭卡民主社會主義共和國 +斯里兰卡电信 斯里蘭卡電信 +斯里巴加湾港 斯里巴加灣港 +斯里査潘 斯里查潘 +斯雷布雷尼察 斯雷布雷尼察 +斯须 斯須 +新不伦瑞克 新不倫瑞克 +新丰 新豐 +新丰乡 新豐鄉 +新丰县 新豐縣 +新丰酒 新豐酒 +新书看板 新書看板 +新余 新餘 +新余市 新餘市 +新修本草 新修本草 +新党 新黨 +新几內亚 新幾內亞 +新几內亚岛 新幾內亞島 +新几内亚 新幾內亞 +新出 新出 +新出土 新出土 +新出土儿 新出土兒 +新出手儿 新出手兒 +新出来 新出來 +新出炉 新出爐 +新出生 新出生 +新制 新制 +新制度 新制度 +新剧同志会 新劇同志會 +新单曲 新單曲 +新历 新曆 +新历史 新歷史 +新发 新發 +新发明 新發明 +新发村 新發村 +新台 新臺 +新台币 新臺幣 +新叶 新葉 +新喀里多尼亚 新喀里多尼亞 +新城电台 新城電臺 +新娘 新娘 +新娘子 新娘子 +新娘花 新娘花 +新娘车 新娘車 +新娘进了房媒人扔过墙 新娘進了房媒人扔過牆 +新婚不如远别 新婚不如遠別 +新嫁娘 新嫁娘 +新局面 新局面 +新干 新干 +新干县 新干縣 +新干线 新幹線 +新庄 新莊 +新庄市 新莊市 +新征 新徵 +新德里 新德里 +新慕道团 新慕道團 +新扎 新紮 +新技术 新技術 +新报台 新報臺 +新斯科舍 新斯科舍 +新曲 新曲 +新板特区 新板特區 +新注音 新注音 +新注音输入法 新注音輸入法 +新潮流系 新潮流系 +新疆回变 新疆回變 +新疆师范大学 新疆師範大學 +新秋 新秋 +新竹师范学院 新竹師範學院 +新系 新系 +新纪录 新紀錄 +新罕布什尔 新罕布什爾 +新罕布什尔州 新罕布什爾州 +新罕布夏 新罕布夏 +新胜利 新勝利 +新艺术 新藝術 +新艺综合体 新藝綜合體 +新芬党 新芬黨 +新药 新葯 +新莺出谷 新鶯出谷 +新规范 新規範 +新闻价值 新聞價值 +新闻出版总署 新聞出版總署 +新闻发布会 新聞發佈會 +新闻发言人 新聞發言人 +新闻台 新聞臺 +新闻周刊 新聞週刊 +新闻杂志 新聞雜誌 +新闻系 新聞系 +新集团 新集團 +新面孔 新面孔 +斲雕为朴 斲雕爲樸 +方便面 方便麪 +方公里 方公里 +方几 方几 +方向 方向 +方向仪 方向儀 +方向性 方向性 +方向感 方向感 +方向灯 方向燈 +方向盘 方向盤 +方向线 方向線 +方向舵 方向舵 +方圆十里 方圓十里 +方岳 方岳 +方彩绫 方彩綾 +方志 方誌 +方志友 方志友 +方志贤 方志賢 +方才 方纔 +方斯蔑如 方斯蔑如 +方方面面 方方面面 +方术 方術 +方济谷派 方濟谷派 +方胜 方勝 +方药 方藥 +方言志 方言志 +方里 方里 +方针 方針 +方面 方面 +方面兼圻 方面兼圻 +方面大耳 方面大耳 +方面官 方面官 +方面官员 方面官員 +於夫罗 於夫羅 +施于 施於 +施仁布德 施仁佈德 +施仁布恩 施仁佈恩 +施仁布泽 施仁佈澤 +施佳升 施佳昇 +施出 施出 +施恩布德 施恩佈德 +施放烟火 施放煙火 +施朱傅粉 施朱傅粉 +施瓦布 施瓦布 +施粥舍饭 施粥捨飯 +施耐庵 施耐庵 +施舍 施捨 +施舍之道 施舍之道 +施药 施藥 +施诺布莉琪 施諾布莉琪 +旁出 旁出 +旁征 旁徵 +旁征博引 旁徵博引 +旁注 旁註 +旁系 旁系 +旁系亲 旁系親 +旁系亲属 旁系親屬 +旁系血亲 旁系血親 +旁观者审当局者迷 旁觀者審當局者迷 +旅历 旅歷 +旅游 旅遊 +旅游业 旅遊業 +旅游书 旅遊書 +旅游事业 旅遊事業 +旅游区 旅遊區 +旅游卡 旅遊卡 +旅游史 旅遊史 +旅游团 旅遊團 +旅游城市 旅遊城市 +旅游客 旅遊客 +旅游局 旅遊局 +旅游局长 旅遊局長 +旅游展 旅遊展 +旅游手冊 旅遊手冊 +旅游景点 旅遊景點 +旅游村 旅遊村 +旅游点 旅遊點 +旅游热点 旅遊熱點 +旅游界 旅遊界 +旅游社 旅遊社 +旅游线 旅遊線 +旅游网 旅遊網 +旅游者 旅遊者 +旅游胜地 旅遊勝地 +旅游集散 旅遊集散 +旅游馆 旅遊館 +旅程表 旅程表 +旅舍 旅舍 +旅行剧团 旅行劇團 +旅行团 旅行團 +旋回 旋迴 +旋干转坤 旋乾轉坤 +旋松 旋鬆 +旋毛虫 旋毛蟲 +旋胡 旋胡 +旋转乾坤 旋轉乾坤 +旋转乾坤之力 旋轉乾坤之力 +旋转台 旋轉臺 +旋转曲面 旋轉曲面 +旋转极 旋轉極 +旋辟 旋辟 +旋里 旋里 +旌别 旌別 +旌善惩恶 旌善懲惡 +旌恤 旌卹 +旌表 旌表 +旍表 旍表 +族党 族黨 +族里 族裏 +旗开得胜 旗開得勝 +旗杆 旗杆 +旗杆上绑鸡毛 旗杆上綁雞毛 +旗鼓相当 旗鼓相當 +旛胜 旛勝 +无一幸免 無一倖免 +无业游民 無業遊民 +无业闲散 無業閒散 +无主尸 無主屍 +无了无休 無了無休 +无云 無雲 +无以复加 無以復加 +无价 無價 +无价之宝 無價之寶 +无价事 無价事 +无价宝 無價寶 +无价珍珠 無價珍珠 +无伴奏合唱 無伴奏合唱 +无余 無餘 +无偏无党 無偏無黨 +无党 無黨 +无党无偏 無黨無偏 +无党无派 無黨無派 +无党派 無黨派 +无党派投票人 無黨派投票人 +无党籍 無黨籍 +无农药 無農藥 +无冤无仇 無冤無仇 +无冬无夏 無冬無夏 +无几 無幾 +无凭无据 無憑無據 +无出 無出 +无出其右 無出其右 +无动于衷 無動於衷 +无助于 無助於 +无千大万 無千大萬 +无千带数 無千帶數 +无千无万 無千無萬 +无厘头 無厘頭 +无取胜希望者 無取勝希望者 +无可救药 無可救藥 +无后 無後 +无后为大 無後爲大 +无回豁 無回豁 +无复孑遗 無復孑遺 +无头愿 無頭願 +无尽 無盡 +无尽无休 無盡無休 +无尽无穷 無盡無窮 +无尽缘起 無盡緣起 +无尽藏 無盡藏 +无尿症 無尿症 +无干 無干 +无店面 無店面 +无异于 無異於 +无形输出 無形輸出 +无征不信 無徵不信 +无念 無念 +无恶不作 無惡不作 +无愧于 無愧於 +无所不用其极 無所不用其極 +无所牵挂 無所牽掛 +无所适从 無所適從 +无挂无碍 無掛無礙 +无损于 無損於 +无据 無據 +无敌不克 無敵不克 +无期别 無期別 +无机化合物 無機化合物 +无极 無極 +无极县 無極縣 +无核 無核 +无梁 無樑 +无梁斗 無樑斗 +无梁楼盖 無樑樓蓋 +无欲 無慾 +无欲则刚 無欲則剛 +无止尽 無止盡 +无法克制 無法剋制 +无法挽救 無法挽救 +无济于事 無濟於事 +无烟 無煙 +无烟囱工业 無煙囪工業 +无烟火药 無煙火藥 +无烟炭 無煙炭 +无烟焦煤 無煙焦煤 +无烟煤 無煙煤 +无牵无挂 無牽無掛 +无畏于 無畏於 +无畏布施 無畏佈施 +无穷尽 無窮盡 +无穷无尽 無窮無盡 +无精打采 無精打采 +无线电台 無線電臺 +无缘见面 無緣見面 +无药 無藥 +无药可救 無藥可救 +无补于世 無補於世 +无补于事 無補於事 +无补于时 無補於時 +无表情 無表情 +无视于 無視於 +无计向 無計向 +无足挂齿 無足掛齒 +无适无莫 無適無莫 +无针不引线 無針不引線 +无针注射器 無針注射器 +无钩绦虫 無鉤絛蟲 +无限制 無限制 +无面值邮票 無面值郵票 +无面目 無面目 +无须 無須 +既得陇复望蜀 既得隴復望蜀 +既有今日何必当初 既有今日何必當初 +日久弥新 日久彌新 +日出 日出 +日出万言必有一伤 日出萬言必有一傷 +日出三竿 日出三竿 +日出日落 日出日落 +日出而作 日出而作 +日出而作日入而息 日出而作日入而息 +日制 日製 +日升 日升 +日升月恒 日升月恆 +日占 日佔 +日历 日曆 +日历年度 日曆年度 +日历纸 日曆紙 +日历表 日曆表 +日后 日後 +日均价 日均價 +日复一日 日復一日 +日头打西出来 日頭打西出來 +日子里 日子裏 +日干 日干 +日干夜干 日幹夜幹 +日御 日御 +日志 日誌 +日据 日據 +日据时代 日據時代 +日文系 日文系 +日晒 日曬 +日月升恒 日月升恆 +日月参辰 日月參辰 +日月合璧 日月合璧 +日本共产党 日本共產黨 +日本共同社 日本共同社 +日本制 日本製 +日本台 日本臺 +日本团 日本團 +日本国志 日本國誌 +日本社会党 日本社會黨 +日正当中 日正當中 +日比谷公园 日比谷公園 +日游 日遊 +日理万机 日理萬機 +日番谷 日番谷 +日知录 日知錄 +日程表 日程表 +日系 日系 +日行千里 日行千里 +日表 日表 +日试万言 日試萬言 +日语系 日語系 +日趋恶劣 日趨惡劣 +日转千街 日轉千街 +日转千阶 日轉千階 +日进斗金 日進斗金 +日里 日裏 +日锻月炼 日鍛月煉 +日食万钱 日食萬錢 +旧公烟 舊公煙 +旧制 舊制 +旧制度 舊制度 +旧厂 舊廠 +旧历 舊曆 +旧历史 舊歷史 +旧历年 舊曆年 +旧地重游 舊地重遊 +旧庄 舊莊 +旧念复萌 舊念復萌 +旧态复萌 舊態復萌 +旧恨新仇 舊恨新仇 +旧恶 舊惡 +旧游 舊遊 +旧疾复发 舊疾復發 +旧病复发 舊病復發 +旧症 舊症 +旧皇历 舊皇曆 +旧药 舊藥 +旧表 舊錶 +旧钟 舊鐘 +旧钟表 舊鐘錶 +早了 早了 +早于 早於 +早借 早借 +早借早还 早借早還 +早出 早出 +早出晚归 早出晚歸 +早动手早收获 早動手早收穫 +早占勿药 早占勿藥 +早去早回 早去早回 +早参 早參 +早名必折 早名必折 +早日康复 早日康復 +早有计划 早有計劃 +早知今日何必当初 早知今日何必當初 +早知今日悔不当初 早知今日悔不當初 +早秋 早秋 +早自修 早自修 +早起的鸟儿有虫吃 早起的鳥兒有蟲吃 +旭日东升 旭日東昇 +旭日初升 旭日初昇 +旱干 旱乾 +旱烟 旱菸 +旱烟筒 旱菸筒 +旱烟袋 旱菸袋 +时代不同风尚不同 時代不同風尚不同 +时代周刊 時代週刊 +时价 時價 +时刻准备 時刻準備 +时刻表 時刻表 +时后 時後 +时和岁丰 時和歲豐 +时和年丰 時和年豐 +时宪历 時憲曆 +时尚周 時尚週 +时干下 時幹下 +时念 時念 +时报周刊 時報週刊 +时报杂志 時報雜誌 +时症 時症 +时程表 時程表 +时紧时松 時緊時鬆 +时装周 時裝週 +时装表演 時裝表演 +时针 時針 +时钟 時鐘 +时钟座 時鐘座 +时间电价 時間電價 +时间艺术 時間藝術 +时间范围 時間範圍 +时间表 時間表 +时间里 時間裏 +旷世之才 曠世之才 +旷世奇才 曠世奇才 +旷世逸才 曠世逸才 +旷日弥久 曠日彌久 +旷若发蒙 曠若發矇 +旷荡 曠蕩 +旷课记录 曠課記錄 +旸谷 暘谷 +昂纳克 昂納克 +昆仑 崑崙 +昆仑山 崑崙山 +昆仑山脉 崑崙山脈 +昆剧 崑劇 +昆宁佩克 昆寧佩克 +昆山 崑山 +昆布 昆布 +昆曲 崑曲 +昆腔 崑腔 +昆苏 崑蘇 +昆虫 昆蟲 +昆虫学 昆蟲學 +昆虫学家 昆蟲學家 +昆虫纲 昆蟲綱 +昆调 崑調 +昆都仑 昆都侖 +昆都仑区 昆都侖區 +昊天不吊 昊天不弔 +昊天罔极 昊天罔極 +昌吉回族自治州 昌吉回族自治州 +明中舍去暗中来 明中捨去暗中來 +明了 明瞭 +明争暗斗 明爭暗鬥 +明于 明於 +明于观人 明於觀人 +明人不做暗事 明人不做暗事 +明人不说暗话 明人不說暗話 +明伙画供 明伙畫供 +明修栈道 明修棧道 +明修栈道暗渡陈仓 明修棧道暗渡陳倉 +明升暗降 明升暗降 +明华园歌剧团 明華園歌劇團 +明发 明發 +明台 明臺 +明后 明後 +明后天 明後天 +明复 明覆 +明夷待访录 明夷待訪錄 +明媚闲雅 明媚閒雅 +明实录 明實錄 +明察暗访 明察暗訪 +明察秋毫 明察秋毫 +明尼苏大学 明尼蘇大學 +明尼苏达 明尼蘇達 +明尼苏达州 明尼蘇達州 +明岗暗哨 明崗暗哨 +明师出高徒 明師出高徒 +明并日月 明並日月 +明弃暗取 明棄暗取 +明当 明當 +明征 明徵 +明志 明志 +明志工专 明志工專 +明志工业 明志工業 +明志工业专科学校 明志工業專科學校 +明扣 明釦 +明据 明據 +明摆 明擺 +明摆着 明擺着 +明斯克 明斯克 +明是一盆火暗是一把刀 明是一盆火暗是一把刀 +明暗 明暗 +明暗不定 明暗不定 +明月当空 明月當空 +明杠 明槓 +明来暗往 明來暗往 +明板 明板 +明枪好躲暗箭难防 明槍好躲暗箭難防 +明枪易趓暗箭难防 明槍易趓暗箭難防 +明枪易躲暗箭难防 明槍易躲暗箭難防 +明枪暗箭 明槍暗箭 +明查暗访 明查暗訪 +明査暗访 明查暗訪 +明欺暗骗 明欺暗騙 +明沟暗渠 明溝暗渠 +明潭抽蓄水力发电工程 明潭抽蓄水力發電工程 +明珠暗投 明珠暗投 +明白了当 明白了當 +明目张胆 明目張膽 +明知就里 明知就裏 +明知山有虎偏向虎山行 明知山有虎偏向虎山行 +明知山有虎故作采樵人 明知山有虎故作採樵人 +明确 明確 +明确性 明確性 +明窗净几 明窗淨几 +明窗彩户 明窗彩戶 +明窗淨几 明窗淨几 +明细表 明細表 +明者视于无形聪者听于无声 明者視於無形聰者聽於無聲 +明范 明範 +明见万里 明見萬里 +明里 明裏 +明鉴 明鑑 +明鉴万里 明鑑萬里 +明鉴秋毫 明鑑秋毫 +明鎗容易躲暗剑最难防 明鎗容易躲暗劍最難防 +明鎗容易躲暗箭最难防 明鎗容易躲暗箭最難防 +明鎗易躲暗箭难逃 明鎗易躲暗箭難逃 +明降暗升 明降暗升 +昏了 昏了 +昏困 昏困 +昏天暗地 昏天暗地 +昏头转向 昏頭轉向 +昏昏暗暗 昏昏暗暗 +昏昏欲睡 昏昏欲睡 +昏暗 昏暗 +昏沈 昏沈 +易于 易於 +易于反掌 易於反掌 +易克制 易剋制 +易卜拉辛 易卜拉辛 +易卜生 易卜生 +易发难收 易發難收 +易学启蒙 易學啓蒙 +易容术 易容術 +易游网 易遊網 +易熔合金 易熔合金 +星云 星雲 +星占学 星占學 +星历 星曆 +星历表 星曆錶 +星回 星迴 +星团 星團 +星巴克 星巴克 +星彩 星彩 +星彩正彩 星彩正彩 +星斗 星斗 +星期几 星期幾 +星期后 星期後 +星术 星術 +星点弹涂 星點彈塗 +星相术 星相術 +星移斗换 星移斗換 +星移斗转 星移斗轉 +星系 星系 +星罗云布 星羅雲佈 +星罗云散 星羅雲散 +星罗棋布 星羅棋佈 +星落云散 星落雲散 +星虫 星蟲 +星表 星表 +星象恶曜 星象惡曜 +星辰表 星辰錶 +星驰电发 星馳電發 +映入眼帘 映入眼簾 +映出 映出 +映照出 映照出 +映照出来 映照出來 +春假里 春假裏 +春兰秋菊 春蘭秋菊 +春华秋实 春華秋實 +春卷 春捲 +春卷皮 春捲皮 +春去秋来 春去秋來 +春台 春臺 +春回大地 春回大地 +春困 春困 +春困秋乏 春困秋乏 +春夏秋冬 春夏秋冬 +春天里 春天裏 +春宵一刻值千金 春宵一刻值千金 +春心荡漾 春心蕩漾 +春日里 春日裏 +春来秋去 春來秋去 +春树暮云 春樹暮雲 +春武里府 春武里府 +春游 春遊 +春生夏长秋收冬藏 春生夏長秋收冬藏 +春生秋杀 春生秋殺 +春祈秋报 春祈秋報 +春秋 春秋 +春秋三传 春秋三傳 +春秋五霸 春秋五霸 +春秋儿 春秋兒 +春秋几何 春秋幾何 +春秋大一统 春秋大一統 +春秋大梦 春秋大夢 +春秋左氏传 春秋左氏傳 +春秋战国 春秋戰國 +春秋战国时代 春秋戰國時代 +春秋日高 春秋日高 +春秋时代 春秋時代 +春秋榜 春秋榜 +春秋笔削 春秋筆削 +春秋笔法 春秋筆法 +春秋繁露 春秋繁露 +春秋配 春秋配 +春秋鼎盛 春秋鼎盛 +春笋怒发 春筍怒發 +春纤 春纖 +春胜 春勝 +春花秋月 春花秋月 +春药 春藥 +春蚓秋蛇 春蚓秋蛇 +春露秋霜 春露秋霜 +春风满面 春風滿面 +春风面 春風面 +春香斗学 春香鬥學 +昧于 昧於 +昧于事理 昧於事理 +昧谷 昧谷 +昨夜里 昨夜裏 +昨天夜里 昨天夜裏 +昭君出塞 昭君出塞 +昭回 昭回 +昭苏 昭蘇 +昭苏县 昭蘇縣 +是个 是個 +是只 是隻 +是念 是念 +是罐子也有两个耳朵 是罐子也有兩個耳朵 +是非只为多开口 是非只爲多開口 +是非善恶 是非善惡 +是非曲直 是非曲直 +是须 是須 +昴宿星团 昴宿星團 +昴星团 昴星團 +昼伏夜出 晝伏夜出 +昼伏夜游 晝伏夜游 +显出 顯出 +显微手术 顯微手術 +显摆 顯擺 +显现出 顯現出 +显现出来 顯現出來 +显着标志 顯着標志 +显示出 顯示出 +显示出来 顯示出來 +显示板 顯示板 +显示表 顯示錶 +显示钟 顯示鐘 +显示钟表 顯示鐘錶 +显著 顯著 +显著标志 顯著標志 +显露出 顯露出 +显露出来 顯露出來 +晃了 晃了 +晃晃荡荡 晃晃蕩蕩 +晃荡 晃盪 +晋升 晉升 +晋升为 晉升爲 +晋阳秋 晉陽秋 +晏几道 晏幾道 +晏子春秋 晏子春秋 +晒伤 曬傷 +晒台 曬臺 +晒图 曬圖 +晒图纸 曬圖紙 +晒干 曬乾 +晒成 曬成 +晒晒 曬曬 +晒烟 曬菸 +晒种 曬種 +晒衣 曬衣 +晒谷 曬穀 +晒谷场 曬穀場 +晒黑 曬黑 +晕台 暈臺 +晕头转向 暈頭轉向 +晕船药 暈船藥 +晕血症 暈血症 +晕车药 暈車藥 +晕针 暈針 +晚于 晚於 +晚参 晚參 +晚娘 晚娘 +晚娘的拳头云里的日头 晚娘的拳頭雲裏的日頭 +晚娘面孔 晚娘面孔 +晚期癌症 晚期癌症 +晚生后学 晚生後學 +晚秋 晚秋 +晚秋作物 晚秋作物 +晚秋时节 晚秋時節 +晚钟 晚鐘 +晚食当肉 晚食當肉 +晞发 晞髮 +晤面 晤面 +晦暗 晦暗 +晦蒙 晦蒙 +晨参暮省 晨參暮省 +晨参暮礼 晨參暮禮 +晨钟 晨鐘 +晨钟暮鼓 晨鐘暮鼓 +普冬冬 普鼕鼕 +普列谢茨克 普列謝茨克 +普列谢茨克卫星发射场 普列謝茨克衛星發射場 +普利艾托 普利艾托 +普勒托利亚 普勒托利亞 +普勒斯威克 普勒斯威克 +普同文化 普同文化 +普天同庆 普天同慶 +普奥同盟 普奧同盟 +普庵咒 普庵咒 +普庵老祖 普庵老祖 +普拉斯尼克 普拉斯尼克 +普拉玛娜苏达 普拉瑪娜蘇達 +普拉齐克 普拉齊克 +普普艺术 普普藝術 +普朗克 普朗克 +普朗克常数 普朗克常數 +普氏立克次体 普氏立克次體 +普罗扎克 普羅扎克 +普莱克斯 普萊克斯 +普里 普里 +普里什蒂纳 普裏什蒂納 +普里切特 普里切特 +普里斯特 普里斯特 +普里斯特莱 普里斯特萊 +普里斯莱 普里斯萊 +普里斯蒂纳 普里斯蒂納 +普里查德 普里查德 +普里霍吉可 普里霍吉可 +普隆克 普隆克 +普雷克斯流程 普雷克斯流程 +景云 景雲 +景从云合 景從雲合 +景从云集 景從雲集 +景星庆云 景星慶雲 +景胄 景胄 +景致 景緻 +景谷 景谷 +景谷傣族彝族自治县 景谷傣族彝族自治縣 +景谷县 景谷縣 +晴了 晴了 +晴云秋月 晴雲秋月 +晴时多云 晴時多雲 +晴空万里 晴空萬里 +晴转多云 晴轉多雲 +晴雨表 晴雨表 +晶体三极体 晶體三極體 +晶体三极管 晶體三極管 +晶体二极体 晶體二極體 +晶体二极管 晶體二極管 +晶体振荡 晶體振盪 +晶圆厂 晶圓廠 +晶核 晶核 +晶系 晶系 +晶面 晶面 +智周 智周 +智囊团 智囊團 +智尽能索 智盡能索 +智慧型车辆暨公路系统 智慧型車輛暨公路系統 +智慧板 智慧板 +智术 智術 +智者千虑必有一失 智者千慮必有一失 +晾干 晾乾 +暂于 暫於 +暂借 暫借 +暂别 暫別 +暂升 暫升 +暂并列 暫並列 +暂扣 暫扣 +暂染发慕丝 暫染髮慕絲 +暌合 暌合 +暑修 暑修 +暑修班 暑修班 +暑假里 暑假裏 +暖云兜 暖雲兜 +暖冬 暖冬 +暖厂 暖廠 +暖帘 暖簾 +暖气团 暖氣團 +暖种 暖種 +暖色系 暖色系 +暖荡撩锅 暖盪撩鍋 +暗下去 暗下去 +暗下来 暗下來 +暗中 暗中 +暗中作怪 暗中作怪 +暗中摸索 暗中摸索 +暗中监视 暗中監視 +暗中行事 暗中行事 +暗九 暗九 +暗乱 闇亂 +暗了 暗了 +暗了下来 暗了下來 +暗事 暗事 +暗井 暗井 +暗付 暗付 +暗伤 暗傷 +暗伦 闇倫 +暗光鸟 暗光鳥 +暗公鸟 暗公鳥 +暗冥 闇冥 +暗到 暗到 +暗劣 闇劣 +暗反应 暗反應 +暗叫 暗叫 +暗叫一声 暗叫一聲 +暗号 暗號 +暗号灯 暗號燈 +暗叹 暗歎 +暗吃一惊 暗吃一驚 +暗合 暗合 +暗含 暗含 +暗喜 暗喜 +暗喻 暗喻 +暗器 暗器 +暗地 暗地 +暗地里 暗地裏 +暗场 暗場 +暗堡 暗堡 +暗处 暗處 +暗娼 暗娼 +暗室 暗室 +暗室不欺 暗室不欺 +暗室亏心 暗室虧心 +暗室可欺 暗室可欺 +暗室私心 暗室私心 +暗室逢灯 暗室逢燈 +暗害 暗害 +暗察明访 暗察明訪 +暗射 暗射 +暗射地图 暗射地圖 +暗屋 暗屋 +暗巷 暗巷 +暗底下 暗底下 +暗度陈仓 暗度陳倉 +暗弱 闇弱 +暗影 暗影 +暗得 暗得 +暗忖 暗忖 +暗念 暗念 +暗恋 暗戀 +暗想 暗想 +暗房 暗房 +暗扣 暗釦 +暗指 暗指 +暗探 暗探 +暗操贱业 暗操賤業 +暗斗 暗鬥 +暗无天日 暗無天日 +暗昧 闇昧 +暗暗 暗暗 +暗暗淡淡 暗暗淡淡 +暗杀 暗殺 +暗杀党 暗殺黨 +暗杀案 暗殺案 +暗杠 暗槓 +暗格 暗格 +暗桩 暗樁 +暗楼子 暗樓子 +暗槓 暗槓 +暗沈沈 暗沈沈 +暗沉 暗沉 +暗沟 暗溝 +暗沟里 暗溝裏 +暗河 暗河 +暗泣 暗泣 +暗流 暗流 +暗浅 闇淺 +暗涌 暗涌 +暗淡 暗淡 +暗淡无光 暗淡無光 +暗渠 暗渠 +暗渡陈仓 暗渡陳倉 +暗滩 暗灘 +暗潮 暗潮 +暗潮汹涌 暗潮洶涌 +暗澹 暗澹 +暗火 闇火 +暗灰色 暗灰色 +暗然 闇然 +暗然失色 暗然失色 +暗电流 暗電流 +暗疾 暗疾 +暗的 暗的 +暗盒 暗盒 +暗盘 暗盤 +暗盼 暗盼 +暗码 暗碼 +暗礁 暗礁 +暗示 暗示 +暗移 暗移 +暗笑 暗笑 +暗笑一声 暗笑一聲 +暗算 暗算 +暗管 暗管 +暗箭 暗箭 +暗箭中人 暗箭中人 +暗箭伤人 暗箭傷人 +暗箭明鎗 暗箭明鎗 +暗箭难防 暗箭難防 +暗箱 暗箱 +暗箱操作 暗箱操作 +暗紫色 暗紫色 +暗红 暗紅 +暗红色 暗紅色 +暗线 暗線 +暗线光谱 暗線光譜 +暗练 暗練 +暗结珠胎 暗結珠胎 +暗自 暗自 +暗自流泪 暗自流淚 +暗自神伤 暗自神傷 +暗自窃喜 暗自竊喜 +暗色 暗色 +暗色彩 暗色彩 +暗花儿 暗花兒 +暗莫 闇莫 +暗蓝发 闇藍髮 +暗蔼 暗藹 +暗藏 暗藏 +暗藏春色 暗藏春色 +暗藏玄机 暗藏玄機 +暗藏着 暗藏着 +暗藏鬼胎 暗藏鬼胎 +暗虚 暗虛 +暗行 暗行 +暗袋 暗袋 +暗褐 暗褐 +暗褐色 暗褐色 +暗计 暗計 +暗记 暗記 +暗记儿 暗記兒 +暗记在心 暗記在心 +暗讽 暗諷 +暗访 暗訪 +暗语 暗語 +暗诵 闇誦 +暗跳 闇跳 +暗转 暗轉 +暗边 暗邊 +暗送 暗送 +暗送秋波 暗送秋波 +暗适应 暗適應 +暗通 暗通 +暗通款曲 暗通款曲 +暗道 暗道 +暗部 暗部 +暗里 暗裏 +暗釦 暗釦 +暗锁 暗鎖 +暗门 暗門 +暗门子 暗門子 +暗问 暗問 +暗间儿 暗間兒 +暗顺应 暗順應 +暗香 暗香 +暗香疏影 暗香疏影 +暗骂 暗罵 +暗黑 暗黑 +暗黑王 暗黑王 +暗黝 暗黝 +暮云亲舍 暮雲親舍 +暮云春树 暮雲春樹 +暮烟 暮煙 +暮礼晨参 暮禮晨參 +暮秋 暮秋 +暮雨朝云 暮雨朝雲 +暮鼓晨钟 暮鼓晨鐘 +暴发 暴發 +暴发出来 暴發出來 +暴发户 暴發戶 +暴君焚城录 暴君焚城錄 +暴尸于市 暴屍於市 +暴扣 暴扣 +暴敛横征 暴斂橫徵 +暴晒 暴曬 +暴腌 暴醃 +暴露出 暴露出 +暴露出来 暴露出來 +暴面 暴面 +暴食症 暴食症 +曙后星孤 曙後星孤 +曝光表 曝光表 +曝晒 曝曬 +曝露出 曝露出 +曰云 曰云 +曲从 曲從 +曲体 曲體 +曲别针 曲別針 +曲匝 曲匝 +曲卷 曲捲 +曲台 曲臺 +曲名 曲名 +曲周 曲周 +曲周县 曲周縣 +曲园 曲園 +曲士 曲士 +曲媚取容 曲媚取容 +曲子 曲子 +曲学 曲學 +曲学诐行 曲學詖行 +曲学阿世 曲學阿世 +曲室 曲室 +曲宴 曲宴 +曲射炮 曲射炮 +曲尘 麴塵 +曲尺 曲尺 +曲尺楼梯 曲尺樓梯 +曲尽人情 曲盡人情 +曲尽其妙 曲盡其妙 +曲局 曲局 +曲希瑞 曲希瑞 +曲庇 曲庇 +曲度 曲度 +曲式 曲式 +曲张 曲張 +曲径 曲徑 +曲径通幽 曲徑通幽 +曲律 曲律 +曲心矫肚 曲心矯肚 +曲意 曲意 +曲意俯就 曲意俯就 +曲意奉承 曲意奉承 +曲意逢迎 曲意逢迎 +曲房 曲房 +曲折 曲折 +曲折离奇 曲折離奇 +曲折萦纡 曲折縈紆 +曲拐 曲拐 +曲拗 曲拗 +曲挠 曲撓 +曲曲 曲曲 +曲曲弯弯 曲曲彎彎 +曲曲折折 曲曲折折 +曲本 曲本 +曲松 曲松 +曲松县 曲松縣 +曲柄 曲柄 +曲柄笠 曲柄笠 +曲柄钻 曲柄鑽 +曲栏 曲欄 +曲棍 曲棍 +曲棍球 曲棍球 +曲棍球员 曲棍球員 +曲棍球赛 曲棍球賽 +曲水 曲水 +曲水县 曲水縣 +曲水流觞 曲水流觴 +曲江 曲江 +曲江区 曲江區 +曲江县 曲江縣 +曲江宴 曲江宴 +曲江池 曲江池 +曲池穴 曲池穴 +曲沃 曲沃 +曲沃县 曲沃縣 +曲流 曲流 +曲牌 曲牌 +曲率 曲率 +曲率向量 曲率向量 +曲球 曲球 +曲琼 曲瓊 +曲生 麴生 +曲盖 曲蓋 +曲目 曲目 +曲直 曲直 +曲直分明 曲直分明 +曲礼 曲禮 +曲秀才 麴秀才 +曲突徙薪 曲突徙薪 +曲笔 曲筆 +曲笛 曲笛 +曲线 曲線 +曲线图 曲線圖 +曲线拟合 曲線擬合 +曲线板 曲線板 +曲线美 曲線美 +曲线论 曲線論 +曲线运动 曲線運動 +曲终人散 曲終人散 +曲终奏雅 曲終奏雅 +曲绕 曲繞 +曲肱为枕 曲肱爲枕 +曲肱之乐 曲肱之樂 +曲肱而枕 曲肱而枕 +曲背 曲背 +曲膝礼 曲膝禮 +曲臂 曲臂 +曲致 曲致 +曲艺 曲藝 +曲艾玲 曲艾玲 +曲菌 麴菌 +曲薄 曲薄 +曲蘖 麴櫱 +曲蟮 曲蟮 +曲裾 曲裾 +曲解 曲解 +曲话 曲話 +曲说 曲說 +曲调 曲調 +曲谨 曲謹 +曲谱 曲譜 +曲赦 曲赦 +曲躬躬 曲躬躬 +曲车 麴車 +曲轴 曲軸 +曲辫子 曲辮子 +曲道 曲道 +曲道士 麴道士 +曲道赛 曲道賽 +曲酒 麴酒 +曲里拐弯 曲裏拐彎 +曲钱 麴錢 +曲阜 曲阜 +曲阜县 曲阜縣 +曲阜孔庙 曲阜孔廟 +曲阜市 曲阜市 +曲阳 曲陽 +曲阳县 曲陽縣 +曲阻 曲阻 +曲院 麴院 +曲隐 曲隱 +曲霉 麴黴 +曲霉毒素 麴黴毒素 +曲靖 曲靖 +曲靖地区 曲靖地區 +曲靖市 曲靖市 +曲面 曲面 +曲面论 曲面論 +曲项目 曲項目 +曲颈瓶 曲頸瓶 +曲颈甑 曲頸甑 +曲风 曲風 +曲高和寡 曲高和寡 +曲麻莱 曲麻萊 +曲麻莱县 曲麻萊縣 +曳尾泥涂 曳尾泥塗 +曳尾涂中 曳尾塗中 +更仆难数 更僕難數 +更出 更出 +更加规范 更加規範 +更动表 更動表 +更向 更向 +更复 更復 +更大范围 更大範圍 +更当 更當 +更待干罢 更待干罷 +更愿 更願 +更愿意 更願意 +更甚于 更甚於 +更签 更籤 +更胜一筹 更勝一籌 +更钟 更鐘 +更须 更須 +更鼓敲尽 更鼓敲盡 +曷极 曷極 +曹余章 曹餘章 +曹参 曹參 +曹郁芬 曹郁芬 +曼尼托巴省 曼尼托巴省 +曼苏尔 曼蘇爾 +曼苏尔.达杜拉 曼蘇爾.達杜拉 +曼谷 曼谷 +曼谷人 曼谷人 +曼谷包 曼谷包 +曼谷市 曼谷市 +曼谷邮报 曼谷郵報 +曾于 曾於 +曾几何时 曾幾何時 +曾参 曾參 +曾参杀人 曾參殺人 +曾台霖 曾臺霖 +曾孝谷 曾孝谷 +曾志伟 曾志偉 +曾志朗 曾志朗 +曾朴 曾樸 +曾母暗沙 曾母暗沙 +曾砚闲 曾硯閒 +最上面 最上面 +最下面 最下面 +最低价 最低價 +最低谷 最低谷 +最初几天 最初幾天 +最初几年 最初幾年 +最前面 最前面 +最后 最後 +最后一天 最後一天 +最后审判 最後審判 +最后晚餐 最後晚餐 +最后更新 最後更新 +最后期限 最後期限 +最后的审判 最後的審判 +最后的晚餐 最後的晚餐 +最后胜利 最後勝利 +最后通牒 最後通牒 +最后面 最後面 +最大范围 最大範圍 +最小范围 最小範圍 +最近几天 最近幾天 +最近几年 最近幾年 +最适化 最適化 +最适合 最適合 +最里面 最裏面 +最须 最須 +最高当局 最高當局 +會干擾 會干擾 +月中折桂 月中折桂 +月丽于箕 月麗於箕 +月余 月餘 +月光曲 月光曲 +月入数万 月入數萬 +月出 月出 +月历 月曆 +月台 月臺 +月台票 月臺票 +月团 月團 +月地云阶 月地雲階 +月坠花折 月墜花折 +月娘 月娘 +月宫娘娘 月宮娘娘 +月岩 月岩 +月御 月御 +月核 月核 +月桂叶 月桂葉 +月桂树叶 月桂樹葉 +月牙铲 月牙鏟 +月球表面 月球表面 +月相表 月相錶 +月离于毕 月離於畢 +月经规则术 月經規則術 +月色迷蒙 月色迷濛 +月落参横 月落參橫 +月里嫦娥 月裏嫦娥 +月锻季炼 月鍛季煉 +月面 月面 +有一分心尽一分力 有一分心盡一分力 +有个 有個 +有个人 有個人 +有个地洞钻了下去 有個地洞鑽了下去 +有个好歹 有個好歹 +有个屁用 有個屁用 +有个数儿 有個數兒 +有个说儿 有個說兒 +有了 有了 +有了人家 有了人家 +有了存孝不显彦章 有了存孝不顯彥章 +有了胎 有了胎 +有事之无范 有事之無範 +有云 有云 +有仆 有僕 +有仇 有仇 +有仇不报非君子 有仇不報非君子 +有仇必报 有仇必報 +有价 有價 +有价值 有價值 +有价証券 有價証券 +有价证券 有價證券 +有何面目 有何面目 +有余 有餘 +有佛出世 有佛出世 +有准 有準 +有凭有据 有憑有據 +有出入 有出入 +有出好戏 有齣好戲 +有出息 有出息 +有利于 有利於 +有别 有別 +有别于 有別於 +有刮划没是处 有刮劃沒是處 +有力出力 有力出力 +有助于 有助於 +有助于记忆 有助於記憶 +有勾当 有勾當 +有去无回 有去無回 +有发头陀寺 有髮頭陀寺 +有只 有隻 +有同情心 有同情心 +有后 有後 +有周 有周 +有够赞 有夠讚 +有奖征答 有獎徵答 +有女同车 有女同車 +有子万事足 有子萬事足 +有孔虫 有孔蟲 +有宝何必人前夸 有寶何必人前誇 +有害于 有害於 +有尽有让 有儘有讓 +有幸 有幸 +有当 有當 +有征 有徵 +有征无战 有征無戰 +有志 有志 +有志一同 有志一同 +有志之士 有志之士 +有志于 有志於 +有志气 有志氣 +有志竟成 有志竟成 +有志者事竟成 有志者事竟成 +有志难酬 有志難酬 +有恒街 有恆街 +有意志 有意志 +有意栽花花不发无心插柳柳成阴 有意栽花花不發無心插柳柳成陰 +有感于 有感於 +有感而云 有感而云 +有感而发 有感而發 +有所不同 有所不同 +有才 有才 +有才干 有才幹 +有才无命 有才無命 +有把傢伙 有把傢伙 +有损于 有損於 +有效范围 有效範圍 +有效面积 有效面積 +有晚娘就有晚爷 有晚娘就有晚爺 +有机化合 有機化合 +有机化合物 有機化合物 +有机合成 有機合成 +有板有眼 有板有眼 +有极 有極 +有枝有叶 有枝有葉 +有枝添叶 有枝添葉 +有栖川 有棲川 +有核 有核 +有棱有角 有棱有角 +有求于人 有求於人 +有烟煤 有煙煤 +有益于 有益於 +有福同享 有福同享 +有福同享有祸同当 有福同享有禍同當 +有福同享有难同当 有福同享有難同當 +有秋 有秋 +有种 有種 +有种人 有種人 +有系统 有系統 +有缘千里来相会 有緣千里來相會 +有缘千里来相会无缘对面不相逢 有緣千里來相會無緣對面不相逢 +有联系 有聯繫 +有脸面 有臉面 +有致 有致 +有致癌 有致癌 +有色人种 有色人種 +有节制 有節制 +有苦说不出 有苦說不出 +有药瘾者 有藥癮者 +有被种子植物纲 有被種子植物綱 +有计划 有計劃 +有赖于 有賴於 +有进有出 有進有出 +有采 有采 +有鉴 有鑑 +有鉴于 有鑑於 +有鉴于此 有鑑於此 +有钩绦虫 有鉤絛蟲 +有钱不买半年闲 有錢不買半年閒 +有钱出钱 有錢出錢 +有钱有闲 有錢有閒 +有难同当 有難同當 +有面 有面 +有面儿 有面兒 +朋克 朋克 +朋党 朋黨 +朋党为奸 朋黨爲奸 +朋党之争 朋黨之爭 +朋党比周 朋黨比周 +朋党论 朋黨論 +朋奸 朋奸 +朋比为奸 朋比爲奸 +朋比作仇 朋比作仇 +朋比作奸 朋比作奸 +服了 服了 +服于 服於 +服从于 服從於 +服制 服制 +服务于 服務於 +服务台 服務檯 +服务周到 服務周到 +服务团 服務團 +服务范围 服務範圍 +服务规范 服務規範 +服御 服御 +服毒自尽 服毒自盡 +服用药 服用藥 +服舍 服舍 +服药 服藥 +服药过量 服藥過量 +服装周 服裝週 +服饰周 服飾週 +朔云 朔雲 +朔党 朔黨 +朗朗云天 朗朗雲天 +望乡台 望鄉臺 +望了望 望了望 +望云 望雲 +望云之情 望雲之情 +望出去 望出去 +望后石 望后石 +望向 望向 +望幸 望幸 +望弥撒 望彌撒 +望楼台 望樓臺 +望眼欲穿 望眼欲穿 +望秋先零 望秋先零 +望穿秋水 望穿秋水 +望胄 望胄 +朝东面 朝東面 +朝乾夕惕 朝乾夕惕 +朝云 朝雲 +朝云暮雨 朝雲暮雨 +朝出夕改 朝出夕改 +朝北面 朝北面 +朝升暮合 朝升暮合 +朝南面 朝南面 +朝参暮礼 朝參暮禮 +朝发夕至 朝發夕至 +朝后 朝後 +朝向 朝向 +朝圣团 朝聖團 +朝核 朝核 +朝梁暮晋 朝梁暮晉 +朝梁暮陈 朝梁暮陳 +朝西面 朝西面 +朝野两党 朝野兩黨 +朝钟 朝鐘 +朝钟暮鼓 朝鐘暮鼓 +朝鲜冷面 朝鮮冷麪 +朝鲜劳动党 朝鮮勞動黨 +期货价 期貨價 +朦在鼓里 朦在鼓裏 +朦胧 朦朧 +木偶戏扎 木偶戲紮 +木制 木製 +木制品 木製品 +木厂 木廠 +木叶 木葉 +木垒哈萨克自治县 木壘哈薩克自治縣 +木强则折 木強則折 +木心板 木心板 +木朽不雕 木朽不雕 +木杆 木杆 +木材干馏 木材乾餾 +木杠 木槓 +木板 木板 +木板凳 木板凳 +木板地 木板地 +木板墙 木板牆 +木板大鼓 木板大鼓 +木板牀 木板牀 +木板画 木板畫 +木柜 木櫃 +木梁 木樑 +木芍药 木芍藥 +木薯淀粉 木薯澱粉 +木蜡 木蠟 +木表法 木表法 +木里藏族自治县 木裏藏族自治縣 +木钟 木鐘 +木铲 木鏟 +木雕 木雕 +木雕像 木雕像 +木雕家 木雕家 +木雕师 木雕師 +木雕泥塑 木雕泥塑 +木雕节 木雕節 +未了 未了 +未了公案 未了公案 +未了因 未了因 +未了情 未了情 +未了缘 未了緣 +未几 未幾 +未出 未出 +未出去 未出去 +未出来 未出來 +未出货 未出貨 +未卜 未卜 +未卜先知 未卜先知 +未发货 未發貨 +未可同日而语 未可同日而語 +未嫁新娘 未嫁新娘 +未尝 未嘗 +未尝不可 未嘗不可 +未尽 未盡 +未尽事宜 未盡事宜 +未干 未乾 +未开发国家 未開發國家 +未必尽然 未必盡然 +未扣 未扣 +未折现 未折現 +未易才 未易才 +未松下 未松下 +未知万一 未知萬一 +未确定 未確定 +未竟之志 未竟之志 +未签字者 未簽字者 +未获 未獲 +未获得 未獲得 +未降反升 未降反升 +末了 末了 +末叶 末葉 +末后 末後 +末大必折 末大必折 +末娘 末娘 +末末了 末末了 +末胄 末胄 +末药 末藥 +本价 本價 +本位制 本位制 +本党 本黨 +本厂 本廠 +本古里昂 本古里昂 +本台 本臺 +本台消息 本臺消息 +本同末异 本同末異 +本周 本週 +本周一 本週一 +本周三 本週三 +本周二 本週二 +本周五 本週五 +本周六 本週六 +本周四 本週四 +本回 本回 +本团 本團 +本地管理界面 本地管理界面 +本垒板 本壘板 +本当 本當 +本征 本徵 +本征值 本徵值 +本征向量 本徵向量 +本志 本志 +本愿 本願 +本所同仁 本所同仁 +本报台 本報臺 +本期发生 本期發生 +本期发生额 本期發生額 +本机振荡 本機振盪 +本来面目 本來面目 +本杰明 本傑明 +本杰明富兰克林 本傑明富蘭克林 +本科系 本科系 +本系 本系 本係 +本色当行 本色當行 +本草药学 本草藥學 +本里 本里 +札什伦布寺 札什倫布寺 +札夸威 札夸威 +札板儿 札板兒 +札格拉布 札格拉布 +札格瑞布 札格瑞布 +术业有专攻 術業有專攻 +术前 術前 +术后 術後 +术士 術士 +术德兼修 術德兼修 +术数 術數 +术科 術科 +术艺 術藝 +术语 術語 +术赤 朮赤 +朱一贵 朱一貴 +朱丝栏抄本 朱絲欄抄本 +朱丹 朱丹 +朱丽亚 朱麗亞 +朱丽叶 朱麗葉 +朱书麟 朱書麟 +朱买臣 朱買臣 +朱云折槛 朱雲折檻 +朱云鹏 朱雲鵬 +朱亥 朱亥 +朱仑街 朱崙街 +朱仙镇 朱仙鎮 +朱俊 朱儁 +朱俊彰 朱俊彰 +朱俊晓 朱俊曉 +朱允炆 朱允炆 +朱元璋 朱元璋 +朱元祥 朱元祥 +朱先 朱先 +朱光 朱光 +朱全忠 朱全忠 +朱兴荣 朱興榮 +朱凤芝 朱鳳芝 +朱利亚尼 朱利亞尼 +朱利娅 朱利婭 +朱利安 朱利安 +朱利安尼 朱利安尼 +朱华 朱華 +朱印本 朱印本 +朱卷 硃卷 +朱口皓齿 朱口皓齒 +朱古力 朱古力 +朱古力糖 朱古力糖 +朱古律糖 朱古律糖 +朱咏薇 朱詠薇 +朱哲琴 朱哲琴 +朱唇 朱脣 +朱唇皓齿 硃脣皓齒 +朱培庆 朱培慶 +朱墨 朱墨 +朱墨本 朱墨本 +朱墨烂然 朱墨爛然 +朱士行 朱士行 +朱天文 朱天文 +朱姓 朱姓 +朱婉琪 朱婉琪 +朱子 朱子 +朱子学 朱子學 +朱子家训 朱子家訓 +朱子治家格言 朱子治家格言 +朱子语录 朱子語錄 +朱子语类 朱子語類 +朱孝天 朱孝天 +朱学 朱學 +朱宏瑜 朱宏瑜 +朱宗庆 朱宗慶 +朱实 朱實 +朱家 朱家 +朱家兴 朱家興 +朱家彦 朱家彥 +朱家欣 朱家欣 +朱家鼎 朱家鼎 +朱容基 朱容基 +朱尉铭 朱尉銘 +朱尔典 朱爾典 +朱尔金 朱爾金 +朱尼奥 朱尼奧 +朱巴一 朱巴一 +朱希真 朱希真 +朱干玉戚 朱干玉鏚 +朱广沪 朱廣滬 +朱庆余 朱慶餘 +朱庇特 朱庇特 +朱延平 朱延平 +朱弁 朱弁 +朱弦 朱弦 +朱弦玉磬 朱弦玉磬 +朱德 朱德 +朱执信 朱執信 +朱批 硃批 +朱拉隆功 朱拉隆功 +朱提 朱提 +朱敦儒 朱敦儒 +朱敬一 朱敬一 +朱文 朱文 +朱文庆 朱文慶 +朱明 朱明 +朱智勳 朱智勳 +朱朝亮 朱朝亮 +朱木炎 朱木炎 +朱朱 朱朱 +朱权 朱權 +朱柏庐 朱柏廬 +朱标 朱標 +朱棣 朱棣 +朱槿 朱槿 +朱比特 朱比特 +朱永弘 朱永弘 +朱泽民 朱澤民 +朱洪武 朱洪武 +朱淑真 朱淑真 +朱温 朱溫 +朱漆 朱漆 +朱熔基 朱熔基 +朱熹 朱熹 +朱理安历 朱理安曆 +朱理安历史 朱理安歷史 +朱瑞 朱瑞 +朱瑞特兹 朱瑞特茲 +朱瓦碧甍 朱瓦碧甍 +朱甍碧瓦 朱甍碧瓦 +朱由校 朱由校 +朱瞻基 朱瞻基 +朱砂 硃砂 +朱砂痣 硃砂痣 +朱砂符 硃砂符 +朱砂红 硃砂紅 +朱祁钰 朱祁鈺 +朱祁镇 朱祁鎮 +朱祖谋 朱祖謀 +朱穆 朱穆 +朱立伦 朱立倫 +朱笔 硃筆 +朱筠 朱筠 +朱紫 朱紫 +朱紫难别 朱紫難別 +朱红 硃紅 +朱红灯 朱紅燈 +朱红色 硃紅色 +朱经武 朱經武 +朱美 朱美 +朱耷 朱耷 +朱脣 朱脣 +朱脣榴齿 朱脣榴齒 +朱脣皓齿 朱脣皓齒 +朱脣粉面 朱脣粉面 +朱自清 朱自清 +朱舜水 朱舜水 +朱色 硃色 +朱茵 朱茵 +朱莉娅 朱莉婭 +朱蒙 朱蒙 +朱蕉 朱蕉 +朱衣 朱衣 +朱衣吏 朱衣吏 +朱衣点头 朱衣點頭 +朱衣神 朱衣神 +朱记 朱記 +朱诺 朱諾 +朱谕 硃諭 +朱贝尔 朱貝爾 +朱轓皂盖 朱轓皂蓋 +朱轩 朱軒 +朱轮 朱輪 +朱轮华毂 朱輪華轂 +朱迪亚 朱迪亞 +朱邸 朱邸 +朱郁信 朱郁信 +朱铭 朱銘 +朱镕基 朱鎔基 +朱门 朱門 +朱门绣户 朱門繡戶 +朱阁 朱閣 +朱阁青楼 朱閣青樓 +朱阿英 朱阿英 +朱陆异同 朱陸異同 +朱陈 朱陳 +朱陈之好 朱陳之好 +朱雀 朱雀 +朱雀号 朱雀號 +朱雀桥 朱雀橋 +朱顶 朱頂 +朱颜 朱顏 +朱颜粉面 朱顏粉面 +朱颜鹤发 朱顏鶴髮 +朱马亚 朱馬亞 +朱骏声 朱駿聲 +朱高炽 朱高熾 +朱鸟 朱鳥 +朱鹭 朱鷺 +朱鹮 朱䴉 +朱黄 朱黃 +朴世莉 朴世莉 +朴京琳 朴京琳 +朴仔树 朴仔樹 +朴修斯 樸修斯 +朴克牌 樸克牌 +朴凤柱 朴鳳柱 +朴刀 朴刀 +朴厚 樸厚 +朴吉渊 朴吉淵 +朴周永 朴周永 +朴子 朴子 +朴子市 朴子市 +朴子溪 朴子溪 +朴学 樸學 +朴实 樸實 +朴实作风 樸實作風 +朴实无华 樸實無華 +朴宣英 朴宣英 +朴志胤 朴志胤 +朴忠 朴忠 +朴念仁 樸念仁 +朴恩惠 朴恩惠 +朴拙 樸拙 +朴新阳 朴新陽 +朴智星 朴智星 +朴树 朴樹 +朴槿惠 朴槿惠 +朴樕 樸樕 +朴次茅斯 樸次茅斯 +朴正恩 朴正恩 +朴正熙 朴正熙 +朴正祥 朴正祥 +朴永训 朴永訓 +朴泰桓 朴泰桓 +朴父 朴父 +朴璐美 朴璐美 +朴直 樸直 +朴真熙 朴真熙 +朴硝 朴硝 +朴素 樸素 +朴素无华 樸素無華 +朴茂 朴茂 +朴茨茅斯 朴茨茅斯 +朴茨茅斯队 朴茨茅斯隊 +朴讷 樸訥 +朴讷诚笃 樸訥誠篤 +朴诗妍 朴詩妍 +朴质 樸質 +朴质无华 樸質無華 +朴资茅斯 朴資茅斯 +朴资茅斯条约 朴資茅斯條約 +朴赞浩 朴贊浩 +朴鄙 樸鄙 +朴重 樸重 +朴野 樸野 +朴野无文 樸野無文 +朴钝 樸鈍 +朴陋 樸陋 +朴马 樸馬 +朴鲁 樸魯 +朵云 朵雲 +机修厂 機修廠 +机关团体 機關團體 +机关布景 機關佈景 +机关用尽 機關用盡 +机制 機制 機製 +机发 機發 +机台 機臺 +机器压制 機器壓製 +机壳厂 機殼廠 +机板 機板 +机柜 機櫃 +机械学系 機械學系 +机械系 機械系 +机械表 機械錶 +机械钟 機械鐘 +机械钟表 機械鐘錶 +机种 機種 +机绣 機繡 +机缘巧合 機緣巧合 +机辟 機辟 +朽木不雕 朽木不雕 +朽木之才 朽木之才 +朽木虫 朽木蟲 +朽棘不雕 朽棘不雕 +杀人须见血救人须救彻 殺人須見血救人須救徹 +杀价 殺價 +杀出 殺出 +杀出去 殺出去 +杀出来 殺出來 +杀出重围 殺出重圍 +杀千刀 殺千刀 +杀千刀的 殺千刀的 +杀才 殺才 +杀敌致果 殺敵致果 +杀虫 殺蟲 +杀虫剂 殺蟲劑 +杀虫器 殺蟲器 +杀虫药 殺蟲藥 +杀蠹药 殺蠹藥 +杀身出生 殺身出生 +杂交种 雜交種 +杂交育种 雜交育種 +杂合菜 雜合菜 +杂合面儿 雜合麪兒 +杂和面 雜和麪 +杂和面儿 雜和麪兒 +杂婚制 雜婚制 +杂当 雜當 +杂录 雜錄 +杂志 雜誌 +杂志奖 雜誌獎 +杂志社 雜誌社 +杂志纸 雜誌紙 +杂念 雜念 +杂技团 雜技團 +杂技表演 雜技表演 +杂板令 雜板令 +杂症 雜症 +杂种 雜種 +杂种人 雜種人 +杂种优势 雜種優勢 +杂种后代 雜種後代 +杂种狗 雜種狗 +杂谷脑 雜谷腦 +杂谷脑镇 雜谷腦鎮 +杂酱面 雜醬麪 +杂面 雜麪 +权利请愿书 權利請願書 +权制 權制 +权力斗争 權力鬥爭 +权力欲 權力慾 +权变锋出 權變鋒出 +权幸 權幸 +权术 權術 +权欲熏心 權慾薰心 +权能划分 權能劃分 +杆上 杆上 +杆儿 杆兒 桿兒 +杆刀 桿刀 +杆塔 杆塔 +杆子 杆子 桿子 +杆状 桿狀 +杆直 桿直 +杆秤 桿秤 +杆茵 桿茵 +杆菌 桿菌 +杆菌性 桿菌性 +杆菌类 桿菌類 +杆菌素 桿菌素 +杈杆儿 杈桿兒 +李万进 李萬進 +李三娘 李三娘 +李丰永 李豐永 +李云光 李雲光 +李云娜 李雲娜 +李代桃僵 李代桃僵 +李修贤 李修賢 +李克强 李克強 +李克昂 李克昂 +李克齐 李克齊 +李公朴 李公樸 +李准基 李準基 +李千娜 李千娜 +李卜克內西 李卜克內西 +李卜克内西 李卜克內西 +李叔同 李叔同 +李后主 李後主 +李咸阳 李咸陽 +李国修 李國修 +李圣杰 李聖傑 +李复兴 李復興 +李复甸 李復甸 +李大同 李大同 +李安修 李安修 +李家同 李家同 +李布瑞斯库 李布瑞斯庫 +李干龙 李乾龍 +李开复 李開復 +李志勳 李志勳 +李志宏 李志宏 +李志强 李志強 +李念 李念 +李斯特氏杆菌 李斯特氏桿菌 +李杰 李傑 +李洪志 李洪志 +李炳千 李炳千 +李百药 李百藥 +李盟干 李盟乾 +李秋静 李秋靜 +李胜琛 李勝琛 +李连杰 李連杰 +李連杰 李連杰 +李郭同舟 李郭同舟 +李鉴原 李鑑原 +李钟奭 李鐘奭 +李钟郁 李鍾郁 +李链福 李鍊福 +李锺郁 李鍾郁 +杏坛 杏壇 +杏干儿 杏乾兒 +杏核 杏核 +杏雨梨云 杏雨梨雲 +材干 材幹 +材料系 材料系 +材疏志大 材疏志大 +材种 材種 +材积表 材積表 +村子里 村子裏 +村干事 村幹事 +村庄 村莊 +村胄 村胄 +村舍 村舍 +村里 村裏 +村里长 村裏長 +杜丽娘 杜麗娘 +杜克 杜克 +杜寅杰 杜寅傑 +杜尔伯特蒙古族自治县 杜爾伯特蒙古族自治縣 +杜尚别 杜尚別 +杜尼克 杜尼克 +杜布林斯基 杜布林斯基 +杜布罗夫尼克 杜布羅夫尼克 +杜斯特布拉吉 杜斯特布拉吉 +杜杰利 杜傑利 +杜杰尔 杜傑爾 +杜松子酒 杜松子酒 +杜正胜 杜正勝 +杜秋娘 杜秋娘 +杜秋娘歌 杜秋娘歌 +杜绝后患 杜絕後患 +杜老志道 杜老誌道 +杜门不出 杜門不出 +杜雅里克 杜雅里克 +杞宋无征 杞宋無徵 +杞梁 杞梁 +杞梁妻 杞梁妻 +束修 束脩 +束发 束髮 +束发封帛 束髮封帛 +束发金冠 束髮金冠 +束矢难折 束矢難折 +束身修行 束身修行 +束身自修 束身自修 +杠上 槓上 +杠头 槓頭 +杠子 槓子 +杠杆 槓桿 +杠杆收购 杠杆收購 +杠杠 槓槓 +杠牌 槓牌 +杠着 槓着 +杠起 槓起 +杠铃 槓鈴 +杠龟 槓龜 +条几 條几 +条干 條幹 +条据 條據 +条板箱 條板箱 +条纹布 條紋布 +来个 來個 +来了 來了 +来于 來於 +来千去万 來千去萬 +来历 來歷 +来历不明 來歷不明 +来发 來發 +来台 來臺 +来台访问 來臺訪問 +来叶 來葉 +来回 來回 +来回来去 來回來去 +来回来去地 來回來去地 +来回的话 來回的話 +来回票 來回票 +来复 來複 +来复日 來復日 +来复枪 來複槍 +来复线 來複線 +来宾致词 來賓致詞 +来念 來念 +来来回回 來來回回 +来杯 來杯 +来自于 來自於 +来苏 來蘇 +来苏水 來蘇水 +来苏糖 來蘇糖 +杨万发 楊萬發 +杨万里 楊萬里 +杨云龙 楊雲龍 +杨俊胜 楊俊勝 +杨修 楊修 +杨凌示范区 楊淩示範區 +杨千霈 楊千霈 +杨叶窜儿 楊葉竄兒 +杨士梁 楊士樑 +杨宏志 楊宏志 +杨志卖刀 楊志賣刀 +杨文志 楊文誌 +杨日松 楊日松 +杨朱 楊朱 +杨松 楊松 +杨松弦 楊松弦 +杨氏系数 楊氏係數 +杨秋兴 楊秋興 +杨胜帆 楊勝帆 +杨胜旭 楊勝旭 +杨致远 楊致遠 +杨苏棣 楊甦棣 +杨采妮 楊採妮 +杨雅筑 楊雅筑 +杪秋 杪秋 +杭丁顿舞蹈症 杭丁頓舞蹈症 +杭州师范学院 杭州師範學院 +杭州萝卜绍兴种 杭州蘿蔔紹興種 +杭锦后旗 杭錦後旗 +杯上 杯上 +杯中 杯中 +杯中之物 杯中之物 +杯中物 杯中物 +杯了 杯了 +杯具 杯具 +杯内 杯內 +杯前 杯前 +杯口 杯口 +杯台 杯臺 +杯后 杯後 +杯和 杯和 +杯垫 杯墊 +杯壁 杯壁 +杯套 杯套 +杯子 杯子 +杯布 杯佈 +杯干 杯乾 +杯底 杯底 +杯底不可饲金鱼 杯底不可飼金魚 +杯座 杯座 +杯弓蛇影 杯弓蛇影 +杯换 杯換 +杯是 杯是 +杯杯 杯杯 +杯水 杯水 +杯水之谢 杯水之謝 +杯水粒粟 杯水粒粟 +杯水车薪 杯水車薪 +杯沿 杯沿 +杯状 杯狀 +杯白干 杯白乾 +杯的 杯的 +杯盏 杯盞 +杯盖 杯蓋 +杯盘 杯盤 +杯盘狼藉 杯盤狼藉 +杯盘舞 杯盤舞 +杯碟 杯碟 +杯突 杯突 +杯羹 杯羹 +杯茶 杯茶 +杯葛 杯葛 +杯葛运动 杯葛運動 +杯蛇鬼车 杯蛇鬼車 +杯装 杯裝 +杯觥交错 杯觥交錯 +杯赛 盃賽 +杯身 杯身 +杯酒 杯酒 +杯酒戈矛 杯酒戈矛 +杯酒解怨 杯酒解怨 +杯酒言欢 杯酒言歡 +杯酒释兵权 杯酒釋兵權 +杯里 杯里 +杯面 杯麪 +杰乐米 傑樂米 +杰伊汉港 傑伊漢港 +杰伦 杰倫 +杰佛兹 傑佛茲 +杰佛利 傑佛利 +杰佛士 傑佛士 +杰佛森 傑佛森 +杰佛瑞 傑佛瑞 +杰佛逊 傑佛遜 +杰作 傑作 +杰克 傑克 +杰克伦敦 傑克倫敦 +杰克尼克逊 傑克尼克遜 +杰克德米 傑克德米 +杰克森 傑克森 +杰克盖伦海 傑克蓋倫海 +杰克葛伦霍 傑克葛倫霍 +杰克逊 傑克遜 +杰出 傑出 +杰利 傑利 +杰卡伯 傑卡伯 +杰哈德巴特勒 傑哈德巴特勒 +杰哈德巴狄厄 傑哈德巴狄厄 +杰士派 傑士派 +杰夫 傑夫 +杰奎琳 傑奎琳 +杰奎琳肯尼迪 傑奎琳肯尼迪 +杰尔 傑爾 +杰尔村 傑爾村 +杰尼斯 傑尼斯 +杰弗逊 傑弗遜 +杰弗里乔叟 傑弗里喬叟 +杰拉 傑拉 +杰拉尔德 傑拉爾德 +杰拉德 傑拉德 +杰斯 傑斯 +杰杰 傑傑 +杰森 傑森 +杰森包恩 傑森包恩 +杰森史塔森 傑森史塔森 +杰特 杰特 +杰瑞 傑瑞 +杰福斯 傑福斯 +杰福瑞 傑福瑞 +杰米 傑米 +杰米森 傑米森 +杰米福克斯 傑米福克斯 +杰西 傑西 +杰西卡 傑西卡 +杰西卡艾尔芭 傑西卡艾爾芭 +杰西詹姆斯 傑西詹姆斯 +杰迪卓 傑迪卓 +杰里森 傑里森 +杰里科 傑里科 +杰里米 傑里米 +杳无人烟 杳無人煙 +松一下 鬆一下 +松一些 鬆一些 +松一口气 鬆一口氣 +松一松 鬆一鬆 +松下 松下 +松下一口 鬆下一口 +松下了 鬆下了 +松下公司 松下公司 +松下来 鬆下來 +松下电器 松下電器 +松下电机 松下電機 +松下电气工业 松下電氣工業 +松不开 鬆不開 +松不松 鬆不鬆 +松不紧 鬆不緊 +松丘 松丘 +松乔 松喬 +松乔之寿 松喬之壽 +松了 鬆了 +松了一口气 鬆了一口氣 +松了松 鬆了鬆 +松井秀 松井秀 +松井秀喜 松井秀喜 +松些 鬆些 +松仁 松仁 +松元音 鬆元音 +松冈 松岡 +松冈利胜 松岡利勝 +松出一 鬆出一 +松出口 鬆出口 +松动 鬆動 +松劲 鬆勁 +松化石 松化石 +松北 松北 +松北区 松北區 +松原 松原 +松原市 松原市 +松口 鬆口 +松口气 鬆口氣 +松口蘑 松口蘑 +松叶 松葉 +松叶牡丹 松葉牡丹 +松叶蕨 松葉蕨 +松喉 鬆喉 +松土 鬆土 +松土机 鬆土機 +松垮 鬆垮 +松大辅 松大輔 +松子 松子 +松宽 鬆寬 +松尾芭蕉 松尾芭蕉 +松山 松山 +松山区 松山區 +松山庄 松山莊 +松山机场 松山機場 +松岭 松嶺 +松岭区 松嶺區 +松巴哇 松巴哇 +松巴哇岛 松巴哇島 +松开 鬆開 +松弛 鬆弛 +松弛下来 鬆弛下來 +松弛到 鬆弛到 +松弛剂 鬆弛劑 +松弛法 鬆弛法 +松得多 鬆得多 +松快 鬆快 +松懈 鬆懈 +松懈下 鬆懈下 +松懈下来 鬆懈下來 +松手 鬆手 +松扣 鬆釦 +松指部 松指部 +松掉 鬆掉 +松放 鬆放 +松散 鬆散 +松散物料 鬆散物料 +松明 松明 +松智路 松智路 +松木 松木 +松本 松本 +松本仞 松本仞 +松本润 松本潤 +松本秀树 松本秀樹 +松材线虫 松材線蟲 +松松 鬆鬆 +松松垮垮 鬆鬆垮垮 +松松散散 鬆鬆散散 +松松脆脆 鬆鬆脆脆 +松松软软 鬆鬆軟軟 +松林 松林 +松果 松果 +松果体 松果體 +松果腺 松果腺 +松枝 松枝 +松柏 松柏 +松柏之坚 松柏之堅 +松柏之寿 松柏之壽 +松柏之茂 松柏之茂 +松柏后凋 松柏後凋 +松柏园 松柏園 +松柏节操 松柏節操 +松柏长青 松柏長青 +松柏长青茶 松柏長青茶 +松柔 鬆柔 +松树 松樹 +松桃县 松桃縣 +松桃苗族自治县 松桃苗族自治縣 +松毛 松毛 +松毛松翼 鬆毛鬆翼 +松毛虫 松毛蟲 +松毬 松毬 +松气 鬆氣 +松江 松江 +松江区 松江區 +松江省 松江省 +松江路 松江路 +松油 松油 +松油管 鬆油管 +松油门 鬆油門 +松浦 松浦 +松浦亚 松浦亞 +松浮 鬆浮 +松涛 松濤 +松溪 松溪 +松溪县 松溪縣 +松滋 鬆滋 +松滋市 鬆滋市 +松漠 松漠 +松潘 松潘 +松潘县 松潘縣 +松烟 松煙 +松烟墨 松煙墨 +松焦油 松焦油 +松煤 松煤 +松狮犬 松獅犬 +松球 松球 +松球鱼 松球魚 +松瓤 松瓤 +松田 松田 +松田圣子 松田聖子 +松皮癣 松皮癬 +松石 松石 +松科 松科 +松竹 松竹 +松竹梅 松竹梅 +松竹路 松竹路 +松筠之操 松筠之操 +松筠之节 松筠之節 +松篁交翠 松篁交翠 +松类 松類 +松糕 鬆糕 +松紧 鬆緊 +松紧带 鬆緊帶 +松纹 松紋 +松绑 鬆綁 +松缓 鬆緩 +松胶 松膠 +松脂 松脂 +松脂石 松脂石 +松脆 鬆脆 +松脆饼 鬆脆餅 +松脱 鬆脫 +松节油 松節油 +松节油精 松節油精 +松花 松花 +松花江 松花江 +松花江平原 松花江平原 +松花砚 松花硯 +松花纸 松花紙 +松花蛋 松花蛋 +松花饼 松花餅 +松茸 松茸 +松菌 松菌 +松萝 松蘿 +松萝共倚 松蘿共倚 +松蕈 松蕈 +松蕊 松蕊 +松藻虫 松藻蟲 +松蘑 松蘑 +松蛋 鬆蛋 +松蛋包 鬆蛋包 +松解 鬆解 +松赞干布 松贊干布 +松赞干布陵 松贊干布陵 +松起 鬆起 +松起来 鬆起來 +松软 鬆軟 +松软适口 鬆軟適口 +松辽平原 松遼平原 +松通 鬆通 +松针 松針 +松阳 松陽 +松阳县 松陽縣 +松隆子 松隆子 +松雪泰子 松雪泰子 +松露 松露 +松露猪 松露豬 +松青 松青 +松风 松風 +松风水月 松風水月 +松风流 松風流 +松饼 鬆餅 +松香 松香 +松香水 松香水 +松香油 松香油 +松高路 松高路 +松鱼 松魚 +松鸡 松雞 +松鸦 松鴉 +松鹤 松鶴 +松鹤延年 松鶴延年 +松鹤遐龄 松鶴遐齡 +松鼠 松鼠 +松鼠猴 松鼠猴 +板上钉钉 板上釘釘 +板书 板書 +板儿 板兒 +板凳 板凳 +板凳区 板凳區 +板刷 板刷 +板块 板塊 +板块构造 板塊構造 +板块理论 板塊理論 +板壁 板壁 +板子 板子 +板岩 板岩 +板式 板式 +板式塔 板式塔 +板房 板房 +板擦 板擦 +板擦儿 板擦兒 +板斧 板斧 +板本 板本 +板机 板機 +板材 板材 +板条 板條 +板条箱 板條箱 +板板 闆闆 +板板六十四 板板六十四 +板极 板極 +板栗 板栗 +板桥 板橋 +板桥中学 板橋中學 +板桥国中 板橋國中 +板桥国小 板橋國小 +板桥市 板橋市 +板桩 板樁 +板梁桥 板梁橋 +板油 板油 +板滞 板滯 +板烟 板煙 +板牙 板牙 +板状 板狀 +板球 板球 +板画 板畫 +板皮 板皮 +板眼 板眼 +板着脸 板着臉 +板纸 板紙 +板结 板結 +板羽球 板羽球 +板胡 板胡 +板脸 板臉 +板荡 板蕩 +板蓝根 板藍根 +板规 板規 +板起 板起 +板起面孔 板起面孔 +板车 板車 +板金 板金 +板金工 板金工 +板门店 板門店 +板门店停战村 板門店停戰村 +板鸭 板鴨 +板鼓 板鼓 +极不 極不 +极为庞大 極爲龐大 +极乐 極樂 +极乐世界 極樂世界 +极乐舞 極樂舞 +极乐鸟 極樂鳥 +极了 極了 +极低 極低 +极低频辐射 極低頻輻射 +极佳 極佳 +极便当 極便當 +极值 極值 +极光 極光 +极其 極其 +极具 極具 +极冠 極冠 +极切瞻韩 極切瞻韓 +极刑 極刑 +极力 極力 +极力争取 極力爭取 +极化 極化 +极口 極口 +极右 極右 +极右份子 極右份子 +极右派 極右派 +极右翼 極右翼 +极品 極品 +极圈 極圈 +极地 極地 +极地年 極地年 +极地气候 極地氣候 +极地狐 極地狐 +极坐标 極座標 +极坐标系 極座標系 +极处 極處 +极大 極大 +极大值 極大值 +极大期 極大期 +极头麻化 極頭麻化 +极好 極好 +极婺联辉 極婺聯輝 +极富 極富 +极小 極小 +极小值 極小值 +极少 極少 +极少数 極少數 +极尽 極盡 +极左 極左 +极左份子 極左份子 +极左派 極左派 +极带 極帶 +极带地区 極帶地區 +极度 極度 +极微 極微 +极性 極性 +极性键 極性鍵 +极恶 極惡 +极愿 極願 +极早 極早 +极有可能 極有可能 +极权 極權 +极权主义 極權主義 +极权国家 極權國家 +极权政治 極權政治 +极板 極板 +极核 極核 +极深研几 極深研幾 +极点 極點 +极爲 極爲 +极盛 極盛 +极盛时期 極盛時期 +极目 極目 +极目望去 極目望去 +极目远望 極目遠望 +极目远眺 極目遠眺 +极短篇 極短篇 +极移 極移 +极端 極端 +极端主义 極端主義 +极端份子 極端份子 +极端派 極端派 +极细小 極細小 +极至 極至 +极致 極致 +极色 極色 +极表同情 極表同情 +极轴 極軸 +极选 極選 +极量 極量 +极锋 極鋒 +极间电容 極間電容 +极限 極限 +极限体育 極限體育 +极限值 極限值 +极限强度 極限強度 +极高 極高 +极高点 極高點 +构筑 構築 +构筑工事 構築工事 +构筑物 構築物 +析出 析出 +析毫剖厘 析毫剖釐 +枕借 枕藉 +枕岩漱流 枕巖漱流 +枕席 枕蓆 +枕戈尝胆 枕戈嘗膽 +枕状玄武岩 枕狀玄武岩 +林下风致 林下風致 +林下风范 林下風範 +林丰正 林豐正 +林义杰 林義傑 +林云 林雲 +林云阁 林雲閣 +林俊杰 林俊杰 +林克 林克 +林克平大学 林克平大學 +林克海德 林克海德 +林克谟 林克謨 +林克谦 林克謙 +林冲 林沖 +林冲夜奔 林沖夜奔 +林占梅 林占梅 +林口台地 林口臺地 +林合隆 林合隆 +林周 林周 +林周县 林周縣 +林国梁 林國樑 +林宏岳 林宏嶽 +林布兰 林布蘭 +林干闵 林乾閔 +林德布拉德 林德布拉德 +林志嘉 林志嘉 +林志坚 林志堅 +林志声 林志聲 +林志杰 林志傑 +林志炫 林志炫 +林志玲 林志玲 +林志翔 林志翔 +林志辉 林志輝 +林志隆 林志隆 +林志雄 林志雄 +林志颖 林志穎 +林恢复 林恢復 +林承志 林承志 +林振丰 林振豐 +林敬杰 林敬傑 +林智胜 林智勝 +林木参天 林木參天 +林杰梁 林杰樑 +林杰闵 林傑閔 +林松 林松 +林松焕 林松煥 +林正杰 林正杰 +林永发 林永發 +林秀合 林秀合 +林秋 林秋 +林秋桂 林秋桂 +林群志 林羣志 +林致光 林致光 +林芳郁 林芳郁 +林英杰 林英傑 +林荣松 林榮松 +林表 林表 +林谷桦 林谷樺 +林谷芳 林谷芳 +林郁方 林郁方 +林钟 林鐘 +林靖杰 林靖傑 +林颖穗 林穎穗 +林默娘 林默娘 +枚卜 枚卜 +果于 果於 +果子干 果子乾 +果子干儿 果子乾兒 +果子药 果子藥 +果干 果乾 +果杯 果杯 +果核 果核 +果穗 果穗 +枝不得大于干 枝不得大於榦 +枝叶 枝葉 +枝叶扶疏 枝葉扶疏 +枝叶茂盛 枝葉茂盛 +枝干 枝幹 +枝干断折 枝幹斷折 +枝繁叶茂 枝繁葉茂 +枝胄 枝胄 +枝针 枝針 +枣庄 棗莊 +枣核 棗核 +枪打出头鸟 槍打出頭鳥 +枪托 槍托 +枪术 槍術 +枪杆 槍桿 +枪杆儿 槍桿兒 +枪杆子 槍桿子 +枪枝弹药 槍枝彈藥 +枪械弹药 槍械彈藥 +枫之谷 楓之谷 +枫叶 楓葉 +枯叶 枯葉 +枯叶蝶 枯葉蝶 +枯干 枯乾 +枯草杆菌 枯草桿菌 +架个 架個 +架了 架了 +架出 架出 +架出去 架出去 +架出来 架出來 +架回 架回 +架回去 架回去 +架回来 架回來 +架梁 架樑 +架海金梁 架海金梁 +架钟 架鐘 +枷板 枷板 +柏克 柏克 +柏克曼温度计 柏克曼溫度計 +柏克莱 柏克萊 +柏克郡 柏克郡 +柏克里克千佛洞 柏克里克千佛洞 +柏南克 柏南克 +柏卡里 柏卡里 +柏台 柏臺 +柏台乌府 柏臺烏府 +柏戴克 柏戴克 +柏林剧团 柏林劇團 +柏梁体 柏梁體 +柏梁台 柏梁臺 +柏梁诗 柏梁詩 +柏纳马修兹 柏納馬修茲 +柏罗米修斯 柏羅米修斯 +柏节松操 柏節松操 +柏里斯 柏里斯 +某个 某個 +某个人 某個人 +某只 某隻 +某方面 某方面 +某种 某種 +某种人 某種人 +某舍 某舍 +某面 某面 +染发 染髮 +染发剂 染髮劑 +染布 染布 +染干 染干 +染指于 染指於 +染指于鼎 染指於鼎 +染殿后 染殿後 +染织厂 染織廠 +柔克 柔克 +柔情万千 柔情萬千 +柔情万种 柔情萬種 +柔术 柔術 +柔枝嫩叶 柔枝嫩葉 +柔能克刚 柔能克剛 +柔能制刚 柔能制剛 +柜上 櫃上 +柜买中心 櫃買中心 +柜位 櫃位 +柜台 櫃檯 +柜台委讬 櫃檯委託 +柜员机 櫃員機 +柜坊赌局 櫃坊賭局 +柜子 櫃子 +柜房 櫃房 +柜柳 柜柳 +柜橱 櫃櫥 +柜身子 櫃身子 +柜里 櫃裏 +柜面儿 櫃面兒 +查克拉 查克拉 +查克瑞 查克瑞 +查准率 查準率 +查出 查出 +查扣 查扣 +查核 查覈 +查获 查獲 +柬埔寨人民党 柬埔寨人民黨 +柯克 柯克 +柯克斯 柯克斯 +柯尔克孜 柯爾克孜 +柯尔克孜族 柯爾克孜族 +柯尔克孜语 柯爾克孜語 +柯普里亚诺夫 柯普里亞諾夫 +柯札克 柯札克 +柯里 柯里 +柱梁 柱樑 +柳升耀 柳昇耀 +柳叶 柳葉 +柳叶儿 柳葉兒 +柳叶刀 柳葉刀 +柳叶描 柳葉描 +柳叶眉 柳葉眉 +柳叶鱼 柳葉魚 +柳斌杰 柳斌杰 +柳暗花明 柳暗花明 +柳暗花明又一村 柳暗花明又一村 +柳烟花雾 柳煙花霧 +柳诒征 柳詒徵 +柴埼幸 柴埼幸 +柴油发动机 柴油發動機 +柴胡 柴胡 +査不出 查不出 +査价 查價 +査修 查修 +査克拉 查克拉 +査兑克 查兌克 +査出 查出 +査出来 查出來 +査卷 查卷 +査号台 查號臺 +査回 查回 +査回去 查回去 +査回来 查回來 +査找周期 查找周期 +査报表 查報表 +査无实据 查無實據 +査获 查獲 +査表 查表 +査询台 查詢檯 +査问出 查問出 +栅极 柵極 +标价 標價 +标准 標準 +标准值 標準值 +标准偏差 標準偏差 +标准像 標準像 +标准元音 標準元音 +标准公顷 標準公頃 +标准制 標準制 +标准动作 標準動作 +标准化 標準化 +标准单位 標準單位 +标准号码 標準號碼 +标准型 標準型 +标准大气 標準大氣 +标准字体 標準字體 +标准尺寸 標準尺寸 +标准局 標準局 +标准工资 標準工資 +标准差 標準差 +标准接口 標準接口 +标准时 標準時 +标准时区 標準時區 +标准时间 標準時間 +标准普尔 標準普爾 +标准木 標準木 +标准杆 標準桿 +标准模型 標準模型 +标准气压 標準氣壓 +标准溶液 標準溶液 +标准版 標準版 +标准状况 標準狀況 +标准状态 標準狀態 +标准电阻 標準電阻 +标准组 標準組 +标准组织 標準組織 +标准细分表 標準細分表 +标准舞 標準舞 +标准规 標準規 +标准规格 標準規格 +标准规范 標準規範 +标准语 標準語 +标准镜头 標準鏡頭 +标准间 標準間 +标准音 標準音 +标准题名 標準題名 +标出 標出 +标出去 標出去 +标出来 標出來 +标占 標占 +标同伐异 標同伐異 +标志 標誌 +标志性 標誌性 +标志着 標誌着 +标本同治 標本同治 +标本虫 標本蟲 +标杆 標杆 +标标致致 標標致致 +标注 標註 +标示出 標示出 +标签 標籤 +标致 標緻 +标表 標表 +标记识别 標記識別 +栈板 棧板 +栉发工 櫛髮工 +栋折榱崩 棟折榱崩 +栋梁 棟樑 +栋梁之任 棟樑之任 +栋梁之材 棟樑之材 +栋梁之臣 棟樑之臣 +栏干 欄干 +栏杆 欄杆 +栏板 欄板 +栏柜 欄櫃 +树党 樹黨 +树叶 樹葉 +树干 樹幹 +树干断 樹幹斷 +树德技术学院 樹德技術學院 +树梁 樹樑 +树欲息而风不停 樹欲息而風不停 +树欲静而风不止 樹欲靜而風不止 +树种 樹種 +树虫子 樹蟲子 +树高千丈落叶归根 樹高千丈落葉歸根 +栖于 棲於 +栖栖皇皇 棲棲皇皇 +栗冽 慄冽 +栗凿 栗鑿 +栗喇 栗喇 +栗子 栗子 +栗尾 栗尾 +栗暴 栗暴 +栗栗 慄慄 +栗栗不安 慄慄不安 +栗栗危惧 慄慄危懼 +栗烈 栗烈 +栗然 慄然 +栗爆 栗爆 +栗田雄介 栗田雄介 +栗碌 栗碌 +栗色 栗色 +栗苞 栗苞 +栗薪 栗薪 +栗鼠 栗鼠 +校准 校準 +校园骨干 校園骨幹 +校核 校覈 +校舍 校舍 +样板 樣板 +样板戏 樣板戲 +样范 樣範 +核下 核下 +核二厂 核二廠 +核人 核人 +核仁 核仁 +核以 核以 +核价 覈價 +核体 核體 +核保 覈保 +核僵持 核僵持 +核儿 核兒 +核冬天 核冬天 +核准 覈准 +核准的 覈準的 +核减 覈減 +核出口控制 核出口控制 +核力 核力 +核办 核辦 +核化 核化 +核区 核區 +核发 核發 +核发电 核發電 +核发电厂 核發電廠 +核可 核可 +核合成 核合成 +核和 核和 +核四 核四 +核型 核型 +核复 覈覆 +核子 核子 +核子厂 核子廠 +核孔 核孔 +核字 覈字 +核定 覈定 +核实 覈實 +核审 覈審 +核对 覈對 +核对表 覈對表 +核岛 核島 +核工 核工 +核弹 核彈 +核当量 核當量 +核心 核心 +核战 核戰 +核战斗部 核戰斗部 +核批 覈批 +核技术 核技術 +核报 覈報 +核拨 覈撥 +核收 覈收 +核数 核數 +核是 核是 +核有 核有 +核果 核果 +核查 覈查 +核查制度 核查制度 +核桃 核桃 +核武 核武 +核火箭发动机 核火箭發動機 +核炫 核炫 +核点 核點 +核燃料后处理 核燃料後處理 +核爆 核爆 +核爆炸烟云 核爆炸煙雲 +核状 核狀 +核球 核球 +核甘 核甘 +核电 核電 +核电厂 核電廠 +核电磁脉冲 核電磁脈衝 +核的 核的 +核磁 核磁 +核示 覈示 +核种 核種 +核突 核突 +核算 覈算 +核粒 核粒 +核糖 核糖 +核糖核酸 核糖核酸 +核素 核素 +核线 核線 +核编 覈編 +核能 核能 +核能发电 核能發電 +核能发电厂 核能發電廠 +核能技术 核能技術 +核能电厂 核能電廠 +核膜 核膜 +核苷 核苷 +核菌 核菌 +核融合 核融合 +核融合发电 核融合發電 +核解 核解 +核计 覈計 +核计划 核計劃 +核试 核試 +核谈 核談 +核质 核質 +核资 覈資 +核载 核載 +核配 核配 +核酪 核酪 +核酶 核酶 +核酸 核酸 +核销 覈銷 +核防御 核防禦 +核验 覈驗 +根壮叶茂 根壯葉茂 +根据 根據 +根据上表 根據上表 +根据地 根據地 +根据规定 根據規定 +根深叶茂 根深葉茂 +根烟 根菸 +根目录 根目錄 +根系 根系 +根茎叶 根莖葉 +根须 根鬚 +格丹斯克 格丹斯克 +格于 格於 +格于成例 格於成例 +格于环境 格於環境 +格但斯克 格但斯克 +格兰杰 格蘭傑 +格列佛游记 格列佛遊記 +格列高利历 格列高利曆 +格别乌 格別烏 +格勒诺布尔 格勒諾布爾 +格子布 格子布 +格子棉布 格子棉布 +格拉哥里字母 格拉哥里字母 +格斗 格鬥 +格斗王 格鬥王 +格斗者 格鬥者 +格斗赛 格鬥賽 +格林威治天文台 格林威治天文臺 +格林威治标准时间 格林威治標準時間 +格林尼治标准时间 格林尼治標準時間 +格物致知 格物致知 +格瑞诺布 格瑞諾布 +格致 格致 +格致中学 格致中學 +格致国中 格致國中 +格致诚正修齐 格致誠正修齊 +格范 格範 +格里 格里 +格里历 格里曆 +格里姆斯塔 格里姆斯塔 +格里高利 格里高利 +格里高利历 格里高利曆 +栽种 栽種 +栽种机 栽種機 +栽种法 栽種法 +栽觔斗 栽觔斗 +栽跟斗 栽跟斗 +桂仔云 桂仔云 +桂圆干 桂圓乾 +桂林团 桂林團 +桂秋 桂秋 +桂系军阀 桂系軍閥 +桃叶歌 桃葉歌 +桃叶渡 桃葉渡 +桃叶珊瑚 桃葉珊瑚 +桃园中坜台地 桃園中壢臺地 +桃核 桃核 +桃核雕 桃核雕 +桃花人面 桃花人面 +桃花面 桃花面 +桃金娘 桃金娘 +桃金娘科 桃金娘科 +桅杆 桅杆 +框出 框出 +框出来 框出來 +案准 案准 +案几 案几 +案卷 案卷 +案发 案發 +案发前 案發前 +案发后 案發後 +案发时 案發時 +案头柜 案頭櫃 +案据 案據 +案板 案板 +桉叶油 桉葉油 +桌几 桌几 +桌别林 桌別林 +桌历 桌曆 +桌布 桌布 +桌椅板凳 桌椅板凳 +桌面 桌面 +桌面上 桌面上 +桌面儿 桌面兒 +桌面儿上 桌面兒上 +桌面系统 桌面系統 +桐叶知秋 桐葉知秋 +桐花烟 桐花煙 +桑叶 桑葉 +桑定党 桑定黨 +桑布森 桑布森 +桑干 桑乾 +桑干河 桑乾河 +桑干盆地 桑乾盆地 +桑托斯 桑托斯 +桑托荣 桑托榮 +桑托里尼岛 桑托里尼島 +桑杰士 桑傑士 +桑蓬之志 桑蓬之志 +桑虫 桑蟲 +桑针 桑針 +桓台 桓臺 +桓台县 桓臺縣 +桓表 桓表 +桢干 楨幹 +档卷 檔卷 +档案传输系统 檔案傳輸系統 +档案搜寻系统 檔案搜尋系統 +档案柜 檔案櫃 +桥台 橋臺 +桥梁 橋樑 +桥梁工事 橋樑工事 +桥梁工程 橋樑工程 +桥面 橋面 +桥面上 橋面上 +桶里 桶裏 +桶里射鱼 桶裏射魚 +梁上 樑上 +梁上君子 樑上君子 +梁世煌 梁世煌 +梁丽 梁麗 +梁书 梁書 +梁仙台 梁仙臺 +梁任公 梁任公 +梁伟聪 梁偉聰 +梁伟铭 梁偉銘 +梁修身 梁修身 +梁元帝 梁元帝 +梁又琳 梁又琳 +梁启超 梁啓超 +梁咏琪 梁詠琪 +梁唐晋汉周书 梁唐晉漢周書 +梁园 梁園 +梁园区 梁園區 +梁园宴雪 梁園宴雪 +梁园虽好不是久恋之家 梁園雖好不是久戀之家 +梁国荣 梁國榮 +梁太祖 梁太祖 +梁姓 梁姓 +梁子 樑子 +梁子湖 梁子湖 +梁子湖区 梁子湖區 +梁孟 梁孟 +梁实秋 梁實秋 +梁家辉 梁家輝 +梁容银 梁容銀 +梁山 梁山 +梁山伯 梁山伯 +梁山伯与祝英台 梁山伯與祝英臺 +梁山伯祝英台 梁山伯祝英臺 +梁山县 梁山縣 +梁山市 梁山市 +梁山泊 梁山泊 +梁州 梁州 +梁平 梁平 +梁平县 梁平縣 +梁建伟 梁建偉 +梁德馨 梁德馨 +梁心颐 梁心頤 +梁惠王 梁惠王 +梁敬帝 梁敬帝 +梁文冲 梁文沖 +梁文音 梁文音 +梁文骐 梁文騏 +梁朝伟 梁朝偉 +梁木 梁木 +梁木其坏 樑木其壞 +梁架 樑架 +梁柱 樑柱 +梁栋 樑棟 +梁案 梁案 +梁楷 梁楷 +梁次震 梁次震 +梁武帝 梁武帝 +梁氏 梁氏 +梁河 梁河 +梁河县 梁河縣 +梁漱溟 梁漱溟 +梁父吟 梁父吟 +梁狱上书 梁獄上書 +梁玉芳 梁玉芳 +梁皇宝忏 梁皇寶懺 +梁皇忏 梁皇懺 +梁祝 梁祝 +梁简文帝 梁簡文帝 +梁红玉 梁紅玉 +梁经伦 梁經倫 +梁羽生 梁羽生 +梁茜雯 梁茜雯 +梁赞 梁贊 +梁辀 梁輈 +梁辰鱼 梁辰魚 +梁锦兴 梁錦興 +梁静茹 梁靜茹 +梁靜茹 梁靜茹 +梁鸿 梁鴻 +梁鸿五噫 梁鴻五噫 +梁龙 樑龍 +梅克 梅克 +梅克尔 梅克爾 +梅尼尔氏症 梅尼爾氏症 +梅布托 梅布托 +梅干 梅乾 +梅干菜 梅乾菜 +梅德韦杰夫 梅德韋傑夫 +梅杰 梅傑 +梅核 梅核 +梅花小娘 梅花小娘 +梅西叶 梅西葉 +梅西叶星表 梅西葉星表 +梅西耶星表 梅西耶星表 +梅里 梅里 +梅里亚 梅里亞 +梅里亚姆韦伯斯特 梅里亞姆韋伯斯特 +梅里斯 梅里斯 +梅里斯区 梅里斯區 +梅里斯达斡尔族区 梅里斯達斡爾族區 +梅里美 梅里美 +梅里雪山 梅里雪山 +梓里 梓里 +梢云 梢雲 +梦云 夢雲 +梦兰叶吉 夢蘭叶吉 +梦卜 夢卜 +梦周公 夢周公 +梦回 夢迴 +梦工厂 夢工廠 +梦工厂动画 夢工廠動畫 +梦往神游 夢往神遊 +梦有五不占 夢有五不占 +梦游 夢遊 +梦游症 夢遊症 +梦粱录 夢粱錄 +梦系 夢繫 +梦行症 夢行症 +梦里 夢裏 +梦里蝴蝶 夢裏蝴蝶 +梨云 梨雲 +梨干 梨乾 +梨干儿 梨乾兒 +梯冲 梯衝 +梯度回波 梯度回波 +梯恩梯当量 梯恩梯當量 +梯板 梯板 +械斗 械鬥 +械系 械繫 +梳了 梳了 +梳发 梳髮 +梳头发 梳頭髮 +梳妆台 梳妝檯 +梵册贝叶 梵冊貝葉 +梵谷 梵谷 +检修 檢修 +检出 檢出 +检复 檢覆 +检字表 檢字表 +检尸 檢屍 +检录 檢錄 +检査出 檢查出 +检査出来 檢查出來 +检核 檢核 +检核表 檢核表 +检测出 檢測出 +检测出来 檢測出來 +检索系统 檢索系統 +检警合一 檢警合一 +检阅台 檢閱臺 +检验出 檢驗出 +检验出来 檢驗出來 +棉制 棉製 +棉卷 棉卷 +棉厂 棉廠 +棉布 棉布 +棉布婚 棉布婚 +棉签 棉籤 +棉纤维 棉纖維 +棉纺厂 棉紡廠 +棉花布 棉花布 +棉花店里歇工 棉花店裏歇工 +棉花火药 棉花火藥 +棉药签 棉藥籤 +棉铃虫 棉鈴蟲 +棋布 棋佈 +棋布星罗 棋佈星羅 +棋杯 棋杯 +棋罗星布 棋羅星佈 +棋逢对手将遇良才 棋逢對手將遇良才 +棒下出孝子 棒下出孝子 +棒坛 棒壇 +棒头出孝子 棒頭出孝子 +棒子面 棒子麪 +棒曲霉素 棒麴黴素 +棒极了 棒極了 +棒状杆菌 棒狀桿菌 +棒针 棒針 +棒针衫 棒針衫 +棕色种 棕色種 +棘针 棘針 +棘针科 棘針科 +棘针门 棘針門 +棫朴 棫樸 +森林学系 森林學系 +森林抚育采伐 森林撫育採伐 +森林游乐区 森林遊樂區 +森林生态系 森林生態系 +森林里 森林裏 +森罗万象 森羅萬象 +棱体 棱體 +棱台 棱臺 +棱层 棱層 +棱等登 棱等登 +棱线 棱線 +棱缝 棱縫 +棱锥 棱錐 +棱锥台 棱錐臺 +棱镜 棱鏡 +棺材出了讨挽歌郎钱 棺材出了討挽歌郎錢 +棺材板 棺材板 +棺材里 棺材裏 +棺材里伸手 棺材裏伸手 +椅里 椅裏 +植党 植黨 +植党营私 植黨營私 +植发 植髮 +植基于 植基於 +植根于 植根於 +植物区系 植物區系 +植物志 植物誌 +植物纤维 植物纖維 +植病系 植病系 +植皮手术 植皮手術 +椭圆面 橢圓面 +椰枣干 椰棗乾 +椰胡 椰胡 +椰雕 椰雕 +椽梁 椽梁 +椿萱并茂 椿萱並茂 +楚庄王 楚莊王 +楚庄绝缨 楚莊絕纓 +楚庄问鼎 楚莊問鼎 +楚汉春秋 楚漢春秋 +楚霸王困垓下 楚霸王困垓下 +楞了楞 楞了楞 +楣运当头 楣運當頭 +楮叶 楮葉 +楼台 樓臺 +楼台亭阁 樓臺亭閣 +楼台会 樓臺會 +楼板 樓板 +楼梯台 樓梯臺 +楼梯平台 樓梯平臺 +楼阁亭台 樓閣亭臺 +楼面 樓面 +概念 概念 +榆叶梅 榆葉梅 +榔板 榔板 +榕坛问业 榕壇問業 +榨干 榨乾 +榨油厂 榨油廠 +榻布 榻布 +槃才 槃才 +槃槃大才 槃槃大才 +槓杆 槓桿 +槓杆原理 槓桿原理 +槭叶止血草 槭葉止血草 +樊于期 樊於期 +樛曲 樛曲 +模制 模製 +模制品 模製品 +模块板 模塊板 +模型板 模型板 +模式种 模式種 +模拟出 模擬出 +模板 模板 +模板工 模板工 +模糊集合理论 模糊集合理論 +模组厂 模組廠 +模网论坛 模網論壇 +模胡 模胡 +模范 模範 +模范丈夫 模範丈夫 +模范人物 模範人物 +模范作用 模範作用 +模范区 模範區 +模范城市 模範城市 +模范大音阶 模範大音階 +模范学生 模範學生 +模范带头 模範帶頭 +模范带头作用 模範帶頭作用 +模范教师 模範教師 +模范村 模範村 +模范棒棒堂 模範棒棒堂 +模范生 模範生 +模范省 模範省 +模范章 模範章 +模范行动 模範行動 +模表 模表 +模里西斯 模里西斯 +模里西斯共和国 模里西斯共和國 +横了心 橫了心 +横冲 橫衝 +横冲直撞 橫衝直撞 +横出 橫出 +横切面 橫切面 +横剖面 橫剖面 +横向 橫向 +横尸 橫屍 +横尸遍野 橫屍遍野 +横征暴敛 橫徵暴斂 +横截面 橫截面 +横打鼻梁儿 橫打鼻梁兒 +横扫千军 橫掃千軍 +横折 橫折 +横摆 橫擺 +横断面 橫斷面 +横杆 橫杆 +横杠 橫槓 +横梁 橫樑 +横眉冷对千夫指 橫眉冷對千夫指 +横秋 橫秋 +横筋斗 橫筋斗 +横舍 橫舍 +横面 橫面 +横须贺 橫須賀 +横须贺市 橫須賀市 +樱花杯 櫻花盃 +樵苏 樵蘇 +樵苏不爨 樵蘇不爨 +樵采 樵採 +橄榄岩 橄欖岩 +橘核 橘核 +橡子面 橡子麪 +橡子面儿 橡子麪兒 +橡斗 橡斗 +橡木叶 橡木葉 +橦布 橦布 +橫亘 橫亙 +橱柜 櫥櫃 +檀板 檀板 +欠当 欠當 +欠据 欠據 +次一个 次一個 +次于 次於 +次分面 次分面 +次后 次後 +次级团体 次級團體 +欢乐谷 歡樂谷 +欢喜欲狂 歡喜欲狂 +欣喜欲狂 欣喜欲狂 +欣戚 欣戚 +欣欣向荣 欣欣向榮 +欣然同意 欣然同意 +欣生恶死 欣生惡死 +欧伯托 歐伯托 +欧佩克 歐佩克 +欧克 歐克 +欧克曼 歐克曼 +欧克莱 歐克萊 +欧几里得 歐幾里得 +欧几里得原理 歐幾里得原理 +欧几里德 歐幾里德 +欧化倾向 歐化傾向 +欧布拉多 歐布拉多 +欧布莱特 歐布萊特 +欧式几何 歐式幾何 +欧式几何学 歐式幾何學 +欧式建筑 歐式建築 +欧战后 歐戰後 +欧拉朱万 歐拉朱萬 +欧普艺术 歐普藝術 +欧氏几何学 歐氏幾何學 +欧洲共同体 歐洲共同體 +欧洲共同市场 歐洲共同市場 +欧洲复兴计画 歐洲復興計畫 +欧洲安全与合作组织 歐洲安全與合作組織 +欧洲安全和合作组织 歐洲安全和合作組織 +欧洲杯 歐洲盃 +欧洲火药库 歐洲火藥庫 +欧洲货币体系 歐洲貨幣體系 +欧游 歐遊 +欧游之行 歐遊之行 +欧游记趣 歐遊記趣 +欧特里尼 歐特里尼 +欧秋雅 歐秋雅 +欧系 歐系 +欧罗巴人种 歐羅巴人種 +欧胡岛 歐胡島 +欧萨苏纳 歐薩蘇納 +欧足联杯 歐足聯杯 +欧里庇得斯 歐里庇得斯 +欧里桑 歐里桑 +欧阳修 歐陽修 +欧雷克 歐雷克 +欲不可从 欲不可從 +欲人不知莫若勿为 欲人不知莫若勿爲 +欲令智昏 慾令智昏 +欲加之罪 欲加之罪 +欲加之罪何患无词 慾加之罪何患無詞 +欲加之罪何患无辞 欲加之罪何患無辭 +欲取固与 欲取固與 +欲取姑与 欲取姑與 +欲取姑予 欲取姑予 +欲哭无泪 欲哭無淚 +欲善其事必先利其器 慾善其事必先利其器 +欲堑 欲塹 +欲壑难填 慾壑難填 +欲壑难饱 欲壑難飽 +欲女 慾女 +欲就还推 欲就還推 +欲待 欲待 +欲得 欲得 +欲念 慾念 +欲振乏力 欲振乏力 +欲擒故纵 欲擒故縱 +欲望 慾望 +欲求 欲求 +欲求不满 慾求不滿 +欲海 慾海 +欲海难填 欲海難填 +欲深谿壑 欲深谿壑 +欲火 慾火 +欲火焚身 慾火焚身 +欲炙之色 欲炙之色 +欲界 欲界 +欲益反损 欲益反損 +欲盖弥彰 欲蓋彌彰 +欲穷千里目 欲窮千里目 +欲绝 欲絕 +欲罢不能 欲罷不能 +欲裂 欲裂 +欲言又止 欲言又止 +欲语还休 欲語還休 +欲说还休 欲說還休 +欲购从速 欲購從速 +欲速不达 欲速不達 +欲速则不达 欲速則不達 +欲速而不达 欲速而不達 +欲障 慾障 +欷吁 欷吁 +欸乃曲 欸乃曲 +欹嵚历落 欹嶔歷落 +欺善怕恶 欺善怕惡 +欺敌战术 欺敵戰術 +欺蒙 欺矇 +款冬 款冬 +款曲 款曲 +歇了 歇了 +歇后 歇後 +歇后语 歇後語 +歇斯底里 歇斯底里 +歇洛克福尔摩斯 歇洛克福爾摩斯 +歇马杯 歇馬杯 +歌仔戏团 歌仔戲團 +歌剧团 歌劇團 +歌台舞榭 歌臺舞榭 +歌后 歌后 +歌坛 歌壇 +歌坛上 歌壇上 +歌坛新秀 歌壇新秀 +歌声绕梁 歌聲繞梁 +歌曲 歌曲 +歌板 歌板 +歌舞升平 歌舞昇平 +歌舞团 歌舞團 +歌钟 歌鐘 +歌风台 歌風臺 +歎吁 歎吁 +歛出来 歛出來 +止不了 止不了 +止于 止於 +止于至善 止於至善 +止咳药 止咳藥 +止恶扬善 止惡揚善 +止泻药 止瀉藥 +止涨回跌 止漲回跌 +止痛药 止痛藥 +止血药 止血藥 +止谤莫如自修 止謗莫如自脩 +止跌回升 止跌回升 +止饥 止飢 +正义党 正義黨 +正义发展党 正義發展黨 +正义斗争 正義鬥爭 +正于 正於 +正修工专 正修工專 +正凶 正凶 +正出 正出 +正则参数 正則參數 +正反两面 正反兩面 +正反合 正反合 +正反面 正反面 +正合 正合 +正合我意 正合我意 +正合适 正合適 +正后 正後 +正后像 正後像 +正后方 正後方 +正向 正向 +正向前看 正向前看 +正处于 正處於 +正多面体 正多面體 +正头娘子 正頭娘子 +正官庄 正官莊 +正宫娘娘 正宮娘娘 +正对面 正對面 +正常范围 正常範圍 +正弦 正弦 +正弦曲线 正弦曲線 +正当 正當 +正当中 正當中 +正当化 正當化 +正当性 正當性 +正当时 正當時 +正当理由 正當理由 +正当行为 正當行爲 +正当防卫 正當防衛 +正当防卫行为 正當防衛行爲 +正念 正念 +正手板 正手板 +正方向 正方向 +正方晶系 正方晶系 +正极 正極 +正枝正叶 正枝正葉 +正梁 正樑 +正正当当 正正當當 +正正确确 正正確確 +正电子发射体层 正電子發射體層 +正电子发射层析 正電子發射層析 +正电子发射断层照相术 正電子發射斷層照相術 +正电子发射计算机断层 正電子發射計算機斷層 +正电极 正電極 +正确 正確 +正确处理 正確處理 +正确处理人民内部矛盾 正確處理人民內部矛盾 +正确度 正確度 +正确性 正確性 +正确路线 正確路線 +正误表 正誤表 +正集团 正集團 +正面 正面 +正面人物 正面人物 +正面图 正面圖 +正面攻击 正面攻擊 +此仆彼起 此仆彼起 +此发彼应 此發彼應 +此后 此後 +此系 此係 +步人后尘 步人後塵 +步兵团 步兵團 +步出 步出 +步后尘 步後塵 +步向 步向 +步斗踏罡 步斗踏罡 +步月登云 步月登雲 +步枪团 步槍團 +步步行凶 步步行兇 +步步高升 步步高昇 +步线行针 步線行針 +步罡踏斗 步罡踏斗 +步行虫 步行蟲 +步调一致 步調一致 +步进制 步進制 +步青云 步青雲 +步飞烟传 步飛煙傳 +武丑 武丑 +武偃文修 武偃文修 +武后 武后 +武器系统 武器系統 +武器级别材料 武器級別材料 +武坛 武壇 +武大郎吃毒药 武大郎吃毒藥 +武当 武當 +武当山 武當山 +武当派 武當派 +武斗 武鬥 +武曲星 武曲星 +武术 武術 +武术指导 武術指導 +武术比赛 武術比賽 +武术竞赛 武術競賽 +武松 武松 +武松打虎 武松打虎 +武梁祠画像 武梁祠畫像 +武胜关 武勝關 +武胜县 武勝縣 +武装冲突 武裝衝突 +武装斗争 武裝鬥爭 +武里省 武里省 +歧出 歧出 +歪向 歪向 +歪才 歪才 +歪摆布 歪擺佈 +歪曲 歪曲 +歪曲事实 歪曲事實 +歹念 歹念 +歹斗 歹鬥 +死不了 死不了 +死不了心 死不了心 +死了 死了 +死于 死於 +死于安乐 死於安樂 +死于非命 死於非命 +死伤枕借 死傷枕藉 +死伤相借 死傷相藉 +死党 死黨 +死别 死別 +死别生离 死別生離 +死后 死後 +死后多年 死後多年 +死命挣扎 死命掙扎 +死定了 死定了 +死尸 死屍 +死当 死當 +死扣 死扣 +死有余辜 死有餘辜 +死有重于泰山轻于鸿毛 死有重於泰山輕於鴻毛 +死板 死板 +死板板 死板板 +死气沈沈 死氣沈沈 +死海古卷 死海古卷 +死海经卷 死海經卷 +死灰复然 死灰復然 +死灰复燃 死灰復燃 +死灰复燎 死灰復燎 +死生未卜 死生未卜 +死症 死症 +死知府不如一个活老鼠 死知府不如一個活老鼠 +死而不僵 死而不僵 +死而后已 死而後已 +死而后止 死而後止 +死而复活 死而復活 +死而复生 死而復生 +死而复苏 死而復甦 +死胡同 死衚衕 +死要面子 死要面子 +死谷 死谷 +死里求生 死裏求生 +死里逃生 死裏逃生 +死难同胞 死難同胞 +死面 死麪 死面 +死马当活马医 死馬當活馬醫 +死马当活马治 死馬當活馬治 +殆尽 殆盡 +殊别 殊別 +殊域周咨录 殊域周咨錄 +殊属不当 殊屬不當 +殊方同致 殊方同致 +殊涂一致 殊塗一致 +殊涂同会 殊塗同會 +殊涂同归 殊塗同歸 +殊涂同致 殊塗同致 +殊胜 殊勝 +殊致 殊致 +殊致同归 殊致同歸 +殊路同归 殊路同歸 +殊途同归 殊途同歸 +残余 殘餘 +残余沾染 殘餘沾染 +残余物 殘餘物 +残党 殘黨 +残冬 殘冬 +残冬腊月 殘冬臘月 +残卷 殘卷 +残干 殘幹 +残废后 殘廢後 +残念 殘念 +残春雾雨余 殘春霧雨餘 +残杯冷炙 殘杯冷炙 +残秋 殘秋 +残肴 殘餚 +殖谷 殖穀 +殚尽 殫盡 +殚思极虑 殫思極慮 +殚精极思 殫精極思 +殚精极虑 殫精極慮 +殡舍 殯舍 +殢云尤雨 殢雲尤雨 +殢雨尤云 殢雨尤雲 +殴斗 毆鬥 +段皇云 段皇雲 +殷同 殷同 +殷墟卜辞 殷墟卜辭 +殷师牛斗 殷師牛鬥 +殷鉴 殷鑑 +殷鉴不远 殷鑑不遠 +殿后 殿後 +殿钟自鸣 殿鐘自鳴 +毁了 毀了 +毁于 毀於 +毁于一旦 毀於一旦 +毁尸灭迹 毀屍滅跡 +毁廉蔑耻 毀廉蔑恥 +毁弃 譭棄 +毁方瓦合 毀方瓦合 +毁炎 燬炎 +毁犀 燬犀 +毁誉 譭譽 +毁誉参半 譭譽參半 +毁诬 譭誣 +毁钟为铎 譭鐘爲鐸 +毋贻后患 毋貽後患 +毋须 毋須 +毋须乎 毋須乎 +母丑 母醜 +母乳喂养 母乳餵養 +母党 母黨 +母后 母后 +母大虫 母大蟲 +母权制 母權制 +母板 母板 +母系 母系 +母系亲属 母系親屬 +母系制度 母系制度 +母系社会 母系社會 +母范 母範 +母钟 母鐘 +母音表 母音表 +每一个人 每一個人 +每一个人都有美中不足的地方 每一個人都有美中不足的地方 +每个 每個 +每个人 每個人 +每个月 每個月 +每于 每於 +每公里 每公里 +每分钟 每分鐘 +每只 每隻 +每周 每週 +每周一次 每週一次 +每回 每回 +每当 每當 +每战必胜 每戰必勝 +每日限价 每日限價 +每杯 每杯 +每每只 每每只 +每种 每種 +每秒钟 每秒鐘 +每端口价格 每端口價格 +每面 每面 +毒僵指 毒殭指 +毒症 毒症 +毒药 毒藥 +毒药苦口 毒藥苦口 +毒药苦口利于病 毒藥苦口利於病 +毒虫 毒蟲 +毒针 毒針 +比上不足比下有余 比上不足比下有餘 +比不了 比不了 +比个 比個 +比个大哥二哥 比個大哥二哥 +比个高下 比個高下 +比个高低 比個高低 +比了 比了 +比什凯克 比什凱克 +比价 比價 +比众不同 比衆不同 +比例代表制 比例代表制 +比出 比出 +比划 比劃 +比合 比合 +比周 比周 +比喻失当 比喻失當 +比干 比干 +比并 比並 +比手划脚 比手劃腳 +比才 比才 +比杆赛 比桿賽 +比约克曼 比約克曼 +比舍 比舍 +比表面 比表面 +比较规范 比較規範 +毕业于 畢業於 +毕于 畢於 +毕其功于一役 畢其功於一役 +毕力同心 畢力同心 +毕升 畢昇 +毕宿星团 畢宿星團 +毕生发展 畢生發展 +毕翠克丝 畢翠克絲 +毗婆尸佛 毗婆尸佛 +毗舍婆佛 毗舍婆佛 +毙而后已 斃而後已 +毚欲 毚欲 +毛了 毛了 +毛了手脚 毛了手腳 +毛出在羊身上 毛出在羊身上 +毛卷 毛卷 +毛厕里砖儿 毛廁裏磚兒 +毛发 毛髮 +毛发之功 毛髮之功 +毛发俱竖 毛髮俱豎 +毛发倒竖 毛髮倒豎 +毛发悚然 毛髮悚然 +毛发森竖 毛髮森豎 +毛发皆竖 毛髮皆豎 +毛发耸然 毛髮聳然 +毛司里砖儿 毛司裏磚兒 +毛囊虫 毛囊蟲 +毛团 毛團 +毛团把戏 毛團把戲 +毛坑没后壁 毛坑沒後壁 +毛姜 毛薑 +毛巾布 毛巾布 +毛布 毛布 +毛板 毛板 +毛栗子 毛栗子 +毛毛虫 毛毛蟲 +毛毯里 毛毯裏 +毛纺厂 毛紡廠 +毛线针 毛線針 +毛织布 毛織布 +毛虫 毛蟲 +毛语录 毛語錄 +毛里塔尼亚 毛里塔尼亞 +毛里拖毡 毛裏拖氈 +毛里求斯 毛里求斯 +毫不相干 毫不相干 +毫仑目 毫侖目 +毫克 毫克 +毫升 毫升 +毫厘 毫釐 +毫厘不差 毫釐不差 +毫厘不爽 毫釐不爽 +毫厘之差 毫釐之差 +毫厘千里 毫釐千里 +毫发 毫髮 +毫发不差 毫髮不差 +毫发不爽 毫髮不爽 +毫发之差 毫髮之差 +毫发无损 毫髮無損 +毫发未伤 毫髮未傷 +毫居里 毫居里 +毫无二致 毫無二致 +毫无价值 毫無價值 +毫无准备 毫無準備 +毫无根据 毫無根據 +毫无节制 毫無節制 +毫针 毫針 +毯里 毯裏 +氏症 氏症 +氏胄 氏胄 +民丰 民豐 +民丰县 民豐縣 +民主党 民主黨 +民主党人 民主黨人 +民主党员 民主黨員 +民主党派 民主黨派 +民主党籍 民主黨籍 +民主制度 民主制度 +民主进步党 民主進步黨 +民主进步党籍 民主進步黨籍 +民主集中制 民主集中制 +民众代表 民衆代表 +民众党 民衆黨 +民众团体 民衆團體 +民俗曲艺 民俗曲藝 +民党 民黨 +民和回族土族自治县 民和回族土族自治縣 +民和年丰 民和年豐 +民团 民團 +民坠涂炭 民墜塗炭 +民心向背 民心向背 +民心所向 民心所向 +民心趋向 民心趨向 +民志 民志 +民怨盈涂 民怨盈塗 +民意代表 民意代表 +民意向背 民意向背 +民政里 民政里 +民族党 民族黨 +民族同化 民族同化 +民族团结 民族團結 +民族复兴运动 民族復興運動 +民族志 民族誌 +民族色彩 民族色彩 +民极 民極 +民生涂炭 民生塗炭 +民生雕敝 民生雕敝 +民用建筑 民用建築 +民用航空器适航证书 民用航空器適航證書 +民社党 民社黨 +民穷财尽 民窮財盡 +民谣歌曲 民謠歌曲 +民进党 民進黨 +民进党员 民進黨員 +民进党团 民進黨團 +民进党版 民進黨版 +民进党籍 民進黨籍 +民间合会 民間合會 +民间团体 民間團體 +民间艺术 民間藝術 +民风淳朴 民風淳樸 +民风纯朴 民風純樸 +气一冲 氣一衝 +气了 氣了 +气充志定 氣充志定 +气充志骄 氣充志驕 +气克斗牛 氣克斗牛 +气冲冲 氣沖沖 +气冲斗牛 氣衝斗牛 +气冲牛斗 氣衝牛斗 +气冲霄汉 氣衝霄漢 +气出 氣出 +气力用尽 氣力用盡 +气动控制 氣動控制 +气势熏灼 氣勢熏灼 +气压表 氣壓表 +气吁吁 氣吁吁 +气吞牛斗 氣吞牛斗 +气味相合 氣味相合 +气喘吁吁 氣喘吁吁 +气团 氣團 +气在心里 氣在心裏 +气定神闲 氣定神閒 +气尽 氣盡 +气帘 氣簾 +气扬采飞 氣揚采飛 +气数已尽 氣數已盡 +气极败坏 氣極敗壞 +气满志得 氣滿志得 +气满志骄 氣滿志驕 +气焰熏天 氣焰熏天 +气燄万丈 氣燄萬丈 +气管切开术 氣管切開術 +气管插管术 氣管插管術 +气若游丝 氣若游絲 +气象万千 氣象萬千 +气象台 氣象臺 +气郁 氣鬱 +气铲 氣鏟 +气骄志满 氣驕志滿 +氘核 氘核 +氢净合成油 氫淨合成油 +氢卤酸 氫鹵酸 +氤郁 氤鬱 +氨基甲酸酯类化合物 氨基甲酸酯類化合物 +氮血症 氮血症 +水一冲 水一衝 +水上摩托车 水上摩托車 +水俣症 水俁症 +水光云影 水光雲影 +水再冲 水再衝 +水准 水準 +水准仪 水準儀 +水准器 水準器 +水准图 水準圖 +水准测量 水準測量 +水利制 水利制 +水力发电 水力發電 +水力发电站 水力發電站 +水厂 水廠 +水又冲 水又衝 +水合 水合 +水合物 水合物 +水团 水團 +水尽山穷 水盡山窮 +水尽鹅飞 水盡鵝飛 +水已冲 水已衝 +水已干 水已乾 +水帘 水簾 +水帘洞 水簾洞 +水干 水乾 +水干尽 水乾盡 +水干掉 水乾掉 +水平面 水平面 +水底写字板 水底寫字板 +水底捞针 水底撈針 +水当当 水噹噹 +水彩 水彩 +水彩画 水彩畫 +水彩画家 水彩畫家 +水彩笔 水彩筆 +水彩颜料 水彩顏料 +水急冲 水急衝 +水成岩 水成岩 +水才干 水纔乾 +水斗 水斗 +水斧虫 水斧蟲 +水晶杯 水晶杯 +水曲 水曲 +水曲柳 水曲柳 +水来汤里去 水來湯裏去 +水杯 水杯 +水柜 水櫃 +水波荡漾 水波盪漾 +水泥厂 水泥廠 +水泥板 水泥板 +水注 水注 +水流內布袋戏台 水流內布袋戲臺 +水浒后传 水滸後傳 +水清可鉴 水清可鑑 +水温表 水溫表 +水烟 水煙 +水烟袋 水菸袋 +水磨腔曲子 水磨腔曲子 +水秋千 水鞦韆 +水穷山尽 水窮山盡 +水管系 水管系 +水管面 水管麪 +水精帘 水精簾 +水系 水系 +水经注 水經注 +水落石出 水落石出 +水蜡树 水蠟樹 +水蜡虫 水蠟蟲 +水表 水錶 +水谷 水谷 +水谷之海 水穀之海 +水谷隼 水谷隼 +水轮发动 水輪發動 +水轮发动机 水輪發動機 +水轮发电 水輪發電 +水逝云卷 水逝雲卷 +水里 水裏 +水里乡 水里鄉 +水里水里来火里火里去 水裏水裏來火裏火裏去 +水里纳瓜 水裏納瓜 +水里鄉 水里鄉 +水鉴 水鑑 +水陆并进 水陸並進 +水面 水面 +水面上 水面上 +水面系数 水面係數 +水面舰 水面艦 +水面舰艇 水面艦艇 +水龙卷 水龍捲 +永不录用 永不錄用 +永丰 永豐 +永丰余 永豐餘 +永丰县 永豐縣 +永丰堂 永豐堂 +永丰舰 永豐艦 +永丰舰事件 永豐艦事件 +永丰金 永豐金 +永修县 永修縣 +永别 永別 +永别酒 永別酒 +永历 永曆 +永发 永發 +永志 永志 +永志不忘 永誌不忘 +永结同心 永結同心 +永续发展 永續發展 +永胜 永勝 +永胜县 永勝縣 +汀曲 汀曲 +求借 求借 +求出 求出 +求助于 求助於 +求助于人 求助於人 +求古录 求古錄 +求同 求同 +求同存异 求同存異 +求才 求才 +求才若渴 求才若渴 +求教于 求教於 +求死愿望 求死願望 +求生意志 求生意志 +求田问舍 求田問舍 +求知欲 求知慾 +求神问卜 求神問卜 +求签 求籤 +求胜 求勝 +求胜心 求勝心 +求过于供 求過於供 +求道于盲 求道於盲 +汇业 匯業 +汇业财经集团 匯業財經集團 +汇业银行 匯業銀行 +汇丰 匯豐 +汇丰银行 匯豐銀行 +汇付 匯付 +汇价 匯價 +汇信 匯信 +汇兑 匯兌 +汇入 匯入 +汇出 匯出 +汇出行 匯出行 +汇刊 彙刊 +汇划 匯劃 +汇到 匯到 +汇合 匯合 +汇回 匯回 +汇寄 匯寄 +汇展 匯展 +汇川区 匯川區 +汇差 匯差 +汇市 匯市 +汇总 彙總 +汇成 匯成 +汇报 彙報 +汇拢 匯攏 +汇改 匯改 +汇整 彙整 +汇映 彙映 +汇款 匯款 +汇款人 匯款人 +汇款单 匯款單 +汇水 匯水 +汇流 匯流 +汇流处 匯流處 +汇流排 匯流排 +汇流点 匯流點 +汇流环 匯流環 +汇演 匯演 +汇点 匯點 +汇爲 匯爲 +汇率 匯率 +汇率制 匯率制 +汇率差额 匯率差額 +汇电 匯電 +汇票 匯票 +汇算 彙算 +汇纂 彙纂 +汇给 匯給 +汇编 彙編 +汇编语言 彙編語言 +汇聚 匯聚 +汇至 匯至 +汇费 匯費 +汇辑 彙輯 +汇进 匯進 +汇通 匯通 +汇金 匯金 +汇钱 匯錢 +汇银 匯銀 +汇集 彙集 +汇集点 匯集點 +汉书艺文志 漢書藝文志 +汉克 漢克 +汉克阿伦 漢克阿倫 +汉台 漢臺 +汉台区 漢臺區 +汉城特别市 漢城特別市 +汉奸 漢奸 +汉奸走狗 漢奸走狗 +汉学系 漢學系 +汉宫秋 漢宮秋 +汉宫秋月 漢宮秋月 +汉弥尔顿 漢彌爾頓 +汉弥登钟 漢彌登鐘 +汉弥登钟表公司 漢彌登鐘錶公司 +汉志 漢志 +汉杰 漢傑 +汉满蒙回藏 漢滿蒙回藏 +汉药方 漢藥方 +汉萨同盟 漢薩同盟 +汉藏语系 漢藏語系 +汗出如浆 汗出如漿 +汗出如雨 汗出如雨 +汗洽股栗 汗洽股慄 +汗流满面 汗流滿面 +汗腾格里 汗騰格里 +汙蔑 污衊 +汞合金 汞合金 +江云渭树 江雲渭樹 +江南四大才子 江南四大才子 +江南机器制造局 江南機器製造局 +江参 江參 +江宏杰 江宏傑 +江干 江干 +江干区 江乾區 +江志雄 江志雄 +江村销夏录 江村銷夏錄 +江海同归 江海同歸 +江淹才尽 江淹才盡 +江湖术士 江湖術士 +江湖术语 江湖術語 +江苏 江蘇 +江苏人 江蘇人 +江苏南钢 江蘇南鋼 +江苏省 江蘇省 +江苏队 江蘇隊 +江表 江表 +江西师范大学 江西師範大學 +江郎才尽 江郎才盡 +江采苹 江采蘋 +江面 江面 +江面上 江面上 +江韦仑 江韋侖 +池里 池裏 +污水处理厂 污水處理廠 +污蔑 污衊 +汤下面 湯下麪 +汤加里罗 湯加里羅 +汤团 湯糰 +汤姆历险记 湯姆歷險記 +汤姆斯杯 湯姆斯杯 +汤姆汉克 湯姆漢克 +汤姆索亚历险记 湯姆索亞歷險記 +汤杯 湯杯 +汤药 湯藥 +汤里来水里去 湯裏來水裏去 +汤面 湯麪 +汪少杰 汪少傑 +汪曲克 汪曲克 +汲于 汲於 +汲汲于 汲汲於 +汴梁 汴梁 +汹涌 洶涌 +汽电共生系统 汽電共生系統 +汽表 汽表 +汽车厂 汽車廠 +汽轮发电 汽輪發電 +汽轮发电机 汽輪發電機 +沃依采克 沃依采克 +沃兹尼克 沃茲尼克 +沃枝叶不如培根本 沃枝葉不如培根本 +沃达丰 沃達豐 +沃野千里 沃野千里 +沈下 沈下 +沈不住气 沈不住氣 +沈世宏 沈世宏 +沈世朋 沈世朋 +沈丘县 沈丘縣 +沈云英 沈雲英 +沈亚之 沈亞之 +沈从文 沈從文 +沈伟豪 沈偉豪 +沈传芷 沈傳芷 +沈住气 沈住氣 +沈佳颖 沈佳穎 +沈佺期 沈佺期 +沈兼士 沈兼士 +沈冤 沈冤 +沈勇 沈勇 +沈北新 沈北新 +沈北新区 沈北新區 +沈厚 沈厚 +沈吉线 瀋吉線 +沈吟 沈吟 +沈周 沈周 +沈哲鲲 沈哲鯤 +沈国放 沈國放 +沈塔尼 沈塔尼 +沈复 沈復 +沈天俞 沈天俞 +沈如峰 沈如峯 +沈家诚 沈家誠 +沈寂 沈寂 +沈富雄 沈富雄 +沈尹默 沈尹默 +沈山线 瀋山線 +沈崇诲 沈崇誨 +沈州 瀋州 +沈庆京 沈慶京 +沈庆光 沈慶光 +沈建宏 沈建宏 +沈得住气 沈得住氣 +沈思 沈思 +沈思往事 沈思往事 +沈惠珍 沈惠珍 +沈括 沈括 +沈振来 沈振來 +沈文台 沈文臺 +沈文成 沈文成 +沈明伦 沈明倫 +沈春华 沈春華 +沈昭铭 沈昭銘 +沈柏苍 沈柏蒼 +沈殿霞 沈殿霞 +沈水 瀋水 +沈没 沈沒 +沈沦 沈淪 +沈河 瀋河 +沈河区 瀋河區 +沈浊 沈濁 +沈浮 沈浮 +沈海 瀋海 +沈海铁路 瀋海鐵路 +沈浸 沈浸 +沈淀 沈澱 +沈淀出来 沈澱出來 +沈淀法 沈澱法 +沈淀物 沈澱物 +沈淑敏 沈淑敏 +沈湎 沈湎 +沈湎酒色 沈湎酒色 +沈溺 沈溺 +沈滞 沈滯 +沈滞性 沈滯性 +沈玉琳 沈玉琳 +沈璀庭 沈璀庭 +沈甸甸 沈甸甸 +沈疔 沈疔 +沈疴 沈痾 +沈痛 沈痛 +沈痼 沈痼 +沈睡 沈睡 +沈睡不醒 沈睡不醒 +沈砂池 沈砂池 +沈积 沈積 +沈积岩 沈積岩 +沈积石 沈積石 +沈筒 沈筒 +沈筱珺 沈筱珺 +沈约 沈約 +沈继昌 沈繼昌 +沈腰 沈腰 +沈腰潘鬓 沈腰潘鬢 +沈船 沈船 +沈落 沈落 +沈葆桢 沈葆楨 +沈葆祯 沈葆禎 +沈诗钧 沈詩鈞 +沈迷 沈迷 +沈迷不醒 沈迷不醒 +沈郁 沈鬱 +沈醉 沈醉 +沈重 沈重 +沈钧启 沈鈞啓 +沈钰杰 沈鈺傑 +沈闷 沈悶 +沈阳 瀋陽 +沈阳市 瀋陽市 +沈阳师范 瀋陽師範 +沈阳师范大学 瀋陽師範大學 +沈降 沈降 +沈陷 沈陷 +沈雁冰 沈雁冰 +沈雨庭 沈雨庭 +沈静 沈靜 +沈静下来 沈靜下來 +沈靳扬 沈靳揚 +沈香 沈香 +沈鱼落雁 沈魚落雁 +沈默 沈默 +沈默不语 沈默不語 +沈默寡言 沈默寡言 +沉浸于 沉浸於 +沉淀 沉澱 +沉淀剂 沉澱劑 +沉淀法 沉澱法 +沉淀物 沉澱物 +沉渐刚克 沉漸剛克 +沉湎于 沉湎於 +沉溺于 沉溺於 +沉潜刚克 沉潛剛克 +沉积岩 沉積岩 +沉迷于 沉迷於 +沉郁 沉鬱 +沉郁顿挫 沉鬱頓挫 +沉醉于 沉醉於 +沉陷于 沉陷於 +沓合 沓合 +沙仑 沙崙 +沙依巴克 沙依巴克 +沙依巴克区 沙依巴克區 +沙克 沙克 +沙克疫苗 沙克疫苗 +沙参 沙蔘 +沙发 沙發 +沙发垫 沙發墊 +沙发椅 沙發椅 +沙发牀 沙發牀 +沙坑杆 沙坑桿 +沙岩 沙岩 +沙弥 沙彌 +沙弥戒 沙彌戒 +沙洛培克 沙洛培克 +沙漠生态系 沙漠生態系 +沙茶面 沙茶麪 +沙里夫 沙里夫 +沙里淘金 沙裏淘金 +沙雕 沙雕 +沙雕赛 沙雕賽 +沟谷 溝谷 +没个出豁 沒個出豁 +没个好结果 沒個好結果 +没个开交 沒個開交 +没个是处 沒個是處 +没个见识 沒個見識 +没个里儿表儿 沒個裏兒表兒 +没个黑家白日的 沒個黑家白日的 +没乱里 沒亂裏 +没了 沒了 +没了主意 沒了主意 +没了当 沒了當 +没了指望 沒了指望 +没了期 沒了期 +没了法 沒了法 +没了王的蜜蜂 沒了王的蜜蜂 +没了经纬 沒了經緯 +没了结处 沒了結處 +没了落 沒了落 +没事干 沒事幹 +没人烟 沒人煙 +没件好当眼的 沒件好當眼的 +没体面 沒體面 +没借 沒借 +没做摆布 沒做擺佈 +没关系 沒關係 +没准 沒準 +没准儿 沒準兒 +没准头 沒準頭 +没出 沒出 +没出去 沒出去 +没出息 沒出息 +没出来 沒出來 +没出豁 沒出豁 +没出豁处 沒出豁處 +没出长 沒出長 +没升 沒升 +没合杀 沒合殺 +没合煞 沒合煞 +没合眼儿 沒合眼兒 +没后 沒後 +没后跟 沒後跟 +没向 沒向 +没回 沒回 +没回去 沒回去 +没回来 沒回來 +没地里 沒地里 +没地里巡检 沒地裏巡檢 +没处出豁 沒處出豁 +没头当 沒頭當 +没完没了 沒完沒了 +没家亲引不出外鬼来 沒家親引不出外鬼來 +没干 沒幹 +没干头 沒幹頭 +没干没净 沒乾沒淨 +没干没淨 沒乾沒淨 +没干淨 沒幹淨 +没幸 沒幸 +没张没致 沒張沒致 +没当 沒當 +没当家花花 沒當家花花 +没志气 沒志氣 +没志行 沒志行 +没念 沒念 +没意志 沒意志 +没折至 沒摺至 +没担当 沒擔當 +没摆布处 沒擺佈處 +没有人烟 沒有人煙 +没有准儿 沒有準兒 +没有差别 沒有差別 +没有来历 沒有來歷 +没有联系 沒有聯繫 +没有面目 沒有面目 +没来历 沒來歷 +没来回 沒來回 +没松下 沒鬆下 +没极奈何 沒極奈何 +没样范 沒樣範 +没梁桶 沒梁桶 +没梢干 沒梢幹 +没爷娘的祖宗 沒爺孃的祖宗 +没爹没娘 沒爹沒孃 +没种 沒種 +没签 沒簽 +没算当 沒算當 +没精打彩 沒精打彩 +没精打采 沒精打采 +没脊梁 沒脊樑 +没脸面 沒臉面 +没药 沒藥 +没见世面 沒見世面 +没见食面 沒見食面 +没讨个空处 沒討個空處 +没说出 沒說出 +没造化的种子 沒造化的種子 +没采 沒采 +没里没外 沒裏沒外 +没量斗 沒量斗 +没雕当 沒雕當 +没面子 沒面子 +没面皮 沒面皮 +没面目 沒面目 +沤郁 漚鬱 +沥干 瀝乾 +沥血之仇 瀝血之仇 +沦于 淪於 +沮舍 沮舍 +河不出图 河不出圖 +河升镇 河昇鎮 +河岳 河嶽 +河干 河干 +河曲 河曲 +河曲县 河曲縣 +河曲智叟 河曲智叟 +河梁 河梁 +河流汇集 河流匯集 +河涸海干 河涸海乾 +河系 河系 +河落海干 河落海乾 +河谷 河谷 +河里 河裏 +河里孩儿岸上娘 河裏孩兒岸上孃 +河里淹死是会水的 河裏淹死是會水的 +河面 河面 +河面上 河面上 +油价 油價 +油伙儿 油夥兒 +油光满面 油光滿面 +油厂 油廠 +油回磨转 油回磨轉 +油头粉面 油頭粉面 +油尽灯枯 油盡燈枯 +油布 油布 +油当量 油當量 +油彩 油彩 +油斗 油鬥 +油杯 油杯 +油松 油松 +油漆未干 油漆未乾 +油烟 油煙 +油烟机 油煙機 +油电混合车 油電混合車 +油花卜 油花卜 +油茶面儿 油茶麪兒 +油表 油表 +油里滑 油裏滑 +油面 油麪 +油页岩 油頁岩 +治了 治了 +治愈 治癒 +治术 治術 +治疗炎症 治療炎症 +沽名吊誉 沽名吊譽 +沽名干誉 沽名干譽 +沽酒当炉 沽酒當爐 +沾染控制 沾染控制 +沿才授职 沿才授職 +沿门托钵 沿門托鉢 +沿门挨户 沿門挨戶 +泄了 泄了 +泄出 泄出 +泄出去 泄出去 +泄出来 泄出來 +泄欲 泄慾 +泅游 泅遊 +泉台 泉臺 +泉州师范学院 泉州師範學院 +泊松 泊松 +泊松分布 泊松分佈 +泌尿系统 泌尿系統 +泌水乐饥 泌水樂飢 +泐复 泐覆 +泐布 泐布 +法书要录 法書要錄 +法云 法雲 +法云地 法雲地 +法克斯 法克斯 +法兰克 法蘭克 +法兰克林 法蘭克林 +法兰克欧兹 法蘭克歐茲 +法兰克福 法蘭克福 +法兰克福学派 法蘭克福學派 +法兰克福汇报 法蘭克福匯報 +法兰克福证券交易所 法蘭克福證券交易所 +法兰克福车展 法蘭克福車展 +法出多门 法出多門 +法制 法制 +法制办公室 法制辦公室 +法制化 法制化 +法制局 法制局 +法制日报 法制日報 +法占 法佔 +法台 法臺 +法向量 法向量 +法因于敝而成于过 法因於敝而成於過 +法国共产党 法國共產黨 +法国杯 法國杯 +法坛 法壇 +法定准备率 法定準備率 +法布尔 法布爾 +法布施 法佈施 +法律制裁 法律制裁 +法律学系 法律學系 +法律系 法律系 +法律规范 法律規範 +法律面 法律面 +法念 法念 +法拉托 法拉托 +法文系 法文系 +法曲 法曲 +法术 法術 +法术无边 法術無邊 +法柜奇兵 法櫃奇兵 +法系 法系 +法西斯党 法西斯黨 +法语系 法語系 +法身舍利 法身舍利 +法鲁克 法魯克 +泛了 泛了 +泛亚 泛亞 +泛亮 泛亮 +泛光 泛光 +泛出 泛出 +泛出来 泛出來 +泛函 泛函 +泛动 泛動 +泛化 泛化 +泛区 泛區 +泛听 泛聽 +泛味 泛味 +泛地 泛地 +泛型 泛型 +泛微 泛微 +泛指 泛指 +泛欧 泛歐 +泛水 泛水 +泛水凌山 汎水淩山 +泛池 泛池 +泛油 泛油 +泛泛 泛泛 +泛泰 泛泰 +泛海 泛海 +泛游 泛遊 +泛湖 泛湖 +泛滥 氾濫 +泛点 泛點 +泛爱 泛愛 +泛现 泛現 +泛珠 泛珠 +泛白 泛白 +泛的 泛的 +泛着 泛着 +泛称 泛稱 +泛素 泛素 +泛红 泛紅 +泛绿 泛綠 +泛美 泛美 +泛舟 泛舟 +泛蓝 泛藍 +泛览 泛覽 +泛论 泛論 +泛读 泛讀 +泛谈 泛談 +泛起 泛起 +泛酸 泛酸 +泛青 泛青 +泛非 泛非 +泛音 泛音 +泛黄 泛黃 +泡出 泡出 +泡制 泡製 +泡杯 泡杯 +泡沫发胶 泡沫髮膠 +泡面 泡麪 +泡面哲学 泡麪哲學 +泡面市场 泡麪市場 +波光荡漾 波光盪漾 +波克夏 波克夏 +波克特 波克特 +波兰舞曲 波蘭舞曲 +波勒瓦裘克 波勒瓦裘克 +波发藻 波髮藻 +波哥里卡 波哥里卡 +波塞冬 波塞冬 +波士顿艺术博物馆 波士頓藝術博物館 +波多马克河 波多馬克河 +波娘 波娘 +波尔卡舞曲 波爾卡舞曲 +波尔布特 波爾布特 +波尔干 波爾干 +波尔干地区 波爾干地區 +波布政权 波布政權 +波布那 波布那 +波布那共和国 波布那共和國 +波形板 波形板 +波托马克河 波托馬克河 +波折 波折 +波拉克 波拉克 +波斯里亚 波斯里亞 +波来克 波來克 +波杰曼 波傑曼 +波棱菜 波棱菜 +波洛克 波洛克 +波浪周期 波浪週期 +波涌云乱 波涌雲亂 +波状云 波狀雲 +波荡 波盪 +波诡云谲 波詭雲譎 +波谲云诡 波譎雲詭 +波谷 波谷 +波里 波里 +波里尼西亚 波里尼西亞 +波里尼西亚人 波里尼西亞人 +波里斯 波里斯 +波雷罗舞曲 波雷羅舞曲 +波面 波面 +泣别 泣別 +泣别虞姬 泣別虞姬 +泥于 泥於 +泥土面 泥土面 +泥塑木雕 泥塑木雕 +泥封函谷 泥封函谷 +泥岩 泥岩 +泥板 泥板 +泥板岩 泥板岩 +泥涂 泥塗 +泥灰岩 泥灰岩 +泥质岩 泥質岩 +泥质页岩 泥質頁岩 +泥铲 泥鏟 +注上 註上 +注中奖 注中獎 +注云 注云 +注儿 注兒 +注入 注入 +注入式敎学法 注入式教學法 +注冊 註冊 +注冊主任 註冊主任 +注冊单 註冊單 +注冊商标 註冊商標 +注冊手续 註冊手續 +注冊日 註冊日 +注冊用户 注冊用戶 +注冊码 註冊碼 +注冊组 註冊組 +注冊费 註冊費 +注册 註冊 +注册人 註冊人 +注册商标 註冊商標 +注册表 註冊表 +注到 注到 +注名 註名 +注塑 注塑 +注失 註失 +注子 注子 +注定 註定 +注射 注射 +注射剂 注射劑 +注射器 注射器 +注射液 注射液 +注射筒 注射筒 +注射针 注射針 +注射针头 注射針頭 +注心 注心 +注意 注意 +注意事项 注意事項 +注意到 注意到 +注意力 注意力 +注意力缺陷过动症 注意力缺陷過動症 +注意看 注意看 +注慕 注慕 +注批 註批 +注文 註文 +注明 註明 +注本 注本 +注标 註標 +注水 注水 +注油 注油 +注消 注消 +注满 注滿 +注生娘娘 註生娘娘 +注疏 註疏 +注目 注目 +注目礼 注目禮 +注脚 註腳 +注色 注色 +注视 注視 +注解 註解 +注记 註記 +注译 註譯 +注资 注資 +注释 註釋 +注重 注重 +注销 註銷 +注音 註音 +注音一式 注音一式 +注音字母 注音字母 +注音文 注音文 +注音法 註音法 +注音符号 注音符號 +泪出痛肠 淚出痛腸 +泪如泉涌 淚如泉涌 +泪容满面 淚容滿面 +泪干 淚乾 +泪干肠断 淚乾腸斷 +泪流满面 淚流滿面 +泪眼蒙眬 淚眼矇矓 +泪蜡 淚蠟 +泫然欲泣 泫然欲泣 +泫然欲泪 泫然欲淚 +泰克 泰克 +泰勒制度 泰勒制度 +泰国新娘 泰國新娘 +泰坛 泰壇 +泰坦尼克号 泰坦尼克號 +泰山北斗 泰山北斗 +泰山娘娘 泰山娘娘 +泰山梁木 泰山梁木 +泰斗 泰斗 +泰极而否 泰極而否 +泰极还生否乐处又逢悲 泰極還生否樂處又逢悲 +泰爱泰党 泰愛泰黨 +泰特斯安德洛尼克斯 泰特斯安德洛尼克斯 +泰特美术馆 泰特美術館 +泰系各族 泰系各族 +泱郁 泱鬱 +泳坛 泳壇 +泳气钟 泳氣鐘 +泻了 瀉了 +泻出 瀉出 +泻出去 瀉出去 +泻出来 瀉出來 +泻药 瀉藥 +泼出 潑出 +泼出去 潑出去 +泼出来 潑出來 +泼出胆子 潑出膽子 +泼才 潑才 +泼毛团 潑毛團 +泼烟花 潑煙花 +泼脏水 潑髒水 +泼花团 潑花團 +泼贱烟花 潑賤煙花 +泽卤 澤鹵 +泽当 澤當 +泽当镇 澤當鎮 +泽梁 澤梁 +泽渗漓而下降 澤滲灕而下降 +泽里可 澤里可 +洁面乳 潔面乳 +洁面露 潔面露 +洄暗 洄闇 +洄游 洄游 +洋参 洋蔘 +洋场恶少 洋場惡少 +洋布 洋布 +洋干漆 洋乾漆 +洋浦经济开发区 洋浦經濟開發區 +洋烟 洋菸 +洋相尽出 洋相盡出 +洋相百出 洋相百出 +洋紫苏 洋紫蘇 +洋药 洋藥 +洋蜡 洋蠟 +洋面 洋麪 +洒出 灑出 +洒向 灑向 +洒扫 灑掃 +洒水 灑水 +洒洒 灑灑 +洒涤 灑滌 +洒淅 灑淅 +洒濯 灑濯 +洒然 灑然 +洒脱 灑脫 +洗了 洗了 +洗冤集录 洗冤集錄 +洗出 洗出 +洗出作用 洗出作用 +洗出去 洗出去 +洗出来 洗出來 +洗刮 洗刮 +洗发 洗髮 +洗发乳 洗髮乳 +洗发剂 洗髮劑 +洗发动机 洗發動機 +洗发水 洗髮水 +洗发水儿 洗髮水兒 +洗发皂 洗髮皂 +洗发粉 洗髮粉 +洗发精 洗髮精 +洗发膏 洗髮膏 +洗发露 洗髮露 +洗地板 洗地板 +洗头发 洗頭髮 +洗干淨 洗乾淨 +洗心革志 洗心革志 +洗心革面 洗心革面 +洗手不干 洗手不幹 +洗手台 洗手檯 +洗炼 洗煉 +洗练 洗練 +洗脑术 洗腦術 +洗脸台 洗臉檯 +洗荡 洗盪 +洗衣板 洗衣板 +洗钱防制法 洗錢防制法 +洗面 洗面 +洗面乳 洗面乳 +洗面奶 洗面奶 +洗面皂 洗面皂 +洗面革心 洗面革心 +洛克 洛克 +洛克希德 洛克希德 +洛克希德马丁 洛克希德馬丁 +洛克斐勒 洛克斐勒 +洛克比 洛克比 +洛克菲勒 洛克菲勒 +洛克西德 洛克西德 +洛党 洛黨 +洛可可美术 洛可可美術 +洛扎 洛扎 +洛扎县 洛扎縣 +洛皮塔瀑布 洛皮塔瀑布 +洛迪克 洛迪克 +洛钟东应 洛鐘東應 +洛阳师范学院 洛陽師範學院 +洛阳才子 洛陽才子 +洞察其奸 洞察其奸 +洞山良价 洞山良价 +洞烛其奸 洞燭其奸 +洞烛奸邪 洞燭奸邪 +洞窟美术 洞窟美術 +洞见症结 洞見癥結 +洞鉴 洞鑑 +洞鉴古今 洞鑑古今 +津发 津發 +津巴布韦 津巴布韋 +津梁 津樑 +津贴制度 津貼制度 +洪万春 洪萬春 +洪信杰 洪信傑 +洪升 洪昇 +洪士杰 洪士杰 +洪复 洪覆 +洪宪帝制 洪憲帝制 +洪志善 洪志善 +洪志宏 洪志宏 +洪杰鸿 洪傑鴻 +洪泛 洪泛 +洪炉燎发 洪爐燎髮 +洪胄 洪胄 +洪若朴 洪若樸 +洪范 洪範 +洪适 洪适 +洪都百炼生 洪都百鍊生 +洪钟 洪鐘 +洲际杯 洲際盃 +洴澼药 洴澼藥 +活不了 活不了 +活了 活了 +活体肝脏移植 活體肝臟移植 +活出 活出 +活动于 活動於 +活动曲尺 活動曲尺 +活动桌面 活動桌面 +活动看板 活動看板 +活动范围 活動範圍 +活厂经营 活廠經營 +活塞杆 活塞桿 +活干 活幹 +活扣 活釦 +活泛 活泛 +活饥荒 活饑荒 +派克 派克 +派克大衣 派克大衣 +派克斯顿 派克斯頓 +派出 派出 +派出去 派出去 +派出所 派出所 +派出机关 派出機關 +派出来 派出來 +派别 派別 +派发 派發 +派团 派團 +派团参加 派團參加 +派屈克 派屈克 +派屈克节 派屈克節 +派崔克 派崔克 +派彩 派彩 +派拉蒙 派拉蒙 +派拉蒙影 派拉蒙影 +派系 派系 +派翠克 派翠克 +流个 流個 +流了 流了 +流于 流於 +流于形式 流於形式 +流云 流雲 +流亡曲 流亡曲 +流传于世 流傳於世 +流传后世 流傳後世 +流体冶金术 流體冶金術 +流出 流出 +流出去 流出去 +流出来 流出來 +流别 流別 +流向 流向 +流回 流回 +流回去 流回去 +流回来 流回來 +流域面积 流域面積 +流尽 流盡 +流布 流佈 +流干 流乾 +流当 流當 +流当品 流當品 +流征 流徵 +流氓集团 流氓集團 +流水板 流水板 +流水行云 流水行雲 +流泄出来 流泄出來 +流泛 流泛 +流注 流注 +流涎症 流涎症 +流理台 流理臺 +流离遇合 流離遇合 +流程表 流程表 +流纹岩 流紋岩 +流芳后世 流芳後世 +流苏 流蘇 +流苏帐 流蘇帳 +流苏髻 流蘇髻 +流荡 流蕩 +流荡忘反 流蕩忘反 +流血冲突 流血衝突 +流血千里 流血千里 +流血浮尸 流血浮尸 +流血漂卤 流血漂鹵 +流行于 流行於 +流行曲 流行曲 +流行歌曲 流行歌曲 +流行症 流行症 +流觞曲水 流觴曲水 +流通出来 流通出來 +流里流气 流裏流氣 +流量表 流量表 +流露出 流露出 +流露出来 流露出來 +流风余 流風餘 +流风余俗 流風餘俗 +流风余韵 流風餘韻 +流风回雪 流風迴雪 +流鱼出听 流魚出聽 +浅尝 淺嘗 淺嚐 +浅尝则止 淺嘗則止 +浅尝者 淺嘗者 +浅尝辄止 淺嘗輒止 +浅浮雕 淺浮雕 +浅淀 淺澱 +浅雕 淺雕 +浇制 澆製 +浇注 澆注 +浇漓 澆漓 +浇筑 澆築 +浊积岩 濁積岩 +浊臭熏天 濁臭熏天 +测不准 測不準 +测光表 測光表 +测出 測出 +测出来 測出來 +测地曲率 測地曲率 +测地线曲率 測地線曲率 +测录到 測錄到 +测量出 測量出 +测量术 測量術 +测量杆 測量桿 +测量范围 測量範圍 +测验卷 測驗卷 +济世之才 濟世之才 +济困 濟困 +济困扶危 濟困扶危 +济州特别自治道 濟州特別自治道 +济恶 濟惡 +济胜之具 濟勝之具 +浑个 渾個 +浑仪注 渾儀註 +浑朴 渾樸 +浑朴自然 渾樸自然 +浑身发抖 渾身發抖 +浑身发软 渾身發軟 +浑闲事 渾閒事 +浓于 濃於 +浓云 濃雲 +浓云密布 濃雲密佈 +浓发 濃髮 +浓墨重彩 濃墨重彩 +浓暗 濃暗 +浓淡适中 濃淡適中 +浓烟 濃煙 +浓血症 濃血症 +浓郁 濃郁 +浓雾密布 濃霧密佈 +浙江天台县 浙江天台縣 +浙江师范大学 浙江師範大學 +浥注 浥注 +浦发 浦發 +浩克 浩克 +浩叹 浩嘆 +浩如烟气 浩如煙氣 +浩如烟海 浩如煙海 +浩浩荡荡 浩浩蕩蕩 +浩荡 浩蕩 +浪冲 浪衝 +浪又冲 浪又衝 +浪子回头 浪子回頭 +浪子回头金不换 浪子回頭金不換 +浪板 浪板 +浪游 浪遊 +浪漫曲 浪漫曲 +浪琴表 浪琴錶 +浪荡 浪蕩 +浪荡乾坤 浪蕩乾坤 +浪荡子 浪蕩子 +浪荡子式 浪蕩子式 +浪荡灯 浪蕩燈 +浪蝶游蜂 浪蝶游蜂 +浮于 浮於 +浮云 浮雲 +浮云富贵 浮雲富貴 +浮云惊龙 浮雲驚龍 +浮云朝露 浮雲朝露 +浮云游子 浮雲遊子 +浮云翳日 浮雲翳日 +浮云蔽日 浮雲蔽日 +浮借 浮借 +浮出 浮出 +浮动汇率 浮動匯率 +浮台 浮臺 +浮吊 浮吊 +浮夸 浮誇 +浮尸 浮屍 +浮托 浮托 +浮松 浮鬆 +浮梁 浮樑 +浮梁县 浮樑縣 +浮沈 浮沈 +浮泛 浮泛 +浮游 浮游 +浮游动物 浮游動物 +浮游植物 浮游植物 +浮游生物 浮游生物 +浮现出 浮現出 +浮现出来 浮現出來 +浮签 浮簽 +浮荡 浮蕩 +浮词曲说 浮詞曲說 +浮雕 浮雕 +浮雕像 浮雕像 +浮雕墙纸 浮雕牆紙 +浮面 浮面 +浴帘 浴簾 +海上台风警报 海上颱風警報 +海上布雷 海上佈雷 +海上游 海上游 +海上采油 海上採油 +海丰 海豐 +海丰县 海豐縣 +海于格松 海于格松 +海参 海蔘 +海参威 海參威 +海参崴 海參崴 +海台 海臺 +海国图志 海國圖志 +海埔姜 海埔姜 +海宇升平 海宇昇平 +海尔布隆 海爾布隆 +海岳名言 海岳名言 +海带卷 海帶卷 +海干 海乾 +海干河尽 海乾河盡 +海平面 海平面 +海底峡谷 海底峽谷 +海底捞针 海底撈針 +海德里 海德里 +海曲 海曲 +海松 海松 +海水不可斗量 海水不可斗量 +海水淡化厂 海水淡化廠 +海水面 海水面 +海洋开发 海洋開發 +海洋温差发电 海洋溫差發電 +海洋生态系 海洋生態系 +海涂 海塗 +海涂围垦 海塗圍墾 +海淀 海淀 +海淀区 海淀區 +海淀图书城 海淀圖書城 +海湾合作理事会 海灣合作理事會 +海湾布雷 海灣佈雷 +海漂杯 海漂杯 +海百合 海百合 +海立云垂 海立雲垂 +海苔 海苔 +海蒂克伦 海蒂克倫 +海藻虫 海藻蟲 +海蚀台地 海蝕臺地 +海蚀平台 海蝕平臺 +海表 海表 +海西蒙古族藏族自治州 海西蒙古族藏族自治州 +海谷 海谷 +海里 海里 海裏 +海里海外 海裏海外 +海面 海面 +海马回 海馬迴 +海鲜面 海鮮麪 +海默症 海默症 +浸于 浸於 +浸制 浸製 +浸种 浸種 +涂上 塗上 +涂乙 塗乙 +涂了 塗了 +涂写 塗寫 +涂到 塗到 +涂刷 塗刷 +涂去 塗去 +涂善妮 涂善妮 +涂在 塗在 +涂地 塗地 +涂坤 涂坤 +涂壮勋 涂壯勳 +涂壯勳 涂壯勳 +涂天相 涂天相 +涂好 塗好 +涂姓 涂姓 +涂姓技士 塗姓技士 +涂尔干 涂爾干 +涂层 塗層 +涂居贤 涂居賢 +涂山 塗山 +涂布 塗布 +涂序瑄 涂序瑄 +涂惠元 涂惠元 +涂惠源 涂惠源 +涂惠源雨 涂惠源雨 +涂成 塗成 +涂抹 塗抹 +涂抹诗书 塗抹詩書 +涂掉 塗掉 +涂改 塗改 +涂改无效 塗改無效 +涂敏恆 涂敏恆 +涂敏恒 涂敏恆 +涂文生 涂文生 +涂料 塗料 +涂月 涂月 +涂有 塗有 +涂来涂去 塗來塗去 +涂永辉 涂永輝 +涂油 塗油 +涂油于 塗油於 +涂泽 塗澤 +涂泽民 涂澤民 +涂浆台 塗漿檯 +涂涂 塗塗 +涂涂改改 塗塗改改 +涂消 塗消 +涂满 塗滿 +涂漆 塗漆 +涂潦 塗潦 +涂潭 塗潭 +涂澤民 涂澤民 +涂炭 塗炭 +涂炭生民 塗炭生民 +涂炭生灵 塗炭生靈 +涂着 塗着 +涂窜 塗竄 +涂粉 塗粉 +涂绍煃 涂紹煃 +涂美伦 涂美倫 +涂羽卿 涂羽卿 +涂胶 塗膠 +涂脂抹粉 塗脂抹粉 +涂色 塗色 +涂色板 塗色板 +涂药 塗藥 +涂蜡 塗蠟 +涂謹申 涂謹申 +涂说 塗說 +涂谨申 涂謹申 +涂过 塗過 +涂过去 塗過去 +涂过来 塗過來 +涂逢年 涂逢年 +涂醒哲 涂醒哲 +涂金 塗金 +涂销 塗銷 +涂長望 涂長望 +涂长望 涂長望 +涂附 塗附 +涂饰 塗飾 +涂饰剂 塗飾劑 +涂鴻欽 涂鴻欽 +涂鸦 塗鴉 +涂鸦区 塗鴉區 +涂鸭 塗鴨 +涂鸿钦 涂鴻欽 +涂黑 塗黑 +涅面 涅面 +消了 消了 +消化系统 消化系統 +消噪 消噪 +消息面 消息面 +消折 消折 +消极 消極 +消极性 消極性 +消极论 消極論 +消毒药 消毒藥 +消毒药水 消毒藥水 +消沈 消沈 +消渴症 消渴症 +消灭殆尽 消滅殆盡 +消灭淨尽 消滅淨盡 +消炎药 消炎藥 +消肿药 消腫藥 +消费价格指数 消費價格指數 +消费借贷 消費借貸 +消费合作社 消費合作社 +消费欲 消費慾 +消闲儿 消閒兒 +涉历 涉歷 +涉台 涉臺 +涉谷 涉谷 +涉足于 涉足於 +涌出 涌出 +涌出去 涌出去 +涌出来 涌出來 +涌升流 涌升流 +涌向 涌向 +涌来 涌來 +涌现 涌現 +涌现出 涌現出 +涌进 涌進 +涛生云灭 濤生雲滅 +涡虫 渦蟲 +涡虫纲 渦蟲綱 +涡轮喷气发动机 渦輪噴氣發動機 +涡轮轴发动机 渦輪軸發動機 +涤尽 滌盡 +涤瑕荡垢 滌瑕盪垢 +涤瑕荡秽 滌瑕盪穢 +涤秽荡瑕 滌穢盪瑕 +涤荡 滌盪 +涤面 滌面 +润发 潤髮 +涨了 漲了 +涨价 漲價 +涨价归公 漲價歸公 +涨停板 漲停板 +涨出 漲出 +涨升 漲升 +涨回 漲回 +涨回去 漲回去 +涨回来 漲回來 +涨红了脸 漲紅了臉 +涨跌幅限制 漲跌幅限制 +涨过了头 漲過了頭 +涩谷 澀谷 +液体燃料火箭发动机 液體燃料火箭發動機 +液压千斤顶 液壓千斤頂 +液晶板 液晶板 +液晶表 液晶錶 +液面 液麪 +涳蒙 涳濛 +涸干 涸乾 +淀乃不耕之地 澱乃不耕之地 +淀北片 澱北片 +淀山 澱山 +淀山湖 澱山湖 +淀淀 澱澱 +淀积 澱積 +淀积物 澱積物 +淀粉 澱粉 +淀粉样 澱粉樣 +淀粉类 澱粉類 +淀粉类作物 澱粉類作物 +淀粉糖 澱粉糖 +淀粉脢 澱粉脢 +淀粉质 澱粉質 +淀解物 澱解物 +淀谓之滓 澱謂之滓 +淋余土 淋餘土 +淋冲 淋沖 +淋巴系统 淋巴系統 +淋淋漓漓 淋淋漓漓 +淋漓 淋漓 +淋漓尽致 淋漓盡致 +淋漓痛快 淋漓痛快 +淌板船 淌板船 +淑范 淑範 +淑郁 淑郁 +淘尽 淘盡 +淘汰制 淘汰制 +淘闲气 淘閒氣 +淡于 淡於 +淡于名利 淡於名利 +淡入淡出 淡入淡出 +淡出 淡出 +淡出淡入 淡出淡入 +淡彩 淡彩 +淡朱 淡朱 +淡水河系 淡水河系 +淡水生态系 淡水生態系 +淡蒙蒙 淡濛濛 +淨价 淨價 +淨化系统 淨化系統 +淨发 淨髮 +淨尽 淨盡 +淨水厂 淨水廠 +淨流出 淨流出 +淨胜球 淨勝球 +淨面 淨面 +淩云笔 淩雲筆 +淫念 淫念 +淫欲 淫慾 +淫游 淫遊 +淫荡 淫蕩 +淬炼 淬鍊 +淮阴师范学院 淮陰師範學院 +深于 深於 +深仇 深仇 +深仇大恨 深仇大恨 +深入敌后 深入敵後 +深入显出 深入顯出 +深入浅出 深入淺出 +深奸巨猾 深奸巨猾 +深居简出 深居簡出 +深山何处钟 深山何處鐘 +深山出俊鸟 深山出俊鳥 +深山穷谷 深山窮谷 +深念 深念 +深思极虑 深思極慮 +深恶 深惡 +深恶痛嫉 深惡痛嫉 +深恶痛绝 深惡痛絕 +深成岩 深成岩 +深文周纳 深文周納 +深暗 深暗 +深有同感 深有同感 +深沈 深沈 +深沈不露 深沈不露 +深沟墩台 深溝墩臺 +深浅不同 深淺不同 +深海烟囱 深海煙囪 +深渊里 深淵裏 +深秋 深秋 +深色系列 深色系列 +深获 深獲 +深获好评 深獲好評 +深表 深表 +深表同情 深表同情 +深谷 深谷 +淳于 淳于 +淳于意 淳于意 +淳于髡 淳于髡 +淳朴 淳樸 +混了 混了 +混出 混出 +混出去 混出去 +混出来 混出來 +混合 混合 +混合体 混合體 +混合使用 混合使用 +混合列车 混合列車 +混合动力车 混合動力車 +混合器 混合器 +混合型 混合型 +混合失语症 混合失語症 +混合层 混合層 +混合式 混合式 +混合性 混合性 +混合感染 混合感染 +混合林 混合林 +混合模型 混合模型 +混合毒剂 混合毒劑 +混合比 混合比 +混合法 混合法 +混合泳 混合泳 +混合物 混合物 +混合疫苗 混合疫苗 +混合组 混合組 +混合经济 混合經濟 +混合结构 混合結構 +混合肥料 混合肥料 +混合裁判 混合裁判 +混合语 混合語 +混合齿列 混合齒列 +混同 混同 +混同江 混同江 +混炼 混煉 +混虫 混蟲 +混血种 混血種 +添个 添個 +添了 添了 +添枝加叶 添枝加葉 +添枝接叶 添枝接葉 +清丰 清豐 +清丰县 清豐縣 +清了 清了 +清修 清修 +清党 清黨 +清党行动 清黨行動 +清出 清出 +清发 清發 +清台 清檯 +清实录 清實錄 +清异录 清異錄 +清心寡欲 清心寡慾 +清晨杯 清晨盃 +清杆运动 清桿運動 +清査不当党产 清查不當黨產 +清水下杂面 清水下雜麪 +清水烟 清水煙 +清汤挂面 清湯掛麪 +清浊同流 清濁同流 +清算斗争 清算鬥爭 +清胄 清胄 +清芬志 清芬志 +清贫寡欲 清貧寡欲 +清酒红人面财帛动人心 清酒紅人面財帛動人心 +清闲自在 清閒自在 +清静寡欲 清靜寡欲 +渊淳岳峙 淵淳嶽峙 +渊源录 淵源錄 +渊谷 淵谷 +渊鉴类函 淵鑑類函 +渍已干 漬已乾 +渐升 漸升 +渐暗 漸暗 +渔娘 漁娘 +渔梁 漁梁 +渔民团体 漁民團體 +渔获 漁獲 +渔获量 漁獲量 +渗出 滲出 +渗出来 滲出來 +渗出物 滲出物 +渗出量 滲出量 +渠冲 渠衝 +渡了 渡了 +渡假胜地 渡假勝地 +渡头云 渡頭雲 +渡海小轮 渡海小輪 +渡轮 渡輪 +渥兹尼克 渥茲尼克 +温克 溫克 +温卷 溫卷 +温州师范学院 溫州師範學院 +温布尔登 溫布爾登 +温布尔登网球公开赛 溫布爾登網球公開賽 +温布尔顿 溫布爾頓 +温布敦 溫布敦 +温布里 溫布里 +温布顿 溫布頓 +温布顿赛 溫布頓賽 +温度范围 溫度範圍 +温度表 溫度表 +温得和克 溫得和克 +温祥云 溫祥雲 +港制 港製 +港制品 港製品 +港台 港臺 +港台地区 港臺地區 +港澳台 港澳臺 +渴了 渴了 +渴念 渴念 +渴欲 渴欲 +渴饮饥餐 渴飲飢餐 +游上 游上 +游上去 游上去 +游上来 游上來 +游下 游下 +游下去 游下去 +游下来 游下來 +游丝 遊絲 +游丝飞絮 遊絲飛絮 +游中国 遊中國 +游乃海 游乃海 +游乐 遊樂 +游乐区 遊樂區 +游乐器 遊樂器 +游乐园 遊樂園 +游乐场 遊樂場 +游乐林 遊樂林 +游乡 遊鄉 +游了 遊了 +游云惊龙 遊雲驚龍 +游亚洲 遊亞洲 +游人 遊人 +游人如织 遊人如織 +游仙 遊仙 +游仙区 遊仙區 +游仙枕 遊仙枕 +游仙窟 遊仙窟 +游仙诗 遊仙詩 +游仪 遊儀 +游伴 遊伴 +游侠 遊俠 +游侠骑士 遊俠騎士 +游僧攒住持 游僧攢住持 +游兴 遊興 +游兴正浓 遊興正濃 +游冶 遊冶 +游出 游出 +游击 遊擊 +游击区 游擊區 +游击战 游擊戰 +游击手 游擊手 +游击队 游擊隊 +游刃 遊刃 +游刃有余 遊刃有餘 +游刃有馀 遊刃有餘 +游到 游到 +游动 遊動 +游勇 遊勇 +游北美 遊北美 +游历 遊歷 +游去 游去 +游台湾 遊臺灣 +游吟诗人 遊吟詩人 +游嘴光棍 遊嘴光棍 +游回 游回 +游回去 游回去 +游回来 游回來 +游园 遊園 +游园会 遊園會 +游园惊梦 遊園驚夢 +游囿伦 游囿倫 +游士 遊士 +游头浪子 遊頭浪子 +游奕 遊奕 +游女 遊女 +游子 遊子 +游子吟 遊子吟 +游子天涯 遊子天涯 +游学 遊學 +游学生 遊學生 +游完 游完 +游客 遊客 +游客如织 遊客如織 +游客止步 遊客止步 +游客量 遊客量 +游宦 遊宦 +游尘 游塵 +游尺 遊尺 +游履 游履 +游山 遊山 +游山玩水 遊山玩水 +游幕 遊幕 +游庠 遊庠 +游廊 遊廊 +游开 遊開 +游弋 遊弋 +游徼 遊徼 +游心寓目 遊心寓目 +游心骋目 遊心騁目 +游必有方 遊必有方 +游志宏 游志宏 +游惰 遊惰 +游憩 遊憩 +游憩区 遊憩區 +游戏 遊戲 +游戏三昧 遊戲三昧 +游戏业 遊戲業 +游戏人间 遊戲人間 +游戏包 遊戲包 +游戏区 遊戲區 +游戏场 遊戲場 +游戏尘寰 遊戲塵寰 +游戏机 遊戲機 +游戏机台 遊戲機檯 +游戏池 遊戲池 +游戏王 遊戲王 +游戏町 遊戲町 +游戏规则 遊戲規則 +游戏设备 遊戲設備 +游戏说 遊戲說 +游手 遊手 +游手人户 遊手人戶 +游手好闲 遊手好閒 +游手恣睢 遊手恣睢 +游手游食 遊手遊食 +游扬 遊揚 +游文宏 游文宏 +游方 遊方 +游日 遊日 +游旧 遊舊 +游明金 游明金 +游易网 遊易網 +游星 遊星 +游春 遊春 +游昭钦 游昭欽 +游来 游來 +游来游去 游來游去 +游标 遊標 +游标位置 遊標位置 +游标卡尺 遊標卡尺 +游欧洲 遊歐洲 +游民 遊民 +游民收容所 遊民收容所 +游民改造 遊民改造 +游气 遊氣 +游水 游水 +游河 遊河 +游泮 游泮 +游泳 游泳 +游泳圈 游泳圈 +游泳池 游泳池 +游泳衣 游泳衣 +游泳裤 游泳褲 +游泳课 游泳課 +游泳赛 游泳賽 +游泳镜 游泳鏡 +游泳队 游泳隊 +游泳馆 游泳館 +游游磨磨儿 遊遊磨磨兒 +游湖 遊湖 +游湖借伞 遊湖借傘 +游澳洲 遊澳洲 +游牧 遊牧 +游牧区 遊牧區 +游牧民族 遊牧民族 +游猎 遊獵 +游玩 遊玩 +游畋 遊畋 +游皓玮 游皓瑋 +游盈隆 游盈隆 +游益网 遊益網 +游目 遊目 +游目骋怀 遊目騁懷 +游离 遊離 +游离份子 遊離份子 +游离层 遊離層 +游离电子 遊離電子 +游离票 遊離票 +游离辐射 遊離輻射 +游禽类 游禽類 +游移 遊移 +游移不决 遊移不決 +游移不定 遊移不定 +游程 遊程 +游网 遊網 +游美洲 遊美洲 +游耕 遊耕 +游船 遊船 +游艇 遊艇 +游艇业 遊艇業 +游艇业者 遊艇業者 +游艺 遊藝 +游艺会 遊藝會 +游艺团 遊藝團 +游艺场 遊藝場 +游花插趣 遊花插趣 +游芳来 游芳來 +游荡 遊蕩 +游荡不归 遊蕩不歸 +游营撞尸 遊營撞屍 +游蜂戏蝶 遊蜂戲蝶 +游蜂浪蝶 遊蜂浪蝶 +游行 遊行 +游行到 遊行到 +游行法 遊行法 +游行示众 遊行示衆 +游行示威 遊行示威 +游衍 遊衍 +游街 遊街 +游街示众 遊街示衆 +游观 遊觀 +游览 遊覽 +游览区 遊覽區 +游览胜地 遊覽勝地 +游览车 遊覽車 +游言 遊言 +游记 遊記 +游说 遊說 +游说团 遊說團 +游说团体 遊說團體 +游谈 遊談 +游谈无根 遊談無根 +游资 遊資 +游资氾滥 遊資氾濫 +游赏 遊賞 +游走 遊走 +游踪 遊蹤 +游轮 遊輪 +游辞 遊辭 +游辞浮说 遊辭浮說 +游过 遊過 +游过去 游過去 +游过来 游過來 +游进 遊進 +游进去 游進去 +游进来 游進來 +游逛 遊逛 +游遍 遊遍 +游错 遊錯 +游锡 遊錫 +游锡坤 游錫坤 +游锡堃 游錫堃 +游锡昆 游錫昆 +游隼 遊隼 +游非洲 遊非洲 +游食 遊食 +游食之民 遊食之民 +游香港 遊香港 +游骑 遊騎 +游骑兵 遊騎兵 +游骑兵队 遊騎兵隊 +游骑无归 遊騎無歸 +游魂 遊魂 +游鱼 游魚 +游鸿儒 游鴻儒 +游鸿明 游鴻明 +游龙 游龍 +游龙戏凤 游龍戲鳳 +游龙荣 游龍榮 +渺无人烟 渺無人煙 +湖北师范学院 湖北師範學院 +湖南师范大学 湖南師範大學 +湖州师范学院 湖州師範學院 +湖里 湖裏 +湖里区 湖裏區 +湖面 湖面 +湘帘 湘簾 +湘累 湘累 +湘绣 湘繡 +湛江师范学院 湛江師範學院 +湟潦生苹 湟潦生苹 +湮灭证据 湮滅證據 +湮郁 湮鬱 +湾里 灣裏 +湾里区 灣裏區 +湿地松 溼地松 +湿度表 溼度表 +湿肉伴干柴 溼肉伴乾柴 +溃于 潰於 +溅了 濺了 +源于 源於 +源汇区 源匯區 +源泉万斛 源泉萬斛 +源自于 源自於 +溜个 溜個 +溜了 溜了 +溜冰团 溜冰團 +溜出 溜出 +溜出去 溜出去 +溜出来 溜出來 +溜回 溜回 +溜回去 溜回去 +溜回来 溜回來 +溜溜秋秋 溜溜秋秋 +溜滑板 溜滑板 +溜须 溜鬚 +溜须拍马 溜鬚拍馬 +溟蒙 溟濛 +溢于 溢於 +溢于言表 溢於言表 +溢价 溢價 +溢出 溢出 +溢出来 溢出來 +溢恶 溢惡 +溥天同庆 溥天同慶 +溪谷 溪谷 +溯游 溯游 +溲面 溲麪 +溶于 溶於 +溶合 溶合 +溶岩 溶岩 +溶岩流 溶岩流 +溶液聚合 溶液聚合 +溶溶荡荡 溶溶蕩蕩 +溺于 溺於 +溺志 溺志 +溺谷 溺谷 +滃郁 滃鬱 +滇西纵谷 滇西縱谷 +滋补药品 滋補藥品 +滑了 滑了 +滑了一跤 滑了一跤 +滑借 滑藉 +滑出 滑出 +滑出跑道 滑出跑道 +滑动面 滑動面 +滑回 滑回 +滑杆 滑桿 +滑板 滑板 +滑板运动 滑板運動 +滑水板 滑水板 +滑铲 滑鏟 +滑雪术 滑雪術 +滑雪板 滑雪板 +滔滔不尽 滔滔不盡 +滔荡 滔蕩 +滚了 滾了 +滚出 滾出 +滚出去 滾出去 +滚出来 滾出來 +滚回 滾回 +滚回去 滾回去 +滚回来 滾回來 +滚彩蛋 滾彩蛋 +滚成一团 滾成一團 +滚杠 滾槓 +滚杯 滾杯 +滚石合唱团 滾石合唱團 +滞后 滯後 +滞后现象 滯後現象 +满了 滿了 +满于 滿於 +满出 滿出 +满出来 滿出來 +满口称赞 滿口稱讚 +满口胡扯 滿口胡扯 +满口胡柴 滿口胡柴 +满口胡言 滿口胡言 +满口胡说 滿口胡說 +满口脏话 滿口髒話 +满场一致 滿場一致 +满坑满谷 滿坑滿谷 +满城尽带黄金甲 滿城盡帶黃金甲 +满堂彩 滿堂彩 +满天星斗 滿天星斗 +满头洋发 滿頭洋髮 +满好个 滿好個 +满山满谷 滿山滿谷 +满布 滿布 +满布疑云 滿佈疑雲 +满当当 滿當當 +满怀心腹事尽在不言中 滿懷心腹事盡在不言中 +满拚自尽 滿拚自盡 +满杯 滿杯 +满洲里 滿洲里 +满洲里市 滿洲裏市 +满满当当 滿滿當當 +满脸溅朱 滿臉濺朱 +满腹才学 滿腹才學 +满腹疑云 滿腹疑雲 +满面 滿面 +满面堆笑 滿面堆笑 +满面怒容 滿面怒容 +满面愁容 滿面愁容 +满面春生 滿面春生 +满面春风 滿面春風 +满面杀气 滿面殺氣 +满面生春 滿面生春 +满面生花 滿面生花 +满面笑容 滿面笑容 +满面红胀 滿面紅脹 +满面羞惭 滿面羞慚 +满面羞愧 滿面羞愧 +满面通红 滿面通紅 +满面雪霜 滿面雪霜 +满面飞红 滿面飛紅 +滤出 濾出 +滤出去 濾出去 +滤出来 濾出來 +滥发 濫發 +滨松市 濱松市 +滩涂 灘塗 +滴了 滴了 +滴了天 滴了天 +滴修都速 滴修都速 +滴出 滴出 +滴出来 滴出來 +滴向 滴向 +滴干 滴乾 +滴水漏斗 滴水漏斗 +滴注 滴注 +滴羞都苏 滴羞都蘇 +滴苏 滴蘇 +滴虫 滴蟲 +滴里嘟噜 滴里嘟嚕 +滴里搭拉 滴里搭拉 +滴里耷拉 滴里耷拉 +漂了 漂了 +漂向 漂向 +漂布 漂布 +漂摆 漂擺 +漂游 漂游 +漂荡 漂盪 +漂荡子弟 漂蕩子弟 +漆了 漆了 +漆出 漆出 +漆器雕 漆器雕 +漆布 漆布 +漆雕 漆雕 +漆黑一团 漆黑一團 +漏了 漏了 +漏了眼 漏了眼 +漏借 漏借 +漏出 漏出 +漏出去 漏出去 +漏出来 漏出來 +漏尽 漏盡 +漏尽更阑 漏盡更闌 +漏尽通 漏盡通 +漏斗 漏斗 +漏斗器 漏斗器 +漏斗状花冠 漏斗狀花冠 +漏斗管 漏斗管 +漏斗胸 漏斗胸 +漏洞百出 漏洞百出 +漏电保护接地极插座 漏電保護接地極插座 +漏网游鱼 漏網游魚 +漏脯充饥 漏脯充飢 +漏针 漏針 +漏面贼 漏面賊 +漓水 灕水 +漓江 灕江 +漓湘 灕湘 +漓漓拉拉 漓漓拉拉 +漓然 灕然 +演个 演個 +演出 演出 +演出人 演出人 +演出地点 演出地點 +演出来 演出來 +演出者 演出者 +演化出 演化出 +演员表 演員表 +演奏出 演奏出 +演奏台 演奏臺 +演奏曲 演奏曲 +演武修文 演武修文 +演绎出 演繹出 +演讲台 演講臺 +漕挽 漕輓 +漫出 漫出 +漫出来 漫出來 +漫卷 漫卷 +漫天叫价 漫天叫價 +漫天开价 漫天開價 +漫天索价 漫天索價 +漫天要价就地还钱 漫天要價就地還錢 +漫天讨价 漫天討價 +漫布 漫布 +漫录 漫錄 +漫无节制 漫無節制 +漫无限制 漫無限制 +漫游 漫遊 +漫游四方 漫遊四方 +漫游费 漫遊費 +漱口杯 漱口杯 +漱口药水 漱口藥水 +漳州师范学院 漳州師範學院 +漾出 漾出 +潇洒 瀟灑 +潘太克斯 潘太克斯 +潘威志 潘威誌 +潘安白发 潘安白髮 +潘岳 潘岳 +潘嶽白发 潘嶽白髮 +潘谷 潘谷 +潘越云 潘越雲 +潘连周 潘連周 +潘金莲给武松敬酒 潘金蓮給武松敬酒 +潘鬓沈腰 潘鬢沈腰 +潜了 潛了 +潜修 潛修 +潜出 潛出 +潜出去 潛出去 +潜出来 潛出來 +潜台词 潛臺詞 +潜回 潛回 +潜意识历程 潛意識歷程 +潜意识里 潛意識裏 +潜水夫症 潛水夫症 +潜水表 潛水錶 +潜水钟 潛水鐘 +潜水钟表 潛水鐘錶 +潜游 潛游 +潜移暗化 潛移暗化 +潜蛟困凤 潛蛟困鳳 +潟卤 潟鹵 +潭祉叶吉 潭祉叶吉 +潭里 潭裏 +潮力发电 潮力發電 +潮烟 潮菸 +澄澹精致 澄澹精致 +澎湖天后宫 澎湖天后宮 +澒蒙 澒濛 +澥谷 澥谷 +澳新军团 澳新軍團 +澳新军团日 澳新軍團日 +澳洲广播电台 澳洲廣播電臺 +澳门汇业 澳門匯業 +澹台 澹臺 +澹台灭明 澹臺滅明 +澹彩 澹彩 +澹泊寡欲 澹泊寡欲 +澹泊明志 澹泊明志 +澹荡 澹盪 +激于 激於 +激于义愤 激於義憤 +激光二极管 激光二極管 +激出 激出 +激发 激發 +激发出 激發出 +激发出来 激發出來 +激发态 激發態 +激发注射 激發注射 +激发起 激發起 +激发起来 激發起來 +激活整合模型 激活整合模型 +激荡 激盪 +激荡不已 激盪不已 +激荡出 激盪出 +激进党 激進黨 +濒临绝种 瀕臨絕種 +濒于 瀕於 +濒于绝境 瀕於絕境 +濒于绝种 瀕於絕種 +濒危物种 瀕危物種 +濒危野生动植物种国际贸易公约 瀕危野生動植物種國際貿易公約 +濠梁 濠梁 +瀍河回族区 瀍河回族區 +瀑布 瀑布 +瀑布区 瀑布區 +瀑布群 瀑布羣 +瀛台 瀛臺 +瀛表 瀛表 +灌个 灌個 +灌了 灌了 +灌于 灌於 +灌云 灌雲 +灌云县 灌雲縣 +灌出 灌出 +灌出去 灌出去 +灌出来 灌出來 +灌制 灌製 +灌向 灌向 +灌回 灌回 +灌回去 灌回去 +灌录 灌錄 +灌注 灌注 +灌注器 灌注器 +灌溉系统 灌溉系統 +灌溉面积 灌溉面積 +灌药 灌藥 +火中取栗 火中取栗 +火了 火了 +火云 火雲 +火云邪 火雲邪 +火力发电 火力發電 +火力发电厂 火力發電廠 +火山岩 火山岩 +火山爆发 火山爆發 +火山爆发指数 火山爆發指數 +火并 火併 +火彩儿 火彩兒 +火成岩 火成岩 +火折子 火摺子 +火斗 火斗 +火杯 火盃 +火柴杆 火柴桿 +火海战术 火海戰術 +火灭烟消 火滅煙消 +火炬松 火炬松 +火烟 火煙 +火烧云 火燒雲 +火症 火症 +火种 火種 +火签 火籤 +火箭发动 火箭發動 +火箭喷射推进系统 火箭噴射推進系統 +火箭布雷 火箭佈雷 +火绳杆 火繩桿 +火耕水种 火耕水種 +火耕流种 火耕流種 +火药 火藥 +火药味 火藥味 +火药味甚浓 火藥味甚濃 +火药库 火藥庫 +火虫儿 火蟲兒 +火里火去水里水去 火裏火去水裏水去 +火里火发 火裏火發 +火里赤 火裏赤 +火链片 火鏈片 +灭不个 滅不個 +灭尸 滅屍 +灭尽 滅盡 +灭种 滅種 +灭种罪 滅種罪 +灭绝种族 滅絕種族 +灭罪修因 滅罪修因 +灭虫剂 滅蟲劑 +灭虫宁 滅蟲寧 +灯台 燈臺 +灯台不照自己 燈臺不照自己 +灯台不自照 燈臺不自照 +灯彩 燈綵 +灯杆 燈杆 +灯杯 燈杯 +灯火万家 燈火萬家 +灯火管制 燈火管制 +灰云 灰雲 +灰发 灰髮 +灰同协 灰同協 +灰头土面 灰頭土面 +灰头草面 灰頭草面 +灰姑娘 灰姑娘 +灰岩残丘 灰巖殘丘 +灰暗 灰暗 +灰灰暗暗 灰灰暗暗 +灰灰蒙蒙 灰灰濛濛 +灰胡 灰鬍 +灰蒙 灰濛 +灰蒙蒙 灰濛濛 +灰面鵟鹰 灰面鵟鷹 +灰飞烟灭 灰飛煙滅 +灵丹圣药 靈丹聖藥 +灵丹妙药 靈丹妙藥 +灵修 靈脩 +灵台 靈臺 +灵台县 靈臺縣 +灵坛 靈壇 +灵欲 靈慾 +灵药 靈藥 +灵谷寺 靈谷寺 +灵迹 靈蹟 +灸术 灸術 +灾后 災後 +灾害链 災害鏈 +灿烂多彩 燦爛多彩 +炆面 炆麪 +炉台 爐臺 +炊烟 炊煙 +炊烟袅袅 炊煙裊裊 +炊臼之戚 炊臼之鏚 +炎日当空 炎日當空 +炎症 炎症 +炎症性 炎症性 +炒栗子 炒栗子 +炒汇 炒匯 +炒菜铲 炒菜鏟 +炒面 炒麪 +炒面块子 炒麪塊子 +炕席 炕蓆 +炕面砖 炕面磚 +炖了 燉了 +炖药 燉藥 +炭水化合物 炭水化合物 +炭疽杆菌 炭疽桿菌 +炮制 炮製 +炮台 炮臺 +炮炼 炮煉 +炳烛夜游 炳燭夜遊 +炸出 炸出 +炸毁 炸燬 +炸药 炸藥 +炸药箱 炸藥箱 +炸酱面 炸醬麪 +点个 點個 +点了 點了 +点出 點出 +点出去 點出去 +点出来 點出來 +点击一个链接 點擊一個鏈接 +点半钟 點半鐘 +点发 點發 +点名表 點名表 +点回 點回 +点回去 點回去 +点回来 點回來 +点多钟 點多鐘 +点将录 點將錄 +点扎 點扎 +点播曲 點播曲 +点染云烟 點染雲煙 +点核 點核 +点烟 點菸 +点烟器 點菸器 +点线面 點線面 +点里 點裏 +点钟 點鐘 +点集合 點集合 +点面结合 點面結合 +点饥 點飢 +炼丹 煉丹 +炼丹八卦炉 煉丹八卦爐 +炼丹术 煉丹術 +炼之未定 煉之未定 +炼乳 煉乳 +炼冶 鍊冶 +炼制 煉製 +炼制厂 煉製廠 +炼化 煉化 +炼句 煉句 +炼字 煉字 +炼山 煉山 +炼师 鍊師 +炼度 鍊度 +炼形 煉形 +炼气 煉氣 +炼汞 鍊汞 +炼油 煉油 +炼油厂 煉油廠 +炼焦 煉焦 +炼焦厂 煉焦廠 +炼焦炉 煉焦爐 +炼焦煤 煉焦煤 +炼狱 煉獄 +炼石 煉石 +炼石补天 煉石補天 +炼糖 煉糖 +炼糖厂 煉糖廠 +炼药 煉藥 +炼贫 鍊貧 +炼金 鍊金 +炼金术 鍊金術 +炼金术士 煉金術士 +炼钢 鍊鋼 +炼钢业 鍊鋼業 +炼钢厂 鍊鋼廠 +炼钢炉 鍊鋼爐 +炼铁 鍊鐵 +炼铁厂 鍊鐵廠 +炼铁炉 鍊鐵爐 +炼铜 鍊銅 +炼铜厂 鍊銅廠 +炼铝 鍊鋁 +炽热火山云 熾熱火山雲 +烂了嘴 爛了嘴 +烂了舌头 爛了舌頭 +烂板乌龟 爛板烏龜 +烂板洋钱 爛板洋錢 +烂游 爛遊 +烈日当空 烈日當空 +烈火干柴 烈火乾柴 +烘了 烘了 +烘云托月 烘雲托月 +烘制 烘製 +烘干 烘乾 +烘干机 烘乾機 +烘托 烘托 +烘托出 烘托出 +烘板 烘板 +烘熏 烘燻 +烛台 燭臺 +烛杯 燭杯 +烛穗 燭穗 +烝尝 烝嘗 +烟丝 菸絲 +烟云 煙雲 +烟云供养 煙雲供養 +烟云过眼 煙雲過眼 +烟供 煙供 +烟傢伙 煙傢伙 +烟具 煙具 +烟农 菸農 +烟卷 菸捲 +烟卷儿 菸捲兒 +烟厂 菸廠 +烟台 煙臺 +烟台地区 煙臺地區 +烟台市 煙臺市 +烟台师范学院 煙臺師範學院 +烟叶 菸葉 +烟合包 煙合包 +烟味 煙味 +烟商 煙商 +烟嘴 菸嘴 +烟嘴儿 菸嘴兒 +烟囱 煙囪 +烟圈 菸圈 +烟土 煙土 +烟夜蛾 煙夜蛾 +烟头 菸頭 +烟子 煙子 +烟客 煙客 +烟害 菸害 +烟尘 煙塵 +烟屁股 菸屁股 +烟岚 煙嵐 +烟岚云岫 煙嵐雲岫 +烟岸 煙岸 +烟幕 煙幕 +烟幕弹 煙幕彈 +烟户 煙戶 +烟斗 菸斗 +烟斗丝 菸斗絲 +烟景 煙景 +烟月 煙月 +烟月场 煙月場 +烟月牌 煙月牌 +烟机 煙機 +烟杆 煙桿 +烟枪 煙槍 +烟枪洞 煙槍洞 +烟柱 煙柱 +烟树 煙樹 +烟毒 煙毒 +烟毒犯 煙毒犯 +烟民 菸民 +烟气 煙氣 +烟油 煙油 +烟泡 煙泡 +烟波 煙波 +烟波万顷 煙波萬頃 +烟波客 煙波客 +烟波浩渺 煙波浩渺 +烟波钓叟 煙波釣叟 +烟波钓徒 煙波釣徒 +烟海 煙海 +烟消云散 煙消雲散 +烟消冰释 煙消冰釋 +烟消火灭 煙消火滅 +烟消雾散 煙消霧散 +烟渚 煙渚 +烟火 煙火 +烟火之警 煙火之警 +烟火气 煙火氣 +烟火秀 煙火秀 +烟火节 煙火節 +烟火邻居 煙火鄰居 +烟火食 煙火食 +烟灯 煙燈 +烟灰 菸灰 +烟灰缸 菸灰缸 +烟煤 煙煤 +烟煴 煙熅 +烟熏 煙燻 +烟熏妆 煙燻妝 +烟熏火燎 煙熏火燎 +烟燄障天 煙燄障天 +烟爨 煙爨 +烟生喉舌 煙生喉舌 +烟瘴 煙瘴 +烟瘾 煙癮 +烟盒 煙盒 +烟硝 煙硝 +烟硝味 煙硝味 +烟碱 菸鹼 +烟碱酸 菸鹼酸 +烟禁 菸禁 +烟穗 煙穗 +烟突 煙突 +烟窝 煙窩 +烟笼雾锁 煙籠霧鎖 +烟筒 煙筒 +烟筒山 煙筒山 +烟管 煙管 +烟管面 煙管麪 +烟篷 煙篷 +烟粉 煙粉 +烟纸店 菸紙店 +烟缕 煙縷 +烟缸 菸缸 +烟肉 煙肉 +烟膏 煙膏 +烟臭 煙臭 +烟臭味 煙臭味 +烟花 煙花 +烟花债 煙花債 +烟花厂 煙花廠 +烟花场 煙花場 +烟花女 煙花女 +烟花寨 煙花寨 +烟花寨主 煙花寨主 +烟花巷 煙花巷 +烟花市 煙花市 +烟花柳巷 煙花柳巷 +烟花窟 煙花窟 +烟花簿 煙花簿 +烟花粉柳 煙花粉柳 +烟花粉黛 煙花粉黛 +烟花行院 煙花行院 +烟花门户 煙花門戶 +烟花队 煙花隊 +烟花阵 煙花陣 +烟花风月 煙花風月 +烟苗 煙苗 +烟草 菸草 +烟草味 菸草味 +烟蒂 菸蒂 +烟蓑雨笠 煙蓑雨笠 +烟蚜 菸蚜 +烟袋 菸袋 +烟袋哨子 菸袋哨子 +烟袋嘴 菸袋嘴 +烟袋嘴儿 菸袋嘴兒 +烟袋杆儿 菸袋桿兒 +烟袋油子 菸袋油子 +烟袋荷包 菸袋荷包 +烟袋锅子 菸袋鍋子 +烟视媚行 煙視媚行 +烟酒 菸酒 +烟酒不沾 煙酒不沾 +烟酒公卖 菸酒公賣 +烟酒公卖局 菸酒公賣局 +烟酒税 菸酒稅 +烟酸 煙酸 +烟锅 煙鍋 +烟雨 煙雨 +烟雨楼 煙雨樓 +烟雾 煙霧 +烟雾剂 煙霧劑 +烟雾弥漫 煙霧瀰漫 +烟雾弹 煙霧彈 +烟雾症 煙霧症 +烟雾迷漫 煙霧迷漫 +烟霏 煙霏 +烟霏雾集 煙霏霧集 +烟霏露结 煙霏露結 +烟霞 煙霞 +烟霞外人 煙霞外人 +烟霞痼疾 煙霞痼疾 +烟霞癖 煙霞癖 +烟霭 煙靄 +烟霾 煙霾 +烟飞星散 煙飛星散 +烟馆 煙館 +烟鬟 煙鬟 +烟鬼 煙鬼 +烤个 烤個 +烤了 烤了 +烤干 烤乾 +烤晒 烤曬 +烤漆板 烤漆板 +烤烟 烤煙 +烤胡椒香肠 烤胡椒香腸 +烤面包 烤麪包 +烤面包机 烤麪包機 +烦了 煩了 +烦复 煩複 +烦恼皆因强出头 煩惱皆因強出頭 +烦死了 煩死了 +烧了 燒了 +烧出 燒出 +烧出去 燒出去 +烧出来 燒出來 +烧制 燒製 +烧尽 燒盡 +烧干 燒乾 +烧录 燒錄 +烧录器 燒錄器 +烧录机 燒錄機 +烧杯 燒杯 +烧杯架 燒杯架 +烧毁 燒燬 +烧炼 燒煉 +烧糊了洗脸水 燒糊了洗臉水 +烩面 燴麪 +烫一个发 燙一個髮 +烫一次发 燙一次髮 +烫个 燙個 +烫个发 燙個髮 +烫出 燙出 +烫发 燙髮 +烫发师 燙髮師 +烫头发 燙頭髮 +烫完发 燙完髮 +烫次发 燙次髮 +烫蜡 燙蠟 +烫衣板 燙衣板 +烫面 燙麪 +烬余 燼餘 +热出病来 熱出病來 +热功当量 熱功當量 +热发光剂量计 熱發光劑量計 +热合 熱合 +热干面 熱乾麪 +热当量 熱當量 +热核 熱核 +热电厂 熱電廠 +热症 熱症 +热脉冲 熱脈衝 +热药 熱藥 +热衷于 熱衷於 +热量表 熱量表 +热闹哄哄 熱鬧哄哄 +烹制 烹製 +烹调术 烹調術 +烽火台 烽火臺 +烽烟 烽煙 +烽烟四起 烽煙四起 +烽烟四起战火纷飞 烽煙四起戰火紛飛 +烽烟遍地 烽煙遍地 +焉耆回族自治县 焉耆回族自治縣 +焊了 焊了 +焊出 焊出 +焊接艺术 焊接藝術 +焕别 煥別 +焕发 煥發 +焕发起来 煥發起來 +焙干 焙乾 +焚修 焚修 +焚化厂 焚化廠 +焚尸 焚屍 +焚尸扬灰 焚屍揚灰 +焚尸案 焚屍案 +焚尸炉 焚屍爐 +焚毁 焚燬 +無言不仇 無言不讎 +焦了 焦了 +焦化厂 焦化廠 +焦干 焦乾 +焦急万分 焦急萬分 +焦获 焦穫 +焦虑症 焦慮症 +焦面大士 焦面大士 +焰彩 焰彩 +然后 然後 +然后就 然後就 +然身死才数月耳 然身死纔數月耳 +煅炼 煅煉 +煎个 煎個 +煎了 煎了 +煎出 煎出 +煎药 煎藥 +煎药法 煎藥法 +煎蛋卷 煎蛋卷 +煎面 煎麪 +煞费周章 煞費周章 +煤核 煤核 +煤气工厂 煤氣工廠 +煤气表 煤氣表 +煤烟 煤煙 +煤系 煤系 +煤铲 煤鏟 +照个 照個 +照了 照了 +照价 照價 +照价征税 照價徵稅 +照价收买 照價收買 +照价收购 照價收購 +照价赔偿 照價賠償 +照入签 照入籤 +照准 照準 +照出 照出 +照出去 照出去 +照出来 照出來 +照占 照佔 +照发 照發 +照台 照臺 +照后镜 照後鏡 +照录 照錄 +照相制版 照相製版 +照相干片 照相乾片 +照相术 照相術 +照签 照簽 +照签不误 照簽不誤 +照表 照表 +照面 照面 +煨干 煨乾 +煨干就湿 煨乾就溼 +煨干避湿 煨乾避溼 +煮个 煮個 +煮出 煮出 +煮出来 煮出來 +煮字疗饥 煮字療飢 +煮沸后 煮沸後 +煮熟的鸭子飞了 煮熟的鴨子飛了 +煮粥焚须 煮粥焚鬚 +煮面 煮麪 +煴斗 熅斗 +熄了 熄了 +熊克武 熊克武 +熊据虎跱 熊據虎跱 +熏习 熏習 +熏人 燻人 +熏制 熏製 +熏天 熏天 +熏染 薰染 +熏沐 薰沐 +熏烝 熏烝 +熏烤 燻烤 +熏熏 熏熏 +熏笼 熏籠 +熏肉 燻肉 +熏腐 熏腐 +熏草 燻草 +熏草纸 燻草紙 +熏蒸 燻蒸 +熏蒸剂 熏蒸劑 +熏蒸室 熏蒸室 +熏衣草 熏衣草 +熏赫 燻赫 +熏鑪 燻鑪 +熏陶 薰陶 +熏陶成性 熏陶成性 +熏风 薰風 +熏风徐来 熏風徐來 +熏香 薰香 +熏鱼儿 燻魚兒 +熏鸡 燻雞 +熏黑 燻黑 +熏黑了 燻黑了 +熔于 熔於 +熔合 熔合 +熔岩 熔岩 +熔岩流 熔岩流 +熔岩湖 熔岩湖 +熔岩穹丘 熔岩穹丘 +熔核 熔核 +熔毁 熔燬 +熔炼 熔鍊 +熔炼炉 熔煉爐 +熔融岩浆 熔融岩漿 +熟了 熟了 +熟念 熟念 +熟药 熟藥 +熟读王叔和不如临症多 熟讀王叔和不如臨症多 +熟面人 熟面人 +熟面孔 熟面孔 +熨斗 熨斗 +熬出 熬出 +熬出头 熬出頭 +熬出来 熬出來 +熬制 熬製 +熬姜呷醋 熬薑呷醋 +熬炼 熬煉 +熬药 熬藥 +熬药汤 熬藥湯 +燃尽 燃盡 +燃料处理厂 燃料處理廠 +燃料组合 燃料組合 +燃气电厂 燃氣電廠 +燎发 燎髮 +燔针 燔針 +燕云十六州 燕雲十六州 +燕几 燕几 +燕台 燕臺 +燕巢于幕 燕巢於幕 +燕昭筑台 燕昭築臺 +燕游 燕遊 +燕燕于飞 燕燕于飛 +燕雀安知鸿鹄之志 燕雀安知鴻鵠之志 +燕雀焉知鸿鹄之志 燕雀焉知鴻鵠之志 +爆出 爆出 +爆发 爆發 +爆发出 爆發出 +爆发出来 爆發出來 +爆发力 爆發力 +爆发性 爆發性 +爆发星 爆發星 +爆发音 爆發音 +爆扣 爆扣 +爆破术 爆破術 +爬出 爬出 +爬出去 爬出去 +爬出来 爬出來 +爬升 爬升 +爬回 爬回 +爬满了 爬滿了 +爬虫 爬蟲 +爬虫动物 爬蟲動物 +爬虫类 爬蟲類 +爬虫类动物 爬蟲類動物 +爰历篇 爰歷篇 +爱丽丝梦游记 愛麗絲夢遊記 +爱丽丝漫游奇境记 愛麗絲漫遊奇境記 +爱丽舍宫 愛麗捨宮 +爱之欲其生恶之欲其死 愛之欲其生惡之欲其死 +爱之适足以害之 愛之適足以害之 +爱乐乐团 愛樂樂團 +爱了 愛了 +爱别离苦 愛別離苦 +爱困 愛睏 +爱在心里 愛在心裏 +爱幸 愛幸 +爱弥儿 愛彌兒 +爱彼表 愛彼錶 +爱德蒙 愛德蒙 +爱心刮刮乐 愛心刮刮樂 +爱心彩券 愛心彩券 +爱念 愛念 +爱恶分明 愛惡分明 +爱情征服一切 愛情征服一切 +爱情里 愛情裏 +爱才 愛才 +爱才好士 愛才好士 +爱才如命 愛才如命 +爱才若渴 愛才若渴 +爱抽烟 愛抽菸 +爱欲 愛慾 +爱游玩 愛遊玩 +爱管闲事 愛管閒事 +爱莫大于心死 愛莫大於心死 +爱远恶近 愛遠惡近 +爱里 愛裏 +爱面子 愛面子 +爲了 爲了 +爲幸 爲幸 +爲恶 爲惡 +爵仇 爵仇 +父党 父黨 +父台 父臺 +父子同牝 父子同牝 +父权制 父權制 +父母两系血统主义 父母兩系血統主義 +父母在不远游 父母在不遠游 +父系 父系 +父系亲属 父系親屬 +父系制度 父系制度 +爷娘 爺孃 +爷羹娘饭 爺羹孃飯 +爷饭娘羹 爺飯孃羹 +爹娘 爹孃 +爽荡 爽蕩 +牀头柜 牀頭櫃 +牀头金尽 牀頭金盡 +片云遮顶 片雲遮頂 +片价 片價 +片善小才 片善小才 +片头曲 片頭曲 +片尾曲 片尾曲 +片岩 片岩 +片甲不回 片甲不回 +片石千钧 片石千鈞 +片纸只字 片紙隻字 +片言只字 片言隻字 +片言只语 片言隻語 +片言折狱 片言折獄 +片语只字 片語隻字 +片语只辞 片語隻辭 +片面 片面 +片面之言 片面之言 +片面之词 片面之詞 +片面性 片面性 +片面最惠国 片面最惠國 +片麻岩 片麻岩 +版筑 版築 +版面 版面 +牉合 牉合 +牌价 牌價 +牌子曲 牌子曲 +牌板 牌板 +牌面 牌面 +牒发 牒發 +牖里 牖里 +牙买加胡椒 牙買加胡椒 +牙克石 牙克石 +牙克石市 牙克石市 +牙医系 牙醫系 +牙后慧 牙後慧 +牙周 牙周 +牙周炎 牙周炎 +牙周病 牙周病 +牙机巧制 牙機巧制 +牙板 牙板 +牙签 牙籤 +牙签万轴 牙籤萬軸 +牙签犀轴 牙籤犀軸 +牙签玉轴 牙籤玉軸 +牙签锦轴 牙籤錦軸 +牙籤万轴 牙籤萬軸 +牙缝里 牙縫裏 +牙虫 牙蟲 +牙雕 牙雕 +牙齿咬合不正 牙齒咬合不正 +牛仔布 牛仔布 +牛只 牛隻 +牛后 牛後 +牛回磨转 牛回磨轉 +牛头马面 牛頭馬面 +牛李党争 牛李黨爭 +牛柳面 牛柳麪 +牛肉干 牛肉乾 +牛肉拉面 牛肉拉麪 +牛肉汤面 牛肉湯麪 +牛肉炒面 牛肉炒麪 +牛肉面 牛肉麪 +牛肉面节 牛肉麪節 +牛舍 牛舍 +牛表牛觔 牛表牛觔 +牛角挂书 牛角掛書 +牛角面包 牛角麪包 +牛骥同一皁 牛驥同一皁 +牛骥同槽 牛驥同槽 +牛骥同皁 牛驥同皁 +牛骥同皂 牛驥同皂 +牡丹虽好全仗绿叶扶 牡丹雖好全仗綠葉扶 +牡丹虽好全仗绿叶扶持 牡丹雖好全仗綠葉扶持 +牡丹虽好全凭绿叶扶持 牡丹雖好全憑綠葉扶持 +牡丹虽好终须绿叶扶持 牡丹雖好終須綠葉扶持 +牢靠妥当 牢靠妥當 +牧神午后 牧神午後 +物产丰富 物產豐富 +物件导向 物件導向 +物价 物價 +物价指数 物價指數 +物价督导会报 物價督導會報 +物尽其用 物盡其用 +物极则衰 物極則衰 +物极必反 物極必反 +物欲 物慾 +物欲世界 物慾世界 +物欲横流 物慾橫流 +物流系统 物流系統 +物理系 物理系 +物种 物種 +物种来由 物種來由 +物种起源 物種起源 +物美价廉 物美價廉 +物腐虫生 物腐蟲生 +物色人才 物色人才 +物阜民丰 物阜民豐 +牲干下 牲幹下 +牵一发 牽一髮 +牵一发而动全身 牽一髮而動全身 +牵三挂四 牽三掛四 +牵了 牽了 +牵五挂四 牽五掛四 +牵出 牽出 +牵出去 牽出去 +牵出来 牽出來 +牵制 牽制 +牵制行动 牽制行動 +牵合 牽合 +牵合附会 牽合附會 +牵回 牽回 +牵回去 牽回去 +牵回来 牽回來 +牵引出 牽引出 +牵引发电机 牽引發電機 +牵彩 牽彩 +牵心挂肠 牽心掛腸 +牵念 牽念 +牵挂 牽掛 +牵机药 牽機藥 +牵籐带叶 牽籐帶葉 +牵系 牽繫 +牵肚挂肠 牽肚掛腸 +牵肠挂肚 牽腸掛肚 +特于 特於 +特价 特價 +特价品 特價品 +特价菜 特價菜 +特使团 特使團 +特克斯 特克斯 +特克斯县 特克斯縣 +特克斯市 特克斯市 +特克斯河 特克斯河 +特内里费 特內里費 +特准 特准 +特出 特出 +特别 特別 +特别任务连 特別任務連 +特别公积 特別公積 +特别公积金 特別公積金 +特别刑法 特別刑法 +特别助理 特別助理 +特别卖力 特別賣力 +特别号 特別號 +特别奖 特別獎 +特别客串 特別客串 +特别座 特別座 +特别待遇 特別待遇 +特别感谢 特別感謝 +特别扣除额 特別扣除額 +特别护士 特別護士 +特别护理 特別護理 +特别报导 特別報導 +特别报道 特別報道 +特别提款权 特別提款權 +特别是 特別是 +特别来宾 特別來賓 +特别法 特別法 +特别版 特別版 +特别组 特別組 +特别股 特別股 +特别致 特別緻 +特别节目 特別節目 +特别行政区 特別行政區 +特别费 特別費 +特别费案 特別費案 +特别长 特別長 +特别门诊 特別門診 +特制 特製 +特制住 特制住 +特制品 特製品 +特制定 特制定 +特制止 特制止 +特制订 特制訂 +特地回 特地回 +特征 特徵 +特征值 特徵值 +特征向量 特徵向量 +特征多项式 特徵多項式 +特征联合 特徵聯合 +特惠价 特惠價 +特惠制度 特惠制度 +特技团 特技團 +特效药 特效藥 +特有种 特有種 +特松加 特松加 +特林布 特林布 +特殊性向 特殊性向 +特殊才能 特殊才能 +特种 特種 +特种作战 特種作戰 +特种兵 特種兵 +特种工艺 特種工藝 +特种文书 特種文書 +特种空勤团 特種空勤團 +特种考试 特種考試 +特种行业 特種行業 +特种警察 特種警察 +特种部队 特種部隊 +特种钢 特種鋼 +特艺彩色 特藝彩色 +特蒙德 特蒙德 +特里 特里 +特里尔 特里爾 +犁板 犁板 +犬只 犬隻 +犬牙相制 犬牙相制 +犯了 犯了 +犯台 犯臺 +犯奸 犯奸 +犯恶 犯惡 +犯罪团伙 犯罪團伙 +犯罪集团 犯罪集團 +犵党 犵黨 +状态参数 狀態參數 +状态表 狀態表 +犹太历 猶太曆 +犹太复国主义 猶太復國主義 +犹太复国主义者 猶太復國主義者 +犹太复国运动 猶太復國運動 +犹如表 猶如錶 +犹如钟 猶如鐘 +犹如钟表 猶如鐘錶 +狂占 狂佔 +狂并潮 狂併潮 +狂想曲 狂想曲 +狂搜 狂搜 +狂牛症 狂牛症 +狂花病叶 狂花病葉 +狂荡 狂蕩 +狂药 狂藥 +狃于 狃於 +狃于成见 狃於成見 +狄仁杰 狄仁傑 +狄克 狄克 +狄克森 狄克森 +狄志杰 狄志杰 +狄拉克 狄拉克 +狄里斯 狄里斯 +狎妓冶游 狎妓冶游 +狎游 狎遊 +狐借虎威 狐藉虎威 +狐朋狗党 狐朋狗黨 +狐群狗党 狐羣狗黨 +狐裘蒙戎 狐裘蒙戎 +狗党狐群 狗黨狐羣 +狗占马坑 狗占馬坑 +狗口里吐不出象牙 狗口裏吐不出象牙 +狗嘴里 狗嘴裏 +狗嘴里还有象牙 狗嘴裏還有象牙 +狗娘养的 狗孃養的 +狗才 狗才 +狗扣 狗釦 +狗杂种 狗雜種 +狗皮膏药 狗皮膏藥 +狗链 狗鏈 +狞恶 獰惡 +狞恶可怖 獰惡可怖 +狠了 狠了 +狠干 狠幹 +狠恶 狠惡 +独具只眼 獨具隻眼 +独出一时 獨出一時 +独出心裁 獨出心裁 +独出机杼 獨出機杼 +独占 獨佔 +独占事业 獨佔事業 +独占市场 獨佔市場 +独占性 獨佔性 +独占权 獨佔權 +独占花魁 獨佔花魁 +独占资本 獨佔資本 +独占鳌头 獨佔鰲頭 +独占鼇头 獨佔鼇頭 +独唱曲 獨唱曲 +独奏曲 獨奏曲 +独尊儒术 獨尊儒術 +独当 獨當 +独当一面 獨當一面 +独挑大梁 獨挑大樑 +独擅胜场 獨擅勝場 +独曲 獨曲 +独根孤种 獨根孤種 +独步当时 獨步當時 +独立党 獨立黨 +独立出来 獨立出來 +独立千古 獨立千古 +独立国家联合体 獨立國家聯合體 +独立悬吊系统 獨立懸吊系統 +独自个 獨自個 +独辟蹊径 獨闢蹊徑 +独钟 獨鍾 +狭心症 狹心症 +狭谷 狹谷 +狮坛 獅壇 +狮身人面像 獅身人面像 +狱里 獄裏 +狼仆 狼僕 +狼卜食 狼卜食 +狼吞虎咽 狼吞虎嚥 +狼心狗幸 狼心狗幸 +狼来了 狼來了 +狼烟 狼煙 +狼烟四起 狼煙四起 +狼狈万状 狼狽萬狀 +狼狈为奸 狼狽爲奸 +狼虎药 狼虎藥 +狼飧虎咽 狼飧虎嚥 +狼餐虎咽 狼餐虎嚥 +猎狩终极 獵狩終極 +猎获 獵獲 +猎获物 獵獲物 +猛个劲 猛個勁 +猛于 猛於 +猛冲 猛衝 +猛升 猛升 +猛可里 猛可裏 +猛回头 猛回頭 +猛地里 猛地裏 +猛干 猛幹 +猛药 猛藥 +猜三划五 猜三划五 +猜出 猜出 +猜出来 猜出來 +猜得出 猜得出 +猜测出 猜測出 +猝发 猝發 +猢狲入布袋 猢猻入布袋 +猥当大任 猥當大任 +猪八戒吃人参果 豬八戒吃人參果 +猪只 豬隻 +猪肉干 豬肉乾 +猪肝面 豬肝麪 +猪脚面 豬腳麪 +猪脚面线 豬腳麪線 +猪舌面 豬舌麪 +猪舍 豬舍 +猪链球菌 豬鏈球菌 +猪链球菌病 豬鏈球菌病 +猫儿见了鱼鲜饭 貓兒見了魚鮮飯 +猫鼠同眠 貓鼠同眠 +猫鼠游戏 貓鼠遊戲 +献丑 獻醜 +献了 獻了 +献出 獻出 +献台 獻臺 +猴面包 猴麪包 +猴面包树 猴麪包樹 +猿鹤沙虫 猿鶴沙蟲 +猿鹤虫沙 猿鶴蟲沙 +獑胡 獑胡 +獠面 獠面 +玄了 玄了 +玄云 玄雲 +玄冬 玄冬 +玄制 玄製 +玄参 玄蔘 +玄机暗藏 玄機暗藏 +玄武岩 玄武岩 +玄武质熔岩 玄武質熔岩 +玄胄 玄胄 +玄针 玄鍼 +玄黄翻复 玄黃翻覆 +率先垂范 率先垂範 +率同 率同 +率团 率團 +率团参加 率團參加 +率土同庆 率土同慶 +玉制 玉製 +玉勒雕鞍 玉勒雕鞍 +玉卮无当 玉卮無當 +玉历 玉曆 +玉参差 玉參差 +玉台 玉臺 +玉台体 玉臺體 +玉台新咏 玉臺新詠 +玉叶金枝 玉葉金枝 +玉叶金柯 玉葉金柯 +玉叶金花 玉葉金花 +玉尺量才 玉尺量才 +玉山杯 玉山杯 +玉帘 玉簾 +玉手纤纤 玉手纖纖 +玉斗 玉斗 +玉杯 玉杯 +玉枝卜寿 玉枝卜壽 +玉石同沉 玉石同沉 +玉石同烬 玉石同燼 +玉石同焚 玉石同焚 +玉米淀粉 玉米澱粉 +玉米面 玉米麪 +玉米须 玉米鬚 +玉纤 玉纖 +玉虫 玉蟲 +玉里 玉里 +玉里镇 玉里鎮 +玉镜台 玉鏡臺 +玉雕 玉雕 +玉面 玉面 +玉面貍 玉面貍 +王于真 王于真 +王云五 王雲五 +王佐之才 王佐之才 +王余鱼 王餘魚 +王侯后 王侯后 +王公贵戚 王公貴戚 +王制 王制 +王台 王臺 +王后 王后 +王太后 王太后 +王婆卖瓜自卖自夸 王婆賣瓜自賣自誇 +王婆子卖了磨 王婆子賣了磨 +王子犯法与庶民同罪 王子犯法與庶民同罪 +王子面 王子麪 +王干发 王乾發 +王幸男 王幸男 +王庄 王莊 +王彩桦 王彩樺 +王彩碧 王彩碧 +王志华 王志華 +王志文 王志文 +王志群 王志羣 +王志贞 王志貞 +王杰 王傑 +王杰胜 王傑勝 +王正杰 王正杰 +王母娘娘 王母娘娘 +王涂发 王塗發 +王熙松 王熙松 +王献极 王獻極 +王秋凤 王秋鳳 +王者风范 王者風範 +王茂松 王茂松 +王蒙 王蒙 +王鉴 王鑑 +玛斯克 瑪斯克 +玛曲 瑪曲 +玛曲县 瑪曲縣 +玛泰克 瑪泰克 +玢岩 玢岩 +玩了 玩了 +玩具厂 玩具廠 +玩出 玩出 +玩出去 玩出去 +玩出来 玩出來 +玩团 玩團 +玩物丧志 玩物喪志 +环保斗士 環保鬥士 +环安系 環安系 +环工系 環工系 +环扣 環扣 +环极涡旋 環極渦旋 +环游 環遊 +环游世界 環遊世界 +环球定位系统 環球定位系統 +环面 環面 +环顾四周 環顧四周 +现于 現於 +现代修正主义 現代修正主義 +现代建筑 現代建築 +现代舞团 現代舞團 +现代艺术 現代藝術 +现代集团 現代集團 +现出 現出 +现出原形 現出原形 +现出原身 現出原身 +现制性 現制性 +现场表演 現場表演 +现场采访 現場採訪 +现实面 現實面 +现货价 現貨價 +现进现出 現進現出 +现金流量表 現金流量表 +玳梁 玳梁 +玳瑁梁 玳瑁梁 +玻璃布 玻璃布 +玻璃杯 玻璃杯 +玻璃板 玻璃板 +玻璃柜 玻璃櫃 +玻璃浮雕 玻璃浮雕 +玻璃纤维 玻璃纖維 +玻里尼西 玻里尼西 +玻里尼西亚人 玻里尼西亞人 +珂里 珂里 +珊卓布拉克 珊卓布拉克 +珊瑚虫 珊瑚蟲 +珍同拱璧 珍同拱璧 +珍娜杰克森 珍娜傑克森 +珍珠岩 珍珠岩 +珍珠项链 珍珠項鍊 +珍肴异馔 珍餚異饌 +珐瑯彩 琺瑯彩 +珠帘 珠簾 +珠斗烂班 珠斗爛班 +珠联璧合 珠聯璧合 +珠胎暗结 珠胎暗結 +珠还合浦 珠還合浦 +班代表 班代表 +班克劳夫 班克勞夫 +班克拉夫特 班克拉夫特 +班克斯 班克斯 +班兰叶 班蘭葉 +班别 班別 +班师回朝 班師回朝 +班游 班遊 +班艾伏列克 班艾伏列克 +班艾佛列克 班艾佛列克 +班里 班裏 +球台 球檯 +球后 球后 +球后小 球后小 +球后艾宁 球后艾寧 +球后辛吉丝 球后辛吉絲 +球团 球團 +球团矿 球團礦 +球坛 球壇 +球坛上 球壇上 +球杆 球杆 +球状星团 球狀星團 +球面 球面 +球面三角 球面三角 +球面体 球面體 +球面几何 球面幾何 +球面几何学 球面幾何學 +球面镜 球面鏡 +琅邪台刻石 琅邪臺刻石 +理一个发 理一個髮 +理一次发 理一次髮 +理不胜辞 理不勝辭 +理个 理個 +理个发 理個髮 +理了 理了 +理事长杯 理事長盃 +理出 理出 +理出头绪 理出頭緒 +理发 理髮 +理发匠 理髮匠 +理发厅 理髮廳 +理发员 理髮員 +理发师 理髮師 +理发师傅 理髮師傅 +理发店 理髮店 +理发院 理髮院 +理合 理合 +理固当然 理固當然 +理头发 理頭髮 +理完发 理完髮 +理当 理當 +理当如此 理當如此 +理念 理念 +理所当然 理所當然 +理次发 理次髮 +理胡子 理鬍子 +理致 理致 +琉璃厂 琉璃廠 +琐才 瑣才 +琛板 琛板 +琨玉秋霜 琨玉秋霜 +琴弦 琴絃 +琴断朱弦 琴斷朱絃 +琴斯托霍瓦 琴斯托霍瓦 +琴杆 琴桿 +琴钟 琴鐘 +琵琶别抱 琵琶別抱 +琵琶录 琵琶錄 +琵琶虫 琵琶蟲 +琼台玉宇 瓊臺玉宇 +琼台玉阁 瓊臺玉閣 +琼枝玉叶 瓊枝玉葉 +琼瑶曲 瓊瑤曲 +瑜伽术 瑜伽術 +瑜珈术 瑜珈術 +瑞丰 瑞豐 +瑞云 瑞雲 +瑞克 瑞克 +瑞克希 瑞克希 +瑞士卷 瑞士捲 +瑞征 瑞徵 +瑞气祥云 瑞氣祥雲 +瑞秋 瑞秋 +瑞秋怀兹 瑞秋懷茲 +瑞穗 瑞穗 +瑞穗乡 瑞穗鄉 +瑞签 瑞簽 +瑞贝里 瑞貝里 +瑞郎方面 瑞郎方麪 +瑟瑟发抖 瑟瑟發抖 +瑟瑟秋风 瑟瑟秋風 +瑶台 瑤臺 +瑶台琼室 瑤臺瓊室 +瑶台银阙 瑤臺銀闕 +瑶台镜 瑤臺鏡 +瑶签 瑤籤 +璅虫 璅蟲 +璧合 璧合 +璧合珠联 璧合珠聯 +璧合珠连 璧合珠連 +璧回 璧回 +璧炉台 璧爐臺 +瓠叶 瓠葉 +瓢虫 瓢蟲 +瓦利泰克 瓦利泰克 +瓦历斯 瓦歷斯 +瓦合 瓦合 +瓦合之卒 瓦合之卒 +瓦尔基里 瓦爾基里 +瓦尔达克 瓦爾達克 +瓦尔达克省 瓦爾達克省 +瓦当 瓦當 +瓦当文 瓦當文 +瓦松 瓦松 +瓦特表 瓦特表 +瓦瑞泰克 瓦瑞泰克 +瓦舍 瓦舍 +瓦萨里 瓦薩里 +瓦西里 瓦西里 +瓦西里耶维奇 瓦西裏耶維奇 +瓦解云散 瓦解雲散 +瓦达克 瓦達克 +瓦里 瓦里 +瓦里斯 瓦里斯 +瓮安 甕安 +瓶坠簪折 瓶墜簪折 +瓷杯 瓷杯 +甄别 甄別 +甄别考试 甄別考試 +甄后 甄后 +甄奇录异 甄奇錄異 +甄才品能 甄才品能 +甄选人才 甄選人才 +甕尽杯干 甕盡杯乾 +甕里醯鸡 甕裏醯雞 +甘于 甘於 +甘居下游 甘居下游 +甘巴里 甘巴里 +甘当 甘當 +甘心情愿 甘心情願 +甘愿 甘願 +甘托克 甘托克 +甘谷 甘谷 +甘谷县 甘谷縣 +甚么 甚麼 +甚么都干 甚麼都幹 +甚于 甚於 +甚至于 甚至於 +甚获 甚獲 +甚获好评 甚獲好評 +甜水面 甜水麪 +甜萝卜 甜蘿蔔 +甜面酱 甜麪醬 +甜面醬 甜麪醬 +生个 生個 +生了 生了 +生于 生於 +生于忧患 生於憂患 +生于忧患死于安乐 生於憂患死於安樂 +生产合作 生產合作 +生产合作社 生產合作社 +生产斗争 生產鬥爭 +生产管制 生產管制 +生佛万家 生佛萬家 +生出 生出 +生出来 生出來 +生刍致祭 生芻致祭 +生别死离 生別死離 +生力面 生力麪 +生华发 生華髮 +生发 生髮 +生发剂 生髮劑 +生发水 生髮水 +生发药 生髮藥 +生同衾死同穴 生同衾死同穴 +生命不息战斗不止 生命不息戰鬥不止 +生命共同体 生命共同體 +生命周期 生命週期 +生命在于运动 生命在於運動 +生命征象 生命徵象 +生命表 生命表 +生姜 生薑 +生姜丝 生薑絲 +生姜汁 生薑汁 +生姜片 生薑片 +生字表 生字表 +生布 生布 +生态旅游 生態旅遊 +生态环境游 生態環境游 +生态系 生態系 +生态系统 生態系統 +生情发意 生情發意 +生技医药 生技醫藥 +生旦淨末丑 生旦淨末丑 +生栋复屋 生棟覆屋 +生死别离 生死別離 +生死斗 生死鬥 +生死未卜 生死未卜 +生死轮回 生死輪迴 +生殖洄游 生殖洄游 +生殖系统 生殖系統 +生民涂炭 生民塗炭 +生活杯 生活杯 +生活水准 生活水準 +生活面 生活面 +生涯规划 生涯規劃 +生灵涂地 生靈塗地 +生灵涂炭 生靈塗炭 +生物伦琴当量 生物倫琴當量 +生物制剂 生物製劑 +生物制品 生物製品 +生物合成 生物合成 +生物学系 生物學系 +生物弹药 生物彈藥 +生物技术 生物技術 +生物技术与制药工业发展推动小组 生物技術與製藥工業發展推動小組 +生物时钟 生物時鐘 +生物系 生物系 +生物钟 生物鐘 +生理时钟 生理時鐘 +生田斗 生田斗 +生离死别 生離死別 +生药 生藥 +生药局 生藥局 +生词表 生詞表 +生迭水准 生迭水準 +生锈 生鏽 +生长板 生長板 +生面 生面 +生面团 生麪糰 +生面孔 生面孔 +用一当十 用一當十 +用不了 用不了 +用于 用於 +用于修饰 用於修飾 +用作配种 用作配種 +用出 用出 +用字不当 用字不當 +用尽 用盡 +用尽心思 用盡心思 +用尽心机 用盡心機 +用尽方法 用盡方法 +用幸福 用幸福 +用志不分 用志不分 +用户数据 用戶數據 +用户界面 用戶界面 +用报台 用報臺 +用搜 用搜 +用来配种 用來配種 +用汇 用匯 +用舍失宜 用舍失宜 +用舍行藏 用舍行藏 +用药 用藥 +用行舍藏 用行舍藏 +用词不当 用詞不當 +甩出 甩出 +甩出去 甩出去 +甩出来 甩出來 +甩发 甩髮 +甩手掌柜 甩手掌櫃 +甪里 甪里 +田园交响曲 田園交響曲 +田园曲 田園曲 +田字面 田字面 +田家庵 田家庵 +田家庵区 田家庵區 +田庄 田莊 +田志兴 田志興 +田父之获 田父之獲 +田秋堇 田秋堇 +田种玉 田種玉 +田舍 田舍 +田舍奴 田舍奴 +田舍翁 田舍翁 +田舍郎 田舍郎 +田螺含水过冬 田螺含水過冬 +田谷 田穀 +田里 田裏 +由于 由於 +由余 由余 +由恪志远 由恪志遠 +由表及里 由表及裏 +甲壳虫 甲殼蟲 +甲壳虫类 甲殼蟲類 +甲板 甲板 +甲种 甲種 +甲种体位 甲種體位 +甲种国库券 甲種國庫券 +甲种国民兵役 甲種國民兵役 +甲种维生素 甲種維生素 +甲第连云 甲第連雲 +甲胄 甲冑 +甲胄鱼类 甲冑魚類 +甲虫 甲蟲 +甲虫类 甲蟲類 +甲虫车 甲蟲車 +申复 申覆 +申扎 申扎 +申扎县 申扎縣 +申曲 申曲 +申请表 申請表 +电价 電價 +电冰柜 電冰櫃 +电冲 電衝 +电力厂 電力廠 +电卷星飞 電卷星飛 +电卷风驰 電卷風馳 +电厂 電廠 +电压表 電壓表 +电台 電臺 +电唱针 電唱針 +电复 電覆 +电子云 電子雲 +电子反制 電子反制 +电子学系 電子學系 +电子布告栏 電子布告欄 +电子店面 電子店面 +电子数据交换 電子數據交換 +电子杂志 電子雜誌 +电子游戏 電子遊戲 +电子看板 電子看板 +电子系 電子系 +电子表 電子錶 +电子表单 電子表單 +电子表情 電子表情 +电子表格 電子表格 +电子表示 電子表示 +电子论坛 電子論壇 +电子邮件系统 電子郵件系統 +电子钟 電子鐘 +电子钟表 電子鐘錶 +电宰厂 電宰廠 +电度表 電度表 +电影分级制 電影分級制 +电影制作 電影製作 +电影制片 電影製片 +电影台 電影臺 +电影回顾展 電影回顧展 +电影美术 電影美術 +电影艺术 電影藝術 +电影集团 電影集團 +电机及电子学工程师联合会 電機及電子學工程師聯合會 +电机系 電機系 +电杆 電杆 +电板 電板 +电极 電極 +电检制 電檢制 +电汇 電匯 +电池厂 電池廠 +电池板 電池板 +电流表 電流表 +电热杯 電熱杯 +电熨斗 電熨斗 +电码表 電碼表 +电磁干扰 電磁干擾 +电磁振荡 電磁振盪 +电磁脉冲 電磁脈衝 +电线杆 電線杆 +电缆调制解调器 電纜調制解調器 +电胡刀 電鬍刀 +电脑与电话系统整合 電腦與電話系統整合 +电脑互动艺术 電腦互動藝術 +电脑台 電腦檯 +电脑周边设备 電腦周邊設備 +电脑图书出版业 電腦圖書出版業 +电脑彩喷 電腦彩噴 +电脑游戏 電腦遊戲 +电脑看板 電腦看板 +电脑系统 電腦系統 +电脑系统业 電腦系統業 +电脑绘图技术 電腦繪圖技術 +电脑网志 電腦網誌 +电脑艺术 電腦藝術 +电脑闸道系统 電腦閘道系統 +电荷耦合 電荷耦合 +电荷耦合器件 電荷耦合器件 +电表 電錶 +电视兴奋症 電視興奮症 +电视台 電視臺 +电视柜 電視櫃 +电视游乐器 電視遊樂器 +电视采访 電視採訪 +电话录音 電話錄音 +电话答录机 電話答錄機 +电路板 電路板 +电量表 電量表 +电针 電針 +电针麻醉 電針麻醉 +电钟 電鐘 +电铲 電鏟 +电须刀 電鬚刀 +男仆 男僕 +男佣 男傭 +男佣人 男傭人 +男同学 男同學 +男同志 男同志 +男大当婚 男大當婚 +男女有别 男女有別 +男尸 男屍 +男性厌恶 男性厭惡 +男才女貌 男才女貌 +男生宿舍 男生宿舍 +男用表 男用錶 +男系 男系 +甸后 甸後 +画了 畫了 +画出 畫出 +画出来 畫出來 +画卷 畫卷 +画坛 畫壇 +画布 畫布 +画板 畫板 +画栋雕梁 畫棟雕樑 +画栋飞云 畫棟飛雲 +画梁雕栋 畫樑雕棟 +画法几何 畫法幾何 +画着 畫着 +画表 畫表 +画表格 畫表格 +画阁朱楼 畫閣朱樓 +画面 畫面 +画饼充饥 畫餅充飢 +畅所欲为 暢所欲爲 +畅所欲言 暢所欲言 +畅游 暢遊 +畅销曲 暢銷曲 +界分别观 界分別觀 +界别 界別 +界面 界面 +畎亩下才 畎畝下才 +畏于 畏於 +畏影恶迹 畏影惡跡 +留个 留個 +留中不发 留中不發 +留了 留了 +留余地 留餘地 +留出 留出 +留别 留別 +留发 留髮 +留后 留後 +留后手 留後手 +留后步 留後步 +留后路 留後路 +留头发 留頭髮 +留念 留念 +留种 留種 +留种地 留種地 +留美同学会 留美同學會 +留胡子 留鬍子 +留胡须 留鬍鬚 +留脸面 留臉面 +留芳千古 留芳千古 +留芳后世 留芳後世 +留言板 留言板 +留连不舍 留連不捨 +留针 留針 +留面子 留面子 +畚斗 畚斗 +略低于 略低於 +略同 略同 +略大于 略大於 +略尽情谊 略盡情誼 +略无参商 略無參商 +略有出入 略有出入 +略胜 略勝 +略胜一筹 略勝一籌 +略语表 略語表 +略高于 略高於 +番泻叶 番瀉葉 +畸形发展 畸形發展 +疋先里 疋先裏 +疏于 疏於 +疏于防备 疏於防備 +疏于防范 疏於防範 +疏松 疏鬆 +疏松症 疏鬆症 +疑云 疑雲 +疑云重重 疑雲重重 +疑信参半 疑信參半 +疑凶 疑兇 +疑团 疑團 +疑心生暗鬼 疑心生暗鬼 +疑念 疑念 +疑核 疑核 +疑系 疑係 +疑难杂症 疑難雜症 +疗饥 療飢 +疟原虫 瘧原蟲 +疟虫 瘧蟲 +疥癣虫 疥癬蟲 +疥虫 疥蟲 +疯了 瘋了 +疱疹性咽狭症 皰疹性咽狹症 +疲于 疲於 +疲于奔命 疲於奔命 +疲劳极限 疲勞極限 +疲劳症 疲勞症 +疲困 疲睏 +疵蒙谬累 疵蒙謬累 +疾之如仇 疾之如仇 +疾之若仇 疾之若仇 +疾恶 疾惡 +疾恶好善 疾惡好善 +疾恶如仇 疾惡如仇 +疾恶若仇 疾惡若仇 +疾病控制中心 疾病控制中心 +疾病突发 疾病突發 +疾风扫秋叶 疾風掃秋葉 +病了 病了 +病从口入祸从口出 病從口入禍從口出 +病余 病餘 +病出 病出 +病历 病歷 +病历卡 病歷卡 +病历室 病歷室 +病历表 病歷表 +病原虫 病原蟲 +病发 病發 +病后 病後 +病后初愈 病後初愈 +病后初癒 病後初癒 +病容满面 病容滿面 +病征 病徵 +病愈 病癒 +病毒血症 病毒血症 +病毒防范 病毒防範 +病症 病症 +病舍 病舍 +病虫 病蟲 +病虫危害 病蟲危害 +病虫害 病蟲害 +症侯群 症侯羣 +症候 症候 +症候群 症候羣 +症状 症狀 +症状性 症狀性 +症结 癥結 +症结点 癥結點 +痊愈 痊癒 +痒了 癢了 +痒疹 癢疹 +痒痒 癢癢 +痔核 痔核 +痕迹 痕跡 +痖弦 瘂弦 +痘疹娘娘 痘疹娘娘 +痛不欲生 痛不欲生 +痛了 痛了 +痛失英才 痛失英才 +痛快淋漓 痛快淋漓 +痛恶 痛惡 +痛毁极诋 痛毀極詆 +痛苦万分 痛苦萬分 +痛赞 痛贊 +痢疾杆菌 痢疾桿菌 +痨虫 癆蟲 +痫症 癇症 +痰症 痰症 +痲痹不了 痲痹不了 +痲痺不了 痲痺不了 +痴呆症 癡呆症 +痴念 癡念 +痴虫 癡蟲 +瘅恶彰善 癉惡彰善 +瘙痒症 瘙癢症 +瘦了 瘦了 +瘦小枯干 瘦小枯乾 +瘫子掉在井里 癱子掉在井裏 +癌症 癌症 +癌症病患 癌症病患 +癌症肿瘤 癌症腫瘤 +癒合 癒合 +癫痫症 癲癇症 +癸丑 癸丑 +登个 登個 +登了 登了 +登云梯 登雲梯 +登出 登出 +登出去 登出去 +登出来 登出來 +登台 登臺 +登台拜将 登臺拜將 +登台演唱 登臺演唱 +登台表演 登臺表演 +登坛 登壇 +登坛拜将 登壇拜將 +登峰造极 登峯造極 +登庸人才 登庸人才 +登录 登錄 +登机手续柜台 登機手續櫃檯 +登极 登極 +登科录 登科錄 +登记表 登記表 +登革出血热 登革出血熱 +登龙术 登龍術 +發表 發表 +白了了 白了了 +白云 白雲 +白云乡 白雲鄉 +白云亲舍 白雲親舍 +白云区 白雲區 +白云孤飞 白雲孤飛 +白云山 白雲山 +白云岩 白雲岩 +白云机场 白雲機場 +白云母 白雲母 +白云片片 白雲片片 +白云石 白雲石 +白云矿区 白雲礦區 +白云苍狗 白雲蒼狗 +白云观 白雲觀 +白僵蚕 白殭蠶 +白兔捣药 白兔搗藥 +白净面皮 白淨面皮 +白刀子进去红刀子出来 白刀子進去紅刀子出來 +白化症 白化症 +白千层 白千層 +白卷 白卷 +白发 白髮 +白发人 白髮人 +白发其事 白發其事 +白发如新 白髮如新 +白发朱颜 白髮朱顏 +白发相守 白髮相守 +白发红颜 白髮紅顏 +白发苍苍 白髮蒼蒼 +白发苍颜 白髮蒼顏 +白发郎潜 白髮郎潛 +白发银须 白髮銀鬚 +白发青衫 白髮青衫 +白发齐眉 白髮齊眉 +白变种 白變種 +白古苏花 白古蘇花 +白合金 白合金 +白喉杆菌 白喉桿菌 +白团 白團 +白团扇 白團扇 +白垩系 白堊系 +白娘子 白娘子 +白布 白布 +白干 白乾 白幹 +白干儿 白乾兒 +白当 白當 +白得发亮 白得發亮 +白搽白折 白搽白折 +白斑症 白斑症 +白日升天 白日昇天 +白日飞升 白日飛昇 +白术 白朮 +白朴 白樸 +白杆兵 白桿兵 +白松 白松 +白板 白板 +白板单吊 白板單吊 +白板天子 白板天子 +白板笔 白板筆 +白果松 白果松 +白洋淀 白洋澱 +白淨面皮 白淨面皮 +白烟 白煙 +白皮松 白皮松 +白种 白種 +白种人 白種人 +白粉面 白粉麪 +白胡 白鬍 +白胡椒 白胡椒 +白色人种 白色人種 +白色系 白色系 +白苏 白蘇 +白苹 白蘋 +白苹洲 白蘋洲 +白药 白藥 +白菜价 白菜價 +白萝卜 白蘿蔔 +白蒙蒙 白濛濛 +白蜡 白蠟 +白蜡明经 白蠟明經 +白蜡杆子 白蠟杆子 +白蜡树 白蠟樹 +白蜡虫 白蠟蟲 +白蜡蜡 白蠟蠟 +白血球过多症 白血球過多症 +白里安 白里安 +白里透红 白裏透紅 +白雪公主症候群 白雪公主症候羣 +白雪曲 白雪曲 +白霉 白黴 +白面 白麪 +白面书生 白面書生 +白面书郎 白面書郎 +白面僧面猴 白面僧面猴 +白面儿 白麪兒 +白面无须 白面無鬚 +白面鼯鼠 白面鼯鼠 +白须 白鬚 +白首北面 白首北面 +白首同归 白首同歸 +白马归周 白馬歸周 +白驹空谷 白駒空谷 +白骨松 白骨松 +白鹤梁 白鶴梁 +白鹤秀才 白鶴秀才 +百万 百萬 +百万之众 百萬之衆 +百万买宅千万买邻 百萬買宅千萬買鄰 +百万位 百萬位 +百万分之一 百萬分之一 +百万吨 百萬噸 +百万吨级核武器 百萬噸級核武器 +百万富翁 百萬富翁 +百万年 百萬年 +百万赫兹 百萬赫茲 +百万雄兵 百萬雄兵 +百万雄师 百萬雄師 +百不当一 百不當一 +百个 百個 +百中百发 百中百發 +百了 百了 +百了千当 百了千當 +百事和合 百事和合 +百余 百餘 +百余只 百餘隻 +百余里 百餘里 +百几个 百幾個 +百出 百出 +百分之一千 百分之一千 +百分制 百分制 +百分表 百分表 +百划 百劃 +百卉千葩 百卉千葩 +百发 百發 +百发百中 百發百中 +百只 百隻 +百只足够 百只足夠 +百叶 百葉 +百叶卷 百葉捲 +百叶窗 百葉窗 +百叶窗帘 百葉窗簾 +百叶箱 百葉箱 +百合 百合 +百合子 百合子 +百合科 百合科 +百合花 百合花 +百合花饰 百合花飾 +百团大战 百團大戰 +百多只 百多隻 +百天后 百天後 +百姿千态 百姿千態 +百媚千娇 百媚千嬌 +百子千孙 百子千孫 +百孔千创 百孔千創 +百孔千疮 百孔千瘡 +百尺竿头更尽一步 百尺竿頭更盡一步 +百岁之后 百歲之後 +百岁千秋 百歲千秋 +百巧千穷 百巧千窮 +百年之后 百年之後 +百年后 百年後 +百年好合 百年好合 +百度表 百度表 +百当 百當 +百念 百念 +百战百胜 百戰百勝 +百扎 百紮 +百折不回 百折不回 +百折不挠 百折不撓 +百折不挫 百折不挫 +百折裙 百摺裙 +百拙千丑 百拙千醜 +百汇 百匯 +百炼 百鍊 +百炼成钢 百鍊成鋼 +百科里 百科裏 +百紫千红 百紫千紅 +百纵千随 百縱千隨 +百老汇 百老匯 +百胜餐饮 百勝餐飲 +百胜餐饮集团 百勝餐飲集團 +百脑汇 百腦匯 +百舍重茧 百舍重繭 +百舍重趼 百舍重趼 +百花历 百花曆 +百花历史 百花歷史 +百花娘子 百花娘子 +百药之长 百藥之長 +百虑一致 百慮一致 +百计千心 百計千心 +百计千方 百計千方 +百计千谋 百計千謀 +百谋千计 百謀千計 +百谷 百穀 +百谷王 百谷王 +百足不僵 百足不僵 +百足之虫 百足之蟲 +百足之虫死而不僵 百足之蟲死而不僵 +百足之虫至死不僵 百足之蟲至死不僵 +百足虫 百足蟲 +百辟 百辟 +百里 百里 +百里之才 百里之才 +百里侯 百里侯 +百里傒 百里傒 +百里挑一 百裏挑一 +百里香 百里香 +百锻千练 百鍛千練 +百面雷 百面雷 +皂化 皂化 +皂白 皁白 +皂荚 皂莢 +皂荚树 皂莢樹 +皂角 皁角 +的一确二 的一確二 +的历 的歷 +的当 的當 +的扣 的扣 +的杯 的杯 +的核 的核 +的泛 的泛 +的的确确 的的確確 +的确 的確 +的确会 的確會 +的确如此 的確如此 +的确是 的確是 +的确良 的確良 +的钟 的鐘 +的黎波里 的黎波里 +皆准 皆準 +皆可作淀 皆可作澱 +皇亲国戚 皇親國戚 +皇冠出版 皇冠出版 +皇冠出版集团 皇冠出版集團 +皇历 皇曆 +皇后 皇后 +皇后区 皇后區 +皇后号 皇后號 +皇后镇 皇后鎮 +皇天后土 皇天后土 +皇太后 皇太后 +皇太极 皇太極 +皇太极清太宗 皇太極清太宗 +皇家加勒比海游轮公司 皇家加勒比海遊輪公司 +皇家马德里 皇家馬德里 +皇庄 皇莊 +皇恩浩荡 皇恩浩蕩 +皇极 皇極 +皇极历 皇極曆 +皇极历史 皇極歷史 +皇极数 皇極數 +皇胄 皇胄 +皇辟 皇辟 +皓发 皓髮 +皓月千里 皓月千里 +皓月当空 皓月當空 +皓齿朱唇 皓齒硃脣 +皓齿朱脣 皓齒朱脣 +皖系军阀 皖系軍閥 +皖系战败 皖系戰敗 +皙面 皙面 +皮下出血 皮下出血 +皮下注射 皮下注射 +皮克斯 皮克斯 +皮克林 皮克林 +皮克罗比 皮克羅比 +皮划艇 皮划艇 +皮划艇激流回旋 皮劃艇激流回旋 +皮划艇静水 皮劃艇靜水 +皮制 皮製 +皮制品 皮製品 +皮制服 皮制服 +皮困秋 皮困秋 +皮夹克 皮夾克 +皮尔斯布洛斯南 皮爾斯布洛斯南 +皮层下失语症 皮層下失語症 +皮托管 皮托管 +皮松 皮鬆 +皮松肉紧 皮鬆肉緊 +皮松骨痒 皮鬆骨癢 +皮板儿 皮板兒 +皮特拉克 皮特拉克 +皮里抽肉 皮裏抽肉 +皮里春秋 皮裏春秋 +皮里晋书 皮裏晉書 +皮里膜外 皮裏膜外 +皮里走肉 皮裏走肉 +皮里阳秋 皮裏陽秋 +皮雕 皮雕 +皮面 皮面 +皱别 皺彆 +皱叶欧芹 皺葉歐芹 +皱折 皺摺 +盆吊 盆吊 +盆里 盆裏 +盈余 盈餘 +盈千累万 盈千累萬 +盈千累百 盈千累百 +盈泛 盈泛 +盈盈秋水 盈盈秋水 +盈车嘉穗 盈車嘉穗 +盈馀加征 盈餘加徵 +益于 益於 +益发 益發 +益州名画录 益州名畫錄 +益虫 益蟲 +益觉困难 益覺困難 +益鸟益虫 益鳥益蟲 +盎格鲁撒克逊 盎格魯撒克遜 +盎格鲁萨克逊 盎格魯薩克遜 +盎格鲁萨克逊人 盎格魯薩克遜人 +盎盂相系 盎盂相繫 +盐余 鹽餘 +盐卤 鹽滷 +盐城师范学院 鹽城師範學院 +盐打怎么咸 鹽打怎麼鹹 +盐打怎么咸醋打怎么酸 鹽打怎麼鹹醋打怎麼酸 +盐水选种 鹽水選種 +盐水针 鹽水針 +盐酸克仑特罗 鹽酸克侖特羅 +监修 監修 +监制 監製 +监听系统 監聽系統 +监察御史 監察御史 +监管不周 監管不周 +监管体制 監管體制 +监管范围 監管範圍 +监系 監繫 +盒子里 盒子裏 +盒式录音带 盒式錄音帶 +盒式录音磁带 盒式錄音磁帶 +盒里 盒裏 +盖世之才 蓋世之才 +盖了 蓋了 +盖了又盖 蓋了又蓋 +盖于 蓋於 +盖杯 蓋杯 +盖板 蓋板 +盖维克 蓋維克 +盗录 盜錄 +盗御马 盜御馬 +盗无实据 盜無實據 +盗版党 盜版黨 +盗采 盜採 +盗钟 盜鐘 +盗钟掩耳 盜鐘掩耳 +盘回 盤迴 +盘扣 盤扣 +盘据 盤據 +盘旋曲折 盤旋曲折 +盘曲 盤曲 +盘术 盤術 +盘松 盤松 +盘游 盤遊 +盘获 盤獲 +盘谷 盤谷 +盘里 盤裏 +盘面 盤面 +盛了 盛了 +盛价 盛价 +盛冬 盛冬 +盛德遗范 盛德遺範 +盛极一时 盛極一時 +盛极必衰 盛極必衰 +盛极而衰 盛極而衰 +盛行于 盛行於 +盛赞 盛讚 +盟旗制度 盟旗制度 +目前目后 目前目後 +目力表 目力表 +目录 目錄 +目录卡 目錄卡 +目录学 目錄學 +目无余子 目無餘子 +目标价 目標價 +目牛游刃 目牛游刃 +目眦尽裂 目眥盡裂 +目瞪口僵 目瞪口僵 +目瞪舌僵 目瞪舌僵 +目短于自见 目短於自見 +目视云霄 目視雲霄 +目骇耳回 目駭耳回 +盲干 盲幹 +直上青云 直上青雲 +直了 直了 +直于 直於 +直冲 直衝 +直升 直升 +直升机 直升機 +直升飞机 直升飛機 +直发 直髮 +直发女 直髮女 +直发毛 直發毛 +直头布袋 直頭布袋 +直布罗陀 直布羅陀 +直布罗陀海峡 直布羅陀海峽 +直截了当 直截了當 +直捷了当 直捷了當 +直接了当 直接了當 +直接制版 直接制版 +直接参与 直接參與 +直接征税 直接徵稅 +直接数据 直接數據 +直接证据 直接證據 +直摆 直襬 +直杆 直杆 +直流发电机 直流發電機 +直系 直系 +直系亲 直系親 +直系亲属 直系親屬 +直系军阀 直係軍閥 +直系祖先 直系祖先 +直系血亲 直系血親 +直致 直致 +直落布兰雅 直落布蘭雅 +直言尽意 直言盡意 +直言极谏 直言極諫 +直进直出 直進直出 +直链 直鏈 +直销式传销制度 直銷式傳銷制度 +直须 直須 +相为表里 相爲表裏 +相于 相於 +相交满天下知心能几人 相交滿天下知心能幾人 +相克 相剋 +相克制 相剋制 +相克服 相克服 +相关系数 相關係數 +相冲 相沖 +相别 相別 +相别多年 相別多年 +相去万里 相去萬里 +相去无几 相去無幾 +相台 相臺 +相叶雅纪 相葉雅紀 +相合 相合 +相同 相同 +相同点 相同點 +相向 相向 +相奸 相姦 +相对于 相對於 +相对极 相對極 +相差无几 相差無幾 +相干 相干 +相平面 相平面 +相并 相併 +相当 相當 +相当于 相當於 +相当于或大于 相當於或大於 +相当程度 相當程度 +相念 相念 +相托 相托 +相扣 相扣 +相提并论 相提並論 +相斗 相鬥 +相术 相術 +相生相克 相生相剋 +相符合 相符合 +相等于 相等於 +相结合 相結合 +相融合 相融合 +相距千里 相距千里 +相适应 相適應 +相里 相里 +相门出相 相門出相 +相面 相面 +相须为命 相須爲命 +相须而行 相須而行 +盼了 盼了 +盼既示复 盼既示覆 +盾板 盾板 +省个 省個 +省了 省了 +省党部 省黨部 +省出 省出 +省出来 省出來 +省欲去奢 省慾去奢 +省民同胞 省民同胞 +省道台 省道臺 +眉分八彩 眉分八彩 +眉南面北 眉南面北 +眉垂目合 眉垂目合 +眉毛胡子一把抓 眉毛鬍子一把抓 +眉里 眉裏 +眉面 眉面 +看上了 看上了 +看下表 看下錶 +看下钟 看下鐘 +看不出 看不出 +看不出来 看不出來 +看个究竟 看個究竟 +看中了 看中了 +看了 看了 +看了又看 看了又看 +看傻了眼 看傻了眼 +看准 看準 +看出 看出 +看出了神 看出了神 +看出去 看出去 +看出来 看出來 +看台 看臺 +看台股 看臺股 +看向 看向 +看回 看回 +看尽 看盡 +看得出 看得出 +看得出来 看得出來 +看朱成碧 看朱成碧 +看板 看板 +看淡后市 看淡後市 +看终了 看終了 +看花了 看花了 +看表 看錶 +看表面 看表面 +看走了眼 看走了眼 +看钟 看鐘 +看风向 看風向 +看麦娘 看麥娘 +真个 真個 +真主党 真主黨 +真保志 真保志 +真值表 真值表 +真凭实据 真憑實據 +真凶 真兇 +真凶实犯 真兇實犯 +真受不了 真受不了 +真后生动物 真後生動物 +真实面 真實面 +真彩色 真彩色 +真成了 真成了 +真才实学 真才實學 +真服了 真服了 +真核 真核 +真理必胜 真理必勝 +真确 真確 +真草千字文 真草千字文 +真身舍利 真身舍利 +真金不怕火炼 真金不怕火煉 +真面目 真面目 +眠云 眠雲 +眷念 眷念 +眷注 眷注 +眷舍 眷舍 +眺台 眺臺 +眺望台 眺望臺 +眼前花发 眼前花發 +眼动技术 眼動技術 +眼动记录 眼動記錄 +眼同 眼同 +眼周 眼周 +眼圈红了 眼圈紅了 +眼如秋水 眼如秋水 +眼帘 眼簾 +眼干 眼乾 +眼干症 眼乾症 +眼手并用 眼手並用 +眼扎毛 眼扎毛 +眼泛 眼泛 +眼泪往肚子里流 眼淚往肚子裏流 +眼泪洗面 眼淚洗面 +眼球干燥症 眼球乾燥症 +眼看四面 眼看四面 +眼眶里 眼眶裏 +眼睛里 眼睛裏 +眼花了乱 眼花瞭亂 +眼药 眼藥 +眼药水 眼藥水 +眼药膏 眼藥膏 +眼虫 眼蟲 +眼观四面 眼觀四面 +眼迷心荡 眼迷心蕩 +眼酸 眼痠 +眼里 眼裏 +眼里不揉沙子 眼裏不揉沙子 +眼里揉不下沙子 眼裏揉不下沙子 +眼镜布 眼鏡布 +着儿 着兒 +着手于 着手於 +着眼于 着眼於 +着色软体 着色軟體 +着迷于 着迷於 +着重于 着重於 +着重指出 着重指出 +睁一只眼 睜一隻眼 +睟面盎背 睟面盎背 +睡个 睡個 +睡个夠 睡個夠 +睡个痛快 睡個痛快 +睡个觉 睡個覺 +睡了 睡了 +睡了又睡 睡了又睡 +睡游病 睡遊病 +睡病虫 睡病蟲 +睡眠曲 睡眠曲 +睡眠欲 睡眠慾 +睡眠虫 睡眠蟲 +睡眼蒙眬 睡眼矇矓 +睡莲叶 睡蓮葉 +睥睨物表 睥睨物表 +睽合 睽合 +瞄不准 瞄不準 +瞄了 瞄了 +瞄准 瞄準 +瞄准到 瞄準到 +瞄出 瞄出 +瞅下表 瞅下錶 +瞅下钟 瞅下鐘 +瞅不准 瞅不準 +瞇了 瞇了 +瞌睡虫 瞌睡蟲 +瞎了 瞎了 +瞎了眼 瞎了眼 +瞎了眼睛 瞎了眼睛 +瞎扎呼 瞎扎呼 +瞑子里 瞑子裏 +瞒了 瞞了 +瞒哄 瞞哄 +瞒天讨价就地还钱 瞞天討價就地還錢 +瞠乎其后 瞠乎其後 +瞠乎后矣 瞠乎後矣 +瞧不准 瞧不準 +瞧不出 瞧不出 +瞧了 瞧了 +瞧出 瞧出 +瞩托 矚託 +瞪了 瞪了 +瞬发中子 瞬發中子 +瞬发辐射 瞬發輻射 +瞬得彩色电影 瞬得彩色電影 +瞬息万变 瞬息萬變 +瞬息千变 瞬息千變 +瞭望台 瞭望臺 +瞳蒙 瞳矇 +瞻前忽后 瞻前忽後 +瞻前顾后 瞻前顧後 +瞻念 瞻念 +瞿秋白 瞿秋白 +矛头指向 矛頭指向 +矛盾百出 矛盾百出 +矛盾相向 矛盾相向 +矜功负胜 矜功負勝 +矜夸 矜誇 +矜庄 矜莊 +矜才使气 矜才使氣 +矜能负才 矜能負才 +矞云 矞雲 +矢不虚发 矢不虛發 +矢尽兵穷 矢盡兵窮 +矢志 矢志 +矢志不移 矢志不移 +矢无虚发 矢無虛發 +知了 知了 +知人知面 知人知面 +知人知面不知心 知人知面不知心 +知几其神 知幾其神 +知制诰 知制誥 +知名当世 知名當世 +知尽能索 知盡能索 +知往鉴今 知往鑑今 +知情同意 知情同意 +知感不尽 知感不盡 +知无不言言无不尽 知無不言言無不盡 +知行合一 知行合一 +知识范围 知識範圍 +知道了 知道了 +矫制 矯制 +矫情干誉 矯情干譽 +矫正术 矯正術 +矫若游龙 矯若遊龍 +矬个儿 矬個兒 +矬子里头选将军 矬子裏頭選將軍 +短不了 短不了 +短了 短了 +短于 短於 +短价 短價 +短几 短几 +短发 短髮 +短发性 短發性 +短叹 短嘆 +短叹长吁 短嘆長吁 +短后 短後 +短小精干 短小精幹 +短幸 短幸 +短折 短折 +短曲 短曲 +短板 短板 +短纤维 短纖維 +短针 短針 +短须 短鬚 +矮个 矮個 +矮个儿 矮個兒 +矮个子 矮個子 +矮了 矮了 +矮了一截 矮了一截 +矮了半截 矮了半截 +矮冬瓜 矮冬瓜 +矮几 矮几 +矮子里拔将军 矮子裏拔將軍 +矮杆品种 矮桿品種 +石內卜 石內卜 +石几 石几 +石化厂 石化廠 +石台 石臺 +石台县 石臺縣 +石坛 石壇 +石头布 石頭布 +石家庄 石家莊 +石屋制果 石屋製果 +石工术 石工術 +石志伟 石志偉 +石拐 石柺 +石敢当 石敢當 +石松 石松 +石松粉 石松粉 +石板 石板 +石板屋 石板屋 +石板瓦 石板瓦 +石板路 石板路 +石板道 石板道 +石枯松老 石枯松老 +石柜 石櫃 +石梁 石樑 +石棉布 石棉布 +石棉板 石棉板 +石棉症 石棉症 +石油蜡 石油蠟 +石油输出 石油輸出 +石油输出国家组织 石油輸出國家組織 +石油输出国组织 石油輸出國組織 +石灰岩 石灰岩 +石灰岩洞 石灰巖洞 +石炭系 石炭系 +石版术 石版術 +石百合 石百合 +石绵板 石綿板 +石胡荽 石胡荽 +石膏墙板 石膏牆板 +石英卤素灯 石英鹵素燈 +石英岩 石英岩 +石英表 石英錶 +石英钟 石英鐘 +石英钟表 石英鐘錶 +石莼 石蓴 +石蜡 石蠟 +石蜡像 石蠟像 +石针 石針 +石钟乳 石鐘乳 +石雕 石雕 +石雕像 石雕像 +石雕家 石雕家 +石黑彩 石黑彩 +矽岩 矽岩 +矽肺症 矽肺症 +矽谷 矽谷 +矽质岩 矽質岩 +矿物纤维 礦物纖維 +码表 碼錶 碼表 +砂岩 砂岩 +砂布 砂布 +砂锅面 砂鍋麪 +砌合 砌合 +砌合法 砌合法 +砌块建筑 砌塊建築 +砍了 砍了 +砍出 砍出 +砍出去 砍出去 +砍出来 砍出來 +砍向 砍向 +研修 研修 +研修员 研修員 +研修班 研修班 +研几探赜 研幾探賾 +研几析理 研幾析理 +研判出 研判出 +研制 研製 +研制出 研製出 +研制过程 研製過程 +研发 研發 +研发出 研發出 +研发出来 研發出來 +研发替代役 研發替代役 +研发部 研發部 +研发部门 研發部門 +研拟出 研擬出 +研究出 研究出 +研究出来 研究出來 +砖厂 磚廠 +砖雕 磚雕 +砖面 磚面 +砚台 硯臺 +砥志砺行 砥志礪行 +砥据 砥據 +砧板 砧板 +砭灸术 砭灸術 +砭针 砭鍼 +砰当 砰噹 +砲台 砲臺 +破了脸 破了臉 +破产财团 破產財團 +破价 破價 +破发 破發 +破发点 破發點 +破坏欲 破壞慾 +破布 破布 +破布子 破布子 +破折号 破折號 +破格录用 破格錄用 +破盘价 破盤價 +破纪录 破紀錄 +破绽百出 破綻百出 +破茧而出 破繭而出 +破获 破獲 +破蒸笼只会撒气 破蒸籠只會撒氣 +破表 破錶 +破记录 破記錄 +破釜沈舟 破釜沈舟 +破镜重合 破鏡重合 +破面 破面 +破风筝抖起来了 破風箏抖起來了 +砸了 砸了 +砻谷机 礱穀機 +砾岩 礫岩 +硅谷 硅谷 +硅质岩 硅質岩 +硗确 磽确 +硝烟 硝煙 +硝烟弹雨 硝煙彈雨 +硫磺谷 硫磺谷 +硫酸烟碱 硫酸菸鹼 +硬了 硬了 +硬了起来 硬了起來 +硬件平台 硬件平臺 +硬冲 硬衝 +硬化症 硬化症 +硬咽 硬嚥 +硬干 硬幹 +硬彩 硬彩 +硬核 硬核 +硬纸板 硬紙板 +硬肥皂 硬肥皂 +硬质合金 硬質合金 +硬里子 硬裏子 +硬面 硬麪 +硬页岩 硬頁岩 +确乎 確乎 +确保 確保 +确保安全 確保安全 +确信 確信 +确信无疑 確信無疑 +确凿 確鑿 +确凿不移 確鑿不移 +确切 確切 +确切不变 確切不變 +确切性 確切性 +确定 確定 +确定会 確定會 +确定判决 確定判決 +确定性 確定性 +确定故意 確定故意 +确定效应 確定效應 +确定是 確定是 +确定有 確定有 +确定能 確定能 +确实 確實 +确实会 確實會 +确实可靠 確實可靠 +确实在 確實在 +确实性 確實性 +确实是 確實是 +确实有 確實有 +确实能 確實能 +确山县 確山縣 +确当 確當 +确是 確是 +确有 確有 +确有其事 確有其事 +确有其人 確有其人 +确有此事 確有此事 +确瘠 确瘠 +确知 確知 +确确实实 確確實實 +确立 確立 +确系 確係 +确认 確認 +确认为 確認爲 +确认是 確認是 +确论 確論 +确证 確證 +确非 確非 +碌曲 碌曲 +碌曲县 碌曲縣 +碌碌庸才 碌碌庸才 +碍于 礙於 +碍于情面 礙於情面 +碍难照准 礙難照准 +碍面子 礙面子 +碎修儿 碎修兒 +碎发 碎髮 +碎尸万段 碎屍萬段 +碎屑岩 碎屑岩 +碎布 碎布 +碎布条 碎布條 +碑坛 碑壇 +碑志 碑誌 +碑面 碑面 +碗柜 碗櫃 +碗白干 碗白乾 +碗面 碗麪 +碛卤 磧鹵 +碧云 碧雲 +碧云寺 碧雲寺 +碧娜芝.布托 碧娜芝.布托 +碧波万顷 碧波萬頃 +碧波荡漾 碧波盪漾 +碧瓦朱甍 碧瓦朱甍 +碧眼紫须 碧眼紫鬚 +碧眼胡 碧眼胡 +碧眼金发 碧眼金髮 +碧鸡漫志 碧雞漫志 +碰了 碰了 +碰杯 碰杯 +碰碰胡 碰碰胡 +碰钟 碰鐘 +碰面 碰面 +碱性岩 鹼性岩 +碱纤维素 鹼纖維素 +碳氢化合 碳氫化合 +碳氢化合物 碳氫化合物 +碳水化合 碳水化合 +碳水化合物 碳水化合物 +碳的化合物 碳的化合物 +碳纤 碳纖 +碳纤维 碳纖維 +碳酸岩 碳酸岩 +碳链纤维 碳鏈纖維 +碾米厂 碾米廠 +磁制 磁製 +磁北极 磁北極 +磁单极子 磁單極子 +磁南极 磁南極 +磁扣 磁扣 +磁极 磁極 +磁核 磁核 +磁碟作业系统 磁碟作業系統 +磁针 磁針 +磊落轶荡 磊落軼蕩 +磕个响头 磕個響頭 +磕个头 磕個頭 +磕头虫 磕頭蟲 +磨了 磨了 +磨了半截舌头 磨了半截舌頭 +磨出 磨出 +磨制 磨製 +磨制石器 磨製石器 +磨厉以须 磨厲以須 +磨变岩 磨變岩 +磨合 磨合 +磨合期 磨合期 +磨合罗 磨合羅 +磨折 磨折 +磨杵成针 磨杵成針 +磨炼 磨鍊 +磨皮术 磨皮術 +磨石子面 磨石子面 +磨石粗砂岩 磨石粗砂岩 +磨砺以须 磨礪以須 +磨粉厂 磨粉廠 +磨耗症 磨耗症 +磨脊梁 磨脊樑 +磨针溪 磨針溪 +磨铁成针 磨鐵成針 +磬折 磬折 +磬钟 磬鐘 +磷酸盐岩 磷酸鹽岩 +礁岩 礁岩 +示复 示覆 +示威游行 示威遊行 +示范 示範 +示范企业 示範企業 +示范作用 示範作用 +示范动作 示範動作 +示范区 示範區 +示范单位 示範單位 +示范厂 示範廠 +示范园 示範園 +示范园区 示範園區 +示范场 示範場 +示范基地 示範基地 +示范学校 示範學校 +示范岗 示範崗 +示范工程 示範工程 +示范带 示範帶 +示范店 示範店 +示范性 示範性 +示范户 示範戶 +示范效应 示範效應 +示范教学 示範教學 +示范文本 示範文本 +示范村 示範村 +示范校 示範校 +示范法 示範法 +示范点 示範點 +示范片 示範片 +示范班 示範班 +示范田 示範田 +示范社区 示範社區 +示范街 示範街 +示范表演 示範表演 +示范课 示範課 +示范赛 示範賽 +示范项目 示範項目 +礼义生于富足 禮義生於富足 +礼乐射御 禮樂射御 +礼仪规范 禮儀規範 +礼制 禮制 +礼台 禮臺 +礼所当然 禮所當然 +礼数周到 禮數周到 +礼斗 禮斗 +礼赞 禮讚 +礼轻人意重千里送鹅毛 禮輕人意重千里送鵝毛 +社交恐惧症 社交恐懼症 +社交才能 社交才能 +社会主义制度 社會主義制度 +社会价值 社會價值 +社会体系 社會體系 +社会党 社會黨 +社会制度 社會制度 +社会发展 社會發展 +社会团体 社會團體 +社会学系 社會學系 +社会控制 社會控制 +社会整合 社會整合 +社会民主党 社會民主黨 +社会民主党人 社會民主黨人 +社会福利彩券 社會福利彩券 +社会系 社會系 +社会规范 社會規範 +社会调适 社會調適 +社党 社黨 +社区发展 社區發展 +社区电台 社區電臺 +社团 社團 +社团活动 社團活動 +社团课 社團課 +社工系 社工系 +社教系 社教系 +社民党 社民黨 +社里 社裏 +祁奚荐仇 祁奚薦仇 +祇洹精舍 祇洹精舍 +祈仙台 祈仙臺 +祈愿 祈願 +祈祷团 祈禱團 +祖冲之 祖沖之 +祖国光复会 祖國光復會 +祛痰药 祛痰藥 +祛蠹除奸 祛蠹除奸 +祝厘 祝釐 +祝发 祝髮 +祝年丰 祝年豐 +祝愿 祝願 +祝英台 祝英臺 +祝赞 祝讚 +神不守舍 神不守舍 +神人鉴知 神人鑑知 +神出鬼入 神出鬼入 +神出鬼没 神出鬼沒 +神分志夺 神分志奪 +神台 神臺 +神圣同盟 神聖同盟 +神圣周 神聖週 +神坛 神壇 +神头鬼面 神頭鬼面 +神彩奕奕 神彩奕奕 +神志 神志 +神志昏迷 神志昏迷 +神态悠闲 神態悠閒 +神摇魂荡 神搖魂盪 +神曲 神曲 +神曲茶 神麴茶 +神术 神術 +神术妙法 神術妙法 +神术妙策 神術妙策 +神术妙计 神術妙計 +神机妙术 神機妙術 +神杯 神杯 +神游 神遊 +神游太虚 神遊太虛 +神经干 神經幹 +神经战术 神經戰術 +神经症 神經症 +神经系 神經系 +神经系统 神經系統 +神经纤维 神經纖維 +神经纤维瘤 神經纖維瘤 +神胄 神胄 +神荼郁垒 神荼鬱壘 +神迹 神蹟 +神采 神采 +神采奕奕 神采奕奕 +神采奕然 神采奕然 +神采焕发 神采煥發 +神采英拔 神采英拔 +神采飘逸 神采飄逸 +神采飞扬 神采飛揚 +神采骏发 神采駿發 +神雕 神鵰 +神雕侠侣 神鵰俠侶 +神雕像 神雕像 +神魂摇荡 神魂搖盪 +神魂荡漾 神魂盪漾 +神魂荡飏 神魂盪颺 +神魂飘荡 神魂飄蕩 +神魂飞荡 神魂飛蕩 +神魂驰荡 神魂馳蕩 +祥丰街 祥豐街 +祥云 祥雲 +祥云县 祥雲縣 +祥云瑞彩 祥雲瑞彩 +祥云瑞气 祥雲瑞氣 +祥风庆云 祥風慶雲 +票价 票價 +票庄 票莊 +票房价值 票房價值 +票房毒药 票房毒藥 +票房纪录 票房紀錄 +票房记录 票房記錄 +票据 票據 +票据交换 票據交換 +票据交易所 票據交易所 +票据存款 票據存款 +票据法 票據法 +票据行为 票據行爲 +票汇 票匯 +票选出 票選出 +票面 票面 +票面价值 票面價值 +票面值 票面值 +祭东施娘 祭東施娘 +祭了 祭了 +祭五脏庙 祭五臟廟 +祭出 祭出 +祭台 祭臺 +祭司权术 祭司權術 +祭吊 祭弔 +祭吊文 祭弔文 +祭坛 祭壇 +祭尸 祭尸 +祭遵布被 祭遵布被 +祷念 禱唸 +祸于 禍於 +祸从口出 禍從口出 +祸出不测 禍出不測 +祸发萧墙 禍發蕭牆 +祸发齿牙 禍發齒牙 +祸因恶积 禍因惡積 +祸生于忽 禍生於忽 +祸盈恶稔 禍盈惡稔 +祸福吉凶 禍福吉凶 +祸福同门 禍福同門 +祸种 禍種 +祸种头 禍種頭 +祸稔恶盈 禍稔惡盈 +禀复 稟覆 +禁制 禁制 +禁制令 禁制令 +禁制品 禁製品 +禁奸除猾 禁奸除猾 +禁当 禁當 +禁忌站台 禁忌站臺 +禁核 禁核 +禁欲 禁慾 +禁欲主义 禁慾主義 +禁止吸烟 禁止吸菸 +禁止外出 禁止外出 +禁毁 禁燬 +禁毁书 禁燬書 +禁烟 禁菸 +禁烟令 禁菸令 +禁烟节 禁菸節 +禁药 禁藥 +禁药案 禁藥案 +禄丰 祿豐 +禄丰县 祿豐縣 +福克 福克 +福克兰群岛 福克蘭羣島 +福克斯 福克斯 +福克纳 福克納 +福兰克 福蘭克 +福尽灾生 福盡災生 +福布斯 福布斯 +福建师范 福建師範 +福建师范大学 福建師範大學 +福惠双修 福惠雙修 +福无重受日祸有并来时 福無重受日禍有並來時 +福瑞克 福瑞克 +福生于微 福生于微 +福维克 福維克 +福舍 福舍 +福荫 福廕 +禹余粮 禹餘糧 +禹王台 禹王臺 +禹王台区 禹王臺區 +禺谷 禺谷 +离不了 離不了 +离了 離了 +离于 離於 +离别 離別 +离别多年 離別多年 +离别已久 離別已久 +离合 離合 +离合体诗 離合體詩 +离合器 離合器 +离合悲欢 離合悲歡 +离合板 離合板 +离合诗 離合詩 +离奇曲折 離奇曲折 +离娘饭 離娘飯 +离婚同意书 離婚同意書 +离家出走 離家出走 +离岸价 離岸價 +离弦 離弦 +离弦走板儿 離弦走板兒 +离情别绪 離情別緒 +离题万里 離題萬里 +离鸾别凤 離鸞別鳳 +禽困复车 禽困覆車 +禽滑厘 禽滑釐 +禽舍 禽舍 +禾虫 禾蟲 +禾谷 禾穀 +禾谷类作物 禾穀類作物 +秀出 秀出 +秀出班行 秀出班行 +秀发 秀髮 +秀发垂肩 秀髮垂肩 +秀才 秀才 +秀才不出门能知天下事 秀才不出門能知天下事 +秀才人情 秀才人情 +秀才作医如菜作虀 秀才作醫如菜作虀 +秀才造反 秀才造反 +私下里 私下裏 +私了 私了 +私仇 私仇 +私党 私黨 +私心藏奸 私心藏奸 +私念 私念 +私斗 私鬥 +私曲 私曲 +私有制 私有制 +私有财产制 私有財產制 +私欲 私慾 +私自同意 私自同意 +秃发 禿髮 +秃发症 禿髮症 +秃妃之发 禿妃之髮 +秃秃里 禿禿裏 +秉台衡 秉臺衡 +秉烛夜游 秉燭夜遊 +秉鉴 秉鑑 +秋不干 秋不乾 +秋事 秋事 +秋令 秋令 +秋假里 秋假裏 +秋冬 秋冬 +秋决 秋決 +秋凉 秋涼 +秋凉时节 秋涼時節 +秋刀鱼 秋刀魚 +秋分 秋分 +秋分点 秋分點 +秋初 秋初 +秋千 鞦韆 +秋发 秋髮 +秋叶 秋葉 +秋叶原 秋葉原 +秋后 秋後 +秋后算帐 秋後算帳 +秋后算账 秋後算賬 +秋场 秋場 +秋士 秋士 +秋声 秋聲 +秋声赋 秋聲賦 +秋夜 秋夜 +秋天 秋天 +秋天里 秋天裏 +秋娘 秋娘 +秋季 秋季 +秋季学期 秋季學期 +秋季旅行 秋季旅行 +秋季档 秋季檔 +秋季班 秋季班 +秋季赛 秋季賽 +秋官 秋官 +秋审 秋審 +秋山 秋山 +秋川雅史 秋川雅史 +秋庄稼 秋莊稼 +秋征 秋征 +秋心 秋心 +秋思 秋思 +秋意 秋意 +秋意已浓 秋意已濃 +秋成 秋成 +秋扇 秋扇 +秋扇见捐 秋扇見捐 +秋播 秋播 +秋收 秋收 +秋收冬藏 秋收冬藏 +秋收季节 秋收季節 +秋收起义 秋收起義 +秋方 秋方 +秋日 秋日 +秋日里 秋日裏 +秋旱 秋旱 +秋景 秋景 +秋月 秋月 +秋月寒江 秋月寒江 +秋月春风 秋月春風 +秋期 秋期 +秋枫 秋楓 +秋榜 秋榜 +秋毫 秋毫 +秋毫不犯 秋毫不犯 +秋毫之末 秋毫之末 +秋毫无犯 秋毫無犯 +秋气 秋氣 +秋水 秋水 +秋水仙素 秋水仙素 +秋水伊人 秋水伊人 +秋汛 秋汛 +秋河 秋河 +秋波 秋波 +秋波送情 秋波送情 +秋海棠 秋海棠 +秋海棠花 秋海棠花 +秋游 秋遊 +秋灌 秋灌 +秋熟 秋熟 +秋燥 秋燥 +秋牡丹 秋牡丹 +秋瑾 秋瑾 +秋田 秋田 +秋田县 秋田縣 +秋眉 秋眉 +秋石 秋石 +秋社 秋社 +秋禊 秋禊 +秋篠宫 秋篠宮 +秋粮 秋糧 +秋老虎 秋老虎 +秋耕 秋耕 +秋胡 秋胡 +秋胡变文 秋胡變文 +秋胡戏妻 秋胡戲妻 +秋色 秋色 +秋色宜人 秋色宜人 +秋节 秋節 +秋茶 秋茶 +秋草 秋草 +秋草人情 秋草人情 +秋荼 秋荼 +秋荼密网 秋荼密網 +秋莲 秋蓮 +秋菊 秋菊 +秋菊傲霜 秋菊傲霜 +秋菜 秋菜 +秋葵 秋葵 +秋葵荚 秋葵莢 +秋虫 秋蟲 +秋蝉 秋蟬 +秋衣 秋衣 +秋装 秋裝 +秋裤 秋褲 +秋试 秋試 +秋闱 秋闈 +秋阳 秋陽 +秋阴入井干 秋陰入井幹 +秋雨 秋雨 +秋霖 秋霖 +秋霜 秋霜 +秋颜 秋顏 +秋风 秋風 +秋风团扇 秋風團扇 +秋风扫落叶 秋風掃落葉 +秋风落叶 秋風落葉 +秋风辞 秋風辭 +秋风过耳 秋風過耳 +秋风送爽 秋風送爽 +秋风飒飒 秋風颯颯 +秋香 秋香 +秋高气爽 秋高氣爽 +秋高气肃 秋高氣肅 +秋高马肥 秋高馬肥 +秋麻 秋麻 +种上 種上 +种上蒺藜就要扎脚 種上蒺藜就要扎腳 +种下 種下 +种下祸根 種下禍根 +种人 種人 +种仁 種仁 +种公畜 種公畜 +种出 種出 +种出来 種出來 +种切 種切 +种别 種別 +种到 種到 +种名 種名 +种因 種因 +种地 種地 +种姓 種姓 +种姓制 種姓制 +种姓制度 種姓製度 +种子 種子 +种子园 種子園 +种子地 種子地 +种子岛 種子島 +种子植物 種子植物 +种子球员 種子球員 +种子田 種子田 +种子网路 種子網路 +种子选手 種子選手 +种子队 種子隊 +种差 種差 +种师中 种師中 +种师道 种師道 +种庄稼 種莊稼 +种得 種得 +种德 種德 +种性 種性 +种户 種戶 +种放 种放 +种族 種族 +种族中心主义 種族中心主義 +种族主义 種族主義 +种族主义者 種族主義者 +种族偏见 種族偏見 +种族平等 種族平等 +种族歧视 種族歧視 +种族清洗 種族清洗 +种族清除 種族清除 +种族灭绝 種族滅絕 +种族迫害 種族迫害 +种族问题 種族問題 +种族隔离 種族隔離 +种树 種樹 +种植 種植 +种植业 種植業 +种植义齿 種植義齒 +种植区 種植區 +种植园 種植園 +种植户 種植戶 +种概念 種概念 +种源中心 種源中心 +种源论 種源論 +种火又长拄门又短 種火又長拄門又短 +种牛 種牛 +种牛痘 種牛痘 +种猪 種豬 +种玉 種玉 +种瓜得瓜 種瓜得瓜 +种瓜得瓜种豆得豆 種瓜得瓜種豆得豆 +种生 種生 +种田 種田 +种田人家 種田人家 +种畜 種畜 +种痘 種痘 +种的 種的 +种皮 種皮 +种祸 種禍 +种种 種種 +种稻 種稻 +种类 種類 +种籽 種籽 +种系 種系 +种群 種羣 +种肥 種肥 +种花 種花 +种花草 種花草 +种菜 種菜 +种蛋 種蛋 +种谷 種穀 +种豆 種豆 +种豆得豆 種豆得豆 +种起 種起 +种过 種過 +种过去 種過去 +种过来 種過來 +种近乎幻想 種近乎幻想 +种间杂交 種間雜交 +种马 種馬 +种麦得麦 種麥得麥 +种麻 種麻 +科举制 科舉制 +科举制度 科舉制度 +科克 科克 +科克林 科克林 +科别 科別 +科学万能 科學萬能 +科学中药 科學中藥 +科学技术 科學技術 +科学技术是第一生产力 科學技術是第一生產力 +科学技术现代化 科學技術現代化 +科学种田 科學種田 +科学规范 科學規範 +科学面 科學麪 +科尔沁左翼后 科爾沁左翼後 +科尔沁左翼后旗 科爾沁左翼後旗 +科尼賽克 科尼賽克 +科布多 科布多 +科布多河 科布多河 +科布多盆地 科布多盆地 +科托努 科托努 +科技示范户 科技示範戶 +科斗 科斗 +科斗书 科斗書 +科斗文 科斗文 +科特布斯 科特布斯 +科班出身 科班出身 +科目表 科目表 +科系 科系 +科纳克里 科納克里 +科罗拉多大峡谷 科羅拉多大峽谷 +科范 科範 +科迪勒拉山系 科迪勒拉山系 +科际整合 科際整合 +秒表 秒錶 +秒针 秒針 +秒钟 秒鐘 +秕谷 秕穀 +秘制 祕製 +秘录 祕錄 +租价 租價 +租借 租借 +租借人 租借人 +租借地 租借地 +租借法案 租借法案 +租借给 租借給 +租出 租出 +租出去 租出去 +秤平斗满 秤平斗滿 +秤斤注两 秤斤注兩 +秤杆 秤桿 +秤砣虽小压千斤 秤砣雖小壓千斤 +秦吉了 秦吉了 +秦少游 秦少游 +秧针 秧針 +积于忽微 積於忽微 +积云 積雲 +积分制 積分制 +积层云 積層雲 +积恶 積惡 +积恶余殃 積惡餘殃 +积恶馀殃 積惡餘殃 +积极 積極 +积极份子 積極份子 +积极参与 積極參與 +积极参加 積極參加 +积极反应 積極反應 +积极因素 積極因素 +积极型 積極型 +积极性 積極性 +积极论 積極論 +积淀 積澱 +积谷 積穀 +积谷防饥 積穀防饑 +积郁 積鬱 +积里渐里 積裏漸裏 +积金至斗 積金至斗 +积雨云 積雨雲 +称出 稱出 +称制 稱制 +称叹 稱歎 +称心满志 稱心滿志 +称念 稱念 +称愿 稱願 +称王封后 稱王封后 +称谓录 稱謂錄 +称赞 稱讚 +称赞不已 稱讚不已 +移出 移出 +移出去 移出去 +移出来 移出來 +移向 移向 +移回 移回 +移山志 移山志 +移情别恋 移情別戀 +移星换斗 移星換斗 +移植手术 移植手術 +移祸于 移禍於 +秾纤 穠纖 +秾纤合度 穠纖合度 +稀松 稀鬆 +稀松平常 稀鬆平常 +稀松骨质 稀鬆骨質 +稀释后 稀釋後 +稀里 稀里 +稀里哗啦 稀里嘩啦 +稀里打哄 稀里打哄 +稀里糊涂 稀裏糊塗 +程序修宪 程序修憲 +程序控制 程序控制 +程式控制 程式控制 +程式规划 程式規劃 +程朱 程朱 +程砚秋 程硯秋 +稍出 稍出 +稍占上风 稍占上風 +稍后 稍後 +稍干的 稍乾的 +稍感不适 稍感不適 +稍有不准 稍有不準 +稍胜一筹 稍勝一籌 +稍高于 稍高於 +税制 稅制 +税后 稅後 +税捐稽征 稅捐稽徵 +税捐稽征处 稅捐稽徵處 +税种 稅種 +税负制 稅負制 +稔恶不悛 稔惡不悛 +稠云 稠雲 +稳占 穩佔 +稳吃三注 穩吃三注 +稳坐钓鱼台 穩坐釣魚臺 +稳定物价 穩定物價 +稳当 穩當 +稳当性 穩當性 +稳扎 穩紮 +稳扎稳打 穩紮穩打 +稳操胜券 穩操勝券 +稳操胜算 穩操勝算 +稳稳当当 穩穩當當 +稳获 穩獲 +稻种 稻種 +稻穗 稻穗 +稻谷 稻穀 +稽征 稽徵 +稽征处 稽徵處 +稽征所 稽徵所 +稽核 稽覈 +穆克吉 穆克吉 +穆巴拉克 穆巴拉克 +穆斯坦西里 穆斯坦西里 +穆罕默德历 穆罕默德曆 +穆罕默德历史 穆罕默德歷史 +穗儿 穗兒 +穗子 穗子 +穗帏飘井干 繐幃飄井幹 +穗帐 繐帳 +穗帷 繐帷 +穗状 穗狀 +穗状花序 穗狀花序 +穗肥 穗肥 +穗花杉 穗花杉 +穗裳 繐裳 +穗轴 穗軸 +穗选 穗選 +穷于 窮於 +穷于应付 窮於應付 +穷兵极武 窮兵極武 +穷兵黩武 窮兵黷武 +穷冬 窮冬 +穷凶恶极 窮兇惡極 +穷凶极恶 窮兇極惡 +穷凶极虐 窮兇極虐 +穷发 窮髮 +穷困 窮困 +穷困人家 窮困人家 +穷天极地 窮天極地 +穷奢极侈 窮奢極侈 +穷奢极多 窮奢極多 +穷奢极欲 窮奢極欲 +穷妙极巧 窮妙極巧 +穷富极贵 窮富極貴 +穷尽 窮盡 +穷山恶水 窮山惡水 +穷工极巧 窮工極巧 +穷幽极微 窮幽極微 +穷当益坚 窮當益堅 +穷当益坚老当益壮 窮當益堅老當益壯 +穷形尽相 窮形盡相 +穷形极状 窮形極狀 +穷本极源 窮本極源 +穷极 窮極 +穷极其妙 窮極其妙 +穷极则变 窮極則變 +穷极无聊 窮極無聊 +穷极要妙 窮極要妙 +穷根寻叶 窮根尋葉 +穷灵尽妙 窮靈盡妙 +穷理尽性 窮理盡性 +穷纤入微 窮纖入微 +穷而后工 窮而後工 +穷贵极富 窮貴極富 +穷追不舍 窮追不捨 +穷里 窮里 +穹谷 穹谷 +空个 空個 +空中交通管制 空中交通管制 +空中交通管制员 空中交通管制員 +空中布雷 空中佈雷 +空中格斗 空中格鬥 +空中管制站 空中管制站 +空了 空了 +空优迷彩 空優迷彩 +空余 空餘 +空军航空技术学院 空軍航空技術學院 +空出 空出 +空出来 空出來 +空前绝后 空前絕後 +空前绝后后 空前絕后後 +空叹 空嘆 +空当 空當 +空当儿 空當兒 +空当子 空當子 +空心汤团 空心湯糰 +空心萝卜 空心蘿蔔 +空手而回 空手而回 +空投布雷 空投佈雷 +空杯 空杯 +空柜子 空櫃子 +空梁落燕泥 空梁落燕泥 +空气喷气发动机 空氣噴氣發動機 +空气缓冲间 空氣緩衝間 +空泛 空泛 +空疏无据 空疏無據 +空空荡荡 空空蕩蕩 +空荡 空蕩 +空荡荡 空蕩蕩 +空蒙 空濛 +空调症 空調症 +空谷 空谷 +空谷幽兰 空谷幽蘭 +空谷足音 空谷足音 +空钟 空鐘 +空间曲线 空間曲線 +空间艺术 空間藝術 +空集合 空集合 +穿不出 穿不出 +穿云裂石 穿雲裂石 +穿出 穿出 +穿出去 穿出去 +穿出来 穿出來 +穿回 穿回 +穿跟斗 穿跟斗 +穿针 穿針 +穿针引线 穿針引線 +穿针走线 穿針走線 +穿鞋的不斗赤脚的 穿鞋的不鬥赤腳的 +穿麻挂孝 穿麻掛孝 +突出 突出 +突出去 突出去 +突出来 突出來 +突出重围 突出重圍 +突升 突升 +突发 突發 +突发事件 突發事件 +突发奇想 突發奇想 +突发性 突發性 +突发状况 突發狀況 +突围而出 突圍而出 +突显出 突顯出 +突袭战术 突襲戰術 +突触后 突觸後 +窃占 竊占 +窃占罪 竊占罪 +窃幸乘宠 竊幸乘寵 +窃据 竊據 +窃钟掩耳 竊鐘掩耳 +窅娘 窅娘 +窒欲 窒慾 +窗台 窗臺 +窗台上 窗臺上 +窗帘 窗簾 +窗帘布 窗簾布 +窗明几亮 窗明几亮 +窗明几净 窗明几淨 +窘困 窘困 +窘态百出 窘態百出 +窜出 竄出 +窜升 竄升 +窜游 竄遊 +窜进窜出 竄進竄出 +窝里 窩裏 +窝里反 窩裏反 +窝里发炮 窩裏發炮 +窝里炮 窩裏炮 +窝里窝囊 窩里窩囊 +窝里翻 窩裏翻 +窝阔台 窩闊臺 +窝阔台汗 窩闊臺汗 +窝阔台汗国 窩闊臺汗國 +窟里拔蛇 窟裏拔蛇 +窥御激夫 窺御激夫 +窦太后 竇太后 +窨子里秋月 窨子裏秋月 +立了 立了 +立于 立於 +立于不败 立於不敗 +立于不败之地 立於不敗之地 +立体几何 立體幾何 +立体身历声 立體身歷聲 +立克次体 立克次體 +立冬 立冬 +立升 立升 +立宪民主党 立憲民主黨 +立当 立當 +立志 立志 +立扎 立扎 +立扫千言 立掃千言 +立方公里 立方公里 +立方厘米 立方厘米 +立杆 立杆 +立杆见影 立杆見影 +立柜 立櫃 +立氏立克次体 立氏立克次體 +立秋 立秋 +立联合医院 立聯合醫院 +立范 立範 +立面 立面 +立面图 立面圖 +竖人毛发 豎人毛髮 +竖柱上梁 豎柱上梁 +竖起脊梁 豎起脊梁 +站个 站個 +站了 站了 +站出 站出 +站出去 站出去 +站出来 站出來 +站台 站臺 +站台票 站臺票 +站干岸儿 站乾岸兒 +站柜台 站櫃檯 +竞争和聚合 競爭和聚合 +竞价 競價 +竞合现象 競合現象 +竞向 競向 +竞斗 競鬥 +竟于 竟於 +竟须 竟須 +章台 章臺 +章台杨柳 章臺楊柳 +章台柳 章臺柳 +章回 章回 +章回体 章回體 +章回小说 章回小說 +章表 章表 +童仆 童僕 +童军团 童軍團 +童蒙 童蒙 +童蒙训 童蒙訓 +童颜鹤发 童顏鶴髮 +竭尽 竭盡 +竭尽全力 竭盡全力 +竭尽力量 竭盡力量 +竭尽心思 竭盡心思 +竭尽所能 竭盡所能 +竭尽棉薄 竭盡棉薄 +竭智尽力 竭智盡力 +竭智尽忠 竭智盡忠 +竭智尽虑 竭智盡慮 +竭诚尽节 竭誠盡節 +端了 端了 +端出 端出 +端出去 端出去 +端出来 端出來 +端庄 端莊 +端杯 端杯 +端系统 端系統 +端面 端面 +竹几 竹几 +竹制 竹製 +竹叶 竹葉 +竹叶青 竹葉青 +竹叶青蛇 竹葉青蛇 +竹叶鲢 竹葉鰱 +竹布 竹布 +竹帘 竹簾 +竹席 竹蓆 +竹扣 竹扣 +竹杠 竹槓 +竹板 竹板 +竹板书 竹板書 +竹板歌 竹板歌 +竹林之游 竹林之遊 +竹笋干 竹筍乾 +竹签 竹籤 +竹篱茅舍 竹籬茅舍 +竹节虫 竹節蟲 +竹苞松茂 竹苞松茂 +竹野內丰 竹野內豐 +笃志 篤志 +笃志好学 篤志好學 +笃志爱古 篤志愛古 +笆斗 笆斗 +笋干 筍乾 +笋里不知茆里 筍裏不知茆裏 +笏板 笏板 +笑个 笑個 +笑个痛快 笑個痛快 +笑了 笑了 +笑了起来 笑了起來 +笑代表拒绝 笑代表拒絕 +笑出 笑出 +笑出来 笑出來 +笑出眼泪 笑出眼淚 +笑口弥勒 笑口彌勒 +笑容满面 笑容滿面 +笑成一团 笑成一團 +笑耍头回 笑耍頭回 +笑话百出 笑話百出 +笑里藏刀 笑裏藏刀 +笑面 笑面 +笑面夜叉 笑面夜叉 +笑面虎 笑面虎 +笔划 筆劃 +笔划检字表 筆劃檢字表 +笔力万钧 筆力萬鈞 +笔卷 筆捲 +笔参造化 筆參造化 +笔录 筆錄 +笔扫千军 筆掃千軍 +笔据 筆據 +笔杆 筆桿 +笔杆子 筆桿子 +笔秃墨干 筆禿墨乾 +笔管面 筆管麪 +笔胜于刀文比武强 筆勝於刀文比武強 +笔致 筆致 +笔迹鉴定 筆跡鑑定 +笙磬同音 笙磬同音 +笛卡儿坐标制 笛卡兒座標制 +笛布斯 笛布斯 +符号表 符號表 +符合 符合 +符合标准 符合標準 +符合美国利益 符合美國利益 +符拉迪沃斯托克 符拉迪沃斯託克 +符拉迪沃斯讬克 符拉迪沃斯託克 +符采 符采 +笨蛋挂 笨蛋掛 +第一个 第一個 +第一个层次 第一個層次 +第一信号系统 第一信號系統 +第一准备金 第一準備金 +第一出 第一齣 +第一千 第一千 +第一千万 第一千萬 +第一卷 第一卷 +第一回 第一回 +第一志愿 第一志願 +第一性征 第一性徵 +第七个 第七個 +第七出 第七齣 +第七回 第七回 +第三个 第三個 +第三出 第三齣 +第三回 第三回 +第九个 第九個 +第九出 第九齣 +第九回 第九回 +第九艺术 第九藝術 +第二个 第二個 +第二出 第二齣 +第二回 第二回 +第二性征 第二性徵 +第五个 第五個 +第五个现代化 第五個現代化 +第五出 第五齣 +第五回 第五回 +第八个 第八個 +第八出 第八齣 +第八回 第八回 +第八艺术 第八藝術 +第六个 第六個 +第六出 第六齣 +第六回 第六回 +第几 第幾 +第几个 第幾個 +第几冊 第幾冊 +第几名 第幾名 +第几回 第幾回 +第几次 第幾次 +第几章 第幾章 +第几节 第幾節 +第几课 第幾課 +第十个 第十個 +第十出 第十齣 +第十回 第十回 +第四个 第四個 +第四出 第四齣 +第四出局 第四出局 +第四台 第四臺 +第四回 第四回 +笺注 箋註 +等个 等個 +等了 等了 +等于 等於 +等于在 等於在 +等于是 等於是 +等于有 等於有 +等于零 等於零 +等价 等價 +等价交换 等價交換 +等价关系 等價關係 +等价物 等價物 +等值价格 等值價格 +等势面 等勢面 +等同 等同 +等同于 等同於 +等周不等式 等周不等式 +等效百万吨当量 等效百萬噸當量 +等级制度 等級制度 +等轴晶系 等軸晶系 +等速圆周运动 等速圓周運動 +等闲之辈 等閒之輩 +等闲人物 等閒人物 +等闲视之 等閒視之 +等高种植 等高種植 +筋斗 筋斗 +筋斗云 筋斗雲 +筋疲力尽 筋疲力盡 +筋面粉 筋麪粉 +筑前 筑前 +筑北 筑北 +筑后 筑後 +筑土墙 築土牆 +筑坛 築壇 +筑坛拜将 築壇拜將 +筑城 築城 +筑堤 築堤 +筑墙 築牆 +筑室 築室 +筑室反耕 築室反耕 +筑室道谋 築室道謀 +筑屋 築屋 +筑州 筑州 +筑巢 築巢 +筑底 築底 +筑底巷 築底巷 +筑後 筑後 +筑成 築成 +筑有 築有 +筑栏 築欄 +筑波 筑波 +筑磕 築磕 +筑筑磕磕 築築磕磕 +筑紫 筑紫 +筑肥 筑肥 +筑西 筑西 +筑起 築起 +筑路 築路 +筑路工程 築路工程 +筑邦 筑邦 +筑阳 筑陽 +筑陽 筑陽 +答不出 答不出 +答出 答出 +答出来 答出來 +答剌苏 答剌蘇 +答卷 答卷 +答复 答覆 +答录机 答錄機 +答案卷 答案卷 +策划 策劃 +策划人 策劃人 +策划者 策劃者 +筛子喂驴 篩子餵驢 +筛板 篩板 +筛检出 篩檢出 +筛选出 篩選出 +筲斗 筲斗 +筵几 筵几 +筹划 籌劃 +筹码面 籌碼面 +签上 簽上 +签上去 簽上去 +签上来 簽上來 +签下 簽下 +签下去 簽下去 +签下来 簽下來 +签书会 簽書會 +签了 簽了 +签些 簽些 +签入 簽入 +签写 簽寫 +签出 簽出 +签到 簽到 +签到处 簽到處 +签到簿 簽到簿 +签单 簽單 +签印 簽印 +签发 簽發 +签发地点 簽發地點 +签发日期 簽發日期 +签名 簽名 +签名会 簽名會 +签名信 簽名信 +签名球 簽名球 +签名簿 簽名簿 +签名运动 簽名運動 +签呈 簽呈 +签唱 簽唱 +签唱会 簽唱會 +签在 簽在 +签好 簽好 +签妥 簽妥 +签子 籤子 +签字 簽字 +签字笔 簽字筆 +签字者 簽字者 +签字费 簽字費 +签完 簽完 +签定 簽定 +签帐 簽帳 +签帐卡 簽帳卡 +签幐 籤幐 +签得 簽得 +签报 簽報 +签押 簽押 +签押房 簽押房 +签收 簽收 +签有 簽有 +签条 籤條 +签注 簽註 +签派室 簽派室 +签爲 簽爲 +签着 簽着 +签章 簽章 +签筒 籤筒 +签约 簽約 +签约人 簽約人 +签约国 簽約國 +签约奖金 簽約獎金 +签约金 簽約金 +签结 簽結 +签署 簽署 +签署人 簽署人 +签署国 簽署國 +签証 簽証 +签订 簽訂 +签证 簽證 +签证费 簽證費 +签诗 籤詩 +签语饼 簽語餅 +签赌 簽賭 +签赌案 簽賭案 +签赌站 簽賭站 +签过 簽過 +签退 簽退 +简余晏 簡余晏 +简别 簡別 +简单明了 簡單明瞭 +简历 簡歷 +简历表 簡歷表 +简字表 簡字表 +简字谱录 簡字譜錄 +简尸 簡屍 +简并 簡併 +简截了当 簡截了當 +简报导览系统 簡報導覽系統 +简易包扎法 簡易包紮法 +简易师范 簡易師範 +简朝仑 簡朝崙 +简朴 簡樸 +简板 簡板 +简氏防务周刊 簡氏防務週刊 +箕山之志 箕山之志 +箕斗 箕斗 +算不了 算不了 +算不出 算不出 +算了 算了 +算了又算 算了又算 +算准 算準 +算出 算出 +算出去 算出去 +算出来 算出來 +算历 算曆 +算发 算髮 +算得了 算得了 +算术 算術 +算术和 算術和 +算术家 算術家 +算术平均 算術平均 +算术平均数 算術平均數 +算术式 算術式 +算术级数 算術級數 +算术课 算術課 +箝制 箝制 +管不了 管不了 +管个 管個 +管乐团 管樂團 +管了 管了 +管人吊脚儿事 管人弔腳兒事 +管制 管制 +管制中心 管制中心 +管制区 管制區 +管制区域 管制區域 +管制员 管制員 +管制品 管制品 +管制塔台 管制塔臺 +管制局 管制局 +管制法 管制法 +管制点 管制點 +管制空域 管制空域 +管制站 管制站 +管制路线 管制路線 +管圆线虫 管圓線蟲 +管城回族区 管城回族區 +管家娘子 管家娘子 +管干 管幹 +管弦 管絃 +管弦乐团 管弦樂團 +管理人才 管理人才 +管理体制 管理體制 +管理系 管理系 +管理规范 管理規範 +管理资讯系统 管理資訊系統 +管辖范围 管轄範圍 +管道升 管道昇 +管闲事 管閒事 +箭不虚发 箭不虛發 +箭在弦上不得不发 箭在弦上不得不發 +箭无虚发 箭無虛發 +箭杆 箭桿 +箭虫 箭蟲 +箱帘 箱簾 +箱扣 箱釦 +箱梁 箱梁 +箱里 箱裏 +篆烟 篆煙 +篇卷 篇卷 +篡党 篡黨 +篮下三秒钟 籃下三秒鐘 +篮坛 籃壇 +篮板 籃板 +篮板王 籃板王 +篮板球 籃板球 +篮虹杯 籃虹盃 +篷盖布 篷蓋佈 +簇合 簇合 +簌簌发抖 簌簌發抖 +簪笔磬折 簪筆磬折 +簪缨世胄 簪纓世胄 +簳面杖 簳麪杖 +簸荡 簸盪 +簿历 簿歷 +簿录 簿錄 +簿据 簿據 +籧篨戚施 籧篨戚施 +米价 米價 +米克 米克 +米克杰格 米克傑格 +米克森 米克森 +米克诺斯 米克諾斯 +米利托 米利托 +米制 米制 +米卤蛋 米滷蛋 +米厘米突 米釐米突 +米德尔伯里 米德爾伯裏 +米格式战斗机 米格式戰鬥機 +米纳谷 米納谷 +米罗的维纳斯雕像 米羅的維納斯雕像 +米苏里 米蘇里 +米苏里州 米蘇里州 +米虫 米蟲 +米蛀虫 米蛀蟲 +米谷 米穀 +米里 米里 +米雅托维奇 米雅托維奇 +米雕 米雕 +米面 米麪 +类似于 類似於 +类别 類別 +类别的团体 類別的團體 +类同 類同 +类同法 類同法 +类球面 類球面 +粉丝团 粉絲團 +粉丝谷 粉絲谷 +粉团儿 粉團兒 +粉团儿似的 粉團兒似的 +粉彩 粉彩 +粉拳绣腿 粉拳繡腿 +粉板 粉板 +粉砂岩 粉砂岩 +粉签子 粉籤子 +粉红色系 粉紅色系 +粉面 粉面 +粉面朱脣 粉面硃脣 +粉面油头 粉面油頭 +粉饰门面 粉飾門面 +粒变岩 粒變岩 +粗制 粗製 +粗制品 粗製品 +粗制滥造 粗製濫造 +粗卤 粗鹵 +粗布 粗布 +粗布条 粗布條 +粗恶 粗惡 +粗枝大叶 粗枝大葉 +粗毛布 粗毛布 +粗管面 粗管麪 +粗纤维 粗纖維 +粗衣恶食 粗衣惡食 +粗面 粗麪 +粗面岩 粗面岩 +粘合剂 粘合劑 +粘板岩 粘板岩 +粜出 糶出 +粤曲 粵曲 +粤胡 粵胡 +粥厂 粥廠 +粪坑里的石头 糞坑裏的石頭 +粪秽蔑面 糞穢衊面 +粪缸里掷骰子 糞缸裏擲骰子 +粮尽援绝 糧盡援絕 +粮食平准基金 糧食平準基金 +粲夸克 粲夸克 +粽粑叶 粽粑葉 +精于 精於 +精于此道 精於此道 +精兵制 精兵制 +精准 精準 +精准度 精準度 +精制 精製 +精制品 精製品 +精制服 精制服 +精奇里江 精奇里江 +精干 精幹 +精干高效 精幹高效 +精当 精當 +精彩 精彩 +精彩度 精彩度 +精彩生动 精彩生動 +精彩逼人 精彩逼人 +精彩镜头 精彩鏡頭 +精心制作 精心製作 +精心制造 精心製造 +精心杰作 精心傑作 +精悟玄鉴 精悟玄鑑 +精明干练 精明幹練 +精明强干 精明強幹 +精明能干 精明能幹 +精松 精鬆 +精核 精核 +精炼 精煉 +精炼厂 精煉廠 +精炼炉 精煉爐 +精疲力尽 精疲力盡 +精确 精確 +精确到 精確到 +精确度 精確度 +精确性 精確性 +精神分裂症 精神分裂症 +精神官能症 精神官能症 +精神性厌食症 精神性厭食症 +精神焕发 精神煥發 +精神药物 精神藥物 +精致 精緻 +精致化 精緻化 +精致度 精緻度 +精舍 精舍 +精虫 精蟲 +精虫冲脑 精蟲衝腦 +精诚团结 精誠團結 +精辟 精闢 +精通于 精通於 +精采 精采 +精采度 精采度 +精采绝伦 精采絕倫 +精金百炼 精金百煉 +精雕 精雕 +精雕细刻 精雕細刻 +精雕细琢 精雕細琢 +精雕细镂 精雕細鏤 +糅合 糅合 +糊口 餬口 +糊涂 糊塗 +糊涂一时 糊塗一時 +糊涂帐 糊塗帳 +糊涂油蒙心 糊塗油蒙心 +糊涂虫 糊塗蟲 +糊涂蛋 糊塗蛋 +糊涂账 糊塗賬 +糊糊涂涂 糊糊塗塗 +糊里糊涂 糊里糊塗 +糕干 糕乾 +糖厂 糖廠 +糖堆里养的 糖堆裏養的 +糖炒栗子 糖炒栗子 +糖萝卜 糖蘿蔔 +糖醋里脊 糖醋里脊 +糙叶树 糙葉樹 +糙面内质网 糙面內質網 +糟了 糟了 +糟糕了 糟糕了 +糟透了 糟透了 +糟齿类爬虫 糟齒類爬蟲 +糠穗 糠穗 +糯米团 糯米糰 +系一片 係一片 +系一番 係一番 +系一种 係一種 +系一线 繫一線 +系上 繫上 +系世 繫世 +系丝带 繫絲帶 +系个 繫個 +系为 係爲 +系主任 系主任 +系了 繫了 +系争 係爭 +系争物 係爭物 +系于 繫於 +系于一发 繫於一髮 +系住 繫住 +系出名门 系出名門 +系刊 系刊 +系列 系列 +系列化 系列化 +系列战 系列戰 +系列放大器 系列放大器 +系列片 系列片 +系列电视剧 系列電視劇 +系列赛 系列賽 +系列里 系列裏 +系到 繫到 +系务 系務 +系发带 繫髮帶 +系命 繫命 +系囚 繫囚 +系头巾 繫頭巾 +系好 繫好 +系孙 系孫 +系学会 系學會 +系带 繫帶 +系心 繫心 +系念 繫念 +系怀 繫懷 +系恋 繫戀 +系所 系所 +系扣 係扣 +系指 係指 +系捻儿 繫捻兒 +系数 係數 +系族 系族 +系有 繫有 +系条 繫條 +系泊 繫泊 +系爪 繫爪 +系爲 係爲 +系牢 繫牢 +系狱 繫獄 +系璧 系璧 +系留 繫留 +系着 繫着 +系系 繫系 +系紧 繫緊 +系累 繫累 +系结 繫結 +系统 系統 +系统分析 系統分析 +系统分类 系統分類 +系统化 系統化 +系统发育 系統發育 +系统商 系統商 +系统图 系統圖 +系统工程 系統工程 +系统性 系統性 +系统抽样法 系統抽樣法 +系统流程图 系統流程圖 +系统理论 系統理論 +系统级 系統級 +系统论 系統論 +系统设计 系統設計 +系统软体 系統軟體 +系统部 系統部 +系绳 繫繩 +系缆 繫纜 +系缚 繫縛 +系而不食 繫而不食 +系胄 系胄 +系腰 繫腰 +系臂 係臂 +系臂之宠 繫臂之寵 +系船桩 繫船樁 +系花 系花 +系获 係獲 +系裤子 繫褲子 +系裹 繫裹 +系词 系詞 +系谱 系譜 +系趾 繫趾 +系踵 係踵 +系蹄 係蹄 +系辞 繫辭 +系里 系裏 +系铃人 繫鈴人 +系铃解铃 繫鈴解鈴 +系鞋带 繫鞋帶 +系颈 繫頸 +系颈阙庭 係頸闕庭 +系风捕影 繫風捕影 +系风捕景 繫風捕景 +系馆 系館 +系马 繫馬 +素借 素藉 +素发 素髮 +素志 素志 +素愿 素願 +素未谋面 素未謀面 +素朴 素樸 +素面 素面 素麪 +素面朝天 素面朝天 +素食面 素食麪 +素餐尸位 素餐尸位 +索价 索價 +索价过高 索價過高 +索克 索克 +索克斯队 索克斯隊 +索合 索合 +索夫克 索夫克 +索夫克郡 索夫克郡 +索尔兹伯里平原 索爾茲伯里平原 +索尔兹伯里石环 索爾茲伯里石環 +索尽枯肠 索盡枯腸 +索托 索托 +索杰纳 索傑納 +索福克勒斯 索福克勒斯 +索福克里斯 索福克裏斯 +索里亚 索里亞 +索里士 索里士 +索面 索麪 +索馬里 索馬里 +索马里 索馬里 +索马里亚 索馬里亞 +紧了 緊了 +紧关里 緊關裏 +紧密配合 緊密配合 +紧急制动 緊急制動 +紧急集合 緊急集合 +紧扣 緊扣 +紧挨 緊挨 +紧溜子里 緊溜子裏 +紧系 緊繫 +紧绷 緊繃 +紧绷着 緊繃着 +紧绷绷 緊繃繃 +紧致 緊緻 +紧追不舍 緊追不捨 +紧随其后 緊隨其後 +紫云 紫雲 +紫云乡 紫雲鄉 +紫云苗族布依族自治县 紫云苗族布依族自治縣 +紫云英 紫雲英 +紫台 紫臺 +紫姜 紫薑 +紫微斗数 紫微斗數 +紫穗槐 紫穗槐 +紫苏 紫蘇 +紫苏属 紫蘇屬 +紫苏梅 紫蘇梅 +紫药水 紫藥水 +紫金山天文台 紫金山天文臺 +累了 累了 +累囚 累囚 +累块积苏 累塊積蘇 +累堆 累堆 +累瓦结绳 累瓦結繩 +累积性伤害症候群 累積性傷害症候羣 +累积折耗 累積折耗 +累绁 累紲 +累臣 累臣 +絣扒吊拷 絣扒吊拷 +綑了 綑了 +綑吊 綑吊 +綑扎 綑紮 +緝凶 緝兇 +縻系 縻繫 +繁台 繁臺 +繁复 繁複 +繁征博引 繁徵博引 +繁殖系数 繁殖係數 +繁钟 繁鐘 +繃价 繃價 +繃针 繃針 +纂修 纂修 +纂胄 纂胄 +纠合 糾合 +纡余 紆餘 +纡回 紆迴 +纡曲 紆曲 +纡朱怀金 紆朱懷金 +纡郁 紆鬱 +红丝暗系 紅絲暗繫 +红中白板 紅中白板 +红了 紅了 +红云 紅雲 +红光满面 紅光滿面 +红冬冬 紅鼕鼕 +红发 紅髮 +红发女郎 紅髮女郎 +红叶 紅葉 +红叶之题 紅葉之題 +红叶少棒队 紅葉少棒隊 +红叶村 紅葉村 +红叶杯 紅葉盃 +红叶树 紅葉樹 +红叶题诗 紅葉題詩 +红头发 紅頭髮 +红姑娘 紅姑娘 +红娘 紅娘 +红孩症 紅孩症 +红尘万丈 紅塵萬丈 +红岩 紅巖 +红得发紫 紅得發紫 +红日当午 紅日當午 +红曲 紅曲 +红杏出墙 紅杏出牆 +红杠 紅槓 +红松 紅松 +红极一时 紅極一時 +红牙板 紅牙板 +红眼症 紅眼症 +红种人 紅種人 +红紫乱朱 紅紫亂朱 +红绳系足 紅繩繫足 +红胡子 紅鬍子 +红色娘子军 紅色娘子軍 +红色系 紅色系 +红花绿叶 紅花綠葉 +红药 紅藥 +红药水 紅藥水 +红萝卜 紅蘿蔔 +红萝卜炒辣椒 紅蘿蔔炒辣椒 +红虫 紅蟲 +红运当头 紅運當頭 +红醋栗 紅醋栗 +红钟 紅鐘 +红铃虫 紅鈴蟲 +红霉素 紅黴素 +红面番鸭 紅面番鴨 +红须绿眼 紅鬚綠眼 +纤不盈握 纖不盈握 +纤丽 纖麗 +纤云 纖雲 +纤人 纖人 +纤介 纖介 +纤体 纖體 +纤儿 纖兒 +纤夫 縴夫 +纤妍 纖妍 +纤密 纖密 +纤小 纖小 +纤尘不染 纖塵不染 +纤屑 纖屑 +纤巧 纖巧 +纤度 纖度 +纤弱 纖弱 +纤微 纖微 +纤悉 纖悉 +纤悉无遗 纖悉無遺 +纤户 縴戶 +纤手 纖手 縴手 +纤指 纖指 +纤柔 纖柔 +纤毛 纖毛 +纤毛动力蛋白 纖毛動力蛋白 +纤毛虫 纖毛蟲 +纤毛运动 纖毛運動 +纤毫 纖毫 +纤玉 纖玉 +纤画 纖畫 +纤瘦 纖瘦 +纤离 纖離 +纤秾中度 纖穠中度 +纤纤 纖纖 +纤纤弱质 纖纖弱質 +纤纤玉手 纖纖玉手 +纤细 纖細 +纤细画 纖細畫 +纤维 纖維 +纤维丛 纖維叢 +纤维光学 纖維光學 +纤维化 纖維化 +纤维囊泡症 纖維囊泡症 +纤维工业 纖維工業 +纤维板 纖維板 +纤维植物 纖維植物 +纤维状 纖維狀 +纤维素 纖維素 +纤维细胞 纖維細胞 +纤维肌痛 纖維肌痛 +纤维胶 纖維膠 +纤维蛋原 纖維蛋原 +纤维蛋白 纖維蛋白 +纤维蛋白原 纖維蛋白原 +纤维质 纖維質 +纤维镜 纖維鏡 +纤维长度 纖維長度 +纤美 纖美 +纤腰 纖腰 +纤芥不遗 纖芥不遺 +纤芯直径 纖芯直徑 +纤长 纖長 +纤阿 纖阿 +约克 約克 +约克夏 約克夏 +约克夏猪 約克夏豬 +约克曼 約克曼 +约克维奇 約克維奇 +约克郡 約克郡 +约出 約出 +约占 約佔 +约同 約同 +约当现金 約當現金 +约据 約據 +约柜 約櫃 +约核 約核 +约等于 約等於 +约维克 約維克 +约翰参书 約翰參書 +约翰松 約翰松 +级任制 級任制 +级别 級別 +纪元后 紀元後 +纪历 紀曆 +纪录 紀錄 +纪录下来 紀錄下來 +纪录创造者 紀錄創造者 +纪录器 紀錄器 +纪录点 紀錄點 +纪录片 紀錄片 +纪录片儿 紀錄片兒 +纪录片奖 紀錄片獎 +纪录表 紀錄表 +纪念 紀念 +纪念周 紀念週 +纪里谷 紀里谷 +纯属巧合 純屬巧合 +纯情蜡妹 純情蠟妹 +纯朴 純樸 +纯种 純種 +纯种牛 純種牛 +纯种马 純種馬 +纱厂 紗廠 +纱布 紗布 +纱布口罩 紗布口罩 +纱布绷带 紗布繃帶 +纲鉴 綱鑑 +纳修斯 納修斯 +纳克希班迪 納克希班迪 +纳合 納合 +纳吉布 納吉布 +纳奇录异 納奇錄異 +纳征 納徵 +纳德阿里 納德阿里 +纳扎尔巴耶夫 納扎爾巴耶夫 +纳斯达克 納斯達克 +纳杰夫 納傑夫 +纳波里塔诺 納波里塔諾 +纳米技术 納米技術 +纳粹党 納粹黨 +纳莉台风 納莉颱風 +纳采 納采 +纵出 縱出 +纵切面 縱切面 +纵剖面 縱剖面 +纵向 縱向 +纵情恣欲 縱情恣欲 +纵情遂欲 縱情遂欲 +纵断面 縱斷面 +纵曲枉直 縱曲枉直 +纵梁 縱梁 +纵横交布 縱橫交佈 +纵欲 縱慾 +纵欲主义 縱慾主義 +纵欲无度 縱慾無度 +纵谷 縱谷 +纵谷区 縱谷區 +纷如烟 紛如煙 +纸制 紙製 +纸卷子 紙卷子 +纸叶子 紙葉子 +纸团 紙團 +纸尿布 紙尿布 +纸扎 紙紮 +纸扎店 紙紮店 +纸杯 紙杯 +纸板 紙板 +纸板盒 紙板盒 +纸板部 紙板部 +纸浆厂 紙漿廠 +纸烟 紙菸 +纸雕 紙雕 +纸面 紙面 +纹光针 紋光針 +纹板 紋板 +纹面 紋面 +纺纤 紡纖 +纺纱厂 紡紗廠 +纺织厂 紡織廠 +纺织娘 紡織娘 +纺锤虫 紡錘蟲 +纽几内亚 紐幾內亞 +纽华克 紐華克 +纽扣 鈕釦 +纽瓦克 紐瓦克 +纽芬兰与拉布拉多 紐芬蘭與拉布拉多 +纽蒙特 紐蒙特 +纾困 紓困 +线上 線上 +线团 線團 +线性系统 線性系統 +线性规划 線性規劃 +线虫 線蟲 +练习曲 練習曲 +练了 練了 +练出 練出 +练出来 練出來 +练团室 練團室 +练声曲 練聲曲 +练武术 練武術 +组党 組黨 +组别 組別 +组合 組合 +组合为 組合爲 +组合式 組合式 +组合成 組合成 +组合数学 組合數學 +组合服装 組合服裝 +组合法 組合法 +组合而成 組合而成 +组合菜 組合菜 +组合论 組合論 +组合语言 組合語言 +组合音响 組合音響 +组团 組團 +组曲 組曲 +组立式建筑 組立式建築 +组里 組裏 +细不容发 細不容髮 +细别 細別 +细叶山茶 細葉山茶 +细叶脉 細葉脈 +细叶金午时花 細葉金午時花 +细咽 細嚥 +细如发 細如髮 +细娘 細娘 +细布 細布 +细曲 細曲 +细术 細術 +细炼 細鍊 +细胞周期 細胞週期 +细胞融合 細胞融合 +细致 細緻 +细致入微 細緻入微 +细蒙蒙 細濛濛 +细表 細表 +细袅袅 細嫋嫋 +细针密缕 細針密縷 +细雨蒙蒙 細雨濛濛 +细雨蒙蒙忆当年 細雨濛濛憶當年 +织出 織出 +织布 織布 +织布厂 織布廠 +织布娘 織布娘 +织布机 織布機 +织席 織蓆 +织当访婢 織當訪婢 +织锦回文 織錦回文 +终了 終了 +终于 終於 +终制 終制 +终南别业 終南別業 +终归于 終歸於 +终曲 終曲 +终极 終極 +终极杀阵 終極殺陣 +终极目标 終極目標 +终止症 終止症 +终焉之志 終焉之志 +终端台 終端檯 +终而复始 終而復始 +终身有托 終身有托 +终须 終須 +绉布 縐布 +经世之才 經世之才 +经世致用 經世致用 +经丝彩色显花 經絲彩色顯花 +经典动力系统 經典動力系統 +经制 經制 +经卷 經卷 +经厂本 經廠本 +经历 經歷 +经历过 經歷過 +经历风雨 經歷風雨 +经发 經發 +经合会 經合會 +经合组织 經合組織 +经国之才 經國之才 +经坛 經壇 +经折 經摺 +经折装 經摺裝 +经曲 經曲 +经有云 經有云 +经术 經術 +经板儿 經板兒 +经济之才 經濟之才 +经济体制 經濟體制 +经济体系 經濟體系 +经济制度 經濟制度 +经济制裁 經濟制裁 +经济协力开发机构 經濟協力開發機構 +经济发展 經濟發展 +经济合作与发展组织 經濟合作與發展組織 +经济合作开发组织 經濟合作開發組織 +经济周期 經濟週期 +经济困境 經濟困境 +经济技术 經濟技術 +经济槓杆 經濟槓桿 +经济系 經濟系 +经济范畴 經濟範疇 +经济落后 經濟落後 +经济计划 經濟計劃 +经济部标准检验局 經濟部標準檢驗局 +经济面 經濟面 +经营决策资讯系统 經營決策資訊系統 +经营范围 經營範圍 +经贸关系 經貿關係 +经验丰富 經驗豐富 +绑回 綁回 +绑回去 綁回去 +绑回来 綁回來 +绑扎 綁紮 +绒布 絨布 +结了 結了 +结仇 結仇 +结伙 結夥 +结伙抢劫 結夥搶劫 +结伴同游 結伴同遊 +结伴同行 結伴同行 +结余 結餘 +结党 結黨 +结党聚群 結黨聚羣 +结党营私 結黨營私 +结党连群 結黨連羣 +结出 結出 +结制 結制 +结发 結髮 +结发事师 結髮事師 +结发人 結髮人 +结发夫妻 結髮夫妻 +结合 結合 +结合为 結合爲 +结合体 結合體 +结合剂 結合劑 +结合实际 結合實際 +结合律 結合律 +结合成 結合成 +结合模型 結合模型 +结合水 結合水 +结合点 結合點 +结合线 結合線 +结合能 結合能 +结合起来 結合起來 +结合过程 結合過程 +结合韵 結合韻 +结合韵母 結合韻母 +结彩 結綵 +结扎 結紮 +结扎手术 結紮手術 +结扎术 結紮術 +结托 結托 +结扣 結釦 +结晶岩 結晶岩 +结核 結核 +结核杆菌 結核桿菌 +结梁子 結樑子 +结汇 結匯 +结汇证 結匯證 +结草虫 結草蟲 +结采 結采 +绕回 繞回 +绕梁 繞樑 +绕梁三日 繞樑三日 +绕梁之音 繞樑之音 +绕梁韵永 繞樑韻永 +绘事后素 繪事後素 +绘出 繪出 +绘制 繪製 +绘制图 繪製圖 +绘图板 繪圖板 +绘彩陶 繪彩陶 +绘画板 繪畫板 +绘里 繪里 +给个棒锤当针认 給個棒錘當針認 +给于 給於 +给价 給價 +给出 給出 +给我干脆 給我干脆 +给药 給藥 +绚丽多彩 絢麗多彩 +绚烂归于平淡 絢爛歸於平淡 +络合 絡合 +络合物 絡合物 +络绎于途 絡繹於途 +络腮胡 絡腮鬍 +络腮胡子 絡腮鬍子 +绝不相同 絕不相同 +绝世出尘 絕世出塵 +绝于 絕於 +绝后 絕後 +绝后光前 絕後光前 +绝后患 絕後患 +绝后计 絕後計 +绝对参照 絕對參照 +绝岩 絕巖 +绝才 絕才 +绝望已极 絕望已極 +绝症 絕症 +绝种 絕種 +绝缘台 絕緣檯 +绞刑台 絞刑臺 +绞包针 絞包針 +绞尽 絞盡 +绞尽脑汁 絞盡腦汁 +绞干 絞乾 +绞面 絞面 +统一党 統一黨 +统一发票 統一發票 +统一规划 統一規劃 +统一规范 統一規範 +统一计划 統一計劃 +统制 統制 +统合 統合 +统合体 統合體 +统合力 統合力 +统合性 統合性 +统御 統御 +统筹规划 統籌規劃 +统计出 統計出 +统计制图 統計製圖 +统计图表 統計圖表 +统计数据 統計數據 +统计表 統計表 +绢布版 絹布版 +绣像 繡像 +绣出 繡出 +绣口 繡口 +绣帘 繡簾 +绣得 繡得 +绣户 繡戶 +绣房 繡房 +绣毯 繡毯 +绣球 繡球 +绣的 繡的 +绣花 繡花 +绣花针 繡花針 +绣花针儿 繡花針兒 +绣虎雕龙 繡虎雕龍 +绣衣 繡衣 +绣衣御史 繡衣御史 +绣衣朱履 繡衣朱履 +绣起 繡起 +绣针 繡針 +绣阁 繡閣 +绣面 繡面 +绣鞋 繡鞋 +绦虫 絛蟲 +绦虫纲 絛蟲綱 +继天立极 繼天立極 +继志 繼志 +继志述事 繼志述事 +继续干 繼續幹 +绪余 緒餘 +绪胄 緒胄 +续借 續借 +续借手续 續借手續 +续发性 續發性 +续发感染 續發感染 +续后 續後 +续后汉书 續後漢書 +续弦 續絃 +续杯 續杯 +续签 續簽 +续西游记 續西遊記 +续通志 續通志 +绮云 綺雲 +绮想曲 綺想曲 +绰板 綽板 +绰板婆 綽板婆 +绰绰有余 綽綽有餘 +绳扣 繩釦 +维修 維修 +维修区 維修區 +维修费 維修費 +维克 維克 +维克佛瑞柏加 維克佛瑞柏加 +维克利 維克利 +维克托 維克托 +维几尼亚 維幾尼亞 +维几尼亚州 維幾尼亞州 +维基数据 維基數據 +维基物种 維基物種 +维多利亚瀑布 維多利亞瀑布 +维多里欧 維多里歐 +维尔布鲁根 維爾布魯根 +维斗 維斗 +维斯杯 維斯杯 +维杰辛 維傑辛 +维科扬斯克 維科揚斯克 +维系 維繫 +维系人心 維繫人心 +维苏威 維蘇威 +维苏威火山 維蘇威火山 +绵中刺笑里刀 綿中刺笑裏刀 +绵历 綿歷 +绵延不尽 綿延不盡 +绵里藏针 綿裏藏針 +绵里针 綿裏針 +绷住 繃住 +绷场面 繃場面 +绷子 繃子 +绷巴吊拷 繃巴吊拷 +绷带 繃帶 +绷开 繃開 +绷扒吊拷 繃扒吊拷 +绷爬吊拷 繃爬吊拷 +绷紧 繃緊 +绷脸 繃臉 +绸布 綢布 +绸缎庄 綢緞莊 +综合 綜合 +综合业务数字网 綜合業務數字網 +综合体 綜合體 +综合医院 綜合醫院 +综合叙述 綜合敘述 +综合型 綜合型 +综合存款 綜合存款 +综合布线 綜合佈線 +综合平衡 綜合平衡 +综合征 綜合徵 +综合性 綜合性 +综合所得 綜合所得 +综合所得税 綜合所得稅 +综合扩大机 綜合擴大機 +综合报导 綜合報導 +综合报道 綜合報道 +综合服务数位网络 綜合服務數位網絡 +综合杂志 綜合雜誌 +综合法 綜合法 +综合症 綜合症 +综合类 綜合類 +综合艺术 綜合藝術 +综合语 綜合語 +综合课 綜合課 +综合银行 綜合銀行 +综合防治 綜合防治 +综合险 綜合險 +综核 綜覈 +综艺团 綜藝團 +绽出 綻出 +绽放出 綻放出 +绾发 綰髮 +绿云 綠雲 +绿党 綠黨 +绿发 綠髮 +绿叶 綠葉 +绿叶成荫 綠葉成蔭 +绿叶成阴 綠葉成陰 +绿暗红稀 綠暗紅稀 +绿松石 綠松石 +绿游网 綠遊網 +绿烟红雾 綠煙紅霧 +绿蜡 綠蠟 +绿衣黄里 綠衣黃裏 +绿鬓朱颜 綠鬢朱顏 +缀出 綴出 +缀合 綴合 +缆索吊椅 纜索吊椅 +缉凶 緝兇 +缉获 緝獲 +缎面 緞面 +缓了 緩了 +缓冲 緩衝 +缓冲体 緩衝體 +缓冲作用 緩衝作用 +缓冲剂 緩衝劑 +缓冲区 緩衝區 +缓冲器 緩衝器 +缓冲国 緩衝國 +缓冲地 緩衝地 +缓冲地区 緩衝地區 +缓冲地带 緩衝地帶 +缓冲式 緩衝式 +缓冲期 緩衝期 +缓冲液 緩衝液 +缓冲溶液 緩衝溶液 +缓发中子 緩發中子 +缓征 緩徵 +缔造出 締造出 +缕当 縷當 +编个 編個 +编了 編了 +编余 編余 +编余人员 編餘人員 +编修 編修 +编写出 編寫出 +编出 編出 +编出来 編出來 +编列出 編列出 +编制 編制 編製 +编制成 編製成 +编制法 編制法 +编发 編髮 +编录 編錄 +编曲 編曲 +编构出 編構出 +编注 編注 +编目表格 編目表格 +编目记录 編目記錄 +编目记录输入 編目記錄輸入 +编码系统 編碼系統 +编码表 編碼表 +编算出 編算出 +编织出 編織出 +编选出 編選出 +编造出 編造出 +编采 編採 +编钟 編鐘 +缘分已尽 緣分已盡 +缙云 縉雲 +缙云县 縉雲縣 +缜致 縝緻 +缝个 縫個 +缝了 縫了 +缝制 縫製 +缝制成 縫製成 +缝合 縫合 +缝合处 縫合處 +缝合带 縫合帶 +缝合线 縫合線 +缝衣针 縫衣針 +缝里 縫裏 +缝针 縫針 +缝针补线 縫針補線 +缝针迹 縫針跡 +缠回 纏回 +缠斗 纏鬥 +缣缃黄卷 縑緗黃卷 +缥致 縹致 +缦胡 縵胡 +缩合 縮合 +缩回 縮回 +缩回去 縮回去 +缩回来 縮回來 +缩影微卷 縮影微捲 +缩成一团 縮成一團 +缩栗 縮慄 +缪种流传 繆種流傳 +缮修 繕修 +缱绻难舍 繾綣難捨 +缴不出来 繳不出來 +缴出 繳出 +缴出去 繳出去 +缴出来 繳出來 +缴卷 繳卷 +缴回 繳回 +缴白卷 繳白卷 +缴获 繳獲 +缺乏症 缺乏症 +缺氧症 缺氧症 +缺课纪录 缺課紀錄 +缾沉簪折 缾沉簪折 +罂粟种子 罌粟種子 +罄尽 罄盡 +网上杂志 網上雜誌 +网坛 網壇 +网坛史 網壇史 +网布 網布 +网开一面 網開一面 +网开三面 網開三面 +网御 網禦 +网志 網誌 +网志上 網誌上 +网扣 網扣 +网游 網遊 +网状系统 網狀系統 +网管系统 網管系統 +网络技术 網絡技術 +网络操作系统 網絡操作系統 +网络游戏 網絡遊戲 +网络管理系统 網絡管理系統 +网络规划人员 網絡規劃人員 +网罗人才 網羅人才 +网里 網裏 +网际电台 網際電臺 +罔极 罔極 +罗克耶 羅克耶 +罗兴梁 羅興樑 +罗圣杰 羅聖傑 +罗宗胜 羅宗勝 +罗密欧与朱丽叶 羅密歐與朱麗葉 +罗密欧与茱丽叶 羅密歐與茱麗葉 +罗布 羅布 +罗布林卡 羅布林卡 +罗布森 羅布森 +罗布泊 羅布泊 +罗布麻 羅布麻 +罗式几何 羅式幾何 +罗彦杰 羅彥傑 +罗德里奎兹 羅德里奎茲 +罗德里格兹 羅德里格茲 +罗德里格斯 羅德里格斯 +罗德里盖兹 羅德里蓋茲 +罗志恩 羅志恩 +罗志明 羅志明 +罗志祥 羅志祥 +罗志良 羅志良 +罗拉巴克 羅拉巴克 +罗斯托克 羅斯托克 +罗斯托夫 羅斯托夫 +罗时丰 羅時豐 +罗曼蒂克 羅曼蒂克 +罗杰 羅傑 +罗杰斯 羅傑斯 +罗杰斯杯 羅傑斯杯 +罗柜 羅櫃 +罗氏几何 羅氏幾何 +罗汉松 羅漢松 +罗盘针 羅盤針 +罗素克洛 羅素克洛 +罗胡斯 羅胡斯 +罗致 羅致 +罗致人材 羅致人材 +罗致政 羅致政 +罗蒙诺索 羅蒙諾索 +罗西里尼 羅西里尼 +罗迪克 羅迪克 +罗马建筑 羅馬建築 +罗马里奥 羅馬裏奧 +罚不当罪 罰不當罪 +罚个 罰個 +罚了 罰了 +罚出 罰出 +罚出去 罰出去 +罚出来 罰出來 +罢了 罷了 +罢于 罷於 +罢于奔命 罷於奔命 +罢黜百家独尊儒术 罷黜百家獨尊儒術 +罩杯 罩杯 +罪大恶极 罪大惡極 +罪当万死 罪當萬死 +罪恶 罪惡 +罪恶如山 罪惡如山 +罪恶感 罪惡感 +罪恶深重 罪惡深重 +罪恶滔天 罪惡滔天 +罪恶累累 罪惡累累 +罪恶行径 罪惡行徑 +罪恶贯盈 罪惡貫盈 +罪证确凿 罪證確鑿 +罪该万死 罪該萬死 +置之死地而后生 置之死地而後生 +置之脑后 置之腦後 +置于 置於 +置信系数 置信係數 +置换术 置換術 +置物柜 置物櫃 +置言成范 置言成範 +羁系 羈繫 +羊卜 羊卜 +羊布婚 羊布婚 +羊拐 羊拐 +羊毛出在羊身上 羊毛出在羊身上 +羊瘙痒症 羊瘙癢症 +羊群里跑出骆驼来 羊羣裏跑出駱駝來 +羊肉落在狗嘴里 羊肉落在狗嘴裏 +羊膜穿刺术 羊膜穿刺術 +羊舍 羊舍 +羊角面包 羊角麪包 +羊须疮 羊鬚瘡 +羌胡 羌胡 +美不胜收 美不勝收 +美丑 美醜 +美于 美於 +美仑 美崙 +美仑美奂 美侖美奐 +美利坚合众国 美利堅合衆國 +美制 美製 +美占 美佔 +美发 美髮 +美发业 美髮業 +美发师 美髮師 +美发店 美髮店 +美台 美臺 +美后 美后 +美国制 美國製 +美国参议院 美國參議院 +美国国际开发总署 美國國際開發總署 +美国国际集团 美國國際集團 +美国在台协会 美國在臺協會 +美国存托凭证 美國存託憑證 +美国总统报复权 美國總統報復權 +美国标准交换码 美國標準交換碼 +美国谷 美國谷 +美国资讯交换标准码 美國資訊交換標準碼 +美女如云 美女如雲 +美娇娘 美嬌娘 +美容手术 美容手術 +美容术 美容術 +美恶 美惡 +美才 美才 +美日关系 美日關係 +美术 美術 +美术史 美術史 +美术品 美術品 +美术商 美術商 +美术大师 美術大師 +美术字 美術字 +美术家 美術家 +美术灯 美術燈 +美术班 美術班 +美术电影 美術電影 +美术界 美術界 +美术系 美術系 +美术节 美術節 +美术设计 美術設計 +美术课 美術課 +美术馆 美術館 +美林集团 美林集團 +美沙冬 美沙冬 +美泽鉴人 美澤鑑人 +美洲杯 美洲盃 +美白针 美白針 +美穗 美穗 +美系 美系 +美耐板 美耐板 +美苏 美蘇 +美苏关系 美蘇關係 +美里 美里 +美里达 美里達 +羑里 羑里 +羚羊挂角 羚羊掛角 +羞于 羞於 +羞于启齿 羞於啓齒 +羞以牛后 羞以牛後 +羞恶 羞惡 +羞恶之心 羞惡之心 +羞愧难当 羞愧難當 +羞面见人 羞面見人 +羡余 羨餘 +羡叹 羨歎 +群丑 羣醜 +群众关系 羣衆關係 +群众团体 羣衆團體 +群后 羣后 +群系 羣系 +群而不党 羣而不黨 +群谋咸同 羣謀咸同 +群轻折轴 羣輕折軸 +群辟 羣辟 +群雕 羣雕 +羹里来饭里去 羹裏來飯裏去 +羽毛丰满 羽毛豐滿 +羽毛未丰 羽毛未豐 +羽状复叶 羽狀複葉 +羽翼丰满 羽翼豐滿 +羽翼已丰 羽翼已豐 +羽虫 羽蟲 +翁同和 翁同和 +翁同龢 翁同龢 +翁山苏姬 翁山蘇姬 +翁干晃 翁乾晃 +翁郁容 翁郁容 +翊赞 翊贊 +翕辟 翕闢 +翘了 翹了 +翘出 翹出 +翘出去 翹出去 +翘出来 翹出來 +翘曲 翹曲 +翘翘板 翹翹板 +翠云裘 翠雲裘 +翡翠谷 翡翠谷 +翦彩 翦綵 +翰墨志 翰墨志 +翱游 翱遊 +翱游四海 翱遊四海 +翻个 翻個 +翻了 翻了 +翻云复雨 翻雲覆雨 +翻云覆雨 翻雲覆雨 +翻修 翻修 +翻出 翻出 +翻出去 翻出去 +翻出来 翻出來 +翻卷 翻卷 +翻台 翻檯 +翻复 翻覆 +翻复无常 翻覆無常 +翻天复地 翻天覆地 +翻录 翻錄 +翻手为云 翻手爲雲 +翻手为云覆手变雨 翻手爲雲覆手變雨 +翻手作云复手雨 翻手作雲覆手雨 +翻把恩人当仇人 翻把恩人當仇人 +翻新后 翻新後 +翻来吊去 翻來吊去 +翻来复去 翻來覆去 +翻松 翻鬆 +翻空出奇 翻空出奇 +翻筋斗 翻筋斗 +翻箱倒柜 翻箱倒櫃 +翻觔斗 翻觔斗 +翻译出 翻譯出 +翻跟斗 翻跟斗 +翻过筋斗 翻過筋斗 +翻面 翻面 +翻面皮 翻面皮 +翾风回雪 翾風迴雪 +老不修 老不修 +老个 老個 +老了 老了 +老于 老於 +老于世故 老於世故 +老人失智症 老人失智症 +老人癡呆症 老人癡呆症 +老仆 老僕 +老克 老克 +老八板儿 老八板兒 +老几 老幾 +老医少卜 老醫少卜 +老千 老千 +老古板 老古板 +老台 老臺 +老合儿 老合兒 +老同学 老同學 +老向 老向 +老和尚撞钟 老和尚撞鐘 +老咬虫 老咬蟲 +老大娘 老大娘 +老太婆的裹脚布 老太婆的裹腳布 +老奴才 老奴才 +老奸 老奸 +老奸巨猾 老奸巨猾 +老姑娘 老姑娘 +老姜 老薑 +老娘 老孃 +老娘儿 老孃兒 +老婆娘 老婆娘 +老婆当军 老婆當軍 +老实的终须在 老實的終須在 +老少一同 老少一同 +老少咸宜 老少咸宜 +老干 老幹 +老干妈 老乾媽 +老干部 老幹部 +老年性痴呆症 老年性癡呆症 +老年痴呆症 老年癡呆症 +老年癡呆症 老年癡呆症 +老幼咸宜 老幼咸宜 +老庄 老莊 +老当 老當 +老当益壮 老當益壯 +老态龙钟 老態龍鍾 +老斗 老斗 +老板 老闆 +老板人 老闆人 +老板娘 老闆娘 +老板家 老闆家 +老残游记 老殘遊記 +老气横秋 老氣橫秋 +老烟枪 老煙槍 +老烟鬼 老煙鬼 +老熊当道 老熊當道 +老爷钟 老爺鐘 +老猫鼻子上挂咸鱼 老貓鼻子上掛鹹魚 +老王卖瓜自卖自夸 老王賣瓜自賣自誇 +老皇历 老皇曆 +老米饭捏杀不成团 老米飯捏殺不成團 +老糊涂 老糊塗 +老罴当道 老羆當道 +老而弥坚 老而彌堅 +老胡 老胡 +老腌儿 老醃兒 +老腌瓜 老醃瓜 +老臊胡 老臊胡 +老舍 老舍 +老蒙 老懞 +老虎挂念佛珠 老虎掛念佛珠 +老虎生了翅膀一般 老虎生了翅膀一般 +老蚌出明珠 老蚌出明珠 +老表 老表 +老起面皮 老起面皮 +老郎庵 老郎庵 +老雕 老鵰 +老面子 老面子 +老面孔 老面孔 +老面皮 老面皮 +老骥伏枥志在千里 老驥伏櫪志在千里 +老鸹窝里出凤凰 老鴰窩裏出鳳凰 +考个 考個 +考了 考了 +考信录 考信錄 +考克斯 考克斯 +考出 考出 +考出来 考出來 +考前考后 考前考後 +考卷 考卷 +考卷纸 考卷紙 +考后 考後 +考察团 考察團 +考征 考徵 +考据 考據 +考据学 考據學 +考核 考覈 +考种 考種 +考虑不周 考慮不周 +考虑周到 考慮周到 +考试卷 考試卷 +考试卷子 考試卷子 +考试范围 考試範圍 +者回 者回 +而于 而於 +而云 而云 +而今而后 而今而後 +而克制 而剋制 +而后 而後 +而回 而回 +而胜于蓝 而勝於藍 +耍奸 耍奸 +耍得团团转 耍得團團轉 +耍斗 耍鬥 +耍笔杆 耍筆桿 +耍老千 耍老千 +耐克 耐克 +耐冬 耐冬 +耐多药 耐多藥 +耐多药结核病 耐多藥結核病 +耐热合金 耐熱合金 +耐药性 耐藥性 +耕九余三 耕九餘三 +耕作制度 耕作制度 +耕佣 耕傭 +耕出 耕出 +耕前耡后 耕前耡後 +耕地面积 耕地面積 +耕当问奴 耕當問奴 +耕当问奴织当访婢 耕當問奴織當訪婢 +耕御路 耕御路 +耕种 耕種 +耕获 耕穫 +耗尽 耗盡 +耘荡 耘盪 +耦合 耦合 +耳余 耳餘 +耳刮子 耳刮子 +耳卜 耳卜 +耳后 耳後 +耳提面命 耳提面命 +耳提面训 耳提面訓 +耳朵里冒出脚来 耳朵裏冒出腳來 +耳朵里响 耳朵裏響 +耳檐儿当不的胡帽 耳檐兒當不的胡帽 +耳沈 耳沈 +耳目之欲 耳目之欲 +耳红面赤 耳紅面赤 +耳针 耳針 +耶娘 耶孃 +耶烈万 耶烈萬 +耶稣升天节 耶穌升天節 +耶稣基督后期圣徒教会 耶穌基督後期聖徒教會 +耸了 聳了 +耸了耸 聳了聳 +耸了耸肩 聳了聳肩 +耸入云霄 聳入雲霄 +耸出 聳出 +耻居王后 恥居王後 +耽于 耽於 +耿于 耿於 +耿耿于心 耿耿於心 +耿耿于怀 耿耿於懷 +聊个 聊個 +聊个天 聊個天 +聊个痛快 聊個痛快 +聊复备数 聊復備數 +聊复尔尔 聊復爾爾 +聊复尔耳 聊復爾耳 +聊斋志异 聊齋志異 +聊胜一筹 聊勝一籌 +聊胜于无 聊勝於無 +聊表 聊表 +聊表寸心 聊表寸心 +聊表心意 聊表心意 +聋哑症 聾啞症 +聋虫 聾蟲 +职业代表制 職業代表制 +职业倦怠症 職業倦怠症 +职业团体 職業團體 +职业水准 職業水準 +职别 職別 +职员录 職員錄 +职员表 職員表 +职志 職志 +职权范围 職權範圍 +职系 職系 +职能范围 職能範圍 +职责范围 職責範圍 +聒噪 聒噪 +联于 聯於 +联体别墅 聯體別墅 +联准会 聯準會 +联发科 聯發科 +联合 聯合 +联合企业 聯合企業 +联合会 聯合會 +联合体 聯合體 +联合作战 聯合作戰 +联合促销 聯合促銷 +联合党 聯合黨 +联合內阁 聯合內閣 +联合公报 聯合公報 +联合军演 聯合軍演 +联合利华 聯合利華 +联合制 聯合制 +联合包裹服务公司 聯合包裹服務公司 +联合参谋 聯合參謀 +联合发表 聯合發表 +联合号 聯合號 +联合国 聯合國 +联合国日 聯合國日 +联合国案 聯合國案 +联合声明 聯合聲明 +联合大学 聯合大學 +联合战线 聯合戰線 +联合报 聯合報 +联合报系 聯合報系 +联合政府 聯合政府 +联合文学 聯合文學 +联合晚报 聯合晚報 +联合机 聯合機 +联合演习 聯合演習 +联合王国 聯合王國 +联合目录 聯合目錄 +联合社 聯合社 +联合组织 聯合組織 +联合自强 聯合自強 +联合舰队 聯合艦隊 +联合行 聯合行 +联合行动 聯合行動 +联合通讯社 聯合通訊社 +联合部队 聯合部隊 +联想集团 聯想集團 +联机游戏 聯機遊戲 +联盟党 聯盟黨 +联盟杯 聯盟杯 +联系 聯繫 +联系实际 聯繫實際 +联系方式 聯繫方式 +联系汇率 聯繫匯率 +联系群众 聯繫羣衆 +联赛杯 聯賽盃 +联邦制 聯邦制 +聘任制 聘任制 +聘姑娘 聘姑娘 +聘雇 聘僱 +聚了 聚了 +聚合 聚合 +聚合体 聚合體 +聚合作用 聚合作用 +聚合反应 聚合反應 +聚合果 聚合果 +聚合物 聚合物 +聚合脢 聚合脢 +聚合资讯订阅 聚合資訊訂閱 +聚合起来 聚合起來 +聚合酶 聚合酶 +聚药雄蕊 聚葯雄蕊 +聚酯纤维 聚酯纖維 +聪了 聰了 +聪明一世糊涂一时 聰明一世糊塗一時 +聪明才智 聰明才智 +肃北蒙古族自治县 肅北蒙古族自治縣 +肆奸植党 肆奸植黨 +肆志 肆志 +肇因于 肇因於 +肉丝面 肉絲麪 +肉吊窗 肉吊窗 +肉干 肉乾 +肉松 肉鬆 +肉松罐头 肉鬆罐頭 +肉欲 肉慾 +肉欲主义 肉慾主義 +肉毒杆菌 肉毒桿菌 +肉毒杆菌毒素 肉毒桿菌毒素 +肉毒梭状芽孢杆菌 肉毒梭狀芽孢桿菌 +肉汤面 肉湯麪 +肉穗花序 肉穗花序 +肉羹面 肉羹麪 +肉袒面缚 肉袒面縛 +肉里钱 肉裏錢 +肉重千斤 肉重千斤 +肌原纤维 肌原纖維 +肌理丰盈 肌理豐盈 +肌纤维 肌纖維 +肌纤蛋白 肌纖蛋白 +肌肉发达 肌肉發達 +肌肉松弛剂 肌肉鬆弛劑 +肌肉注射 肌肉注射 +肌肉萎缩症 肌肉萎縮症 +肌腺症 肌腺症 +肐膊只折在袖子里 肐膊只折在袖子裏 +肐膊折了往袖子里藏 肐膊折了往袖子裏藏 +肘后方 肘後方 +肘手链足 肘手鍊足 +肚儿里有勾当 肚兒裏有勾當 +肚子里点灯 肚子裏點燈 +肚皮里 肚皮裏 +肚肠阁落里边 肚腸閣落裏邊 +肚里 肚裏 +肚里一轮 肚裏一輪 +肚里寻思 肚裏尋思 +肚里明白 肚裏明白 +肚里的蛔虫 肚裏的蛔蟲 +肚里说不出来的苦 肚裏說不出來的苦 +肚里踌躇 肚裏躊躇 +肚里雷鸣 肚裏雷鳴 +肚饥 肚飢 +肝吸虫 肝吸蟲 +肝脏 肝臟 +肝脑涂地 肝腦塗地 +肝郁 肝鬱 +肠系膜 腸繫膜 +肠胃药 腸胃藥 +肠脏 腸臟 +股东特别大会 股東特別大會 +股价 股價 +股价指数 股價指數 +股份制 股份制 +股栗 股慄 +股栗肤粟 股栗膚粟 +股票价值 股票價值 +股票价格 股票價格 +股票投资获利率 股票投資獲利率 +肢体冲突 肢體衝突 +肤发 膚髮 +肥了 肥了 +肥冬瘦年 肥冬瘦年 +肥料厂 肥料廠 +肥水不过别人田 肥水不過別人田 +肥皂 肥皂 +肥皂劇 肥皂剧 +肥皂泡 肥皂泡 +肥皂粉 肥皂粉 +肥皂絲 肥皂丝 +肥皂莢 肥皂荚 +肥筑方言 肥筑方言 +肥胖症 肥胖症 +肥虫蚁 肥蟲蟻 +肩并肩 肩並肩 +肩须拍 肩須拍 +肮肮脏脏 骯骯髒髒 +肮脏 骯髒 +肮脏鬼 骯髒鬼 +肯出 肯出 +肯出去 肯出去 +肯出来 肯出來 +肯回 肯回 +肯回去 肯回去 +肯回来 肯回來 +肯定并例句 肯定並例句 +肯干 肯幹 +肯干啊 肯幹啊 +肯德瑞克 肯德瑞克 +育乐台 育樂臺 +育才 育才 +育种 育種 +肴馔 餚饌 +肺出血 肺出血 +肺叶 肺葉 +肺吸虫 肺吸蟲 +肺尘矽症 肺塵矽症 +肺炎克雷伯氏菌 肺炎克雷伯氏菌 +肺脏 肺臟 +肽链 肽鏈 +肾脏 腎臟 +肾脏炎 腎臟炎 +肾脏病 腎臟病 +肾脏癌 腎臟癌 +肾脏科 腎臟科 +肿大症 腫大症 +肿瘤切除术 腫瘤切除術 +胁制 脅制 +胃出血 胃出血 +胃脏 胃臟 +胃药 胃藥 +胃药片 胃藥片 +胃里 胃裏 +胄嗣 胄嗣 +胄子 胄子 +胄序 胄序 +胄族 胄族 +胄甲 冑甲 +胄监 胄監 +胄科 冑科 +胄绪 胄緒 +胄胤 胄胤 +胄裔 胄裔 +胄裔繁衍 胄裔繁衍 +胄阀 胄閥 +胆大于天 膽大於天 +胆大如斗 膽大如斗 +胆石症 膽石症 +背人 揹人 +背他 揹他 +背你 揹你 +背來 揹來 +背债 揹債 +背出 背出 +背出去 揹出去 +背出来 背出來 揹出來 +背前背后 背前背後 +背包 揹包 +背包袱 揹包袱 +背后 背後 +背向 背向 揹向 +背回 揹回 +背回家去 揹回家去 +背地里 背地裏 +背城借一 背城借一 +背她 揹她 +背小孩 揹小孩 +背山面水 背山面水 +背带 揹帶 +背我 揹我 +背暗投明 背暗投明 +背板 背板 +背梁骨 背梁骨 +背榜 揹榜 +背物 揹物 +背着 揹着 +背筐 揹筐 +背篓 揹簍 +背胶布 背膠布 +背负 揹負 +背走 揹走 +背酸 背痠 +背面 背面 +背风面 背風面 +背饥荒 揹饑荒 +胎发 胎髮 +胎里坏 胎裏壞 +胎里富 胎裏富 +胎里毒 胎裏毒 +胎里素 胎裏素 +胎面 胎面 +胖姑娘坐小轿儿 胖姑娘坐小轎兒 +胚叶 胚葉 +胚胎发生 胚胎發生 +胚胎干 胚胎幹 +胜不骄 勝不驕 +胜不骄败不馁 勝不驕敗不餒 +胜之不武 勝之不武 +胜乐金刚 勝樂金剛 +胜了 勝了 +胜事 勝事 +胜于 勝於 +胜仗 勝仗 +胜任 勝任 +胜任愉快 勝任愉快 +胜任能力 勝任能力 +胜会 勝會 +胜似 勝似 +胜出 勝出 +胜利 勝利 +胜利在望 勝利在望 +胜利归来 勝利歸來 +胜利投手 勝利投手 +胜利果实 勝利果實 +胜利组 勝利組 +胜利者 勝利者 +胜利队 勝利隊 +胜券 勝券 +胜券在握 勝券在握 +胜博殿 勝博殿 +胜在 勝在 +胜地 勝地 +胜境 勝境 +胜局 勝局 +胜常 勝常 +胜得 勝得 +胜投数 勝投數 +胜投王 勝投王 +胜景 勝景 +胜朝 勝朝 +胜概 勝概 +胜残去杀 勝殘去殺 +胜流 勝流 +胜游 勝遊 +胜率 勝率 +胜的 勝的 +胜算 勝算 +胜肽 胜肽 +胜落袋 勝落袋 +胜衣 勝衣 +胜诉 勝訴 +胜负 勝負 +胜负乃兵家常事 勝負乃兵家常事 +胜败 勝敗 +胜败乃兵家常事 勝敗乃兵家常事 +胜跡 勝跡 +胜过 勝過 +胜过一个诸葛亮 勝過一個諸葛亮 +胜迹 勝蹟 +胜部 勝部 +胜部冠军 勝部冠軍 +胜键 胜鍵 +胞子虫 胞子蟲 +胞芽杯 胞芽杯 +胡三 胡三 +胡三省 胡三省 +胡为慎 胡爲慎 +胡乐 胡樂 +胡乱 胡亂 +胡二巴越 胡二巴越 +胡云 胡云 +胡亥 胡亥 +胡人 胡人 +胡伶 胡伶 +胡佛 胡佛 +胡作非为 胡作非爲 +胡佩兰 胡佩蘭 +胡佳 胡佳 +胡侃 胡侃 +胡做乔为 胡做喬爲 +胡儿 胡兒 +胡元辉 胡元輝 +胡克 胡克 +胡克定律 胡克定律 +胡克斯特拉 胡克斯特拉 +胡兰成 胡蘭成 +胡力 胡力 +胡匪 鬍匪 +胡卢 胡盧 +胡卢提 胡盧提 +胡厮哄 胡廝哄 +胡厮混 胡廝混 +胡厮缠 胡廝纏 +胡吃海喝 胡吃海喝 +胡吃闷睡 胡吃悶睡 +胡同 衚衕 +胡吣 胡唚 +胡吹 胡吹 +胡吹乱捧 胡吹亂捧 +胡吹乱滂 胡吹亂滂 +胡吹乱诌 胡吹亂謅 +胡吹大气 胡吹大氣 +胡哨 胡哨 +胡喷 胡噴 +胡嘈 胡嘈 +胡噜 胡嚕 +胡国强 胡國強 +胡图族 胡圖族 +胡天 胡天 +胡天胡地 胡天胡地 +胡天胡帝 胡天胡帝 +胡夫 胡夫 +胡如虹 胡如虹 +胡姑姑 胡姑姑 +胡姓 胡姓 +胡姬花 胡姬花 +胡婷婷 胡婷婷 +胡子 鬍子 +胡子工程 鬍子工程 +胡子拉碴 鬍子拉碴 +胡子昂 胡子昂 +胡子渣 鬍子渣 +胡子阿姨 鬍子阿姨 +胡孟轩 胡孟軒 +胡学东 胡學東 +胡宁 胡寧 +胡安国 胡安國 +胡宝元 胡寶元 +胡富雄 胡富雄 +胡幼伟 胡幼偉 +胡幼凤 胡幼鳳 +胡幼幼 胡幼幼 +胡床 胡牀 +胡建雄 胡建雄 +胡弄局 胡弄局 +胡彦斌 胡彥斌 +胡德夫 胡德夫 +胡心夫 胡心夫 +胡志强 胡志強 +胡志明 胡志明 +胡志明市 胡志明市 +胡志隆 胡志隆 +胡忠信 胡忠信 +胡思 胡思 +胡思乱想 胡思亂想 +胡思乱量 胡思亂量 +胡惟庸 胡惟庸 +胡想 胡想 +胡慧中 胡慧中 +胡才勇 胡才勇 +胡扑掩 胡撲掩 +胡扑搭 胡撲搭 +胡打海摔 胡打海摔 +胡托莫 胡托莫 +胡扯 胡扯 +胡扯八溜 胡扯八溜 +胡扯淡 胡扯淡 +胡技烜 胡技烜 +胡抡混闹 胡掄混鬧 +胡拉混扯 胡拉混扯 +胡拨四 胡撥四 +胡掳 胡擄 +胡掳忙乱 胡擄忙亂 +胡搅 胡攪 +胡搅蛮缠 胡攪蠻纏 +胡搞 胡搞 +胡支对 胡支對 +胡支扯叶 胡支扯葉 +胡敲 胡敲 +胡旋舞 胡旋舞 +胡晓菁 胡曉菁 +胡服 胡服 +胡朴安 胡樸安 +胡来 胡來 +胡杨 胡楊 +胡杰 胡杰 +胡林翼 胡林翼 +胡枝子 胡枝子 +胡柏 胡柏 +胡某 胡某 +胡柴 胡柴 +胡根班德 胡根班德 +胡桃 胡桃 +胡桃木 胡桃木 +胡桃科 胡桃科 +胡桃钳 胡桃鉗 +胡桃钳组曲 胡桃鉗組曲 +胡桐 胡桐 +胡梢 鬍梢 +胡梦卜 胡夢卜 +胡梯 胡梯 +胡椒 胡椒 +胡椒子 胡椒子 +胡椒属 胡椒屬 +胡椒盐 胡椒鹽 +胡椒粉 胡椒粉 +胡椒粒 胡椒粒 +胡椒薄荷 胡椒薄荷 +胡椒面 胡椒麪 +胡椒饼 胡椒餅 +胡歌 胡歌 +胡歌野调 胡歌野調 +胡母敬 胡母敬 +胡汉民 胡漢民 +胡浩德 胡浩德 +胡海 胡海 +胡海峰 胡海峯 +胡涂 胡塗 +胡涂虫 胡塗蟲 +胡淑贞 胡淑貞 +胡混 胡混 +胡清晖 胡清暉 +胡渣 鬍渣 +胡温新政 胡溫新政 +胡渰 胡渰 +胡燕妮 胡燕妮 +胡爲 胡爲 +胡牌 胡牌 +胡猜 胡猜 +胡琴 胡琴 +胡琴儿 胡琴兒 +胡瑗 胡瑗 +胡瓜 胡瓜 +胡瓜鱼 胡瓜魚 +胡碴子 鬍碴子 +胡祖庆 胡祖慶 +胡秦 胡秦 +胡突 胡突 +胡立宗 胡立宗 +胡笙 胡笙 +胡笳 胡笳 +胡笳十八拍 胡笳十八拍 +胡粉 胡粉 +胡素秋 胡素秋 +胡紫微 胡紫微 +胡紫薇 胡紫薇 +胡编乱造 胡編亂造 +胡缠 胡纏 +胡羼 胡羼 +胡耀邦 胡耀邦 +胡耈 胡耈 +胡胜川 胡勝川 +胡胜正 胡勝正 +胡胡卢卢 胡胡盧盧 +胡自强 胡自強 +胡臭 胡臭 +胡芦巴 胡蘆巴 +胡荽 胡荽 +胡萝卜 胡蘿蔔 +胡萝卜就烧酒 胡蘿蔔就燒酒 +胡萝卜汁 胡蘿蔔汁 +胡萝卜素 胡蘿蔔素 +胡蓝之狱 胡藍之獄 +胡蔓草 胡蔓草 +胡蔓藤 胡蔓藤 +胡虏 胡虜 +胡蜂 胡蜂 +胡蝶梦 胡蝶夢 +胡行 胡行 +胡行乱作 胡行亂作 +胡言 胡言 +胡言乱语 胡言亂語 +胡言汉语 胡言漢語 +胡讲 胡講 +胡诌 胡謅 +胡诌乱傍 胡謅亂傍 +胡诌乱扯 胡謅亂扯 +胡诌乱说 胡謅亂說 +胡诌乱道 胡謅亂道 +胡诌八扯 胡謅八扯 +胡话 胡話 +胡语 胡語 +胡说 胡說 +胡说乱语 胡說亂語 +胡说乱道 胡說亂道 +胡说八道 胡說八道 +胡说散道 胡說散道 +胡说白道 胡說白道 +胡豆 胡豆 +胡赖 胡賴 +胡越 胡越 +胡越一家 胡越一家 +胡适 胡適 +胡适之 胡適之 +胡适纪念馆 胡適紀念館 +胡遮刺 胡遮刺 +胡鄂公 胡鄂公 +胡里胡涂 胡里胡塗 +胡金龙 胡金龍 +胡铨 胡銓 +胡铺搭 胡鋪搭 +胡锦涛 胡錦濤 +胡长豪 胡長豪 +胡闹 胡鬧 +胡雕刺 胡雕刺 +胡雪岩 胡雪巖 +胡须 鬍鬚 +胡须渣 鬍鬚渣 +胡颓子 胡頹子 +胡风 胡風 +胡饼 胡餅 +胡马 胡馬 +胡马依北风 胡馬依北風 +胡骑 胡騎 +胡髭 鬍髭 +胡髯 鬍髯 +胡麻 胡麻 +胡麻油 胡麻油 +胡麻籽 胡麻籽 +胤胄 胤胄 +胰淀粉酶 胰澱粉酶 +胰脏 胰臟 +胰脏炎 胰臟炎 +胰脏癌 胰臟癌 +胶卷 膠捲 +胶原纤维 膠原纖維 +胶合 膠合 +胶合板 膠合板 +胶布 膠布 +胶布膏 膠布膏 +胶彩画 膠彩畫 +胶莱谷地 膠萊谷地 +胸廓切开术 胸廓切開術 +胸怀坦荡 胸懷坦蕩 +胸怀大志 胸懷大志 +胸无大志 胸無大志 +胸杯 胸杯 +胸罗万象 胸羅萬象 +胸部手术 胸部手術 +胸针 胸針 +胼胚种 胼胚種 +能借 能借 +能克制 能剋制 +能力范围 能力範圍 +能干 能幹 +能干巴巴 能乾巴巴 +能干扰 能干擾 +能干杯 能乾杯 +能干涉 能干涉 +能干着急 能乾着急 +能干耗 能乾耗 +能干脆 能乾脆 +能干预 能干預 +能征善战 能征善戰 +能征惯战 能征慣戰 +能愿动词 能願動詞 +能自制 能自制 +能舍 能捨 +能说不能干 能說不能幹 +脂漏性角化症 脂漏性角化症 +脆快了当 脆快了當 +脆谷乐 脆穀樂 +脉不制肉 脈不制肉 +脉冲 脈衝 +脉冲光 脈衝光 +脉冲式 脈衝式 +脉冲数 脈衝數 +脉冲星 脈衝星 +脉冲电磁场 脈衝電磁場 +脉冲雷达 脈衝雷達 +脉岩 脈岩 +脊梁 脊樑 +脊梁背 脊樑背 +脊梁骨 脊樑骨 +脊百合 脊百合 +脏东西 髒東西 +脏乱 髒亂 +脏乱点 髒亂點 +脏了 髒了 +脏兮兮 髒兮兮 +脏发 髒髮 +脏器 臟器 +脏土 髒土 +脏字 髒字 +脏字儿 髒字兒 +脏弹 髒彈 +脏得 髒得 +脏心 髒心 +脏死 髒死 +脏水 髒水 +脏污 髒污 +脏病 髒病 +脏的 髒的 +脏脏 髒髒 +脏腑 臟腑 +脏词 髒詞 +脏话 髒話 +脏钱 髒錢 +脑出血 腦出血 +脑前额叶 腦前額葉 +脑力激荡 腦力激盪 +脑力激荡术 腦力激盪術 +脑力激荡法 腦力激盪法 +脑后 腦後 +脑回 腦回 +脑子里 腦子裏 +脑干 腦幹 +脑成像技术 腦成像技術 +脑海里 腦海裏 +脑震荡 腦震盪 +脓团 膿團 +脚价 腳價 +脚划船 腳划船 +脚后跟 腳後跟 +脚夫 腳伕 +脚底板 腳底板 +脚底板儿 腳底板兒 +脚扣 腳釦 +脚板 腳板 +脚注 腳註 +脚炼 腳鍊 +脚踏板 腳踏板 +脚酸 腳痠 +脚面 腳面 +脣似抹朱 脣似抹硃 +脣如涂朱 脣如塗朱 +脣彩盘 脣彩盤 +脣若抹朱 脣若抹硃 +脣若涂朱 脣若塗硃 +脣若涂脂 脣若塗脂 +脱不了 脫不了 +脱不了身 脫不了身 +脱了 脫了 +脱党 脫黨 +脱出 脫出 +脱出重围 脫出重圍 +脱发 脫髮 +脱发剂 脫髮劑 +脱口而出 脫口而出 +脱困 脫困 +脱尽 脫盡 +脱帽致敬 脫帽致敬 +脱离不了 脫離不了 +脱离关系 脫離關係 +脱蜡 脫蠟 +脱衣舞娘 脫衣舞娘 +脱谷机 脫穀機 +脱身而出 脫身而出 +脱轨而出 脫軌而出 +脱颎而出 脫熲而出 +脱颖而出 脫穎而出 +脸上挂了招牌 臉上掛了招牌 +脸都绿了 臉都綠了 +脸面 臉面 +脸面之情 臉面之情 +脸面无光 臉面無光 +脺脏 脺臟 +脾脏 脾臟 +腊之以为饵 腊之以爲餌 +腊味 臘味 +腊尽 臘盡 +腊尽冬残 臘盡冬殘 +腊斯克 臘斯克 +腊笔 臘筆 +腌䐶 腌䐶 +腌制 醃製 +腌成 醃成 +腌汁 醃汁 +腌泡 醃泡 +腌渍 醃漬 +腌渍物 醃漬物 +腌猪肉 醃豬肉 +腌肉 醃肉 +腌腊 醃臘 +腌臜 腌臢 +腌菜 醃菜 +腌起来 醃起來 +腌过 醃過 +腌酱瓜 醃醬瓜 +腌里巴臜 腌裏巴臢 +腌鱼 醃魚 +腌鱼肉 醃魚肉 +腌黄瓜 醃黃瓜 +腐余 腐餘 +腐女军团 腐女軍團 +腐干 腐乾 +腐恶 腐惡 +腐肠之药 腐腸之藥 +腑脏 腑臟 +腕表 腕錶 +腕道症候群 腕道症候羣 +腕隧道症 腕隧道症 +腕隧道症候群 腕隧道症候羣 +腥黑穗病 腥黑穗病 +腮托 腮托 +腮斗 腮斗 +腰一卷 腰一捲 +腰布 腰布 +腰扣 腰釦 +腰杆 腰桿 +腰杆子 腰桿子 +腰板 腰板 +腰板儿 腰板兒 +腰板脖硬 腰板脖硬 +腰柜 腰櫃 +腰椎间盘突出 腰椎間盤突出 +腰椎间盘突出症 腰椎間盤突出症 +腰系 腰繫 +腰缠万贯 腰纏萬貫 +腰酸 腰痠 +腰里 腰裏 +腰里硬 腰裏硬 +腰间系 腰間繫 +腹板 腹板 +腹泻药 腹瀉藥 +腹语术 腹語術 +腹里 腹裏 +腹面 腹面 +腻了 膩了 +腻云 膩雲 +腼面 靦面 +腼颜事仇 靦顏事仇 +腼颜借命 靦顏借命 +腾云 騰雲 +腾云跨风 騰雲跨風 +腾云驾雾 騰雲駕霧 +腾冲 騰衝 +腾冲县 騰衝縣 +腾出 騰出 +腾出来 騰出來 +腾升 騰昇 +腾捷飞升 騰捷飛升 +腾格里 騰格里 +腾格里山 騰格里山 +腾格里沙漠 騰格裏沙漠 +腾格里湖 騰格里湖 +腿后腱 腿後腱 +腿酸 腿痠 +膊风板 膊風板 +膏药 膏藥 +膏药旗 膏藥旗 +膨土岩 膨土岩 +膨松 膨鬆 +膨松剂 膨鬆劑 +膨胀系数 膨脹係數 +膻中 膻中 +膻中穴 膻中穴 +臀位取胎术 臀位取胎術 +臂一卷 臂一捲 +臣仆 臣僕 +臣服于 臣服於 +臣服于心 臣服於心 +臥云 臥雲 +臥游 臥遊 +臥狼当道 臥狼當道 +臥薪尝胆 臥薪嘗膽 +臧获 臧獲 +臧谷亡羊 臧穀亡羊 +自个 自個 +自个儿 自個兒 +自主旅游 自主旅遊 +自主游 自主遊 +自主系统 自主系統 +自了 自了 +自于 自於 +自交系 自交系 +自修 自修 +自修室 自修室 +自修法 自修法 +自修课 自修課 +自出 自出 +自出一家 自出一家 +自出机杼 自出機杼 +自制 自制 自製 +自制一下 自制一下 +自制下来 自制下來 +自制之力 自制之力 +自制之能 自制之能 +自制力 自制力 +自制炸弹 自製炸彈 +自制的能 自制的能 +自制能力 自制能力 +自动化技术 自動化技術 +自动恢复 自動恢復 +自动挂挡 自動掛擋 +自动控制 自動控制 +自动柜员机 自動櫃員機 +自动离合 自動離合 +自动自发 自動自發 +自动表 自動錶 +自动资料处理系统 自動資料處理系統 +自助旅游 自助旅遊 +自助游 自助遊 +自卖自夸 自賣自誇 +自发 自發 +自发对称破缺 自發對稱破缺 +自发性 自發性 +自发电位 自發電位 +自发运动 自發運動 +自叹 自嘆 +自同寒蝉 自同寒蟬 +自后 自後 +自夸 自誇 +自学成才 自學成才 +自尽 自盡 +自度曲 自度曲 +自当 自當 +自愿 自願 +自愿书 自願書 +自愿就学方案 自願就學方案 +自愿性 自願性 +自愿者 自願者 +自我表现 自我表現 +自我评价 自我評價 +自来水厂 自來水廠 +自核 自核 +自此以后 自此以後 +自此而后 自此而後 +自民党 自民黨 +自治制 自治制 +自注 自注 +自点曲 自點曲 +自然卷 自然捲 +自然历 自然歷 +自然接种 自然接種 +自由亚洲电台 自由亞洲電臺 +自由党 自由黨 +自由意志 自由意志 +自由意志主义 自由意志主義 +自由斗士 自由鬥士 +自由民主党 自由民主黨 +自由组合 自由組合 +自由组合规律 自由組合規律 +自由面 自由面 +自种 自種 +自觉自愿 自覺自願 +自诒伊戚 自詒伊戚 +自贻伊戚 自貽伊戚 +自赞 自贊 +自适 自適 +自适应 自適應 +自选曲 自選曲 +自采 自採 +自闭症 自閉症 +自须 自須 +自食恶果 自食惡果 +自驾汽车出租 自駕汽車出租 +自鸣钟 自鳴鐘 +臭不可当 臭不可當 +臭局 臭侷 +臭气冲天 臭氣沖天 +臭气熏天 臭氣熏天 +臭熏熏 臭燻燻 +臭虫 臭蟲 +至于 至於 +至当 至當 +至愚极陋 至愚極陋 +至极 至極 +至矣尽矣 至矣盡矣 +致上 致上 +致书 致書 +致乱 致亂 +致于 致於 +致仕 致仕 +致以 致以 +致使 致使 +致使动词 致使動詞 +致冷 致冷 +致冷劑 致冷劑 +致函 致函 +致力 致力 +致力于 致力於 +致命 致命 +致命伤 致命傷 +致命处 致命處 +致命性 致命性 +致哀 致哀 +致奠 致奠 +致密 緻密 +致富 致富 +致富之道 致富之道 +致师 致師 +致志 致志 +致思 致思 +致意 致意 +致政 致政 +致敬 致敬 +致歉 致歉 +致死 致死 +致死剂量 致死劑量 +致死性 致死性 +致死性毒剂 致死性毒劑 +致死案 致死案 +致死率 致死率 +致死量 致死量 +致残 致殘 +致理 致理 +致理商专 致理商專 +致理商业专科学校 致理商業專科學校 +致理技术学院 致理技術學院 +致用 致用 +致电 致電 +致畸 緻畸 +致疑 致疑 +致病 致病 +致病性 致病性 +致病菌 致病菌 +致癌 致癌 +致癌物 致癌物 +致癌物质 致癌物質 +致知 致知 +致祭 致祭 +致胜 致勝 +致胜率 致勝率 +致胜球 致勝球 +致获 致獲 +致词 致詞 +致详 致詳 +致语 致語 +致谢 致謝 +致贺 致賀 +致赠 致贈 +致身 致身 +致辞 致辭 +致远 致遠 +致远任重 致遠任重 +致送 致送 +臻于 臻於 +臻于完善 臻於完善 +臻于郅治 臻於郅治 +舂谷 舂穀 +舄卤 舄鹵 +舆台 輿臺 +舆地志 輿地志 +舆尸 輿尸 +舆志 輿志 +舌一卷 舌一捲 +舌叶 舌葉 +舌叶音 舌葉音 +舌后 舌後 +舌尖后音 舌尖後音 +舌干唇焦 舌乾脣焦 +舌干脣焦 舌乾脣焦 +舌面 舌面 +舌面元音 舌面元音 +舌面前音 舌面前音 +舌面后音 舌面後音 +舌面如镜 舌面如鏡 +舌面音 舌面音 +舍下 舍下 +舍下他 捨下他 +舍下你 捨下你 +舍下她 捨下她 +舍下我 捨下我 +舍不得 捨不得 +舍亲 舍親 +舍人 舍人 +舍出 捨出 +舍利 舍利 +舍利佛 舍利佛 +舍利塔 舍利塔 +舍利子 舍利子 +舍利子塔 舍利子塔 +舍利弗 舍利弗 +舍去 捨去 +舍命 捨命 +舍命救人 捨命救人 +舍堕 捨墮 +舍妹 舍妹 +舍姪 舍姪 +舍安就危 捨安就危 +舍实 捨實 +舍实求虚 捨實求虛 +舍己 捨己 +舍己为人 捨己爲人 +舍己为公 捨己爲公 +舍己为国 捨己爲國 +舍己从人 捨己從人 +舍己就人 捨己就人 +舍己成人 捨己成人 +舍己救人 捨己救人 +舍己芸人 捨己芸人 +舍弃 捨棄 +舍弗勒 舍弗勒 +舍弟 舍弟 +舍得 捨得 +舍德主义 舍德主義 +舍我其谁 捨我其誰 +舍我复谁 捨我復誰 +舍旧迎新 捨舊迎新 +舍本 捨本 +舍本事末 捨本事末 +舍本逐末 捨本逐末 +舍本问末 捨本問末 +舍正从邪 捨正從邪 +舍死忘生 捨死忘生 +舍生 捨生 +舍生取义 捨生取義 +舍生存义 舍生存義 +舍生忘死 捨生忘死 +舍监 舍監 +舍短从长 捨短從長 +舍短取长 捨短取長 +舍短录长 捨短錄長 +舍短用长 捨短用長 +舍身 捨身 +舍身为国 捨身爲國 +舍身图报 捨身圖報 +舍身报国 捨身報國 +舍身救人 捨身救人 +舍身求法 捨身求法 +舍车保帅 捨車保帥 +舍近务远 捨近務遠 +舍近即远 捨近即遠 +舍近求远 捨近求遠 +舍近谋远 捨近謀遠 +舍间 舍間 +舒卷 舒捲 +舒卷自如 舒捲自如 +舒发 舒發 +舒适 舒適 +舒适度 舒適度 +舒适性 舒適性 +舒适感 舒適感 +舒适音 舒適音 +舒马克 舒馬克 +舔干淨 舔乾淨 +舞出 舞出 +舞台 舞臺 +舞台剧 舞臺劇 +舞台区 舞臺區 +舞台戏 舞臺戲 +舞台效果 舞臺效果 +舞台秀 舞臺秀 +舞台艺术 舞臺藝術 +舞台音乐 舞臺音樂 +舞后 舞后 +舞团 舞團 +舞娘 舞娘 +舞曲 舞曲 +舞榭歌台 舞榭歌臺 +舞水端里 舞水端里 +舞蹈团 舞蹈團 +舞蹈症 舞蹈症 +舞蹈系 舞蹈系 +舟曲 舟曲 +舟曲县 舟曲縣 +舢板 舢板 +舢板运动 舢板運動 +航发中心 航發中心 +航发会 航發會 +航向 航向 +航太系 航太系 +航海历 航海曆 +航海历史 航海歷史 +航海年表 航海年表 +航海日志 航海日誌 +航班表 航班表 +航空术 航空術 +航空母舰战斗群 航空母艦戰鬥羣 +舰只 艦隻 +舳舻千里 舳艫千里 +船不漏针漏针没外人 船不漏針漏針沒外人 +船厂 船廠 +船只 船隻 +船台 船臺 +船员表 船員表 +船夫 船伕 +船娘 船孃 +船期表 船期表 +船板 船板 +船载的金银填不满烟花债 船載的金銀填不滿煙花債 +船边交货价 船邊交貨價 +船钟 船鐘 +艇甲板 艇甲板 +艎板 艎板 +艨冲 艨衝 +良价 良价 +良心发现 良心發現 +良游 良遊 +良田万顷不如薄艺随身 良田萬頃不如薄藝隨身 +良种 良種 +良种繁育 良種繁育 +良药 良藥 +良药苦口 良藥苦口 +良药苦口忠言逆耳 良藥苦口忠言逆耳 +艰困 艱困 +艰巨 艱鉅 +艰苦备尝 艱苦備嚐 +艰苦奋斗 艱苦奮鬥 +艰难困苦 艱難困苦 +艰难曲折 艱難曲折 +色当 色當 +色彩 色彩 +色彩三要素 色彩三要素 +色彩学 色彩學 +色彩缤纷 色彩繽紛 +色彩艳丽 色彩豔麗 +色彩鲜明 色彩鮮明 +色彩鲜艳 色彩鮮豔 +色情杂志 色情雜誌 +色欲 色慾 +色盲症 色盲症 +色系 色系 +艳后 豔后 +艸木丰丰 艸木丰丰 +艺压当行 藝壓當行 +艺坛 藝壇 +艺文志 藝文志 +艺术 藝術 +艺术体操 藝術體操 +艺术化 藝術化 +艺术区 藝術區 +艺术厅 藝術廳 +艺术史 藝術史 +艺术品 藝術品 +艺术团 藝術團 +艺术奖 藝術獎 +艺术字 藝術字 +艺术季 藝術季 +艺术学院 藝術學院 +艺术家 藝術家 +艺术展 藝術展 +艺术性 藝術性 +艺术指导 藝術指導 +艺术村 藝術村 +艺术歌曲 藝術歌曲 +艺术片 藝術片 +艺术界 藝術界 +艺术类 藝術類 +艺术系 藝術系 +艺术美 藝術美 +艺术者 藝術者 +艺术节 藝術節 +艺术街 藝術街 +艺术院 藝術院 +艺术馆 藝術館 +艺穗节 藝穗節 +艾万斯 艾萬斯 +艾伦图克 艾倫圖克 +艾克尔 艾克爾 +艾克斯 艾克斯 +艾克曼 艾克曼 +艾克森 艾克森 +艾克森美孚 艾克森美孚 +艾克雷史东 艾克雷史東 +艾力克 艾力克 +艾力克斯 艾力克斯 +艾回 艾迴 +艾布兰 艾布蘭 +艾布拉莫维奇 艾布拉莫維奇 +艾希克罗 艾希克羅 +艾弥尔 艾彌爾 +艾德蒙斯 艾德蒙斯 +艾德蒙顿 艾德蒙頓 +艾斯托利尔 艾斯托利爾 +艾瑞克 艾瑞克 +艾瑞克森 艾瑞克森 +艾瑞斯托 艾瑞斯托 +艾瑞里 艾瑞里 +艾米里 艾米里 +艾维斯普里斯莱 艾維斯普里斯萊 +艾赛克斯 艾賽克斯 +艾达克 艾達克 +艾里亚森 艾里亞森 +艾里斯 艾里斯 +艾里森 艾里森 +艾里赛宫 艾里賽宮 +节余 節餘 +节制 節制 +节制资本 節制資本 +节奏布鲁斯 節奏布魯斯 +节录 節錄 +节录自 節錄自 +节欲 節慾 +节流踏板 節流踏板 +节目表 節目表 +节节上升 節節上升 +节节胜利 節節勝利 +芍药 芍藥 +芍药花 芍藥花 +芒果干 芒果乾 +芒种 芒種 +芙蓉出水 芙蓉出水 +芟秋 芟秋 +芥子气恶病质 芥子氣惡病質 +芥子纳须弥 芥子納須彌 +芦帘 蘆簾 +芦席 蘆蓆 +芦洲蟹舍 蘆洲蟹舍 +芦花荡 蘆花蕩 +芦荡 蘆蕩 +芧栗 芧栗 +芫荽叶 芫荽葉 +芬郁 芬鬱 +芭丝克华 芭絲克華 +芭托莉 芭托莉 +芭蕉布 芭蕉布 +芭蕾舞团 芭蕾舞團 +芯慧同用 芯慧同用 +芯片厂 芯片廠 +花个 花個 +花了 花了 +花价 花價 +花儿针 花兒針 +花六出 花六出 +花卷 花捲 +花厂 花廠 +花发老 花髮老 +花台 花臺 +花叶 花葉 +花叶病 花葉病 +花哄 花鬨 +花团 花團 +花团锦簇 花團錦簇 +花园里 花園裏 +花坛 花壇 +花坛乡 花壇鄉 +花娘 花娘 +花尽 花盡 +花岗岩 花崗岩 +花岗岩质层 花崗岩質層 +花布 花布 +花布包 花布包 +花庵词选 花菴詞選 +花彩 花彩 +花心萝卜 花心蘿蔔 +花托 花托 +花招百出 花招百出 +花拳绣腿 花拳繡腿 +花旗参 花旗參 +花无百日开人无千日好 花無百日開人無千日好 +花明柳暗 花明柳暗 +花栗鼠 花栗鼠 +花样游泳 花樣游泳 +花样百出 花樣百出 +花椒面 花椒麪 +花烟馆 花煙館 +花盆里 花盆裏 +花种 花種 +花粉症 花粉症 +花红叶绿 花紅葉綠 +花胜 花勝 +花胡同 花衚衕 +花胡瓜 花胡瓜 +花荡 花蕩 +花药 花葯 +花药瓣 花葯瓣 +花莲师范学院 花蓮師範學院 +花藜胡哨 花藜胡哨 +花虫 花蟲 +花车游行 花車遊行 +花采 花采 +花里胡哨 花裏胡哨 +花钟 花鐘 +花雕 花雕 +花雕酒 花雕酒 +花面 花面 +花面狸 花面狸 +花马吊嘴 花馬弔嘴 +花魁娘子 花魁娘子 +芳名录 芳名錄 +芸苔 蕓薹 +芸薹 蕓薹 +芸辉 蕓輝 +苇席 葦蓆 +苇苕系巢 葦苕繫巢 +苍发 蒼髮 +苍术 蒼朮 +苍松 蒼松 +苍松翠柏 蒼松翠柏 +苍生涂炭 蒼生塗炭 +苍翠欲滴 蒼翠欲滴 +苍蝇掐了头 蒼蠅掐了頭 +苍郁 蒼鬱 +苍黄翻复 蒼黃翻覆 +苎麻 苧麻 +苏东启案 蘇東啓案 +苏东坡 蘇東坡 +苏丹 蘇丹 +苏丹人 蘇丹人 +苏丹共和国 蘇丹共和國 +苏丹达佛 蘇丹達佛 +苏丹达佛区 蘇丹達佛區 +苏丽文 蘇麗文 +苏乐明 蘇樂明 +苏乐桃 蘇樂桃 +苏仙区 甦仙區 +苏伊士 蘇伊士 +苏伊士河 蘇伊士河 +苏伊士运河 蘇伊士運河 +苏俄 蘇俄 +苏俄在中国 蘇俄在中國 +苏俊仁 蘇俊仁 +苏俊宾 蘇俊賓 +苏克雷 蘇克雷 +苏公隄 蘇公隄 +苏共 蘇共 +苏凡纳布 蘇凡納布 +苏利南 蘇利南 +苏利南共和国 蘇利南共和國 +苏利文 蘇利文 +苏北 蘇北 +苏区 蘇區 +苏占区 蘇佔區 +苏合香 蘇合香 +苏启荣 蘇啓榮 +苏哈托 蘇哈托 +苏哲毅 蘇哲毅 +苏嘉全 蘇嘉全 +苏圣斌 蘇聖斌 +苏堤 蘇堤 +苏头 蘇頭 +苏妮儿 蘇妮兒 +苏妮妮 蘇妮妮 +苏妮萨 蘇妮薩 +苏姆盖特 蘇姆蓋特 +苏子油 蘇子油 +苏宁 蘇寧 +苏宁电器 蘇寧電器 +苏家屯 甦家屯 +苏家屯区 甦家屯區 +苏家明 蘇家明 +苏富比 蘇富比 +苏富比公司 蘇富比公司 +苏小妹 蘇小妹 +苏尔 蘇爾 +苏尔坦 蘇爾坦 +苏尔奈 蘇爾奈 +苏尼特右旗 蘇尼特右旗 +苏尼特左旗 蘇尼特左旗 +苏峻 蘇峻 +苏州 蘇州 +苏州人 蘇州人 +苏州地区 蘇州地區 +苏州城 蘇州城 +苏州大学 蘇州大學 +苏州市 蘇州市 +苏州弹词 蘇州彈詞 +苏州片 蘇州片 +苏州码 蘇州碼 +苏州码子 蘇州碼子 +苏州评弹 蘇州評彈 +苏州话 蘇州話 +苏巴猜 蘇巴猜 +苏帕瑞 蘇帕瑞 +苏建 蘇建 +苏建和 蘇建和 +苏建忠 蘇建忠 +苏建荣 蘇建榮 +苏式 蘇式 +苏彝士 蘇彝士 +苏彝士运河 蘇彝士運河 +苏德曼 蘇德曼 +苏必利尔湖 蘇必利爾湖 +苏必略湖 蘇必略湖 +苏志明 蘇志明 +苏志燮 蘇志燮 +苏息 蘇息 +苏恺二七战机 蘇愷二七戰機 +苏打 蘇打 +苏打水 蘇打水 +苏打粉 蘇打粉 +苏打绿 蘇打綠 +苏打饼干 蘇打餅乾 +苏扬托 蘇揚托 +苏报案 蘇報案 +苏拉 蘇拉 +苏拉威 蘇拉威 +苏拉威西 蘇拉威西 +苏拉威西岛 蘇拉威西島 +苏拉朋 蘇拉朋 +苏拉特 蘇拉特 +苏拉育 蘇拉育 +苏拾平 蘇拾平 +苏振平 蘇振平 +苏文生 蘇文生 +苏方 蘇方 +苏昆 蘇崑 +苏易简 蘇易簡 +苏曼殊 蘇曼殊 +苏有朋 蘇有朋 +苏木 蘇木 +苏杭 蘇杭 +苏杯 蘇盃 +苏枋木 蘇枋木 +苏格兰 蘇格蘭 +苏格兰人 蘇格蘭人 +苏格兰场 蘇格蘭場 +苏格兰女王玛丽 蘇格蘭女王瑪麗 +苏格兰帽 蘇格蘭帽 +苏格兰折耳猫 蘇格蘭摺耳貓 +苏格兰王 蘇格蘭王 +苏格兰裙 蘇格蘭裙 +苏格兰队 蘇格蘭隊 +苏格拉底 蘇格拉底 +苏梅岛 蘇梅島 +苏步青 蘇步青 +苏武 蘇武 +苏武牧羊 蘇武牧羊 +苏比克湾 蘇比克灣 +苏氨酸 蘇氨酸 +苏永康 蘇永康 +苏永耀 蘇永耀 +苏永钦 蘇永欽 +苏治芬 蘇治芬 +苏泽光 蘇澤光 +苏洵 蘇洵 +苏海韩潮 蘇海韓潮 +苏澳 蘇澳 +苏澳港 蘇澳港 +苏澳镇 蘇澳鎮 +苏炎坤 蘇炎坤 +苏炳宪 蘇炳憲 +苏焕智 蘇煥智 +苏玲瑶 蘇玲瑤 +苏珊 蘇珊 +苏珊娜 蘇珊娜 +苏珊莎兰登 蘇珊莎蘭登 +苏瓦 蘇瓦 +苏白 蘇白 +苏盈贵 蘇盈貴 +苏禄岛 蘇祿島 +苏福男 蘇福男 +苏秦 蘇秦 +苏秦刺股 蘇秦刺股 +苏立吉 蘇立吉 +苏糖 蘇糖 +苏绣 蘇繡 +苏维埃 蘇維埃 +苏维埃俄国 蘇維埃俄國 +苏维埃社会主义共和国联盟 蘇維埃社會主義共和國聯盟 +苏维埃社会主义共和国联邦 蘇維埃社會主義共和國聯邦 +苏维成 蘇維成 +苏罗河 蘇羅河 +苏美人 蘇美人 +苏美尔 蘇美爾 +苏翊杰 蘇翊傑 +苏联 蘇聯 +苏联之友社 蘇聯之友社 +苏联人 蘇聯人 +苏联共产党 蘇聯共產黨 +苏联最高苏维埃 蘇聯最高蘇維埃 +苏胺酸 蘇胺酸 +苏舜钦 蘇舜欽 +苏芮 蘇芮 +苏花公路 蘇花公路 +苏花高 蘇花高 +苏苏 蘇蘇 +苏莱曼 蘇萊曼 +苏菜 蘇菜 +苏菲 蘇菲 +苏菲亚 蘇菲亞 +苏菲亚罗兰 蘇菲亞羅蘭 +苏菲玛索 蘇菲瑪索 +苏菲雅 蘇菲雅 +苏蕙 蘇蕙 +苏西洛 蘇西洛 +苏谢配 蘇謝配 +苏贞昌 蘇貞昌 +苏超凡 蘇超凡 +苏轩弘 蘇軒弘 +苏轼 蘇軾 +苏辙 蘇轍 +苏达拉 蘇達拉 +苏达索诺 蘇達索諾 +苏迪曼杯 蘇迪曼杯 +苏迪约梭 蘇迪約梭 +苏醒 甦醒 +苏醒剂 甦醒劑 +苏醒过来 甦醒過來 +苏里 蘇里 +苏里南 蘇里南 +苏里南河 蘇裏南河 +苏里安提沙洛索 蘇里安提沙洛索 +苏金达 蘇金達 +苏铁 蘇鐵 +苏镇霖 蘇鎮霖 +苏门答腊 蘇門答臘 +苏门答腊岛 蘇門答臘島 +苏门答腊省 蘇門答臘省 +苏门达腊 蘇門達臘 +苏门达腊岛 蘇門達臘島 +苏门长啸 蘇門長嘯 +苏隄 蘇隄 +苏雷曼 蘇雷曼 +苏非 蘇非 +苏非主义 蘇非主義 +苏非教派 蘇非教派 +苏黎世 蘇黎世 +苏黎世联邦理工学院 蘇黎世聯邦理工學院 +苏黎士 蘇黎士 +苑里 苑裏 +苑里镇 苑裏鎮 +苗栗 苗栗 +苗栗人 苗栗人 +苗栗县 苗栗縣 +苗栗市 苗栗市 +苗种 苗種 +苗胄 苗胄 +苛政猛于虎 苛政猛於虎 +苜蓿长栏干 苜蓿長欄干 +苞叶 苞葉 +苞虫 苞蟲 +苟合 苟合 +苟合取容 苟合取容 +苟同 苟同 +苟容曲从 苟容曲從 +若个 若個 +若于 若於 +若出一辙 若出一轍 +若合符节 若合符節 +若向 若向 +若干 若干 +若干个 若干個 +若干人 若干人 +若干年 若干年 +若虫 若蟲 +苦了 苦了 +苦于 苦於 +苦修 苦修 +苦卤 苦鹵 +苦参 苦蔘 +苦大仇深 苦大仇深 +苦尽甘来 苦盡甘來 +苦干 苦幹 +苦干实干 苦幹實幹 +苦思恶想 苦思惡想 +苦斗 苦鬥 +苦杯 苦杯 +苦海无边回头是岸 苦海無邊回頭是岸 +苦瓜干 苦瓜乾 +苦药 苦藥 +苦读出身 苦讀出身 +苦酒满杯 苦酒滿杯 +苦里 苦裏 +苧悴 薴悴 +苧烯 薴烯 +苫布 苫布 +苯并噻吩 苯並噻吩 +苯酮尿症 苯酮尿症 +英克丝特 英克絲特 +英制 英制 +英占 英佔 +英发 英發 +英国广播电台 英國廣播電臺 +英姿焕发 英姿煥發 +英布 英布 +英才 英才 +英才俊伟 英才俊偉 +英文系 英文系 +英日同盟 英日同盟 +英杰 英傑 +英气风发 英氣風發 +英烈千秋 英烈千秋 +英联合王国 英聯合王國 +英胄 英胄 +英语系 英語系 +英里 英里 +英雄交响曲 英雄交響曲 +英雄只怕病来磨 英雄只怕病來磨 +英雄所见略同 英雄所見略同 +英雄模范 英雄模範 +英雄豪杰 英雄豪傑 +苴布 苴布 +苹叶 蘋葉 +苹婆 蘋婆 +苹果 蘋果 +苹果公司 蘋果公司 +苹果园 蘋果園 +苹果子姜尼 蘋果子姜尼 +苹果干 蘋果乾 +苹果手机 蘋果手機 +苹果树 蘋果樹 +苹果核 蘋果核 +苹果汁 蘋果汁 +苹果派 蘋果派 +苹果电脑 蘋果電腦 +苹果皮 蘋果皮 +苹果绿 蘋果綠 +苹果肉 蘋果肉 +苹果脸 蘋果臉 +苹果螺 蘋果螺 +苹果蠹蛾 蘋果蠹蛾 +苹果迷 蘋果迷 +苹果酒 蘋果酒 +苹果酱 蘋果醬 +苹果酸 蘋果酸 +苹果馅饼 蘋果餡餅 +苹萦 苹縈 +苹风 蘋風 +茂发 茂發 +茂才 茂才 +茂才异等 茂才異等 +茂松 茂松 +茂都淀 茂都澱 +范仲淹 范仲淹 +范伦铁诺 范倫鐵諾 +范佩西 范佩西 +范例 範例 +范光群 范光羣 +范公偁 范公偁 +范公堤 范公堤 +范冰冰 范冰冰 +范可钦 范可欽 +范哈能 范哈能 +范嘉骅 范嘉驊 +范围 範圍 +范围之内 範圍之內 +范围之外 範圍之外 +范围之广 範圍之廣 +范围内 範圍內 +范围属性 範圍屬性 +范围很大 範圍很大 +范围是 範圍是 +范围查询 範圍查詢 +范围调整 範圍調整 +范国铨 范國銓 +范增 范增 +范士丹 范士丹 +范姜 范姜 +范字 範字 +范家谦 范家謙 +范宽 范寬 +范小姐 范小姐 +范尼斯特鲁伊 范尼斯特魯伊 +范履霜 范履霜 +范式 範式 +范张鸡黍 范張雞黍 +范德林特 范德林特 +范德格拉夫 范德格拉夫 +范德瓦耳斯 范德瓦耳斯 +范德瓦耳斯力 范德瓦耳斯力 +范德维德 范德維德 +范德萨 范德薩 +范志毅 范志毅 +范性形变 範性形變 +范戈德 范戈德 +范成大 范成大 +范文 範文 +范文同 范文同 +范文正公 范文正公 +范文澜 范文瀾 +范文照 范文照 +范文程 范文程 +范文网 範文網 +范文芳 范文芳 +范文藤 范文藤 +范文虎 范文虎 +范文选读 範文選讀 +范斯坦 范斯坦 +范晓萱 范曉萱 +范晔 范曄 +范本 範本 +范植伟 范植偉 +范植谷 范植谷 +范欣妤 范欣妤 +范正祥 范正祥 +范洪森 范洪森 +范湘暄 范湘暄 +范特尔 范特爾 +范特西 范特西 +范玮琪 范瑋琪 +范琪斐 范琪斐 +范甘迪 范甘迪 +范畴 範疇 +范畴内 範疇內 +范畴论 範疇論 +范登堡 范登堡 +范皓阗 范皓闐 +范筱梵 范筱梵 +范纲武 范綱武 +范织钦 范織欽 +范绮馨 范綺馨 +范范之辈 范範之輩 +范蠡 范蠡 +范进 范進 +范逸臣 范逸臣 +范金 範金 +范阳 范陽 +范陈柏 范陳柏 +范雎 范雎 +范靖瑶 范靖瑤 +茄二十八星瓢虫 茄二十八星瓢蟲 +茄克 茄克 +茄克衫 茄克衫 +茄冬 茄冬 +茅以升 茅以升 +茅厕里的石头 茅廁裏的石頭 +茅台 茅臺 +茅台酒 茅臺酒 +茅舍 茅舍 +茅针 茅針 +茈胡 茈胡 +茉莉克 茉莉克 +茎干 莖幹 +茑萝施乔松 蔦蘿施喬松 +茧栗 繭栗 +茧纤维 繭纖維 +茨万吉拉伊 茨萬吉拉伊 +茫茫荡荡 茫茫蕩蕩 +茱丽叶 茱麗葉 +茵借 茵藉 +茶余 茶餘 +茶几 茶几 +茶卤 茶滷 +茶叶 茶葉 +茶叶末 茶葉末 +茶叶末儿 茶葉末兒 +茶叶碱 茶葉鹼 +茶叶罐 茶葉罐 +茶叶蛋 茶葉蛋 +茶已干 茶已乾 +茶庄 茶莊 +茶托 茶托 +茶杯 茶杯 +茶毛虫 茶毛蟲 +茶里王 茶裏王 +茶面 茶麪 +茶面子 茶麪子 +茶馀酒后 茶餘酒後 +茶馀饭后 茶餘飯後 +茹志鹃 茹誌鵑 +荅布 荅布 +荆尸 荊尸 +荆布 荊布 +荆钗布袄 荊釵布襖 +荆钗布裙 荊釵布裙 +荆钗裙布 荊釵裙布 +草丛里 草叢裏 +草原千里 草原千里 +草台班子 草臺班子 +草叶 草葉 +草叶集 草葉集 +草团瓢 草團瓢 +草头药 草頭藥 +草字汇 草字彙 +草履虫 草履蟲 +草席 草蓆 +草庵 草菴 +草枝摆 草枝擺 +草签 草簽 +草舍 草舍 +草草了事 草草了事 +草荐 草荐 +草药 草藥 +草药方 草藥方 +草虫 草蟲 +荐居 荐居 +荐臻 荐臻 +荐饥 荐饑 +荑手纤纤 荑手纖纖 +荒了 荒了 +荒年谷 荒年穀 +荒无人烟 荒無人煙 +荒烟漫草 荒煙漫草 +荒烟蔓草 荒煙蔓草 +荒腔走板 荒腔走板 +荞面 蕎麪 +荞麦面 蕎麥麪 +荡产 蕩產 +荡产倾家 蕩產傾家 +荡出 盪出 +荡到 盪到 +荡口 盪口 +荡垢涤汙 盪垢滌污 +荡复 蕩覆 +荡女 蕩女 +荡妇 蕩婦 +荡子 蕩子 +荡寇 蕩寇 +荡寒 盪寒 +荡尽 蕩盡 +荡平 蕩平 +荡开 盪開 +荡心 蕩心 +荡志 蕩志 +荡悠悠 盪悠悠 +荡散 蕩散 +荡来荡去 盪來盪去 +荡析 蕩析 +荡析离居 蕩析離居 +荡检逾闲 蕩檢逾閑 +荡气回肠 蕩氣迴腸 +荡气回阳 蕩氣迴陽 +荡涤 盪滌 +荡漾 盪漾 +荡漾出 盪漾出 +荡潏 蕩潏 +荡然 蕩然 +荡然无存 蕩然無存 +荡瑕涤秽 蕩瑕滌穢 +荡田 蕩田 +荡秋千 盪鞦韆 +荡舟 盪舟 +荡船 盪船 +荡荡 蕩蕩 +荡荡悠悠 盪盪悠悠 +荡酒 盪酒 +荡风 盪風 +荣升 榮升 +荣幸 榮幸 +荣幸之至 榮幸之至 +荣归故里 榮歸故里 +荣登后座 榮登后座 +荣获 榮獲 +荣获冠军 榮獲冠軍 +荤油蒙了心 葷油蒙了心 +荦确 犖确 +荧郁 熒鬱 +荫生 廕生 +荫监 廕監 +荫蔽 廕庇 +荫袭 廕襲 +药专 藥專 +药业 藥業 +药丸 藥丸 +药事法 藥事法 +药价 藥價 +药价差 藥價差 +药典 藥典 +药兽 藥獸 +药农 藥農 +药到命除 藥到命除 +药到回春 藥到回春 +药到病除 藥到病除 +药剂 藥劑 +药剂士 藥劑士 +药剂学 藥劑學 +药剂师 藥劑師 +药剂量 藥劑量 +药力 藥力 +药包 藥包 +药医 藥醫 +药医不死病佛度有缘人 藥醫不死病佛度有緣人 +药医学系 藥醫學系 +药单 藥單 +药厂 藥廠 +药叉 藥叉 +药发傀儡 藥發傀儡 +药吊子 藥吊子 +药名 藥名 +药味 藥味 +药品 藥品 +药品店 藥品店 +药商 藥商 +药壶 藥壺 +药头 藥頭 +药妆 藥妝 +药妆品 藥妝品 +药妆店 藥妝店 +药婆 藥婆 +药学 藥學 +药学专科学校 藥學專科學校 +药学系 藥學系 +药害 藥害 +药局 藥局 +药师 藥師 +药师佛 藥師佛 +药师如来 藥師如來 +药师经 藥師經 +药师节 藥師節 +药店 藥店 +药引 藥引 +药引子 藥引子 +药性 藥性 +药性气 藥性氣 +药房 藥房 +药捻子 藥捻子 +药政处 藥政處 +药效 藥效 +药效持久 藥效持久 +药方 藥方 +药方儿 藥方兒 +药明康德 藥明康德 +药末 藥末 +药杀 藥殺 +药材 藥材 +药械 藥械 +药检 藥檢 +药检局 藥檢局 +药棉 藥棉 +药死 藥死 +药水 藥水 +药水儿 藥水兒 +药水味 藥水味 +药汁 藥汁 +药油 藥油 +药法 藥法 +药浴 藥浴 +药液 藥液 +药渣 藥渣 +药片 藥片 +药物 藥物 +药物中毒 藥物中毒 +药物学 藥物學 +药物学家 藥物學家 +药物成瘾 藥物成癮 +药物治疗 藥物治療 +药王 藥王 +药王菩萨 藥王菩薩 +药理 藥理 +药理学 藥理學 +药瓶 藥瓶 +药瓶子 藥瓶子 +药用 藥用 +药用价值 藥用價值 +药用植物 藥用植物 +药疗 藥療 +药疹 藥疹 +药瘾 藥癮 +药皂 藥皂 +药监局 藥監局 +药盒 藥盒 +药石 藥石 +药石之言 藥石之言 +药石罔效 藥石罔效 +药科 藥科 +药笼中物 藥籠中物 +药筒 藥筒 +药签 藥籤 +药箭 藥箭 +药箱 藥箱 +药籤 藥籤 +药粉 藥粉 +药糖 藥糖 +药线 藥線 +药罐 藥罐 +药罐子 藥罐子 +药而愈 藥而癒 +药膏 藥膏 +药膛 藥膛 +药膳 藥膳 +药舖 藥舖 +药茶 藥茶 +药草 藥草 +药草茶 藥草茶 +药行 藥行 +药衡 藥衡 +药衣子 藥衣子 +药补 藥補 +药袋 藥袋 +药裹关心 藥裹關心 +药言 藥言 +药贩 藥販 +药贴 藥貼 +药费 藥費 +药酒 藥酒 +药量 藥量 +药针 藥針 +药铺 藥鋪 +药锭 藥錠 +药面儿 藥麪兒 +药食同源 藥食同源 +药饵 藥餌 +药饼 藥餅 +药骰 藥骰 +荷叶 荷葉 +荷叶先师 荷葉先師 +荷叶肉 荷葉肉 +荷尔蒙 荷爾蒙 +荷花出水 荷花出水 +荷花淀 荷花澱 +荷里活 荷里活 +荷雷克 荷雷克 +莎玛海耶克 莎瑪海耶克 +莒光周 莒光週 +莜面 莜麪 +莫三比克 莫三比克 +莫三比克人民共和国 莫三比克人民共和國 +莫乃耳合金 莫乃耳合金 +莫余毒也 莫余毒也 +莫信直中直须防人不仁 莫信直中直須防人不仁 +莫克姆湾 莫克姆灣 +莫吉托 莫吉托 +莫布里 莫布里 +莫干山 莫干山 +莫当 莫當 +莫扎特 莫扎特 +莫扎里拉 莫扎里拉 +莫折大提 莫折大提 +莫报万一 莫報萬一 +莫曼斯克 莫曼斯克 +莫杰斯特 莫傑斯特 +莫桑比克 莫桑比克 +莫管闲事 莫管閒事 +莫索里尼 莫索里尼 +莫荷不连续面 莫荷不連續面 +莫莉克 莫莉克 +莫蹪于山而蹪于垤 莫蹪於山而蹪於垤 +莫辨楮叶 莫辨楮葉 +莫过于 莫過於 +莫里 莫里 +莫里叶 莫里葉 +莫里哀 莫里哀 +莫里尼奥 莫里尼奧 +莫里希 莫里希 +莫里斯 莫里斯 +莫里森 莫里森 +莫里纳 莫里納 +莫霍洛维奇不连续面 莫霍洛維奇不連續面 +莫霍面 莫霍面 +莫须 莫須 +莫须有 莫須有 +莱布尼兹 萊布尼茲 +莱彩 萊彩 +莱彩北堂 萊綵北堂 +莱德杯 萊德杯 +莱索托 萊索托 +莱里达 萊里達 +莲台 蓮臺 +莲叶 蓮葉 +莲开并蒂 蓮開並蒂 +莲须 蓮鬚 +获准 獲准 +获刑 獲刑 +获判 獲判 +获判无罪 獲判無罪 +获利 獲利 +获利倍蓰 獲利倍蓰 +获利率 獲利率 +获利王 獲利王 +获到 獲到 +获匪其丑 獲匪其醜 +获取 獲取 +获咎 獲咎 +获嘉 獲嘉 +获嘉县 獲嘉縣 +获奖 獲獎 +获奖人 獲獎人 +获奖率 獲獎率 +获奖者 獲獎者 +获得 獲得 +获得四 獲得四 +获得四坏 獲得四壞 +获得性 獲得性 +获得者 獲得者 +获得胜利 獲得勝利 +获悉 獲悉 +获戾 獲戾 +获报 獲報 +获捷 獲捷 +获救 獲救 +获暴利者 獲暴利者 +获有 獲有 +获益 獲益 +获益不浅 獲益不淺 +获益匪浅 獲益匪淺 +获益者 獲益者 +获益良多 獲益良多 +获知 獲知 +获罪 獲罪 +获胜 獲勝 +获胜者 獲勝者 +获致 獲致 +获赠 獲贈 +获赦 獲赦 +获选 獲選 +获选为 獲選爲 +获邀 獲邀 +获释 獲釋 +获颁 獲頒 +获鹿 獲鹿 +获鹿县 獲鹿縣 +获鹿镇 獲鹿鎮 +莺谷 鶯谷 +莽卤 莽鹵 +莽荡 莽蕩 +菁英杯 菁英盃 +菅野美穗 菅野美穗 +菊坛 菊壇 +菊秋 菊秋 +菌丝体 菌絲體 +菌托 菌托 +菌核 菌核 +菌种 菌種 +菌胶团 菌膠團 +菌血症 菌血症 +菜干 菜乾 +菜板 菜板 +菜瓜布 菜瓜布 +菜种 菜種 +菜系 菜系 +菜肴 菜餚 +菜苔 菜薹 +菜虫 菜蟲 +菜青虫 菜青蟲 +菠棱菜 菠棱菜 +菠萝干 菠蘿乾 +菩提流志 菩提流志 +菲佣 菲傭 +菲利克斯 菲利克斯 +菲利克斯.米达麦亚 菲利克斯.米達麥亞 +菲力克斯 菲力克斯 +菲尼克斯 菲尼克斯 +菲德烈克 菲德烈克 +菲才寡学 菲才寡學 +菲舍尔 菲捨爾 +菲茨杰拉德 菲茨傑拉德 +菲衣恶食 菲衣惡食 +萌发 萌發 +萎缩症 萎縮症 +萎雕 萎雕 +萝卜 蘿蔔 +萝卜头 蘿蔔頭 +萝卜干 蘿蔔乾 +萝卜精 蘿蔔精 +萝卜精头上青 蘿蔔精頭上青 +萝卜糕 蘿蔔糕 +萝卜腿 蘿蔔腿 +萤光板 螢光板 +萤火虫 螢火蟲 +萤火虫儿 螢火蟲兒 +营养价值 營養價值 +营养系 營養系 +营干 營幹 +营建厂 營建廠 +营舍 營舍 +营造出 營造出 +营造出来 營造出來 +营造厂 營造廠 +营造厂商 營造廠商 +萦回 縈迴 +萦系 縈繫 +萧万长 蕭萬長 +萧参 蕭蔘 +萧太后 蕭太后 +萧娘 蕭娘 +萧扎某 蕭扎某 +萧玮志 蕭瑋志 +萧行范篆 蕭行范篆 +萨克 薩克 +萨克号 薩克號 +萨克司风 薩克司風 +萨克斯 薩克斯 +萨克斯管 薩克斯管 +萨克斯风 薩克斯風 +萨克森 薩克森 +萨克森州 薩克森州 +萨克森邦 薩克森邦 +萨克洛夫 薩克洛夫 +萨克洛夫奖 薩克洛夫獎 +萨克管 薩克管 +萨克逊 薩克遜 +萨克逊人 薩克遜人 +萨尔布吕肯 薩爾布呂肯 +萨巴托 薩巴托 +萨布里 薩布里 +萨布里多 薩布里多 +萨格勒布 薩格勒布 +萨瓦里 薩瓦里 +萨迪克 薩迪克 +萨里 薩里 +萨里郡 薩里郡 +落个直过儿 落個直過兒 +落了 落了 +落了灶 落了竈 +落于 落於 +落于下风 落於下風 +落价 落價 +落卷 落卷 +落发 落髮 +落发为僧 落髮爲僧 +落台 落臺 +落叶 落葉 +落叶乔木 落葉喬木 +落叶剂 落葉劑 +落叶层 落葉層 +落叶归根 落葉歸根 +落叶归根家 落葉歸根家 +落叶松 落葉松 +落叶林 落葉林 +落叶果树 落葉果樹 +落叶树 落葉樹 +落叶植物 落葉植物 +落叶知秋 落葉知秋 +落后 落後 +落后国家 落後國家 +落在后面 落在後面 +落地签证 落地簽證 +落托 落托 +落月屋梁 落月屋梁 +落腮胡 落腮鬍 +落落寡合 落落寡合 +落落难合 落落難合 +葑菲之采 葑菲之采 +著录 著錄 +著志 著志 +著称于世 著稱於世 +葛兰素史克 葛蘭素史克 +葛布 葛布 +葛托维纳 葛托維納 +葛拉斯里 葛拉斯里 +葛斯范桑 葛斯范桑 +葛罗托斯基 葛羅托斯基 +葛里芬 葛里芬 +葡占 葡佔 +葡萄叶银莲花 葡萄葉銀蓮花 +葡萄干 葡萄乾 +葡萄干儿 葡萄乾兒 +董氏封发 董氏封髮 +董里府 董里府 +葫芦谷 葫蘆谷 +葫芦里卖甚么药 葫蘆裏賣甚麼藥 +葫芦里卖的甚么药 葫蘆裏賣的甚麼藥 +葱姜蒜 蔥薑蒜 +葱胡子 蔥鬍子 +葱葱郁郁 蔥蔥郁郁 +葱郁 蔥鬱 +蒂森克虏伯 蒂森克虜伯 +蒋国梁 蔣國樑 +蒋干 蔣幹 +蒋百里 蔣百里 +蒋舍三径 蔣舍三徑 +蒌叶 蔞葉 +蒐录 蒐錄 +蒙一饭之恩尚杀身以报 蒙一飯之恩尚殺身以報 +蒙上 蒙上 +蒙主宠召 蒙主寵召 +蒙事 矇事 +蒙人 蒙人 +蒙代尔 蒙代爾 +蒙住 矇住 +蒙兀儿 蒙兀兒 +蒙兀儿帝国 蒙兀兒帝國 +蒙养 蒙養 +蒙冤 蒙冤 +蒙冲 蒙衝 +蒙受 蒙受 +蒙叟 蒙叟 +蒙古 蒙古 +蒙古人 蒙古人 +蒙古人民共和国 蒙古人民共和國 +蒙古人种 蒙古人種 +蒙古儿 蒙古兒 +蒙古利亚 蒙古利亞 +蒙古包 蒙古包 +蒙古国 蒙古國 +蒙古地方 蒙古地方 +蒙古大夫 蒙古大夫 +蒙古帝国 蒙古帝國 +蒙古文 蒙古文 +蒙古斑 蒙古斑 +蒙古族 蒙古族 +蒙古症 蒙古症 +蒙古话 蒙古話 +蒙古语 蒙古語 +蒙古高原 蒙古高原 +蒙台梭利 蒙臺梭利 +蒙吏 蒙吏 +蒙哄 蒙哄 +蒙哥 蒙哥 +蒙哥马利 蒙哥馬利 +蒙嘉慧 蒙嘉慧 +蒙在 蒙在 +蒙在鼓里 矇在鼓裏 +蒙地卡罗 蒙地卡羅 +蒙坑 蒙坑 +蒙垢 蒙垢 +蒙城 蒙城 +蒙城县 蒙城縣 +蒙塔丝 蒙塔絲 +蒙塔尼斯 蒙塔尼斯 +蒙塔斯 蒙塔斯 +蒙塔达 蒙塔達 +蒙大拿 蒙大拿 +蒙大拿州 蒙大拿州 +蒙太奇 蒙太奇 +蒙太奇电影 蒙太奇電影 +蒙头 矇頭 +蒙头大睡 矇頭大睡 +蒙头衲被 矇頭衲被 +蒙头转 矇頭轉 +蒙头转向 矇頭轉向 +蒙娜丽莎 蒙娜麗莎 +蒙学 蒙學 +蒙尘 蒙塵 +蒙山 蒙山 +蒙山县 蒙山縣 +蒙巴萨 蒙巴薩 +蒙巴顿 蒙巴頓 +蒙师 蒙師 +蒙帕纳斯 蒙帕納斯 +蒙席 蒙席 +蒙庄 蒙莊 +蒙彼利埃 蒙彼利埃 +蒙得维的亚 蒙得維的亞 +蒙恩 蒙恩 +蒙恬 蒙恬 +蒙恬造笔 蒙恬造筆 +蒙懂 懞懂 +蒙戎 蒙戎 +蒙托罗拉 蒙托羅拉 +蒙拾 蒙拾 +蒙故业 蒙故業 +蒙文 蒙文 +蒙族 蒙族 +蒙昧 矇昧 +蒙昧不清 濛昧不清 +蒙昧无知 矇昧無知 +蒙松雨 濛鬆雨 +蒙求 蒙求 +蒙汗药 蒙汗藥 +蒙汜 濛汜 +蒙混 矇混 +蒙混过关 矇混過關 +蒙爱 蒙愛 +蒙牛 蒙牛 +蒙特 蒙特 +蒙特內哥罗 蒙特內哥羅 +蒙特利 蒙特利 +蒙特利尔 蒙特利爾 +蒙特卡洛 蒙特卡洛 +蒙特卡洛法 蒙特卡洛法 +蒙特卡罗 蒙特卡羅 +蒙特卡罗方法 蒙特卡羅方法 +蒙特塞拉特 蒙特塞拉特 +蒙特娄 蒙特婁 +蒙特维多 蒙特維多 +蒙特贝娄 蒙特貝婁 +蒙特雷 蒙特雷 +蒙特鲁 蒙特魯 +蒙狄维欧 蒙狄維歐 +蒙田 蒙田 +蒙皮 蒙皮 +蒙盖 蒙蓋 +蒙直 懞直 +蒙眬 矇矓 +蒙眼 矇眼 +蒙瞍 矇瞍 +蒙稚 蒙稚 +蒙童 蒙童 +蒙笼 蒙籠 +蒙笼暗碧 蒙籠暗碧 +蒙络 蒙絡 +蒙罗维亚 蒙羅維亞 +蒙羞 蒙羞 +蒙聩 矇聵 +蒙胞 蒙胞 +蒙脸 蒙臉 +蒙自 蒙自 +蒙自县 蒙自縣 +蒙茏 蒙蘢 +蒙药 蒙藥 +蒙菲尔斯 蒙菲爾斯 +蒙蒙 濛濛 矇矇 +蒙蒙亮 矇矇亮 +蒙蒙懂懂 懞懞懂懂 +蒙蒙眬眬 矇矇矓矓 +蒙蒙细雨 濛濛細雨 +蒙蒙谷 濛濛谷 +蒙蒙黑 矇矇黑 +蒙蔽 矇蔽 +蒙藏 蒙藏 +蒙藏同胞 蒙藏同胞 +蒙藏委员 蒙藏委員 +蒙藏委员会 蒙藏委員會 +蒙语 蒙語 +蒙谷 蒙谷 +蒙贝列 蒙貝列 +蒙起 蒙起 +蒙起来 蒙起來 +蒙阴 蒙陰 +蒙阴县 蒙陰縣 +蒙难 蒙難 +蒙难记 蒙難記 +蒙雾 濛霧 +蒙雾露 濛霧露 +蒙面 蒙面 +蒙面人 蒙面人 +蒙面侠 蒙面俠 +蒙面客 蒙面客 +蒙馆 蒙館 +蒙骗 矇騙 +蒙鸿 濛鴻 +蒜发 蒜髮 +蒜苔 蒜薹 +蒲团 蒲團 +蒲扇价增 蒲扇價增 +蒲松龄 蒲松齡 +蒸了 蒸了 +蒸便当 蒸便當 +蒸发 蒸發 +蒸发器 蒸發器 +蒸发掉 蒸發掉 +蒸发散 蒸發散 +蒸发热 蒸發熱 +蒸发皿 蒸發皿 +蒸发空调 蒸發空調 +蒸发计 蒸發計 +蒸发量 蒸發量 +蒸干 蒸乾 +蒸汽熨斗 蒸汽熨斗 +蒸沤历澜 蒸漚歷瀾 +蒸藜出妻 蒸藜出妻 +蒸面 蒸麪 +蒸骨验尸 蒸骨驗屍 +蒿里 蒿里 +蓄势待发 蓄勢待發 +蓄发 蓄髮 +蓄志 蓄志 +蓄胡 蓄鬍 +蓄长发 蓄長髮 +蓄须 蓄鬚 +蓄须明志 蓄鬚明志 +蓊郁 蓊鬱 +蓝发 藍髮 +蓝布 藍布 +蓝托斯 藍托斯 +蓝板 藍板 +蓝淀 藍澱 +蓝田出玉 藍田出玉 +蓝田种玉 藍田種玉 +蓝胡子 藍鬍子 +蓝色系 藍色系 +蓝采和 藍采和 +蓟训历家 薊訓歷家 +蓦然回首 驀然回首 +蓬勃发展 蓬勃發展 +蓬发 蓬髮 +蓬头垢面 蓬頭垢面 +蓬松 蓬鬆 +蓬蓬松松 蓬蓬鬆鬆 +蓬门荆布 蓬門荊布 +蓬首垢面 蓬首垢面 +蔑如 蔑如 +蔑弃 蔑棄 +蔑称 蔑稱 +蔑蒙 蔑蒙 +蔑视 蔑視 +蔗板 蔗板 +蔡丰州 蔡豐州 +蔡仰秋 蔡仰秋 +蔡克嵩 蔡克嵩 +蔡同荣 蔡同榮 +蔡志忠 蔡志忠 +蔡志杰 蔡志傑 +蔡文丰 蔡文豐 +蔡松坡 蔡松坡 +蔡清游 蔡清遊 +蔡秋炎 蔡秋炎 +蔡细历 蔡細歷 +蔷薇十字团 薔薇十字團 +蔼彩 藹彩 +蔽形术 蔽形術 +蔽面 蔽面 +蕃薯叶 蕃薯葉 +蕈状云 蕈狀雲 +蕉叶 蕉葉 +蕉布 蕉布 +蕲向 蘄向 +蕴借 蘊藉 +蕴借含蓄 蘊藉含蓄 +蕴奇待价 蘊奇待價 +薄云 薄雲 +薄干 薄幹 +薄幸 薄倖 +薄幸人 薄倖人 +薄恶 薄惡 +薄曲 薄曲 +薄松松 薄鬆鬆 +薄板 薄板 +薄海同仇 薄海同仇 +薄荷叶 薄荷葉 +薄面 薄面 +薄面含嗔 薄面含嗔 +薙发 薙髮 +薙发令 薙髮令 +薛松干 薛松乾 +薝卜 薝蔔 +薪尽火传 薪盡火傳 +薪资表 薪資表 +薰修 薰脩 +薰莸不同器 薰蕕不同器 +薰莸同器 薰蕕同器 +藉资挹注 藉資挹注 +藏于 藏於 +藏匿于 藏匿於 +藏南纵谷 藏南縱谷 +藏历 藏曆 +藏奸 藏奸 +藏尸 藏屍 +藏幸 藏幸 +藏蒙歌儿 藏矇歌兒 +藏量丰富 藏量豐富 +藏针缝 藏針縫 +藕复 藕覆 +藜藿不采 藜藿不採 +藤制 藤製 +藩台 藩臺 +蘑菇云 蘑菇雲 +虎克 虎克 +虎克党 虎克黨 +虎克定律 虎克定律 +虎兕出柙 虎兕出柙 +虎山艺术馆 虎山藝術館 +虎据 虎據 +虎斗 虎鬥 +虎斗龙争 虎鬥龍爭 +虎甲虫 虎甲蟲 +虎皮松 虎皮松 +虎荡羊群 虎蕩羊羣 +虎须 虎鬚 +虏获 虜獲 +虑周行果 慮周行果 +虑无不周 慮無不周 +虚云大师 虛雲大師 +虚冲 虛沖 +虚发 虛發 +虚夸 虛誇 +虚心使人进步骄傲使人落后 虛心使人進步驕傲使人落後 +虚怀若谷 虛懷若谷 +虚拟通道标志符 虛擬通道標誌符 +虚有其表 虛有其表 +虚症 虛症 +虚荡 虛蕩 +虞歌决别 虞歌決別 +虫书 蟲書 +虫体 蟲體 +虫儿 蟲兒 +虫出 蟲出 +虫卵 蟲卵 +虫吃牙 蟲吃牙 +虫声 蟲聲 +虫媒病毒 蟲媒病毒 +虫媒花 蟲媒花 +虫子 蟲子 +虫子牙 蟲子牙 +虫孔 蟲孔 +虫字旁儿 蟲字旁兒 +虫害 蟲害 +虫情 蟲情 +虫沙微类 蟲沙微類 +虫沙猿鹤 蟲沙猿鶴 +虫漆 蟲漆 +虫灾 蟲災 +虫牙 蟲牙 +虫瘿 蟲癭 +虫白蜡 蟲白蠟 +虫眼 蟲眼 +虫篆 蟲篆 +虫篆之技 蟲篆之技 +虫类 蟲類 +虫胶 蟲膠 +虫臂鼠肝 蟲臂鼠肝 +虫虫 蟲蟲 +虫虫蚁蚁 蟲蟲蟻蟻 +虫蚀 蟲蝕 +虫蚁 蟲蟻 +虫蛭 蟲蛭 +虫豸 蟲豸 +虫部 虫部 +虫霜水旱 蟲霜水旱 +虫鱼 蟲魚 +虫鸟叫声 蟲鳥叫聲 +虫鸣 蟲鳴 +虫鸣水沸 蟲鳴水沸 +虬须 虯鬚 +虮蝨相吊 蟣蝨相弔 +虹彩 虹彩 +虹彩流辉 虹彩流輝 +虹彩炎 虹彩炎 +虹彩膜 虹彩膜 +虽复能复 雖覆能復 +虾干 蝦乾 +虾蟆跳在戥盘子里 蝦蟆跳在戥盤子裏 +虾须 蝦鬚 +蚀船虫 蝕船蟲 +蚁合 蟻合 +蚁后 蟻后 +蚁斗蜗争 蟻鬥蝸爭 +蚁术 蟻術 +蚊动牛斗 蚊動牛鬥 +蚊子遭扇打只为嘴伤人 蚊子遭扇打只爲嘴傷人 +蚊睫之虫 蚊睫之蟲 +蚊虫 蚊蟲 +蚊虫叮咬 蚊蟲叮咬 +蚕种 蠶種 +蚕豆症 蠶豆症 +蚜虫 蚜蟲 +蚵仔面线 蚵仔麪線 +蛀虫 蛀蟲 +蛆虫 蛆蟲 +蛇发女妖 蛇髮女妖 +蛇口蜂针 蛇口蜂針 +蛇皮松 蛇皮松 +蛇纹岩 蛇紋岩 +蛇绿岩 蛇綠岩 +蛇绿混杂岩 蛇綠混雜岩 +蛇绿混杂岩带 蛇綠混雜岩帶 +蛋卷 蛋卷 +蛋彩画 蛋彩畫 +蛋杯 蛋杯 +蛏干 蟶乾 +蛓毛虫 蛓毛蟲 +蛔虫 蛔蟲 +蛔虫病 蛔蟲病 +蛔虫症 蛔蟲症 +蛔虫药 蛔蟲藥 +蛙鼓虫吟 蛙鼓蟲吟 +蛟龙得云雨 蛟龍得雲雨 +蛮干 蠻幹 +蛮干到底 蠻幹到底 +蛮干淨 蠻乾淨 +蛮干爽 蠻乾爽 +蛮针瞎灸 蠻針瞎灸 +蛰虫 蟄蟲 +蛲虫 蟯蟲 +蛲虫病 蟯蟲病 +蜀党 蜀黨 +蜂准 蜂準 +蜂出 蜂出 +蜂午并起 蜂午並起 +蜂后 蜂后 +蜂巢式行动电话系统 蜂巢式行動電話系統 +蜂涌而出 蜂涌而出 +蜂蒙 蜂蒙 +蜂蜡 蜂蠟 +蜗杆 蝸桿 +蜗舍 蝸舍 +蜜蜡 蜜蠟 +蜜里调油 蜜裏調油 +蜡丸 蠟丸 +蜡书 蠟書 +蜡人 蠟人 +蜡人馆 蠟人館 +蜡像 蠟像 +蜡像馆 蠟像館 +蜡光纸 蠟光紙 +蜡原型 蠟原型 +蜡台 蠟臺 +蜡嘴 蠟嘴 +蜡坨儿 蠟坨兒 +蜡坨子 蠟坨子 +蜡头儿 蠟頭兒 +蜡封 蠟封 +蜡屐 蠟屐 +蜡布 蠟布 +蜡弹 蠟彈 +蜡扦 蠟扦 +蜡月 蜡月 +蜡本 蠟本 +蜡板 蠟板 +蜡果 蠟果 +蜡枪头 蠟槍頭 +蜡染 蠟染 +蜡査 蠟查 +蜡梅 蠟梅 +蜡油 蠟油 +蜡泪 蠟淚 +蜡渣 蠟渣 +蜡灯 蠟燈 +蜡炬 蠟炬 +蜡烛 蠟燭 +蜡烛不点不亮 蠟燭不點不亮 +蜡烛台 蠟燭臺 +蜡烛油 蠟燭油 +蜡烛相 蠟燭相 +蜡烛线 蠟燭線 +蜡版 蠟版 +蜡珀 蠟珀 +蜡画 蠟畫 +蜡疗 蠟療 +蜡皮 蠟皮 +蜡祭 蜡祭 +蜡笔 蠟筆 +蜡笔小新 蠟筆小新 +蜡笔画 蠟筆畫 +蜡笺 蠟箋 +蜡纸 蠟紙 +蜡膏 蠟膏 +蜡膜 蠟膜 +蜡芯儿 蠟芯兒 +蜡花 蠟花 +蜡茶 蠟茶 +蜡虫 蠟蟲 +蜡蜂 蠟蜂 +蜡诏 蠟詔 +蜡质 蠟質 +蜡铺 蠟鋪 +蜡黄 蠟黃 +蜡黄色 蠟黃色 +蜰虫 蜰蟲 +蜷曲 蜷曲 +蝎谮 蠍譖 +蝗虫 蝗蟲 +蝗虫过境 蝗蟲過境 +蝨多了不咬债多了不愁 蝨多了不咬債多了不愁 +蝴蝶谷 蝴蝶谷 +蝼蚁得志 螻蟻得志 +融合 融合 +融合为 融合爲 +融合为一 融合爲一 +融合式 融合式 +融合式翼梢小翼 融合式翼梢小翼 +融合线 融合線 +融汇 融匯 +螟虫 螟蟲 +螫针 螫針 +螳臂当车 螳臂當車 +螳螂捕蝉黄雀在后 螳螂捕蟬黃雀在後 +螹胡 螹胡 +螺旋千斤顶 螺旋千斤頂 +螺旋曲面 螺旋曲面 +螺旋杆菌 螺旋桿菌 +螺旋面 螺旋麪 +螺杆 螺桿 +螽斯之征 螽斯之徵 +螾庐曲谈 螾廬曲談 +蟠尾丝虫 蟠尾絲蟲 +蟠尾丝虫症 蟠尾絲蟲症 +蟠据 蟠據 +蟠曲 蟠曲 +蟠桃胜会 蟠桃勝會 +蟠采 蟠采 +蟠龙松 蟠龍松 +蟪蛄不知春秋 蟪蛄不知春秋 +蟭蟟虫 蟭蟟蟲 +蟹黄鲍鱼面 蟹黃鮑魚麪 +蟻后 蟻后 +蟾宫折桂 蟾宮折桂 +蟾彩 蟾彩 +蠁干 蠁幹 +蠓虫 蠓蟲 +蠕虫 蠕蟲 +蠕虫形 蠕蟲形 +蠢虫 蠢蟲 +蠢蠢欲动 蠢蠢欲動 +蠹书虫 蠹書蟲 +蠹众木折 蠹衆木折 +蠹众而木折隙大而墙坏 蠹衆而木折隙大而牆壞 +蠹啄剖梁柱 蠹啄剖梁柱 +蠹虫 蠹蟲 +血亲复仇 血親復仇 +血仇 血仇 +血余 血餘 +血克帮帮 血克幫幫 +血制品 血製品 +血参 血蔘 +血吸虫 血吸蟲 +血吸虫病 血吸蟲病 +血小板 血小板 +血已干 血已乾 +血才干 血纔乾 +血栓症 血栓症 +血汗工厂 血汗工廠 +血洗台湾 血洗臺灣 +血流如注 血流如注 +血浓于水 血濃於水 +血海尸山 血海屍山 +血海深仇 血海深仇 +血液恐怖症 血液恐怖症 +血症 血癥 +血线虫 血線蟲 +血缘关系 血緣關係 +血肉淋漓 血肉淋漓 +血胄 血胄 +血胡同 血衚衕 +血脂升高症 血脂升高症 +血色素沉积症 血色素沉積症 +衅发萧墙 釁發蕭牆 +衅恶 釁惡 +衅钟 釁鐘 +衅面 釁面 +行万里路 行萬里路 +行万里路胜读万卷书 行萬裏路勝讀萬捲書 +行万里路读万卷书 行萬里路讀萬卷書 +行不苟合 行不苟合 +行业别 行業別 +行业规范 行業規範 +行个方便 行個方便 +行为世范 行爲世範 +行为准则 行爲準則 +行为艺术 行爲藝術 +行为艺术者 行爲藝術者 +行为规范 行爲規範 +行了 行了 +行事历 行事曆 +行事历史 行事歷史 +行于 行於 +行云 行雲 +行云流水 行雲流水 +行伍出身 行伍出身 +行佣 行佣 +行修寺 行修寺 +行兵布阵 行兵佈陣 +行凶 行兇 +行凶前 行兇前 +行凶后 行兇後 +行凶後 行兇後 +行凶杀人 行兇殺人 +行凶者 行兇者 +行动党 行動黨 +行动计划 行動計劃 +行千里路读万卷书 行千裏路讀萬卷書 +行卷 行卷 +行台 行臺 +行合趋同 行合趨同 +行同 行同 +行同狗彘 行同狗彘 +行同狗豨 行同狗豨 +行善不欲人知 行善不欲人知 +行复 行復 +行奸卖俏 行奸賣俏 +行尸 行屍 +行尸视肉 行屍視肉 +行尸走肉 行屍走肉 +行尸走骨 行屍走骨 +行幸 行幸 +行当 行當 +行志 行志 +行情价 行情價 +行情表 行情表 +行成于思 行成於思 +行政区划 行政區劃 +行政区划图 行政區劃圖 +行政当局 行政當局 +行政系 行政系 +行政诉愿 行政訴願 +行有余力 行有餘力 +行李卷 行李捲 +行板 行板 +行游 行遊 +行百里 行百里 +行百里者半于九十 行百里者半於九十 +行短才乔 行短才喬 +行短才高 行短才高 +行程表 行程表 +行药 行藥 +行行出状元 行行出狀元 +行针 行鍼 +行针布线 行鍼佈線 +行针走线 行鍼走線 +行雨朝云 行雨朝雲 +衍声复词 衍聲複詞 +衍极 衍極 +衍生出 衍生出 +衍生出来 衍生出來 +衔哀致诚 銜哀致誠 +衔恨蒙枉 銜恨蒙枉 +衔缺相当 銜缺相當 +街坊邻舍 街坊鄰舍 +街坊邻里 街坊鄰里 +街里街坊 街里街坊 +街面儿 街面兒 +衡量制 衡量制 +衣不兼采 衣不兼采 +衣不完采 衣不完采 +衣不布体 衣不布體 +衣不重采 衣不重采 +衣丰食足 衣豐食足 +衣丰食饱 衣豐食飽 +衣冠云集 衣冠雲集 +衣扣 衣釦 +衣摆 衣襬 +衣斗木 衣斗木 +衣柜 衣櫃 +衣物已干 衣物已乾 +衣物柜 衣物櫃 +衣物渐干 衣物漸乾 +衣绣昼行 衣繡晝行 +衣衫已干 衣衫已乾 +衣锦夜游 衣錦夜游 +衣锦昼游 衣錦晝游 +补于 補於 +补修 補修 +补发 補發 +补回 補回 +补回来 補回來 +补扣 補釦 +补挂朝珠 補掛朝珠 +补气固表 補氣固表 +补注 補註 +补票价 補票價 +补种 補種 +补药 補藥 +补血药 補血藥 +补血针 補血針 +补针 補針 +表亲 表親 +表仪 表儀 +表件 表件 +表侄 表侄 +表停 錶停 +表兄 表兄 +表兄弟 表兄弟 +表冊 表冊 +表册 表冊 +表冠 錶冠 +表决 表決 +表决权 表決權 +表出 表出 +表出来 表出來 +表列 表列 +表列出 表列出 +表功 表功 +表单 表單 +表厂 錶廠 +表叔 表叔 +表同情 表同情 +表哥 表哥 +表唱 表唱 +表土 表土 +表土层 表土層 +表壮不如里壮 表壯不如裏壯 +表壳 錶殼 +表壳儿 錶殼兒 +表头 表頭 +表奏 表奏 +表妹 表妹 +表姊 表姊 +表姊妹 表姊妹 +表姊家 表姊家 +表姐 表姐 +表姐妹 表姐妹 +表姑 表姑 +表姨 表姨 +表姨父 表姨父 +表姪 表姪 +表婶 表嬸 +表嫂 表嫂 +表子 表子 +表字 表字 +表尺 表尺 +表层 表層 +表层水 表層水 +表带 錶帶 +表店 錶店 +表弟 表弟 +表形文字 表形文字 +表彰 表彰 +表彰出来 表彰出來 +表征 表徵 +表德 表德 +表快 錶快 +表态 表態 +表态句 表態句 +表情 表情 +表意 表意 +表意文字 表意文字 +表意符阶段 表意符階段 +表慢 錶慢 +表扬 表揚 +表扬大会 表揚大會 +表报 表報 +表揭 表揭 +表文 表文 +表明 表明 +表明心迹 表明心跡 +表显 表顯 +表本 表本 +表板 錶板 +表格 表格 +表格化公文 表格化公文 +表款 錶款 +表沃夫 表沃夫 +表海 表海 +表演 表演 +表演会 表演會 +表演区 表演區 +表演厅 表演廳 +表演性 表演性 +表演欲 表演慾 +表演秀 表演秀 +表演给 表演給 +表演艺术 表演藝術 +表演赛 表演賽 +表演过火 表演過火 +表照 表照 +表率 表率 +表王 錶王 +表现 表現 +表现为 表現爲 +表现为费 表現爲費 +表现主义 表現主義 +表现出 表現出 +表现出来 表現出來 +表现力 表現力 +表现型 表現型 +表现对象 表現對象 +表现形 表現形 +表现自己 表現自己 +表甥 表甥 +表白 表白 +表的历史 錶的歷史 +表的嘀嗒 錶的嘀嗒 +表皮 表皮 +表皮剥脱素 表皮剝脫素 +表皮层 表皮層 +表盘 錶盤 +表相 表相 +表示 表示 +表示出 表示出 +表示出来 表示出來 +表示层 表示層 +表示敬意 表示敬意 +表礼 表禮 +表章 表章 +表笔 表筆 +表笺 表箋 +表背 表背 +表舅 表舅 +表舅母 表舅母 +表荐 表薦 +表蒙子 錶蒙子 +表行 錶行 +表表 表表 +表襮 表襮 +表观 表觀 +表记 表記 +表记物件 表記物件 +表证 表證 +表识 表識 +表词 表詞 +表语 表語 +表象 表象 +表转 錶轉 +表达 表達 +表达出 表達出 +表达出来 表達出來 +表达力 表達力 +表达失语症 表達失語症 +表达式 表達式 +表达能力 表達能力 +表述 表述 +表速 錶速 +表里 表裏 +表里一致 表裏一致 +表里不一 表裏不一 +表里受敌 表裏受敵 +表里如一 表裏如一 +表里山河 表裏山河 +表里相合 表裏相合 +表里相应 表裏相應 +表里相济 表裏相濟 +表针 錶針 +表链 錶鏈 +表错 表錯 +表错情 表錯情 +表门 表門 +表露 表露 +表露出 表露出 +表露无遗 表露無遺 +表面 表面 +表面上 表面上 +表面信息 表面信息 +表面光洁 表面光潔 +表面化 表面化 +表面外膜 表面外膜 +表面工夫 表面工夫 +表面张力 表面張力 +表面性 表面性 +表面活化剂 表面活化劑 +表面活性剂 表面活性劑 +表面流 表面流 +表面的 表面的 +表面积 表面積 +表面质量 表面質量 +表音 表音 +表音文字 表音文字 +表题 表題 +表饰 表飾 +衬出 襯出 +衬布 襯布 +衬托 襯托 +衬托出 襯托出 +衬托底 襯托底 +衬托物 襯托物 +衬里 襯裏 +衰变曲线 衰變曲線 +衰变链 衰變鏈 +衲被蒙头 衲被蒙頭 +衷于 衷於 +衷曲 衷曲 +衿曲 衿曲 +袁于令 袁于令 +袁友范 袁友范 +袁承志 袁承志 +袁术 袁術 +袅娜 嫋娜 +袅娜纤巧 嫋娜纖巧 +袅娜风流 嫋娜風流 +袅窕 裊窕 +袅绕 裊繞 +袅袅 嫋嫋 +袅袅上升 裊裊上升 +袅袅娉娉 嫋嫋娉娉 +袅袅娜娜 嫋嫋娜娜 +袅袅婷婷 嫋嫋婷婷 +袅袅炊烟 裊裊炊煙 +袋表 袋錶 +袋里 袋裏 +袒露出 袒露出 +袖一卷 袖一捲 +袖扣 袖釦 +袖里 袖裏 +袖里乾坤 袖裏乾坤 +袖里来袖里去 袖裏來袖裏去 +被人背 被人揹 +被动吸烟 被動吸菸 +被动挨打 被動挨打 +被发 被髮 +被发佯狂 被髮佯狂 +被发入山 被髮入山 +被发左衽 被髮左衽 +被发文身 被髮文身 +被发现 被發現 +被发缨冠 被髮纓冠 +被发觉 被發覺 +被发阳狂 被髮陽狂 +被复 被複 +被头散发 被頭散髮 +被害妄想症 被害妄想症 +被干 被幹 +被扣 被扣 +被拐 被拐 +被服厂 被服廠 +被泽蒙庥 被澤蒙庥 +被灾蒙祸 被災蒙禍 +被窝里 被窩裏 +被窝里放屁 被窩裏放屁 +被里 被裏 +被面 被面 +袭击战术 襲擊戰術 +袭卷 襲捲 +裁划 裁劃 +裁制 裁製 +裁并 裁併 +裁样板 裁樣板 +裁衣合帐 裁衣合帳 +裂了 裂了 +裂体吸虫 裂體吸蟲 +裂变同位素 裂變同位素 +裂致 裂致 +裂解厂 裂解廠 +裂谷 裂谷 +裂谷热 裂谷熱 +裂谷热病毒 裂谷熱病毒 +装修 裝修 +装修门面 裝修門面 +装出 裝出 +装出去 裝出去 +装出来 裝出來 +装回 裝回 +装岩机 裝岩機 +装折 裝摺 +装点门面 裝點門面 +装糊涂 裝糊塗 +装订厂 裝訂廠 +装配厂 裝配廠 +装配工厂 裝配工廠 +装门面 裝門面 +裒克 裒剋 +裔胄 裔胄 +裕丰 裕豐 +裘弊金尽 裘弊金盡 +裘馨氏肌肉萎缩症 裘馨氏肌肉萎縮症 +裙布荆钗 裙布荊釵 +裙带关系 裙帶關係 +裙摆 裙襬 +裤扣 褲釦 +裴回 裴回 +裴松之 裴松之 +裴航遇云英 裴航遇雲英 +裴里诺 裴里諾 +裸体女尸 裸體女屍 +裸叶 裸葉 +裸尸 裸屍 +裸岩 裸岩 +裸露出 裸露出 +裹了 裹了 +裹尸 裹屍 +裹尸布 裹屍布 +裹尸马革 裹屍馬革 +裹布 裹布 +裹扎 裹紮 +裹脚布 裹腳布 +褐布 褐布 +褒善贬恶 褒善貶惡 +褒录 褒錄 +褒贤遏恶 褒賢遏惡 +褒赞 褒讚 +褒采一介 褒采一介 +褚人获 褚人獲 +褪前擦后 褪前擦後 +褶子了 褶子了 +褶曲 褶曲 +褶曲山脉 褶曲山脈 +褶皱山系 褶皺山系 +褶皱山系火地岛 褶皺山系火地島 +襄赞 襄贊 +襟曲 襟曲 +西丰 西豐 +西丰县 西豐縣 +西冲 西衝 +西出 西出 +西利古里 西利古里 +西北向 西北向 +西北师范大学 西北師範大學 +西北面 西北面 +西华师范大学 西華師範大學 +西南向 西南向 +西南师范大学 西南師範大學 +西南面 西南面 +西占 西佔 +西厂 西廠 +西历 西曆 +西历纪元 西曆紀元 +西原借款 西原借款 +西发里亚条约 西發里亞條約 +西台 西臺 +西台人 西臺人 +西台古堡 西臺古堡 +西台帝国 西臺帝國 +西后 西后 +西向 西向 +西周 西周 +西周时 西周時 +西周时代 西周時代 +西周时期 西周時期 +西周钟 西周鐘 +西哈努克 西哈努克 +西太后 西太后 +西尼克学派 西尼克學派 +西岳 西嶽 +西征 西征 +西文系 西文系 +西方极乐 西方極樂 +西方极乐世界 西方極樂世界 +西晒 西曬 +西曲 西曲 +西来庵 西來庵 +西松 西松 +西松建设 西松建設 +西极 西極 +西格蒙德 西格蒙德 +西欧集团 西歐集團 +西洋参 西洋參 +西游 西遊 +西游补 西遊補 +西游记 西遊記 +西点面包 西點麪包 +西王母娘娘 西王母娘娘 +西米谷 西米谷 +西药 西藥 +西药房 西藥房 +西蒙 西蒙 +西蒙斯 西蒙斯 +西蒙逊 西蒙遜 +西藏百万农奴解放纪念日 西藏百萬農奴解放紀念日 +西西里 西西里 +西西里岛 西西里島 +西谷椰子 西谷椰子 +西谷米 西谷米 +西谷米冻 西谷米凍 +西里 西里 +西里尔 西里爾 +西里尔字母 西裏爾字母 +西里西亚 西里西亞 +西门子电机厂 西門子電機廠 +西除东荡 西除東蕩 +西面 西面 +要么 要麼 +要了 要了 +要价 要價 +要价还价 要價還價 +要克制 要剋制 +要冲 要衝 +要千取万 要千取萬 +要占 要佔 +要占卜 要占卜 +要回 要回 +要干了 要乾了 +要念 要念 +要怎么收获先怎么栽 要怎麼收穫先怎麼栽 +要扣 要扣 +要拐 要拐 +要自制 要自制 +要面子 要面子 +覃天同 覃天同 +覆巢之下无完卵 覆巢之下無完卵 +覆水难收 覆水難收 +覆没 覆沒 +覆盖 覆蓋 +覆盖范围 覆蓋範圍 +覆盖面 覆蓋面 +覆辙 覆轍 +覆雨翻云 覆雨翻雲 +視如寇仇 視如寇讎 +见不到面 見不到面 +见世面 見世面 +见个 見個 +见个情 見個情 +见之不取思之千里 見之不取思之千里 +见了 見了 +见了和尚骂贼秃 見了和尚罵賊禿 +见了新人忘旧人 見了新人忘舊人 +见了面 見了面 +见于 見於 +见几 見幾 +见几而作 見幾而作 +见到面 見到面 +见危致命 見危致命 +见复 見覆 +见尧于墙 見堯於牆 +见得多了 見得多了 +见棱见角 見棱見角 +见物不取失之千里 見物不取失之千里 +见笑于人 見笑於人 +见素抱朴 見素抱樸 +见缝插针 見縫插針 +见见面 見見面 +见过世面 見過世面 +见鉴 見鑒 +见钟不打 見鐘不打 +见钟不打更去炼铜 見鐘不打更去煉銅 +见面 見面 +见面三分情 見面三分情 +见面会 見面會 +见面礼 見面禮 +见马克思 見馬克思 +观众台 觀衆臺 +观光台 觀光臺 +观光周 觀光週 +观光团 觀光團 +观光游憩性资源 觀光遊憩性資源 +观光签证 觀光簽證 +观光胜地 觀光勝地 +观台 觀臺 +观叶植物 觀葉植物 +观后感 觀後感 +观审制度 觀審制度 +观察出来 觀察出來 +观察团 觀察團 +观念 觀念 +观护制度 觀護制度 +观星台 觀星臺 +观景台 觀景臺 +观望台 觀望臺 +观测台 觀測臺 +观礼台 觀禮臺 +观者云集 觀者雲集 +观者如云 觀者如雲 +观象台 觀象臺 +观采 觀採 +观音庵 觀音庵 +规划 規劃 +规划为 規劃爲 +规划人员 規劃人員 +规划出 規劃出 +规划好 規劃好 +规划局 規劃局 +规划成 規劃成 +规划案 規劃案 +规划范围 規劃範圍 +规制 規制 +规复 規復 +规定价格 規定價格 +规定出来 規定出來 +规定地价 規定地價 +规定范围 規定範圍 +规画出 規畫出 +规矩准绳 規矩準繩 +规章制度 規章制度 +规范 規範 +规范企业 規範企業 +规范作用 規範作用 +规范动作 規範動作 +规范化 規範化 +规范化管理 規範化管理 +规范司 規範司 +规范名 規範名 +规范字 規範字 +规范学 規範學 +规范市场 規範市場 +规范性 規範性 +规范性文件 規範性文件 +规范意见 規範意見 +规范执法 規範執法 +规范技术 規範技術 +规范文件 規範文件 +规范理论 規範理論 +规范的 規範的 +规范管理 規範管理 +规范行为 規範行爲 +规范语言 規範語言 +视于 視於 +视力表 視力表 +视同 視同 +视同儿戏 視同兒戲 +视同具文 視同具文 +视同手足 視同手足 +视同秦越 視同秦越 +视同陌路 視同陌路 +视如寇仇 視如寇仇 +视如己出 視如己出 +视杯 視杯 +视空间系统 視空間系統 +视窗基准 視窗基準 +视觉系 視覺系 +视觉艺术 視覺藝術 +视觉适应 視覺適應 +视讯会议系统 視訊會議系統 +视讯系统 視訊系統 +视野范围 視野範圍 +览胜 覽勝 +觉出 覺出 +觉发 覺發 +觉察出 覺察出 +觊幸 覬倖 +觊觎之志 覬覦之志 +觌面 覿面 +觑个意顺 覷個意順 +觑当 覷當 +角化症 角化症 +角斗 角鬥 +角斗场 角鬥場 +角斗士 角鬥士 +角曲尺 角曲尺 +角砾岩 角礫岩 +角膜移植术 角膜移植術 +角色冲突 角色衝突 +角色扮演游戏 角色扮演遊戲 +角落发 角落發 +角落里 角落裏 +角谷猜想 角谷猜想 +角里 角里 +角页岩 角頁岩 +觔斗 觔斗 +觔斗云 觔斗雲 +解了 解了 +解决不了 解決不了 +解决困难 解決困難 +解出 解出 +解出来 解出來 +解到县里 解到縣裏 +解制 解制 +解剑拜仇 解劍拜仇 +解发 解發 +解发佯狂 解髮佯狂 +解困 解困 +解当 解當 +解当铺 解當鋪 +解扣 解釦 +解放出来 解放出來 +解放后 解放後 +解救出来 解救出來 +解析几何 解析幾何 +解析几何学 解析幾何學 +解毒药 解毒藥 +解理方向 解理方向 +解理面 解理面 +解痛药 解痛藥 +解药 解藥 +解酸药 解酸藥 +解铃仍须系铃人 解鈴仍須繫鈴人 +解铃系铃 解鈴繫鈴 +解铃还是系铃人 解鈴還是繫鈴人 +解铃还须系铃人 解鈴還須繫鈴人 +解雇 解僱 +触事面墙 觸事面牆 +触发 觸發 +触发器 觸發器 +触发式 觸發式 +触发引信 觸發引信 +触发清单 觸發清單 +触技曲 觸技曲 +触控板 觸控板 +触斗蛮争 觸鬥蠻爭 +触板 觸板 +触须 觸鬚 +觱发 觱發 +言不尽意 言不盡意 +言且表过 言且表過 +言之不尽 言之不盡 +言之有据 言之有據 +言云 言云 +言出如山 言出如山 +言出必行 言出必行 +言出患入 言出患入 +言出法随 言出法隨 +言大而夸 言大而夸 +言尽于此 言盡於此 +言归于好 言歸於好 +言必有据 言必有據 +言文一致 言文一致 +言无不尽 言無不盡 +言无二价 言無二價 +言行一致 言行一致 +言行合一 言行合一 +言行录 言行錄 +言语失常症 言語失常症 +言辩而确 言辯而确 +言隐于荣华 言隱於榮華 +証据 証據 +詩云 詩云 +詹千慧 詹千慧 +詹姆士布朗 詹姆士布朗 +詹姆斯布朗 詹姆斯布朗 +詹宏志 詹宏志 +詹志宏 詹志宏 +詹志维 詹誌維 +詹氏年鉴 詹氏年鑑 +詹江布尔 詹江布爾 +誊写板 謄寫板 +誊出 謄出 +誊出来 謄出來 +誊录 謄錄 +誊录所 謄錄所 +誓同生死 誓同生死 +誓愿 誓願 +誓无二志 誓無二志 +謷丑 謷醜 +謻台 謻臺 +譆譆出出 譆譆出出 +警世钟 警世鐘 +警察制度 警察制度 +警察广播电台 警察廣播電臺 +警报系统 警報系統 +警报钟 警報鐘 +警民冲突 警民衝突 +警示钟 警示鐘 +警辟 警闢 +警钟 警鐘 +譬似闲 譬似閒 +譬如闲 譬如閒 +计价 計價 +计价器 計價器 +计出万全 計出萬全 +计分板 計分板 +计划 計劃 +计划书 計劃書 +计划图 計劃圖 +计划好 計劃好 +计划性 計劃性 +计划案 計劃案 +计划生育 計劃生育 +计划目标 計劃目標 +计划经济 計劃經濟 +计划署 計劃署 +计划者 計劃者 +计将安出 計將安出 +计尽力穷 計盡力窮 +计无所出 計無所出 +计时表 計時錶 +计白当黑 計白當黑 +计程车共乘制 計程車共乘制 +计穷力尽 計窮力盡 +计穷力极 計窮力極 +计穷虑极 計窮慮極 +计算出 計算出 +计算出来 計算出來 +计算机制图 計算機制圖 +计算机集成制造 計算機集成制造 +计量制 計量制 +订个 訂個 +订了 訂了 +订于 訂於 +订价 訂價 +订出 訂出 +订出来 訂出來 +订制 訂製 +订制服 訂製服 +订杂志 訂雜誌 +认不出 認不出 +认不出来 認不出來 +认个 認個 +认了 認了 +认准 認準 +认出 認出 +认出来 認出來 +认制修 認製修 +认同 認同 +认同卡 認同卡 +认同度 認同度 +认同感 認同感 +认尸 認屍 +认得出 認得出 +认知神经心里学 認知神經心裏學 +讦发 訐發 +讨个 討個 +讨个分晓 討個分曉 +讨个吉利 討個吉利 +讨了 討了 +讨价 討價 +讨价还价 討價還價 +讨出 討出 +讨出来 討出來 +讨回 討回 +讨彩 討彩 +讨恶剪暴 討惡剪暴 +讨没脸面 討沒臉面 +讨针线 討針線 +讨面皮 討面皮 +让价 讓價 +让位于 讓位於 +让出 讓出 +让出去 讓出去 +让出来 讓出來 +让胡路 讓胡路 +让胡路区 讓胡路區 +讫了 訖了 +讬了 託了 +训兽术 訓獸術 +训练出 訓練出 +训练出来 訓練出來 +训胄 訓胄 +训蒙 訓蒙 +议事录 議事錄 +议事纪录 議事紀錄 +议价 議價 +议价空间 議價空間 +议会制 議會制 +议会斗争 議會鬥爭 +议和团 議和團 +议坛 議壇 +讯息处理系统 訊息處理系統 +讯框传送论坛 訊框傳送論壇 +记不了 記不了 +记了 記了 +记事板 記事板 +记仇 記仇 +记分板 記分板 +记录 記錄 +记录下 記錄下 +记录下来 記錄下來 +记录为 記錄爲 +记录仪 記錄儀 +记录到 記錄到 +记录员 記錄員 +记录器 記錄器 +记录本 記錄本 +记录板 記錄板 +记录片 記錄片 +记录簿 記錄簿 +记念 記念 +记挂 記掛 +记者团 記者團 +讲个 講個 +讲了 講了 +讲价 講價 +讲信修睦 講信修睦 +讲出 講出 +讲出去 講出去 +讲出来 講出來 +讲台 講臺 +讲坛 講壇 +讲情面 講情面 +讲闲话 講閒話 +讲面子 講面子 +讳恶不悛 諱惡不悛 +许下愿心 許下願心 +许久以后 許久以後 +许人丰 許人丰 +许凯克 許凱克 +许历农 許歷農 +许圣杰 許聖杰 +许建发 許建發 +许志华 許志華 +许志彰 許志彰 +许志煌 許志煌 +许志雄 許志雄 +许志鸿 許志鴻 +许愿 許願 +许愿树 許願樹 +许愿池 許願池 +许愿牌 許願牌 +许愿起经 許愿起經 +许振发 許振發 +许智杰 許智傑 +许杰 許傑 +许杰辉 許傑輝 +许胜发 許勝發 +许胜雄 許勝雄 +许至胜 許至勝 +许致强 許致強 +许豪升 許豪升 +许铭杰 許銘傑 +论价 論價 +论千论万 論千論萬 +论坛 論壇 +论坛区 論壇區 +论坛报 論壇報 +论据 論據 +论据不足 論據不足 +论赞 論贊 +设了 設了 +设于 設於 +设党 設黨 +设厂 設廠 +设台 設臺 +设坛 設壇 +设坛祭拜 設壇祭拜 +设朝升殿 設朝升殿 +设柜 設櫃 +设言托意 設言托意 +设计出 設計出 +设计出来 設計出來 +设计系 設計系 +设计规范 設計規範 +设限于 設限於 +设鼓悬钟 設鼓懸鐘 +访华团 訪華團 +访台 訪臺 +访台之旅 訪臺之旅 +访察团 訪察團 +访日团 訪日團 +访问团 訪問團 +访韩团 訪韓團 +诀别 訣別 +诀别书 訣別書 +证于 證於 +证出 證出 +证出来 證出來 +证卷 證卷 +证卷交易所 證卷交易所 +证据 證據 +证据力 證據力 +证据裁判主义 證據裁判主義 +证明了 證明了 +证明出来 證明出來 +评个 評個 +评个分数 評個分數 +评了 評了 +评价 評價 +评价分类 評價分類 +评估板 評估板 +评出 評出 +评出来 評出來 +评分标准 評分標準 +评判出 評判出 +评卷 評卷 +评审团 評審團 +评审团特别奖 評審團特別獎 +评核 評覈 +评注 評註 +评选出 評選出 +评鉴 評鑑 +评鉴为 評鑑爲 +评鉴报告 評鑑報告 +识别 識別 +识别信号 識別信號 +识别力 識別力 +识别区 識別區 +识别号 識別號 +识别字 識別字 +识别码 識別碼 +识别証 識別証 +识别证 識別證 +识多才广 識多才廣 +识微见几 識微見幾 +识才 識才 +识才尊贤 識才尊賢 +识时务者为俊杰 識時務者爲俊傑 +识面 識面 +识面台官 識面臺官 +诈哄 詐哄 +诈奸不及 詐奸不及 +诈尸 詐屍 +诈术 詐術 +诈胡 詐胡 +诉愿 訴願 +诉愿权 訴願權 +诉愿状 訴願狀 +诉说出来 訴說出來 +诉诸于 訴諸於 +诊断出 診斷出 +诊断出来 診斷出來 +诋毁 詆譭 +词余 詞餘 +词干 詞幹 +词干启动 詞幹啓動 +词无枝叶 詞無枝葉 +词曲 詞曲 +词汇 詞彙 +词汇分解 詞彙分解 +词汇判断 詞彙判斷 +词汇判断任务 詞彙判斷任務 +词汇判断作业 詞彙判斷作業 +词汇判断法 詞彙判斷法 +词汇学 詞彙學 +词汇通路 詞彙通路 +词穷理尽 詞窮理盡 +词表 詞表 +词语汇 詞語彙 +词采 詞采 +诏板 詔板 +译制 譯製 +译注 譯註 +诓哄 誆哄 +诔赞 誄讚 +试出 試出 +试出来 試出來 +试制 試製 +试卷 試卷 +试周 試周 +试炼 試煉 +试种 試種 +试算表 試算表 +试胄 試胄 +试药 試藥 +试表 試表 +试验台 試驗檯 +诗云 詩云 +诗云子曰 詩云子曰 +诗以言志 詩以言志 +诗余 詩餘 +诗卷 詩卷 +诗坛 詩壇 +诗才 詩才 +诗言志歌永言 詩言志歌永言 +诗词曲语辞汇释 詩詞曲語辭匯釋 +诗赞 詩讚 +诗钟 詩鐘 +诘曲 詰曲 +诙谐曲 詼諧曲 +诚征 誠徵 +诚朴 誠樸 +诛凶殄逆 誅兇殄逆 +诛凶讨逆 誅兇討逆 +诛尽杀绝 誅盡殺絕 +诛戮殆尽 誅戮殆盡 +话别 話別 +话剧表演 話劇表演 +话又说回来 話又說回來 +话念 話念 +话说回来 話說回來 +话里套话 話裏套話 +话里有话 話裏有話 +话里藏阄 話裏藏鬮 +诠注 詮註 +诠释出 詮釋出 +诡计百出 詭計百出 +诡辩术 詭辯術 +询于 詢於 +询于刍荛 詢於芻蕘 +询价 詢價 +该于 該於 +该党 該黨 +该厂 該廠 +该向 該向 +该回 該回 +该当 該當 +该当何罪 該當何罪 +该扣 該扣 +该钟 該鐘 +该面 該面 +详尽 詳盡 +详尽无遗 詳盡無遺 +详征博引 詳徵博引 +详注 詳註 +详确 詳確 +诬蔑 誣衊 +诬蔑性 誣衊性 +语云 語云 +语出 語出 +语录 語錄 +语意区分量表 語意區分量表 +语意差别量表 語意差別量表 +语有云 語有云 +语汇 語彙 +语法术语 語法術語 +语系 語系 +语言规范 語言規範 +语音合成 語音合成 +语音失语症 語音失語症 +语音数据机 語音數據機 +语音识别 語音識別 +语音输入系统 語音輸入系統 +误尽天下苍生 誤盡天下蒼生 +误差范围 誤差範圍 +诱出 誘出 +诱发 誘發 +诱发型 誘發型 +诱发式 誘發式 +诱奸 誘姦 +诱拐 誘拐 +诱掖后进 誘掖後進 +诱敌战术 誘敵戰術 +诱杀战术 誘殺戰術 +诱虫灯 誘蟲燈 +诱降战术 誘降戰術 +说不了 說不了 +说不准 說不準 +说不出 說不出 +说不出口 說不出口 +说不出来 說不出來 +说不出话来 說不出話來 +说不尽 說不盡 +说了 說了 +说了又说 說了又說 +说了算 說了算 +说出 說出 +说出去 說出去 +说出口 說出口 +说出来 說出來 +说出话来 說出話來 +说千说万 說千說萬 +说单生万岁 說單生萬歲 +说参请 說參請 +说合 說合 +说合了盖儿了 說合了蓋兒了 +说嘴郎中无好药 說嘴郎中無好藥 +说回来 說回來 +说好了 說好了 +说定了 說定了 +说家克计 說家克計 +说尽 說盡 +说岳 說岳 +说岳全传 說岳全傳 +说干便干 說幹便幹 +说教术 說教術 +说文解字注 說文解字注 +说明了 說明了 +说白了 說白了 +说穿了 說穿了 +说话不当话 說話不當話 +说闲话 說閒話 +说雨谈云 說雨談雲 +诵念 誦唸 +诵经台 誦經臺 +请个 請個 +请了 請了 +请出 請出 +请出去 請出去 +请出来 請出來 +请别见怪 請別見怪 +请勿吸烟 請勿吸菸 +请参阅 請參閱 +请向 請向 +请君入瓮 請君入甕 +请回 請回 +请回到 請回到 +请回去 請回去 +请回来 請回來 +请您回复 請您回復 +请愿 請願 +请愿书 請願書 +请愿团 請願團 +请愿权 請願權 +请愿案 請願案 +请托 請託 +请说出 請說出 +诸余 諸餘 +诸葛亮借东风 諸葛亮借東風 +诺瓦克 諾瓦克 +诺福克 諾福克 +诺福克岛 諾福克島 +诺美克斯 諾美克斯 +诺里 諾里 +读万卷书 讀萬卷書 +读万卷书行万里路 讀萬卷書行萬里路 +读不舍手 讀不捨手 +读书三余 讀書三余 +读书种子 讀書種子 +读了 讀了 +读出 讀出 +读出来 讀出來 +读后 讀後 +读后心得 讀後心得 +读后感 讀後感 +读心术 讀心術 +读脣术 讀脣術 +课余 課餘 +课前课后 課前課後 +课卷 課卷 +课后 課後 +课后复习 課後複習 +课后练习 課後練習 +课后辅导 課後輔導 +课征 課徵 +课田制 課田制 +课程标准 課程標準 +课程表 課程表 +课表 課表 +课表上 課表上 +诿过于人 諉過於人 +谁个 誰個 +谁家灶內无烟 誰家竈內無煙 +谁干 誰幹 +谁干净 誰乾淨 +谁干的 誰幹的 +谁笑到最后谁笑得最好 誰笑到最後誰笑得最好 +谁笑在最后谁笑得最好 誰笑在最後誰笑得最好 +调个 調個 +调了 調了 +调价 調價 +调准 調準 +调出 調出 +调出去 調出去 +调出来 調出來 +调制 調製 +调制出 調製出 +调制法 調製法 +调制波 調製波 +调制解调器 調制解調器 +调升 調升 +调卷 調卷 +调发 調發 +调合 調合 +调合漆 調合漆 +调哄 調哄 +调回 調回 +调回去 調回去 +调回来 調回來 +调幅台 調幅臺 +调干 調幹 +调弦 調絃 +调当 調當 +调整范围 調整範圍 +调查团 調查團 +调查范围 調查範圍 +调查表 調查表 +调査出 調查出 +调査团 調查團 +调査表 調查表 +调画出 調畫出 +调色板 調色板 +调药刀 調藥刀 +调表 調錶 +调适 調適 +调配出 調配出 +调钟表 調鐘錶 +调频台 調頻臺 +调频电台 調頻電臺 +谈不出 談不出 +谈了 談了 +谈出 談出 +谈判代表 談判代表 +谈判制度 談判制度 +谈吐有致 談吐有致 +谈征 談徵 +谈得出 談得出 +谈论出 談論出 +谈辞如云 談辭如雲 +谋出路 謀出路 +谋划 謀劃 +谋取面试 謀取面試 +谋定后动 謀定後動 +谋定而后 謀定而後 +谋干 謀幹 +谋而后动 謀而後動 +谋面 謀面 +谎价 謊價 +谎敲才 謊敲才 +谐价 諧價 +谐当 諧當 +谙历 諳歷 +谜团 謎團 +谜面 謎面 +谢了 謝了 +谢克 謝克 +谢克尔 謝克爾 +谢别 謝別 +谢叶配 謝葉配 +谢安折屐 謝安折屐 +谢尔托夫 謝爾托夫 +谢志伟 謝志偉 +谢福松 謝福松 +谢秋华 謝秋華 +谢系 謝系 +谢绝参观 謝絕參觀 +谢苏 謝蘇 +谢苏叶 謝蘇葉 +谢苏配 謝蘇配 +谢表 謝表 +谢里 謝里 +谢里夫 謝里夫 +谣言止于 謠言止於 +谣言止于智者 謠言止於智者 +谦克 謙克 +谦冲 謙沖 +谦冲自牧 謙沖自牧 +谨于心 謹於心 +谨致 謹致 +谨致谢意 謹致謝意 +谬以千里 謬以千里 +谬种 謬種 +谬种流传 謬種流傳 +谬误百出 謬誤百出 +谬赞 謬讚 +谬采虚声 謬採虛聲 +谭嗣同 譚嗣同 +谯周 譙周 +谯周独笑 譙周獨笑 +谱出 譜出 +谱出来 譜出來 +谱录 譜錄 +谱曲 譜曲 +谱系 譜系 +谱系分类法 譜系分類法 +谱表 譜表 +谷中谷 谷中谷 +谷人 穀人 +谷仓 穀倉 +谷保家商 穀保家商 +谷关 谷關 +谷口 谷口 +谷口耕岩 谷口耕岩 +谷圭 穀圭 +谷地 谷地 +谷场 穀場 +谷垣 谷垣 +谷垣祯 谷垣禎 +谷垣祯一 谷垣禎一 +谷城 谷城 +谷城县 谷城縣 +谷壁 谷壁 +谷壳 穀殼 +谷子 穀子 +谷峰 谷峯 +谷川 谷川 +谷底 谷底 +谷日 穀日 +谷旦 穀旦 +谷月涵 谷月涵 +谷树皮 谷樹皮 +谷梁 穀梁 +谷梁传 穀梁傳 +谷歌 谷歌 +谷氨酰胺 谷氨醯胺 +谷氨酸 穀氨酸 +谷永 谷永 +谷湾 谷灣 +谷物 穀物 +谷皮 穀皮 +谷神 穀神 +谷神星 穀神星 +谷穗 穀穗 +谷米 穀米 +谷类 穀類 +谷类作物 穀類作物 +谷粉 穀粉 +谷粒 穀粒 +谷糠 穀糠 +谷舱 穀艙 +谷苗 穀苗 +谷草 穀草 +谷蠡 谷蠡 +谷象 谷象 +谷贱伤农 穀賤傷農 +谷贵饿农 穀貴餓農 +谷贵饿农谷贱伤农 穀貴餓農穀賤傷農 +谷道 穀道 +谷都 谷都 +谷雨 穀雨 +谷风 穀風 谷風 +谷食 穀食 +谷饮 谷飲 +谿谷 谿谷 +豁上干 豁上幹 +豁出 豁出 +豁出去 豁出去 +豁出命干 豁出命幹 +豁出来 豁出來 +豁命干 豁命幹 +豁死干 豁死幹 +豁荡 豁蕩 +豁起来干 豁起來幹 +豆制品 豆製品 +豆姑娘 豆姑娘 +豆娘 豆娘 +豆娘子 豆娘子 +豆干 豆乾 +豆干展 豆乾展 +豆干肉丝 豆干肉絲 +豆签 豆簽 +豆腐干 豆腐乾 +豆面 豆麪 +豉虫 豉蟲 +象征 象徵 +象征主义 象徵主義 +象征性 象徵性 +象征着 象徵着 +象板 象板 +象牙针尖 象牙針尖 +象牙雕 象牙雕 +象箸玉杯 象箸玉杯 +象鼻虫 象鼻蟲 +豪华游 豪華遊 +豪厘千里 豪釐千里 +豪情万丈 豪情萬丈 +豪情壮志 豪情壯志 +豪情逸致 豪情逸致 +豪杰 豪傑 +豪气万丈 豪氣萬丈 +豪气万千 豪氣萬千 +豪气干云 豪氣干雲 +豪门贵胄 豪門貴胄 +豫游 豫遊 +豺狼当涂 豺狼當塗 +豺狼当路 豺狼當路 +豺狼当道 豺狼當道 +貂复额 貂覆額 +貌合心离 貌合心離 +貌合情离 貌合情離 +貌合神离 貌合神離 +貌合行离 貌合行離 +貌同实异 貌同實異 +買凶 買兇 +賈后 賈后 +賢后 賢后 +賨布 賨布 +賸余 賸餘 +賸馀价值 賸餘價值 +贝他系数 貝他係數 +贝伽特症候群 貝伽特症候羣 +贝克 貝克 +贝克休斯 貝克休斯 +贝克勒 貝克勒 +贝克勒耳 貝克勒耳 +贝克尔 貝克爾 +贝克曼 貝克曼 +贝克汉 貝克漢 +贝克汉姆 貝克漢姆 +贝克特 貝克特 +贝卡谷地 貝卡谷地 +贝叶 貝葉 +贝叶书 貝葉書 +贝叶树 貝葉樹 +贝叶棕 貝葉棕 +贝叶经 貝葉經 +贝奇马克 貝奇馬克 +贝娅特丽克丝 貝婭特麗克絲 +贝娜齐尔布托 貝娜齊爾布托 +贝尔托內 貝爾托內 +贝尔杰 貝爾傑 +贝当 貝當 +贝理克 貝理克 +贝胄 貝冑 +贝西克 貝西克 +贝那芬托 貝那芬托 +贝里 貝里 +贝里拉 貝里拉 +贝里斯 貝里斯 +贝雕 貝雕 +贞丰 貞豐 +贞丰县 貞豐縣 +贞卜文字 貞卜文字 +负乘致寇 負乘致寇 +负债累累 負債累累 +负债表 負債表 +负后像 負後像 +负向 負向 +负回馈 負回饋 +负图之托 負圖之托 +负心违愿 負心違願 +负手板 負手板 +负才 負才 +负才任气 負才任氣 +负才使气 負才使氣 +负极 負極 +负气斗狠 負氣鬥狠 +负笈千里 負笈千里 +负荷不了 負荷不了 +负重致远 負重致遠 +负面 負面 +负面性 負面性 +负鼎之愿 負鼎之願 +贡烟 貢菸 +贡献出 貢獻出 +财产价值 財產價值 +财产关系 財產關係 +财产目录 財產目錄 +财划法 財劃法 +财务报表 財務報表 +财团 財團 +财团法人 財團法人 +财布施 財佈施 +财政支出 財政支出 +财殚力尽 財殫力盡 +财竭力尽 財竭力盡 +财金系 財金系 +责任准备 責任準備 +责任制 責任制 +责任范围 責任範圍 +贤后 賢后 +贤奸倒置 賢奸倒置 +贤才 賢才 +贤胄 賢胄 +败于 敗於 +败于垂成 敗於垂成 +败兵折将 敗兵折將 +败军折将 敗軍折將 +败叶 敗葉 +败坏门面 敗壞門面 +败子回头 敗子回頭 +败子回头金不换 敗子回頭金不換 +败血性休克 敗血性休克 +败血症 敗血症 +败血脓毒症 敗血膿毒症 +败部复活 敗部復活 +账面 賬面 +货价 貨價 +货卜 貨卜 +货布 貨布 +货柜 貨櫃 +货柜场 貨櫃場 +货柜屋 貨櫃屋 +货柜船 貨櫃船 +货柜车 貨櫃車 +货柜轮 貨櫃輪 +货物柜 貨物櫃 +货真价实 貨真價實 +质当 質當 +质朴 質樸 +质量规范 質量規範 +贪婪是万恶之源 貪婪是萬惡之源 +贪念 貪念 +贪杯 貪杯 +贪杯惜醉人 貪杯惜醉人 +贪欲 貪慾 +贪欲无艺 貪慾無藝 +贪生恶死 貪生惡死 +贪生舍义 貪生舍義 +贪睡虫 貪睡蟲 +贫嘴恶舌 貧嘴惡舌 +贫困 貧困 +贫困人家 貧困人家 +贫困化 貧困化 +贫困地区 貧困地區 +贫困率 貧困率 +贫寒出身 貧寒出身 +贫血症 貧血症 +贬价 貶價 +购买欲 購買慾 +购回 購回 +购并 購併 +购并案 購併案 +购彩 購彩 +购物台 購物臺 +购物欲 購物慾 +购销差价 購銷差價 +贯云石 貫雲石 +贯注 貫注 +贱价 賤價 +贱价出售 賤價出售 +贱才 賤才 +贱敛贵出 賤斂貴出 +贱敛贵发 賤斂貴發 +贴个 貼個 +贴出 貼出 +贴出去 貼出去 +贴出来 貼出來 +贴后 貼後 +贴布 貼布 +贵了 貴了 +贵价 貴价 +贵州师范大学 貴州師範大學 +贵干 貴幹 +贵幸 貴幸 +贵征 貴徵 +贵戚 貴戚 +贵极人臣 貴極人臣 +贵游子弟 貴遊子弟 +贵胄 貴胄 +贵贱之别 貴賤之別 +贷个 貸個 +贷了 貸了 +贷借 貸借 +贷出 貸出 +贸易伙伴 貿易伙伴 +费了 費了 +费利克斯 費利克斯 +费占 費佔 +费卢杰 費盧傑 +费后 費後 +费周章 費周章 +费尔干纳 費爾干納 +费尔干纳槃地 費爾干納槃地 +费尔干纳盆地 費爾幹納盆地 +费尔法克斯 費爾法克斯 +费尽 費盡 +费尽心思 費盡心思 +费尽心机 費盡心機 +费尽精神 費盡精神 +费尽脣舌 費盡脣舌 +费杰罗 費傑羅 +费洛蒙 費洛蒙 +费玛最后定理 費瑪最後定理 +费舍尔 費舍爾 +费茨派垂克 費茨派垂克 +费达克 費達克 +费里克斯 費里克斯 +费里克斯布朗 費里克斯布朗 +费里尼 費里尼 +费里斯特 費里斯特 +贺后骂殿 賀后罵殿 +贺尔蒙 賀爾蒙 +贺尔蒙针 賀爾蒙針 +贺表 賀表 +贻笑万世 貽笑萬世 +贻笑千古 貽笑千古 +贻笑千秋 貽笑千秋 +贻羞万年 貽羞萬年 +贻范 貽範 +贻范古今 貽範古今 +贼党 賊黨 +贼没种只怕哄 賊沒種只怕哄 +贾克逊 賈克遜 +贾可布斯 賈可布斯 +贾后 賈后 +贾斯克 賈斯克 +贾氏症 賈氏症 +贾第虫 賈第蟲 +贾第虫属 賈第蟲屬 +贾第虫病 賈第蟲病 +贾胡 賈胡 +贾里尔 賈里爾 +贾鸿秋 賈鴻秋 +贿赂并行 賄賂並行 +资产价值 資產價值 +资产组合 資產組合 +资产负债表 資產負債表 +资历 資歷 +资料介面 資料介面 +资料库管理系统 資料庫管理系統 +资料录 資料錄 +资料表 資料表 +资料链结层 資料鏈結層 +资方代表 資方代表 +资治通鉴 資治通鑑 +资源回收 資源回收 +资管系 資管系 +资讯学系 資訊學系 +资讯系 資訊系 +资金占用 資金佔用 +资金面 資金面 +赈饥 賑饑 +赉发 賚發 +赊借 賒借 +赋别 賦別 +赋格曲 賦格曲 +赋诗言志 賦詩言志 +赋闲 賦閒 +赌了 賭了 +赌台 賭檯 +赌咒发愿 賭咒發願 +赌当 賭當 +赌斗 賭鬥 +赌注 賭注 +赌胜 賭勝 +赌身发誓 賭身發誓 +赌钱厂 賭錢廠 +赍发 齎發 +赍志没地 齎志沒地 +赍志而殁 齎志而歿 +赎回 贖回 +赎回权 贖回權 +赎当 贖當 +赎药 贖藥 +赏不当功 賞不當功 +赏了 賞了 +赏同罚异 賞同罰異 +赏善罚恶 賞善罰惡 +赏罚不当 賞罰不當 +赏赞 賞讚 +赏鉴 賞鑑 +赏面子 賞面子 +赐了 賜了 +赐复 賜復 +赐恤 賜卹 +赒急扶困 賙急扶困 +赔个不是 賠個不是 +赔了 賠了 +赔了夫人 賠了夫人 +赔了夫人又折兵 賠了夫人又折兵 +赔出 賠出 +赔尽 賠盡 +赖于 賴於 +赖幸媛 賴幸媛 +赖索托 賴索托 +赖索托王国 賴索托王國 +赖蒙特 賴蒙特 +赚不了 賺不了 +赚个 賺個 +赚个饱 賺個飽 +赚了 賺了 +赚哄 賺哄 +赚回来 賺回來 +赛制 賽制 +赛后 賽後 +赛德克 賽德克 +赛愿 賽願 +赛程表 賽程表 +赛里木湖 賽里木湖 +赛门铁克 賽門鐵克 +赞一句 讚一句 +赞一声 讚一聲 +赞一赞 讚一讚 +赞不绝口 讚不絕口 +赞两句 讚兩句 +赞个不 讚個不 +赞乐 讚樂 +赞了 讚了 +赞伯拉诺 贊伯拉諾 +赞佩 讚佩 +赞佩不已 讚佩不已 +赞助 贊助 +赞助人 贊助人 +赞助商 贊助商 +赞助者 贊助者 +赞口不 讚口不 +赞叹 讚歎 +赞叹不已 讚歎不已 +赞叹声 讚歎聲 +赞同 贊同 +赞呗 讚唄 +赞善 贊善 +赞布拉诺 贊布拉諾 +赞成 贊成 +赞成票 贊成票 +赞成者 贊成者 +赞我 讚我 +赞扬 讚揚 +赞拜 贊拜 +赞普 贊普 +赞歌 讚歌 +赞歎 讚歎 +赞比亚 贊比亞 +赞理 贊理 +赞的 讚的 +赞皇 贊皇 +赞皇县 贊皇縣 +赞礼 贊禮 +赞美 讚美 +赞美有加 讚美有加 +赞美歌 讚美歌 +赞美诗 讚美詩 +赞羡 讚羨 +赞翼 贊翼 +赞自己 讚自己 +赞誉 讚譽 +赞誉为 讚譽爲 +赞许 讚許 +赞词 讚詞 +赞语 讚語 +赞赏 讚賞 +赞赏不已 讚賞不已 +赞辞 讚辭 +赞道 讚道 +赞颂 讚頌 +赠别 贈別 +赡表子 贍表子 +赢不了 贏不了 +赢了 贏了 +赢余 贏餘 +赢出 贏出 +赢回 贏回 +赢回去 贏回去 +赢回来 贏回來 +赢奸卖俏 贏奸賣俏 +赢定了 贏定了 +赢面 贏面 +赣南师范学院 贛南師範學院 +赤地千里 赤地千里 +赤术 赤朮 +赤松 赤松 +赤松子 赤松子 +赤留出律 赤留出律 +赤绳系足 赤繩繫足 +赤道几內亚 赤道幾內亞 +赤道几內亚共和国 赤道幾內亞共和國 +赤道几内亚 赤道幾內亞 +赤道面 赤道面 +赤霉素 赤黴素 +赫克比 赫克比 +赫克迈泰尔 赫克邁泰爾 +赫弗里希 赫弗里希 +赫拉克利特 赫拉克利特 +赫里斯 赫里斯 +赫里斯登科 赫里斯登科 +赫麦汉士台 赫麥漢士臺 +赭面 赭面 +走上台 走上臺 +走下台 走下臺 +走不了 走不了 +走了 走了 +走了和尚走不了庙 走了和尚走不了廟 +走了大褶儿 走了大褶兒 +走了风声 走了風聲 +走了马脚 走了馬腳 +走亲戚 走親戚 +走出 走出 +走出去 走出去 +走出来 走出來 +走千门踏万户 走千門踏萬戶 +走后 走後 +走后门 走後門 +走向 走向 +走向前去 走向前去 +走向前来 走向前來 +走向断层 走向斷層 +走向滑动断层 走向滑動斷層 +走回 走回 +走回去 走回去 +走回头 走回頭 +走回头路 走回頭路 +走回来 走回來 +走回路 走迴路 +走娘家 走孃家 +走廊里 走廊裏 +走来回 走來回 +走板 走板 +走板眼 走板眼 +走肉行尸 走肉行屍 +走马章台 走馬章臺 +走骨行尸 走骨行屍 +赴台 赴臺 +赴曲 赴曲 +赵丰邦 趙豐邦 +赵云 趙雲 +赵五娘 趙五娘 +赵坤郁 趙坤郁 +赵庄 趙莊 +赵志坚 趙志堅 +赵无极 趙無極 +赵治勋 趙治勳 +赵玄坛 趙玄壇 +赶下台 趕下臺 +赶不出 趕不出 +赶不出来 趕不出來 +赶了 趕了 +赶修 趕修 +赶出 趕出 +赶出去 趕出去 +赶出来 趕出來 +赶制 趕製 +赶前不赶后 趕前不趕後 +赶向 趕向 +赶回 趕回 +赶回到 趕回到 +赶回去 趕回去 +赶回来 趕回來 +赶尸 趕屍 +赶尽杀绝 趕盡殺絕 +赶录 趕錄 +赶得出 趕得出 +赶面棍 趕麪棍 +起不了 起不了 +起了 起了 +起价 起價 +起出 起出 +起发性 起發性 +起吊 起吊 +起哄 起鬨 +起因于 起因於 +起复 起復 +起居注 起居注 +起扑杆 起撲桿 +起承转合 起承轉合 +起根发脚 起根發腳 +起模范 起模範 +起死回生 起死回生 +起源于 起源於 +起航出海 起航出海 +起获 起獲 +起跳价 起跳價 +起跳板 起跳板 +趁哄 趁哄 +趁哄打劫 趁哄打劫 +趁愿 趁願 +超世之才 超世之才 +超低价 超低價 +超党派 超黨派 +超准 超準 +超凡出世 超凡出世 +超出 超出 +超出范围 超出範圍 +超前瞄准 超前瞄準 +超前绝后 超前絕後 +超升 超升 +超友谊关系 超友誼關係 +超基性岩 超基性岩 +超大曲道赛 超大曲道賽 +超完美告别 超完美告別 +超尘出俗 超塵出俗 +超平面 超平面 +超手游廊 超手遊廊 +超新星剩余 超新星剩餘 +超杯 超杯 +超然台 超然臺 +超级台风 超級颱風 +超级杯 超級盃 +超级链接 超級鏈接 +超群出众 超羣出衆 +超范围 超範圍 +超计划利润 超計劃利潤 +超赞 超讚 +超链接 超鏈接 +超额录取 超額錄取 +越冬 越冬 +越冬作物 越冬作物 +越出 越出 +越出界线 越出界線 +越发 越發 +越活越回去 越活越回去 +越王尝蓼 越王嘗蓼 +趋于 趨於 +趋于平稳 趨於平穩 +趋于稳定 趨於穩定 +趋前退后 趨前退後 +趋吉避凶 趨吉避凶 +趋同 趨同 +趋向 趨向 +趋舍 趨舍 +趋舍异路 趨舍異路 +趋舍指凑 趨舍指湊 +趋近于 趨近於 +趫才 趫才 +趱干 趲幹 +足不出户 足不出戶 +足不出门 足不出門 +足了十人 足了十人 +足于 足於 +足协杯 足協盃 +足坛 足壇 +足总杯 足總杯 +足食丰衣 足食豐衣 +趸售物价 躉售物價 +趸当 躉當 +趸批卖出 躉批賣出 +趿拉板儿 趿拉板兒 +跃出 躍出 +跃升 躍升 +跃升为 躍升爲 +跃跃欲试 躍躍欲試 +跋前疐后 跋前疐後 +跋前踬后 跋前躓後 +跋涉千里 跋涉千里 +跋胡疐尾 跋胡疐尾 +跌了 跌了 +跌了弹的斑鸠 跌了彈的斑鳩 +跌价 跌價 +跌停板 跌停板 +跌出 跌出 +跌回 跌回 +跌打药 跌打藥 +跌至谷底 跌至谷底 +跌荡 跌蕩 +跌荡不羁 跌蕩不羈 +跌荡放言 跌蕩放言 +跎纤 跎縴 +跑不了 跑不了 +跑了和尚跑不了寺 跑了和尚跑不了寺 +跑了和尚跑不了庙 跑了和尚跑不了廟 +跑出 跑出 +跑出去 跑出去 +跑出来 跑出來 +跑台子 跑檯子 +跑合 跑合 +跑回 跑回 +跑回去 跑回去 +跑回来 跑回來 +跑得了和尚跑不了庙 跑得了和尚跑不了廟 +跑表 跑表 +跗注 跗注 +跗面 跗面 +跛行症 跛行症 +跛鳖千里 跛鱉千里 +跟了 跟了 +跟他一斗 跟他一鬥 +跟出 跟出 +跟出去 跟出去 +跟出来 跟出來 +跟前跟后 跟前跟後 +跟头虫 跟頭蟲 +跟屁虫 跟屁蟲 +跟斗 跟斗 +跨了 跨了 +跨党 跨黨 +跨出 跨出 +跨出去 跨出去 +跨出来 跨出來 +跨向 跨向 +跨媒体合作 跨媒體合作 +跨平台 跨平臺 +跨步向前 跨步向前 +跨通道启动技术 跨通道啓動技術 +跨鹤西游 跨鶴西遊 +跪了 跪了 +跬步千里 跬步千里 +路克 路克 +路克索 路克索 +路志 路誌 +路怒症 路怒症 +路码表 路碼表 +路签 路籤 +路线斗争 路線鬥爭 +路透集团 路透集團 +路里 路里 +路面 路面 +路面等级 路面等級 +跳了 跳了 +跳井自尽 跳井自盡 +跳价 跳價 +跳伞台 跳傘臺 +跳出 跳出 +跳出去 跳出去 +跳出来 跳出來 +跳出火坑 跳出火坑 +跳出釜底进火坑 跳出釜底進火坑 +跳升 跳升 +跳只舞 跳隻舞 +跳台 跳臺 +跳台滑雪 跳臺滑雪 +跳回 跳回 +跳回去 跳回去 +跳回来 跳回來 +跳墙出去 跳牆出去 +跳板 跳板 +跳梁 跳梁 +跳梁小丑 跳樑小醜 +跳梁猖獗之小丑 跳樑猖獗之小醜 +跳楼自尽 跳樓自盡 +跳水台 跳水臺 +跳河自尽 跳河自盡 +跳海自尽 跳海自盡 +跳脱出 跳脫出 +跳脱出来 跳脫出來 +跳荡 跳蕩 +跳表 跳錶 +跳针 跳針 +践极 踐極 +跷了 蹺了 +跷出 蹺出 +跷跷板 蹺蹺板 +跻升 躋升 +跻身于 躋身於 +踅回 踅回 +踅门了户 踅門瞭戶 +踊跃发言 踊躍發言 +踌躇满志 躊躇滿志 +踏了 踏了 +踏出 踏出 +踏出去 踏出去 +踏出来 踏出來 +踏小板凳儿糊险道神 踏小板凳兒糊險道神 +踏板 踏板 +踏板车 踏板車 +踏脚板 踏腳板 +踏足板 踏足板 +踔厉风发 踔厲風發 +踢不出 踢不出 +踢了 踢了 +踢出 踢出 +踢出去 踢出去 +踢出来 踢出來 +踢到铁板 踢到鐵板 +踢向 踢向 +踢回 踢回 +踢回去 踢回去 +踢回来 踢回來 +踩坏了 踩壞了 +踪迹 蹤跡 +踬仆 躓仆 +蹀里蹀斜 蹀里蹀斜 +蹇修 蹇修 +蹈厉之志 蹈厲之志 +蹈厉奋发 蹈厲奮發 +蹦出 蹦出 +蹦出来 蹦出來 +蹪于 蹪於 +蹭棱子 蹭棱子 +蹲了 蹲了 +蹲板 蹲板 +躁狂抑郁症 躁狂抑鬱症 +躁狂症 躁狂症 +躁郁 躁鬱 +躁郁症 躁鬱症 +躄出 躄出 +躏借 躪藉 +躐胡 躐胡 +身于 身於 +身价 身價 +身价百倍 身價百倍 +身价非凡 身價非凡 +身份识别卡 身份識別卡 +身体发肤 身體髮膚 +身做身当 身做身當 +身历其境 身歷其境 +身历声 身歷聲 +身后 身後 +身后之事 身後之事 +身后事 身後事 +身后萧条 身後蕭條 +身当其境 身當其境 +身当矢石 身當矢石 +身才 身才 +身敎重于言敎 身教重於言教 +身教胜于言教 身教勝於言教 +身有同感 身有同感 +身板 身板 +身板儿 身板兒 +身正为范 身正爲範 +身系囹圄 身繫囹圄 +躬先表率 躬先表率 +躬擐甲胄 躬擐甲冑 +躯干 軀幹 +躯干骨 軀幹骨 +躲一棒槌挨一榔头 躲一棒槌挨一榔頭 +躲不了 躲不了 +躲了雷公遇了霹雳 躲了雷公遇了霹靂 +躲回 躲回 +躲得不知去向 躲得不知去向 +躲清闲 躲清閒 +躲过了风暴又遇了雨 躲過了風暴又遇了雨 +躺个 躺個 +躺出 躺出 +躺出去 躺出去 +躺出来 躺出來 +躺回 躺回 +軟肥皂 软肥皂 +车仔面 車仔麪 +车到没恶路 車到沒惡路 +车厂 車廠 +车厂子 車廠子 +车叶草 車葉草 +车同轨书同文 車同軌書同文 +车后箱 車後箱 +车坛 車壇 +车夫 車伕 +车库里 車庫裏 +车攻马同 車攻馬同 +车斗 車斗 +车无退表 車無退表 +车百合 車百合 +车种 車種 +车站里 車站裏 +车系 車系 +车载斗量 車載斗量 +车里 車裏 +车里雅宾斯克 車里雅賓斯克 +车马夫 車馬伕 +轧制 軋製 +轧钢厂 軋鋼廠 +轨制 軌制 +轨范 軌範 +轨道面 軌道面 +轩辟 軒闢 +转个 轉個 +转个弯 轉個彎 +转了 轉了 +转借 轉借 +转关系 轉關係 +转出 轉出 +转出去 轉出去 +转出来 轉出來 +转动曲柄 轉動曲柄 +转发 轉發 +转台 轉檯 +转向 轉向 +转向信号 轉向信號 +转向器 轉向器 +转向往 轉向往 +转向架 轉向架 +转向灯 轉向燈 +转向离合 轉向離合 +转回 轉回 +转回去 轉回去 +转回来 轉回來 +转圜余地 轉圜餘地 +转干 轉幹 +转录 轉錄 +转念 轉念 +转意回心 轉意回心 +转战千里 轉戰千里 +转托 轉託 +转折 轉折 +转折亲 轉折親 +转折点 轉折點 +转换方向 轉換方向 +转播台 轉播臺 +转斗千里 轉鬥千里 +转日回天 轉日回天 +转汇 轉匯 +转注 轉註 +转注字 轉註字 +转游 轉游 +转系 轉系 +转胜为败 轉勝爲敗 +转败为胜 轉敗爲勝 +转车台 轉車臺 +转速表 轉速錶 +转面无情 轉面無情 +轮休制 輪休制 +轮作制度 輪作制度 +轮值表 輪值表 +轮发 輪發 +轮台 輪臺 +轮台县 輪臺縣 +轮台古城 輪臺古城 +轮唱曲 輪唱曲 +轮回 輪迴 +轮奸 輪姦 +轮旋曲 輪旋曲 +轮流制 輪流制 +轮流干 輪流幹 +轮班制 輪班制 +轮生叶 輪生葉 +轮种 輪種 +轮种法 輪種法 +轮系 輪系 +轮虫 輪蟲 +软不叮当 軟不叮噹 +软件平台 軟件平臺 +软件开发 軟件開發 +软件开发人员 軟件開發人員 +软件技术 軟件技術 +软件系统 軟件系統 +软体出版协会 軟體出版協會 +软困 軟困 +软软松松 軟軟鬆鬆 +软面筋 軟麪筋 +轰了 轟了 +轰出 轟出 +轰出去 轟出去 +轰出来 轟出來 +轴承合金 軸承合金 +轴根系 軸根系 +轴系 軸系 +轴舻千里 軸艫千里 +轴面 軸面 +轶荡 軼蕩 +轸念 軫念 +轻于 輕於 +轻于鸿毛 輕於鴻毛 +轻叹 輕嘆 +轻度台风 輕度颱風 +轻扣 輕釦 +轻松 輕鬆 +轻松地 輕鬆地 +轻松愉快 輕鬆愉快 +轻松自在 輕鬆自在 +轻松自如 輕鬆自如 +轻核 輕核 +轻油裂解工厂 輕油裂解工廠 +轻烟 輕煙 +轻蔑 輕蔑 +轻蔑性 輕蔑性 +轻袅袅 輕嫋嫋 +轻轻松松 輕輕鬆鬆 +载于 載於 +载出 載出 +载出去 載出去 +载出来 載出來 +载浮载沈 載浮載沈 +载满了 載滿了 +载舟复舟 載舟覆舟 +轿夫 轎伕 +辁才 輇才 +较于 較於 +较胜一筹 較勝一籌 +辅导团 輔導團 +辅系 輔系 +辅赞 輔贊 +辈出 輩出 +辈子干 輩子幹 +辉格党 輝格黨 +辉格党人 輝格黨人 +辉绿岩 輝綠岩 +辉长岩 輝長岩 +辍耕录 輟耕錄 +辐射警告标志 輻射警告標志 +辑录 輯錄 +辑录出来 輯錄出來 +输了 輸了 +输入系统 輸入系統 +输入许可制 輸入許可制 +输出 輸出 +输出业 輸出業 +输出入 輸出入 +输出到 輸出到 +输出功率 輸出功率 +输出区 輸出區 +输出去 輸出去 +输出变压 輸出變壓 +输出品 輸出品 +输出国 輸出國 +输出来 輸出來 +输出管 輸出管 +输出表 輸出表 +输出量 輸出量 +输出阻抗 輸出阻抗 +输征 輸征 +辖制 轄制 +辗米厂 輾米廠 +辛丑 辛丑 +辛丑和约 辛丑和約 +辛丑条约 辛丑條約 +辛克 辛克 +辛辣面 辛辣麪 +辛里希 辛里希 +辜濂松 辜濂松 +辞不获命 辭不獲命 +辞丰意雄 辭豐意雄 +辞修 辭修 +辞别 辭別 +辞汇 辭彙 +辞致 辭致 +辞采 辭采 +辟世 辟世 +辟为 闢爲 +辟举 辟舉 +辟书 辟書 +辟人之士 辟人之士 +辟佛 闢佛 +辟作 闢作 +辟出 闢出 +辟划 闢劃 +辟匿 辟匿 +辟历施鞭 辟歷施鞭 +辟召 辟召 +辟君三舍 辟君三舍 +辟命 辟命 +辟咡 辟咡 +辟土 闢土 +辟地 闢地 +辟地开天 闢地開天 +辟室 闢室 +辟廱 辟廱 +辟建 闢建 +辟引 辟引 +辟恶 辟惡 +辟恶除患 辟惡除患 +辟支佛 辟支佛 +辟易 辟易 +辟淫 辟淫 +辟然 辟然 +辟爲 闢爲 +辟田 闢田 +辟筑 闢築 +辟纑 辟纑 +辟色 辟色 +辟芷 辟芷 +辟言 辟言 +辟设 闢設 +辟谣 闢謠 +辟谷 辟穀 +辟谷绝粒 辟穀絕粒 +辟辟 闢辟 +辟违 辟違 +辟逻 辟邏 +辟邪 辟邪 +辟邪以律 闢邪以律 +辟雍 辟雍 +辟雍砚 辟雍硯 +辣哈布 辣哈布 +辣椒面 辣椒麪 +辣胡椒 辣胡椒 +辨出 辨出 +辨别 辨別 +辨别出来 辨別出來 +辨别力 辨別力 +辨别是非 辨別是非 +辨向 辨向 +辨复 辨復 +辨奸论 辨姦論 +辨志 辨志 +辨折 辨折 +辨认出 辨認出 +辨认出来 辨認出來 +辨识出 辨識出 +辩别 辯別 +辩才 辯才 +辩才天 辯才天 +辩才无碍 辯才無礙 +辩斗 辯鬥 +辩论术 辯論術 +辫发 辮髮 +辫穗头 辮穗頭 +辱游 辱游 +边修 邊修 +边境冲突 邊境衝突 +边幅不修 邊幅不修 +边核 邊核 +辽三彩 遼三彩 +辽太后 遼太后 +辽宁师范大学 遼寧師範大學 +辽沈 遼瀋 +达克 達克 +达克龙 達克龍 +达尔罕茂明安联合旗 達爾罕茂明安聯合旗 +达布洛沃斯基 達布洛沃斯基 +达欣杯 達欣盃 +达沃斯论坛 達沃斯論壇 +达瓦党 達瓦黨 +达致 達致 +迁了 遷了 +迁于 遷於 +迁出 遷出 +迁出去 遷出去 +迁出来 遷出來 +迁升 遷升 +迁厂 遷廠 +迁台 遷臺 +迁回 遷回 +迁回去 遷回去 +迁回来 遷回來 +迁就于 遷就於 +迁怒于 遷怒於 +迁怒于人 遷怒於人 +迁思回虑 遷思迴慮 +迂回 迂迴 +迂回奔袭 迂回奔襲 +迂回战术 迂迴戰術 +迂回曲折 迂迴曲折 +迂徊战术 迂徊戰術 +迂曲 迂曲 +迂迂曲曲 迂迂曲曲 +迅即出发 迅即出發 +迅速发展 迅速發展 +过不了 過不了 +过世面 過世面 +过了 過了 +过了这个村儿没这个店儿 過了這個村兒沒這個店兒 +过了这村没这店 過了這村沒這店 +过了青春无少年 過了青春無少年 +过于 過於 +过云雨 過雲雨 +过人才略 過人才略 +过傢伙 過傢伙 +过冬 過冬 +过冲 過沖 +过几 過幾 +过出 過出 +过动症 過動症 +过历 過歷 +过去了 過去了 +过后 過後 +过境签证 過境簽證 +过失致死 過失致死 +过干瘾 過乾癮 +过庭录 過庭錄 +过当 過當 +过当防卫 過當防衛 +过录 過錄 +过得了 過得了 +过敏性休克 過敏性休克 +过敏症 過敏症 +过杆 過杆 +过松 過鬆 +过桥抽板 過橋抽板 +过梁 過樑 +过水面 過水麪 +过河抽板 過河抽板 +过滤出来 過濾出來 +过滤嘴香烟 過濾嘴香菸 +过眼云烟 過眼雲煙 +过眼烟云 過眼煙雲 +过耳秋风 過耳秋風 +过蒙 過蒙 +迈了 邁了 +迈克 邁克 +迈克尔 邁克爾 +迈克尔克莱顿 邁克爾克萊頓 +迈克尔杰克逊 邁克爾傑克遜 +迈出 邁出 +迈向 邁向 +迈科里 邁科里 +迎出 迎出 +迎合 迎合 +迎合人心 迎合人心 +迎向 迎向 +迎向前去 迎向前去 +迎回 迎回 +迎奸卖俏 迎奸賣俏 +迎宾曲 迎賓曲 +迎斗灯 迎斗燈 +迎春曲 迎春曲 +迎面 迎面 +迎面而来 迎面而來 +运出 運出 +运出去 運出去 +运出来 運出來 +运出运费 運出運費 +运动党 運動黨 +运回 運回 +运回去 運回去 +运回来 運回來 +运用之妙在于一心 運用之妙在於一心 +运用于 運用於 +运筹决胜 運籌決勝 +运筹千里 運籌千里 +运输系统 運輸系統 +运输设备修护费 運輸設備修護費 +近乎同步 近乎同步 +近乎同步数位阶层 近乎同步數位階層 +近了 近了 +近于 近於 +近似于 近似於 +近几年 近幾年 +近几年来 近幾年來 +近思录 近思錄 +近日無仇 近日無讎 +近日里 近日裏 +近朱者赤 近朱者赤 +近朱者赤近墨者黑 近朱者赤近墨者黑 +近朱近墨 近朱近墨 +近水楼台 近水樓臺 +近视眼生了瞎子 近視眼生了瞎子 +返台 返臺 +返吟复吟 返吟復吟 +返回 返回 +返复 返復 +返朴 返樸 +返照回光 返照回光 +返还占有 返還占有 +返里 返里 +返魂乏术 返魂乏術 +还不出 還不出 +还乡团 還鄉團 +还了 還了 +还了得 還了得 +还于 還於 +还价 還價 +还修 還修 +还冲 還衝 +还出 還出 +还出去 還出去 +还出来 還出來 +还占 還佔 +还原熔炼 還原熔煉 +还发 還發 +还向 還向 +还回 還回 +还愿 還願 +还愿意 還願意 +还政于民 還政於民 +还淳反朴 還淳反樸 +还淳返朴 還淳返樸 +还珠合浦 還珠合浦 +还辟 還辟 +还采 還採 +这一个 這一個 +这个 這個 +这个当儿 這個當兒 +这个挡口 這個擋口 +这个月 這個月 +这个那个 這個那個 +这么 這麼 +这么个 這麼個 +这么干 這麼幹 +这么着 這麼着 +这些个 這些個 +这伙人 這夥人 +这几个 這幾個 +这几个人 這幾個人 +这几个月 這幾個月 +这几天 這幾天 +这几次 這幾次 +这出剧 這齣劇 +这出好戏 這齣好戲 +这出电影 這齣電影 +这只 這隻 +这只不 這只不 +这只是 這只是 +这回 這回 +这回事 這回事 +这当儿 這當兒 +这方面 這方面 +这杯 這杯 +这注 這注 +这种 這種 +这种事 這種事 +这种人 這種人 +这种方式 這種方式 +这般干法 這般幹法 +这还了得 這還了得 +这里 這裏 +这里会 這裏會 +这里在 這裏在 +这里是 這裏是 +这里有 這裏有 +这里能 這裏能 +这钟 這鐘 +进一步规范 進一步規範 +进不了 進不了 +进了 進了 +进了天堂 進了天堂 +进价 進價 +进修 進修 +进修班 進修班 +进出 進出 +进出口 進出口 +进出口商 進出口商 +进出境 進出境 +进占 進佔 +进厂 進廠 +进发 進發 +进口加签权 進口加簽權 +进士出身 進士出身 +进幸 進幸 +进度表 進度表 +进得了 進得了 +进德修业 進德修業 +进步伙伴展 進步夥伴展 +进步党 進步黨 +进行曲 進行曲 +进货价 進貨價 +进货让价 進貨讓價 +进货退出 進貨退出 +进货退回 進貨退回 +进进出出 進進出出 +进退失据 進退失據 +进退无据 進退無據 +进退维谷 進退維谷 +进香团 進香團 +远东技术学院 遠東技術學院 +远于 遠於 +远亲近戚 遠親近戚 +远别 遠別 +远县才至 遠縣纔至 +远处夸称近方卖弄 遠處誇稱近方賣弄 +远引曲喻 遠引曲喻 +远征 遠征 +远征军 遠征軍 +远志 遠志 +远恶 遠惡 +远房亲戚 遠房親戚 +远打周折 遠打週折 +远期外汇 遠期外匯 +远水救不了近火 遠水救不了近火 +远游 遠遊 +远程登录 遠程登錄 +远端监控系统 遠端監控系統 +远端签入 遠端簽入 +远胄 遠胄 +远胜 遠勝 +远距图书服务系统 遠距圖書服務系統 +远远落后 遠遠落後 +远隔千里 遠隔千里 +违禁药品 違禁藥品 +违章建筑 違章建築 +违纪参选 違紀參選 +连三并四 連三併四 +连个 連個 +连了 連了 +连于 連於 +连云 連雲 +连云区 連雲區 +连云叠嶂 連雲疊嶂 +连云港 連雲港 +连云港市 連雲港市 +连出 連出 +连升 連升 +连占 連佔 +连卷 連卷 +连发 連發 +连台 連臺 +连台好戏 連臺好戲 +连台本戏 連臺本戲 +连合 連合 +连同 連同 +连哄带骗 連哄帶騙 +连回 連回 +连城之价 連城之價 +连带关系 連帶關係 +连年丰收 連年豐收 +连庄 連莊 +连战连胜 連戰連勝 +连杆 連桿 +连杆机构 連桿機構 +连枝分叶 連枝分葉 +连枝带叶 連枝帶葉 +连根拔出 連根拔出 +连珠合璧 連珠合璧 +连登云路 連登雲路 +连种 連種 +连系 連繫 +连系词 連繫詞 +连系起来 連繫起來 +连胜 連勝 +连胜文 連勝文 +连采 連採 +连镳并轸 連鑣並軫 +连须胡子 連鬚鬍子 +连鬓胡子 連鬢鬍子 +连鬼也不见一个 連鬼也不見一個 +迟了 遲了 +迟发性损伤 遲發性損傷 +迟回 遲迴 +迟回观望 遲回觀望 +迢迢千里 迢迢千里 +迥不相同 迥不相同 +迥乎不同 迥乎不同 +迥别 迥別 +迥然不同 迥然不同 +迥然回异 迥然迴異 +迥然有别 迥然有別 +迦叶 迦葉 +迦叶佛 迦葉佛 +迦叶尊者 迦葉尊者 +迪克 迪克 +迪克斯 迪克斯 +迪化厂 迪化廠 +迪吉苏斯 迪吉蘇斯 +迪拉萨布里 迪拉薩布里 +迪雅巴克 迪雅巴克 +迪黑苏斯 迪黑蘇斯 +迫于 迫於 +迫于形势 迫於形勢 +迫于眉睫 迫於眉睫 +迭出迭入 迭出迭入 +迭有发现 迭有發現 +迭有斩获 迭有斬獲 +迭见杂出 迭見雜出 +迷了 迷了 +迷了心窍 迷了心竅 +迷于 迷於 +迷团 迷團 +迷奸 迷姦 +迷幻药 迷幻藥 +迷彩 迷彩 +迷彩服 迷彩服 +迷彩裤 迷彩褲 +迷暗 迷暗 +迷胡 迷胡 +迷药 迷藥 +迷蒙 迷濛 +迷迷蒙蒙 迷迷濛濛 +迷魂药 迷魂藥 +迸出 迸出 +迸发 迸發 +迸发出 迸發出 +迹蹈 蹟蹈 +追凶 追兇 +追出 追出 +追向 追向 +追回 追回 +追念 追念 +追思弥撒 追思彌撒 +追査出 追查出 +追求幸福 追求幸福 +退伍军人症 退伍軍人症 +退伙 退夥 +退党 退黨 +退出 退出 +退出运行 退出運行 +退前缩后 退前縮後 +退后 退後 +退向 退向 +退回 退回 +退团 退團 +退居幕后 退居幕後 +退徙三舍 退徙三舍 +退有后言 退有後言 +退烧药 退燒藥 +退藏于密 退藏於密 +退避三舍 退避三舍 +退针 退針 +送个 送個 +送了 送了 +送修 送修 +送出 送出 +送别 送別 +送君千里 送君千里 +送回 送回 +送客出门 送客出門 +送客台 送客臺 +送报夫 送報伕 +送秋波 送秋波 +适不适合 適不適合 +适中 適中 +适中下怀 適中下懷 +适之 適之 +适于 適於 +适人 適人 +适从 適從 +适任 適任 +适值 適值 +适切 適切 +适口 適口 +适可而止 適可而止 +适合 適合 +适合于 適合於 +适合者 適合者 +适婚 適婚 +适婚年龄 適婚年齡 +适婚期 適婚期 +适婚者 適婚者 +适孙 適孫 +适宜 適宜 +适应 適應 +适应力 適應力 +适应性 適應性 +适应期 適應期 +适应环境 適應環境 +适应症 適應症 +适度 適度 +适度微调 適度微調 +适当 適當 +适当地 適當地 +适当性 適當性 +适当的 適當的 +适当范围 適當範圍 +适得其反 適得其反 +适得其所 適得其所 +适性 適性 +适意 適意 +适才 適才 +适时 適時 +适时地 適時地 +适材适用 適材適用 +适来 適來 +适法性 適法性 +适然 適然 +适用 適用 +适用于 適用於 +适用性 適用性 +适用者 適用者 +适用范围 適用範圍 +适者 適者 +适者生存 適者生存 +适航性 適航性 +适航指令 適航指令 +适逢 適逢 +适逢其会 適逢其會 +适配器 適配器 +适配层 適配層 +适量 適量 +适销 適銷 +适销对路 適銷對路 +适间 適間 +适闷 適悶 +适龄 適齡 +逃不了 逃不了 +逃不出 逃不出 +逃不出手掌心 逃不出手掌心 +逃了 逃了 +逃债台 逃債臺 +逃出 逃出 +逃出去 逃出去 +逃出来 逃出來 +逃出生天 逃出生天 +逃出虎口 逃出虎口 +逃回 逃回 +逃脱出 逃脫出 +逆价差 逆價差 +逆党 逆黨 +逆入平出 逆入平出 +逆向 逆向 +逆向公车 逆向公車 +逆向思考 逆向思考 +逆向拥塞通知 逆向擁塞通知 +逆向整合 逆向整合 +逆向行驶 逆向行駛 +逆折 逆折 +逆时针 逆時針 +逆转录 逆轉錄 +逆转录病毒 逆轉錄病毒 +逆转录酶 逆轉錄酶 +逆钟 逆鐘 +逆钟向 逆鐘向 +选个 選個 +选举人团 選舉人團 +选举制 選舉制 +选举制度 選舉制度 +选修 選修 +选修科 選修科 +选修科目 選修科目 +选修课 選修課 +选出 選出 +选出来 選出來 +选后 選後 +选录 選錄 +选手表决 選手表決 +选手表明 選手表明 +选手表现 選手表現 +选手表示 選手表示 +选手表达 選手表達 +选才 選才 +选拔出 選拔出 +选拔干部 選拔幹部 +选择范围 選擇範圍 +选曲 選曲 +选校不选系 選校不選系 +选民代表 選民代表 +选民参加率 選民參加率 +选派代表 選派代表 +选种 選種 +选系 選系 +选系不选校 選系不選校 +选美皇后 選美皇后 +逊于 遜於 +逊克 遜克 +逊克县 遜克縣 +逊志时敏 遜志時敏 +逋发 逋髮 +逋荡 逋蕩 +逍遥游 逍遙遊 +透了 透了 +透出 透出 +透出去 透出去 +透出来 透出來 +透漏出 透漏出 +透辟 透闢 +透露出 透露出 +逐个 逐個 +逐出 逐出 +逐出门墙 逐出門牆 +逐末舍本 逐末捨本 +逐步升级 逐步升級 +递了 遞了 +递出 遞出 +递出去 遞出去 +递出来 遞出來 +递升 遞升 +递向 遞向 +递回 遞迴 +递回去 遞回去 +递回来 遞回來 +递推关系 遞推關係 +递解出境 遞解出境 +逗人发笑 逗人發笑 +通了 通了 +通事舍人 通事舍人 +通于 通於 +通人达才 通人達才 +通便药 通便藥 +通信技术 通信技術 +通信系统 通信系統 +通党库 通黨庫 +通力合作 通力合作 +通历 通曆 +通合一气 通合一氣 +通同 通同 +通同一气 通同一氣 +通向 通向 +通奸 通姦 +通奸罪 通姦罪 +通布图 通佈圖 +通庄 通莊 +通心面 通心麪 +通志 通志 +通志馆 通志館 +通才 通才 +通才教育 通才教育 +通才练识 通才練識 +通汇 通匯 +通用汉字标准交换码 通用漢字標準交換碼 +通用资源识别号 通用資源識別號 +通盘计划 通盤計劃 +通联记录 通聯記錄 +通讯录 通訊錄 +通讯系统 通訊系統 +通过事后 通過事後 +通鉴 通鑑 +逛了 逛了 +逛出 逛出 +逛出去 逛出去 +逛出来 逛出來 +逛回 逛回 +逛回去 逛回去 +逛回来 逛回來 +逛荡 逛蕩 +逞凶 逞兇 +逞凶斗狠 逞兇鬥狠 +逞凶鬥狠 逞兇鬥狠 +逞异夸能 逞異誇能 +逞志 逞志 +逞恶 逞惡 +逞欲 逞欲 +速克达 速克達 +速去速回 速去速回 +速回 速回 +速食面 速食麪 +造价 造價 +造出 造出 +造出来 造出來 +造型艺术 造型藝術 +造天立极 造天立極 +造就人才 造就人才 +造就出 造就出 +造岩矿物 造岩礦物 +造币厂 造幣廠 +造形艺术 造形藝術 +造曲 造麴 +造极登峰 造極登峯 +造福万民 造福萬民 +造纸厂 造紙廠 +造纸术 造紙術 +造船厂 造船廠 +造船系 造船系 +造血干 造血幹 +造血干细胞 造血幹細胞 +造钟 造鐘 +造钟表 造鐘錶 +逢人只说三分话 逢人只說三分話 +逢低卖出 逢低賣出 +逢凶化吉 逢凶化吉 +逢君之恶 逢君之惡 +逢蒙 逢蒙 +逢高卖出 逢高賣出 +連亘 連亙 +逮系 逮繫 +逮获 逮獲 +逸出 逸出 +逸出功 逸出功 +逸周书 逸周書 +逸才 逸才 +逸欲 逸欲 +逸游自恣 逸游自恣 +逸群之才 逸羣之才 +逸致 逸緻 +逻辑链路控制 邏輯鏈路控制 +逼上梁山 逼上梁山 +逼下台 逼下臺 +逼了 逼了 +逼出 逼出 +逼出去 逼出去 +逼出来 逼出來 +逼向 逼向 +逼回 逼回 +逼回去 逼回去 +逼回来 逼回來 +逼并 逼併 +逾出 逾出 +逾闲荡检 逾閑蕩檢 +遂其所愿 遂其所願 +遂愿 遂願 +遄征 遄征 +遇合 遇合 +遇风后 遇風後 +遍于 遍於 +遍历 遍歷 +遍布 遍佈 +遏云 遏雲 +遏云社 遏雲社 +遏制 遏制 +遏恶扬善 遏惡揚善 +遐布 遐布 +遐志 遐志 +遐胄 遐胄 +道不了 道不了 +道不出 道不出 +道不同不相为谋 道不同不相爲謀 +道克鲁 道克魯 +道出 道出 +道别 道別 +道合志同 道合志同 +道同志合 道同志合 +道听涂说 道聽塗說 +道尔顿制 道爾頓制 +道尽 道盡 +道尽涂殚 道盡塗殫 +道尽途穷 道盡途窮 +道德发展 道德發展 +道德困境 道德困境 +道德规范 道德規範 +道曲 道曲 +道术 道術 +道格拉斯麦克阿瑟 道格拉斯麥克阿瑟 +道种智 道種智 +道范 道範 +道范长昭 道範長昭 +道里 道里 +道里区 道里區 +遗传钟 遺傳鐘 +遗体告别式 遺體告別式 +遗害万年 遺害萬年 +遗志 遺志 +遗忘症 遺忘症 +遗念 遺念 +遗恨千古 遺恨千古 +遗愿 遺願 +遗才 遺才 +遗胄 遺胄 +遗臭万代 遺臭萬代 +遗臭万年 遺臭萬年 +遗臭万载 遺臭萬載 +遗臭千年 遺臭千年 +遗范 遺範 +遗葑菲采 遺葑菲采 +遗表 遺表 +遗迹 遺蹟 +遗风余 遺風餘 +遣发 遣發 +遣回 遣回 +遣送出境 遣送出境 +遣送回 遣送回 +遣送回国 遣送回國 +遥地里 遙地裏 +遥念 遙念 +遥测技术 遙測技術 +遥胄 遙胄 +遥遥华胄 遙遙華胄 +遨游 遨遊 +遨游四海 遨遊四海 +遨游天下 遨遊天下 +遭人暗算 遭人暗算 +遭致 遭致 +遮不了 遮不了 +遮丑 遮醜 +遮光板 遮光板 +遮前掩后 遮前掩後 +遮场面 遮場面 +遮复 遮覆 +遮头盖面 遮頭蓋面 +遮护板 遮護板 +遮挽 遮挽 +遮羞布 遮羞布 +遮道挽留 遮道挽留 +遮阳板 遮陽板 +遮面 遮面 +遴选出 遴選出 +遴选出来 遴選出來 +避不见面 避不見面 +避了 避了 +避凶就吉 避凶就吉 +避凶趋吉 避凶趨吉 +避向 避向 +避孕药 避孕藥 +避恶 避惡 +避暑胜地 避暑勝地 +避雷针 避雷針 +避风台 避風臺 +邀天之幸 邀天之幸 +邅回 邅回 +邋里邋遢 邋里邋遢 +邑里 邑里 +邓布利多 鄧布利多 +邢台 邢臺 +邢台县 邢臺縣 +邢台地区 邢臺地區 +邢台市 邢臺市 +那个 那個 +那个人 那個人 +那个猫儿不吃腥 那個貓兒不吃腥 +那个耗子不偷油 那個耗子不偷油 +那么 那麼 +那么干 那麼幹 +那么着 那麼着 +那伙人 那夥人 +那借 那借 +那几 那幾 +那几个 那幾個 +那几天 那幾天 +那几次 那幾次 +那出剧 那齣劇 +那出好戏 那齣好戲 +那出电影 那齣電影 +那卷 那捲 +那只 那隻 +那只是 那只是 +那只有 那只有 +那台 那臺 +那回 那回 +那当儿 那當兒 +那斯达克 那斯達克 +那方面 那方面 +那曲 那曲 +那曲县 那曲縣 +那曲地区 那曲地區 +那曲市 那曲市 +那杯 那杯 +那种 那種 +那种人 那種人 +那维克 那維克 +那里 那裏 +邪不干正 邪不干正 +邪不胜正 邪不勝正 +邪念 邪念 +邪恶 邪惡 +邪恶轴心 邪惡軸心 +邪曲 邪曲 +邪术 邪術 +邪辟 邪辟 +邮政储金转存制度 郵政儲金轉存制度 +邮政划拨 郵政劃撥 +邮政汇票 郵政匯票 +邮汇 郵匯 +邮汇局 郵匯局 +邮购价格 郵購價格 +邮购目录 郵購目錄 +邱富郁 邱富郁 +邱正杰 邱正傑 +邱胜琦 邱勝琦 +邱胜翊 邱勝翊 +邱郁婷 邱郁婷 +邵廷采 邵廷采 +邵杰 邵傑 +邵维杰 邵維傑 +邸舍 邸舍 +邻曲 鄰曲 +邻舍 鄰舍 +邻里 鄰里 +邻里乡党 鄰里鄉黨 +邻里长 鄰里長 +郁伊 鬱伊 +郁勃 鬱勃 +郁卒 鬱卒 +郁南 鬱南 +郁南县 鬱南縣 +郁哉 郁哉 +郁垒 鬱壘 +郁堙不偶 鬱堙不偶 +郁塞 鬱塞 +郁律 鬱律 +郁悒 鬱悒 +郁愤 鬱憤 +郁抑 鬱抑 +郁挹 鬱挹 +郁朴 郁樸 +郁李 郁李 +郁林 鬱林 +郁气 鬱氣 +郁江 鬱江 +郁沉沉 鬱沉沉 +郁泱 鬱泱 +郁火 鬱火 +郁烈 郁烈 +郁热 鬱熱 +郁燠 鬱燠 +郁症 鬱症 +郁离子 郁離子 +郁积 鬱積 +郁穆 郁穆 +郁纡 鬱紆 +郁结 鬱結 +郁蒸 鬱蒸 +郁蓊 鬱蓊 +郁血 鬱血 +郁达夫 郁達夫 +郁邑 鬱邑 +郁郁 鬱郁 +郁郁不乐 鬱鬱不樂 +郁郁不平 鬱鬱不平 +郁郁寡欢 鬱鬱寡歡 +郁郁而终 鬱鬱而終 +郁郁菲菲 郁郁菲菲 +郁郁葱葱 鬱鬱蔥蔥 +郁郁青青 郁郁青青 +郁金 鬱金 +郁金香 鬱金香 +郁闭 鬱閉 +郁闭度 鬱閉度 +郁闷 鬱悶 +郁闷不乐 鬱悶不樂 +郁陶 鬱陶 +郁馥 郁馥 +郁黑 鬱黑 +郇山隐修会 郇山隱修會 +郊坛 郊壇 +郊坛下窑 郊壇下窯 +郊游 郊遊 +郎个 郎個 +郎之万 郎之萬 +郎当 郎當 +郎才女姿 郎才女姿 +郎才女貌 郎才女貌 +郎潜白发 郎潛白髮 +郑丞杰 鄭丞傑 +郑丰喜 鄭豐喜 +郑余豪 鄭余豪 +郑俊杰 鄭俊傑 +郑克塽 鄭克塽 +郑克爽 鄭克爽 +郑凯云 鄭凱云 +郑卫之曲 鄭衛之曲 +郑家钟 鄭家鐘 +郑幸娟 鄭幸娟 +郑庄公 鄭莊公 +郑志龙 鄭志龍 +郑明修 鄭明修 +郑易里 鄭易里 +郑杰文 鄭傑文 +郑板桥 鄭板橋 +郑梦准 鄭夢準 +郑苹如 鄭蘋如 +郑荣松 鄭榮松 +郑重宣布 鄭重宣佈 +郘钟 郘鐘 +郡县制 郡縣制 +郡县制度 郡縣制度 +郡国制 郡國制 +部党 部黨 +部发 部發 +部曲 部曲 +部胡林 部胡林 +部落发 部落發 +部落同盟 部落同盟 +郭勇志 郭勇志 +郭台成 郭臺成 +郭台铭 郭臺銘 +郭子干 郭子乾 +郭志明 郭志明 +郭松焘 郭松燾 +郭泓志 郭泓志 +郭采洁 郭采潔 +都于 都於 +都俞吁咈 都俞吁咈 +都出 都出 +都出去 都出去 +都出来 都出來 +都卜勒 都卜勒 +都发 都發 +都发局 都發局 +都发局长 都發局長 +都受了 都受了 +都向 都向 +都回 都回 +都回去 都回去 +都回来 都回來 +都市计划 都市計劃 +都必须 都必須 +都念 都念 +都肯干 都肯幹 +都舍下 都捨下 +鄂托克 鄂托克 +鄂托克前旗 鄂托克前旗 +鄂托克旗 鄂托克旗 +鄂温克族 鄂溫克族 +鄂温克族自治旗 鄂溫克族自治旗 +鄂温克语 鄂溫克語 +鄂霍次克海 鄂霍次克海 +鄭凱云 鄭凱云 +酋长制度 酋長制度 +酌古御今 酌古御今 +配了 配了 +配享千秋 配享千秋 +配件挂勾 配件掛勾 +配位化合物 配位化合物 +配出 配出 +配出来 配出來 +配制 配製 +配制饲料 配制飼料 +配发 配發 +配合 配合 +配合上 配合上 +配合度 配合度 +配合款 配合款 +配合着 配合着 +配合禁忌 配合禁忌 +配合起来 配合起來 +配合题 配合題 +配水干管 配水幹管 +配水系统 配水系統 +配种 配種 +配种季节 配種季節 +配给制度 配給制度 +配膳台 配膳檯 +配药 配藥 +配药师 配藥師 +配送范围 配送範圍 +酒入舌出 酒入舌出 +酒厂 酒廠 +酒后 酒後 +酒后吐真言 酒後吐真言 +酒后失态 酒後失態 +酒后失言 酒後失言 +酒后驾车 酒後駕車 +酒后驾驶 酒後駕駛 +酒困 酒困 +酒坛 酒罈 +酒娘子 酒娘子 +酒已干 酒已乾 +酒帘 酒帘 +酒帘子 酒帘子 +酒干了 酒乾了 +酒干尽 酒乾盡 +酒干掉 酒乾掉 +酒恶 酒惡 +酒曲 酒麴 +酒有别肠 酒有別腸 +酒杯 酒杯 +酒柜 酒櫃 +酒气冲天 酒氣沖天 +酒气熏人 酒氣熏人 +酒注子 酒注子 +酒游花 酒游花 +酒病酒药医 酒病酒藥醫 +酒肴 酒餚 +酒胡 酒胡 +酒药 酒藥 +酒逢知己千钟少 酒逢知己千鍾少 +酒逢知己千锺少话不投机半句多 酒逢知己千鍾少話不投機半句多 +酒醴曲蘖 酒醴麴櫱 +酒面 酒面 +酒食征逐 酒食徵逐 +酝借 醞藉 +酝酿出 醞釀出 +酣嬉淋漓 酣嬉淋漓 +酣畅淋漓 酣暢淋漓 +酥松 酥鬆 +酥松可口 酥鬆可口 +酥松油脂 酥鬆油脂 +酥签 酥簽 +酩子里 酩子裏 +酮体症 酮體症 +酷刑折磨 酷刑折磨 +酷毙了 酷斃了 +酸懒 痠懶 +酸疼 痠疼 +酸痛 痠痛 +酸软 痠軟 +酸酸咸咸 酸酸鹹鹹 +酸麻 痠麻 +酿出 釀出 +酿制 釀製 +酿造出来 釀造出來 +酿酒厂 釀酒廠 +醇朴 醇樸 +醇郁 醇郁 +醉于 醉於 +醉心于 醉心於 +醉熏熏 醉熏熏 +醋坛 醋罈 +醋坛子 醋罈子 +醋娘子要食杨梅 醋娘子要食楊梅 +醋栗 醋栗 +醋酸纤维 醋酸纖維 +醒吾技术学院 醒吾技術學院 +醒狮团 醒獅團 +醣厂 醣廠 +醮坛 醮壇 +醯鸡甕里 醯雞甕裏 +醲郁 醲郁 +采下 採下 +采下去 採下去 +采下来 採下來 +采为 採爲 +采买 採買 +采伐 採伐 +采住 採住 +采信 採信 +采光 採光 +采光剖璞 采光剖璞 +采兰赠芍 采蘭贈芍 +采兰赠药 採蘭贈藥 +采写 採寫 +采到 採到 +采制 採製 +采办 採辦 +采区 採區 +采去 採去 +采及葑菲 采及葑菲 +采取 採取 +采取措施 採取措施 +采取行动 採取行動 +采回 採回 +采回去 採回去 +采回来 採回來 +采在 採在 +采地 采地 +采场 採場 +采声 采聲 +采头 采頭 +采女 采女 +采好 採好 +采录 採錄 +采得 採得 +采择 採擇 +采拾 採拾 +采挖 採挖 +采掘 採掘 +采摘 採摘 +采摭 採摭 +采撷 採擷 +采收 採收 +采收期 採收期 +采收率 採收率 +采料 採料 +采景 採景 +采暖 採暖 +采果 採果 +采树种 採樹種 +采样 採樣 +采样法 採樣法 +采样率 採樣率 +采桑 採桑 +采棉机 採棉機 +采椽不斲 采椽不斲 +采樵人 採樵人 +采气 採氣 +采水 採水 +采油 採油 +采烈 采烈 +采煤 採煤 +采煤矿 採煤礦 +采爲 採爲 +采猎 採獵 +采珠 採珠 +采珠人 採珠人 +采生折割 採生折割 +采用 採用 +采用到 採用到 +采用率 採用率 +采的 採的 +采盐 採鹽 +采石 採石 +采石之役 采石之役 +采石之战 采石之戰 +采石之戰 采石之戰 +采石厂 採石廠 +采石场 採石場 +采矿 採礦 +采矿业 採礦業 +采矿场 採礦場 +采矿工 採礦工 +采矿工业 採礦工業 +采矿工程 採礦工程 +采矿方法 採礦方法 +采砂场 採砂場 +采种 採種 +采稿 採稿 +采空区 採空區 +采空采穗 採空採穗 +采精 採精 +采納 採納 +采纳 採納 +采给 採給 +采绿 采綠 +采缉 采緝 +采编 採編 +采脂 採脂 +采色 采色 +采芑 采芑 +采花 採花 +采花贼 採花賊 +采芹 采芹 +采芹人 採芹人 +采苓 采苓 +采茶 採茶 +采茶戏 採茶戲 +采茶歌 採茶歌 +采药 採藥 +采药人 採藥人 +采莲 採蓮 +采莲曲 採蓮曲 +采莲船 採蓮船 +采获 採獲 +采菊 採菊 +采菽 采菽 +采葛 采葛 +采薇 采薇 +采薪 採薪 +采薪之忧 采薪之憂 +采薪之疾 采薪之疾 +采蘩 采蘩 +采蜜 採蜜 +采血 採血 +采行 採行 +采衣 采衣 +采补 採補 +采认 採認 +采访 採訪 +采访使 採訪使 +采访员 採訪員 +采访团 採訪團 +采访新闻 採訪新聞 +采访权 採訪權 +采访编辑 採訪編輯 +采访记者 採訪記者 +采证 採證 +采诗 采詩 +采购 採購 +采购供应 採購供應 +采购员 採購員 +采购团 採購團 +采购案 採購案 +采购法 採購法 +采购站 採購站 +采购规则 採購規則 +采购量 採購量 +采过 採過 +采过去 採過去 +采过来 採過來 +采运 採運 +采选 採選 +采邑 采邑 +采采 采采 +采金 採金 +采铁 採鐵 +采集 採集 +采集到 採集到 +采集法 採集法 +采集起来 採集起來 +采风 採風 +采风录 采風錄 +采风问俗 採風問俗 +采食 採食 +釉下彩 釉下彩 +釉彩 釉彩 +釉药 釉藥 +释出 釋出 +释卷 釋卷 +释放出 釋放出 +释放出来 釋放出來 +释放出狱 釋放出獄 +释迦佛也恼下莲台 釋迦佛也惱下蓮臺 +里亚 里亞 +里人 里人 +里仁 里仁 +里仁为美 里仁爲美 +里克特 里克特 +里党 里黨 +里兹 里茲 +里出外进 裏出外進 +里加 里加 +里勾外连 裏勾外連 +里包恩 里包恩 +里名 里名 +里君 里君 +里咽 裏咽 +里士满 里士滿 +里外 裏外 +里奇蒙 里奇蒙 +里契蒙 里契蒙 +里奥 里奧 +里奥斯 里奧斯 +里奥格兰德 里奧格蘭德 +里子 裏子 +里实 裏實 +里宰 里宰 +里尔 里爾 +里尔队 里爾隊 +里尼 里尼 +里层 裏層 +里居 里居 +里屋 裏屋 +里巷 里巷 +里布 里布 +里希特霍芬 里希特霍芬 +里带 裏帶 +里应外合 裏應外合 +里廉 裏廉 +里弄 里弄 +里弗赛德 里弗賽德 +里弦 裏弦 +里急后重 裏急後重 +里手 裏手 +里扣 里扣 +里拉 里拉 +里斯 里斯 +里斯本 里斯本 +里昂 里昂 +里昂市 里昂市 +里昂队 里昂隊 +里根 里根 +里欧 里歐 +里欧斯 里歐斯 +里正 里正 +里氏 里氏 +里氏震级 里氏震級 +里民 里民 +里民大会 里民大會 +里海 裏海 +里港 里港 +里港乡 里港鄉 +里牵绵 裏牽綿 +里特维宁科 里特維寧科 +里瓦几亚条约 里瓦幾亞條約 +里瓦尔多 裏瓦爾多 +里社 里社 +里科 里科 +里程 里程 +里程碑 里程碑 +里程碑式 里程碑式 +里程表 里程錶 +里程计 里程計 +里约 里約 +里约热內卢 里約熱內盧 +里约热内卢 里約熱內盧 +里纳 里納 +里维拉 里維拉 +里美 里美 +里老 里老 +里耳 里耳 +里肌 里肌 +里胥 里胥 +里脊 裏脊 +里脊肉 裏脊肉 +里舍 里舍 +里蒙诺夫 里蒙諾夫 +里虚 裏虛 +里衣 裏衣 +里言 裏言 +里语 里語 +里谈巷议 里談巷議 +里谚 里諺 +里豪 里豪 +里贝利 里貝利 +里贾纳 里賈納 +里路 里路 +里踢 裏踢 +里边 裏邊 +里边儿 裏邊兒 +里进外出 裏進外出 +里通外国 裏通外國 +里通外敌 裏通外敵 +里邻长 里鄰長 +里里 裏裏 +里里外外 裏裏外外 +里长 里長 +里长伯 里長伯 +里门 里門 +里闬 里閈 +里间 裏間 +里闾 里閭 +里面 裏面 +里面儿 裏面兒 +重于 重於 +重于泰山 重於泰山 +重价 重價 +重估后 重估後 +重修 重修 +重修旧好 重修舊好 +重出 重出 +重划 重劃 +重划区 重劃區 +重划局 重劃局 +重制 重製 +重力摆 重力擺 +重历旧游 重歷舊遊 +重发 重發 +重台 重臺 +重合 重合 +重回 重回 +重复 重複 +重复使用 重複使用 +重复启动效应 重複啓動效應 +重复式 重複式 +重复本 重複本 +重复法 重複法 +重复节 重複節 +重复记录 重複記錄 +重复语境 重複語境 +重复课税 重複課稅 +重定向 重定向 +重岩叠嶂 重巖疊嶂 +重庆师范大学 重慶師範大學 +重当 重當 +重托 重託 +重扣 重扣 +重折 重摺 +重新装修 重新裝修 +重新评价 重新評價 +重涂 重塗 +重游 重遊 +重游旧地 重遊舊地 +重点采访 重點採訪 +重生爷娘 重生爺孃 +重症 重症 +重罗面 重羅麪 +重置价格 重置價格 +重获 重獲 +重见复出 重見複出 +重蹈复辙 重蹈覆轍 +重重困难 重重困難 +重锤 重錘 +野台 野臺 +野台戏 野臺戲 +野合 野合 +野外定向 野外定向 +野姜 野薑 +野姜花 野薑花 +野无遗才 野無遺才 +野游 野遊 +野种 野種 +野胡萝卜 野胡蘿蔔 +野草闲花 野草閒花 +野获编 野獲編 +野鹤闲云 野鶴閒雲 +量了 量了 +量入为出 量入爲出 +量出 量出 +量出制入 量出制入 +量回 量回 +量回去 量回去 +量回来 量回來 +量才录用 量才錄用 +量材录用 量材錄用 +量杯 量杯 +量表 量表 +量雨表 量雨表 +金万福 金萬福 +金乌西坠玉兔东升 金烏西墜玉兔東昇 +金云母 金雲母 +金仆姑 金僕姑 +金仑溪 金崙溪 +金价 金價 +金伯利岩 金伯利岩 +金光党 金光黨 +金创药 金創藥 +金升圭 金昇圭 +金升渊 金升淵 +金印如斗 金印如斗 +金发 金髮 +金发女郎 金髮女郎 +金发碧眼 金髮碧眼 +金台 金臺 +金台区 金臺區 +金叶 金葉 +金合欢 金合歡 +金周成 金周成 +金坛 金壇 +金坛市 金壇市 +金基范 金基範 +金头银面 金頭銀面 +金属制 金屬製 +金属杆 金屬桿 +金属板 金屬板 +金布道 金布道 +金斗 金斗 +金曲 金曲 +金曲奖 金曲獎 +金本位制 金本位制 +金杯 金盃 金杯 +金枝玉叶 金枝玉葉 +金柜 金櫃 +金柜石室 金櫃石室 +金榜挂名 金榜掛名 +金正云 金正雲 +金氏纪录 金氏紀錄 +金玉其表 金玉其表 +金甲虫 金甲蟲 +金疮药 金瘡藥 +金盏银台 金盞銀臺 +金石录 金石錄 +金穗奖 金穗獎 +金粉楼台 金粉樓臺 +金线虫 金線蟲 +金胄 金胄 +金花虫 金花蟲 +金范 金範 +金虫 金蟲 +金融杠杆 金融槓桿 +金融系统 金融系統 +金衡制 金衡制 +金表 金錶 +金表态 金表態 +金表情 金表情 +金表扬 金表揚 +金表明 金表明 +金表演 金表演 +金表现 金表現 +金表示 金表示 +金表达 金表達 +金表露 金表露 +金表面 金表面 +金装玉里 金裝玉裏 +金谷 金谷 +金谷园 金谷園 +金谷堕楼 金谷墮樓 +金谷酒数 金谷酒數 +金里奇 金裏奇 +金针 金針 +金针山 金針山 +金针度人 金針度人 +金针花 金針花 +金针菇 金針菇 +金针菜 金針菜 +金钟 金鐘 +金钟罩 金鐘罩 +金钟铲 金鐘鏟 +金钱万能 金錢萬能 +金钱挂帅 金錢掛帥 +金钱松 金錢松 +金钱游戏 金錢遊戲 +金链 金鍊 +金面 金面 +金马仑道 金馬崙道 +金马克 金馬克 +釜底游魂 釜底遊魂 +釜底游鱼 釜底游魚 +釜里之鱼 釜裏之魚 +鉴于 鑑於 +鉴别 鑑別 +鉴别力 鑑別力 +鉴别器 鑑別器 +鉴别能力 鑑別能力 +鉴古推今 鑑古推今 +鉴堂 鑑堂 +鉴定 鑑定 +鉴定为 鑑定爲 +鉴定书 鑑定書 +鉴定人 鑑定人 +鉴定委员会 鑑定委員會 +鉴定考试 鑑定考試 +鉴宝 鑑寶 +鉴察 鑑察 +鉴往知来 鑑往知來 +鉴戒 鑑戒 +鉴明 鑑明 +鉴核备査 鑑核備查 +鉴此 鑑此 +鉴毛辨色 鑑毛辨色 +鉴泉 鑑泉 +鉴湖 鑑湖 +鉴湖女侠 鑑湖女俠 +鉴照 鑑照 +鉴相 鑑相 +鉴真 鑑真 +鉴真和尚 鑑真和尚 +鉴藏印 鑑藏印 +鉴证 鑑證 +鉴识 鑑識 +鉴诫 鑑誡 +鉴谅 鑑諒 +鉴貌辨色 鑑貌辨色 +鉴赏 鑑賞 +鉴赏力 鑑賞力 +鉴赏印 鑑賞印 +鉴赏家 鑑賞家 +鉴赏能力 鑑賞能力 +鉴频 鑑頻 +鉴频器 鑑頻器 +针关 鍼關 +针具 針具 +针刺 針刺 +针刺麻醉 針刺麻醉 +针剂 針劑 +针口 鍼口 +针叶 針葉 +针叶林 針葉林 +针叶树 針葉樹 +针叶植物 針葉植物 +针头 針頭 +针孔 針孔 +针孔摄影机 針孔攝影機 +针孔照像 針孔照像 +针孔照像机 針孔照像機 +针孔现象 針孔現象 +针对 針對 +针对于 針對於 +针对性 針對性 +针尖 針尖 +针尖儿 針尖兒 +针工 針工 +针布 針布 +针形叶 針形葉 +针扣 針釦 +针指 針指 +针挑刀挖 針挑刀挖 +针梳机 針梳機 +针毡 針氈 +针法 針法 +针灸 鍼灸 +针灸铜人 鍼灸銅人 +针灸麻醉 鍼灸麻醉 +针炙 針炙 +针状 針狀 +针状物 針狀物 +针盘 針盤 +针眼 針眼 +针眼子 針眼子 +针砭 鍼砭 +针神 針神 +针笔 針筆 +针笔匠 針筆匠 +针筒 針筒 +针箍 針箍 +针箍儿 針箍兒 +针线 針線 +针线包 針線包 +针线娘 針線娘 +针线活 針線活 +针线活计 針線活計 +针线盒 針線盒 +针线箔篱 針線箔籬 +针织 針織 +针织厂 針織廠 +针织品 針織品 +针织料 針織料 +针脚 針腳 +针芒 鍼芒 +针芥相投 鍼芥相投 +针针见血 針針見血 +针铓 針鋩 +针锋 針鋒 +针锋相对 針鋒相對 +针锋相投 針鋒相投 +针饵莫减 針餌莫減 +针骨 針骨 +针鱼 針魚 +针黹 針黹 +针黹纺绩 針黹紡績 +针鼹 針鼴 +针鼻 針鼻 +针鼻儿 針鼻兒 +钉个 釘個 +钉书针 釘書針 +钉了 釘了 +钉合 釘合 +钉扣 釘釦 +钉锤 釘錘 +钓个 釣個 +钓了 釣了 +钓游旧地 釣遊舊地 +钓鱼台 釣魚臺 +钓鱼台列岛 釣魚臺列島 +钓鱼杆 釣魚杆 +钗云 釵雲 +钛合金 鈦合金 +钜万 鉅萬 +钞录 鈔錄 +钟上 鐘上 +钟下 鐘下 +钟不 鐘不 +钟不扣不鸣 鐘不扣不鳴 +钟不撞不鸣 鐘不撞不鳴 +钟不敲不响 鐘不敲不響 +钟不空则哑 鐘不空則啞 +钟乐 鐘樂 +钟乳洞 鐘乳洞 +钟乳石 鐘乳石 +钟体 鐘體 +钟停 鐘停 +钟关 鐘關 +钟匠 鐘匠 +钟发音 鐘發音 +钟口 鐘口 +钟响 鐘響 +钟响声 鐘響聲 +钟在寺里 鐘在寺裏 +钟塔 鐘塔 +钟壁 鐘壁 +钟声 鐘聲 +钟太 鐘太 +钟头 鐘頭 +钟好 鐘好 +钟山 鐘山 +钟山区 鐘山區 +钟山县 鐘山縣 +钟左右 鐘左右 +钟差 鐘差 +钟座 鐘座 +钟形 鐘形 +钟形虫 鐘形蟲 +钟律 鐘律 +钟快 鐘快 +钟情 鍾情 +钟意 鐘意 +钟慢 鐘慢 +钟摆 鐘擺 +钟敲 鐘敲 +钟无豔 鍾無豔 +钟有 鐘有 +钟楚红 鐘楚紅 +钟楼 鐘樓 +钟楼区 鐘樓區 +钟楼怪人 鐘樓怪人 +钟模 鐘模 +钟欣桐 鍾欣桐 +钟没 鐘沒 +钟漏 鐘漏 +钟点 鐘點 +钟点房 鐘點房 +钟点费 鐘點費 +钟爱 鍾愛 +钟王 鐘王 +钟珮瑄 鐘珮瑄 +钟琴 鐘琴 +钟的 鐘的 +钟盘 鐘盤 +钟相 鐘相 +钟磬 鐘磬 +钟祥 鍾祥 +钟祥县 鍾祥縣 +钟祥市 鍾祥市 +钟福松 鐘福松 +钟纽 鐘紐 +钟罩 鐘罩 +钟腰 鐘腰 +钟螺 鐘螺 +钟行 鐘行 +钟表 鐘錶 +钟表停 鐘錶停 +钟表盘 鐘表盤 +钟被 鐘被 +钟调 鐘調 +钟身 鐘身 +钟速 鐘速 +钟面 鐘面 +钟顶 鐘頂 +钟馗 鍾馗 +钟鸣 鐘鳴 +钟鸣漏尽 鐘鳴漏盡 +钟鸣鼎食 鐘鳴鼎食 +钟鼎 鐘鼎 +钟鼎之家 鐘鼎之家 +钟鼎人家 鐘鼎人家 +钟鼎山林 鐘鼎山林 +钟鼎文 鐘鼎文 +钟鼎款识 鐘鼎款識 +钟鼎高门 鐘鼎高門 +钟鼓 鐘鼓 +钟鼓齐鸣 鐘鼓齊鳴 +钢制 鋼製 +钢制品 鋼製品 +钢扣 鋼釦 +钢曲尺 鋼曲尺 +钢杯 鋼杯 +钢板 鋼板 +钢梁 鋼樑 +钢琴曲 鋼琴曲 +钢种 鋼種 +钢笔杆 鋼筆桿 +钢针 鋼針 +钢铁厂 鋼鐵廠 +钢铲 鋼鏟 +钥匙链 鑰匙鏈 +钧复 鈞覆 +钧鉴 鈞鑒 +钩党 鉤黨 +钩心斗角 鉤心鬥角 +钩深致远 鉤深致遠 +钩花点叶 鉤花點葉 +钩虫 鉤蟲 +钩虫病 鉤蟲病 +钩针 鉤針 +钮扣 鈕釦 +钱复 錢復 +钱布 錢布 +钱庄 錢莊 +钱板儿 錢板兒 +钱柜 錢櫃 +钱柜杂志 錢櫃雜誌 +钱玄同 錢玄同 +钱眼里安身 錢眼裏安身 +钱范 錢範 +钱谷 錢穀 +钱过北斗 錢過北斗 +钱钟书 錢鍾書 +钳制 鉗制 +钻井平台 鑽井平臺 +钻出 鑽出 +钻台 鑽臺 +钻心虫 鑽心蟲 +钻懒帮闲 鑽懶幫閒 +钻杆 鑽桿 +钻皮出羽 鑽皮出羽 +钻石项链 鑽石項鍊 +钿合 鈿合 +钿头云篦 鈿頭雲篦 +铁云藏龟 鐵雲藏龜 +铁价 鐵價 +铁制 鐵製 +铁厂 鐵廠 +铁叶 鐵葉 +铁合金 鐵合金 +铁娘子 鐵娘子 +铁托 鐵托 +铁扣 鐵釦 +铁拐 鐵柺 +铁杆 鐵桿 +铁杠 鐵槓 +铁板 鐵板 +铁板大鼓 鐵板大鼓 +铁板快书 鐵板快書 +铁板注脚 鐵板註腳 +铁板烧 鐵板燒 +铁板牛柳 鐵板牛柳 +铁板牛肉 鐵板牛肉 +铁板茄子 鐵板茄子 +铁板路 鐵板路 +铁板面 鐵板麪 +铁柜 鐵櫃 +铁栏杆 鐵欄杆 +铁甲虫 鐵甲蟲 +铁索郎当 鐵索郎當 +铁胃团 鐵胃團 +铁苏木 鐵蘇木 +铁路干线 鐵路幹線 +铁针 鐵針 +铁钟 鐵鐘 +铁铲 鐵鏟 +铁链 鐵鏈 +铁锤 鐵錘 +铁雕 鐵雕 +铁面 鐵面 +铁面判官 鐵面判官 +铁面御史 鐵面御史 +铁面无情 鐵面無情 +铁面无私 鐵面無私 +铃虫 鈴蟲 +铅制 鉛製 +铅字合金 鉛字合金 +铅板 鉛板 +铉台 鉉臺 +铜制 銅製 +铜制品 銅製品 +铜厂 銅廠 +铜叶 銅葉 +铜山西崩洛钟东应 銅山西崩洛鐘東應 +铜扣 銅釦 +铜斗儿 銅斗兒 +铜斗儿家缘 銅斗兒家緣 +铜板 銅板 +铜梁 銅梁 +铜梁县 銅梁縣 +铜琵铁板 銅琵鐵板 +铜范 銅範 +铜钟 銅鐘 +铜雀台 銅雀臺 +铜雕 銅雕 +铝制 鋁製 +铝制品 鋁製品 +铝合金 鋁合金 +铝板 鋁板 +铠胄 鎧冑 +铯钟 銫鐘 +铰链 鉸鏈 +铰链叶 鉸鏈葉 +铲下 剷下 +铲伤 剷傷 +铲倒 剷倒 +铲出 剷出 +铲凿 剷鑿 +铲刀 剷刀 +铲刈 剷刈 +铲土 剷土 +铲土机 鏟土機 +铲头 鏟頭 +铲子 鏟子 +铲射 剷射 +铲平 剷平 +铲抢 剷搶 +铲掉 剷掉 +铲斗 剷鬥 +铲断 剷斷 +铲板 剷板 +铲煤 剷煤 +铲球 剷球 +铲草 剷草 +铲起 剷起 +铲车 剷車 +铲运机 鏟運機 +铲运车 鏟運車 +铲铲 剷剷 +铲除 剷除 +铲雪 剷雪 +铲雪车 鏟雪車 +银丝卷 銀絲捲 +银价 銀價 +银制 銀製 +银发 銀髮 +银发产业 銀髮產業 +银发族 銀髮族 +银台 銀臺 +银合欢 銀合歡 +银团 銀團 +银本位制 銀本位制 +银朱 銀硃 +银杯 銀盃 銀杯 +银杯羽化 銀杯羽化 +银板 銀板 +银柜 銀櫃 +银核 銀核 +银河系 銀河系 +银纤维 銀纖維 +银色冲浪手 銀色衝浪手 +银行体系资金 銀行體系資金 +银行借款 銀行借款 +银行利害关系人 銀行利害關係人 +银行团 銀行團 +银行存折 銀行存摺 +银针 銀針 +银须 銀鬚 +铸币厂 鑄幣廠 +铸造出 鑄造出 +铸钟 鑄鐘 +铺了 鋪了 +铺伙 鋪夥 +铺出 鋪出 +铺出去 鋪出去 +铺出来 鋪出來 +铺向 鋪向 +铺地板 鋪地板 +铺板 鋪板 +铺盖卷儿 鋪蓋捲兒 +铺眉蒙眼 鋪眉蒙眼 +铺锦列绣 鋪錦列繡 +铺面 鋪面 +铺面临街房 鋪面臨街房 +铺面房 鋪面房 +链传动 鏈傳動 +链反应 鏈反應 +链坠 鍊墜 +链套 鏈套 +链子 鏈子 +链式 鏈式 +链式反应 鏈式反應 +链式裂变反应 鏈式裂變反應 +链形 鍊形 +链扣 鏈釦 +链接 鏈接 +链条 鏈條 +链椎 鏈椎 +链烃 鏈烴 +链环 鏈環 +链球 鏈球 +链球菌 鏈球菌 +链甲 鍊甲 +链罩 鏈罩 +链表 鏈表 +链路 鏈路 +链路层 鏈路層 +链轨 鏈軌 +链轮 鏈輪 +链钳子 鏈鉗子 +链锁 鏈鎖 +链锁反应 鏈鎖反應 +链锯 鏈鋸 +链霉素 鏈黴素 +销了 銷了 +销出 銷出 +销售价格 銷售價格 +销售时点情报系统 銷售時點情報系統 +销回 銷回 +销回去 銷回去 +销回来 銷回來 +销志 銷志 +销毁 銷燬 +销货折扣 銷貨折扣 +锁扣 鎖釦 +锁柜 鎖櫃 +锁链 鎖鏈 +锄奸 鋤奸 +锄铲 鋤鏟 +锅伙 鍋伙 +锅台 鍋臺 +锅庄 鍋莊 +锅烟 鍋煙 +锅烟子 鍋煙子 +锅铲 鍋鏟 +锆合金 鋯合金 +锈病 鏽病 +锈菌 鏽菌 +锈蚀 鏽蝕 +锋出 鋒出 +锋发韵流 鋒發韻流 +锋芒所向 鋒芒所向 +锋面 鋒面 +锋面雨 鋒面雨 +锌板 鋅板 +锐不可当 銳不可當 +锐志 銳志 +锐未可当 銳未可當 +锕系元素 錒系元素 +错不了 錯不了 +错了 錯了 +错了又错 錯了又錯 +错别字 錯別字 +错化合物 錯化合物 +错彩镂金 錯彩鏤金 +错综复杂 錯綜複雜 +错落有致 錯落有致 +错觉结合 錯覺結合 +错觉结合的词 錯覺結合的詞 +错误百出 錯誤百出 +错误的结合 錯誤的結合 +锚杆 錨杆 +锚虫 錨蟲 +锚链 錨鏈 +锚链孔 錨鏈孔 +锡克拜 錫克拜 +锡克教 錫克教 +锡克族 錫克族 +锡当河 錫當河 +锡杯 錫杯 +锤儿 錘兒 +锤头 錘頭 +锤子 錘子 +锤炼 錘鍊 +锥虫病 錐蟲病 +锥面 錐面 +锦囊佳制 錦囊佳製 +锦回文 錦迴文 +锦熏笼 錦熏籠 +锦绣 錦繡 +锦绣花园 錦繡花園 +锦胡同 錦衚衕 +锯了 鋸了 +锯了嘴的葫芦 鋸了嘴的葫蘆 +锯出 鋸出 +锯木厂 鋸木廠 +锲而不舍 鍥而不捨 +锹形虫 鍬形蟲 +锺万梅 鍾萬梅 +锺重发 鍾重發 +锺馗 鍾馗 +锻炼 鍛鍊 +锻炼出 鍛煉出 +锻炼身体 鍛鍊身體 +镁合金 鎂合金 +镂冰雕朽 鏤冰雕朽 +镂彩摛文 鏤彩摛文 +镂月裁云 鏤月裁雲 +镂金错采 鏤金錯采 +镇台 鎮臺 +镇宁布依族苗族自治县 鎮寧布依族苗族自治縣 +镇定药 鎮定藥 +镇痛药 鎮痛藥 +镇荣里 鎮榮里 +镇静药 鎮靜藥 +镍合金 鎳合金 +镕岩 鎔岩 +镜台 鏡臺 +镜框舞台 鏡框舞臺 +镜里孤鸾 鏡裏孤鸞 +镜鉴 鏡鑑 +镜面 鏡面 +镜面呢 鏡面呢 +镧系元素 鑭系元素 +镭射印表机 鐳射印表機 +镰仓 鎌倉 +镰形血球贫血症 鐮形血球貧血症 +镰状细胞血症 鐮狀細胞血症 +镶了 鑲了 +镶板 鑲板 +长丰 長豐 +长丰县 長豐縣 +长了 長了 +长于 長於 +长他人志气灭自己威风 長他人志氣滅自己威風 +长几 長几 +长出 長出 +长出来 長出來 +长卷 長卷 +长历 長曆 +长发 長髮 +长叹 長嘆 +长吁 長吁 +长吁短叹 長吁短嘆 +长君之恶 長君之惡 +长坂坡七进七出 長阪坡七進七出 +长头布 長頭布 +长寿烟 長壽菸 +长寿面 長壽麪 +长干巷 長干巷 +长干曲 長干曲 +长征 長征 +长征军 長征軍 +长恶不悛 長惡不悛 +长才 長才 +长春师范学院 長春師範學院 +长期借款 長期借款 +长板 長板 +长板凳 長板凳 +长林丰草 長林豐草 +长歌当哭 長歌當哭 +长江后浪 長江後浪 +长江后浪催前浪一代新人换旧人 長江後浪催前浪一代新人換舊人 +长烟 長煙 +长烟一空 長煙一空 +长生药 長生藥 +长程计划 長程計劃 +长绳系日 長繩繫日 +长绳系景 長繩繫景 +长胡 長鬍 +长虫 長蟲 +长谷 長谷 +长谷川 長谷川 +长辔远御 長轡遠御 +长针 長針 +长须 長鬚 +长须鲸 長鬚鯨 +长风万里 長風萬里 +開發周期 開發周期 +门前门后 門前門後 +门吊儿 門吊兒 +门帘 門簾 +门当户对 門當戶對 +门扣 門扣 +门拐 門拐 +门斗 門斗 +门板 門板 +门殚户尽 門殫戶盡 +门皁 門皁 +门胄 門胄 +门里 門裏 +门里人 門裏人 +门里出身 門裏出身 +门里安心 門裏安心 +门里门外 門裏門外 +门面 門面 +门面话 門面話 +闪了 閃了 +闪出 閃出 +闪含语系 閃含語系 +闪念 閃念 +闪电战术 閃電戰術 +闪闪发光 閃閃發光 +闫怀礼 閆懷禮 +闭一只眼 閉一隻眼 +闭卷 閉卷 +闭卷考 閉卷考 +闭卷考试 閉卷考試 +闭合 閉合 +闭合电路 閉合電路 +闭门家里坐祸从天上来 閉門家裏坐禍從天上來 +闭面 閉面 +问个 問個 +问个问题 問個問題 +问了 問了 +问出 問出 +问卜 問卜 +问卷 問卷 +问卷大调査 問卷大調查 +问卷调査 問卷調查 +问卷调査表 問卷調查表 +问当 問當 +问政于民 問政於民 +问舍求田 問舍求田 +问道于盲 問道於盲 +闯出 闖出 +闯出去 闖出去 +闯出来 闖出來 +闯炼 闖鍊 +闯荡 闖蕩 +闯荡江湖 闖蕩江湖 +闯进闯出 闖進闖出 +闲下来 閒下來 +闲不住 閒不住 +闲云孤鹤 閒雲孤鶴 +闲云野鹤 閒雲野鶴 +闲人免进 閒人免進 +闲余 閒餘 +闲庭信步 閒庭信步 +闲情逸致 閒情逸致 +闲情逸趣 閒情逸趣 +闲打牙儿 閒打牙兒 +闲扯淡 閒扯淡 +闲是闲非 閒是閒非 +闲杂人员 閒雜人員 +闲杂人等 閒雜人等 +闲来无事 閒來無事 +闲淘气 閒淘氣 +闲游 閒遊 +闲理会 閒理會 +闲盘儿 閒盤兒 +闲磕牙 閒磕牙 +闲篇儿 閒篇兒 +闲置不用 閒置不用 +闲聊天 閒聊天 +闲聒七 閒聒七 +闲花野草 閒花野草 +闲茶浪酒 閒茶浪酒 +闲荡 閒蕩 +闲言乱语 閒言亂語 +闲言碎语 閒言碎語 +闲言长语 閒言長語 +闲言闲事 閒言閒事 +闲言闲语 閒言閒語 +闲话家常 閒話家常 +闲话少说 閒話少說 +闲适 閒適 +闲邪存诚 閒邪存誠 +间不容发 間不容髮 +间充质干细胞 間充質幹細胞 +间出 間出 +间别 間別 +间叶干 間葉幹 +间奏曲 間奏曲 +间接证据 間接證據 +间深里 間深裏 +闵凶 閔凶 +闵子里 閔子裏 +闵采尔 閔採爾 +闷出 悶出 +闷在心里 悶在心裏 +闷板 悶板 +闷着头儿干 悶着頭兒幹 +闷表 悶錶 +闸板 閘板 +闹个 鬧個 +闹了半天鬼 鬧了半天鬼 +闹出 鬧出 +闹出去 鬧出去 +闹出来 鬧出來 +闹出笑话 鬧出笑話 +闹别扭 鬧彆扭 +闹台 鬧臺 +闹哄 鬧鬨 +闹哄哄 鬧哄哄 +闹拧了 鬧擰了 +闹着玩儿 鬧着玩兒 +闹表 鬧錶 +闹钟 鬧鐘 +闹铃时钟 鬧鈴時鐘 +闹饥荒 鬧饑荒 +闺范 閨範 +闻了 聞了 +闻出 聞出 +闻名于世 聞名於世 +闻风后 聞風後 +闽台 閩臺 +闽台地区 閩臺地區 +闾党姻娅 閭黨姻婭 +闾里 閭里 +阀杆 閥杆 +阃范 閫範 +阅兵台 閱兵臺 +阅卷 閱卷 +阅卷组 閱卷組 +阅历 閱歷 +阉党 閹黨 +阎云浚 閻雲浚 +阎王好见小鬼难当 閻王好見小鬼難當 +阎罗王面前须没放回的鬼 閻羅王面前須沒放回的鬼 +阐发 闡發 +阑尾切除术 闌尾切除術 +阑干 闌干 +阔别 闊別 +阔别多年 闊別多年 +阔叶林 闊葉林 +阔叶树 闊葉樹 +阙里 闕里 +阜新蒙古族自治县 阜新蒙古族自治縣 +阜阳师范学院 阜陽師範學院 +队别 隊別 +阡表 阡表 +阨困 阨困 +阮咸 阮咸 +防不胜防 防不勝防 +防个 防個 +防了 防了 +防人之口甚于防川 防人之口甚於防川 +防修 防修 +防制 防制 +防制法 防制法 +防卫过当 防衛過當 +防台 防颱 +防守战术 防守戰術 +防御 防禦 +防御力 防禦力 +防御工事 防禦工事 +防御性 防禦性 +防御战 防禦戰 +防御术 防禦術 +防御率 防禦率 +防御率王 防禦率王 +防御网 防禦網 +防患于未然 防患於未然 +防护团 防護團 +防护面具 防護面具 +防晒 防曬 +防毒斗篷 防毒斗篷 +防毒面具 防毒面具 +防民之口甚于防川 防民之口甚於防川 +防水布 防水布 +防水表 防水錶 +防滑链 防滑鏈 +防火布 防火布 +防火系统 防火系統 +防疫针 防疫針 +防秋 防秋 +防空识别区 防空識別區 +防范 防範 +防范体系 防範體系 +防范得宜 防範得宜 +防范意识 防範意識 +防范控制 防範控制 +防范措施 防範措施 +防范未然 防範未然 +防身术 防身術 +防锁死煞车系统 防鎖死煞車系統 +防锈 防鏽 +防雨布 防雨布 +阳历 陽曆 +阳历年 陽曆年 +阳台 陽臺 +阳台云雨 陽臺雲雨 +阳台春梦 陽臺春夢 +阳春面 陽春麪 +阳曲 陽曲 +阳曲县 陽曲縣 +阳极 陽極 +阳极射线 陽極射線 +阳极板 陽極板 +阳极泥 陽極泥 +阳电极 陽電極 +阳秋 陽秋 +阳虚发热 陽虛發熱 +阳谷 陽谷 +阳谷县 陽谷縣 +阳面 陽面 +阴丹布 陰丹布 +阴云 陰雲 +阴占 陰占 +阴历 陰曆 +阴历年 陰曆年 +阴山背后 陰山背後 +阴干 陰乾 +阴恶 陰惡 +阴暗 陰暗 +阴暗处 陰暗處 +阴暗面 陰暗面 +阴极 陰極 +阴极射线 陰極射線 +阴极管 陰極管 +阴核 陰核 +阴沈 陰沈 +阴沈沈 陰沈沈 +阴沟里翻船 陰溝裏翻船 +阴离子界面活性剂 陰離子界面活性劑 +阴胜则寒 陰勝則寒 +阴虚发热 陰虛發熱 +阴郁 陰鬱 +阴阳历 陰陽曆 +阴阳合历 陰陽合曆 +阴阴暗暗 陰陰暗暗 +阴阴沈沈 陰陰沈沈 +阴雕 陰雕 +阴面 陰面 +阵云 陣雲 +阵发性 陣發性 +阵线党 陣線黨 +阵面上 陣面上 +阶梯计价 階梯計價 +阶级斗争 階級鬥爭 +阻于 阻於 +阻修 阻修 +阻挡不了 阻擋不了 +阻止不了 阻止不了 +阿云 阿雲 +阿什克隆 阿什克隆 +阿仙药 阿仙藥 +阿信曲 阿信曲 +阿修罗 阿修羅 +阿修罗道 阿修羅道 +阿克伦 阿克倫 +阿克伦河 阿克倫河 +阿克塞县 阿克塞縣 +阿克塞哈萨克族自治县 阿克塞哈薩克族自治縣 +阿克拉 阿克拉 +阿克苏 阿克蘇 +阿克苏地区 阿克蘇地區 +阿克苏市 阿克蘇市 +阿克苏河 阿克蘇河 +阿克赛钦 阿克賽欽 +阿克达 阿克達 +阿克陶 阿克陶 +阿克陶县 阿克陶縣 +阿党 阿黨 +阿兹海默氏症 阿茲海默氏症 +阿兹海默症 阿茲海默症 +阿兹海默症病患 阿茲海默症病患 +阿列克西斯 阿列克西斯 +阿利托 阿利托 +阿加莎克里斯蒂 阿加莎克里斯蒂 +阿卜杜拉 阿卜杜拉 +阿卡提里 阿卡提里 +阿历山大 阿歷山大 +阿叶德 阿葉德 +阿合奇 阿合奇 +阿合奇县 阿合奇縣 +阿咸 阿咸 +阿奇里斯 阿奇里斯 +阿娘 阿孃 +阿尔发粒子 阿爾發粒子 +阿尔梅里亚 阿爾梅里亞 +阿尔汉格尔斯克州 阿爾漢格爾斯克州 +阿尔茨海默氏症 阿爾茨海默氏症 +阿尔茨海默症 阿爾茨海默症 +阿布 阿布 +阿布加 阿布加 +阿布叔醇 阿布叔醇 +阿布哈兹 阿布哈茲 +阿布尔 阿布爾 +阿布扎比 阿布扎比 +阿布扎比市 阿布扎比市 +阿布杜拉 阿布杜拉 +阿布杜拉曼 阿布杜拉曼 +阿布沙伊夫 阿布沙伊夫 +阿布沙耶夫 阿布沙耶夫 +阿布瑞 阿布瑞 +阿布瑞尤 阿布瑞尤 +阿布贝卡 阿布貝卡 +阿布贾 阿布賈 +阿布贾里布 阿布賈里布 +阿布达 阿布達 +阿布达比 阿布達比 +阿布雷尤 阿布雷尤 +阿弥陀佛 阿彌陀佛 +阿弥陀如来 阿彌陀如來 +阿弥陀经 阿彌陀經 +阿志 阿志 +阿意曲从 阿意曲從 +阿扎伦卡 阿紮倫卡 +阿扎尼亚 阿扎尼亞 +阿托品 阿托品 +阿拉伯共同市场 阿拉伯共衕市場 +阿拉伯联合大公国 阿拉伯聯合大公國 +阿拉伯联合酋长国 阿拉伯聯合酋長國 +阿拉克 阿拉克 +阿拉干山脉 阿拉乾山脈 +阿斗 阿斗 +阿斯图里亚斯 阿斯圖里亞斯 +阿旺曲培 阿旺曲培 +阿旺曲沛 阿旺曲沛 +阿曼苏丹国 阿曼蘇丹國 +阿朱 阿朱 +阿杰 阿杰 +阿松森岛 阿松森島 +阿柏克兹亚 阿柏克茲亞 +阿森松岛 阿森松島 +阿波罗计划 阿波羅計劃 +阿滋海默症 阿滋海默症 +阿瓦里德 阿瓦里德 +阿秋 阿秋 +阿米巴原虫 阿米巴原蟲 +阿米纳达布 阿米納達布 +阿耳忒弥斯 阿耳忒彌斯 +阿胡斯 阿胡斯 +阿芝特克人 阿芝特剋人 +阿芝特克语 阿芝特剋語 +阿苏 阿蘇 +阿苏山 阿蘇山 +阿苏火山 阿蘇火山 +阿蒙 阿蒙 +阿谀苟合 阿諛苟合 +阿贾克斯 阿賈克斯 +阿赫蒂萨里 阿赫蒂薩裏 +阿里 阿里 +阿里亚斯 阿里亞斯 +阿里地区 阿里地區 +阿里山 阿里山 +阿里山之歌 阿里山之歌 +阿里山乡 阿里山鄉 +阿里山区 阿里山區 +阿里山山脉 阿里山山脈 +阿里巴巴 阿里巴巴 +阿里巴巴与四十大盗 阿里巴巴與四十大盜 +阿里斯托芬 阿里斯托芬 +阿里斯托芳 阿里斯託芳 +阿里曼 阿里曼 +阿里桑那 阿里桑那 +阿里郎 阿里郎 +附了 附了 +附于 附於 +附会假借 附會假借 +附加价值 附加價值 +附合 附合 +附录 附錄 +附注 附註 +附膻逐秽 附膻逐穢 +附膻逐腥 附膻逐腥 +附膻逐臭 附膻逐臭 +附致语 附致語 +附表 附表 +附面层 附面層 +际会风云 際會風雲 +陆丰 陸豐 +陆丰市 陸豐市 +陆云 陸雲 +陆云浩 陸雲浩 +陆克文 陸克文 +陆均松 陸均松 +陆征祥 陸徵祥 +陆梁 陸梁 +陆游 陸游 +陆讋水栗 陸讋水慄 +陆龟蒙 陸龜蒙 +陇种 隴種 +陈万松 陳萬松 +陈万立 陳萬立 +陈三五娘 陳三五娘 +陈世杰 陳世杰 +陈义丰 陳義豐 +陈云 陳雲 +陈云林 陳雲林 +陈云程 陳雲程 +陈俊杰 陳俊傑 +陈克帆 陳克帆 +陈冲 陳沖 +陈列台 陳列臺 +陈升 陳昇 +陈同海 陳同海 +陈后主 陳後主 +陈嘉爵沈培智 陳嘉爵沈培智 +陈尸 陳屍 +陈尹杰 陳尹杰 +陈布雷 陳布雷 +陈希同 陳希同 +陈幸 陳幸 +陈幸妤 陳幸妤 +陈幸嫚 陳倖嫚 +陈建志 陳建志 +陈志伟 陳志偉 +陈志勇 陳志勇 +陈志声 陳志聲 +陈志强 陳志強 +陈志忠 陳志忠 +陈志明 陳志明 +陈志玮 陳志瑋 +陈志维 陳志維 +陈志诚 陳志誠 +陈志豪 陳志豪 +陈志远 陳志遠 +陈志鸿 陳志鴻 +陈情表 陳情表 +陈木胜 陳木勝 +陈杰 陳杰 +陈永丰 陳永豐 +陈汉升 陳漢昇 +陈瀛钟 陳瀛鐘 +陈炼 陳鍊 +陈秋婷 陳秋婷 +陈秋扬 陳秋揚 +陈秋火 陳秋火 +陈胜 陳勝 +陈胜在 陳勝在 +陈胜宏 陳勝宏 +陈胜福 陳勝福 +陈致中 陳致中 +陈致远 陳致遠 +陈荣杰 陳榮傑 +陈谷子烂芝麻 陳穀子爛芝麻 +陈郁秀 陳郁秀 +陈隆志 陳隆志 +陈面谟 陳面謨 +陈鼎击钟 陳鼎擊鐘 +降了 降了 +降于 降於 +降价 降價 +降价出售 降價出售 +降价求售 降價求售 +降升调 降升調 +降压药 降壓藥 +降噪 降噪 +降回 降回 +降回到 降回到 +降回去 降回去 +降回来 降回來 +降志辱身 降志辱身 +降血压药 降血壓藥 +降血脂药 降血脂藥 +降表 降表 +限于 限於 +限价 限價 +限制 限制 +限制住 限制住 +限制住居 限制住居 +限制器 限制器 +限制式 限制式 +限制性 限制性 +限制是 限制是 +限制类 限制類 +限制级 限制級 +限制词 限制詞 +限制选举 限制選舉 +限制酶 限制酶 +限制酶图谱 限制酶圖譜 +限尽 限盡 +陕西师范大学 陝西師範大學 +陕飞集团 陝飛集團 +陡然升高 陡然升高 +院系 院系 +院里 院裏 +除不尽 除不盡 +除了 除了 +除奸 除奸 +除恶 除惡 +除恶务尽 除惡務盡 +除恶务本 除惡務本 +除恶扶善 除惡扶善 +除旧布新 除舊佈新 +除臭药 除臭藥 +除虫剂 除蟲劑 +除虫菊 除蟲菊 +陨获 隕穫 +险恶 險惡 +险症 險症 +险胜 險勝 +陪了 陪了 +陪出 陪出 +陪吊 陪弔 +陪同 陪同 +陪同到 陪同到 +陪审制 陪審制 +陪审制度 陪審制度 +陪审团 陪審團 +陵云 陵雲 +陵土未干 陵土未乾 +陵折 陵折 +陵谷 陵谷 +陶制 陶製 +陶土制品 陶土製品 +陶庵梦忆 陶庵夢憶 +陶朱公 陶朱公 +陶板屋 陶板屋 +陶菲克 陶菲克 +陷之死地而后生 陷之死地而後生 +陷于 陷於 +陷入困境 陷入困境 +隆乳手术 隆乳手術 +隆冬 隆冬 +隆准 隆準 +隆准许 隆准許 +隆回 隆回 +隆回县 隆回縣 +隆极 隆極 +隆河谷地 隆河谷地 +随之而后 隨之而後 +随于 隨於 +随口胡诌 隨口胡謅 +随同 隨同 +随后 隨後 +随后就到 隨後就到 +随后就去 隨後就去 +随后就来 隨後就來 +随心所欲 隨心所欲 +随想曲 隨想曲 +随才器使 隨才器使 +随时制宜 隨時制宜 +隐于 隱於 +隐修士 隱修士 +隐修院 隱修院 +隐几 隱几 +隐占 隱佔 +隐名合伙 隱名合夥 +隐形涂料 隱形塗料 +隐恶扬善 隱惡揚善 +隐暗 隱暗 +隐睪症 隱睪症 +隐翅虫 隱翅蟲 +隐身术 隱身術 +隔了 隔了 +隔出 隔出 +隔出来 隔出來 +隔别 隔別 +隔向 隔向 +隔周 隔週 +隔山观虎斗 隔山觀虎鬥 +隔年的皇历 隔年的皇曆 +隔年皇历 隔年皇曆 +隔房同辈 隔房同輩 +隔断板 隔斷板 +隔板 隔板 +隔热板 隔熱板 +隔舱板 隔艙板 +隔音板 隔音板 +隧道尽头的光亮 隧道盡頭的光亮 +隧道症 隧道症 +隶仆 隸僕 +隶属于 隸屬於 +难了 難了 +难于 難於 +难于上天 難於上天 +难于接近 難於接近 +难于登天 難於登天 +难以出口 難以出口 +难住了 難住了 +难偿所愿 難償所願 +难出 難出 +难出手心 難出手心 +难分难舍 難分難捨 +难割难舍 難割難捨 +难咽 難嚥 +难容于 難容於 +难当 難當 +难当重任 難當重任 +难得糊涂 難得糊塗 +难挨 難捱 +难控制 難控制 +难施面目 難施面目 +难易适中 難易適中 +难舍 難捨 +难舍难分 難捨難分 +难舍难离 難捨難離 +难荫 難廕 +雀噪 雀噪 +雁荡 雁蕩 +雁荡山 雁蕩山 +雄师百万 雄師百萬 +雄心万丈 雄心萬丈 +雄心壮志 雄心壯志 +雄才 雄才 +雄才大略 雄才大略 +雄据一方 雄據一方 +雄斗斗 雄斗斗 +雄胜 雄勝 +雅人深致 雅人深致 +雅克 雅克 +雅克萨 雅克薩 +雅库次克 雅庫次克 +雅游 雅游 +雅筑 雅筑 +雅致 雅緻 +雅舍 雅舍 +雅范 雅範 +雅鲁藏布 雅魯藏布 +雅鲁藏布大峡谷 雅魯藏布大峽谷 +雅鲁藏布江 雅魯藏布江 +集中托运 集中托運 +集了 集了 +集于 集於 +集于一身 集於一身 +集会游行法 集會遊行法 +集体强奸 集體強姦 +集合 集合 +集合令 集合令 +集合体 集合體 +集合到 集合到 +集合号 集合號 +集合名词 集合名詞 +集合地点 集合地點 +集合式 集合式 +集合时间 集合時間 +集合点 集合點 +集合论 集合論 +集合起来 集合起來 +集团 集團 +集团主义 集團主義 +集团军 集團軍 +集团案 集團案 +集团结婚 集團結婚 +集团股 集團股 +集录 集錄 +集注 集註 +集游法 集遊法 +集电极 集電極 +集训团 集訓團 +集资合建 集資合建 +雇主 僱主 +雇于 僱於 +雇人 僱人 +雇佣 僱傭 +雇农 僱農 +雇到 僱到 +雇员 僱員 +雇工 僱工 +雇用 僱用 +雌核 雌核 +雌雄同体 雌雄同體 +雌雄同体人 雌雄同體人 +雌雄同株 雌雄同株 +雍容闲雅 雍容閒雅 +雕丧 雕喪 +雕丽 雕麗 +雕云 雕雲 +雕像 雕像 +雕具座 鵰具座 +雕出 雕出 +雕凿 雕鑿 +雕刻 雕刻 +雕刻出 雕刻出 +雕刻刀 雕刻刀 +雕刻匠 雕刻匠 +雕刻品 雕刻品 +雕刻家 雕刻家 +雕刻师 雕刻師 +雕刻术 雕刻術 +雕刻画 雕刻畫 +雕励 雕勵 +雕啄 雕啄 +雕塑 雕塑 +雕塑品 雕塑品 +雕塑家 雕塑家 +雕墙 雕牆 +雕工 雕工 +雕弓 雕弓 +雕心雁爪 鵰心雁爪 +雕悍 鵰悍 +雕戈 雕戈 +雕成 雕成 +雕敝 雕敝 +雕板 雕板 +雕栏 雕欄 +雕梁 雕樑 +雕梁画柱 雕樑畫柱 +雕梁画栋 雕樑畫棟 +雕楹碧槛 雕楹碧檻 +雕残 雕殘 +雕漆 雕漆 +雕版 雕版 +雕版印刷 雕版印刷 +雕琢 雕琢 +雕瑑 雕瑑 +雕砌 雕砌 +雕空 雕空 +雕章缋句 雕章繢句 +雕章镂句 雕章鏤句 +雕簇 雕簇 +雕绘 雕繪 +雕翎 鵰翎 +雕翎扇 鵰翎扇 +雕肝琢肾 雕肝琢腎 +雕肝琢膂 雕肝琢膂 +雕肝镂肾 雕肝鏤腎 +雕胡米 雕胡米 +雕色 雕色 +雕花 雕花 +雕花漆彩 雕花漆彩 +雕虫 雕蟲 +雕虫小技 雕蟲小技 +雕虫小艺 雕蟲小藝 +雕虫篆 雕蟲篆 +雕虫篆刻 雕蟲篆刻 +雕蚶镂蛤 雕蚶鏤蛤 +雕谢 雕謝 +雕镂 雕鏤 +雕镌 雕鐫 +雕零 雕零 +雕青 雕青 +雕题 雕題 +雕飕 雕颼 +雕饰 雕飾 +雕饰品 雕飾品 +雕骚 雕騷 +雕鹗 鵰鶚 +雕龙 雕龍 +雨云 雨雲 +雨余芳草斜阳 雨餘芳草斜陽 +雨刮 雨刮 +雨后 雨後 +雨后春笋 雨後春筍 +雨层云 雨層雲 +雨布 雨布 +雨散云收 雨散雲收 +雨村曲话 雨村曲話 +雨泽下注 雨澤下注 +雨约云期 雨約雲期 +雨花台 雨花臺 +雨花台区 雨花臺區 +雨魄云魂 雨魄雲魂 +雩坛 雩壇 +雪松 雪松 +雪板 雪板 +雪柜 雪櫃 +雪狮子向火 雪獅子向火 +雪窗萤几 雪窗螢几 +雪耻复国 雪恥復國 +雪耻报仇 雪恥報仇 +雪茄烟 雪茄煙 +雪里 雪裏 +雪里红 雪裏紅 +雪里蕻 雪裏蕻 +雪里送炭 雪裏送炭 +雪铲 雪鏟 +零个 零個 +零件厂 零件廠 +零余 零餘 +零余子 零餘子 +零只 零隻 +零周期 零週期 +零售价 零售價 +零售价格 零售價格 +零多只 零多隻 +零天后 零天後 +零布 零布 +零曲率 零曲率 +零系数 零係數 +雷云 雷雲 +雷克 雷克 +雷克南 雷克南 +雷克坦跑道 雷克坦跑道 +雷克斯 雷克斯 +雷克斯暴龙 雷克斯暴龍 +雷克萨斯 雷克薩斯 +雷克雅维克 雷克雅維克 +雷同 雷同 +雷夫范恩斯 雷夫范恩斯 +雷射血管成形术 雷射血管成形術 +雷德克里夫 雷德克里夫 +雷扎耶湖 雷扎耶湖 +雷文克劳 雷文克勞 +雷昂卡发洛 雷昂卡發洛 +雷神进行曲 雷神進行曲 +雷蒙 雷蒙 +雷蒙德 雷蒙德 +雷蒙德电 雷蒙德電 +雷贾帕克斯 雷賈帕克斯 +雷达搜索 雷達搜索 +雷雨云 雷雨雲 +雷霆万钧 雷霆萬鈞 +雷霆万钧之势 雷霆萬鈞之勢 +雷马克 雷馬克 +雾台 霧臺 +雾台乡 霧臺鄉 +雾蒙蒙 霧濛濛 +雾里 霧裏 +雾里看花 霧裏看花 +雾锁云埋 霧鎖雲埋 +需才孔亟 需才孔亟 +需求面 需求面 +需要是发明之母 需要是發明之母 +霁范 霽範 +霁范永存 霽範永存 +霄壤之别 霄壤之別 +震于 震於 +震天价响 震天價響 +震旦方向 震旦方向 +震栗 震慄 +震波曲线 震波曲線 +震源机制 震源機制 +震耳欲聋 震耳欲聾 +震荡 震盪 +震荡不安 震盪不安 +震荡性 震盪性 +霉干菜 黴乾菜 +霉毒 黴毒 +霉气冲天 黴氣沖天 +霉素 黴素 +霉菌 黴菌 +霉运当头 黴運當頭 +霉黑 黴黑 +霉黧 黴黧 +霍乱杆菌 霍亂桿菌 +霍克松 霍克松 +霍克海姆 霍克海姆 +霍尔布鲁克 霍爾布魯克 +霍布斯 霍布斯 +霍布森 霍布森 +霍普曼杯 霍普曼杯 +霍里 霍里 +霑体涂足 霑體塗足 +霓裳羽衣曲 霓裳羽衣曲 +霜叶 霜葉 +霞云 霞雲 +霞彩 霞彩 +霞表 霞表 +露丑 露醜 +露了 露了 +露出 露出 +露出了狐狸尾巴 露出了狐狸尾巴 +露出原形 露出原形 +露出去 露出去 +露出来 露出來 +露出破绽 露出破綻 +露出马脚 露出馬腳 +露台 露臺 +露台弟子 露臺弟子 +露复 露覆 +露天开采 露天開採 +露尸 露屍 +露布 露布 +露才 露才 +露才扬己 露才揚己 +露板 露板 +露营休闲车旅游 露營休閒車旅遊 +露面 露面 +露面抛头 露面拋頭 +霸占 霸佔 +霸占住 霸佔住 +霸才 霸才 +霸据 霸據 +霸术 霸術 +霸王别姬 霸王別姬 +霸陵折柳 霸陵折柳 +霹雳游侠 霹靂遊俠 +青云 青雲 +青云之器 青雲之器 +青云之士 青雲之士 +青云志 青雲志 +青云直上 青雲直上 +青云谱 青雲譜 +青云谱区 青雲譜區 +青出于蓝 青出於藍 +青出于蓝而胜于蓝 青出於藍而勝於藍 +青发 青發 +青叶 青葉 +青少年团 青少年團 +青山一发 青山一髮 +青帘 青帘 +青年党 青年黨 +青年团 青年團 +青年救国团 青年救國團 +青康滇纵谷高原 青康滇縱谷高原 +青杠 青槓 +青松 青松 +青果合作社 青果合作社 +青海师范大学 青海師範大學 +青灯黄卷 青燈黃卷 +青烟 青煙 +青瓦台 青瓦臺 +青石板 青石板 +青红皂白 青紅皁白 +青苹 青苹 +青苹果 青蘋果 +青虫 青蟲 +青蝇吊客 青蠅弔客 +青过于蓝 青過於藍 +青钱万选 青錢萬選 +青霉 青黴 +青霉素 青黴素 +青面獠牙 青面獠牙 +青鞋布袜 青鞋布襪 +靖康传信录 靖康傳信錄 +靖康要录 靖康要錄 +静以制动 靜以制動 +静修 靜修 +静极思动 靜極思動 +静脉曲张 靜脈曲張 +静脉注入 靜脈注入 +静脉注射 靜脈注射 +静脉血栓症 靜脈血栓症 +静舍 靜舍 +静荡荡 靜蕩蕩 +静配合 靜配合 +静默致哀 靜默致哀 +非于 非於 +非借不可 非借不可 +非党人士 非黨人士 +非党员 非黨員 +非出 非出 +非出不可 非出不可 +非占不可 非佔不可 +非发不可 非發不可 +非发光体 非發光體 +非合并 非合併 +非同 非同 +非同儿戏 非同兒戲 +非同容易 非同容易 +非同寻常 非同尋常 +非同小可 非同小可 +非同步 非同步 +非吸烟 非吸菸 +非层岩 非層巖 +非师范 非師範 +非师范类 非師範類 +非当 非當 +非当不可 非當不可 +非形象艺术 非形象藝術 +非意相干 非意相干 +非才 非才 +非据 非據 +非杠杆化 非槓桿化 +非标准 非標準 +非核 非核 +非欧几何 非歐幾何 +非欧几何学 非歐幾何學 +非法勾当 非法勾當 +非洲出血热 非洲出血熱 +非洲团结组织 非洲團結組織 +非洲大裂谷 非洲大裂谷 +非洲开发银行 非洲開發銀行 +非洲锥虫病 非洲錐蟲病 +非涂不可 非塗不可 +非游离辐射伤害 非游離輻射傷害 +非确定性 非確定性 +非种子 非種子 +非签不可 非簽不可 +非致命 非致命 +非营业支出 非營業支出 +非规范 非規範 +非词重复测验 非詞重復測驗 +非贸易创汇 非貿易創匯 +靠了 靠了 +靠前面 靠前面 +靠后 靠後 +靠后面 靠後面 +靠外面 靠外面 +靠外面走 靠外面走 +靠水面 靠水面 +靠里面 靠裏面 +靠里面走 靠裏面走 +靡不有初鲜克有终 靡不有初鮮克有終 +靡然向风 靡然向風 +面上 面上 +面上无光 面上無光 +面不改容 面不改容 +面不改色 面不改色 +面世 面世 +面临 面臨 +面临到 面臨到 +面临困难 面臨困難 +面交 面交 +面人 麪人 +面人儿 麪人兒 +面从 面從 +面价 麪價 +面会 面會 +面值 面值 +面儿 面兒 麪兒 +面允 面允 +面光 面光 +面具 面具 +面分 面分 +面刺 面刺 +面前 面前 +面包 麪包 +面包刀 麪包刀 +面包屑 麪包屑 +面包师 麪包師 +面包师傅 麪包師傅 +面包店 麪包店 +面包心 麪包心 +面包房 麪包房 +面包树 麪包樹 +面包渣 麪包渣 +面包片 麪包片 +面包皮 麪包皮 +面包粉 麪包粉 +面包花 麪包花 +面包车 麪包車 +面北 面北 +面北眉南 面北眉南 +面厂 麪廠 +面叙 面敘 +面向 面向 +面向对象的技术 面嚮對象的技術 +面向对象语言 面嚮對象語言 +面向连接 面向連接 +面君 面君 +面听 面聽 +面呈 面呈 +面告 面告 +面命耳提 面命耳提 +面商 面商 +面善 面善 +面善心恶 面善心惡 +面善心狠 面善心狠 +面嘱 面囑 +面嘴 面嘴 +面团 麪糰 +面团团 面團團 +面圆耳大 面圓耳大 +面圣 面聖 +面坊 麪坊 +面坯儿 麪坯兒 +面型 面型 +面垢 面垢 +面塑 麪塑 +面墙 面牆 +面墙而立 面牆而立 +面壁 面壁 +面壁下帷 面壁下帷 +面壁功深 面壁功深 +面壁坐禅 面壁坐禪 +面壁思过 面壁思過 +面奏 面奏 +面如 面如 +面如傅粉 面如傅粉 +面如冠玉 面如冠玉 +面如噀血 面如噀血 +面如土色 面如土色 +面如敷粉 面如敷粉 +面如桃花 面如桃花 +面如槁木 面如槁木 +面如死灰 面如死灰 +面如灰土 面如灰土 +面如白纸 面如白紙 +面如白蜡 面如白蠟 +面如美玉 面如美玉 +面如重枣 面如重棗 +面如金纸 面如金紙 +面如铁色 面如鐵色 +面嫩 面嫩 +面子 面子 +面子上 面子上 +面子上的人 面子上的人 +面子上的差使 面子上的差使 +面子上的话 面子上的話 +面子情儿 面子情兒 +面子药 麪子藥 +面子话 面子話 +面孔 面孔 +面容 面容 +面宽 面寬 +面对 面對 +面对现实 面對現實 +面对面 面對面 +面尘 面塵 +面山 面山 +面巾 面巾 +面市 面市 +面带 面帶 +面带微笑 面帶微笑 +面带忧容 面帶憂容 +面带愁容 面帶愁容 +面带病容 面帶病容 +面带笑容 面帶笑容 +面店 麪店 +面庞 面龐 +面弱 面弱 +面形 面形 +面影 面影 +面心立方最密堆积 面心立方最密堆積 +面恶 面惡 +面恶心善 面惡心善 +面情 面情 +面折 面折 +面折廷争 面折廷爭 +面授 面授 +面授机宜 面授機宜 +面摊 麪攤 +面摊子 麪攤子 +面数 面數 +面料 面料 +面斥 面斥 +面方口阔 面方口闊 +面旋 面旋 +面无 面無 +面无人色 面無人色 +面无血色 面無血色 +面无表情 面無表情 +面晤 面晤 +面月印记 面月印記 +面有 面有 +面有喜色 面有喜色 +面有忧色 面有憂色 +面有菜色 面有菜色 +面有难色 面有難色 +面朋 面朋 +面朝 面朝 +面杖 麪杖 +面杖吹火 麪杖吹火 +面条 麪條 +面条儿 麪條兒 +面条目 麪條目 +面板 面板 +面板厂 面板廠 +面板股 面板股 +面染 面染 +面桌 面桌 +面條目 面條目 +面水 面水 +面汤 麪湯 +面泛 面泛 +面洽 面洽 +面浆 麪漿 +面海 面海 +面灰 麪灰 +面点 麪點 +面点师 麪點師 +面点王 麪點王 +面熟 面熟 +面版 面版 +面甜 面甜 +面生 面生 +面生不熟 面生不熟 +面疔 面疔 +面疙瘩 麪疙瘩 +面疱 面皰 +面白无须 面白無鬚 +面皂 面皂 +面皮 麪皮 +面皮厚 面皮厚 +面皮失色 面皮失色 +面皮焦黄 面皮焦黃 +面皮薄 面皮薄 +面皮铁青 面皮鐵青 +面盆 面盆 +面盘 面盤 +面目 面目 +面目一新 面目一新 +面目全非 面目全非 +面目可憎 面目可憎 +面目清秀 面目清秀 +面目狰狞 面目猙獰 +面目黄瘦 面目黃瘦 +面相 面相 +面码儿 麪碼兒 +面碗 麪碗 +面票 麪票 +面禀 面稟 +面积 面積 +面积分 面積分 +面筋 麪筋 +面粉 麪粉 +面粉袋 麪粉袋 +面糊 麪糊 +面红 面紅 +面红耳热 面紅耳熱 +面红耳赤 面紅耳赤 +面红过耳 面紅過耳 +面红面赤 面紅面赤 +面纱 面紗 +面纸 面紙 +面缚 面縛 +面缚舆榇 面縛輿櫬 +面缚衔璧 面縛銜璧 +面缸 麪缸 +面罄 面罄 +面罩 面罩 +面肥 麪肥 +面膜 面膜 +面致 面致 +面般 面般 +面色 面色 +面色发白 面色發白 +面色如土 面色如土 +面色如生 面色如生 +面色如金纸 面色如金紙 +面色苍白 面色蒼白 +面若春花 面若春花 +面茶 麪茶 +面薄 面薄 +面薄腰纤 面薄腰纖 +面衣 面衣 +面西 面西 +面见 面見 +面见江东 面見江東 +面誉 面譽 +面誉背毁 面譽背譭 +面议 面議 +面试 面試 +面试会 面試會 +面试官 面試官 +面试工作 面試工作 +面请 面請 +面谀 面諛 +面谈 面談 +面谒 面謁 +面谕 面諭 +面谢 面謝 +面貌 面貌 +面貌一新 面貌一新 +面貌全非 面貌全非 +面质 面質 +面辞 面辭 +面邀 面邀 +面部 面部 +面部表情 面部表情 +面酱 麪醬 +面重 面重 +面门 面門 +面阔口方 面闊口方 +面陈 面陳 +面霜 面霜 +面露不悦 面露不悅 +面霸 麪霸 +面青脣白 面青脣白 +面面 面面 +面面俱全 面面俱全 +面面俱到 面面俱到 +面面俱圆 面面俱圓 +面面厮觑 面面廝覷 +面面相窥 面面相窺 +面面相觑 面面相覷 +面面观 面面觀 +面靥 面靨 +面颊 面頰 +面颜 面顏 +面额 面額 +面食 麪食 +面食类 麪食類 +面饰 面飾 +面饺 麪餃 +面饼 麪餅 +面馆 麪館 +面首 面首 +面驾 面駕 +面黄 面黃 +面黄肌瘦 面黃肌瘦 +面黄肌闳 面黃肌閎 +面黄脣白 面黃脣白 +革出 革出 +革出山门 革出山門 +革出教门 革出教門 +革制品 革製品 +革命党 革命黨 +革命党人 革命黨人 +革命发展 革命發展 +革命发展阶段论 革命發展階段論 +革命干劲 革命幹勁 +革命意志 革命意志 +革命斗志 革命鬥志 +革命积极 革命積極 +革命积极性 革命積極性 +革面 革面 +革面洗心 革面洗心 +靴后跟 靴後跟 +靶台 靶臺 +靶台上 靶臺上 +鞋厂 鞋廠 +鞋扣 鞋釦 +鞋柜 鞋櫃 +鞋里 鞋裏 +鞋面 鞋面 +鞠躬尽力 鞠躬盡力 +鞠躬尽悴 鞠躬盡悴 +鞠躬尽瘁 鞠躬盡瘁 +鞠躬尽瘁死而后已 鞠躬盡瘁死而後已 +鞣制 鞣製 +鞭尸 鞭屍 +鞭毛虫 鞭毛蟲 +鞭虫 鞭蟲 +鞭辟入里 鞭辟入裏 +鞭辟近里 鞭辟近裏 +韦后 韋后 +韦娘 韋娘 +韦布 韋布 +韦布匹夫 韋布匹夫 +韦布斯特 韋布斯特 +韦庄 韋莊 +韦陟朵云 韋陟朵雲 +韧皮纤维 韌皮纖維 +韩侂胄 韓侂冑 +韩信登坛 韓信登壇 +韩制 韓製 +韩升洙 韓昇洙 +韩国制 韓國製 +韩国联合通讯社 韓國聯合通訊社 +韩复矩 韓復榘 +韩宗志 韓宗志 +韩山师范学院 韓山師範學院 +韩巴里 韓巴里 +韩干 韓幹 +韩干画马 韓幹畫馬 +韩幸霖 韓幸霖 +韩康卖药 韓康賣藥 +韩彩英 韓彩英 +韩海苏潮 韓海蘇潮 +韩立克 韓立克 +韩蒙德 韓蒙德 +韫椟待价 韞櫝待價 +韬戈卷甲 韜戈卷甲 +音乐台 音樂臺 +音乐团 音樂團 +音乐系 音樂系 +音像制品 音像製品 +音准 音準 +音同 音同 +音声如钟 音聲如鐘 +音系 音系 +韵致 韻致 +韶山冲 韶山沖 +韶钢松山 韶鋼松山 +頁面 頁面 +页岩 頁岩 +页面 頁面 +顶个 頂個 +顶了 頂了 +顶凶 頂兇 +顶叶 頂葉 +顶回 頂回 +顶回去 頂回去 +顶回来 頂回來 +顶多 頂多 +顶夸克 頂夸克 +顶板 頂板 +顶核 頂核 +顶梁柱 頂樑柱 +顶梁骨走了真魂 頂梁骨走了真魂 +顶针 頂針 +顶针儿 頂針兒 +顶针挨住 頂針捱住 +顶门针 頂門針 +顶面 頂面 +项别骓 項別騅 +项庄 項莊 +项庄舞剑志在沛公 項莊舞劍志在沛公 +项梁 項梁 +项目表 項目表 +项链 項鍊 +顺于 順於 +顺发 順發 +顺向 順向 +顺当 順當 +顺德者吉逆天者凶 順德者吉逆天者凶 +顺时针 順時針 +顺时钟 順時鐘 +顺朱儿 順硃兒 +顺河回族区 順河回族區 +顺适 順適 +顺钟向 順鐘向 +顺风后 順風後 +须不是 須不是 +须发 鬚髮 +须发展 須發展 +须发文 須發文 +须发皆白 鬚髮皆白 +须发表 須發表 +须后水 須後水 +须子 鬚子 +须将有日思无日 須將有日思無日 +须弥 須彌 +须弥山 須彌山 +须弥座 須彌座 +须待 須待 +须得 須得 +须捷 須捷 +须是 須是 +须根 鬚根 +须根据 須根據 +须毛 鬚毛 +须然 須然 +须生 鬚生 +须用 須用 +须留 須留 +须眉 鬚眉 +须知 須知 +须索 須索 +须胡 鬚鬍 +须至 須至 +须臾 須臾 +须菩堤 須菩堤 +须虑 須慮 +须要 須要 +须陀洹 須陀洹 +须须 鬚鬚 +须髯 鬚髯 +须髯如戟 鬚髯如戟 +须鲨 鬚鯊 +须鲸 鬚鯨 +顽卤 頑鹵 +顽症 頑症 +顽童历险记 頑童歷險記 +顽筑舞笈 頑筑舞笈 +顽蒙 頑蒙 +顾不了 顧不了 +顾借 顧藉 +顾前不顾后 顧前不顧後 +顾前顾后 顧前顧後 +顾后瞻前 顧後瞻前 +顾复之恩 顧復之恩 +顾念 顧念 +顾曲 顧曲 +顾曲周郎 顧曲周郎 +顾正秋 顧正秋 +顾问团 顧問團 +顾面子 顧面子 +顾颜面 顧顏面 +顿了 頓了 +顿涅斯克 頓涅斯克 +顿涅茨克 頓涅茨克 +颁出 頒出 +颁发 頒發 +颁发奖品 頒發獎品 +颁发奖杯 頒發獎盃 +颁发奖牌 頒發獎牌 +颁发奖状 頒發獎狀 +颁发奖金 頒發獎金 +颁奖台 頒獎臺 +颁布 頒佈 +颂系 頌繫 +颂赞 頌讚 +预借 預借 +预制 預製 +预制构件 預製構件 +预卜 預卜 +预后 預後 +预征 預徵 +预想出 預想出 +预托证券 預託證券 +预扣 預扣 +预料出 預料出 +预期收入票据 預期收入票據 +预测出 預測出 +预算表 預算表 +预警系统 預警系統 +预防接种 預防接種 +预防注射 預防注射 +预防针 預防針 +颅盖內出血 顱蓋內出血 +颅顶叶 顱頂葉 +领出 領出 +领出去 領出去 +领出来 領出來 +领台 領檯 +领回 領回 +领回去 領回去 +领回来 領回來 +领域里 領域裏 +领导制度 領導制度 +领导干部 領導幹部 +领带针 領帶針 +领悟出 領悟出 +领扣 領釦 +领据 領據 +领略出 領略出 +领表 領表 +领表格 領表格 +领袖欲 領袖慾 +颇复 頗覆 +颇欲 頗欲 +颈链 頸鍊 +颊面 頰面 +颊须 頰鬚 +频危物种 頻危物種 +频数分布 頻數分佈 +频率合成 頻率合成 +频率调制 頻率調製 +颖脱而出 穎脫而出 +题个 題個 +题了 題了 +题名录 題名錄 +题库系统 題庫系統 +题材范围 題材範圍 +题材面 題材面 +题签 題簽 +颛制 顓制 +颛蒙 顓蒙 +颜回 顏回 +颜志麟 顏志麟 +颜范 顏範 +颜面 顏面 +颜面扫地 顏面掃地 +颜面神经 顏面神經 +颜面角 顏面角 +颜面骨 顏面骨 +额征 額徵 +额我略历 額我略曆 +额我略历史 額我略歷史 +额贺福志郎 額賀福志郎 +额面 額面 +颟里颟顸 顢里顢頇 +颠乾倒坤 顛乾倒坤 +颠仆 顛仆 +颠倒乾坤 顛倒乾坤 +颠儿面 顛兒面 +颠复 顛覆 +颠复性 顛覆性 +颠番面皮 顛番面皮 +颠覆 顛覆 +颠连困苦 顛連困苦 +颠颠仆仆 顛顛仆仆 +颤栗 顫慄 +風采 風采 +风举云摇 風舉雲搖 +风云 風雲 +风云不测 風雲不測 +风云之志 風雲之志 +风云人物 風雲人物 +风云人物奖 風雲人物獎 +风云变幻 風雲變幻 +风云变态 風雲變態 +风云变色 風雲變色 +风云叱咤 風雲叱吒 +风云开阖 風雲開闔 +风云感会 風雲感會 +风云月露 風雲月露 +风云气候 風雲氣候 +风云突变 風雲突變 +风云车 風雲車 +风云际会 風雲際會 +风从虎云从龙 風從虎雲從龍 +风入松 風入松 +风兴云蒸 風興雲蒸 +风刮 風颳 +风力发电 風力發電 +风卷 風捲 +风卷残云 風捲殘雲 +风发 風發 +风后 風后 +风向 風向 +风向器 風向器 +风向标 風向標 +风向球 風向球 +风向袋 風向袋 +风向针 風向針 +风吹两面倒 風吹兩面倒 +风吹砂子迷了眼 風吹砂子迷了眼 +风团 風團 +风土志 風土誌 +风在那里起雨在那里落 風在那裏起雨在那裏落 +风尘仆仆 風塵僕僕 +风尘恶俗 風塵惡俗 +风尘表物 風塵表物 +风干 風乾 +风干机 風乾機 +风情万种 風情萬種 +风成砂岩 風成砂岩 +风扫落叶 風掃落葉 +风斗 風斗 +风月宝鉴 風月寶鑑 +风水术 風水術 +风流云散 風流雲散 +风流别致 風流別致 +风流千古 風流千古 +风流才子 風流才子 +风流标致 風流標致 +风流蕴借 風流蘊藉 +风流酝借 風流醞藉 +风浪板 風浪板 +风湿症 風溼症 +风溼性心脏病 風溼性心臟病 +风烟 風煙 +风物志 風物誌 +风胡子 風胡子 +风致 風致 +风范 風範 +风药 風藥 +风虎云龙 風虎雲龍 +风起云布 風起雲布 +风起云涌 風起雲涌 +风起云蒸 風起雲蒸 +风轻云淡 風輕雲淡 +风轻云淨 風輕雲淨 +风速表 風速表 +风采 風采 +风采堂堂 風采堂堂 +风里 風裏 +风里杨花 風裏楊花 +风里言 風裏言 +风里语 風裏語 +风铲 風鏟 +风险与收益的关系 風險與收益的關係 +风险防范 風險防範 +风雨同舟 風雨同舟 +风雨欲来 風雨欲來 +风靡云涌 風靡雲涌 +风靡云蒸 風靡雲蒸 +风飞云会 風飛雲會 +风马云车 風馬雲車 +风马牛不相干 風馬牛不相干 +风驰电卷 風馳電卷 +飘出 飄出 +飘向 飄向 +飘回 飄回 +飘摆 飄擺 +飘游 飄遊 +飘游四海 飄遊四海 +飘然出世 飄然出世 +飘荡 飄蕩 +飘飘欲仙 飄飄欲仙 +飘飘荡荡 飄飄蕩蕩 +飙升 飆升 +飙发电举 飆發電舉 +飞个 飛個 +飞了 飛了 +飞云 飛雲 +飞云掣电 飛雲掣電 +飞出 飛出 +飞出个未来 飛出個未來 +飞出去 飛出去 +飞出来 飛出來 +飞刍挽粒 飛芻輓粒 +飞刍挽粟 飛芻輓粟 +飞刍挽粮 飛芻輓糧 +飞升 飛昇 +飞向 飛向 +飞回 飛回 +飞回去 飛回去 +飞回来 飛回來 +飞征 飛征 +飞必冲天 飛必沖天 +飞扎 飛紮 +飞梁 飛樑 +飞烟传 飛煙傳 +飞燕游龙 飛燕游龍 +飞粮挽秣 飛糧輓秣 +飞腾之药 飛騰之藥 +飞航资料记录器 飛航資料記錄器 +飞虫 飛蟲 +飞蚊症 飛蚊症 +飞行云 飛行雲 +飞行甲板 飛行甲板 +飞行记录 飛行記錄 +飞行记录仪 飛行記錄儀 +飞行记录器 飛行記錄器 +飞行钟 飛行鐘 +飞谷 飛谷 +飞车党 飛車黨 +飞针走线 飛針走線 +飞铲 飛鏟 +飞鸟尽良弓藏 飛鳥盡良弓藏 +飞龙乘云 飛龍乘雲 +食不下咽 食不下咽 +食不充饥 食不充飢 +食不糊口 食不餬口 +食品厂 食品廠 +食品药品监督局 食品藥品監督局 +食品药品监督管理局 食品藥品監督管理局 +食心虫 食心蟲 +食日万钱 食日萬錢 +食欲 食慾 +食欲不佳 食慾不佳 +食欲不振 食慾不振 +食物及药品管理局 食物及藥品管理局 +食物柜 食物櫃 +食物链 食物鏈 +食用蜡烛木 食用蠟燭木 +食虫植物 食蟲植物 +食虫目 食蟲目 +食虫虻 食蟲虻 +食货志 食貨志 +食野之苹 食野之苹 +食面 食麪 +飮胄 飮冑 +餍于游乐 饜於游樂 +餐台 餐檯 +餐松啖柏 餐松啖柏 +餐松食柏 餐松食柏 +餐松饮涧 餐松飲澗 +饥不可食寒不可衣 飢不可食寒不可衣 +饥不择食 飢不擇食 +饥冻交切 飢凍交切 +饥困 飢困 +饥寒 飢寒 +饥寒交切 飢寒交切 +饥寒交迫 飢寒交迫 +饥年 饑年 +饥民 饑民 +饥渴 飢渴 +饥渴交攻 飢渴交攻 +饥渴交迫 飢渴交迫 +饥溺 飢溺 +饥火 飢火 +饥火中焚 飢火中焚 +饥火烧肠 飢火燒腸 +饥者易为食 飢者易爲食 +饥者甘糟糠 飢者甘糟糠 +饥肠 飢腸 +饥肠辘辘 飢腸轆轆 +饥色 飢色 +饥荒 饑荒 +饥虎扑食 飢虎撲食 +饥附饱飏 飢附飽颺 +饥餐渴饮 飢餐渴飲 +饥饱 飢飽 +饥饿 飢餓 +饥饿三十 飢餓三十 +饥饿感 飢餓感 +饥馁 飢餒 +饥馑 饑饉 +饥馑之岁 饑饉之歲 +饥馑荐臻 饑饉薦臻 +饥鹰饿虎 飢鷹餓虎 +饬回 飭回 +饭后 飯後 +饭后服用 飯後服用 +饭后漱口 飯後漱口 +饭后百步走 飯後百步走 +饭后钟 飯後鐘 +饭团 飯糰 +饭庄 飯莊 +饭店回葱 飯店回蔥 +饭店里买葱 飯店裏買蔥 +饭饱生余事 飯飽生餘事 +饮弹自尽 飲彈自盡 +饮杯 飲杯 +饮水曲肱 飲水曲肱 +饯别 餞別 +饰扣 飾釦 +饰板 飾板 +饰面 飾面 +饱人不知饿人饥 飽人不知餓人飢 +饱合 飽合 +饱和化合物 飽和化合物 +饱学秀才 飽學秀才 +饱尝 飽嘗 飽嚐 +饱当知人饥 飽當知人飢 +饱暖思淫欲 飽暖思淫慾 +饱暖生淫欲 飽暖生淫慾 +饱汉不知饿汉饥 飽漢不知餓漢飢 +饲养标准 飼養標準 +饲喂 飼餵 +饶舌歌曲 饒舌歌曲 +饼干 餅乾 +饼干店 餅乾店 +饼干盒 餅乾盒 +饿死了 餓死了 +饿殍枕借 餓殍枕藉 +饿虎饥鹰 餓虎飢鷹 +馂余 餕餘 +馄饨面 餛飩麪 +馆舍 館舍 +馆谷 館穀 +馆里 館裏 +馆际互借 館際互借 +馋涎欲垂 饞涎欲垂 +馋涎欲滴 饞涎欲滴 +首丘夙愿 首丘夙願 +首发 首發 +首只 首隻 +首台 首臺 +首尔 首爾 +首席代表 首席代表 +首开纪录 首開紀錄 +首当 首當 +首当其冲 首當其衝 +首恶 首惡 +首部曲 首部曲 +首都师范大学 首都師範大學 +首都杯 首都盃 +首长制 首長制 +首面 首面 +首须 首須 +香干 香乾 +香愿 香願 +香斗 香斗 +香格里拉 香格里拉 +香格里拉县 香格裏拉縣 +香格里拉怡咖啡 香格里拉怡咖啡 +香榭丽舍 香榭麗舍 +香榭丽舍大街 香榭麗舍大街 +香榭里大道 香榭里大道 +香港工会联合会 香港工會聯合會 +香港游 香港遊 +香港电台 香港電臺 +香港贸易发展局 香港貿易發展局 +香烟 香菸 +香烟不绝 香煙不絕 +香烟后代 香煙後代 +香烟头 香菸頭 +香烟盒 香菸盒 +香熏 香薰 +香熏疗法 香薰療法 +香皂 香皂 +香菜叶 香菜葉 +香蜡 香 +香蜡店 香蠟店 +香蜡纸马 香蠟紙馬 +香蜡铺 香蠟鋪 +香郁 香郁 +馥郁 馥郁 +馬占山 馬占山 +馬格里布 馬格里布 +马丁尼克 馬丁尼克 +马丁杜里荷 馬丁杜里荷 +马云 馬雲 +马修连恩 馬修連恩 +马修麦费狄恩 馬修麥費狄恩 +马克 馬克 +马克吐温 馬克吐溫 +马克安东尼 馬克安東尼 +马克安诺 馬克安諾 +马克思 馬克思 +马克思主义 馬克思主義 +马克思列宁主义 馬克思列寧主義 +马克数 馬克數 +马克斯 馬克斯 +马克斯主义 馬克斯主義 +马克斯威尔 馬克斯威爾 +马克斯普朗克 馬克斯普朗克 +马克杯 馬克杯 +马克沁 馬克沁 +马克沁机枪 馬克沁機槍 +马克笔 馬克筆 +马克西米连 馬克西米連 +马利布 馬利布 +马占山 馬占山 +马厂 馬廠 +马厝卡舞曲 馬厝卡舞曲 +马可波罗游记 馬可波羅遊記 +马台 馬臺 +马吊 馬吊 +马后 馬後 +马后炮 馬後炮 +马后砲 馬後砲 +马后练服 馬后練服 +马噶尔尼使团 馬噶爾尼使團 +马太受难曲 馬太受難曲 +马夫 馬伕 +马头娘 馬頭娘 +马尔克奥雷利 馬爾克奧雷利 +马尔扎赫 馬爾扎赫 +马尔谷 馬爾谷 +马尼托巴 馬尼托巴 +马尾松 馬尾松 +马布 馬布 +马布瑞 馬布瑞 +马干 馬乾 +马德里 馬德里 +马德里队 馬德里隊 +马志英 馬志英 +马戏团 馬戲團 +马戏团表演 馬戲團表演 +马戏表演 馬戲表演 +马戛尔尼使团 馬戛爾尼使團 +马扎 馬紮 +马扎尔 馬扎爾 +马扎尔语 馬扎爾語 +马托格罗索 馬託格羅索 +马拉巴栗 馬拉巴栗 +马拉松 馬拉松 +马拉松式 馬拉松式 +马拉松组 馬拉松組 +马拉松赛 馬拉松賽 +马拉松赛跑 馬拉松賽跑 +马提尼克 馬提尼克 +马斯垂克 馬斯垂剋 +马斯垂克条约 馬斯垂克條約 +马斯特里赫特 馬斯特裏赫特 +马普托 馬普托 +马术 馬術 +马术赛 馬術賽 +马朱罗 馬朱羅 +马杆 馬杆 +马来亚玻里尼西亚语系 馬來亞玻里尼西亞語系 +马杰明 馬傑明 +马格里布 馬格里布 +马桶里 馬桶裏 +马死黄金尽 馬死黃金盡 +马致远 馬致遠 +马苏 馬蘇 +马苏德 馬蘇德 +马苏里拉 馬蘇裏拉 +马表 馬錶 +马车夫 馬車伕 +马里亚纳 馬里亞納 +马里亚纳海沟 馬裏亞納海溝 +马里亚纳群岛 馬里亞納羣島 +马里克 馬里克 +马里兰 馬里蘭 +马里兰州 馬里蘭州 +马里内斯科 馬里內斯科 +马里奇 馬里奇 +马里奥 馬里奧 +马里安纳海沟 馬里安納海溝 +马里布 馬里布 +马里斯 馬里斯 +马面 馬面 +马面战棚 馬面戰棚 +马革裹尸 馬革裹屍 +马鬣松 馬鬣松 +驭兽术 馭獸術 +驭夫有术 馭夫有術 +驱出 驅出 +驱恶向善 驅惡向善 +驱虫 驅蟲 +驱虫剂 驅蟲劑 +驱虫效率 驅蟲效率 +驱逐出 驅逐出 +驱逐出境 驅逐出境 +驳回 駁回 +驳回去 駁回去 +驳回来 駁回來 +驳面子 駁面子 +驴前马后 驢前馬後 +驴蒙虎皮 驢蒙虎皮 +驶出 駛出 +驶向 駛向 +驶回 駛回 +驻台 駐臺 +驻扎 駐紮 +驻扎地 駐紮地 +驻车制动 駐車制動 +驻颜有术 駐顏有術 +驽马十舍 駑馬十舍 +驾了 駕了 +驾云 駕雲 +驾回 駕回 +驾回去 駕回去 +驾回来 駕回來 +驾娘 駕娘 +驾御 駕御 +驾雾腾云 駕霧騰雲 +驾鹤西游 駕鶴西遊 +骀借 駘藉 +骀荡 駘蕩 +骂个 罵個 +骂了 罵了 +骋凶骋势 騁兇騁勢 +骋志 騁志 +验出 驗出 +验尸 驗屍 +验尸官 驗屍官 +验收规范 驗收規範 +验核 驗覈 +骏业宏发 駿業宏發 +骏发 駿發 +骏豪集团 駿豪集團 +骏马雕鞍 駿馬雕鞍 +骑了 騎了 +骑兵团 騎兵團 +骑出 騎出 +骑出去 騎出去 +骑出来 騎出來 +骑回 騎回 +骑回去 騎回去 +骑回来 騎回來 +骑士团 騎士團 +骑术 騎術 +骗不了 騙不了 +骗了 騙了 +骗人布 騙人布 +骗出 騙出 +骗出去 騙出去 +骗出来 騙出來 +骗回 騙回 +骗回去 騙回去 +骗回来 騙回來 +骗拐 騙拐 +骗术 騙術 +骗术奇谭 騙術奇譚 +骨坛 骨罈 +骨头里挣出来的钱才做得肉 骨頭裏掙出來的錢纔做得肉 +骨子里 骨子裏 +骨子里头 骨子裏頭 +骨岩岩 骨巖巖 +骨干 骨幹 +骨干分子 骨幹分子 +骨干网路 骨幹網路 +骨折 骨折 +骨灰坛 骨灰罈 +骨肉团圆 骨肉團圓 +骨肉团聚 骨肉團聚 +骨肉团𪢮 骨肉團圞 +骨质疏松 骨質疏鬆 +骨质疏松症 骨質疏鬆症 +骨质石化症 骨質石化症 +骨质软化症 骨質軟化症 +骨里骨碌 骨裏骨碌 +骨针 骨針 +骺软骨板 骺軟骨板 +骾朴 骾朴 +髀肉复生 髀肉復生 +高个 高個 +高个儿 高個兒 +高个子 高個子 +高丽参 高麗蔘 +高了 高了 +高于 高於 +高云 高雲 +高价 高價 +高价位 高價位 +高价股 高價股 +高几 高几 +高出 高出 +高升 高升 +高参 高參 +高发 高發 +高台 高臺 +高台县 高臺縣 +高周波 高周波 +高唱入云 高唱入雲 +高坛 高壇 +高处不胜寒 高處不勝寒 +高学历 高學歷 +高尔基复合体 高爾基複合體 +高尔察克 高爾察克 +高层云 高層雲 +高层建筑 高層建築 +高山症 高山症 +高岸深谷 高岸深谷 +高干 高幹 +高干扰 高干擾 +高干预 高干預 +高度自制 高度自制 +高度表 高度表 +高志尚 高志尚 +高志纲 高志綱 +高志航 高志航 +高志鹏 高志鵬 +高性价 高性價 +高手如云 高手如雲 +高才 高才 +高才生 高才生 +高技术 高技術 +高抬身价 高擡身價 +高挂 高掛 +高政升 高政昇 +高杆 高杆 +高杠 高槓 +高柏松 高柏松 +高标准 高標準 +高梁 高梁 +高梁川 高梁川 +高梁市 高梁市 +高梁米 高梁米 +高梁酒 高梁酒 +高歌一曲 高歌一曲 +高步云衢 高步雲衢 +高水准 高水準 +高清愿 高清愿 +高球杯 高球杯 +高票当选 高票當選 +高积云 高積雲 +高等师范 高等師範 +高筋面粉 高筋麪粉 +高纤 高纖 +高纤维 高纖維 +高级管理人才 高級管理人才 +高耸入云 高聳入雲 +高胄 高胄 +高能烈性炸药 高能烈性炸藥 +高脂血症 高脂血症 +高脚杯 高腳杯 +高致病性 高致病性 +高致病性禽 高致病性禽 +高良姜 高良薑 +高英杰 高英傑 +高血压症 高血壓症 +高血压药 高血壓藥 +高血脂症 高血脂症 +高跳台 高跳臺 +高郁淨 高郁淨 +髡发 髡髮 +髭胡 髭鬍 +髭须 髭鬚 +髯胡 髯鬍 +髹饰录 髹飾錄 +髼松 髼鬆 +鬅松 鬅鬆 +鬈发 鬈髮 +鬈曲 鬈曲 +鬒发 鬒髮 +鬓云 鬢雲 +鬓发 鬢髮 +鬓发如银 鬢髮如銀 +鬓发皆白 鬢髮皆白 +鬓发皓然 鬢髮皓然 +鬼出电入 鬼出電入 +鬼录 鬼錄 +鬼怕恶人 鬼怕惡人 +鬼才 鬼才 +鬼气冲天 鬼氣沖天 +鬼片当道 鬼片當道 +鬼胡油 鬼胡油 +鬼胡由 鬼胡由 +鬼谷子 鬼谷子 +鬼迷了张天师 鬼迷了張天師 +鬼针草 鬼針草 +鬼门上占卦 鬼門上占卦 +魁人党 魁人黨 +魁北克 魁北克 +魁北克市 魁北克市 +魁北克省 魁北克省 +魂不守舍 魂不守舍 +魂牵梦系 魂牽夢繫 +魏克 魏克 +魏克菲尔 魏克菲爾 +魏台复 魏臺復 +魏幸雄 魏幸雄 +魏征 魏徵 +魏斯里史奈普 魏斯里史奈普 +魏明谷 魏明谷 +魏郁奇 魏郁奇 +魔仆 魔僕 +魔合罗 魔合羅 +魔术 魔術 +魔术家 魔術家 +魔术师 魔術師 +魔术数字 魔術數字 +魔术方块 魔術方塊 +魔术棒 魔術棒 +魔术秀 魔術秀 +魔术蛇 魔術蛇 +魔术贴 魔術貼 +魔术队 魔術隊 +魔板 魔板 +魔表 魔錶 +魔赛克 魔賽克 +鱼丸粗面 魚丸粗麪 +鱼台 魚臺 +鱼台县 魚臺縣 +鱼头参政 魚頭參政 +鱼尾板 魚尾板 +鱼干 魚乾 +鱼松 魚鬆 +鱼板 魚板 +鱼梁 魚梁 +鱼沈雁杳 魚沈雁杳 +鱼游釜中 魚游釜中 +鱼游釜底 魚游釜底 +鱼种 魚種 +鱼篮宝卷 魚籃寶卷 +鱼肉乡里 魚肉鄉里 +鱼胄 魚冑 +鱼虫 魚蟲 +鱼贯而出 魚貫而出 +鱼鳞松 魚鱗松 +鱼鼓简板 魚鼓簡板 +鲁克 魯克 +鲁克斯 魯克斯 +鲁毕克方块 魯畢克方塊 +鲁般手里调大斧 魯般手裏調大斧 +鲁适维 魯適維 +鲇鱼 鮎魚 +鲋鱼困涸辙难待西江水 鮒魚困涸轍難待西江水 +鲍德里亚 鮑德里亞 +鲜于 鮮于 +鲜彩 鮮彩 +鲜明个性 鮮明個性 +鲜胄 鮮胄 +鲜血淋漓 鮮血淋漓 +鲜谷王 鮮穀王 +鲸蜡 鯨蠟 +鲸须 鯨鬚 +鳃叶 鰓葉 +鳌头独占 鰲頭獨佔 +鳌里夺尊 鰲裏奪尊 +鳝鱼面 鱔魚麪 +鳞叶 鱗葉 +鳞游 鱗游 +鳞虫 鱗蟲 +鳣舍 鱣舍 +鸟卜 鳥卜 +鸟尽弓藏 鳥盡弓藏 +鸟松 鳥松 +鸟松乡 鳥松鄉 +鸟虫书 鳥蟲書 +鸟里鸟气 鳥裏鳥氣 +鸟面鹄形 鳥面鵠形 +鸠占鹊巢 鳩佔鵲巢 +鸠合 鳩合 +鸠形鹄面 鳩形鵠面 +鸡丝 雞絲 +鸡丝面 雞絲麪 +鸡争鹅斗 雞爭鵝鬥 +鸡兔同笼 雞兔同籠 +鸡口牛后 雞口牛後 +鸡只 雞隻 +鸡同鸭讲 雞同鴨講 +鸡吵鹅斗 雞吵鵝鬥 +鸡奸 雞姦 +鸡尸牛从 雞尸牛從 +鸡犬俱升 雞犬俱升 +鸡犬升天 雞犬升天 +鸡犬识新丰 雞犬識新豐 +鸡皮栗子 雞皮栗子 +鸡皮鹤发 雞皮鶴髮 +鸡肤鹤发 雞膚鶴髮 +鸡腿面 雞腿麪 +鸡舍 雞舍 +鸡虫得失 雞蟲得失 +鸡蛋里挑骨头 雞蛋裏挑骨頭 +鸡蛋面 雞蛋麪 +鸣咽 鳴咽 +鸣榔板 鳴榔板 +鸣钟 鳴鐘 +鸣钟列鼎 鳴鐘列鼎 +鸦片烟 鴉片煙 +鸦窝里出凤凰 鴉窩裏出鳳凰 +鸭子划水 鴨子划水 +鸳鸯折颈 鴛鴦折頸 +鸷虫 鷙蟲 +鸿志 鴻志 +鸿案相庄 鴻案相莊 +鸿篇巨制 鴻篇鉅製 +鸿篇巨著 鴻篇鉅著 +鸿胄 鴻胄 +鸿范 鴻範 +鸿蒙 鴻蒙 +鸿运当头 鴻運當頭 +鸿鹄之志 鴻鵠之志 +鸿鹄大志 鴻鵠大志 +鹄发 鵠髮 +鹄志 鵠志 +鹄面 鵠面 +鹄面鸟形 鵠面鳥形 +鹄面鸠形 鵠面鳩形 +鹅准 鵝準 +鹊噪 鵲噪 +鹊巢鸠占 鵲巢鳩佔 +鹍鸡曲 鵾雞曲 +鹏程万里 鵬程萬里 +鹘仑吞枣 鶻崙吞棗 +鹤发 鶴髮 +鹤发童颜 鶴髮童顏 +鹤吊 鶴弔 +鹤板 鶴板 +鹤骨松姿 鶴骨松姿 +鹰嘴豆面粉 鷹嘴豆麪粉 +鹰扬万里 鷹揚萬里 +鹰架栈台 鷹架棧臺 +鹰雕 鷹鵰 +鹿台 鹿臺 +鹿谷 鹿谷 +鹿谷乡 鹿谷鄉 +鹿车共挽 鹿車共挽 +鹿门采药 鹿門采藥 +麟台 麟臺 +麟游 麟遊 +麟游县 麟遊縣 +麟种 麟種 +麦个子 麥個子 +麦克 麥克 +麦克伦堡 麥克倫堡 +麦克尼尔 麥克尼爾 +麦克拉伦 麥克拉倫 +麦克斯 麥克斯 +麦克斯韦 麥克斯韋 +麦克林 麥克林 +麦克林登 麥克林登 +麦克格雷格 麥克格雷格 +麦克白 麥克白 +麦克白夫人 麥克白夫人 +麦克米兰 麥克米蘭 +麦克维 麥克維 +麦克罗尼西亚人 麥克羅尼西亞人 +麦克贝 麥克貝 +麦克连 麥克連 +麦克道格拉斯 麥克道格拉斯 +麦克阿瑟 麥克阿瑟 +麦克雷兰 麥克雷蘭 +麦克雷兰说 麥克雷蘭說 +麦克风 麥克風 +麦冬 麥冬 +麦卡托 麥卡托 +麦可杰克森 麥可傑克森 +麦可维克 麥可維克 +麦布莱 麥布萊 +麦当乐 麥當樂 +麦当劳 麥當勞 +麦当劳叔叔 麥當勞叔叔 +麦当娜 麥當娜 +麦托姆 麥托姆 +麦格里 麥格里 +麦特克 麥特克 +麦特戴蒙 麥特戴蒙 +麦秋 麥秋 +麦科里 麥科里 +麦穗 麥穗 +麦穗两歧 麥穗兩歧 +麦考马克 麥考馬克 +麦考马克说 麥考馬克說 +麦达克斯 麥達克斯 +麸曲 麩曲 +麸皮面包 麩皮麪包 +麻了花儿 麻了花兒 +麻做一团 麻做一團 +麻叶皮 麻葉皮 +麻吉死党 麻吉死黨 +麻团 麻團 +麻姑仙坛记 麻姑仙壇記 +麻婆子的裹脚布 麻婆子的裹腳布 +麻布 麻布 +麻布袋 麻布袋 +麻杆 麻桿 +麻栗坡 麻栗坡 +麻栗坡县 麻栗坡縣 +麻油厂 麻油廠 +麻痹不了 麻痹不了 +麻痺不了 麻痺不了 +麻胡 麻胡 +麻药 麻藥 +麻酱面 麻醬麪 +麻醉药 麻醉藥 +麻醉药品 麻醉藥品 +麻醉针 麻醉針 +麻雀在后 麻雀在後 +麻雀虽小五脏俱全 麻雀雖小五臟俱全 +麾之即去招则须来 麾之即去招則須來 +黃鈺筑 黃鈺筑 +黃长发 黃長發 +黄上丰 黃上豐 +黄世杰 黃世傑 +黄东梁 黃東樑 +黄了 黃了 +黄仁杰 黃仁傑 +黄仕杰 黃仕傑 +黄俊杰 黃俊杰 +黄克强 黃克強 +黄凤秋 黃鳳秋 +黄卷 黃卷 +黄卷青灯 黃卷青燈 +黄历 黃曆 +黄发 黃髮 +黄发儿齿 黃髮兒齒 +黄发垂髫 黃髮垂髫 +黄发鲐背 黃髮鮐背 +黄同纸 黃同紙 +黄嘉千 黃嘉千 +黄天荡 黃天蕩 +黄山谷 黃山谷 +黄岩 黃岩 +黄岩区 黃巖區 +黄岩岛 黃巖島 +黄州寒食诗卷 黃州寒食詩卷 +黄干黑瘦 黃乾黑瘦 +黄志中 黃志中 +黄志伟 黃志偉 +黄志勇 黃志勇 +黄志强 黃志強 +黄志成 黃志成 +黄志玮 黃志瑋 +黄志芳 黃志芳 +黄志雄 黃志雄 +黄志鹏 黃志鵬 +黄志龙 黃志龍 +黄旭升 黃旭昇 +黄明志 黃明志 +黄曲毒素 黃麴毒素 +黄曲霉 黃麴黴 +黄曲霉毒素 黃麴黴毒素 +黄曲霉菌 黃麴黴菌 +黄有才 黃有才 +黄梁 黃梁 +黄梁梦 黃梁夢 +黄梁美梦 黃樑美夢 +黄毛团儿 黃毛團兒 +黄沙盖面 黃沙蓋面 +黄河大合唱 黃河大合唱 +黄炎贵胄 黃炎貴胄 +黄珮筑 黃珮筑 +黄白之术 黃白之術 +黄石大峡谷 黃石大峽谷 +黄石瀑布 黃石瀑布 +黄秋庆 黃秋慶 +黄秋燕 黃秋燕 +黄秋生 黃秋生 +黄秋葵 黃秋葵 +黄种 黃種 +黄种人 黃種人 +黄老治术 黃老治術 +黄育杰 黃育杰 +黄色人种 黃色人種 +黄色炸药 黃色炸藥 +黄花后生 黃花後生 +黄英杰 黃英傑 +黄莺出谷 黃鶯出谷 +黄菓树瀑布 黃菓樹瀑布 +黄蜡 黃蠟 +黄蝶翠谷 黃蝶翠谷 +黄表纸 黃表紙 +黄运杰 黃運傑 +黄适卓 黃適卓 +黄道周 黃道周 +黄郁涵 黃郁涵 +黄郁茹 黃郁茹 +黄金入柜 黃金入櫃 +黄金台 黃金臺 +黄金周 黃金週 +黄金存折 黃金存摺 +黄金表 黃金錶 +黄钟 黃鐘 +黄钟大吕 黃鐘大呂 +黄钟毁弃 黃鐘譭棄 +黄钟长弃 黃鐘長棄 +黄钰筑 黃鈺筑 +黄雀在后 黃雀在後 +黄须 黃鬚 +黄鹰抓住了鹞子的脚 黃鷹抓住了鷂子的腳 +黉舍 黌舍 +黍谷生春 黍谷生春 +黎明前的黑暗 黎明前的黑暗 +黎曼几何 黎曼幾何 +黎曼几何学 黎曼幾何學 +黎曼曲面 黎曼曲面 +黎曼面 黎曼面 +黏了 黏了 +黏合 黏合 +黏合剂 黏合劑 +黏合力 黏合力 +黏合成 黏合成 +黑不溜秋 黑不溜秋 +黑了 黑了 +黑云 黑雲 +黑云母 黑雲母 +黑亮发 黑亮髮 +黑克索斯 黑克索斯 +黑加仑 黑加侖 +黑发 黑髮 +黑发人 黑髮人 +黑头虫 黑頭蟲 +黑奴吁天录 黑奴籲天錄 +黑尿症 黑尿症 +黑布 黑布 +黑手党 黑手黨 +黑暗 黑暗 +黑暗世界 黑暗世界 +黑暗大陆 黑暗大陸 +黑暗时代 黑暗時代 +黑暗期 黑暗期 +黑暗面 黑暗面 +黑曜岩 黑曜岩 +黑杰克 黑傑克 +黑松 黑松 +黑松汽水 黑松汽水 +黑松沙士 黑松沙士 +黑板 黑板 +黑板报 黑板報 +黑板擦 黑板擦 +黑板树 黑板樹 +黑气冲天 黑氣沖天 +黑沈沈 黑沈沈 +黑漆一团 黑漆一團 +黑漆板凳 黑漆板凳 +黑烟 黑煙 +黑种 黑種 +黑种人 黑種人 +黑穗病 黑穗病 +黑胡椒 黑胡椒 +黑色棘皮症 黑色棘皮症 +黑色火药 黑色火藥 +黑色系 黑色系 +黑豹党 黑豹黨 +黑醋栗 黑醋栗 +黑里 黑裏 +黑面 黑麪 +黑面包 黑麪包 +黑面琵鹭 黑面琵鷺 +黑须 黑鬚 +黔南布依族苗族自治州 黔南布依族苗族自治州 +黔西南布依族苗族自治州 黔西南布依族苗族自治州 +默克 默克 +默克尔 默克爾 +默哀致意 默哀致意 +默多克 默多克 +默念 默唸 +黛粉叶 黛粉葉 +黝暗 黝暗 +黥布 黥布 +黥面 黥面 +黮暗 黮闇 +黯然欲绝 黯然欲絕 +鼇头独占 鼇頭獨占 +鼎折足 鼎折足 +鼎泰丰 鼎泰豐 +鼎食鸣钟 鼎食鳴鐘 +鼓不打不响钟不撞不鸣 鼓不打不響鐘不撞不鳴 +鼓出 鼓出 +鼓出来 鼓出來 +鼓台 鼓臺 +鼓噪 鼓譟 +鼓子曲 鼓子曲 +鼓板 鼓板 +鼓盆之戚 鼓盆之戚 +鼓腹而游 鼓腹而遊 +鼓荡 鼓盪 +鼓足干劲 鼓足幹勁 +鼓里 鼓裏 +鼓面 鼓面 +鼠得克 鼠得克 +鼠曲草 鼠麴草 +鼠疫杆菌 鼠疫桿菌 +鼠肝虫臂 鼠肝蟲臂 +鼠药 鼠藥 +鼠面人 鼠面人 +鼻中出火 鼻中出火 +鼻准 鼻準 +鼻出血 鼻出血 +鼻咽 鼻咽 +鼻头出火 鼻頭出火 +鼻子上挂鲞鱼 鼻子上掛鯗魚 +鼻子里笑 鼻子裏笑 +鼻无梁柱 鼻無梁柱 +鼻梁 鼻樑 +鼻梁儿 鼻樑兒 +鼻梁骨 鼻樑骨 +鼻烟 鼻菸 +鼻烟壶 鼻菸壺 +鼻烟盒 鼻菸盒 +鼻针疗法 鼻針療法 +齐克果 齊克果 +齐克隆 齊克隆 +齐出 齊出 +齐发 齊發 +齐发式 齊發式 +齐名并价 齊名並價 +齐后破环 齊后破環 +齐头并进 齊頭並進 +齐姜 齊姜 +齐庄 齊莊 +齐心合力 齊心合力 +齐心同力 齊心同力 +齐心并力 齊心併力 +齐打伙儿 齊打夥兒 +齐梁世界 齊梁世界 +齐梁体 齊梁體 +齐民要术 齊民要術 +齐烟九点 齊煙九點 +齐王舍牛 齊王捨牛 +齐足并驰 齊足並馳 +齐足并驱 齊足並驅 +齐驱并驾 齊驅並駕 +齐驱并骤 齊驅並驟 +齧合 齧合 +齧蘗吞针 齧蘗吞針 +齿危发秀 齒危髮秀 +齿发 齒髮 +齿录 齒錄 +齿条千斤顶 齒條千斤頂 +齿胄 齒胄 +齿落发白 齒落髮白 +龄虫 齡蟲 +龙争虎斗 龍爭虎鬥 +龙凤之表 龍鳳之表 +龙利叶 龍利葉 +龙卷 龍捲 +龙卷风 龍捲風 +龙发堂 龍發堂 +龙团 龍團 +龙困浅滩 龍困淺灘 +龙岩 龍巖 +龙岩市 龍巖市 +龙岩村 龍巖村 +龙嵩叶 龍嵩葉 +龙斗虎伤 龍鬥虎傷 +龙无云而不行 龍無雲而不行 +龙游 龍遊 +龙游县 龍遊縣 +龙游浅水 龍游淺水 +龙烟铁矿 龍煙鐵礦 +龙眼干 龍眼乾 +龙种 龍種 +龙胄 龍胄 +龙胜县 龍勝縣 +龙虎并伏 龍虎並伏 +龙虎斗 龍虎鬥 +龙虾面 龍蝦麪 +龙里 龍裏 +龙里县 龍裏縣 +龙钟 龍鍾 +龙门吊 龍門吊 +龙须 龍鬚 +龙须友 龍鬚友 +龙须沟 龍鬚溝 +龙须茶 龍鬚茶 +龙须草 龍鬚草 +龙须菜 龍鬚菜 +龙须面 龍鬚麪 +龙龛手鉴 龍龕手鑑 +龚照胜 龔照勝 +龚胜 龔勝 +龟卜 龜卜 +龟鉴 龜鑑 diff --git a/tools/TSCharacters.txt b/tools/TSCharacters.txt new file mode 100644 index 0000000..9bc2f27 --- /dev/null +++ b/tools/TSCharacters.txt @@ -0,0 +1,4179 @@ +㑮 𫝈 +㑯 㑔 +㑳 㑇 +㑶 㐹 +㒓 𠉂 +㓄 𪠟 +㓨 刾 +㔋 𪟎 +㖮 𪠵 +㗲 𠵾 +㗿 𪡛 +㘉 𠰱 +㘓 𪢌 +㘔 㗷 +㘚 㘎 +㛝 𫝦 +㜄 㚯 +㜏 㛣 +㜐 𫝧 +㜗 𡞋 +㜢 𡞱 +㜷 𡝠 +㞞 𪨊 +㟺 𪩇 +㠏 㟆 +㢗 𪪑 +㢝 𢋈 +㥮 㤘 +㦎 𢛯 +㦛 𢗓 +㦞 𪫷 +㨻 𪮃 +㩋 𪮋 +㩜 㨫 +㩳 㧐 +㩵 擜 +㪎 𪯋 +㯤 𣘐 +㰙 𣗙 +㵗 𣳆 +㵾 𪷍 +㶆 𫞛 +㷍 𤆢 +㷿 𤈷 +㸇 𤎺 +㹽 𫞣 +㺏 𤠋 +㺜 𪺻 +㻶 𪼋 +㿖 𪽮 +㿗 𤻊 +㿧 𤽯 +䀉 𥁢 +䀹 𥅴 +䁪 𥇢 +䁻 䀥 +䂎 𥎝 +䅐 𫀨 +䅳 𫀬 +䆉 𫁂 +䉑 𫁲 +䉙 𥬀 +䉬 𫂈 +䉲 𥮜 +䉶 𫁷 +䊭 𥺅 +䊷 䌶 +䊺 𫄚 +䋃 𫄜 +䋔 𫄞 +䋙 䌺 +䋚 䌻 +䋦 𫄩 +䋹 䌿 +䋻 䌾 +䋼 𫄮 +䋿 𦈓 +䌈 𦈖 +䌋 𦈘 +䌖 𦈜 +䌝 𦈟 +䌟 𦈞 +䌥 𦈠 +䌰 𦈙 +䍤 𫅅 +䍦 䍠 +䍽 𦍠 +䎙 𫅭 +䎱 䎬 +䕤 𫟕 +䕳 𦰴 +䖅 𫟑 +䗅 𫊪 +䗿 𧉞 +䙔 𫋲 +䙡 䙌 +䙱 𧜭 +䚩 𫌯 +䛄 𫍠 +䛳 𫍫 +䜀 䜧 +䜖 𫟢 +䝭 𫎧 +䝻 𧹕 +䝼 䞍 +䞈 𧹑 +䞋 𫎪 +䞓 𫎭 +䟃 𫎺 +䟆 𫎳 +䟐 𫎱 +䠆 𫏃 +䠱 𨅛 +䡐 𫟤 +䡩 𫟥 +䡵 𫟦 +䢨 𨑹 +䤤 𫟺 +䥄 𫠀 +䥇 䦂 +䥗 𫔋 +䥩 𨱖 +䥯 𫔆 +䥱 䥾 +䦘 𨸄 +䦛 䦶 +䦟 䦷 +䦯 𫔵 +䦳 𨷿 +䧢 𨸟 +䪊 𫖅 +䪏 𩏼 +䪗 𩐀 +䪘 𩏿 +䪴 𫖫 +䪾 𫖬 +䫀 𫖱 +䫂 𫖰 +䫟 𫖲 +䫴 𩖗 +䫶 𫖺 +䫻 𫗇 +䫾 𫠈 +䬓 𫗊 +䬘 𩙮 +䬝 𩙯 +䬞 𩙧 +䬧 𫗟 +䭀 𩠇 +䭃 𩠈 +䭑 𫗱 +䭔 𫗰 +䭿 𩧭 +䮄 𫠊 +䮝 𩧰 +䮞 𩨁 +䮠 𩧿 +䮫 𩨇 +䮰 𫘮 +䮳 𩨏 +䮾 𩧪 +䯀 䯅 +䯤 𩩈 +䰾 鲃 +䱀 𫚐 +䱁 𫚏 +䱙 𩾈 +䱧 𫚠 +䱬 𩾊 +䱰 𩾋 +䱷 䲣 +䱸 𫠑 +䱽 䲝 +䲁 鳚 +䲅 𫚜 +䲖 𩾂 +䲘 鳤 +䲰 𪉂 +䳜 𫛬 +䳢 𫛰 +䳤 𫛮 +䳧 𫛺 +䳫 𫛼 +䴉 鹮 +䴋 𫜅 +䴬 𪎈 +䴱 𫜒 +䴴 𪎋 +䴽 𫜔 +䵳 𪑅 +䵴 𫜙 +䶕 𫜨 +䶲 𫜳 +万 万 +丑 丑 +丟 丢 +並 并 +丰 丰 +么 么 +乾 干 乾 +亂 乱 +了 了 +于 于 +云 云 +亙 亘 +亞 亚 +仆 仆 +仇 仇 +价 价 +仿 仿 +伙 伙 +佇 伫 +佈 布 +佔 占 +余 余 +佛 佛 +佣 佣 +併 并 +來 来 +侖 仑 +侶 侣 +侷 局 +俁 俣 +係 系 +俊 俊 +俓 𠇹 +俔 伣 +俠 侠 +俥 伡 +俬 私 +修 修 +倀 伥 +倆 俩 +倈 俫 +倉 仓 +個 个 +們 们 +倖 幸 +借 借 +倫 伦 +倲 㑈 +偉 伟 +偑 㐽 +側 侧 +偵 侦 +偽 伪 +傌 㐷 +傑 杰 +傖 伧 +傘 伞 +備 备 +傢 家 +傭 佣 +傯 偬 +傳 传 +傴 伛 +債 债 +傷 伤 +傾 倾 +僂 偻 +僅 仅 +僉 佥 +僑 侨 +僕 仆 +僞 伪 +僥 侥 +僨 偾 +僱 雇 +僵 僵 +價 价 +儀 仪 +儁 俊 +儂 侬 +億 亿 +儈 侩 +儉 俭 +儎 傤 +儐 傧 +儔 俦 +儕 侪 +儘 尽 侭 +償 偿 +儣 𠆲 +優 优 +儭 𠋆 +儲 储 +儷 俪 +儸 㑩 +儺 傩 +儻 傥 +儼 俨 +兇 凶 +克 克 +兌 兑 +兒 儿 +兗 兖 +党 党 +內 内 +兩 两 +冊 册 +冑 胄 +冪 幂 +冬 冬 +准 准 +凈 净 +凌 凌 +凍 冻 +凙 𪞝 +凜 凛 +几 几 +凱 凯 +凶 凶 +出 出 +划 划 +別 别 +刪 删 +刮 刮 +制 制 +剄 刭 +則 则 +剋 克 +剎 刹 +剗 刬 +剛 刚 +剝 剥 +剮 剐 +剴 剀 +創 创 +剷 铲 +剾 𠛅 +劃 划 㓰 +劇 剧 +劉 刘 +劊 刽 +劌 刿 +劍 剑 +劏 㓥 +劑 剂 +劚 㔉 +勁 劲 +勑 𠡠 +動 动 +務 务 +勛 勋 +勝 胜 +勞 劳 +勢 势 +勣 𪟝 +勩 勚 +勱 劢 +勳 勋 +勵 励 +勸 劝 +勻 匀 +匭 匦 +匯 汇 +匱 匮 +區 区 +千 千 +升 升 +協 协 +卜 卜 +占 占 +卷 卷 +卹 恤 +卻 却 +卽 即 +厂 厂 +厘 厘 +厙 厍 +厠 厕 +厤 历 +厭 厌 +厲 厉 +厴 厣 +參 参 +叄 叁 +叢 丛 +只 只 +台 台 +叶 叶 +吁 吁 +合 合 +吊 吊 +同 同 +后 后 +向 向 +吒 咤 +吳 吴 +吶 呐 +呂 吕 +周 周 +咨 咨 +咸 咸 +咼 呙 +咽 咽 +哄 哄 +員 员 +哯 𠯟 +唄 呗 +唓 𪠳 +唚 吣 +唸 念 +問 问 +啓 启 +啞 哑 +啟 启 +啢 唡 +喂 喂 +喎 㖞 +喚 唤 +喪 丧 +喫 吃 +喬 乔 +單 单 +喲 哟 +嗆 呛 +嗇 啬 +嗊 唝 +嗎 吗 +嗚 呜 +嗩 唢 +嗰 𠮶 +嗶 哔 +嗹 𪡏 +嘆 叹 +嘍 喽 +嘓 啯 +嘔 呕 +嘖 啧 +嘗 尝 +嘜 唛 +嘩 哗 +嘪 𪡃 +嘮 唠 +嘯 啸 +嘰 叽 +嘳 𪡞 +嘵 哓 +嘸 呒 +嘺 𪡀 +嘽 啴 +噁 恶 +噅 𠯠 +噓 嘘 +噚 㖊 +噝 咝 +噞 𪡋 +噠 哒 +噥 哝 +噦 哕 +噪 噪 +噯 嗳 +噲 哙 +噴 喷 +噸 吨 +噹 当 𪠽 +嚀 咛 +嚇 吓 +嚌 哜 +嚐 尝 +嚕 噜 +嚙 啮 +嚛 𪠸 +嚥 咽 +嚦 呖 +嚧 𠰷 +嚨 咙 +嚮 向 +嚲 亸 +嚳 喾 +嚴 严 +嚶 嘤 +嚽 𪢕 +囀 啭 +囁 嗫 +囂 嚣 +囃 𠱞 +囅 冁 +囈 呓 +囉 啰 +囌 苏 +囑 嘱 +囒 𪢠 +回 回 +囪 囱 +困 困 +圇 囵 +國 国 +圍 围 +園 园 +圓 圆 +圖 图 +團 团 +圞 𪢮 +坐 坐 +垵 埯 +埡 垭 +埬 𪣆 +埰 采 +執 执 +堅 坚 +堊 垩 +堖 垴 +堚 𪣒 +堝 埚 +堯 尧 +報 报 +場 场 +塊 块 +塋 茔 +塏 垲 +塒 埘 +塗 涂 +塚 冢 +塢 坞 +塤 埙 +塵 尘 +塹 堑 +塿 𪣻 +墊 垫 +墜 坠 +墮 堕 +墰 坛 +墲 𪢸 +墳 坟 +墶 垯 +墻 墙 +墾 垦 +壇 坛 +壈 𡒄 +壋 垱 +壎 埙 +壓 压 +壗 𡋤 +壘 垒 +壙 圹 +壚 垆 +壜 坛 +壞 坏 +壟 垄 +壠 垅 +壢 坜 +壣 𪤚 +壩 坝 +壪 塆 +壯 壮 +壺 壶 +壼 壸 +壽 寿 +夠 够 +夢 梦 +夥 伙 +夸 夸 +夾 夹 +奐 奂 +奧 奥 +奩 奁 +奪 夺 +奬 奖 +奮 奋 +奸 奸 +奼 姹 +妝 妆 +姍 姗 +姜 姜 +姦 奸 +娘 娘 +娛 娱 +婁 娄 +婡 𫝫 +婦 妇 +婭 娅 +媈 𫝨 +媧 娲 +媯 妫 +媰 㛀 +媼 媪 +媽 妈 +嫋 袅 +嫗 妪 +嫵 妩 +嫺 娴 +嫻 娴 +嫿 婳 +嬀 妫 +嬃 媭 +嬇 𫝬 +嬈 娆 +嬋 婵 +嬌 娇 +嬙 嫱 +嬡 嫒 +嬣 𪥰 +嬤 嬷 +嬦 𫝩 +嬪 嫔 +嬰 婴 +嬸 婶 +嬻 𪥿 +孃 娘 +孄 𫝮 +孆 𫝭 +孇 𪥫 +孋 㛤 +孌 娈 +孎 𡠟 +孫 孙 +學 学 +孻 𡥧 +孾 𪧀 +孿 孪 +宮 宫 +家 家 +寀 采 +寠 𪧘 +寢 寝 +實 实 +寧 宁 +審 审 +寫 写 +寬 宽 +寵 宠 +寶 宝 +將 将 +專 专 +尋 寻 +對 对 +導 导 +尷 尴 +尸 尸 +局 局 +屆 届 +屍 尸 +屓 屃 +屜 屉 +屢 屡 +層 层 +屨 屦 +屩 𪨗 +屬 属 +岡 冈 +岩 岩 +峯 峰 +峴 岘 +島 岛 +峽 峡 +崍 崃 +崑 昆 +崗 岗 +崙 仑 𪨧 +崢 峥 +崬 岽 +嵐 岚 +嵗 岁 +嵼 𡶴 +嵾 㟥 +嶁 嵝 +嶄 崭 +嶇 岖 +嶈 𡺃 +嶔 嵚 +嶗 崂 +嶘 𡺄 +嶠 峤 +嶢 峣 +嶧 峄 +嶨 峃 +嶮 崄 +嶴 岙 +嶸 嵘 +嶹 𫝵 +嶺 岭 +嶼 屿 +嶽 岳 +巊 𪩎 +巋 岿 +巒 峦 +巔 巅 +巖 岩 +巗 𪨷 +巘 𪩘 +巨 巨 +巰 巯 +巹 卺 +布 布 +帘 帘 +帥 帅 +師 师 +席 席 +帳 帐 +帶 带 +幀 帧 +幃 帏 +幓 㡎 +幗 帼 +幘 帻 +幝 𪩷 +幟 帜 +幣 币 +幩 𪩸 +幫 帮 +幬 帱 +干 干 +幸 幸 +幹 干 +幺 幺 +幾 几 +广 广 +座 座 +庫 库 +庵 庵 +廁 厕 +廂 厢 +廄 厩 +廈 厦 +廎 庼 +廕 荫 +廚 厨 +廝 厮 +廟 庙 +廠 厂 +廡 庑 +廢 废 +廣 广 +廧 𪪞 +廩 廪 +廬 庐 𪪏 +廳 厅 +弒 弑 +弔 吊 +弦 弦 +弳 弪 +張 张 +強 强 +彃 𪪼 +彆 别 +彈 弹 +彌 弥 +彎 弯 +彔 录 +彙 汇 +彞 彝 +彠 彟 +彥 彦 +彩 彩 +彫 雕 +彲 彨 +彷 彷 仿 +彿 佛 +征 征 +後 后 +徑 径 +從 从 +徠 徕 +御 御 +復 复 +徵 征 徵 +徹 彻 +徿 𪫌 +志 志 +念 念 +恆 恒 +恥 耻 +悅 悦 +悞 悮 +悵 怅 +悶 闷 +悽 凄 +惡 恶 +惱 恼 +惲 恽 +惻 恻 +愈 愈 +愛 爱 +愜 惬 +愨 悫 +愴 怆 +愷 恺 +愻 𢙏 +愾 忾 +愿 愿 +慄 栗 +態 态 +慍 愠 +慘 惨 +慚 惭 +慟 恸 +慣 惯 +慤 悫 +慪 怄 +慫 怂 +慮 虑 +慳 悭 +慶 庆 +慺 㥪 +慼 戚 +慾 欲 +憂 忧 +憊 惫 +憐 怜 +憑 凭 +憒 愦 +憖 慭 +憚 惮 +憢 𢙒 +憤 愤 +憫 悯 +憮 怃 +憲 宪 +憶 忆 +憸 𪫺 +憹 𢙐 +懀 𢙓 +懇 恳 +應 应 +懌 怿 +懍 懔 +懎 𢠁 +懞 蒙 +懟 怼 +懣 懑 +懤 㤽 +懨 恹 +懲 惩 +懶 懒 +懷 怀 +懸 悬 +懺 忏 +懼 惧 +懾 慑 +戀 恋 +戇 戆 +戔 戋 +戚 戚 +戧 戗 +戩 戬 +戰 战 𢧐 +戱 戯 +戲 戏 +戶 户 +才 才 +扎 扎 +托 托 +扣 扣 +折 折 +拋 抛 +拐 拐 +拚 拚 +挂 挂 +挨 挨 +挩 捝 +挱 挲 +挽 挽 +挾 挟 +捨 舍 +捫 扪 +据 据 +捱 挨 +捲 卷 +掃 扫 +掄 抡 +掆 㧏 +掗 挜 +掙 挣 +掚 𪭵 +掛 挂 +採 采 +揀 拣 +揚 扬 +換 换 +揮 挥 +揯 搄 +損 损 +搖 摇 +搗 捣 +搜 搜 +搵 揾 +搶 抢 +摋 𢫬 +摐 𪭢 +摑 掴 +摜 掼 +摟 搂 +摯 挚 +摳 抠 +摶 抟 +摺 折 +摻 掺 +撈 捞 +撊 𪭾 +撏 挦 +撐 撑 +撓 挠 +撝 㧑 +撟 挢 +撣 掸 +撥 拨 +撧 𪮖 +撫 抚 +撲 扑 +撳 揿 +撻 挞 +撾 挝 +撿 捡 +擁 拥 +擄 掳 +擇 择 +擊 击 +擋 挡 +擓 㧟 +擔 担 +據 据 +擟 𪭧 +擠 挤 +擡 抬 +擣 捣 𢭏 +擫 𢬍 +擬 拟 +擯 摈 +擰 拧 +擱 搁 +擲 掷 +擴 扩 +擷 撷 +擺 摆 +擻 擞 +擼 撸 +擽 㧰 +擾 扰 +攄 摅 +攆 撵 +攋 𪮶 +攏 拢 +攔 拦 +攖 撄 +攙 搀 +攛 撺 +攜 携 +攝 摄 +攢 攒 +攣 挛 +攤 摊 +攪 搅 +攬 揽 +敎 教 +敓 敚 +敗 败 +敘 叙 +敵 敌 +數 数 +斂 敛 +斃 毙 +斅 𢽾 +斆 敩 +斕 斓 +斗 斗 +斬 斩 +斷 断 +斸 𣃁 +於 于 於 +旂 旗 +旣 既 +昆 昆 +昇 升 +時 时 +晉 晋 +晝 昼 +暈 晕 +暉 晖 +暗 暗 +暘 旸 +暢 畅 +暫 暂 +曄 晔 +曆 历 +曇 昙 +曉 晓 +曊 𪰶 +曏 向 +曖 暧 +曠 旷 +曥 𣆐 +曨 昽 +曬 晒 +曲 曲 +書 书 +會 会 +朥 𦛨 +朧 胧 +朮 术 +朱 朱 +朴 朴 +杆 杆 +杠 杠 +杯 杯 +杰 杰 +東 东 +杴 锨 +松 松 +板 板 +极 极 +枴 拐 +柜 柜 +柵 栅 +柺 拐 +査 查 +栗 栗 +核 核 +桱 𣐕 +桿 杆 +梁 梁 +梔 栀 +梖 𪱷 +梘 枧 +條 条 +梟 枭 +梲 棁 +棄 弃 +棊 棋 +棖 枨 +棗 枣 +棟 栋 +棡 㭎 +棧 栈 +棱 棱 +棲 栖 +棶 梾 +椏 桠 +椲 㭏 +楇 𣒌 +楊 杨 +楓 枫 +楨 桢 +業 业 +極 极 +榘 矩 +榦 干 +榪 杩 +榮 荣 +榲 榅 +榿 桤 +構 构 +槍 枪 +槓 杠 +槤 梿 +槧 椠 +槨 椁 +槫 𣏢 +槮 椮 +槳 桨 +槶 椢 +槼 椝 +樁 桩 +樂 乐 +樅 枞 +樑 梁 +樓 楼 +標 标 +樞 枢 +樠 𣗊 +樢 㭤 +樣 样 +樤 𣔌 +樧 榝 +樫 㭴 +樳 桪 +樸 朴 +樹 树 +樺 桦 +樿 椫 +橈 桡 +橋 桥 +機 机 +橢 椭 +橫 横 +橯 𣓿 +檁 檩 +檉 柽 +檔 档 +檜 桧 +檟 槚 +檢 检 +檣 樯 +檭 𣘴 +檮 梼 +檯 台 +檳 槟 +檵 𪲛 +檸 柠 +檻 槛 +檾 𦼖 +櫃 柜 +櫅 𪲎 +櫓 橹 +櫚 榈 +櫛 栉 +櫝 椟 +櫞 橼 +櫟 栎 +櫠 𪲮 +櫥 橱 +櫧 槠 +櫨 栌 +櫪 枥 +櫫 橥 +櫬 榇 +櫱 蘖 +櫳 栊 +櫸 榉 +櫺 棂 +櫻 樱 +欄 栏 +欅 榉 +欇 𪳍 +權 权 +欍 𣐤 +欏 椤 +欐 𪲔 +欑 𪴙 +欒 栾 +欓 𣗋 +欖 榄 +欘 𣚚 +欞 棂 +欲 欲 +欽 钦 +歎 叹 +歐 欧 +歟 欤 +歡 欢 +歲 岁 +歷 历 +歸 归 +歿 殁 +殘 残 +殞 殒 +殢 𣨼 +殤 殇 +殨 㱮 +殫 殚 +殭 僵 +殮 殓 +殯 殡 +殰 㱩 +殲 歼 +殺 杀 +殻 壳 +殼 壳 +毀 毁 +毆 殴 +毊 𪵑 +毿 毵 +氂 牦 +氈 毡 +氌 氇 +氣 气 +氫 氢 +氬 氩 +氭 𣱝 +氳 氲 +氾 泛 +汎 泛 +汙 污 +決 决 +沈 沈 沉 +沒 没 +沖 冲 +況 况 +泛 泛 +泝 溯 +注 注 +洩 泄 +洶 汹 +浹 浃 +涂 涂 +涇 泾 +涌 涌 +涗 涚 +涼 凉 +淀 淀 +淒 凄 +淚 泪 +淥 渌 +淨 净 +淩 凌 +淪 沦 +淵 渊 +淶 涞 +淺 浅 +渙 涣 +減 减 +渢 沨 +渦 涡 +測 测 +游 游 +渾 浑 +湊 凑 +湋 𣲗 +湞 浈 +湧 涌 +湯 汤 +溈 沩 +準 准 +溝 沟 +溡 𪶄 +溫 温 +溮 浉 +溳 涢 +溼 湿 +滄 沧 +滅 灭 +滌 涤 +滎 荥 +滙 汇 +滬 沪 +滯 滞 +滲 渗 +滷 卤 +滸 浒 +滻 浐 +滾 滚 +滿 满 +漁 渔 +漊 溇 +漓 漓 +漚 沤 +漢 汉 +漣 涟 +漬 渍 +漲 涨 +漵 溆 +漸 渐 +漿 浆 +潁 颍 +潑 泼 +潔 洁 +潕 𣲘 +潙 沩 +潚 㴋 +潛 潜 +潣 𫞗 +潤 润 +潯 浔 +潰 溃 +潷 滗 +潿 涠 +澀 涩 +澅 𣶩 +澆 浇 +澇 涝 +澐 沄 +澗 涧 +澠 渑 +澤 泽 +澦 滪 +澩 泶 +澬 𫞚 +澮 浍 +澱 淀 +澾 㳠 +濁 浊 +濃 浓 +濄 㳡 +濆 𣸣 +濕 湿 +濘 泞 +濚 溁 +濛 蒙 +濜 浕 +濟 济 +濤 涛 +濧 㳔 +濫 滥 +濰 潍 +濱 滨 +濺 溅 +濼 泺 +濾 滤 +濿 𪵱 +瀂 澛 +瀃 𣽷 +瀅 滢 +瀆 渎 +瀇 㲿 +瀉 泻 +瀋 沈 渖 +瀏 浏 +瀕 濒 +瀘 泸 +瀝 沥 +瀟 潇 +瀠 潆 +瀦 潴 +瀧 泷 +瀨 濑 +瀰 弥 㳽 +瀲 潋 +瀾 澜 +灃 沣 +灄 滠 +灍 𫞝 +灑 洒 +灒 𪷽 +灕 漓 +灘 滩 +灙 𣺼 +灝 灏 +灡 㳕 +灣 湾 +灤 滦 +灧 滟 +灩 滟 +災 灾 +為 为 +烏 乌 +烴 烃 +無 无 +煇 𪸩 +煉 炼 +煒 炜 +煙 烟 +煢 茕 +煥 焕 +煩 烦 +煬 炀 +煱 㶽 +熂 𪸕 +熅 煴 +熉 𤈶 +熌 𤇄 +熏 熏 +熒 荧 +熓 𤆡 +熗 炝 +熚 𤇹 +熡 𤋏 +熱 热 +熲 颎 +熾 炽 +燁 烨 +燈 灯 +燉 炖 +燒 烧 +燙 烫 +燜 焖 +營 营 +燦 灿 +燬 毁 +燭 烛 +燴 烩 +燶 㶶 +燻 熏 +燼 烬 +燾 焘 +爃 𫞡 +爄 𤇃 +爇 𦶟 +爍 烁 +爐 炉 +爖 𤇭 +爛 烂 +爥 𪹳 +爧 𫞠 +爭 争 +爲 为 +爺 爷 +爾 尔 +牀 床 +牆 墙 +牘 牍 +牴 牴 抵 +牽 牵 +犖 荦 +犛 牦 +犞 𪺭 +犢 犊 +犧 牺 +狀 状 +狹 狭 +狽 狈 +猌 𪺽 +猙 狰 +猶 犹 +猻 狲 +獁 犸 +獃 呆 +獄 狱 +獅 狮 +獊 𪺷 +獎 奖 +獨 独 +獩 𤞃 +獪 狯 +獫 猃 +獮 狝 +獰 狞 +獱 㺍 +獲 获 +獵 猎 +獷 犷 +獸 兽 +獺 獭 +獻 献 +獼 猕 +玀 猡 +玁 𤞤 +珼 𫞥 +現 现 +琱 雕 +琺 珐 +琿 珲 +瑋 玮 +瑒 玚 +瑣 琐 +瑤 瑶 +瑩 莹 +瑪 玛 +瑲 玱 +瑻 𪻲 +瑽 𪻐 +璉 琏 +璊 𫞩 +璝 𪻺 +璡 琎 +璣 玑 +璦 瑷 +璫 珰 +璯 㻅 +環 环 +璵 玙 +璸 瑸 +璼 𫞨 +璽 玺 +璾 𫞦 +瓄 𪻨 +瓊 琼 +瓏 珑 +瓔 璎 +瓕 𤦀 +瓚 瓒 +瓛 𤩽 +瓮 瓮 +甌 瓯 +甕 瓮 +產 产 +産 产 +甦 苏 +甯 宁 +畝 亩 +畢 毕 +畫 画 划 +異 异 +畵 画 +當 当 +畼 𪽈 +疇 畴 +疊 叠 +症 症 +痙 痉 +痠 酸 +痮 𪽪 +痾 疴 +瘂 痖 +瘋 疯 +瘍 疡 +瘓 痪 +瘞 瘗 +瘡 疮 +瘧 疟 +瘮 瘆 +瘱 𪽷 +瘲 疭 +瘺 瘘 +瘻 瘘 +療 疗 +癆 痨 +癇 痫 +癉 瘅 +癐 𤶊 +癒 愈 +癘 疠 +癟 瘪 +癡 痴 +癢 痒 +癤 疖 +癥 症 +癧 疬 +癩 癞 +癬 癣 +癭 瘿 +癮 瘾 +癰 痈 +癱 瘫 +癲 癫 +發 发 +皁 皂 +皚 皑 +皟 𤾀 +皰 疱 +皸 皲 +皺 皱 +盃 杯 +盜 盗 +盞 盏 +盡 尽 +監 监 +盤 盘 +盧 卢 +盨 𪾔 +盪 荡 +眝 𪾣 +眞 真 +眥 眦 +眾 众 +睍 𪾢 +睏 困 +睜 睁 +睞 睐 +睪 睾 睪 +瞘 眍 +瞜 䁖 +瞞 瞒 +瞤 𥆧 +瞭 瞭 了 +瞶 瞆 +瞼 睑 +矇 蒙 +矉 𪾸 +矑 𪾦 +矓 眬 +矚 瞩 +矩 矩 +矯 矫 +硃 朱 +硜 硁 +硤 硖 +硨 砗 +确 确 +硯 砚 +碕 埼 +碙 𥐻 +碩 硕 +碭 砀 +碸 砜 +確 确 +碼 码 +碽 䂵 +磑 硙 +磚 砖 +磠 硵 +磣 碜 +磧 碛 +磯 矶 +磽 硗 +磾 䃅 +礄 硚 +礆 硷 +礎 础 +礒 𥐟 +礙 碍 +礦 矿 +礪 砺 +礫 砾 +礬 矾 +礮 𪿫 +礱 砻 +祇 祇 只 +祕 秘 +祘 祘 +祿 禄 +禍 祸 +禎 祯 +禕 祎 +禡 祃 +禦 御 +禪 禅 +禮 礼 +禰 祢 +禱 祷 +禿 秃 +私 私 +秈 籼 +秋 秋 +种 种 +稅 税 +稈 秆 +稏 䅉 +稜 棱 +稟 禀 +種 种 +稱 称 +穀 谷 +穇 䅟 +穌 稣 +積 积 +穎 颖 +穗 穗 +穠 秾 +穡 穑 +穢 秽 +穩 稳 +穫 获 +穭 穞 +窩 窝 +窪 洼 +窮 穷 +窯 窑 +窵 窎 +窶 窭 +窺 窥 +竄 窜 +竅 窍 +竇 窦 +竈 灶 +竊 窃 +竚 𥩟 +竪 竖 +竱 𫁟 +競 竞 +筆 笔 +筍 笋 +筑 筑 +筧 笕 +筴 䇲 +箇 个 +箋 笺 +箏 筝 +節 节 +範 范 +築 筑 +篋 箧 +篔 筼 +篘 𥬠 +篠 筿 +篤 笃 +篩 筛 +篳 筚 +篸 𥮾 +簀 箦 +簂 𫂆 +簍 篓 +簑 蓑 +簞 箪 +簡 简 +簢 𫂃 +簣 篑 +簫 箫 +簹 筜 +簽 签 +簾 帘 +籃 篮 +籅 𥫣 +籋 𥬞 +籌 筹 +籔 䉤 +籙 箓 +籛 篯 +籜 箨 +籟 籁 +籠 笼 +籤 签 +籩 笾 +籪 簖 +籬 篱 +籮 箩 +籲 吁 +粵 粤 +糉 粽 +糝 糁 +糞 粪 +糧 粮 +糰 团 +糲 粝 +糴 籴 +糶 粜 +糹 纟 +糺 𫄙 +系 系 +糾 纠 +紀 纪 +紂 纣 +約 约 +紅 红 +紆 纡 +紇 纥 +紈 纨 +紉 纫 +紋 纹 +納 纳 +紐 纽 +紓 纾 +純 纯 +紕 纰 +紖 纼 +紗 纱 +紘 纮 +紙 纸 +級 级 +紛 纷 +紜 纭 +紝 纴 +紟 𫄛 +紡 纺 +紬 䌷 +紮 扎 +累 累 +細 细 +紱 绂 +紲 绁 +紳 绅 +紵 纻 +紹 绍 +紺 绀 +紼 绋 +紿 绐 +絀 绌 +絁 𫄟 +終 终 +絃 弦 +組 组 +絅 䌹 +絆 绊 +絍 𫟃 +絎 绗 +結 结 +絕 绝 +絙 𫄠 +絛 绦 +絝 绔 +絞 绞 +絡 络 +絢 绚 +絥 𫄢 +給 给 +絧 𫄡 +絨 绒 +絰 绖 +統 统 +絲 丝 +絳 绛 +絶 绝 +絹 绢 +絺 𫄨 +綀 𦈌 +綁 绑 +綃 绡 +綆 绠 +綇 𦈋 +綈 绨 +綉 绣 +綋 𫟄 +綌 绤 +綏 绥 +綐 䌼 +綑 捆 +經 经 +綖 𫄧 +綜 综 +綞 缍 +綟 𫄫 +綠 绿 +綡 𫟅 +綢 绸 +綣 绻 +綫 线 +綬 绶 +維 维 +綯 绹 +綰 绾 +綱 纲 +網 网 +綳 绷 +綴 缀 +綵 彩 䌽 +綸 纶 +綹 绺 +綺 绮 +綻 绽 +綽 绰 +綾 绫 +綿 绵 +緄 绲 +緇 缁 +緊 紧 +緋 绯 +緍 𦈏 +緑 绿 +緒 绪 +緓 绬 +緔 绱 +緗 缃 +緘 缄 +緙 缂 +線 线 缐 +緝 缉 +緞 缎 +緟 𫟆 +締 缔 +緡 缗 +緣 缘 +緤 𫄬 +緦 缌 +編 编 +緩 缓 +緬 缅 +緮 𫄭 +緯 纬 +緰 𦈕 +緱 缑 +緲 缈 +練 练 +緶 缏 +緷 𦈉 +緸 𦈑 +緹 缇 +緻 致 +緼 缊 +縈 萦 +縉 缙 +縊 缢 +縋 缒 +縍 𫄰 +縎 𦈔 +縐 绉 +縑 缣 +縕 缊 +縗 缞 +縛 缚 +縝 缜 +縞 缟 +縟 缛 +縣 县 +縧 绦 +縫 缝 +縬 𦈚 +縭 缡 +縮 缩 +縰 𫄳 +縱 纵 +縲 缧 +縳 䌸 +縴 纤 +縵 缦 +縶 絷 +縷 缕 +縸 𫄲 +縹 缥 +縺 𦈐 +總 总 +績 绩 +繂 𫄴 +繃 绷 +繅 缫 +繆 缪 +繈 𫄶 +繏 𦈝 +繐 穗 +繒 缯 +繓 𦈛 +織 织 +繕 缮 +繚 缭 +繞 绕 +繟 𦈎 +繡 绣 +繢 缋 +繨 𫄤 +繩 绳 +繪 绘 +繫 系 +繬 𫄱 +繭 茧 +繮 缰 +繯 缳 +繰 缲 +繳 缴 +繶 𫄷 +繷 𫄣 +繸 䍁 +繹 绎 +繻 𦈡 +繼 继 +繽 缤 +繾 缱 +繿 䍀 +纁 𫄸 +纇 颣 +纈 缬 +纊 纩 +續 续 +纍 累 +纏 缠 +纓 缨 +纔 才 +纖 纤 +纗 𫄹 +纘 缵 +纚 𫄥 +纜 缆 +缽 钵 +罃 䓨 +罈 坛 +罌 罂 +罎 坛 +罰 罚 +罵 骂 +罷 罢 +羅 罗 +羆 罴 +羈 羁 +羋 芈 +羣 群 +羥 羟 +羨 羡 +義 义 +羵 𫅗 +羶 膻 +習 习 +翬 翚 +翹 翘 +翽 翙 +耬 耧 +耮 耢 +聖 圣 +聞 闻 +聯 联 +聰 聪 +聲 声 +聳 耸 +聵 聩 +聶 聂 +職 职 +聹 聍 +聻 𫆏 +聽 听 +聾 聋 +肅 肃 +肴 肴 +胜 胜 +胡 胡 +脅 胁 +脈 脉 +脛 胫 +脣 唇 +脥 𣍰 +脩 修 +脫 脱 +脹 胀 +腊 腊 +腌 腌 +腎 肾 +腖 胨 +腡 脶 +腦 脑 +腪 𣍯 +腫 肿 +腳 脚 +腸 肠 +膃 腽 +膕 腘 +膚 肤 +膞 䏝 +膠 胶 +膢 𦝼 +膩 腻 +膹 𪱥 +膽 胆 +膾 脍 +膿 脓 +臉 脸 +臍 脐 +臏 膑 +臗 𣎑 +臘 腊 +臚 胪 +臟 脏 +臠 脔 +臢 臜 +臥 卧 +臨 临 +致 致 +臺 台 +與 与 +興 兴 +舉 举 +舊 旧 +舍 舍 +舘 馆 +艙 舱 +艣 𫇛 +艤 舣 +艦 舰 +艫 舻 +艱 艰 +艷 艳 +芻 刍 +苧 苎 +苹 苹 +范 范 +茲 兹 +荊 荆 +荐 荐 +莊 庄 +莖 茎 +莢 荚 +莧 苋 +菕 芲 +華 华 +菴 庵 +菸 烟 +萇 苌 +萊 莱 +萬 万 +萴 荝 +萵 莴 +葉 叶 +葒 荭 +著 著 +葝 𫈎 +葤 荮 +葦 苇 +葯 药 +葷 荤 +蒍 𫇭 +蒐 搜 +蒓 莼 +蒔 莳 +蒕 蒀 +蒙 蒙 +蒞 莅 +蒭 𫇴 +蒼 苍 +蓀 荪 +蓆 席 +蓋 盖 +蓧 𦰏 +蓮 莲 +蓯 苁 +蓴 莼 +蓽 荜 +蔑 蔑 +蔔 卜 +蔘 参 +蔞 蒌 +蔣 蒋 +蔥 葱 +蔦 茑 +蔭 荫 +蔯 𫈟 +蔿 𫇭 +蕁 荨 +蕆 蒇 +蕎 荞 +蕒 荬 +蕓 芸 +蕕 莸 +蕘 荛 +蕝 𫈵 +蕢 蒉 +蕩 荡 +蕪 芜 +蕭 萧 +蕳 𫈉 +蕷 蓣 +蕽 𫇽 +薀 蕰 +薆 𫉁 +薈 荟 +薊 蓟 +薌 芗 +薑 姜 +薔 蔷 +薘 荙 +薟 莶 +薦 荐 +薩 萨 +薰 薰 熏 +薳 䓕 +薴 苧 +薵 䓓 +薹 苔 薹 +薺 荠 +藉 藉 借 +藍 蓝 +藎 荩 +藝 艺 +藥 药 +藪 薮 +藭 䓖 +藴 蕴 +藶 苈 +藷 𫉄 +藹 蔼 +藺 蔺 +蘀 萚 +蘄 蕲 +蘆 芦 +蘇 苏 +蘊 蕴 +蘋 苹 蘋 +蘚 藓 +蘞 蔹 +蘟 𦻕 +蘢 茏 +蘭 兰 +蘺 蓠 +蘿 萝 +虆 蔂 +處 处 +虛 虚 +虜 虏 +號 号 +虧 亏 +虫 虫 +虯 虬 +蛺 蛱 +蛻 蜕 +蜆 蚬 +蜡 蜡 +蝕 蚀 +蝟 猬 +蝦 虾 +蝨 虱 +蝸 蜗 +螄 蛳 +螞 蚂 +螢 萤 +螮 䗖 +螻 蝼 +螿 螀 +蟂 𫋇 +蟄 蛰 +蟈 蝈 +蟎 螨 +蟘 𫋌 +蟜 𫊸 +蟣 虮 +蟬 蝉 +蟯 蛲 +蟲 虫 +蟳 𫊻 +蟶 蛏 +蟻 蚁 +蠀 𧏗 +蠁 蚃 +蠅 蝇 +蠆 虿 +蠍 蝎 +蠐 蛴 +蠑 蝾 +蠔 蚝 +蠙 𧏖 +蠟 蜡 +蠣 蛎 +蠦 𫊮 +蠨 蟏 +蠱 蛊 +蠶 蚕 +蠻 蛮 +蠾 𧑏 +衆 众 +衊 蔑 +術 术 +衕 同 +衚 胡 +衛 卫 +衝 冲 +表 表 +衹 衹 只 +袞 衮 +裊 袅 +裏 里 +補 补 +裝 装 +裡 里 +製 制 +複 复 +褌 裈 +褘 袆 +褲 裤 +褳 裢 +褸 褛 +褻 亵 +襀 𫌀 +襆 幞 +襇 裥 +襉 裥 +襏 袯 +襓 𫋹 +襖 袄 +襗 𫋷 +襘 𫋻 +襝 裣 +襠 裆 +襤 褴 +襪 袜 +襬 摆 䙓 +襯 衬 +襰 𧝝 +襲 袭 +襴 襕 +襵 𫌇 +覆 覆 复 +覈 核 +見 见 +覎 觃 +規 规 +覓 觅 +視 视 +覘 觇 +覛 𫌪 +覡 觋 +覥 觍 +覦 觎 +親 亲 +覬 觊 +覯 觏 +覲 觐 +覷 觑 +覹 𫌭 +覺 觉 +覼 𫌨 +覽 览 +覿 觌 +觀 观 +觴 觞 +觶 觯 +觸 触 +訁 讠 +訂 订 +訃 讣 +計 计 +訊 讯 +訌 讧 +討 讨 +訐 讦 +訑 𫍙 +訒 讱 +訓 训 +訕 讪 +訖 讫 +託 托 讬 +記 记 +訛 讹 +訜 𫍛 +訝 讶 +訞 𫍚 +訟 讼 +訢 䜣 +訣 诀 +訥 讷 +訨 𫟞 +訩 讻 +訪 访 +設 设 +許 许 +訴 诉 +訶 诃 +診 诊 +註 注 +証 证 +詀 𧮪 +詁 诂 +詆 诋 +詊 𫟟 +詎 讵 +詐 诈 +詑 𫍡 +詒 诒 +詓 𫍜 +詔 诏 +評 评 +詖 诐 +詗 诇 +詘 诎 +詛 诅 +詞 词 +詠 咏 +詡 诩 +詢 询 +詣 诣 +試 试 +詩 诗 +詫 诧 +詬 诟 +詭 诡 +詮 诠 +詰 诘 +話 话 +該 该 +詳 详 +詵 诜 +詷 𫍣 +詼 诙 +詿 诖 +誂 𫍥 +誄 诔 +誅 诛 +誆 诓 +誇 夸 +誋 𫍪 +誌 志 +認 认 +誑 诳 +誒 诶 +誕 诞 +誘 诱 +誚 诮 +語 语 +誠 诚 +誡 诫 +誣 诬 +誤 误 +誥 诰 +誦 诵 +誨 诲 +說 说 +誫 𫍨 +説 说 +誰 谁 +課 课 +誳 𫍮 +誴 𫟡 +誶 谇 +誷 𫍬 +誹 诽 +誺 𫍧 +誼 谊 +誾 訚 +調 调 +諂 谄 +諄 谆 +談 谈 +諉 诿 +請 请 +諍 诤 +諏 诹 +諑 诼 +諒 谅 +論 论 +諗 谂 +諛 谀 +諜 谍 +諝 谞 +諞 谝 +諡 谥 +諢 诨 +諣 𫍩 +諤 谔 +諥 𫍳 +諦 谛 +諧 谐 +諫 谏 𫍝 +諭 谕 +諮 咨 谘 +諯 𫍱 +諰 𫍰 +諱 讳 +諳 谙 +諴 𫍯 +諶 谌 +諷 讽 +諸 诸 +諺 谚 +諼 谖 +諾 诺 +謀 谋 +謁 谒 +謂 谓 +謄 誊 +謅 诌 +謆 𫍸 +謉 𫍷 +謊 谎 +謎 谜 +謏 𫍲 +謐 谧 +謔 谑 +謖 谡 +謗 谤 +謙 谦 +謚 谥 +講 讲 +謝 谢 +謠 谣 +謡 谣 +謨 谟 +謫 谪 +謬 谬 +謭 谫 +謯 𫍹 +謱 𫍴 +謳 讴 +謸 𫍵 +謹 谨 +謾 谩 +譁 哗 +譂 𫟠 +譅 䜧 +譆 𫍻 +證 证 +譊 𫍢 +譎 谲 +譏 讥 +譑 𫍤 +譖 谮 +識 识 +譙 谯 +譚 谭 +譜 谱 +譞 𫍽 +譟 噪 +譨 𫍦 +譫 谵 +譭 毁 +譯 译 +議 议 +譴 谴 +護 护 +譸 诪 +譽 誉 +譾 谫 𫍿 +讀 读 +讅 谉 +變 变 +讋 詟 +讌 䜩 +讎 雠 +讒 谗 +讓 让 +讕 谰 +讖 谶 +讚 赞 +讜 谠 +讞 谳 +谷 谷 +豈 岂 +豎 竖 +豐 丰 +豔 艳 +豬 猪 +豵 𫎆 +豶 豮 +貓 猫 +貗 𫎌 +貙 䝙 +貝 贝 +貞 贞 +貟 贠 +負 负 +財 财 +貢 贡 +貧 贫 +貨 货 +販 贩 +貪 贪 +貫 贯 +責 责 +貯 贮 +貰 贳 +貲 赀 +貳 贰 +貴 贵 +貶 贬 +買 买 𧹒 +貸 贷 +貺 贶 +費 费 +貼 贴 +貽 贻 +貿 贸 +賀 贺 +賁 贲 +賂 赂 +賃 赁 +賄 贿 +賅 赅 +資 资 +賈 贾 +賊 贼 +賑 赈 +賒 赊 +賓 宾 +賕 赇 +賙 赒 +賚 赉 +賜 赐 +賝 𫎩 +賞 赏 +賟 𧹖 +賠 赔 +賡 赓 +賢 贤 +賣 卖 +賤 贱 +賦 赋 +賧 赕 +質 质 +賫 赍 +賬 账 +賭 赌 +賰 䞐 +賴 赖 +賵 赗 +賺 赚 +賻 赙 +購 购 +賽 赛 +賾 赜 +贃 𧹗 +贄 贽 +贅 赘 +贇 赟 +贈 赠 +贉 𫎫 +贊 赞 +贋 赝 +贍 赡 +贏 赢 +贐 赆 +贑 𫎬 +贓 赃 +贔 赑 +贖 赎 +贗 赝 +贚 𫎦 +贛 赣 +贜 赃 +赬 赪 +趕 赶 +趙 赵 +趨 趋 +趲 趱 +跡 迹 +踊 踊 +踐 践 +踰 逾 +踴 踊 +蹌 跄 +蹔 𫏐 +蹕 跸 +蹟 迹 +蹣 蹒 +蹤 踪 +蹳 𫏆 +蹺 跷 +蹻 𫏋 +躂 跶 +躉 趸 +躊 踌 +躋 跻 +躍 跃 +躎 䟢 +躑 踯 +躒 跞 +躓 踬 +躕 蹰 +躘 𨀁 +躚 跹 +躝 𨅬 +躡 蹑 +躥 蹿 +躦 躜 +躪 躏 +軀 躯 +軉 𨉗 +車 车 +軋 轧 +軌 轨 +軍 军 +軏 𫐄 +軑 轪 +軒 轩 +軔 轫 +軕 𫐅 +軗 𨐅 +軛 轭 +軜 𫐇 +軟 软 +軤 轷 +軨 𫐉 +軫 轸 +軬 𫐊 +軲 轱 +軷 𫐈 +軸 轴 +軹 轵 +軺 轺 +軻 轲 +軼 轶 +軾 轼 +軿 𫐌 +較 较 +輄 𨐈 +輅 辂 +輇 辁 +輈 辀 +載 载 +輊 轾 +輋 𪨶 +輒 辄 +輓 挽 +輔 辅 +輕 轻 +輖 𫐏 +輗 𫐐 +輛 辆 +輜 辎 +輝 辉 +輞 辋 +輟 辍 +輢 𫐎 +輥 辊 +輦 辇 +輨 𫐑 +輩 辈 +輪 轮 +輬 辌 +輮 𫐓 +輯 辑 +輳 辏 +輷 𫐒 +輸 输 +輻 辐 +輼 辒 +輾 辗 +輿 舆 +轀 辒 +轂 毂 +轄 辖 +轅 辕 +轆 辘 +轇 𫐖 +轉 转 +轊 𫐕 +轍 辙 +轎 轿 +轐 𫐗 +轔 辚 +轗 𫐘 +轟 轰 +轠 𫐙 +轡 辔 +轢 轹 +轣 𫐆 +轤 轳 +辟 辟 +辦 办 +辭 辞 +辮 辫 +辯 辩 +農 农 +迴 回 +适 适 +逕 迳 +這 这 +連 连 +週 周 +進 进 +遊 游 +運 运 +過 过 +達 达 +違 违 +遙 遥 +遜 逊 +遞 递 +遠 远 +遡 溯 +適 适 +遱 𫐷 +遲 迟 +遷 迁 +選 选 +遺 遗 +遼 辽 +邁 迈 +還 还 +邇 迩 +邊 边 +邏 逻 +邐 逦 +郁 郁 +郟 郏 +郵 邮 +鄆 郓 +鄉 乡 +鄒 邹 +鄔 邬 +鄖 郧 +鄟 𫑘 +鄧 邓 +鄭 郑 +鄰 邻 +鄲 郸 +鄳 𫑡 +鄴 邺 +鄶 郐 +鄺 邝 +酇 酂 +酈 郦 +酸 酸 +醃 腌 +醖 酝 +醜 丑 +醞 酝 +醟 蒏 +醣 糖 +醫 医 +醬 酱 +醱 酦 +醶 𫑷 +釀 酿 +釁 衅 +釃 酾 +釅 酽 +采 采 +釋 释 +里 里 +釐 厘 +釒 钅 +釓 钆 +釔 钇 +釕 钌 +釗 钊 +釘 钉 +釙 钋 +釚 𫟲 +針 针 +釟 𫓥 +釣 钓 +釤 钐 +釦 扣 +釧 钏 +釨 𫓦 +釩 钒 +釲 𫟳 +釳 𨰿 +釵 钗 +釷 钍 +釹 钕 +釺 钎 +釾 䥺 +鈀 钯 +鈁 钫 +鈃 钘 +鈄 钭 +鈅 钥 +鈆 𫓪 +鈇 𫓧 +鈈 钚 +鈉 钠 +鈋 𨱂 +鈍 钝 +鈎 钩 +鈐 钤 +鈑 钣 +鈒 钑 +鈔 钞 +鈕 钮 纽 +鈖 𫟴 +鈗 𫟵 +鈛 𫓨 +鈞 钧 +鈠 𨱁 +鈡 钟 +鈣 钙 +鈥 钬 +鈦 钛 +鈧 钪 +鈮 铌 +鈯 𨱄 +鈰 铈 +鈲 𨱃 +鈳 钶 +鈴 铃 +鈷 钴 +鈸 钹 +鈹 铍 +鈺 钰 +鈽 钸 +鈾 铀 +鈿 钿 +鉀 钾 +鉁 𨱅 +鉅 巨 钜 +鉆 钻 +鉈 铊 +鉉 铉 +鉋 铇 +鉍 铋 +鉑 铂 +鉔 𫓬 +鉕 钷 +鉗 钳 +鉚 铆 +鉛 铅 +鉝 𫟷 +鉞 钺 +鉠 𫓭 +鉢 钵 +鉤 钩 +鉦 钲 +鉬 钼 +鉭 钽 +鉳 锫 +鉶 铏 +鉷 𫟹 +鉸 铰 +鉺 铒 +鉻 铬 +鉽 𫟸 +鉾 𫓴 +鉿 铪 +銀 银 +銁 𫓲 +銂 𫟻 +銃 铳 +銅 铜 +銈 𫓯 +銊 𫓰 +銍 铚 +銏 𫟶 +銑 铣 +銓 铨 +銖 铢 +銘 铭 +銚 铫 +銛 铦 +銜 衔 +銠 铑 +銣 铷 +銥 铱 +銦 铟 +銨 铵 +銩 铥 +銪 铕 +銫 铯 +銬 铐 +銱 铞 +銳 锐 +銶 𨱇 +銷 销 +銹 锈 +銻 锑 +銼 锉 +鋁 铝 +鋂 镅 +鋃 锒 +鋅 锌 +鋇 钡 +鋉 𨱈 +鋌 铤 +鋏 铗 +鋒 锋 +鋗 𫓶 +鋙 铻 +鋝 锊 +鋟 锓 +鋠 𫓵 +鋣 铘 +鋤 锄 +鋥 锃 +鋦 锔 +鋨 锇 +鋩 铓 +鋪 铺 +鋭 锐 +鋮 铖 +鋯 锆 +鋰 锂 +鋱 铽 +鋶 锍 +鋸 锯 +鋼 钢 +錁 锞 +錂 𨱋 +錄 录 +錆 锖 +錇 锫 +錈 锩 +錏 铔 +錐 锥 +錒 锕 +錕 锟 +錘 锤 +錙 锱 +錚 铮 +錛 锛 +錜 𫓻 +錝 𫓽 +錟 锬 +錠 锭 +錡 锜 +錢 钱 +錤 𫓹 +錥 𫓾 +錦 锦 +錨 锚 +錩 锠 +錫 锡 +錮 锢 +錯 错 +録 录 +錳 锰 +錶 表 +錸 铼 +錼 镎 +錽 𫓸 +鍀 锝 +鍁 锨 +鍃 锪 +鍄 𨱉 +鍅 钫 +鍆 钔 +鍇 锴 +鍈 锳 +鍉 𫔂 +鍊 炼 链 𫔀 +鍋 锅 +鍍 镀 +鍒 𫔄 +鍔 锷 +鍘 铡 +鍚 钖 +鍛 锻 +鍠 锽 +鍤 锸 +鍥 锲 +鍩 锘 +鍬 锹 +鍮 𨱎 +鍰 锾 +鍵 键 +鍶 锶 +鍺 锗 +鍼 针 +鍾 钟 锺 +鎂 镁 +鎄 锿 +鎇 镅 +鎈 𫟿 +鎊 镑 +鎌 镰 +鎍 𫔅 +鎔 镕 +鎖 锁 +鎘 镉 +鎙 𫔈 +鎚 锤 +鎛 镈 +鎝 𨱏 +鎞 𫔇 +鎡 镃 +鎢 钨 +鎣 蓥 +鎦 镏 +鎧 铠 +鎩 铩 +鎪 锼 +鎬 镐 +鎭 镇 +鎮 镇 +鎯 𨱍 +鎰 镒 +鎲 镋 +鎳 镍 +鎵 镓 +鎷 𨰾 +鎸 镌 +鎿 镎 +鏃 镞 +鏆 𨱌 +鏇 镟 +鏈 链 +鏉 𨱒 +鏌 镆 +鏍 镙 +鏐 镠 +鏑 镝 +鏗 铿 +鏘 锵 +鏚 戚 +鏜 镗 +鏝 镘 +鏞 镛 +鏟 铲 +鏡 镜 +鏢 镖 +鏤 镂 +鏥 𫔊 +鏦 𫓩 +鏨 錾 +鏰 镚 +鏵 铧 +鏷 镤 +鏹 镪 +鏺 䥽 +鏽 锈 +鏾 𫔌 +鐃 铙 +鐄 𨱑 +鐇 𫔍 +鐈 𫓱 +鐋 铴 +鐍 𫔎 +鐎 𨱓 +鐏 𨱔 +鐐 镣 +鐒 铹 +鐓 镦 +鐔 镡 +鐗 锏 +鐘 钟 +鐙 镫 +鐝 镢 +鐠 镨 +鐥 䦅 +鐦 锎 +鐧 锏 +鐨 镄 +鐪 𫓺 +鐫 镌 +鐮 镰 +鐯 䦃 +鐲 镯 +鐳 镭 +鐵 铁 +鐶 镮 +鐸 铎 +鐺 铛 +鐼 𫔁 +鐽 𫟼 +鐿 镱 +鑀 锿 +鑄 铸 +鑉 𫠁 +鑊 镬 +鑌 镔 +鑑 鉴 +鑒 鉴 +鑔 镲 +鑕 锧 +鑞 镴 +鑠 铄 +鑣 镳 +鑥 镥 +鑭 镧 +鑰 钥 +鑱 镵 +鑲 镶 +鑴 𫔔 +鑷 镊 +鑹 镩 +鑼 锣 +鑽 钻 +鑾 銮 +鑿 凿 +钁 镢 䦆 +钂 镋 +镟 旋 +長 长 +門 门 +閂 闩 +閃 闪 +閆 闫 +閈 闬 +閉 闭 +開 开 𫔭 +閌 闶 +閍 𨸂 +閎 闳 +閏 闰 +閐 𨸃 +閑 闲 +閒 闲 𫔮 +間 间 +閔 闵 +閗 𫔯 +閘 闸 +閝 𫠂 +閞 𫔰 +閡 阂 +閣 阁 +閤 合 +閥 阀 +閨 闺 +閩 闽 +閫 阃 +閬 阆 +閭 闾 +閱 阅 +閲 阅 +閵 𫔴 +閶 阊 +閹 阉 +閻 阎 +閼 阏 +閽 阍 +閾 阈 +閿 阌 +闃 阒 +闆 板 +闇 暗 +闈 闱 +闊 阔 +闋 阕 +闌 阑 +闍 阇 +闐 阗 +闑 𫔶 +闒 阘 +闓 闿 +闔 阖 +闕 阙 +闖 闯 +關 关 +闞 阚 +闠 阓 +闡 阐 +闢 辟 +闤 阛 +闥 闼 +阪 阪 坂 +陘 陉 +陝 陕 +陞 升 +陣 阵 +陰 阴 +陳 陈 +陸 陆 +陽 阳 +隉 陧 +隊 队 +階 阶 +隕 陨 +際 际 +隨 随 +險 险 +隯 陦 +隱 隐 +隴 陇 +隸 隶 +隻 只 +雇 雇 +雋 隽 +雕 雕 +雖 虽 +雙 双 +雛 雏 +雜 杂 +雞 鸡 +離 离 +難 难 +雲 云 +電 电 +霢 霡 +霣 𫕥 +霧 雾 +霼 𪵣 +霽 霁 +靂 雳 +靄 霭 +靆 叇 +靈 灵 +靉 叆 +靚 靓 +靜 静 +靝 靔 +面 面 +靦 腼 䩄 +靧 𫖃 +靨 靥 +鞀 鼗 +鞏 巩 +鞝 绱 +鞦 秋 +鞽 鞒 +鞾 𫖇 +韁 缰 +韃 鞑 +韆 千 +韉 鞯 +韋 韦 +韌 韧 +韍 韨 +韓 韩 +韙 韪 +韚 𫠅 +韛 𫖔 +韜 韬 +韝 鞲 𫖕 +韞 韫 +韠 𫖒 +韻 韵 +響 响 +頁 页 +頂 顶 +頃 顷 +項 项 +順 顺 +頇 顸 +須 须 +頊 顼 +頌 颂 +頍 𫠆 +頎 颀 +頏 颃 +預 预 +頑 顽 +頒 颁 +頓 顿 +頗 颇 +領 领 +頜 颌 +頡 颉 +頤 颐 +頦 颏 +頫 𫖯 +頭 头 +頮 颒 +頰 颊 +頲 颋 +頴 颕 +頵 𫖳 +頷 颔 +頸 颈 +頹 颓 +頻 频 +頽 颓 +顂 𩓋 +顃 𩖖 +顅 𫖶 +顆 颗 +題 题 +額 额 +顎 颚 +顏 颜 +顒 颙 +顓 颛 +顔 颜 +顗 𫖮 +願 愿 𫖸 +顙 颡 +顛 颠 +類 类 +顢 颟 +顣 𫖹 +顥 颢 +顧 顾 +顫 颤 +顬 颥 +顯 显 +顰 颦 +顱 颅 +顳 颞 +顴 颧 +風 风 +颭 飐 +颮 飑 +颯 飒 +颰 𩙥 +颱 台 +颳 刮 +颶 飓 +颷 𩙪 +颸 飔 +颺 飏 +颻 飖 +颼 飕 +颾 𩙫 +飀 飗 +飄 飘 +飆 飙 +飈 飚 +飋 𫗋 +飛 飞 +飠 饣 +飢 饥 +飣 饤 +飥 饦 +飦 𫗞 +飩 饨 +飪 饪 +飫 饫 +飭 饬 +飯 饭 +飱 飧 +飲 饮 +飴 饴 +飵 𫗢 +飶 𫗣 +飼 饲 +飽 饱 +飾 饰 +飿 饳 +餃 饺 +餄 饸 +餅 饼 +餉 饷 +養 养 +餌 饵 +餎 饹 +餏 饻 +餑 饽 +餒 馁 +餓 饿 +餔 𫗦 +餕 馂 +餖 饾 +餗 𫗧 +餘 余 馀 +餚 肴 +餛 馄 +餜 馃 +餞 饯 +餡 馅 +餦 𫗠 +餧 𫗪 +館 馆 +餪 𫗬 +餫 𫗥 +餬 糊 𫗫 +餭 𫗮 +餱 糇 𫗯 +餳 饧 +餵 喂 𫗭 +餶 馉 +餷 馇 +餸 𩠌 +餺 馎 +餼 饩 +餾 馏 +餿 馊 +饁 馌 +饃 馍 +饅 馒 +饈 馐 +饉 馑 +饊 馓 +饋 馈 +饌 馔 +饑 饥 +饒 饶 +饗 飨 +饘 𫗴 +饜 餍 +饞 馋 +饟 𫗵 +饠 𫗩 +饢 馕 +馬 马 +馭 驭 +馮 冯 +馯 𫘛 +馱 驮 +馳 驰 +馴 驯 +馹 驲 +馼 𫘜 +駁 驳 +駃 𫘝 +駊 𫘟 +駎 𩧨 +駐 驻 +駑 驽 +駒 驹 +駔 驵 +駕 驾 +駘 骀 +駙 驸 +駚 𩧫 +駛 驶 +駝 驼 +駞 𫘞 +駟 驷 +駡 骂 +駢 骈 +駤 𫘠 +駧 𩧲 +駩 𩧴 +駫 𫘡 +駭 骇 +駰 骃 +駱 骆 +駶 𩧺 +駸 骎 +駻 𫘣 +駿 骏 +騁 骋 +騂 骍 +騃 𫘤 +騄 𫘧 +騅 骓 +騉 𫘥 +騊 𫘦 +騌 骔 +騍 骒 +騎 骑 +騏 骐 +騔 𩨀 +騖 骛 +騙 骗 +騚 𩨊 +騜 𫘩 +騝 𩨃 +騟 𩨈 +騠 𫘨 +騤 骙 +騧 䯄 +騪 𩨄 +騫 骞 +騭 骘 +騮 骝 +騰 腾 +騱 𫘬 +騴 𫘫 +騵 𫘪 +騶 驺 +騷 骚 +騸 骟 +騻 𫘭 +騼 𫠋 +騾 骡 +驀 蓦 +驁 骜 +驂 骖 +驃 骠 +驄 骢 𩨂 +驅 驱 +驊 骅 +驋 𩧯 +驌 骕 +驍 骁 +驏 骣 +驓 𫘯 +驕 骄 +驗 验 +驙 𫘰 +驚 惊 +驛 驿 +驟 骤 +驢 驴 +驤 骧 +驥 骥 +驦 骦 +驨 𫘱 +驪 骊 +驫 骉 +骯 肮 +髏 髅 +髒 脏 +體 体 +髕 髌 +髖 髋 +髮 发 +鬆 松 +鬍 胡 +鬖 𩭹 +鬚 须 +鬠 𫘽 +鬢 鬓 +鬥 斗 +鬧 闹 +鬨 哄 +鬩 阋 +鬮 阄 +鬱 郁 +鬹 鬶 +魎 魉 +魘 魇 +魚 鱼 +魛 鱽 +魟 𫚉 +魢 鱾 +魥 𩽹 +魦 𫚌 +魨 鲀 +魯 鲁 +魴 鲂 +魵 𫚍 +魷 鱿 +魺 鲄 +魽 𫠐 +鮁 鲅 +鮃 鲆 +鮄 𫚒 +鮅 𫚑 +鮆 𫚖 +鮊 鲌 +鮋 鲉 +鮍 鲏 +鮎 鲇 +鮐 鲐 +鮑 鲍 +鮒 鲋 +鮓 鲊 +鮕 𩾀 +鮚 鲒 +鮜 鲘 +鮝 鲞 +鮞 鲕 +鮟 𩽾 +鮣 䲟 +鮤 𫚓 +鮦 鲖 +鮪 鲔 +鮫 鲛 +鮭 鲑 +鮮 鲜 +鮯 𫚗 +鮰 𫚔 +鮳 鲓 +鮵 𫚛 +鮶 鲪 +鮸 𩾃 +鮺 鲝 +鮿 𫚚 +鯀 鲧 +鯁 鲠 +鯄 𩾁 +鯆 𫚙 +鯇 鲩 +鯉 鲤 +鯊 鲨 +鯒 鲬 +鯔 鲻 +鯕 鲯 +鯖 鲭 +鯗 鲞 +鯛 鲷 +鯝 鲴 +鯞 𫚡 +鯡 鲱 +鯢 鲵 +鯤 鲲 +鯧 鲳 +鯨 鲸 +鯪 鲮 +鯫 鲰 +鯬 𫚞 +鯰 鲶 +鯱 𩾇 +鯴 鲺 +鯶 𩽼 +鯷 鳀 +鯽 鲫 +鯾 𫚣 +鯿 鳊 +鰁 鳈 +鰂 鲗 +鰃 鳂 +鰆 䲠 +鰈 鲽 +鰉 鳇 +鰋 𫚢 +鰌 䲡 +鰍 鳅 +鰏 鲾 +鰐 鳄 +鰑 𫚊 +鰒 鳆 +鰓 鳃 +鰕 𫚥 +鰛 鳁 +鰜 鳒 +鰟 鳑 +鰠 鳋 +鰣 鲥 +鰤 𫚕 +鰥 鳏 +鰦 𫚤 +鰧 䲢 +鰨 鳎 +鰩 鳐 +鰫 𫚦 +鰭 鳍 +鰮 鳁 +鰱 鲢 +鰲 鳌 +鰳 鳓 +鰵 鳘 +鰷 鲦 +鰹 鲣 +鰺 鲹 +鰻 鳗 +鰼 鳛 +鰽 𫚧 +鰾 鳔 +鱂 鳉 +鱄 𫚋 +鱅 鳙 +鱆 𫠒 +鱇 𩾌 +鱈 鳕 +鱉 鳖 +鱊 𫚪 +鱒 鳟 +鱔 鳝 +鱖 鳜 +鱗 鳞 +鱘 鲟 +鱝 鲼 +鱟 鲎 +鱠 鲙 +鱢 𫚫 +鱣 鳣 +鱤 鳡 +鱧 鳢 +鱨 鲿 +鱭 鲚 +鱮 𫚈 +鱯 鳠 +鱲 𫚭 +鱷 鳄 +鱸 鲈 +鱺 鲡 +鳥 鸟 +鳧 凫 +鳩 鸠 +鳬 凫 +鳲 鸤 +鳳 凤 +鳴 鸣 +鳶 鸢 +鳷 𫛛 +鳼 𪉃 +鳽 𫛚 +鳾 䴓 +鴀 𫛜 +鴃 𫛞 +鴅 𫛝 +鴆 鸩 +鴇 鸨 +鴉 鸦 +鴐 𫛤 +鴒 鸰 +鴔 𫛡 +鴕 鸵 +鴗 𫁡 +鴛 鸳 +鴜 𪉈 +鴝 鸲 +鴞 鸮 +鴟 鸱 +鴣 鸪 +鴥 𫛣 +鴦 鸯 +鴨 鸭 +鴮 𫛦 +鴯 鸸 +鴰 鸹 +鴲 𪉆 +鴳 𫛩 +鴴 鸻 +鴷 䴕 +鴻 鸿 +鴽 𫛪 +鴿 鸽 +鵁 䴔 +鵂 鸺 +鵃 鸼 +鵊 𫛥 +鵐 鹀 +鵑 鹃 +鵒 鹆 +鵓 鹁 +鵚 𪉍 +鵜 鹈 +鵝 鹅 +鵟 𫛭 +鵠 鹄 +鵡 鹉 +鵧 𫛨 +鵩 𫛳 +鵪 鹌 +鵫 𫛱 +鵬 鹏 +鵮 鹐 +鵯 鹎 +鵰 雕 𫛲 +鵲 鹊 +鵷 鹓 +鵾 鹍 +鶄 䴖 +鶇 鸫 +鶉 鹑 +鶊 鹒 +鶌 𫛵 +鶒 𫛶 +鶓 鹋 +鶖 鹙 +鶗 𫛸 +鶘 鹕 +鶚 鹗 +鶡 鹖 +鶥 鹛 +鶦 𫛷 +鶩 鹜 +鶪 䴗 +鶬 鸧 +鶭 𫛯 +鶯 莺 +鶰 𫛫 +鶲 鹟 +鶴 鹤 +鶹 鹠 +鶺 鹡 +鶻 鹘 +鶼 鹣 +鶿 鹚 +鷀 鹚 +鷁 鹢 +鷂 鹞 +鷄 鸡 +鷅 𫛽 +鷈 䴘 +鷉 䴘 +鷊 鹝 +鷐 𫜀 +鷓 鹧 +鷔 𪉑 +鷖 鹥 +鷗 鸥 +鷙 鸷 +鷚 鹨 +鷣 𫜃 +鷤 𫛴 +鷥 鸶 +鷦 鹪 +鷨 𪉊 +鷩 𫜁 +鷫 鹔 +鷯 鹩 +鷲 鹫 +鷳 鹇 +鷴 鹇 +鷷 𫜄 +鷸 鹬 +鷹 鹰 +鷺 鹭 +鷽 鸴 +鷿 䴙 +鸂 㶉 +鸇 鹯 +鸊 䴙 +鸋 𫛢 +鸌 鹱 +鸏 鹲 +鸕 鸬 +鸗 𫛟 +鸘 鹴 +鸚 鹦 +鸛 鹳 +鸝 鹂 +鸞 鸾 +鹵 卤 +鹹 咸 +鹺 鹾 +鹼 碱 +鹽 盐 +麗 丽 +麥 麦 +麨 𪎊 +麩 麸 +麪 面 麺 +麫 面 +麬 𤿲 +麯 曲 +麲 𪎉 +麳 𪎌 +麴 曲 麹 +麵 面 麺 +麷 𫜑 +麼 么 麽 +麽 么 麽 +黃 黄 +黌 黉 +點 点 +黨 党 +黲 黪 +黴 霉 +黶 黡 +黷 黩 +黽 黾 +黿 鼋 +鼂 鼌 +鼉 鼍 +鼕 冬 +鼴 鼹 +齇 齄 +齊 齐 +齋 斋 +齎 赍 +齏 齑 +齒 齿 +齔 龀 +齕 龁 +齗 龂 +齙 龅 +齜 龇 +齟 龃 +齠 龆 +齡 龄 +齣 出 +齦 龈 +齧 啮 𫜩 +齩 𫜪 +齪 龊 +齬 龉 +齭 𫜭 +齯 𫠜 +齰 𫜬 +齲 龋 +齴 𫜮 +齶 腭 +齷 龌 +齾 𫜰 +龍 龙 +龎 厐 +龐 庞 +龑 䶮 +龓 𫜲 +龔 龚 +龕 龛 +龜 龟 +龭 𩨎 +龯 𨱆 +鿁 䜤 +𠁞 𠀾 +𠌥 𠆿 +𠏢 𠉗 +𠐊 𫝋 +𠗣 㓆 +𠞆 𠛆 +𠠎 𠚳 +𠬙 𪠡 +𠽃 𪠺 +𠿕 𪜎 +𡂡 𪢒 +𡃄 𪡺 +𡃕 𠴛 +𡃤 𪢐 +𡄔 𠴢 +𡄣 𠵸 +𡅏 𠲥 +𡅯 𪢖 +𡑭 𡋗 +𡓁 𪤄 +𡓾 𡋀 +𡔖 𡍣 +𡞵 㛟 +𡟫 𫝪 +𡠹 㛿 +𡡎 𡞱 +𡢃 㛠 +𡮉 𡭜 +𡮣 𡭬 +𡳳 𡳃 +𡸗 𪨩 +𡹬 𪨹 +𡻕 岁 +𡽗 𡸃 +𡾱 㟜 +𡿖 𪩛 +𢍰 𪪴 +𢠼 𢙑 +𢣐 𪬚 +𢣚 𢘝 +𢣭 𢘞 +𢤩 𪫡 +𢤱 𢘙 +𢤿 𪬯 +𢯷 𪭝 +𢶒 𪭯 +𢶫 𢫞 +𢷬 𢭏 +𢷮 𢫊 +𢹿 𢬦 +𢺳 𪮳 +𣈶 暅 +𣋋 𣈣 +𣍐 𠊉 +𣙎 㭣 +𣜬 𪳗 +𣝕 𣘷 +𣞻 𣘓 +𣠩 𣞎 +𣠲 𣑶 +𣯩 𣯣 +𣯴 𣭤 +𣯶 毶 +𣽏 𪶮 +𣾷 㳢 +𣿉 𣶫 +𤁣 𣺽 +𤄷 𪶒 +𤅶 𣷷 +𤑳 𤎻 +𤑹 𪹀 +𤒎 𤊀 +𤒻 𪹹 +𤓌 𪹠 +𤓩 𤊰 +𤘀 𪺣 +𤛮 𤙯 +𤛱 𫞢 +𤜆 𪺪 +𤠮 𪺸 +𤢟 𤝢 +𤢻 𢢐 +𤩂 𫞧 +𤪺 㻘 +𤫩 㻏 +𤬅 𪼴 +𤳷 𪽝 +𤳸 𤳄 +𤷃 𪽭 +𤸫 𤶧 +𤺔 𪽴 +𥊝 𥅿 +𥌃 𥅘 +𥏝 𪿊 +𥕥 𥐰 +𥖅 𥐯 +𥖲 𪿞 +𥗇 𪿵 +𥜐 𫀓 +𥜰 𫀌 +𥞵 𥞦 +𥢢 䅪 +𥢶 𫞷 +𥢷 𫀮 +𥨐 𥧂 +𥪂 𥩺 +𥯤 𫁳 +𥴨 𫂖 +𥴼 𫁺 +𥵃 𥱔 +𥵊 𥭉 +𥶽 𫁱 +𥸠 𥮋 +𥻦 𫂿 +𥼽 𥹥 +𥽖 𥺇 +𥾯 𫄝 +𥿊 𦈈 +𦀖 𫄦 +𦂅 𦈒 +𦃄 𦈗 +𦃩 𫄯 +𦅇 𫄪 +𦅈 𫄵 +𦆲 𫟇 +𦒀 𫅥 +𦔖 𫅼 +𦘧 𡳒 +𦟼 𫆝 +𦠅 𫞅 +𦡝 𫆫 +𦢈 𣍨 +𦣎 𦟗 +𦧺 𫇘 +𦪙 䑽 +𦪽 𦨩 +𦱌 𫇪 +𦾟 𦶻 +𧎈 𧌥 +𧒯 𫊹 +𧔥 𧒭 +𧕟 𧉐 +𧜗 䘞 +𧜵 䙊 +𧝞 䘛 +𧞫 𫌋 +𧟀 𧝧 +𧡴 𫌫 +𧢄 𫌬 +𧦝 𫍞 +𧦧 𫍟 +𧩕 𫍭 +𧩙 䜥 +𧩼 𫍶 +𧫝 𫍺 +𧬤 𫍼 +𧭈 𫍾 +𧭹 𫍐 +𧳟 𧳕 +𧵳 䞌 +𧶔 𧹓 +𧶧 䞎 +𧷎 𪠀 +𧸘 𫎨 +𧹈 𪥠 +𧽯 𫎸 +𨂐 𫏌 +𨄣 𨀱 +𨅍 𨁴 +𨆪 𫏕 +𨇁 𧿈 +𨇞 𨅫 +𨇤 𫏨 +𨇰 𫏞 +𨇽 𫏑 +𨈊 𨂺 +𨈌 𨄄 +𨊰 䢀 +𨊸 䢁 +𨊻 𨐆 +𨋢 䢂 +𨌈 𫐍 +𨍰 𫐔 +𨎌 𫐋 +𨎮 𨐉 +𨏠 𨐇 +𨏥 𨐊 +𨞺 𫟫 +𨟊 𫟬 +𨢿 𨡙 +𨣈 𨡺 +𨣞 𨟳 +𨣧 𨠨 +𨤻 𨤰 +𨥛 𨱀 +𨥟 𫓫 +𨦫 䦀 +𨧜 䦁 +𨧰 𫟽 +𨧱 𨱊 +𨨛 𫓼 +𨨢 𫓿 +𨩰 𫟾 +𨪕 𫓮 +𨫒 𨱐 +𨬖 𫔏 +𨭖 𫔑 +𨭸 𫔐 +𨮂 𨱕 +𨮳 𫔒 +𨯅 䥿 +𨯟 𫔓 +𨰃 𫔉 +𨰋 𫓳 +𨰥 𫔕 +𨰲 𫔃 +𨲳 𫔖 +𨳑 𨸁 +𨳕 𨸀 +𨴗 𨸅 +𨴹 𫔲 +𨵩 𨸆 +𨵸 𨸇 +𨶀 𨸉 +𨶏 𨸊 +𨶮 𨸌 +𨶲 𨸋 +𨷲 𨸎 +𨼳 𫔽 +𨽏 𨸘 +𩀨 𫕚 +𩅙 𫕨 +𩎖 𫖑 +𩎢 𩏾 +𩏂 𫖓 +𩏠 𫖖 +𩏪 𩏽 +𩏷 𫃗 +𩑔 𫖪 +𩒎 𫖭 +𩓣 𩖕 +𩓥 𫖵 +𩔑 𫖷 +𩔳 𫖴 +𩖰 𫠇 +𩗀 𩙦 +𩗓 𫗈 +𩗴 𫗉 +𩘀 𩙩 +𩘝 𩙭 +𩘹 𩙨 +𩘺 𩙬 +𩙈 𩙰 +𩚛 𩟿 +𩚥 𩠀 +𩚩 𫗡 +𩚵 𩠁 +𩛆 𩠂 +𩛌 𫗤 +𩛡 𫗨 +𩛩 𩠃 +𩜇 𩠉 +𩜦 𩠆 +𩜵 𩠊 +𩝔 𩠋 +𩝽 𫗳 +𩞄 𩠎 +𩞦 𩠏 +𩞯 䭪 +𩟐 𩠅 +𩟗 𫗚 +𩠴 𩠠 +𩡣 𩡖 +𩡺 𩧦 +𩢡 𩧬 +𩢴 𩧵 +𩢸 𩧳 +𩢾 𩧮 +𩣏 𩧶 +𩣑 䯃 +𩣫 𩧸 +𩣵 𩧻 +𩣺 𩧼 +𩤊 𩧩 +𩤙 𩨆 +𩤲 𩨉 +𩤸 𩨅 +𩥄 𩨋 +𩥇 𩨍 +𩥉 𩧱 +𩥑 𩨌 +𩦠 𫠌 +𩧆 𩨐 +𩭙 𩬣 +𩯁 𫙂 +𩯳 𩯒 +𩰀 𩬤 +𩰹 𩰰 +𩳤 𩲒 +𩴵 𩴌 +𩵦 𫠏 +𩵩 𩽺 +𩵹 𩽻 +𩶁 𫚎 +𩶘 䲞 +𩶰 𩽿 +𩶱 𩽽 +𩷰 𩾄 +𩸃 𩾅 +𩸄 𫚝 +𩸡 𫚟 +𩸦 𩾆 +𩻗 𫚨 +𩻬 𫚩 +𩻮 𫚘 +𩼶 𫚬 +𩽇 𩾎 +𩿅 𫠖 +𩿤 𫛠 +𩿪 𪉄 +𪀖 𫛧 +𪀦 𪉅 +𪀾 𪉋 +𪁈 𪉉 +𪁖 𪉌 +𪂆 𪉎 +𪃍 𪉐 +𪃏 𪉏 +𪃒 𫛻 +𪃧 𫛹 +𪄆 𪉔 +𪄕 𪉒 +𪅂 𫜂 +𪆷 𫛾 +𪇳 𪉕 +𪈼 𪉓 +𪉸 𫜊 +𪋿 𪎍 +𪌭 𫜓 +𪍠 𫜕 +𪓰 𫜟 +𪔵 𪔭 +𪘀 𪚏 +𪘯 𪚐 +𪙏 𫜯 +𪟖 𠛾 +𪷓 𣶭 +𫒡 𫓷 +𫜦 𫜫 diff --git a/tools/cpdir.go b/tools/cpdir.go new file mode 100644 index 0000000..8d9a537 --- /dev/null +++ b/tools/cpdir.go @@ -0,0 +1,102 @@ +// Copyright 2013 . All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build ingore + +// +// Copy dir, support regexp. +// +// Example: +// cpdir src dst +// cpdir src dst "\.go$" +// cpdir src dst "\.tiff?$" +// cpdir src dst "\.tiff?|jpg|jpeg$" +// +// Help: +// cpdir -h +// +package main + +import ( + "fmt" + "io" + "io/ioutil" + "log" + "os" + "path/filepath" + "regexp" +) + +const usage = ` +Usage: cpdir src dst [filter] + cpdir -h + +Example: + cpdir src dst + cpdir src dst "\.go$" + cpdir src dst "\.tiff?$" + cpdir src dst "\.tiff?|jpg|jpeg$" + +Report bugs to . +` + +func main() { + if len(os.Args) < 3 { + fmt.Fprintln(os.Stderr, usage[1:len(usage)-1]) + os.Exit(0) + } + filter := ".*" + if len(os.Args) > 3 { + filter = os.Args[3] + } + total := cpDir(os.Args[2], os.Args[1], filter) + fmt.Printf("total %d\n", total) +} + +func cpDir(dst, src, filter string) (total int) { + entryList, err := ioutil.ReadDir(src) + if err != nil && !os.IsExist(err) { + log.Fatal("cpDir: ", err) + } + for _, entry := range entryList { + if entry.IsDir() { + cpDir(dst+"/"+entry.Name(), src+"/"+entry.Name(), filter) + } else { + mathed, err := regexp.MatchString(filter, entry.Name()) + if err != nil { + log.Fatal("regexp.MatchString: ", err) + } + if mathed { + srcFname := filepath.Clean(src + "/" + entry.Name()) + dstFname := filepath.Clean(dst + "/" + entry.Name()) + fmt.Printf("copy %s\n", srcFname) + + cpFile(dstFname, srcFname) + total++ + } + } + } + return +} + +func cpFile(dst, src string) { + err := os.MkdirAll(filepath.Dir(dst), 0666) + if err != nil && !os.IsExist(err) { + log.Fatal("cpFile: ", err) + } + fsrc, err := os.Open(src) + if err != nil { + log.Fatal("cpFile: ", err) + } + defer fsrc.Close() + + fdst, err := os.Create(dst) + if err != nil { + log.Fatal("cpFile: ", err) + } + defer fdst.Close() + if _, err = io.Copy(fdst, fsrc); err != nil { + log.Fatal("cpFile: ", err) + } +} diff --git a/tools/lookpath.go b/tools/lookpath.go new file mode 100644 index 0000000..b8a505c --- /dev/null +++ b/tools/lookpath.go @@ -0,0 +1,44 @@ +// Copyright 2014 . All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build ingore + +// Lookpath is a simple which. +package main + +import ( + "fmt" + "os" + "os/exec" + "strings" +) + +func main() { + if len(os.Args) < 2 { + fmt.Printf(`Usage: lookpath COMMAND [...] +Write the full path of COMMAND(s) to standard output. + +Report bugs to . +`) + os.Exit(0) + } + for i := 1; i < len(os.Args); i++ { + path, err := exec.LookPath(os.Args[i]) + if err != nil { + fmt.Printf("lookpath: no %s in (%v)\n", os.Args[i], GetEnv("PATH")) + os.Exit(0) + } + fmt.Println(path) + } +} + +func GetEnv(key string) string { + key = strings.ToUpper(key) + "=" + for _, env := range os.Environ() { + if strings.HasPrefix(strings.ToUpper(env), key) { + return env[len(key):] + } + } + return "" +} diff --git a/tools/lsdir.go b/tools/lsdir.go new file mode 100644 index 0000000..9617b66 --- /dev/null +++ b/tools/lsdir.go @@ -0,0 +1,97 @@ +// Copyright 2013 . All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build ingore + +// +// List files, support file/header regexp. +// +// Example: +// lsdir dir +// lsdir dir "\.go$" +// lsdir dir "\.go$" "chaishushan" +// lsdir dir "\.tiff?|jpg|jpeg$" +// +// Help: +// lsdir -h +// +package main + +import ( + "fmt" + "io/ioutil" + "log" + "os" + "path/filepath" + "regexp" +) + +const usage = ` +Usage: lsdir dir [nameFilter [dataFilter]] + lsdir -h + +Example: + lsdir dir + lsdir dir "\.go$" + lsdir dir "\.go$" "chaishushan" + lsdir dir "\.tiff?|jpg|jpeg$" + +Report bugs to . +` + +func main() { + if len(os.Args) < 2 || os.Args[1] == "-h" { + fmt.Fprintln(os.Stderr, usage[1:len(usage)-1]) + os.Exit(0) + } + dir, nameFilter, dataFilter := os.Args[1], ".*", "" + if len(os.Args) > 2 { + nameFilter = os.Args[2] + } + if len(os.Args) > 3 { + dataFilter = os.Args[3] + } + + total := 0 + filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { + if err != nil { + log.Fatal("filepath.Walk: ", err) + return err + } + if info.IsDir() { + return nil + } + relpath, err := filepath.Rel(dir, path) + if err != nil { + log.Fatal("filepath.Rel: ", err) + return err + } + mathed, err := regexp.MatchString(nameFilter, relpath) + if err != nil { + log.Fatal("regexp.MatchString: ", err) + } + if mathed { + if dataFilter != "" { + data, err := ioutil.ReadFile(path) + if err != nil { + fmt.Printf("ioutil.ReadFile: %s\n", path) + log.Fatal("ioutil.ReadFile: ", err) + } + mathed, err := regexp.MatchString(dataFilter, string(data)) + if err != nil { + log.Fatal("regexp.MatchString: ", err) + } + if mathed { + fmt.Printf("%s\n", relpath) + total++ + } + } else { + fmt.Printf("%s\n", relpath) + total++ + } + } + return nil + }) + fmt.Printf("total %d\n", total) +} diff --git a/tools/md5.go b/tools/md5.go new file mode 100644 index 0000000..f60c8fb --- /dev/null +++ b/tools/md5.go @@ -0,0 +1,121 @@ +// Copyright 2013 . All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build ingore + +// +// Cacl dir or file MD5Sum, support regexp. +// +// Example: +// md5 file +// md5 dir "\.go$" +// md5 dir "\.tiff?$" +// md5 dir "\.tiff?|jpg|jpeg$" +// +// Help: +// cpdir -h +// +package main + +import ( + "crypto/md5" + "fmt" + "io/ioutil" + "log" + "os" + "path/filepath" + "regexp" + "sort" +) + +const usage = ` +Usage: md5 [dir|file [filter]] + md5 -h + +Example: + md5 file + md5 dir "\.go$" + md5 dir "\.go$" + md5 dir "\.tiff?$" + md5 dir "\.tiff?|jpg|jpeg$" + +Report bugs to . +` + +func main() { + if len(os.Args) < 2 { + fmt.Fprintln(os.Stderr, usage[1:len(usage)-1]) + os.Exit(0) + } + filter := ".*" + if len(os.Args) > 2 { + filter = os.Args[2] + } + + if name := os.Args[1]; IsDir(name) { + m, err := MD5Dir(name, filter) + if err != nil { + log.Fatalf("%s: %v", name, err) + } + var paths []string + for path := range m { + paths = append(paths, path) + } + sort.Strings(paths) + for _, path := range paths { + fmt.Printf("%x *%s\n", m[path], path) + } + } else { + sum, err := MD5File(name) + if err != nil { + log.Fatalf("%s: %v", name, err) + } + fmt.Printf("%x *%s\n", sum, name) + } +} + +func IsDir(name string) bool { + fi, err := os.Lstat(name) + if err != nil { + log.Fatal(err) + } + return fi.IsDir() +} + +func MD5Dir(root string, filter string) (map[string][md5.Size]byte, error) { + m := make(map[string][md5.Size]byte) + err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() { + return nil + } + mathed, err := regexp.MatchString(filter, path) + if err != nil { + log.Fatal("regexp.MatchString: ", err) + } + if mathed { + data, err := ioutil.ReadFile(path) + if err != nil { + return err + } + m[path] = md5.Sum(data) + } + return nil + }) + if err != nil { + return nil, err + } + return m, nil +} + +func MD5File(filename string) (sum [md5.Size]byte, err error) { + data, err := ioutil.ReadFile(filename) + if err != nil { + return + } + sum = md5.Sum(data) + return +} diff --git a/tools/mkqrcode.go b/tools/mkqrcode.go new file mode 100644 index 0000000..e9d3238 --- /dev/null +++ b/tools/mkqrcode.go @@ -0,0 +1,33 @@ +// Copyright 2015 . All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build ingore + +package main + +import ( + "fmt" + "io/ioutil" + "log" + + qr "github.com/chai2010/image/qrencoder" +) + +const ( + gopl_zh_url = "https://github.com/golang-china/gopl-zh" + output = "gopl-zh-qrcode.png" +) + +func main() { + c, err := qr.Encode(gopl_zh_url, qr.H) + if err != nil { + log.Fatal(err) + } + err = ioutil.WriteFile(output, c.PNG(), 0666) + if err != nil { + log.Fatal(err) + } + + fmt.Println("output:", output) +} diff --git a/tools/mktable.go b/tools/mktable.go new file mode 100644 index 0000000..04ae9ab --- /dev/null +++ b/tools/mktable.go @@ -0,0 +1,62 @@ +// Copyright 2013 . All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build ingore + +package main + +import ( + "bufio" + "bytes" + "fmt" + "io/ioutil" + "log" + "os" + "strings" + "unicode/utf8" +) + +func main() { + f, err := os.Open("./TSCharacters.txt") + if err != nil { + log.Fatal("open failed:", err) + } + defer f.Close() + + br := bufio.NewReader(f) + + var out bytes.Buffer + fmt.Fprintf(&out, ` +// Copyright 2013 . All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Auto generated by go generate, DO NOT EDIT !!! + +package main + +var _TSCharactersMap = map[rune]rune{ +`[1:]) + for i := 0; i < 1<<20; i++ { + data, isPrefix, err := br.ReadLine() + if err != nil || isPrefix { + break + } + if !utf8.ValidString(string(data)) { + continue + } + + line := strings.Replace(string(data), "\t", " ", -1) + ss := strings.Split(string(line), " ") + + if len(ss) >= 2 { + tw := strings.TrimSpace(ss[0]) + zh := strings.TrimSpace(ss[1]) + fmt.Fprintf(&out, "\t'%s': '%s',\n", tw, zh) + } + } + fmt.Fprintf(&out, "}\n") + + ioutil.WriteFile("z_TSCharacters.go", []byte(out.Bytes()), 0666) +} diff --git a/update_version.go b/update_version.go new file mode 100644 index 0000000..269b80b --- /dev/null +++ b/update_version.go @@ -0,0 +1,82 @@ +// Copyright 2015 . All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build ingore + +// 更新版本信息 +package main + +import ( + "fmt" + "io/ioutil" + "log" + "os/exec" + "strings" + "time" +) + +func main() { + version, commitTime := getGitCommitVersion() + buildTime := time.Now() + + data := makeVersionMarkdown(version, commitTime, buildTime) + + err := ioutil.WriteFile("./version.md", []byte(data), 0666) + if err != nil { + log.Fatalf("ioutil.WriteFile: err = %v", err) + } + + fmt.Println("build version", version) + fmt.Println(commitTime.Format("2006-01-02 15:04:05")) + fmt.Println(buildTime.Format("2006-01-02 15:04:05")) +} + +// 生成版本文件 +func makeVersionMarkdown(version string, commitTime, buildTime time.Time) string { + return fmt.Sprintf(` + + +### 版本信息 + +- 仓库版本:[%s](gopl-zh-%s.zip) +- 更新时间:%s +- 构建时间:%s +`, + version, version, + commitTime.Format("2006-01-02 15:04:05"), + buildTime.Format("2006-01-02 15:04:05"), + ) +} + +// 获取Git最新的版本号 +// +// git log -1 +// commit 0460c1b3bb8fbb7e2fc88961e69aa37f4041d6c1 +// Merge: b2d582a e826457 +// Author: chai2010 +// Date: Mon Feb 1 08:04:44 2016 +0800 +// +// Merge pull request #249 from sunclx/patch-3 +// +// fix typo +func getGitCommitVersion() (version string, date time.Time) { + cmdOut, err := exec.Command(`git`, `log`, `-1`).CombinedOutput() + if err != nil { + return "unknown", time.Date(2016, 2, 1, 0, 0, 0, 0, time.UTC) // 第一版发布时间 + } + for _, line := range strings.Split(string(cmdOut), "\n") { + line := strings.TrimSpace(line) + if strings.HasPrefix(line, "commit") { + version = strings.TrimSpace(line[len("commit"):]) + } + if strings.HasPrefix(line, "Date") { + const longForm = "Mon Jan 2 15:04:05 2006 -0700" + date, _ = time.Parse(longForm, strings.TrimSpace(line[len("Date:"):])) + } + } + if version == "" || date.IsZero() { + return "unknown", time.Date(2016, 2, 1, 0, 0, 0, 0, time.UTC) // 第一版发布时间 + } + return +} diff --git a/vendor/github.com/chai2010/image/qrencoder/hello.go b/vendor/github.com/chai2010/image/qrencoder/hello.go new file mode 100644 index 0000000..441c79d --- /dev/null +++ b/vendor/github.com/chai2010/image/qrencoder/hello.go @@ -0,0 +1,28 @@ +// Copyright 2015 . All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build ingore + +package main + +import ( + "fmt" + "io/ioutil" + "log" + + qr "github.com/chai2010/image/qrencoder" +) + +func main() { + c, err := qr.Encode("hello, world", qr.L) + if err != nil { + log.Fatal(err) + } + err = ioutil.WriteFile("zz_qrout.png", c.PNG(), 0666) + if err != nil { + log.Fatal(err) + } + + fmt.Print("output: zz_qrout.png\n") +} diff --git a/vendor/github.com/chai2010/image/qrencoder/internal/coding/gen.go b/vendor/github.com/chai2010/image/qrencoder/internal/coding/gen.go new file mode 100644 index 0000000..a3857f2 --- /dev/null +++ b/vendor/github.com/chai2010/image/qrencoder/internal/coding/gen.go @@ -0,0 +1,149 @@ +// +build ignore + +package main + +import "fmt" + +// tables from qrencode-3.1.1/qrspec.c + +var capacity = [41]struct { + width int + words int + remainder int + ec [4]int +}{ + {0, 0, 0, [4]int{0, 0, 0, 0}}, + {21, 26, 0, [4]int{7, 10, 13, 17}}, // 1 + {25, 44, 7, [4]int{10, 16, 22, 28}}, + {29, 70, 7, [4]int{15, 26, 36, 44}}, + {33, 100, 7, [4]int{20, 36, 52, 64}}, + {37, 134, 7, [4]int{26, 48, 72, 88}}, // 5 + {41, 172, 7, [4]int{36, 64, 96, 112}}, + {45, 196, 0, [4]int{40, 72, 108, 130}}, + {49, 242, 0, [4]int{48, 88, 132, 156}}, + {53, 292, 0, [4]int{60, 110, 160, 192}}, + {57, 346, 0, [4]int{72, 130, 192, 224}}, //10 + {61, 404, 0, [4]int{80, 150, 224, 264}}, + {65, 466, 0, [4]int{96, 176, 260, 308}}, + {69, 532, 0, [4]int{104, 198, 288, 352}}, + {73, 581, 3, [4]int{120, 216, 320, 384}}, + {77, 655, 3, [4]int{132, 240, 360, 432}}, //15 + {81, 733, 3, [4]int{144, 280, 408, 480}}, + {85, 815, 3, [4]int{168, 308, 448, 532}}, + {89, 901, 3, [4]int{180, 338, 504, 588}}, + {93, 991, 3, [4]int{196, 364, 546, 650}}, + {97, 1085, 3, [4]int{224, 416, 600, 700}}, //20 + {101, 1156, 4, [4]int{224, 442, 644, 750}}, + {105, 1258, 4, [4]int{252, 476, 690, 816}}, + {109, 1364, 4, [4]int{270, 504, 750, 900}}, + {113, 1474, 4, [4]int{300, 560, 810, 960}}, + {117, 1588, 4, [4]int{312, 588, 870, 1050}}, //25 + {121, 1706, 4, [4]int{336, 644, 952, 1110}}, + {125, 1828, 4, [4]int{360, 700, 1020, 1200}}, + {129, 1921, 3, [4]int{390, 728, 1050, 1260}}, + {133, 2051, 3, [4]int{420, 784, 1140, 1350}}, + {137, 2185, 3, [4]int{450, 812, 1200, 1440}}, //30 + {141, 2323, 3, [4]int{480, 868, 1290, 1530}}, + {145, 2465, 3, [4]int{510, 924, 1350, 1620}}, + {149, 2611, 3, [4]int{540, 980, 1440, 1710}}, + {153, 2761, 3, [4]int{570, 1036, 1530, 1800}}, + {157, 2876, 0, [4]int{570, 1064, 1590, 1890}}, //35 + {161, 3034, 0, [4]int{600, 1120, 1680, 1980}}, + {165, 3196, 0, [4]int{630, 1204, 1770, 2100}}, + {169, 3362, 0, [4]int{660, 1260, 1860, 2220}}, + {173, 3532, 0, [4]int{720, 1316, 1950, 2310}}, + {177, 3706, 0, [4]int{750, 1372, 2040, 2430}}, //40 +} + +var eccTable = [41][4][2]int{ + {{0, 0}, {0, 0}, {0, 0}, {0, 0}}, + {{1, 0}, {1, 0}, {1, 0}, {1, 0}}, // 1 + {{1, 0}, {1, 0}, {1, 0}, {1, 0}}, + {{1, 0}, {1, 0}, {2, 0}, {2, 0}}, + {{1, 0}, {2, 0}, {2, 0}, {4, 0}}, + {{1, 0}, {2, 0}, {2, 2}, {2, 2}}, // 5 + {{2, 0}, {4, 0}, {4, 0}, {4, 0}}, + {{2, 0}, {4, 0}, {2, 4}, {4, 1}}, + {{2, 0}, {2, 2}, {4, 2}, {4, 2}}, + {{2, 0}, {3, 2}, {4, 4}, {4, 4}}, + {{2, 2}, {4, 1}, {6, 2}, {6, 2}}, //10 + {{4, 0}, {1, 4}, {4, 4}, {3, 8}}, + {{2, 2}, {6, 2}, {4, 6}, {7, 4}}, + {{4, 0}, {8, 1}, {8, 4}, {12, 4}}, + {{3, 1}, {4, 5}, {11, 5}, {11, 5}}, + {{5, 1}, {5, 5}, {5, 7}, {11, 7}}, //15 + {{5, 1}, {7, 3}, {15, 2}, {3, 13}}, + {{1, 5}, {10, 1}, {1, 15}, {2, 17}}, + {{5, 1}, {9, 4}, {17, 1}, {2, 19}}, + {{3, 4}, {3, 11}, {17, 4}, {9, 16}}, + {{3, 5}, {3, 13}, {15, 5}, {15, 10}}, //20 + {{4, 4}, {17, 0}, {17, 6}, {19, 6}}, + {{2, 7}, {17, 0}, {7, 16}, {34, 0}}, + {{4, 5}, {4, 14}, {11, 14}, {16, 14}}, + {{6, 4}, {6, 14}, {11, 16}, {30, 2}}, + {{8, 4}, {8, 13}, {7, 22}, {22, 13}}, //25 + {{10, 2}, {19, 4}, {28, 6}, {33, 4}}, + {{8, 4}, {22, 3}, {8, 26}, {12, 28}}, + {{3, 10}, {3, 23}, {4, 31}, {11, 31}}, + {{7, 7}, {21, 7}, {1, 37}, {19, 26}}, + {{5, 10}, {19, 10}, {15, 25}, {23, 25}}, //30 + {{13, 3}, {2, 29}, {42, 1}, {23, 28}}, + {{17, 0}, {10, 23}, {10, 35}, {19, 35}}, + {{17, 1}, {14, 21}, {29, 19}, {11, 46}}, + {{13, 6}, {14, 23}, {44, 7}, {59, 1}}, + {{12, 7}, {12, 26}, {39, 14}, {22, 41}}, //35 + {{6, 14}, {6, 34}, {46, 10}, {2, 64}}, + {{17, 4}, {29, 14}, {49, 10}, {24, 46}}, + {{4, 18}, {13, 32}, {48, 14}, {42, 32}}, + {{20, 4}, {40, 7}, {43, 22}, {10, 67}}, + {{19, 6}, {18, 31}, {34, 34}, {20, 61}}, //40 +} + +var align = [41][2]int{ + {0, 0}, + {0, 0}, {18, 0}, {22, 0}, {26, 0}, {30, 0}, // 1- 5 + {34, 0}, {22, 38}, {24, 42}, {26, 46}, {28, 50}, // 6-10 + {30, 54}, {32, 58}, {34, 62}, {26, 46}, {26, 48}, //11-15 + {26, 50}, {30, 54}, {30, 56}, {30, 58}, {34, 62}, //16-20 + {28, 50}, {26, 50}, {30, 54}, {28, 54}, {32, 58}, //21-25 + {30, 58}, {34, 62}, {26, 50}, {30, 54}, {26, 52}, //26-30 + {30, 56}, {34, 60}, {30, 58}, {34, 62}, {30, 54}, //31-35 + {24, 50}, {28, 54}, {32, 58}, {26, 54}, {30, 58}, //35-40 +} + +var versionPattern = [41]int{ + 0, + 0, 0, 0, 0, 0, 0, + 0x07c94, 0x085bc, 0x09a99, 0x0a4d3, 0x0bbf6, 0x0c762, 0x0d847, 0x0e60d, + 0x0f928, 0x10b78, 0x1145d, 0x12a17, 0x13532, 0x149a6, 0x15683, 0x168c9, + 0x177ec, 0x18ec4, 0x191e1, 0x1afab, 0x1b08e, 0x1cc1a, 0x1d33f, 0x1ed75, + 0x1f250, 0x209d5, 0x216f0, 0x228ba, 0x2379f, 0x24b0b, 0x2542e, 0x26a64, + 0x27541, 0x28c69, +} + +func main() { + fmt.Printf("\t{},\n") + for i := 1; i <= 40; i++ { + apos := align[i][0] - 2 + if apos < 0 { + apos = 100 + } + astride := align[i][1] - align[i][0] + if astride < 1 { + astride = 100 + } + fmt.Printf("\t{%v, %v, %v, %#x, [4]level{{%v, %v}, {%v, %v}, {%v, %v}, {%v, %v}}}, // %v\n", + apos, astride, capacity[i].words, + versionPattern[i], + eccTable[i][0][0]+eccTable[i][0][1], + float64(capacity[i].ec[0])/float64(eccTable[i][0][0]+eccTable[i][0][1]), + eccTable[i][1][0]+eccTable[i][1][1], + float64(capacity[i].ec[1])/float64(eccTable[i][1][0]+eccTable[i][1][1]), + eccTable[i][2][0]+eccTable[i][2][1], + float64(capacity[i].ec[2])/float64(eccTable[i][2][0]+eccTable[i][2][1]), + eccTable[i][3][0]+eccTable[i][3][1], + float64(capacity[i].ec[3])/float64(eccTable[i][3][0]+eccTable[i][3][1]), + i, + ) + } +} diff --git a/vendor/github.com/chai2010/image/qrencoder/internal/coding/qr.go b/vendor/github.com/chai2010/image/qrencoder/internal/coding/qr.go new file mode 100644 index 0000000..701b9d6 --- /dev/null +++ b/vendor/github.com/chai2010/image/qrencoder/internal/coding/qr.go @@ -0,0 +1,815 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package coding implements low-level QR coding details. +package coding + +import ( + "fmt" + "strconv" + "strings" + + "github.com/chai2010/image/qrencoder/internal/gf256" +) + +// Field is the field for QR error correction. +var Field = gf256.NewField(0x11d, 2) + +// A Version represents a QR version. +// The version specifies the size of the QR code: +// a QR code with version v has 4v+17 pixels on a side. +// Versions number from 1 to 40: the larger the version, +// the more information the code can store. +type Version int + +const MinVersion = 1 +const MaxVersion = 40 + +func (v Version) String() string { + return strconv.Itoa(int(v)) +} + +func (v Version) sizeClass() int { + if v <= 9 { + return 0 + } + if v <= 26 { + return 1 + } + return 2 +} + +// DataBytes returns the number of data bytes that can be +// stored in a QR code with the given version and level. +func (v Version) DataBytes(l Level) int { + vt := &vtab[v] + lev := &vt.level[l] + return vt.bytes - lev.nblock*lev.check +} + +// Encoding implements a QR data encoding scheme. +// The implementations--Numeric, Alphanumeric, and String--specify +// the character set and the mapping from UTF-8 to code bits. +// The more restrictive the mode, the fewer code bits are needed. +type Encoding interface { + Check() error + Bits(v Version) int + Encode(b *Bits, v Version) +} + +type Bits struct { + b []byte + nbit int +} + +func (b *Bits) Reset() { + b.b = b.b[:0] + b.nbit = 0 +} + +func (b *Bits) Bits() int { + return b.nbit +} + +func (b *Bits) Bytes() []byte { + if b.nbit%8 != 0 { + panic("fractional byte") + } + return b.b +} + +func (b *Bits) Append(p []byte) { + if b.nbit%8 != 0 { + panic("fractional byte") + } + b.b = append(b.b, p...) + b.nbit += 8 * len(p) +} + +func (b *Bits) Write(v uint, nbit int) { + for nbit > 0 { + n := nbit + if n > 8 { + n = 8 + } + if b.nbit%8 == 0 { + b.b = append(b.b, 0) + } else { + m := -b.nbit & 7 + if n > m { + n = m + } + } + b.nbit += n + sh := uint(nbit - n) + b.b[len(b.b)-1] |= uint8(v >> sh << uint(-b.nbit&7)) + v -= v >> sh << sh + nbit -= n + } +} + +// Num is the encoding for numeric data. +// The only valid characters are the decimal digits 0 through 9. +type Num string + +func (s Num) String() string { + return fmt.Sprintf("Num(%#q)", string(s)) +} + +func (s Num) Check() error { + for _, c := range s { + if c < '0' || '9' < c { + return fmt.Errorf("non-numeric string %#q", string(s)) + } + } + return nil +} + +var numLen = [3]int{10, 12, 14} + +func (s Num) Bits(v Version) int { + return 4 + numLen[v.sizeClass()] + (10*len(s)+2)/3 +} + +func (s Num) Encode(b *Bits, v Version) { + b.Write(1, 4) + b.Write(uint(len(s)), numLen[v.sizeClass()]) + var i int + for i = 0; i+3 <= len(s); i += 3 { + w := uint(s[i]-'0')*100 + uint(s[i+1]-'0')*10 + uint(s[i+2]-'0') + b.Write(w, 10) + } + switch len(s) - i { + case 1: + w := uint(s[i] - '0') + b.Write(w, 4) + case 2: + w := uint(s[i]-'0')*10 + uint(s[i+1]-'0') + b.Write(w, 7) + } +} + +// Alpha is the encoding for alphanumeric data. +// The valid characters are 0-9A-Z$%*+-./: and space. +type Alpha string + +const alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:" + +func (s Alpha) String() string { + return fmt.Sprintf("Alpha(%#q)", string(s)) +} + +func (s Alpha) Check() error { + for _, c := range s { + if strings.IndexRune(alphabet, c) < 0 { + return fmt.Errorf("non-alphanumeric string %#q", string(s)) + } + } + return nil +} + +var alphaLen = [3]int{9, 11, 13} + +func (s Alpha) Bits(v Version) int { + return 4 + alphaLen[v.sizeClass()] + (11*len(s)+1)/2 +} + +func (s Alpha) Encode(b *Bits, v Version) { + b.Write(2, 4) + b.Write(uint(len(s)), alphaLen[v.sizeClass()]) + var i int + for i = 0; i+2 <= len(s); i += 2 { + w := uint(strings.IndexRune(alphabet, rune(s[i])))*45 + + uint(strings.IndexRune(alphabet, rune(s[i+1]))) + b.Write(w, 11) + } + + if i < len(s) { + w := uint(strings.IndexRune(alphabet, rune(s[i]))) + b.Write(w, 6) + } +} + +// String is the encoding for 8-bit data. All bytes are valid. +type String string + +func (s String) String() string { + return fmt.Sprintf("String(%#q)", string(s)) +} + +func (s String) Check() error { + return nil +} + +var stringLen = [3]int{8, 16, 16} + +func (s String) Bits(v Version) int { + return 4 + stringLen[v.sizeClass()] + 8*len(s) +} + +func (s String) Encode(b *Bits, v Version) { + b.Write(4, 4) + b.Write(uint(len(s)), stringLen[v.sizeClass()]) + for i := 0; i < len(s); i++ { + b.Write(uint(s[i]), 8) + } +} + +// A Pixel describes a single pixel in a QR code. +type Pixel uint32 + +const ( + Black Pixel = 1 << iota + Invert +) + +func (p Pixel) Offset() uint { + return uint(p >> 6) +} + +func OffsetPixel(o uint) Pixel { + return Pixel(o << 6) +} + +func (r PixelRole) Pixel() Pixel { + return Pixel(r << 2) +} + +func (p Pixel) Role() PixelRole { + return PixelRole(p>>2) & 15 +} + +func (p Pixel) String() string { + s := p.Role().String() + if p&Black != 0 { + s += "+black" + } + if p&Invert != 0 { + s += "+invert" + } + s += "+" + strconv.FormatUint(uint64(p.Offset()), 10) + return s +} + +// A PixelRole describes the role of a QR pixel. +type PixelRole uint32 + +const ( + _ PixelRole = iota + Position // position squares (large) + Alignment // alignment squares (small) + Timing // timing strip between position squares + Format // format metadata + PVersion // version pattern + Unused // unused pixel + Data // data bit + Check // error correction check bit + Extra +) + +var roles = []string{ + "", + "position", + "alignment", + "timing", + "format", + "pversion", + "unused", + "data", + "check", + "extra", +} + +func (r PixelRole) String() string { + if Position <= r && r <= Check { + return roles[r] + } + return strconv.Itoa(int(r)) +} + +// A Level represents a QR error correction level. +// From least to most tolerant of errors, they are L, M, Q, H. +type Level int + +const ( + L Level = iota + M + Q + H +) + +func (l Level) String() string { + if L <= l && l <= H { + return "LMQH"[l : l+1] + } + return strconv.Itoa(int(l)) +} + +// A Code is a square pixel grid. +type Code struct { + Bitmap []byte // 1 is black, 0 is white + Size int // number of pixels on a side + Stride int // number of bytes per row +} + +func (c *Code) Black(x, y int) bool { + return 0 <= x && x < c.Size && 0 <= y && y < c.Size && + c.Bitmap[y*c.Stride+x/8]&(1<= pad { + break + } + b.Write(0x11, 8) + } + } +} + +func (b *Bits) AddCheckBytes(v Version, l Level) { + nd := v.DataBytes(l) + if b.nbit < nd*8 { + b.Pad(nd*8 - b.nbit) + } + if b.nbit != nd*8 { + panic("qr: too much data") + } + + dat := b.Bytes() + vt := &vtab[v] + lev := &vt.level[l] + db := nd / lev.nblock + extra := nd % lev.nblock + chk := make([]byte, lev.check) + rs := gf256.NewRSEncoder(Field, lev.check) + for i := 0; i < lev.nblock; i++ { + if i == lev.nblock-extra { + db++ + } + rs.ECC(dat[:db], chk) + b.Append(chk) + dat = dat[db:] + } + + if len(b.Bytes()) != vt.bytes { + panic("qr: internal error") + } +} + +func (p *Plan) Encode(text ...Encoding) (*Code, error) { + var b Bits + for _, t := range text { + if err := t.Check(); err != nil { + return nil, err + } + t.Encode(&b, p.Version) + } + if b.Bits() > p.DataBytes*8 { + return nil, fmt.Errorf("cannot encode %d bits into %d-bit code", b.Bits(), p.DataBytes*8) + } + b.AddCheckBytes(p.Version, p.Level) + bytes := b.Bytes() + + // Now we have the checksum bytes and the data bytes. + // Construct the actual code. + c := &Code{Size: len(p.Pixel), Stride: (len(p.Pixel) + 7) &^ 7} + c.Bitmap = make([]byte, c.Stride*c.Size) + crow := c.Bitmap + for _, row := range p.Pixel { + for x, pix := range row { + switch pix.Role() { + case Data, Check: + o := pix.Offset() + if bytes[o/8]&(1< 40 { + return nil, fmt.Errorf("invalid QR version %d", int(v)) + } + siz := 17 + int(v)*4 + m := grid(siz) + p.Pixel = m + + // Timing markers (overwritten by boxes). + const ti = 6 // timing is in row/column 6 (counting from 0) + for i := range m { + p := Timing.Pixel() + if i&1 == 0 { + p |= Black + } + m[i][ti] = p + m[ti][i] = p + } + + // Position boxes. + posBox(m, 0, 0) + posBox(m, siz-7, 0) + posBox(m, 0, siz-7) + + // Alignment boxes. + info := &vtab[v] + for x := 4; x+5 < siz; { + for y := 4; y+5 < siz; { + // don't overwrite timing markers + if (x < 7 && y < 7) || (x < 7 && y+5 >= siz-7) || (x+5 >= siz-7 && y < 7) { + } else { + alignBox(m, x, y) + } + if y == 4 { + y = info.apos + } else { + y += info.astride + } + } + if x == 4 { + x = info.apos + } else { + x += info.astride + } + } + + // Version pattern. + pat := vtab[v].pattern + if pat != 0 { + v := pat + for x := 0; x < 6; x++ { + for y := 0; y < 3; y++ { + p := PVersion.Pixel() + if v&1 != 0 { + p |= Black + } + m[siz-11+y][x] = p + m[x][siz-11+y] = p + v >>= 1 + } + } + } + + // One lonely black pixel + m[siz-8][8] = Unused.Pixel() | Black + + return p, nil +} + +// fplan adds the format pixels +func fplan(l Level, m Mask, p *Plan) error { + // Format pixels. + fb := uint32(l^1) << 13 // level: L=01, M=00, Q=11, H=10 + fb |= uint32(m) << 10 // mask + const formatPoly = 0x537 + rem := fb + for i := 14; i >= 10; i-- { + if rem&(1<>i)&1 == 1 { + pix |= Black + } + if (invert>>i)&1 == 1 { + pix ^= Invert | Black + } + // top left + switch { + case i < 6: + p.Pixel[i][8] = pix + case i < 8: + p.Pixel[i+1][8] = pix + case i < 9: + p.Pixel[8][7] = pix + default: + p.Pixel[8][14-i] = pix + } + // bottom right + switch { + case i < 8: + p.Pixel[8][siz-1-int(i)] = pix + default: + p.Pixel[siz-1-int(14-i)][8] = pix + } + } + return nil +} + +// lplan edits a version-only Plan to add information +// about the error correction levels. +func lplan(v Version, l Level, p *Plan) error { + p.Level = l + + nblock := vtab[v].level[l].nblock + ne := vtab[v].level[l].check + nde := (vtab[v].bytes - ne*nblock) / nblock + extra := (vtab[v].bytes - ne*nblock) % nblock + dataBits := (nde*nblock + extra) * 8 + checkBits := ne * nblock * 8 + + p.DataBytes = vtab[v].bytes - ne*nblock + p.CheckBytes = ne * nblock + p.Blocks = nblock + + // Make data + checksum pixels. + data := make([]Pixel, dataBits) + for i := range data { + data[i] = Data.Pixel() | OffsetPixel(uint(i)) + } + check := make([]Pixel, checkBits) + for i := range check { + check[i] = Check.Pixel() | OffsetPixel(uint(i+dataBits)) + } + + // Split into blocks. + dataList := make([][]Pixel, nblock) + checkList := make([][]Pixel, nblock) + for i := 0; i < nblock; i++ { + // The last few blocks have an extra data byte (8 pixels). + nd := nde + if i >= nblock-extra { + nd++ + } + dataList[i], data = data[0:nd*8], data[nd*8:] + checkList[i], check = check[0:ne*8], check[ne*8:] + } + if len(data) != 0 || len(check) != 0 { + panic("data/check math") + } + + // Build up bit sequence, taking first byte of each block, + // then second byte, and so on. Then checksums. + bits := make([]Pixel, dataBits+checkBits) + dst := bits + for i := 0; i < nde+1; i++ { + for _, b := range dataList { + if i*8 < len(b) { + copy(dst, b[i*8:(i+1)*8]) + dst = dst[8:] + } + } + } + for i := 0; i < ne; i++ { + for _, b := range checkList { + if i*8 < len(b) { + copy(dst, b[i*8:(i+1)*8]) + dst = dst[8:] + } + } + } + if len(dst) != 0 { + panic("dst math") + } + + // Sweep up pair of columns, + // then down, assigning to right then left pixel. + // Repeat. + // See Figure 2 of http://www.pclviewer.com/rs2/qrtopology.htm + siz := len(p.Pixel) + rem := make([]Pixel, 7) + for i := range rem { + rem[i] = Extra.Pixel() + } + src := append(bits, rem...) + for x := siz; x > 0; { + for y := siz - 1; y >= 0; y-- { + if p.Pixel[y][x-1].Role() == 0 { + p.Pixel[y][x-1], src = src[0], src[1:] + } + if p.Pixel[y][x-2].Role() == 0 { + p.Pixel[y][x-2], src = src[0], src[1:] + } + } + x -= 2 + if x == 7 { // vertical timing strip + x-- + } + for y := 0; y < siz; y++ { + if p.Pixel[y][x-1].Role() == 0 { + p.Pixel[y][x-1], src = src[0], src[1:] + } + if p.Pixel[y][x-2].Role() == 0 { + p.Pixel[y][x-2], src = src[0], src[1:] + } + } + x -= 2 + } + return nil +} + +// mplan edits a version+level-only Plan to add the mask. +func mplan(m Mask, p *Plan) error { + p.Mask = m + for y, row := range p.Pixel { + for x, pix := range row { + if r := pix.Role(); (r == Data || r == Check || r == Extra) && p.Mask.Invert(y, x) { + row[x] ^= Black | Invert + } + } + } + return nil +} + +// posBox draws a position (large) box at upper left x, y. +func posBox(m [][]Pixel, x, y int) { + pos := Position.Pixel() + // box + for dy := 0; dy < 7; dy++ { + for dx := 0; dx < 7; dx++ { + p := pos + if dx == 0 || dx == 6 || dy == 0 || dy == 6 || 2 <= dx && dx <= 4 && 2 <= dy && dy <= 4 { + p |= Black + } + m[y+dy][x+dx] = p + } + } + // white border + for dy := -1; dy < 8; dy++ { + if 0 <= y+dy && y+dy < len(m) { + if x > 0 { + m[y+dy][x-1] = pos + } + if x+7 < len(m) { + m[y+dy][x+7] = pos + } + } + } + for dx := -1; dx < 8; dx++ { + if 0 <= x+dx && x+dx < len(m) { + if y > 0 { + m[y-1][x+dx] = pos + } + if y+7 < len(m) { + m[y+7][x+dx] = pos + } + } + } +} + +// alignBox draw an alignment (small) box at upper left x, y. +func alignBox(m [][]Pixel, x, y int) { + // box + align := Alignment.Pixel() + for dy := 0; dy < 5; dy++ { + for dx := 0; dx < 5; dx++ { + p := align + if dx == 0 || dx == 4 || dy == 0 || dy == 4 || dx == 2 && dy == 2 { + p |= Black + } + m[y+dy][x+dx] = p + } + } +} diff --git a/vendor/github.com/chai2010/image/qrencoder/internal/gf256/blog_test.go b/vendor/github.com/chai2010/image/qrencoder/internal/gf256/blog_test.go new file mode 100644 index 0000000..4fbf7ec --- /dev/null +++ b/vendor/github.com/chai2010/image/qrencoder/internal/gf256/blog_test.go @@ -0,0 +1,85 @@ +// Copyright 2012 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// This file contains a straightforward implementation of +// Reed-Solomon encoding, along with a benchmark. +// It goes with http://research.swtch.com/field. +// +// For an optimized implementation, see gf256.go. + +package gf256 + +import ( + "bytes" + "fmt" + "testing" +) + +// BlogECC writes to check the error correcting code bytes +// for data using the given Reed-Solomon parameters. +func BlogECC(rs *RSEncoder, m []byte, check []byte) { + if len(check) < rs.c { + panic("gf256: invalid check byte length") + } + if rs.c == 0 { + return + } + + // The check bytes are the remainder after dividing + // data padded with c zeros by the generator polynomial. + + // p = data padded with c zeros. + var p []byte + n := len(m) + rs.c + if len(rs.p) >= n { + p = rs.p + } else { + p = make([]byte, n) + } + copy(p, m) + for i := len(m); i < len(p); i++ { + p[i] = 0 + } + + gen := rs.gen + + // Divide p by gen, leaving the remainder in p[len(data):]. + // p[0] is the most significant term in p, and + // gen[0] is the most significant term in the generator. + for i := 0; i < len(m); i++ { + k := f.Mul(p[i], f.Inv(gen[0])) // k = pi / g0 + // p -= k·g + for j, g := range gen { + p[i+j] = f.Add(p[i+j], f.Mul(k, g)) + } + } + + copy(check, p[len(m):]) + rs.p = p +} + +func BenchmarkBlogECC(b *testing.B) { + data := []byte{0x10, 0x20, 0x0c, 0x56, 0x61, 0x80, 0xec, 0x11, 0xec, 0x11, 0xec, 0x11, 0xec, 0x11, 0xec, 0x11, 0x10, 0x20, 0x0c, 0x56, 0x61, 0x80, 0xec, 0x11, 0xec, 0x11, 0xec, 0x11, 0xec, 0x11, 0xec, 0x11} + check := []byte{0x29, 0x41, 0xb3, 0x93, 0x8, 0xe8, 0xa3, 0xe7, 0x63, 0x8f} + out := make([]byte, len(check)) + rs := NewRSEncoder(f, len(check)) + for i := 0; i < b.N; i++ { + BlogECC(rs, data, out) + } + b.SetBytes(int64(len(data))) + if !bytes.Equal(out, check) { + fmt.Printf("have %#v want %#v\n", out, check) + } +} + +func TestBlogECC(t *testing.T) { + data := []byte{0x10, 0x20, 0x0c, 0x56, 0x61, 0x80, 0xec, 0x11, 0xec, 0x11, 0xec, 0x11, 0xec, 0x11, 0xec, 0x11} + check := []byte{0xa5, 0x24, 0xd4, 0xc1, 0xed, 0x36, 0xc7, 0x87, 0x2c, 0x55} + out := make([]byte, len(check)) + rs := NewRSEncoder(f, len(check)) + BlogECC(rs, data, out) + if !bytes.Equal(out, check) { + t.Errorf("have %x want %x", out, check) + } +} diff --git a/vendor/github.com/chai2010/image/qrencoder/internal/gf256/gf256.go b/vendor/github.com/chai2010/image/qrencoder/internal/gf256/gf256.go new file mode 100644 index 0000000..bfeeeb3 --- /dev/null +++ b/vendor/github.com/chai2010/image/qrencoder/internal/gf256/gf256.go @@ -0,0 +1,241 @@ +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package gf256 implements arithmetic over the Galois Field GF(256). +package gf256 + +import "strconv" + +// A Field represents an instance of GF(256) defined by a specific polynomial. +type Field struct { + log [256]byte // log[0] is unused + exp [510]byte +} + +// NewField returns a new field corresponding to the polynomial poly +// and generator α. The Reed-Solomon encoding in QR codes uses +// polynomial 0x11d with generator 2. +// +// The choice of generator α only affects the Exp and Log operations. +func NewField(poly, α int) *Field { + if poly < 0x100 || poly >= 0x200 || reducible(poly) { + panic("gf256: invalid polynomial: " + strconv.Itoa(poly)) + } + + var f Field + x := 1 + for i := 0; i < 255; i++ { + if x == 1 && i != 0 { + panic("gf256: invalid generator " + strconv.Itoa(α) + + " for polynomial " + strconv.Itoa(poly)) + } + f.exp[i] = byte(x) + f.exp[i+255] = byte(x) + f.log[x] = byte(i) + x = mul(x, α, poly) + } + f.log[0] = 255 + for i := 0; i < 255; i++ { + if f.log[f.exp[i]] != byte(i) { + panic("bad log") + } + if f.log[f.exp[i+255]] != byte(i) { + panic("bad log") + } + } + for i := 1; i < 256; i++ { + if f.exp[f.log[i]] != byte(i) { + panic("bad log") + } + } + + return &f +} + +// nbit returns the number of significant in p. +func nbit(p int) uint { + n := uint(0) + for ; p > 0; p >>= 1 { + n++ + } + return n +} + +// polyDiv divides the polynomial p by q and returns the remainder. +func polyDiv(p, q int) int { + np := nbit(p) + nq := nbit(q) + for ; np >= nq; np-- { + if p&(1<<(np-1)) != 0 { + p ^= q << (np - nq) + } + } + return p +} + +// mul returns the product x*y mod poly, a GF(256) multiplication. +func mul(x, y, poly int) int { + z := 0 + for x > 0 { + if x&1 != 0 { + z ^= y + } + x >>= 1 + y <<= 1 + if y&0x100 != 0 { + y ^= poly + } + } + return z +} + +// reducible reports whether p is reducible. +func reducible(p int) bool { + // Multiplying n-bit * n-bit produces (2n-1)-bit, + // so if p is reducible, one of its factors must be + // of np/2+1 bits or fewer. + np := nbit(p) + for q := 2; q < 1<<(np/2+1); q++ { + if polyDiv(p, q) == 0 { + return true + } + } + return false +} + +// Add returns the sum of x and y in the field. +func (f *Field) Add(x, y byte) byte { + return x ^ y +} + +// Exp returns the base-α exponential of e in the field. +// If e < 0, Exp returns 0. +func (f *Field) Exp(e int) byte { + if e < 0 { + return 0 + } + return f.exp[e%255] +} + +// Log returns the base-α logarithm of x in the field. +// If x == 0, Log returns -1. +func (f *Field) Log(x byte) int { + if x == 0 { + return -1 + } + return int(f.log[x]) +} + +// Inv returns the multiplicative inverse of x in the field. +// If x == 0, Inv returns 0. +func (f *Field) Inv(x byte) byte { + if x == 0 { + return 0 + } + return f.exp[255-f.log[x]] +} + +// Mul returns the product of x and y in the field. +func (f *Field) Mul(x, y byte) byte { + if x == 0 || y == 0 { + return 0 + } + return f.exp[int(f.log[x])+int(f.log[y])] +} + +// An RSEncoder implements Reed-Solomon encoding +// over a given field using a given number of error correction bytes. +type RSEncoder struct { + f *Field + c int + gen []byte + lgen []byte + p []byte +} + +func (f *Field) gen(e int) (gen, lgen []byte) { + // p = 1 + p := make([]byte, e+1) + p[e] = 1 + + for i := 0; i < e; i++ { + // p *= (x + Exp(i)) + // p[j] = p[j]*Exp(i) + p[j+1]. + c := f.Exp(i) + for j := 0; j < e; j++ { + p[j] = f.Mul(p[j], c) ^ p[j+1] + } + p[e] = f.Mul(p[e], c) + } + + // lp = log p. + lp := make([]byte, e+1) + for i, c := range p { + if c == 0 { + lp[i] = 255 + } else { + lp[i] = byte(f.Log(c)) + } + } + + return p, lp +} + +// NewRSEncoder returns a new Reed-Solomon encoder +// over the given field and number of error correction bytes. +func NewRSEncoder(f *Field, c int) *RSEncoder { + gen, lgen := f.gen(c) + return &RSEncoder{f: f, c: c, gen: gen, lgen: lgen} +} + +// ECC writes to check the error correcting code bytes +// for data using the given Reed-Solomon parameters. +func (rs *RSEncoder) ECC(data []byte, check []byte) { + if len(check) < rs.c { + panic("gf256: invalid check byte length") + } + if rs.c == 0 { + return + } + + // The check bytes are the remainder after dividing + // data padded with c zeros by the generator polynomial. + + // p = data padded with c zeros. + var p []byte + n := len(data) + rs.c + if len(rs.p) >= n { + p = rs.p + } else { + p = make([]byte, n) + } + copy(p, data) + for i := len(data); i < len(p); i++ { + p[i] = 0 + } + + // Divide p by gen, leaving the remainder in p[len(data):]. + // p[0] is the most significant term in p, and + // gen[0] is the most significant term in the generator, + // which is always 1. + // To avoid repeated work, we store various values as + // lv, not v, where lv = log[v]. + f := rs.f + lgen := rs.lgen[1:] + for i := 0; i < len(data); i++ { + c := p[i] + if c == 0 { + continue + } + q := p[i+1:] + exp := f.exp[f.log[c]:] + for j, lg := range lgen { + if lg != 255 { // lgen uses 255 for log 0 + q[j] ^= exp[lg] + } + } + } + copy(check, p[len(data):]) + rs.p = p +} diff --git a/vendor/github.com/chai2010/image/qrencoder/internal/gf256/gf256_test.go b/vendor/github.com/chai2010/image/qrencoder/internal/gf256/gf256_test.go new file mode 100644 index 0000000..f77fa7d --- /dev/null +++ b/vendor/github.com/chai2010/image/qrencoder/internal/gf256/gf256_test.go @@ -0,0 +1,194 @@ +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package gf256 + +import ( + "bytes" + "fmt" + "testing" +) + +var f = NewField(0x11d, 2) // x^8 + x^4 + x^3 + x^2 + 1 + +func TestBasic(t *testing.T) { + if f.Exp(0) != 1 || f.Exp(1) != 2 || f.Exp(255) != 1 { + panic("bad Exp") + } +} + +func TestECC(t *testing.T) { + data := []byte{0x10, 0x20, 0x0c, 0x56, 0x61, 0x80, 0xec, 0x11, 0xec, 0x11, 0xec, 0x11, 0xec, 0x11, 0xec, 0x11} + check := []byte{0xa5, 0x24, 0xd4, 0xc1, 0xed, 0x36, 0xc7, 0x87, 0x2c, 0x55} + out := make([]byte, len(check)) + rs := NewRSEncoder(f, len(check)) + rs.ECC(data, out) + if !bytes.Equal(out, check) { + t.Errorf("have %x want %x", out, check) + } +} + +func TestLinear(t *testing.T) { + d1 := []byte{0x00, 0x00} + c1 := []byte{0x00, 0x00} + out := make([]byte, len(c1)) + rs := NewRSEncoder(f, len(c1)) + if rs.ECC(d1, out); !bytes.Equal(out, c1) { + t.Errorf("ECBytes(%x, %d) = %x, want 0", d1, len(c1), out) + } + d2 := []byte{0x00, 0x01} + c2 := make([]byte, 2) + rs.ECC(d2, c2) + d3 := []byte{0x00, 0x02} + c3 := make([]byte, 2) + rs.ECC(d3, c3) + cx := make([]byte, 2) + for i := range cx { + cx[i] = c2[i] ^ c3[i] + } + d4 := []byte{0x00, 0x03} + c4 := make([]byte, 2) + rs.ECC(d4, c4) + if !bytes.Equal(cx, c4) { + t.Errorf("ECBytes(%x, 2) = %x\nECBytes(%x, 2) = %x\nxor = %x\nECBytes(%x, 2) = %x", + d2, c2, d3, c3, cx, d4, c4) + } +} + +func TestGaussJordan(t *testing.T) { + rs := NewRSEncoder(f, 2) + m := make([][]byte, 16) + for i := range m { + m[i] = make([]byte, 4) + m[i][i/8] = 1 << uint(i%8) + rs.ECC(m[i][:2], m[i][2:]) + } + if false { + fmt.Printf("---\n") + for _, row := range m { + fmt.Printf("%x\n", row) + } + } + b := []uint{0, 1, 2, 3, 12, 13, 14, 15, 20, 21, 22, 23, 24, 25, 26, 27} + for i := 0; i < 16; i++ { + bi := b[i] + if m[i][bi/8]&(1<<(7-bi%8)) == 0 { + for j := i + 1; ; j++ { + if j >= len(m) { + t.Errorf("lost track for %d", bi) + break + } + if m[j][bi/8]&(1<<(7-bi%8)) != 0 { + m[i], m[j] = m[j], m[i] + break + } + } + } + for j := i + 1; j < len(m); j++ { + if m[j][bi/8]&(1<<(7-bi%8)) != 0 { + for k := range m[j] { + m[j][k] ^= m[i][k] + } + } + } + } + if false { + fmt.Printf("---\n") + for _, row := range m { + fmt.Printf("%x\n", row) + } + } + for i := 15; i >= 0; i-- { + bi := b[i] + for j := i - 1; j >= 0; j-- { + if m[j][bi/8]&(1<<(7-bi%8)) != 0 { + for k := range m[j] { + m[j][k] ^= m[i][k] + } + } + } + } + if false { + fmt.Printf("---\n") + for _, row := range m { + fmt.Printf("%x", row) + out := make([]byte, 2) + if rs.ECC(row[:2], out); !bytes.Equal(out, row[2:]) { + fmt.Printf(" - want %x", out) + } + fmt.Printf("\n") + } + } +} + +func BenchmarkECC(b *testing.B) { + data := []byte{0x10, 0x20, 0x0c, 0x56, 0x61, 0x80, 0xec, 0x11, 0xec, 0x11, 0xec, 0x11, 0xec, 0x11, 0xec, 0x11, 0x10, 0x20, 0x0c, 0x56, 0x61, 0x80, 0xec, 0x11, 0xec, 0x11, 0xec, 0x11, 0xec, 0x11, 0xec, 0x11} + check := []byte{0x29, 0x41, 0xb3, 0x93, 0x8, 0xe8, 0xa3, 0xe7, 0x63, 0x8f} + out := make([]byte, len(check)) + rs := NewRSEncoder(f, len(check)) + for i := 0; i < b.N; i++ { + rs.ECC(data, out) + } + b.SetBytes(int64(len(data))) + if !bytes.Equal(out, check) { + fmt.Printf("have %#v want %#v\n", out, check) + } +} + +func TestGen(t *testing.T) { + for i := 0; i < 256; i++ { + _, lg := f.gen(i) + if lg[0] != 0 { + t.Errorf("#%d: %x", i, lg) + } + } +} + +func TestReducible(t *testing.T) { + var count = []int{1, 2, 3, 6, 9, 18, 30, 56, 99, 186} // oeis.org/A1037 + for i, want := range count { + n := 0 + for p := 1 << uint(i+2); p < 1<") + +func (w *pngWriter) writeChunk(name string, data []byte) { + if w.crc == nil { + w.crc = crc32.NewIEEE() + } + binary.BigEndian.PutUint32(w.wctmp[0:4], uint32(len(data))) + w.buf.Write(w.wctmp[0:4]) + w.crc.Reset() + copy(w.wctmp[0:4], name) + w.buf.Write(w.wctmp[0:4]) + w.crc.Write(w.wctmp[0:4]) + w.buf.Write(data) + w.crc.Write(data) + crc := w.crc.Sum32() + binary.BigEndian.PutUint32(w.wctmp[0:4], crc) + w.buf.Write(w.wctmp[0:4]) +} + +func (b *bitWriter) writeCode(c *Code) { + const ftNone = 0 + + b.adler32.Reset() + b.bytes.Reset() + b.nbit = 0 + + scale := c.Scale + siz := c.Size + + // zlib header + b.tmp[0] = 0x78 + b.tmp[1] = 0 + b.tmp[1] += uint8(31 - (uint16(b.tmp[0])<<8+uint16(b.tmp[1]))%31) + b.bytes.Write(b.tmp[0:2]) + + // Start flate block. + b.writeBits(1, 1, false) // final block + b.writeBits(1, 2, false) // compressed, fixed Huffman tables + + // White border. + // First row. + b.byte(ftNone) + n := (scale*(siz+8) + 7) / 8 + b.byte(255) + b.repeat(n-1, 1) + // 4*scale rows total. + b.repeat((4*scale-1)*(1+n), 1+n) + + for i := 0; i < 4*scale; i++ { + b.adler32.WriteNByte(ftNone, 1) + b.adler32.WriteNByte(255, n) + } + + row := make([]byte, 1+n) + for y := 0; y < siz; y++ { + row[0] = ftNone + j := 1 + var z uint8 + nz := 0 + for x := -4; x < siz+4; x++ { + // Raw data. + for i := 0; i < scale; i++ { + z <<= 1 + if !c.Black(x, y) { + z |= 1 + } + if nz++; nz == 8 { + row[j] = z + j++ + nz = 0 + } + } + } + if j < len(row) { + row[j] = z + } + for _, z := range row { + b.byte(z) + } + + // Scale-1 copies. + b.repeat((scale-1)*(1+n), 1+n) + + b.adler32.WriteN(row, scale) + } + + // White border. + // First row. + b.byte(ftNone) + b.byte(255) + b.repeat(n-1, 1) + // 4*scale rows total. + b.repeat((4*scale-1)*(1+n), 1+n) + + for i := 0; i < 4*scale; i++ { + b.adler32.WriteNByte(ftNone, 1) + b.adler32.WriteNByte(255, n) + } + + // End of block. + b.hcode(256) + b.flushBits() + + // adler32 + binary.BigEndian.PutUint32(b.tmp[0:], b.adler32.Sum32()) + b.bytes.Write(b.tmp[0:4]) +} + +// A bitWriter is a write buffer for bit-oriented data like deflate. +type bitWriter struct { + bytes bytes.Buffer + bit uint32 + nbit uint + + tmp [4]byte + adler32 adigest +} + +func (b *bitWriter) writeBits(bit uint32, nbit uint, rev bool) { + // reverse, for huffman codes + if rev { + br := uint32(0) + for i := uint(0); i < nbit; i++ { + br |= ((bit >> i) & 1) << (nbit - 1 - i) + } + bit = br + } + b.bit |= bit << b.nbit + b.nbit += nbit + for b.nbit >= 8 { + b.bytes.WriteByte(byte(b.bit)) + b.bit >>= 8 + b.nbit -= 8 + } +} + +func (b *bitWriter) flushBits() { + if b.nbit > 0 { + b.bytes.WriteByte(byte(b.bit)) + b.nbit = 0 + b.bit = 0 + } +} + +func (b *bitWriter) hcode(v int) { + /* + Lit Value Bits Codes + --------- ---- ----- + 0 - 143 8 00110000 through + 10111111 + 144 - 255 9 110010000 through + 111111111 + 256 - 279 7 0000000 through + 0010111 + 280 - 287 8 11000000 through + 11000111 + */ + switch { + case v <= 143: + b.writeBits(uint32(v)+0x30, 8, true) + case v <= 255: + b.writeBits(uint32(v-144)+0x190, 9, true) + case v <= 279: + b.writeBits(uint32(v-256)+0, 7, true) + case v <= 287: + b.writeBits(uint32(v-280)+0xc0, 8, true) + default: + panic("invalid hcode") + } +} + +func (b *bitWriter) byte(x byte) { + b.hcode(int(x)) +} + +func (b *bitWriter) codex(c int, val int, nx uint) { + b.hcode(c + val>>nx) + b.writeBits(uint32(val)&(1<= 258+3; n -= 258 { + b.repeat1(258, d) + } + if n > 258 { + // 258 < n < 258+3 + b.repeat1(10, d) + b.repeat1(n-10, d) + return + } + if n < 3 { + panic("invalid flate repeat") + } + b.repeat1(n, d) +} + +func (b *bitWriter) repeat1(n, d int) { + /* + Extra Extra Extra + Code Bits Length(s) Code Bits Lengths Code Bits Length(s) + ---- ---- ------ ---- ---- ------- ---- ---- ------- + 257 0 3 267 1 15,16 277 4 67-82 + 258 0 4 268 1 17,18 278 4 83-98 + 259 0 5 269 2 19-22 279 4 99-114 + 260 0 6 270 2 23-26 280 4 115-130 + 261 0 7 271 2 27-30 281 5 131-162 + 262 0 8 272 2 31-34 282 5 163-194 + 263 0 9 273 3 35-42 283 5 195-226 + 264 0 10 274 3 43-50 284 5 227-257 + 265 1 11,12 275 3 51-58 285 0 258 + 266 1 13,14 276 3 59-66 + */ + switch { + case n <= 10: + b.codex(257, n-3, 0) + case n <= 18: + b.codex(265, n-11, 1) + case n <= 34: + b.codex(269, n-19, 2) + case n <= 66: + b.codex(273, n-35, 3) + case n <= 130: + b.codex(277, n-67, 4) + case n <= 257: + b.codex(281, n-131, 5) + case n == 258: + b.hcode(285) + default: + panic("invalid repeat length") + } + + /* + Extra Extra Extra + Code Bits Dist Code Bits Dist Code Bits Distance + ---- ---- ---- ---- ---- ------ ---- ---- -------- + 0 0 1 10 4 33-48 20 9 1025-1536 + 1 0 2 11 4 49-64 21 9 1537-2048 + 2 0 3 12 5 65-96 22 10 2049-3072 + 3 0 4 13 5 97-128 23 10 3073-4096 + 4 1 5,6 14 6 129-192 24 11 4097-6144 + 5 1 7,8 15 6 193-256 25 11 6145-8192 + 6 2 9-12 16 7 257-384 26 12 8193-12288 + 7 2 13-16 17 7 385-512 27 12 12289-16384 + 8 3 17-24 18 8 513-768 28 13 16385-24576 + 9 3 25-32 19 8 769-1024 29 13 24577-32768 + */ + if d <= 4 { + b.writeBits(uint32(d-1), 5, true) + } else if d <= 32768 { + nbit := uint(16) + for d <= 1<<(nbit-1) { + nbit-- + } + v := uint32(d - 1) + v &^= 1 << (nbit - 1) // top bit is implicit + code := uint32(2*nbit - 2) // second bit is low bit of code + code |= v >> (nbit - 2) + v &^= 1 << (nbit - 2) + b.writeBits(code, 5, true) + // rest of bits follow + b.writeBits(uint32(v), nbit-2, false) + } else { + panic("invalid repeat distance") + } +} + +func (b *bitWriter) run(v byte, n int) { + if n == 0 { + return + } + b.byte(v) + if n-1 < 3 { + for i := 0; i < n-1; i++ { + b.byte(v) + } + } else { + b.repeat(n-1, 1) + } +} + +type adigest struct { + a, b uint32 +} + +func (d *adigest) Reset() { d.a, d.b = 1, 0 } + +const amod = 65521 + +func aupdate(a, b uint32, pi byte, n int) (aa, bb uint32) { + // TODO(rsc): 6g doesn't do magic multiplies for b %= amod, + // only for b = b%amod. + + // invariant: a, b < amod + if pi == 0 { + b += uint32(n%amod) * a + b = b % amod + return a, b + } + + // n times: + // a += pi + // b += a + // is same as + // b += n*a + n*(n+1)/2*pi + // a += n*pi + m := uint32(n) + b += (m % amod) * a + b = b % amod + b += (m * (m + 1) / 2) % amod * uint32(pi) + b = b % amod + a += (m % amod) * uint32(pi) + a = a % amod + return a, b +} + +func afinish(a, b uint32) uint32 { + return b<<16 | a +} + +func (d *adigest) WriteN(p []byte, n int) { + for i := 0; i < n; i++ { + for _, pi := range p { + d.a, d.b = aupdate(d.a, d.b, pi, 1) + } + } +} + +func (d *adigest) WriteNByte(pi byte, n int) { + d.a, d.b = aupdate(d.a, d.b, pi, n) +} + +func (d *adigest) Sum32() uint32 { return afinish(d.a, d.b) } diff --git a/vendor/github.com/chai2010/image/qrencoder/png_test.go b/vendor/github.com/chai2010/image/qrencoder/png_test.go new file mode 100644 index 0000000..24598e6 --- /dev/null +++ b/vendor/github.com/chai2010/image/qrencoder/png_test.go @@ -0,0 +1,69 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package qrcode + +import ( + "bytes" + "image" + "image/color" + "image/png" + "testing" +) + +func TestPNG(t *testing.T) { + c, err := Encode("hello, world", L) + if err != nil { + t.Fatal(err) + } + pngdat := c.PNG() + m, err := png.Decode(bytes.NewBuffer(pngdat)) + if err != nil { + t.Fatal(err) + } + gm := m.(*image.Gray) + + scale := c.Scale + siz := c.Size + nbad := 0 + for y := 0; y < scale*(8+siz); y++ { + for x := 0; x < scale*(8+siz); x++ { + v := byte(255) + if c.Black(x/scale-4, y/scale-4) { + v = 0 + } + if gv := gm.At(x, y).(color.Gray).Y; gv != v { + t.Errorf("%d,%d = %d, want %d", x, y, gv, v) + if nbad++; nbad >= 20 { + t.Fatalf("too many bad pixels") + } + } + } + } +} + +func BenchmarkPNG(b *testing.B) { + c, err := Encode("0123456789012345678901234567890123456789", L) + if err != nil { + panic(err) + } + var bytes []byte + for i := 0; i < b.N; i++ { + bytes = c.PNG() + } + b.SetBytes(int64(len(bytes))) +} + +func BenchmarkImagePNG(b *testing.B) { + c, err := Encode("0123456789012345678901234567890123456789", L) + if err != nil { + panic(err) + } + var buf bytes.Buffer + for i := 0; i < b.N; i++ { + buf.Reset() + png.Encode(&buf, c.Image()) + } + b.SetBytes(int64(buf.Len())) +} diff --git a/vendor/github.com/chai2010/image/qrencoder/qr.go b/vendor/github.com/chai2010/image/qrencoder/qr.go new file mode 100644 index 0000000..0980d56 --- /dev/null +++ b/vendor/github.com/chai2010/image/qrencoder/qr.go @@ -0,0 +1,114 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package qrencoder implements a encoder for QR code. +package qrcode + +import ( + "errors" + "image" + "image/color" + + "github.com/chai2010/image/qrencoder/internal/coding" +) + +// A Level denotes a QR error correction level. +// From least to most tolerant of errors, they are L, M, Q, H. +type Level int + +const ( + L Level = iota // 20% redundant + M // 38% redundant + Q // 55% redundant + H // 65% redundant +) + +// Encode returns an encoding of text at the given error correction level. +func Encode(text string, level Level) (*Code, error) { + // Pick data encoding, smallest first. + // We could split the string and use different encodings + // but that seems like overkill for now. + var enc coding.Encoding + switch { + case coding.Num(text).Check() == nil: + enc = coding.Num(text) + case coding.Alpha(text).Check() == nil: + enc = coding.Alpha(text) + default: + enc = coding.String(text) + } + + // Pick size. + l := coding.Level(level) + var v coding.Version + for v = coding.MinVersion; ; v++ { + if v > coding.MaxVersion { + return nil, errors.New("text too long to encode as QR") + } + if enc.Bits(v) <= v.DataBytes(l)*8 { + break + } + } + + // Build and execute plan. + p, err := coding.NewPlan(v, l, 0) + if err != nil { + return nil, err + } + cc, err := p.Encode(enc) + if err != nil { + return nil, err + } + + // TODO: Pick appropriate mask. + + return &Code{cc.Bitmap, cc.Size, cc.Stride, 8}, nil +} + +// A Code is a square pixel grid. +// It implements image.Image and direct PNG encoding. +type Code struct { + Bitmap []byte // 1 is black, 0 is white + Size int // number of pixels on a side + Stride int // number of bytes per row + Scale int // number of image pixels per QR pixel +} + +// Black returns true if the pixel at (x,y) is black. +func (c *Code) Black(x, y int) bool { + return 0 <= x && x < c.Size && 0 <= y && y < c.Size && + c.Bitmap[y*c.Stride+x/8]&(1< 1 { + fmt.Printf("%d\t%s\n", n, line) + } + } +} + +//!- diff --git a/vendor/gopl.io/ch1/dup2/main.go b/vendor/gopl.io/ch1/dup2/main.go new file mode 100644 index 0000000..e894f84 --- /dev/null +++ b/vendor/gopl.io/ch1/dup2/main.go @@ -0,0 +1,48 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 10. +//!+ + +// Dup2 prints the count and text of lines that appear more than once +// in the input. It reads from stdin or from a list of named files. +package main + +import ( + "bufio" + "fmt" + "os" +) + +func main() { + counts := make(map[string]int) + files := os.Args[1:] + if len(files) == 0 { + countLines(os.Stdin, counts) + } else { + for _, arg := range files { + f, err := os.Open(arg) + if err != nil { + fmt.Fprintf(os.Stderr, "dup2: %v\n", err) + continue + } + countLines(f, counts) + f.Close() + } + } + for line, n := range counts { + if n > 1 { + fmt.Printf("%d\t%s\n", n, line) + } + } +} + +func countLines(f *os.File, counts map[string]int) { + input := bufio.NewScanner(f) + for input.Scan() { + counts[input.Text()]++ + } + // NOTE: ignoring potential errors from input.Err() +} + +//!- diff --git a/vendor/gopl.io/ch1/dup3/main.go b/vendor/gopl.io/ch1/dup3/main.go new file mode 100644 index 0000000..50434d8 --- /dev/null +++ b/vendor/gopl.io/ch1/dup3/main.go @@ -0,0 +1,38 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 12. + +//!+ + +// Dup3 prints the count and text of lines that +// appear more than once in the named input files. +package main + +import ( + "fmt" + "io/ioutil" + "os" + "strings" +) + +func main() { + counts := make(map[string]int) + for _, filename := range os.Args[1:] { + data, err := ioutil.ReadFile(filename) + if err != nil { + fmt.Fprintf(os.Stderr, "dup3: %v\n", err) + continue + } + for _, line := range strings.Split(string(data), "\n") { + counts[line]++ + } + } + for line, n := range counts { + if n > 1 { + fmt.Printf("%d\t%s\n", n, line) + } + } +} + +//!- diff --git a/vendor/gopl.io/ch1/echo1/main.go b/vendor/gopl.io/ch1/echo1/main.go new file mode 100644 index 0000000..e8f8969 --- /dev/null +++ b/vendor/gopl.io/ch1/echo1/main.go @@ -0,0 +1,24 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 4. +//!+ + +// Echo1 prints its command-line arguments. +package main + +import ( + "fmt" + "os" +) + +func main() { + var s, sep string + for i := 1; i < len(os.Args); i++ { + s += sep + os.Args[i] + sep = " " + } + fmt.Println(s) +} + +//!- diff --git a/vendor/gopl.io/ch1/echo2/main.go b/vendor/gopl.io/ch1/echo2/main.go new file mode 100644 index 0000000..82d312c --- /dev/null +++ b/vendor/gopl.io/ch1/echo2/main.go @@ -0,0 +1,24 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 6. +//!+ + +// Echo2 prints its command-line arguments. +package main + +import ( + "fmt" + "os" +) + +func main() { + s, sep := "", "" + for _, arg := range os.Args[1:] { + s += sep + arg + sep = " " + } + fmt.Println(s) +} + +//!- diff --git a/vendor/gopl.io/ch1/echo3/main.go b/vendor/gopl.io/ch1/echo3/main.go new file mode 100644 index 0000000..031a650 --- /dev/null +++ b/vendor/gopl.io/ch1/echo3/main.go @@ -0,0 +1,20 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 8. + +// Echo3 prints its command-line arguments. +package main + +import ( + "fmt" + "os" + "strings" +) + +//!+ +func main() { + fmt.Println(strings.Join(os.Args[1:], " ")) +} + +//!- diff --git a/vendor/gopl.io/ch1/fetch/main.go b/vendor/gopl.io/ch1/fetch/main.go new file mode 100644 index 0000000..e704ae6 --- /dev/null +++ b/vendor/gopl.io/ch1/fetch/main.go @@ -0,0 +1,34 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 16. +//!+ + +// Fetch prints the content found at each specified URL. +package main + +import ( + "fmt" + "io/ioutil" + "net/http" + "os" +) + +func main() { + for _, url := range os.Args[1:] { + resp, err := http.Get(url) + if err != nil { + fmt.Fprintf(os.Stderr, "fetch: %v\n", err) + os.Exit(1) + } + b, err := ioutil.ReadAll(resp.Body) + resp.Body.Close() + if err != nil { + fmt.Fprintf(os.Stderr, "fetch: reading %s: %v\n", url, err) + os.Exit(1) + } + fmt.Printf("%s", b) + } +} + +//!- diff --git a/vendor/gopl.io/ch1/fetchall/main.go b/vendor/gopl.io/ch1/fetchall/main.go new file mode 100644 index 0000000..ad32109 --- /dev/null +++ b/vendor/gopl.io/ch1/fetchall/main.go @@ -0,0 +1,49 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 17. +//!+ + +// Fetchall fetches URLs in parallel and reports their times and sizes. +package main + +import ( + "fmt" + "io" + "io/ioutil" + "net/http" + "os" + "time" +) + +func main() { + start := time.Now() + ch := make(chan string) + for _, url := range os.Args[1:] { + go fetch(url, ch) // start a goroutine + } + for range os.Args[1:] { + fmt.Println(<-ch) // receive from channel ch + } + fmt.Printf("%.2fs elapsed\n", time.Since(start).Seconds()) +} + +func fetch(url string, ch chan<- string) { + start := time.Now() + resp, err := http.Get(url) + if err != nil { + ch <- fmt.Sprint(err) // send to channel ch + return + } + + nbytes, err := io.Copy(ioutil.Discard, resp.Body) + resp.Body.Close() // don't leak resources + if err != nil { + ch <- fmt.Sprintf("while reading %s: %v", url, err) + return + } + secs := time.Since(start).Seconds() + ch <- fmt.Sprintf("%.2fs %7d %s", secs, nbytes, url) +} + +//!- diff --git a/vendor/gopl.io/ch1/helloworld/main.go b/vendor/gopl.io/ch1/helloworld/main.go new file mode 100644 index 0000000..cd5e0ab --- /dev/null +++ b/vendor/gopl.io/ch1/helloworld/main.go @@ -0,0 +1,16 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 1. + +// Helloworld is our first Go program. +//!+ +package main + +import "fmt" + +func main() { + fmt.Println("Hello, 世界") +} + +//!- diff --git a/vendor/gopl.io/ch1/lissajous/main.go b/vendor/gopl.io/ch1/lissajous/main.go new file mode 100644 index 0000000..be50e70 --- /dev/null +++ b/vendor/gopl.io/ch1/lissajous/main.go @@ -0,0 +1,86 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// Run with "web" command-line argument for web server. +// See page 13. +//!+main + +// Lissajous generates GIF animations of random Lissajous figures. +package main + +import ( + "image" + "image/color" + "image/gif" + "io" + "math" + "math/rand" + "os" +) + +//!-main +// Packages not needed by version in book. +import ( + "log" + "net/http" + "time" +) + +//!+main + +var palette = []color.Color{color.White, color.Black} + +const ( + whiteIndex = 0 // first color in palette + blackIndex = 1 // next color in palette +) + +func main() { + //!-main + // The sequence of images is deterministic unless we seed + // the pseudo-random number generator using the current time. + // Thanks to Randall McPherson for pointing out the omission. + rand.Seed(time.Now().UTC().UnixNano()) + + if len(os.Args) > 1 && os.Args[1] == "web" { + //!+http + handler := func(w http.ResponseWriter, r *http.Request) { + lissajous(w) + } + http.HandleFunc("/", handler) + //!-http + log.Fatal(http.ListenAndServe("localhost:8000", nil)) + return + } + //!+main + lissajous(os.Stdout) +} + +func lissajous(out io.Writer) { + const ( + cycles = 5 // number of complete x oscillator revolutions + res = 0.001 // angular resolution + size = 100 // image canvas covers [-size..+size] + nframes = 64 // number of animation frames + delay = 8 // delay between frames in 10ms units + ) + freq := rand.Float64() * 3.0 // relative frequency of y oscillator + anim := gif.GIF{LoopCount: nframes} + phase := 0.0 // phase difference + for i := 0; i < nframes; i++ { + rect := image.Rect(0, 0, 2*size+1, 2*size+1) + img := image.NewPaletted(rect, palette) + for t := 0.0; t < cycles*2*math.Pi; t += res { + x := math.Sin(t) + y := math.Sin(t*freq + phase) + img.SetColorIndex(size+int(x*size+0.5), size+int(y*size+0.5), + blackIndex) + } + phase += 0.1 + anim.Delay = append(anim.Delay, delay) + anim.Image = append(anim.Image, img) + } + gif.EncodeAll(out, &anim) // NOTE: ignoring encoding errors +} + +//!-main diff --git a/vendor/gopl.io/ch1/server1/main.go b/vendor/gopl.io/ch1/server1/main.go new file mode 100644 index 0000000..600e92c --- /dev/null +++ b/vendor/gopl.io/ch1/server1/main.go @@ -0,0 +1,26 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 19. +//!+ + +// Server1 is a minimal "echo" server. +package main + +import ( + "fmt" + "log" + "net/http" +) + +func main() { + http.HandleFunc("/", handler) // each request calls handler + log.Fatal(http.ListenAndServe("localhost:8000", nil)) +} + +// handler echoes the Path component of the requested URL. +func handler(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "URL.Path = %q\n", r.URL.Path) +} + +//!- diff --git a/vendor/gopl.io/ch1/server2/main.go b/vendor/gopl.io/ch1/server2/main.go new file mode 100644 index 0000000..dd8ec57 --- /dev/null +++ b/vendor/gopl.io/ch1/server2/main.go @@ -0,0 +1,41 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 20. +//!+ + +// Server2 is a minimal "echo" and counter server. +package main + +import ( + "fmt" + "log" + "net/http" + "sync" +) + +var mu sync.Mutex +var count int + +func main() { + http.HandleFunc("/", handler) + http.HandleFunc("/count", counter) + log.Fatal(http.ListenAndServe("localhost:8000", nil)) +} + +// handler echoes the Path component of the requested URL. +func handler(w http.ResponseWriter, r *http.Request) { + mu.Lock() + count++ + mu.Unlock() + fmt.Fprintf(w, "URL.Path = %q\n", r.URL.Path) +} + +// counter echoes the number of calls so far. +func counter(w http.ResponseWriter, r *http.Request) { + mu.Lock() + fmt.Fprintf(w, "Count %d\n", count) + mu.Unlock() +} + +//!- diff --git a/vendor/gopl.io/ch1/server3/main.go b/vendor/gopl.io/ch1/server3/main.go new file mode 100644 index 0000000..88c4ad5 --- /dev/null +++ b/vendor/gopl.io/ch1/server3/main.go @@ -0,0 +1,37 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 21. + +// Server3 is an "echo" server that displays request parameters. +package main + +import ( + "fmt" + "log" + "net/http" +) + +func main() { + http.HandleFunc("/", handler) + log.Fatal(http.ListenAndServe("localhost:8000", nil)) +} + +//!+handler +// handler echoes the HTTP request. +func handler(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "%s %s %s\n", r.Method, r.URL, r.Proto) + for k, v := range r.Header { + fmt.Fprintf(w, "Header[%q] = %q\n", k, v) + } + fmt.Fprintf(w, "Host = %q\n", r.Host) + fmt.Fprintf(w, "RemoteAddr = %q\n", r.RemoteAddr) + if err := r.ParseForm(); err != nil { + log.Print(err) + } + for k, v := range r.Form { + fmt.Fprintf(w, "Form[%q] = %q\n", k, v) + } +} + +//!-handler diff --git a/vendor/gopl.io/ch10/cross/main.go b/vendor/gopl.io/ch10/cross/main.go new file mode 100644 index 0000000..1de5cf5 --- /dev/null +++ b/vendor/gopl.io/ch10/cross/main.go @@ -0,0 +1,19 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 295. + +// The cross command prints the values of GOOS and GOARCH for this target. +package main + +import ( + "fmt" + "runtime" +) + +//!+ +func main() { + fmt.Println(runtime.GOOS, runtime.GOARCH) +} + +//!- diff --git a/vendor/gopl.io/ch10/jpeg/main.go b/vendor/gopl.io/ch10/jpeg/main.go new file mode 100644 index 0000000..ca84efb --- /dev/null +++ b/vendor/gopl.io/ch10/jpeg/main.go @@ -0,0 +1,52 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 287. + +//!+main + +// The jpeg command reads a PNG image from the standard input +// and writes it as a JPEG image to the standard output. +package main + +import ( + "fmt" + "image" + "image/jpeg" + _ "image/png" // register PNG decoder + "io" + "os" +) + +func main() { + if err := toJPEG(os.Stdin, os.Stdout); err != nil { + fmt.Fprintf(os.Stderr, "jpeg: %v\n", err) + os.Exit(1) + } +} + +func toJPEG(in io.Reader, out io.Writer) error { + img, kind, err := image.Decode(in) + if err != nil { + return err + } + fmt.Fprintln(os.Stderr, "Input format =", kind) + return jpeg.Encode(out, img, &jpeg.Options{Quality: 95}) +} + +//!-main + +/* +//!+with +$ go build gopl.io/ch3/mandelbrot +$ go build gopl.io/ch10/jpeg +$ ./mandelbrot | ./jpeg >mandelbrot.jpg +Input format = png +//!-with + +//!+without +$ go build gopl.io/ch10/jpeg +$ ./mandelbrot | ./jpeg >mandelbrot.jpg +jpeg: image: unknown format +//!-without +*/ diff --git a/vendor/gopl.io/ch11/echo/echo.go b/vendor/gopl.io/ch11/echo/echo.go new file mode 100644 index 0000000..82b5a3e --- /dev/null +++ b/vendor/gopl.io/ch11/echo/echo.go @@ -0,0 +1,41 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 308. +//!+ + +// Echo prints its command-line arguments. +package main + +import ( + "flag" + "fmt" + "io" + "os" + "strings" +) + +var ( + n = flag.Bool("n", false, "omit trailing newline") + s = flag.String("s", " ", "separator") +) + +var out io.Writer = os.Stdout // modified during testing + +func main() { + flag.Parse() + if err := echo(!*n, *s, flag.Args()); err != nil { + fmt.Fprintf(os.Stderr, "echo: %v\n", err) + os.Exit(1) + } +} + +func echo(newline bool, sep string, args []string) error { + fmt.Fprint(out, strings.Join(args, sep)) + if newline { + fmt.Fprintln(out) + } + return nil +} + +//!- diff --git a/vendor/gopl.io/ch11/echo/echo_test.go b/vendor/gopl.io/ch11/echo/echo_test.go new file mode 100644 index 0000000..7d2f47a --- /dev/null +++ b/vendor/gopl.io/ch11/echo/echo_test.go @@ -0,0 +1,45 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// Test of echo command. Run with: go test gopl.io/ch11/echo + +//!+ +package main + +import ( + "bytes" + "fmt" + "testing" +) + +func TestEcho(t *testing.T) { + var tests = []struct { + newline bool + sep string + args []string + want string + }{ + {true, "", []string{}, "\n"}, + {false, "", []string{}, ""}, + {true, "\t", []string{"one", "two", "three"}, "one\ttwo\tthree\n"}, + {true, ",", []string{"a", "b", "c"}, "a,b,c\n"}, + {false, ":", []string{"1", "2", "3"}, "1:2:3"}, + } + + for _, test := range tests { + descr := fmt.Sprintf("echo(%v, %q, %q)", + test.newline, test.sep, test.args) + + out = new(bytes.Buffer) // captured output + if err := echo(test.newline, test.sep, test.args); err != nil { + t.Errorf("%s failed: %v", descr, err) + continue + } + got := out.(*bytes.Buffer).String() + if got != test.want { + t.Errorf("%s = %q, want %q", descr, got, test.want) + } + } +} + +//!- diff --git a/vendor/gopl.io/ch11/storage1/storage.go b/vendor/gopl.io/ch11/storage1/storage.go new file mode 100644 index 0000000..ed8982c --- /dev/null +++ b/vendor/gopl.io/ch11/storage1/storage.go @@ -0,0 +1,45 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 311. + +// Package storage is part of a hypothetical cloud storage server. +//!+main +package storage + +import ( + "fmt" + "log" + "net/smtp" +) + +var usage = make(map[string]int64) + +func bytesInUse(username string) int64 { return usage[username] } + +// Email sender configuration. +// NOTE: never put passwords in source code! +const sender = "notifications@example.com" +const password = "correcthorsebatterystaple" +const hostname = "smtp.example.com" + +const template = `Warning: you are using %d bytes of storage, +%d%% of your quota.` + +func CheckQuota(username string) { + used := bytesInUse(username) + const quota = 1000000000 // 1GB + percent := 100 * used / quota + if percent < 90 { + return // OK + } + msg := fmt.Sprintf(template, used, percent) + auth := smtp.PlainAuth("", sender, password, hostname) + err := smtp.SendMail(hostname+":587", auth, sender, + []string{username}, []byte(msg)) + if err != nil { + log.Printf("smtp.SendMail(%s) failed: %s", username, err) + } +} + +//!-main diff --git a/vendor/gopl.io/ch11/storage2/quota_test.go b/vendor/gopl.io/ch11/storage2/quota_test.go new file mode 100644 index 0000000..2dcd045 --- /dev/null +++ b/vendor/gopl.io/ch11/storage2/quota_test.go @@ -0,0 +1,53 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +//!+test +package storage + +import ( + "strings" + "testing" +) + +func TestCheckQuotaNotifiesUser(t *testing.T) { + var notifiedUser, notifiedMsg string + notifyUser = func(user, msg string) { + notifiedUser, notifiedMsg = user, msg + } + + const user = "joe@example.org" + usage[user] = 980000000 // simulate a 980MB-used condition + + CheckQuota(user) + if notifiedUser == "" && notifiedMsg == "" { + t.Fatalf("notifyUser not called") + } + if notifiedUser != user { + t.Errorf("wrong user (%s) notified, want %s", + notifiedUser, user) + } + const wantSubstring = "98% of your quota" + if !strings.Contains(notifiedMsg, wantSubstring) { + t.Errorf("unexpected notification message <<%s>>, "+ + "want substring %q", notifiedMsg, wantSubstring) + } +} + +//!-test + +/* +//!+defer +func TestCheckQuotaNotifiesUser(t *testing.T) { + // Save and restore original notifyUser. + saved := notifyUser + defer func() { notifyUser = saved }() + + // Install the test's fake notifyUser. + var notifiedUser, notifiedMsg string + notifyUser = func(user, msg string) { + notifiedUser, notifiedMsg = user, msg + } + // ...rest of test... +} +//!-defer +*/ diff --git a/vendor/gopl.io/ch11/storage2/storage.go b/vendor/gopl.io/ch11/storage2/storage.go new file mode 100644 index 0000000..f103fbb --- /dev/null +++ b/vendor/gopl.io/ch11/storage2/storage.go @@ -0,0 +1,49 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 312. + +// Package storage is part of a hypothetical cloud storage server. +package storage + +import ( + "fmt" + "log" + "net/smtp" +) + +var usage = make(map[string]int64) + +func bytesInUse(username string) int64 { return usage[username] } + +// E-mail sender configuration. +// NOTE: never put passwords in source code! +const sender = "notifications@example.com" +const password = "correcthorsebatterystaple" +const hostname = "smtp.example.com" + +const template = `Warning: you are using %d bytes of storage, +%d%% of your quota.` + +//!+factored +var notifyUser = func(username, msg string) { + auth := smtp.PlainAuth("", sender, password, hostname) + err := smtp.SendMail(hostname+":587", auth, sender, + []string{username}, []byte(msg)) + if err != nil { + log.Printf("smtp.SendEmail(%s) failed: %s", username, err) + } +} + +func CheckQuota(username string) { + used := bytesInUse(username) + const quota = 1000000000 // 1GB + percent := 100 * used / quota + if percent < 90 { + return // OK + } + msg := fmt.Sprintf(template, used, percent) + notifyUser(username, msg) +} + +//!-factored diff --git a/vendor/gopl.io/ch11/word1/word.go b/vendor/gopl.io/ch11/word1/word.go new file mode 100644 index 0000000..048abc9 --- /dev/null +++ b/vendor/gopl.io/ch11/word1/word.go @@ -0,0 +1,21 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 303. +//!+ + +// Package word provides utilities for word games. +package word + +// IsPalindrome reports whether s reads the same forward and backward. +// (Our first attempt.) +func IsPalindrome(s string) bool { + for i := range s { + if s[i] != s[len(s)-1-i] { + return false + } + } + return true +} + +//!- diff --git a/vendor/gopl.io/ch11/word1/word_test.go b/vendor/gopl.io/ch11/word1/word_test.go new file mode 100644 index 0000000..bb53404 --- /dev/null +++ b/vendor/gopl.io/ch11/word1/word_test.go @@ -0,0 +1,43 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +//!+test +package word + +import "testing" + +func TestPalindrome(t *testing.T) { + if !IsPalindrome("detartrated") { + t.Error(`IsPalindrome("detartrated") = false`) + } + if !IsPalindrome("kayak") { + t.Error(`IsPalindrome("kayak") = false`) + } +} + +func TestNonPalindrome(t *testing.T) { + if IsPalindrome("palindrome") { + t.Error(`IsPalindrome("palindrome") = true`) + } +} + +//!-test + +// The tests below are expected to fail. +// See package gopl.io/ch11/word2 for the fix. + +//!+more +func TestFrenchPalindrome(t *testing.T) { + if !IsPalindrome("été") { + t.Error(`IsPalindrome("été") = false`) + } +} + +func TestCanalPalindrome(t *testing.T) { + input := "A man, a plan, a canal: Panama" + if !IsPalindrome(input) { + t.Errorf(`IsPalindrome(%q) = false`, input) + } +} + +//!-more diff --git a/vendor/gopl.io/ch11/word2/word.go b/vendor/gopl.io/ch11/word2/word.go new file mode 100644 index 0000000..846c8d2 --- /dev/null +++ b/vendor/gopl.io/ch11/word2/word.go @@ -0,0 +1,29 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 305. +//!+ + +// Package word provides utilities for word games. +package word + +import "unicode" + +// IsPalindrome reports whether s reads the same forward and backward. +// Letter case is ignored, as are non-letters. +func IsPalindrome(s string) bool { + var letters []rune + for _, r := range s { + if unicode.IsLetter(r) { + letters = append(letters, unicode.ToLower(r)) + } + } + for i := range letters { + if letters[i] != letters[len(letters)-1-i] { + return false + } + } + return true +} + +//!- diff --git a/vendor/gopl.io/ch11/word2/word_test.go b/vendor/gopl.io/ch11/word2/word_test.go new file mode 100644 index 0000000..4b17337 --- /dev/null +++ b/vendor/gopl.io/ch11/word2/word_test.go @@ -0,0 +1,148 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +package word + +import ( + "fmt" + "math/rand" + "time" +) + +//!+bench + +import "testing" + +//!-bench + +//!+test +func TestIsPalindrome(t *testing.T) { + var tests = []struct { + input string + want bool + }{ + {"", true}, + {"a", true}, + {"aa", true}, + {"ab", false}, + {"kayak", true}, + {"detartrated", true}, + {"A man, a plan, a canal: Panama", true}, + {"Evil I did dwell; lewd did I live.", true}, + {"Able was I ere I saw Elba", true}, + {"été", true}, + {"Et se resservir, ivresse reste.", true}, + {"palindrome", false}, // non-palindrome + {"desserts", false}, // semi-palindrome + } + for _, test := range tests { + if got := IsPalindrome(test.input); got != test.want { + t.Errorf("IsPalindrome(%q) = %v", test.input, got) + } + } +} + +//!-test + +//!+bench +func BenchmarkIsPalindrome(b *testing.B) { + for i := 0; i < b.N; i++ { + IsPalindrome("A man, a plan, a canal: Panama") + } +} + +//!-bench + +//!+example + +func ExampleIsPalindrome() { + fmt.Println(IsPalindrome("A man, a plan, a canal: Panama")) + fmt.Println(IsPalindrome("palindrome")) + // Output: + // true + // false +} + +//!-example + +/* +//!+random +import "math/rand" + +//!-random +*/ + +//!+random +// randomPalindrome returns a palindrome whose length and contents +// are derived from the pseudo-random number generator rng. +func randomPalindrome(rng *rand.Rand) string { + n := rng.Intn(25) // random length up to 24 + runes := make([]rune, n) + for i := 0; i < (n+1)/2; i++ { + r := rune(rng.Intn(0x1000)) // random rune up to '\u0999' + runes[i] = r + runes[n-1-i] = r + } + return string(runes) +} + +func TestRandomPalindromes(t *testing.T) { + // Initialize a pseudo-random number generator. + seed := time.Now().UTC().UnixNano() + t.Logf("Random seed: %d", seed) + rng := rand.New(rand.NewSource(seed)) + + for i := 0; i < 1000; i++ { + p := randomPalindrome(rng) + if !IsPalindrome(p) { + t.Errorf("IsPalindrome(%q) = false", p) + } + } +} + +//!-random + +/* +// Answer for Exercicse 11.1: Modify randomPalindrome to exercise +// IsPalindrome's handling of punctuation and spaces. + +// WARNING: the conversion r -> upper -> lower doesn't preserve +// the value of r in some cases, e.g., µ Μ, ſ S, ı I + +// randomPalindrome returns a palindrome whose length and contents +// are derived from the pseudo-random number generator rng. +func randomNoisyPalindrome(rng *rand.Rand) string { + n := rng.Intn(25) // random length up to 24 + runes := make([]rune, n) + for i := 0; i < (n+1)/2; i++ { + r := rune(rng.Intn(0x200)) // random rune up to \u99 + runes[i] = r + r1 := r + if unicode.IsLetter(r) && unicode.IsLower(r) { + r = unicode.ToUpper(r) + if unicode.ToLower(r) != r1 { + fmt.Printf("cap? %c %c\n", r1, r) + } + } + runes[n-1-i] = r + } + return "?" + string(runes) + "!" +} + +func TestRandomNoisyPalindromes(t *testing.T) { + // Initialize a pseudo-random number generator. + seed := time.Now().UTC().UnixNano() + t.Logf("Random seed: %d", seed) + rng := rand.New(rand.NewSource(seed)) + + n := 0 + for i := 0; i < 1000; i++ { + p := randomNoisyPalindrome(rng) + if !IsPalindrome(p) { + t.Errorf("IsNoisyPalindrome(%q) = false", p) + n++ + } + } + fmt.Fprintf(os.Stderr, "fail = %d\n", n) +} +*/ diff --git a/vendor/gopl.io/ch12/display/display.go b/vendor/gopl.io/ch12/display/display.go new file mode 100644 index 0000000..933a5b0 --- /dev/null +++ b/vendor/gopl.io/ch12/display/display.go @@ -0,0 +1,90 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 333. + +// Package display provides a means to display structured data. +package display + +import ( + "fmt" + "reflect" + "strconv" +) + +//!+Display + +func Display(name string, x interface{}) { + fmt.Printf("Display %s (%T):\n", name, x) + display(name, reflect.ValueOf(x)) +} + +//!-Display + +// formatAtom formats a value without inspecting its internal structure. +// It is a copy of the the function in gopl.io/ch11/format. +func formatAtom(v reflect.Value) string { + switch v.Kind() { + case reflect.Invalid: + return "invalid" + case reflect.Int, reflect.Int8, reflect.Int16, + reflect.Int32, reflect.Int64: + return strconv.FormatInt(v.Int(), 10) + case reflect.Uint, reflect.Uint8, reflect.Uint16, + reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return strconv.FormatUint(v.Uint(), 10) + // ...floating-point and complex cases omitted for brevity... + case reflect.Bool: + if v.Bool() { + return "true" + } + return "false" + case reflect.String: + return strconv.Quote(v.String()) + case reflect.Chan, reflect.Func, reflect.Ptr, + reflect.Slice, reflect.Map: + return v.Type().String() + " 0x" + + strconv.FormatUint(uint64(v.Pointer()), 16) + default: // reflect.Array, reflect.Struct, reflect.Interface + return v.Type().String() + " value" + } +} + +//!+display +func display(path string, v reflect.Value) { + switch v.Kind() { + case reflect.Invalid: + fmt.Printf("%s = invalid\n", path) + case reflect.Slice, reflect.Array: + for i := 0; i < v.Len(); i++ { + display(fmt.Sprintf("%s[%d]", path, i), v.Index(i)) + } + case reflect.Struct: + for i := 0; i < v.NumField(); i++ { + fieldPath := fmt.Sprintf("%s.%s", path, v.Type().Field(i).Name) + display(fieldPath, v.Field(i)) + } + case reflect.Map: + for _, key := range v.MapKeys() { + display(fmt.Sprintf("%s[%s]", path, + formatAtom(key)), v.MapIndex(key)) + } + case reflect.Ptr: + if v.IsNil() { + fmt.Printf("%s = nil\n", path) + } else { + display(fmt.Sprintf("(*%s)", path), v.Elem()) + } + case reflect.Interface: + if v.IsNil() { + fmt.Printf("%s = nil\n", path) + } else { + fmt.Printf("%s.type = %s\n", path, v.Elem().Type()) + display(path+".value", v.Elem()) + } + default: // basic types, channels, funcs + fmt.Printf("%s = %s\n", path, formatAtom(v)) + } +} + +//!-display diff --git a/vendor/gopl.io/ch12/display/display_test.go b/vendor/gopl.io/ch12/display/display_test.go new file mode 100644 index 0000000..bc847ce --- /dev/null +++ b/vendor/gopl.io/ch12/display/display_test.go @@ -0,0 +1,259 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +package display + +import ( + "io" + "net" + "os" + "reflect" + "sync" + "testing" + + "gopl.io/ch7/eval" +) + +// NOTE: we can't use !+..!- comments to excerpt these tests +// into the book because it defeats the Example mechanism, +// which requires the // Output comment to be at the end +// of the function. + +func Example_expr() { + e, _ := eval.Parse("sqrt(A / pi)") + Display("e", e) + // Output: + // Display e (eval.call): + // e.fn = "sqrt" + // e.args[0].type = eval.binary + // e.args[0].value.op = 47 + // e.args[0].value.x.type = eval.Var + // e.args[0].value.x.value = "A" + // e.args[0].value.y.type = eval.Var + // e.args[0].value.y.value = "pi" +} + +func Example_slice() { + Display("slice", []*int{new(int), nil}) + // Output: + // Display slice ([]*int): + // (*slice[0]) = 0 + // slice[1] = nil +} + +func Example_nilInterface() { + var w io.Writer + Display("w", w) + // Output: + // Display w (): + // w = invalid +} + +func Example_ptrToInterface() { + var w io.Writer + Display("&w", &w) + // Output: + // Display &w (*io.Writer): + // (*&w) = nil +} + +func Example_struct() { + Display("x", struct{ x interface{} }{3}) + // Output: + // Display x (struct { x interface {} }): + // x.x.type = int + // x.x.value = 3 +} + +func Example_interface() { + var i interface{} = 3 + Display("i", i) + // Output: + // Display i (int): + // i = 3 +} + +func Example_ptrToInterface2() { + var i interface{} = 3 + Display("&i", &i) + // Output: + // Display &i (*interface {}): + // (*&i).type = int + // (*&i).value = 3 +} + +func Example_array() { + Display("x", [1]interface{}{3}) + // Output: + // Display x ([1]interface {}): + // x[0].type = int + // x[0].value = 3 +} + +func Example_movie() { + //!+movie + type Movie struct { + Title, Subtitle string + Year int + Color bool + Actor map[string]string + Oscars []string + Sequel *string + } + //!-movie + //!+strangelove + strangelove := Movie{ + Title: "Dr. Strangelove", + Subtitle: "How I Learned to Stop Worrying and Love the Bomb", + Year: 1964, + Color: false, + Actor: map[string]string{ + "Dr. Strangelove": "Peter Sellers", + "Grp. Capt. Lionel Mandrake": "Peter Sellers", + "Pres. Merkin Muffley": "Peter Sellers", + "Gen. Buck Turgidson": "George C. Scott", + "Brig. Gen. Jack D. Ripper": "Sterling Hayden", + `Maj. T.J. "King" Kong`: "Slim Pickens", + }, + + Oscars: []string{ + "Best Actor (Nomin.)", + "Best Adapted Screenplay (Nomin.)", + "Best Director (Nomin.)", + "Best Picture (Nomin.)", + }, + } + //!-strangelove + Display("strangelove", strangelove) + + // We don't use an Output: comment since displaying + // a map is nondeterministic. + /* + //!+output + Display strangelove (display.Movie): + strangelove.Title = "Dr. Strangelove" + strangelove.Subtitle = "How I Learned to Stop Worrying and Love the Bomb" + strangelove.Year = 1964 + strangelove.Color = false + strangelove.Actor["Gen. Buck Turgidson"] = "George C. Scott" + strangelove.Actor["Brig. Gen. Jack D. Ripper"] = "Sterling Hayden" + strangelove.Actor["Maj. T.J. \"King\" Kong"] = "Slim Pickens" + strangelove.Actor["Dr. Strangelove"] = "Peter Sellers" + strangelove.Actor["Grp. Capt. Lionel Mandrake"] = "Peter Sellers" + strangelove.Actor["Pres. Merkin Muffley"] = "Peter Sellers" + strangelove.Oscars[0] = "Best Actor (Nomin.)" + strangelove.Oscars[1] = "Best Adapted Screenplay (Nomin.)" + strangelove.Oscars[2] = "Best Director (Nomin.)" + strangelove.Oscars[3] = "Best Picture (Nomin.)" + strangelove.Sequel = nil + //!-output + */ +} + +// This test ensures that the program terminates without crashing. +func Test(t *testing.T) { + // Some other values (YMMV) + Display("os.Stderr", os.Stderr) + // Output: + // Display os.Stderr (*os.File): + // (*(*os.Stderr).file).fd = 2 + // (*(*os.Stderr).file).name = "/dev/stderr" + // (*(*os.Stderr).file).nepipe = 0 + + var w io.Writer = os.Stderr + Display("&w", &w) + // Output: + // Display &w (*io.Writer): + // (*&w).type = *os.File + // (*(*(*&w).value).file).fd = 2 + // (*(*(*&w).value).file).name = "/dev/stderr" + // (*(*(*&w).value).file).nepipe = 0 + + var locker sync.Locker = new(sync.Mutex) + Display("(&locker)", &locker) + // Output: + // Display (&locker) (*sync.Locker): + // (*(&locker)).type = *sync.Mutex + // (*(*(&locker)).value).state = 0 + // (*(*(&locker)).value).sema = 0 + + Display("locker", locker) + // Output: + // Display locker (*sync.Mutex): + // (*locker).state = 0 + // (*locker).sema = 0 + // (*(&locker)) = nil + + locker = nil + Display("(&locker)", &locker) + // Output: + // Display (&locker) (*sync.Locker): + // (*(&locker)) = nil + + ips, _ := net.LookupHost("golang.org") + Display("ips", ips) + // Output: + // Display ips ([]string): + // ips[0] = "173.194.68.141" + // ips[1] = "2607:f8b0:400d:c06::8d" + + // Even metarecursion! (YMMV) + Display("rV", reflect.ValueOf(os.Stderr)) + // Output: + // Display rV (reflect.Value): + // (*rV.typ).size = 8 + // (*rV.typ).ptrdata = 8 + // (*rV.typ).hash = 871609668 + // (*rV.typ)._ = 0 + // ... + + // a pointer that points to itself + type P *P + var p P + p = &p + if false { + Display("p", p) + // Output: + // Display p (display.P): + // ...stuck, no output... + } + + // a map that contains itself + type M map[string]M + m := make(M) + m[""] = m + if false { + Display("m", m) + // Output: + // Display m (display.M): + // ...stuck, no output... + } + + // a slice that contains itself + type S []S + s := make(S, 1) + s[0] = s + if false { + Display("s", s) + // Output: + // Display s (display.S): + // ...stuck, no output... + } + + // a linked list that eats its own tail + type Cycle struct { + Value int + Tail *Cycle + } + var c Cycle + c = Cycle{42, &c} + if false { + Display("c", c) + // Output: + // Display c (display.Cycle): + // c.Value = 42 + // (*c.Tail).Value = 42 + // (*(*c.Tail).Tail).Value = 42 + // ...ad infinitum... + } +} diff --git a/vendor/gopl.io/ch12/format/format.go b/vendor/gopl.io/ch12/format/format.go new file mode 100644 index 0000000..02f839a --- /dev/null +++ b/vendor/gopl.io/ch12/format/format.go @@ -0,0 +1,44 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 332. + +// Package format provides an Any function that can format any value. +//!+ +package format + +import ( + "reflect" + "strconv" +) + +// Any formats any value as a string. +func Any(value interface{}) string { + return formatAtom(reflect.ValueOf(value)) +} + +// formatAtom formats a value without inspecting its internal structure. +func formatAtom(v reflect.Value) string { + switch v.Kind() { + case reflect.Invalid: + return "invalid" + case reflect.Int, reflect.Int8, reflect.Int16, + reflect.Int32, reflect.Int64: + return strconv.FormatInt(v.Int(), 10) + case reflect.Uint, reflect.Uint8, reflect.Uint16, + reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return strconv.FormatUint(v.Uint(), 10) + // ...floating-point and complex cases omitted for brevity... + case reflect.Bool: + return strconv.FormatBool(v.Bool()) + case reflect.String: + return strconv.Quote(v.String()) + case reflect.Chan, reflect.Func, reflect.Ptr, reflect.Slice, reflect.Map: + return v.Type().String() + " 0x" + + strconv.FormatUint(uint64(v.Pointer()), 16) + default: // reflect.Array, reflect.Struct, reflect.Interface + return v.Type().String() + " value" + } +} + +//!- diff --git a/vendor/gopl.io/ch12/format/format_test.go b/vendor/gopl.io/ch12/format/format_test.go new file mode 100644 index 0000000..77c0846 --- /dev/null +++ b/vendor/gopl.io/ch12/format/format_test.go @@ -0,0 +1,24 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +package format_test + +import ( + "fmt" + "testing" + "time" + + "gopl.io/ch12/format" +) + +func Test(t *testing.T) { + // The pointer values are just examples, and may vary from run to run. + //!+time + var x int64 = 1 + var d time.Duration = 1 * time.Nanosecond + fmt.Println(format.Any(x)) // "1" + fmt.Println(format.Any(d)) // "1" + fmt.Println(format.Any([]int64{x})) // "[]int64 0x8202b87b0" + fmt.Println(format.Any([]time.Duration{d})) // "[]time.Duration 0x8202b87e0" + //!-time +} diff --git a/vendor/gopl.io/ch12/methods/methods.go b/vendor/gopl.io/ch12/methods/methods.go new file mode 100644 index 0000000..3b6b560 --- /dev/null +++ b/vendor/gopl.io/ch12/methods/methods.go @@ -0,0 +1,29 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 351. + +// Package methods provides a function to print the methods of any value. +package methods + +import ( + "fmt" + "reflect" + "strings" +) + +//!+print +// Print prints the method set of the value x. +func Print(x interface{}) { + v := reflect.ValueOf(x) + t := v.Type() + fmt.Printf("type %s\n", t) + + for i := 0; i < v.NumMethod(); i++ { + methType := v.Method(i).Type() + fmt.Printf("func (%s) %s%s\n", t, t.Method(i).Name, + strings.TrimPrefix(methType.String(), "func")) + } +} + +//!-print diff --git a/vendor/gopl.io/ch12/methods/methods_test.go b/vendor/gopl.io/ch12/methods/methods_test.go new file mode 100644 index 0000000..0b509fe --- /dev/null +++ b/vendor/gopl.io/ch12/methods/methods_test.go @@ -0,0 +1,49 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +package methods_test + +import ( + "strings" + "time" + + "gopl.io/ch12/methods" +) + +func ExamplePrintDuration() { + methods.Print(time.Hour) + // Output: + // type time.Duration + // func (time.Duration) Hours() float64 + // func (time.Duration) Minutes() float64 + // func (time.Duration) Nanoseconds() int64 + // func (time.Duration) Seconds() float64 + // func (time.Duration) String() string +} + +func ExamplePrintReplacer() { + methods.Print(new(strings.Replacer)) + // Output: + // type *strings.Replacer + // func (*strings.Replacer) Replace(string) string + // func (*strings.Replacer) WriteString(io.Writer, string) (int, error) +} + +/* +//!+output +methods.Print(time.Hour) +// Output: +// type time.Duration +// func (time.Duration) Hours() float64 +// func (time.Duration) Minutes() float64 +// func (time.Duration) Nanoseconds() int64 +// func (time.Duration) Seconds() float64 +// func (time.Duration) String() string + +methods.Print(new(strings.Replacer)) +// Output: +// type *strings.Replacer +// func (*strings.Replacer) Replace(string) string +// func (*strings.Replacer) WriteString(io.Writer, string) (int, error) +//!-output +*/ diff --git a/vendor/gopl.io/ch12/params/params.go b/vendor/gopl.io/ch12/params/params.go new file mode 100644 index 0000000..8bf48cb --- /dev/null +++ b/vendor/gopl.io/ch12/params/params.go @@ -0,0 +1,90 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 349. + +// Package params provides a reflection-based parser for URL parameters. +package params + +import ( + "fmt" + "net/http" + "reflect" + "strconv" + "strings" +) + +//!+Unpack + +// Unpack populates the fields of the struct pointed to by ptr +// from the HTTP request parameters in req. +func Unpack(req *http.Request, ptr interface{}) error { + if err := req.ParseForm(); err != nil { + return err + } + + // Build map of fields keyed by effective name. + fields := make(map[string]reflect.Value) + v := reflect.ValueOf(ptr).Elem() // the struct variable + for i := 0; i < v.NumField(); i++ { + fieldInfo := v.Type().Field(i) // a reflect.StructField + tag := fieldInfo.Tag // a reflect.StructTag + name := tag.Get("http") + if name == "" { + name = strings.ToLower(fieldInfo.Name) + } + fields[name] = v.Field(i) + } + + // Update struct field for each parameter in the request. + for name, values := range req.Form { + f := fields[name] + if !f.IsValid() { + continue // ignore unrecognized HTTP parameters + } + for _, value := range values { + if f.Kind() == reflect.Slice { + elem := reflect.New(f.Type().Elem()).Elem() + if err := populate(elem, value); err != nil { + return fmt.Errorf("%s: %v", name, err) + } + f.Set(reflect.Append(f, elem)) + } else { + if err := populate(f, value); err != nil { + return fmt.Errorf("%s: %v", name, err) + } + } + } + } + return nil +} + +//!-Unpack + +//!+populate +func populate(v reflect.Value, value string) error { + switch v.Kind() { + case reflect.String: + v.SetString(value) + + case reflect.Int: + i, err := strconv.ParseInt(value, 10, 64) + if err != nil { + return err + } + v.SetInt(i) + + case reflect.Bool: + b, err := strconv.ParseBool(value) + if err != nil { + return err + } + v.SetBool(b) + + default: + return fmt.Errorf("unsupported kind %s", v.Type()) + } + return nil +} + +//!-populate diff --git a/vendor/gopl.io/ch12/search/main.go b/vendor/gopl.io/ch12/search/main.go new file mode 100644 index 0000000..9a94e82 --- /dev/null +++ b/vendor/gopl.io/ch12/search/main.go @@ -0,0 +1,60 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 348. + +// Search is a demo of the params.Unpack function. +package main + +import ( + "fmt" + "log" + "net/http" +) + +//!+ + +import "gopl.io/ch12/params" + +// search implements the /search URL endpoint. +func search(resp http.ResponseWriter, req *http.Request) { + var data struct { + Labels []string `http:"l"` + MaxResults int `http:"max"` + Exact bool `http:"x"` + } + data.MaxResults = 10 // set default + if err := params.Unpack(req, &data); err != nil { + http.Error(resp, err.Error(), http.StatusBadRequest) // 400 + return + } + + // ...rest of handler... + fmt.Fprintf(resp, "Search: %+v\n", data) +} + +//!- + +func main() { + http.HandleFunc("/search", search) + log.Fatal(http.ListenAndServe(":12345", nil)) +} + +/* +//!+output +$ go build gopl.io/ch12/search +$ ./search & +$ ./fetch 'http://localhost:12345/search' +Search: {Labels:[] MaxResults:10 Exact:false} +$ ./fetch 'http://localhost:12345/search?l=golang&l=programming' +Search: {Labels:[golang programming] MaxResults:10 Exact:false} +$ ./fetch 'http://localhost:12345/search?l=golang&l=programming&max=100' +Search: {Labels:[golang programming] MaxResults:100 Exact:false} +$ ./fetch 'http://localhost:12345/search?x=true&l=golang&l=programming' +Search: {Labels:[golang programming] MaxResults:10 Exact:true} +$ ./fetch 'http://localhost:12345/search?q=hello&x=123' +x: strconv.ParseBool: parsing "123": invalid syntax +$ ./fetch 'http://localhost:12345/search?q=hello&max=lots' +max: strconv.ParseInt: parsing "lots": invalid syntax +//!-output +*/ diff --git a/vendor/gopl.io/ch12/sexpr/decode.go b/vendor/gopl.io/ch12/sexpr/decode.go new file mode 100644 index 0000000..696e5bc --- /dev/null +++ b/vendor/gopl.io/ch12/sexpr/decode.go @@ -0,0 +1,162 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 344. + +// Package sexpr provides a means for converting Go objects to and +// from S-expressions. +package sexpr + +import ( + "bytes" + "fmt" + "reflect" + "strconv" + "text/scanner" +) + +//!+Unmarshal +// Unmarshal parses S-expression data and populates the variable +// whose address is in the non-nil pointer out. +func Unmarshal(data []byte, out interface{}) (err error) { + lex := &lexer{scan: scanner.Scanner{Mode: scanner.GoTokens}} + lex.scan.Init(bytes.NewReader(data)) + lex.next() // get the first token + defer func() { + // NOTE: this is not an example of ideal error handling. + if x := recover(); x != nil { + err = fmt.Errorf("error at %s: %v", lex.scan.Position, x) + } + }() + read(lex, reflect.ValueOf(out).Elem()) + return nil +} + +//!-Unmarshal + +//!+lexer +type lexer struct { + scan scanner.Scanner + token rune // the current token +} + +func (lex *lexer) next() { lex.token = lex.scan.Scan() } +func (lex *lexer) text() string { return lex.scan.TokenText() } + +func (lex *lexer) consume(want rune) { + if lex.token != want { // NOTE: Not an example of good error handling. + panic(fmt.Sprintf("got %q, want %q", lex.text(), want)) + } + lex.next() +} + +//!-lexer + +// The read function is a decoder for a small subset of well-formed +// S-expressions. For brevity of our example, it takes many dubious +// shortcuts. +// +// The parser assumes +// - that the S-expression input is well-formed; it does no error checking. +// - that the S-expression input corresponds to the type of the variable. +// - that all numbers in the input are non-negative decimal integers. +// - that all keys in ((key value) ...) struct syntax are unquoted symbols. +// - that the input does not contain dotted lists such as (1 2 . 3). +// - that the input does not contain Lisp reader macros such 'x and #'x. +// +// The reflection logic assumes +// - that v is always a variable of the appropriate type for the +// S-expression value. For example, v must not be a boolean, +// interface, channel, or function, and if v is an array, the input +// must have the correct number of elements. +// - that v in the top-level call to read has the zero value of its +// type and doesn't need clearing. +// - that if v is a numeric variable, it is a signed integer. + +//!+read +func read(lex *lexer, v reflect.Value) { + switch lex.token { + case scanner.Ident: + // The only valid identifiers are + // "nil" and struct field names. + if lex.text() == "nil" { + v.Set(reflect.Zero(v.Type())) + lex.next() + return + } + case scanner.String: + s, _ := strconv.Unquote(lex.text()) // NOTE: ignoring errors + v.SetString(s) + lex.next() + return + case scanner.Int: + i, _ := strconv.Atoi(lex.text()) // NOTE: ignoring errors + v.SetInt(int64(i)) + lex.next() + return + case '(': + lex.next() + readList(lex, v) + lex.next() // consume ')' + return + } + panic(fmt.Sprintf("unexpected token %q", lex.text())) +} + +//!-read + +//!+readlist +func readList(lex *lexer, v reflect.Value) { + switch v.Kind() { + case reflect.Array: // (item ...) + for i := 0; !endList(lex); i++ { + read(lex, v.Index(i)) + } + + case reflect.Slice: // (item ...) + for !endList(lex) { + item := reflect.New(v.Type().Elem()).Elem() + read(lex, item) + v.Set(reflect.Append(v, item)) + } + + case reflect.Struct: // ((name value) ...) + for !endList(lex) { + lex.consume('(') + if lex.token != scanner.Ident { + panic(fmt.Sprintf("got token %q, want field name", lex.text())) + } + name := lex.text() + lex.next() + read(lex, v.FieldByName(name)) + lex.consume(')') + } + + case reflect.Map: // ((key value) ...) + v.Set(reflect.MakeMap(v.Type())) + for !endList(lex) { + lex.consume('(') + key := reflect.New(v.Type().Key()).Elem() + read(lex, key) + value := reflect.New(v.Type().Elem()).Elem() + read(lex, value) + v.SetMapIndex(key, value) + lex.consume(')') + } + + default: + panic(fmt.Sprintf("cannot decode list into %v", v.Type())) + } +} + +func endList(lex *lexer) bool { + switch lex.token { + case scanner.EOF: + panic("end of file") + case ')': + return true + } + return false +} + +//!-readlist diff --git a/vendor/gopl.io/ch12/sexpr/encode.go b/vendor/gopl.io/ch12/sexpr/encode.go new file mode 100644 index 0000000..f2a53a0 --- /dev/null +++ b/vendor/gopl.io/ch12/sexpr/encode.go @@ -0,0 +1,97 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 339. + +package sexpr + +import ( + "bytes" + "fmt" + "reflect" +) + +//!+Marshal +// Marshal encodes a Go value in S-expression form. +func Marshal(v interface{}) ([]byte, error) { + var buf bytes.Buffer + if err := encode(&buf, reflect.ValueOf(v)); err != nil { + return nil, err + } + return buf.Bytes(), nil +} + +//!-Marshal + +// encode writes to buf an S-expression representation of v. +//!+encode +func encode(buf *bytes.Buffer, v reflect.Value) error { + switch v.Kind() { + case reflect.Invalid: + buf.WriteString("nil") + + case reflect.Int, reflect.Int8, reflect.Int16, + reflect.Int32, reflect.Int64: + fmt.Fprintf(buf, "%d", v.Int()) + + case reflect.Uint, reflect.Uint8, reflect.Uint16, + reflect.Uint32, reflect.Uint64, reflect.Uintptr: + fmt.Fprintf(buf, "%d", v.Uint()) + + case reflect.String: + fmt.Fprintf(buf, "%q", v.String()) + + case reflect.Ptr: + return encode(buf, v.Elem()) + + case reflect.Array, reflect.Slice: // (value ...) + buf.WriteByte('(') + for i := 0; i < v.Len(); i++ { + if i > 0 { + buf.WriteByte(' ') + } + if err := encode(buf, v.Index(i)); err != nil { + return err + } + } + buf.WriteByte(')') + + case reflect.Struct: // ((name value) ...) + buf.WriteByte('(') + for i := 0; i < v.NumField(); i++ { + if i > 0 { + buf.WriteByte(' ') + } + fmt.Fprintf(buf, "(%s ", v.Type().Field(i).Name) + if err := encode(buf, v.Field(i)); err != nil { + return err + } + buf.WriteByte(')') + } + buf.WriteByte(')') + + case reflect.Map: // ((key value) ...) + buf.WriteByte('(') + for i, key := range v.MapKeys() { + if i > 0 { + buf.WriteByte(' ') + } + buf.WriteByte('(') + if err := encode(buf, key); err != nil { + return err + } + buf.WriteByte(' ') + if err := encode(buf, v.MapIndex(key)); err != nil { + return err + } + buf.WriteByte(')') + } + buf.WriteByte(')') + + default: // float, complex, bool, chan, func, interface + return fmt.Errorf("unsupported type: %s", v.Type()) + } + return nil +} + +//!-encode diff --git a/vendor/gopl.io/ch12/sexpr/pretty.go b/vendor/gopl.io/ch12/sexpr/pretty.go new file mode 100644 index 0000000..1c5716d --- /dev/null +++ b/vendor/gopl.io/ch12/sexpr/pretty.go @@ -0,0 +1,183 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +package sexpr + +// This file implements the algorithm described in Derek C. Oppen's +// 1979 Stanford technical report, "Pretty Printing". + +import ( + "bytes" + "fmt" + "reflect" +) + +func MarshalIndent(v interface{}) ([]byte, error) { + p := printer{width: margin} + if err := pretty(&p, reflect.ValueOf(v)); err != nil { + return nil, err + } + return p.Bytes(), nil +} + +const margin = 80 + +type token struct { + kind rune // one of "s ()" (string, blank, start, end) + str string + size int +} + +type printer struct { + tokens []*token // FIFO buffer + stack []*token // stack of open ' ' and '(' tokens + rtotal int // total number of spaces needed to print stream + + bytes.Buffer + indents []int + width int // remaining space +} + +func (p *printer) string(str string) { + tok := &token{kind: 's', str: str, size: len(str)} + if len(p.stack) == 0 { + p.print(tok) + } else { + p.tokens = append(p.tokens, tok) + p.rtotal += len(str) + } +} +func (p *printer) pop() (top *token) { + last := len(p.stack) - 1 + top, p.stack = p.stack[last], p.stack[:last] + return +} +func (p *printer) begin() { + if len(p.stack) == 0 { + p.rtotal = 1 + } + t := &token{kind: '(', size: -p.rtotal} + p.tokens = append(p.tokens, t) + p.stack = append(p.stack, t) // push + p.string("(") +} +func (p *printer) end() { + p.string(")") + p.tokens = append(p.tokens, &token{kind: ')'}) + x := p.pop() + x.size += p.rtotal + if x.kind == ' ' { + p.pop().size += p.rtotal + } + if len(p.stack) == 0 { + for _, tok := range p.tokens { + p.print(tok) + } + p.tokens = nil + } +} +func (p *printer) space() { + last := len(p.stack) - 1 + x := p.stack[last] + if x.kind == ' ' { + x.size += p.rtotal + p.stack = p.stack[:last] // pop + } + t := &token{kind: ' ', size: -p.rtotal} + p.tokens = append(p.tokens, t) + p.stack = append(p.stack, t) + p.rtotal++ +} +func (p *printer) print(t *token) { + switch t.kind { + case 's': + p.WriteString(t.str) + p.width -= len(t.str) + case '(': + p.indents = append(p.indents, p.width) + case ')': + p.indents = p.indents[:len(p.indents)-1] // pop + case ' ': + if t.size > p.width { + p.width = p.indents[len(p.indents)-1] - 1 + fmt.Fprintf(&p.Buffer, "\n%*s", margin-p.width, "") + } else { + p.WriteByte(' ') + p.width-- + } + } +} +func (p *printer) stringf(format string, args ...interface{}) { + p.string(fmt.Sprintf(format, args...)) +} + +func pretty(p *printer, v reflect.Value) error { + switch v.Kind() { + case reflect.Invalid: + p.string("nil") + + case reflect.Int, reflect.Int8, reflect.Int16, + reflect.Int32, reflect.Int64: + p.stringf("%d", v.Int()) + + case reflect.Uint, reflect.Uint8, reflect.Uint16, + reflect.Uint32, reflect.Uint64, reflect.Uintptr: + p.stringf("%d", v.Uint()) + + case reflect.String: + p.stringf("%q", v.String()) + + case reflect.Array, reflect.Slice: // (value ...) + p.begin() + for i := 0; i < v.Len(); i++ { + if i > 0 { + p.space() + } + if err := pretty(p, v.Index(i)); err != nil { + return err + } + } + p.end() + + case reflect.Struct: // ((name value ...) + p.begin() + for i := 0; i < v.NumField(); i++ { + if i > 0 { + p.space() + } + p.begin() + p.string(v.Type().Field(i).Name) + p.space() + if err := pretty(p, v.Field(i)); err != nil { + return err + } + p.end() + } + p.end() + + case reflect.Map: // ((key value ...) + p.begin() + for i, key := range v.MapKeys() { + if i > 0 { + p.space() + } + p.begin() + if err := pretty(p, key); err != nil { + return err + } + p.space() + if err := pretty(p, v.MapIndex(key)); err != nil { + return err + } + p.end() + } + p.end() + + case reflect.Ptr: + return pretty(p, v.Elem()) + + default: // float, complex, bool, chan, func, interface + return fmt.Errorf("unsupported type: %s", v.Type()) + } + return nil +} diff --git a/vendor/gopl.io/ch12/sexpr/sexpr_test.go b/vendor/gopl.io/ch12/sexpr/sexpr_test.go new file mode 100644 index 0000000..25b0bf7 --- /dev/null +++ b/vendor/gopl.io/ch12/sexpr/sexpr_test.go @@ -0,0 +1,74 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +package sexpr + +import ( + "reflect" + "testing" +) + +// Test verifies that encoding and decoding a complex data value +// produces an equal result. +// +// The test does not make direct assertions about the encoded output +// because the output depends on map iteration order, which is +// nondeterministic. The output of the t.Log statements can be +// inspected by running the test with the -v flag: +// +// $ go test -v gopl.io/ch12/sexpr +// +func Test(t *testing.T) { + type Movie struct { + Title, Subtitle string + Year int + Actor map[string]string + Oscars []string + Sequel *string + } + strangelove := Movie{ + Title: "Dr. Strangelove", + Subtitle: "How I Learned to Stop Worrying and Love the Bomb", + Year: 1964, + Actor: map[string]string{ + "Dr. Strangelove": "Peter Sellers", + "Grp. Capt. Lionel Mandrake": "Peter Sellers", + "Pres. Merkin Muffley": "Peter Sellers", + "Gen. Buck Turgidson": "George C. Scott", + "Brig. Gen. Jack D. Ripper": "Sterling Hayden", + `Maj. T.J. "King" Kong`: "Slim Pickens", + }, + Oscars: []string{ + "Best Actor (Nomin.)", + "Best Adapted Screenplay (Nomin.)", + "Best Director (Nomin.)", + "Best Picture (Nomin.)", + }, + } + + // Encode it + data, err := Marshal(strangelove) + if err != nil { + t.Fatalf("Marshal failed: %v", err) + } + t.Logf("Marshal() = %s\n", data) + + // Decode it + var movie Movie + if err := Unmarshal(data, &movie); err != nil { + t.Fatalf("Unmarshal failed: %v", err) + } + t.Logf("Unmarshal() = %+v\n", movie) + + // Check equality. + if !reflect.DeepEqual(movie, strangelove) { + t.Fatal("not equal") + } + + // Pretty-print it: + data, err = MarshalIndent(strangelove) + if err != nil { + t.Fatal(err) + } + t.Logf("MarshalIdent() = %s\n", data) +} diff --git a/vendor/gopl.io/ch13/bzip-print/bzip2.c b/vendor/gopl.io/ch13/bzip-print/bzip2.c new file mode 100644 index 0000000..42a3a53 --- /dev/null +++ b/vendor/gopl.io/ch13/bzip-print/bzip2.c @@ -0,0 +1,28 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 362. +// This is the version that appears in print, +// but it does not comply with the proposed +// rules for passing pointers between Go and C. +// (https://github.com/golang/proposal/blob/master/design/12416-cgo-pointers.md) +// See gopl.io/ch13/bzip for an updated version. + +//!+ +/* This file is gopl.io/ch13/bzip/bzip2.c, */ +/* a simple wrapper for libbzip2 suitable for cgo. */ +#include + +int bz2compress(bz_stream *s, int action, + char *in, unsigned *inlen, char *out, unsigned *outlen) { + s->next_in = in; + s->avail_in = *inlen; + s->next_out = out; + s->avail_out = *outlen; + int r = BZ2_bzCompress(s, action); + *inlen -= s->avail_in; + *outlen -= s->avail_out; + return r; +} + +//!- diff --git a/vendor/gopl.io/ch13/bzip-print/bzip2.go b/vendor/gopl.io/ch13/bzip-print/bzip2.go new file mode 100644 index 0000000..1d8c771 --- /dev/null +++ b/vendor/gopl.io/ch13/bzip-print/bzip2.go @@ -0,0 +1,96 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 362. +// This is the version that appears in print, +// but it does not comply with the proposed +// rules for passing pointers between Go and C. +// (https://github.com/golang/proposal/blob/master/design/12416-cgo-pointers.md) +// See gopl.io/ch13/bzip for an updated version. +//!+ + +// Package bzip provides a writer that uses bzip2 compression (bzip.org). +package bzip + +/* +#cgo CFLAGS: -I/usr/include +#cgo LDFLAGS: -L/usr/lib -lbz2 +#include +int bz2compress(bz_stream *s, int action, + char *in, unsigned *inlen, char *out, unsigned *outlen); +*/ +import "C" + +import ( + "io" + "unsafe" +) + +type writer struct { + w io.Writer // underlying output stream + stream *C.bz_stream + outbuf [64 * 1024]byte +} + +// NewWriter returns a writer for bzip2-compressed streams. +func NewWriter(out io.Writer) io.WriteCloser { + const ( + blockSize = 9 + verbosity = 0 + workFactor = 30 + ) + w := &writer{w: out, stream: new(C.bz_stream)} + C.BZ2_bzCompressInit(w.stream, blockSize, verbosity, workFactor) + return w +} + +//!- + +//!+write +func (w *writer) Write(data []byte) (int, error) { + if w.stream == nil { + panic("closed") + } + var total int // uncompressed bytes written + + for len(data) > 0 { + inlen, outlen := C.uint(len(data)), C.uint(cap(w.outbuf)) + C.bz2compress(w.stream, C.BZ_RUN, + (*C.char)(unsafe.Pointer(&data[0])), &inlen, + (*C.char)(unsafe.Pointer(&w.outbuf)), &outlen) + total += int(inlen) + data = data[inlen:] + if _, err := w.w.Write(w.outbuf[:outlen]); err != nil { + return total, err + } + } + return total, nil +} + +//!-write + +//!+close +// Close flushes the compressed data and closes the stream. +// It does not close the underlying io.Writer. +func (w *writer) Close() error { + if w.stream == nil { + panic("closed") + } + defer func() { + C.BZ2_bzCompressEnd(w.stream) + w.stream = nil + }() + for { + inlen, outlen := C.uint(0), C.uint(cap(w.outbuf)) + r := C.bz2compress(w.stream, C.BZ_FINISH, nil, &inlen, + (*C.char)(unsafe.Pointer(&w.outbuf)), &outlen) + if _, err := w.w.Write(w.outbuf[:outlen]); err != nil { + return err + } + if r == C.BZ_STREAM_END { + return nil + } + } +} + +//!-close diff --git a/vendor/gopl.io/ch13/bzip-print/bzip2_test.go b/vendor/gopl.io/ch13/bzip-print/bzip2_test.go new file mode 100644 index 0000000..c52aba0 --- /dev/null +++ b/vendor/gopl.io/ch13/bzip-print/bzip2_test.go @@ -0,0 +1,40 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +package bzip_test + +import ( + "bytes" + "compress/bzip2" // reader + "io" + "testing" + + "gopl.io/ch13/bzip" // writer +) + +func TestBzip2(t *testing.T) { + var compressed, uncompressed bytes.Buffer + w := bzip.NewWriter(&compressed) + + // Write a repetitive message in a million pieces, + // compressing one copy but not the other. + tee := io.MultiWriter(w, &uncompressed) + for i := 0; i < 1000000; i++ { + io.WriteString(tee, "hello") + } + if err := w.Close(); err != nil { + t.Fatal(err) + } + + // Check the size of the compressed stream. + if got, want := compressed.Len(), 255; got != want { + t.Errorf("1 million hellos compressed to %d bytes, want %d", got, want) + } + + // Decompress and compare with original. + var decompressed bytes.Buffer + io.Copy(&decompressed, bzip2.NewReader(&compressed)) + if !bytes.Equal(uncompressed.Bytes(), decompressed.Bytes()) { + t.Error("decompression yielded a different message") + } +} diff --git a/vendor/gopl.io/ch13/bzip/bzip2.c b/vendor/gopl.io/ch13/bzip/bzip2.c new file mode 100644 index 0000000..985869e --- /dev/null +++ b/vendor/gopl.io/ch13/bzip/bzip2.c @@ -0,0 +1,32 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 362. +// +// The version of this program that appeared in the first and second +// printings did not comply with the proposed rules for passing +// pointers between Go and C, described here: +// https://github.com/golang/proposal/blob/master/design/12416-cgo-pointers.md +// +// The version below, which appears in the third printing, +// has been corrected. See bzip2.go for explanation. + +//!+ +/* This file is gopl.io/ch13/bzip/bzip2.c, */ +/* a simple wrapper for libbzip2 suitable for cgo. */ +#include + +int bz2compress(bz_stream *s, int action, + char *in, unsigned *inlen, char *out, unsigned *outlen) { + s->next_in = in; + s->avail_in = *inlen; + s->next_out = out; + s->avail_out = *outlen; + int r = BZ2_bzCompress(s, action); + *inlen -= s->avail_in; + *outlen -= s->avail_out; + s->next_in = s->next_out = NULL; + return r; +} + +//!- diff --git a/vendor/gopl.io/ch13/bzip/bzip2.go b/vendor/gopl.io/ch13/bzip/bzip2.go new file mode 100644 index 0000000..278b238 --- /dev/null +++ b/vendor/gopl.io/ch13/bzip/bzip2.go @@ -0,0 +1,111 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 362. +// +// The version of this program that appeared in the first and second +// printings did not comply with the proposed rules for passing +// pointers between Go and C, described here: +// https://github.com/golang/proposal/blob/master/design/12416-cgo-pointers.md +// +// The rules forbid a C function like bz2compress from storing 'in' +// and 'out' (pointers to variables allocated by Go) into the Go +// variable 's', even temporarily. +// +// The version below, which appears in the third printing, has been +// corrected. To comply with the rules, the bz_stream variable must +// be allocated by C code. We have introduced two C functions, +// bz2alloc and bz2free, to allocate and free instances of the +// bz_stream type. Also, we have changed bz2compress so that before +// it returns, it clears the fields of the bz_stream that contain +// pointers to Go variables. + +//!+ + +// Package bzip provides a writer that uses bzip2 compression (bzip.org). +package bzip + +/* +#cgo CFLAGS: -I/usr/include +#cgo LDFLAGS: -L/usr/lib -lbz2 +#include +#include +bz_stream* bz2alloc() { return calloc(1, sizeof(bz_stream)); } +int bz2compress(bz_stream *s, int action, + char *in, unsigned *inlen, char *out, unsigned *outlen); +void bz2free(bz_stream* s) { free(s); } +*/ +import "C" + +import ( + "io" + "unsafe" +) + +type writer struct { + w io.Writer // underlying output stream + stream *C.bz_stream + outbuf [64 * 1024]byte +} + +// NewWriter returns a writer for bzip2-compressed streams. +func NewWriter(out io.Writer) io.WriteCloser { + const blockSize = 9 + const verbosity = 0 + const workFactor = 30 + w := &writer{w: out, stream: C.bz2alloc()} + C.BZ2_bzCompressInit(w.stream, blockSize, verbosity, workFactor) + return w +} + +//!- + +//!+write +func (w *writer) Write(data []byte) (int, error) { + if w.stream == nil { + panic("closed") + } + var total int // uncompressed bytes written + + for len(data) > 0 { + inlen, outlen := C.uint(len(data)), C.uint(cap(w.outbuf)) + C.bz2compress(w.stream, C.BZ_RUN, + (*C.char)(unsafe.Pointer(&data[0])), &inlen, + (*C.char)(unsafe.Pointer(&w.outbuf)), &outlen) + total += int(inlen) + data = data[inlen:] + if _, err := w.w.Write(w.outbuf[:outlen]); err != nil { + return total, err + } + } + return total, nil +} + +//!-write + +//!+close +// Close flushes the compressed data and closes the stream. +// It does not close the underlying io.Writer. +func (w *writer) Close() error { + if w.stream == nil { + panic("closed") + } + defer func() { + C.BZ2_bzCompressEnd(w.stream) + C.bz2free(w.stream) + w.stream = nil + }() + for { + inlen, outlen := C.uint(0), C.uint(cap(w.outbuf)) + r := C.bz2compress(w.stream, C.BZ_FINISH, nil, &inlen, + (*C.char)(unsafe.Pointer(&w.outbuf)), &outlen) + if _, err := w.w.Write(w.outbuf[:outlen]); err != nil { + return err + } + if r == C.BZ_STREAM_END { + return nil + } + } +} + +//!-close diff --git a/vendor/gopl.io/ch13/bzip/bzip2_test.go b/vendor/gopl.io/ch13/bzip/bzip2_test.go new file mode 100644 index 0000000..c52aba0 --- /dev/null +++ b/vendor/gopl.io/ch13/bzip/bzip2_test.go @@ -0,0 +1,40 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +package bzip_test + +import ( + "bytes" + "compress/bzip2" // reader + "io" + "testing" + + "gopl.io/ch13/bzip" // writer +) + +func TestBzip2(t *testing.T) { + var compressed, uncompressed bytes.Buffer + w := bzip.NewWriter(&compressed) + + // Write a repetitive message in a million pieces, + // compressing one copy but not the other. + tee := io.MultiWriter(w, &uncompressed) + for i := 0; i < 1000000; i++ { + io.WriteString(tee, "hello") + } + if err := w.Close(); err != nil { + t.Fatal(err) + } + + // Check the size of the compressed stream. + if got, want := compressed.Len(), 255; got != want { + t.Errorf("1 million hellos compressed to %d bytes, want %d", got, want) + } + + // Decompress and compare with original. + var decompressed bytes.Buffer + io.Copy(&decompressed, bzip2.NewReader(&compressed)) + if !bytes.Equal(uncompressed.Bytes(), decompressed.Bytes()) { + t.Error("decompression yielded a different message") + } +} diff --git a/vendor/gopl.io/ch13/bzipper/main.go b/vendor/gopl.io/ch13/bzipper/main.go new file mode 100644 index 0000000..a8ebad1 --- /dev/null +++ b/vendor/gopl.io/ch13/bzipper/main.go @@ -0,0 +1,29 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 365. + +//!+ + +// Bzipper reads input, bzip2-compresses it, and writes it out. +package main + +import ( + "io" + "log" + "os" + + "gopl.io/ch13/bzip" +) + +func main() { + w := bzip.NewWriter(os.Stdout) + if _, err := io.Copy(w, os.Stdin); err != nil { + log.Fatalf("bzipper: %v\n", err) + } + if err := w.Close(); err != nil { + log.Fatalf("bzipper: close: %v\n", err) + } +} + +//!- diff --git a/vendor/gopl.io/ch13/equal/equal.go b/vendor/gopl.io/ch13/equal/equal.go new file mode 100644 index 0000000..ec4b40c --- /dev/null +++ b/vendor/gopl.io/ch13/equal/equal.go @@ -0,0 +1,127 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 359. + +// Package equal provides a deep equivalence relation for arbitrary values. +package equal + +import ( + "reflect" + "unsafe" +) + +//!+ +func equal(x, y reflect.Value, seen map[comparison]bool) bool { + if !x.IsValid() || !y.IsValid() { + return x.IsValid() == y.IsValid() + } + if x.Type() != y.Type() { + return false + } + + // ...cycle check omitted (shown later)... + + //!- + //!+cyclecheck + // cycle check + if x.CanAddr() && y.CanAddr() { + xptr := unsafe.Pointer(x.UnsafeAddr()) + yptr := unsafe.Pointer(y.UnsafeAddr()) + if xptr == yptr { + return true // identical references + } + c := comparison{xptr, yptr, x.Type()} + if seen[c] { + return true // already seen + } + seen[c] = true + } + //!-cyclecheck + //!+ + switch x.Kind() { + case reflect.Bool: + return x.Bool() == y.Bool() + + case reflect.String: + return x.String() == y.String() + + // ...numeric cases omitted for brevity... + + //!- + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, + reflect.Int64: + return x.Int() == y.Int() + + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, + reflect.Uint64, reflect.Uintptr: + return x.Uint() == y.Uint() + + case reflect.Float32, reflect.Float64: + return x.Float() == y.Float() + + case reflect.Complex64, reflect.Complex128: + return x.Complex() == y.Complex() + //!+ + case reflect.Chan, reflect.UnsafePointer, reflect.Func: + return x.Pointer() == y.Pointer() + + case reflect.Ptr, reflect.Interface: + return equal(x.Elem(), y.Elem(), seen) + + case reflect.Array, reflect.Slice: + if x.Len() != y.Len() { + return false + } + for i := 0; i < x.Len(); i++ { + if !equal(x.Index(i), y.Index(i), seen) { + return false + } + } + return true + + // ...struct and map cases omitted for brevity... + //!- + case reflect.Struct: + for i, n := 0, x.NumField(); i < n; i++ { + if !equal(x.Field(i), y.Field(i), seen) { + return false + } + } + return true + + case reflect.Map: + if x.Len() != y.Len() { + return false + } + for _, k := range x.MapKeys() { + if !equal(x.MapIndex(k), y.MapIndex(k), seen) { + return false + } + } + return true + //!+ + } + panic("unreachable") +} + +//!- + +//!+comparison +// Equal reports whether x and y are deeply equal. +//!-comparison +// +// Map keys are always compared with ==, not deeply. +// (This matters for keys containing pointers or interfaces.) +//!+comparison +func Equal(x, y interface{}) bool { + seen := make(map[comparison]bool) + return equal(reflect.ValueOf(x), reflect.ValueOf(y), seen) +} + +type comparison struct { + x, y unsafe.Pointer + t reflect.Type +} + +//!-comparison diff --git a/vendor/gopl.io/ch13/equal/equal_test.go b/vendor/gopl.io/ch13/equal/equal_test.go new file mode 100644 index 0000000..4656077 --- /dev/null +++ b/vendor/gopl.io/ch13/equal/equal_test.go @@ -0,0 +1,133 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +package equal + +import ( + "bytes" + "fmt" + "testing" +) + +func TestEqual(t *testing.T) { + one, oneAgain, two := 1, 1, 2 + + type CyclePtr *CyclePtr + var cyclePtr1, cyclePtr2 CyclePtr + cyclePtr1 = &cyclePtr1 + cyclePtr2 = &cyclePtr2 + + type CycleSlice []CycleSlice + var cycleSlice CycleSlice + cycleSlice = append(cycleSlice, cycleSlice) + + ch1, ch2 := make(chan int), make(chan int) + var ch1ro <-chan int = ch1 + + type mystring string + + var iface1, iface1Again, iface2 interface{} = &one, &oneAgain, &two + + for _, test := range []struct { + x, y interface{} + want bool + }{ + // basic types + {1, 1, true}, + {1, 2, false}, // different values + {1, 1.0, false}, // different types + {"foo", "foo", true}, + {"foo", "bar", false}, + {mystring("foo"), "foo", false}, // different types + // slices + {[]string{"foo"}, []string{"foo"}, true}, + {[]string{"foo"}, []string{"bar"}, false}, + {[]string{}, []string(nil), true}, + // slice cycles + {cycleSlice, cycleSlice, true}, + // maps + { + map[string][]int{"foo": {1, 2, 3}}, + map[string][]int{"foo": {1, 2, 3}}, + true, + }, + { + map[string][]int{"foo": {1, 2, 3}}, + map[string][]int{"foo": {1, 2, 3, 4}}, + false, + }, + { + map[string][]int{}, + map[string][]int(nil), + true, + }, + // pointers + {&one, &one, true}, + {&one, &two, false}, + {&one, &oneAgain, true}, + {new(bytes.Buffer), new(bytes.Buffer), true}, + // pointer cycles + {cyclePtr1, cyclePtr1, true}, + {cyclePtr2, cyclePtr2, true}, + {cyclePtr1, cyclePtr2, true}, // they're deeply equal + // functions + {(func())(nil), (func())(nil), true}, + {(func())(nil), func() {}, false}, + {func() {}, func() {}, false}, + // arrays + {[...]int{1, 2, 3}, [...]int{1, 2, 3}, true}, + {[...]int{1, 2, 3}, [...]int{1, 2, 4}, false}, + // channels + {ch1, ch1, true}, + {ch1, ch2, false}, + {ch1ro, ch1, false}, // NOTE: not equal + // interfaces + {&iface1, &iface1, true}, + {&iface1, &iface2, false}, + {&iface1Again, &iface1, true}, + } { + if Equal(test.x, test.y) != test.want { + t.Errorf("Equal(%v, %v) = %t", + test.x, test.y, !test.want) + } + } +} + +func Example_equal() { + //!+ + fmt.Println(Equal([]int{1, 2, 3}, []int{1, 2, 3})) // "true" + fmt.Println(Equal([]string{"foo"}, []string{"bar"})) // "false" + fmt.Println(Equal([]string(nil), []string{})) // "true" + fmt.Println(Equal(map[string]int(nil), map[string]int{})) // "true" + //!- + + // Output: + // true + // false + // true + // true +} + +func Example_equalCycle() { + //!+cycle + // Circular linked lists a -> b -> a and c -> c. + type link struct { + value string + tail *link + } + a, b, c := &link{value: "a"}, &link{value: "b"}, &link{value: "c"} + a.tail, b.tail, c.tail = b, a, c + fmt.Println(Equal(a, a)) // "true" + fmt.Println(Equal(b, b)) // "true" + fmt.Println(Equal(c, c)) // "true" + fmt.Println(Equal(a, b)) // "false" + fmt.Println(Equal(a, c)) // "false" + //!-cycle + + // Output: + // true + // true + // true + // false + // false +} diff --git a/vendor/gopl.io/ch13/unsafeptr/main.go b/vendor/gopl.io/ch13/unsafeptr/main.go new file mode 100644 index 0000000..f906946 --- /dev/null +++ b/vendor/gopl.io/ch13/unsafeptr/main.go @@ -0,0 +1,38 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 357. + +// Package unsafeptr demonstrates basic use of unsafe.Pointer. +package main + +import ( + "fmt" + "unsafe" +) + +func main() { + //!+main + var x struct { + a bool + b int16 + c []int + } + + // equivalent to pb := &x.b + pb := (*int16)(unsafe.Pointer( + uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b))) + *pb = 42 + + fmt.Println(x.b) // "42" + //!-main +} + +/* +//!+wrong + // NOTE: subtly incorrect! + tmp := uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b) + pb := (*int16)(unsafe.Pointer(tmp)) + *pb = 42 +//!-wrong +*/ diff --git a/vendor/gopl.io/ch2/boiling/main.go b/vendor/gopl.io/ch2/boiling/main.go new file mode 100644 index 0000000..b7a1dbe --- /dev/null +++ b/vendor/gopl.io/ch2/boiling/main.go @@ -0,0 +1,22 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 29. +//!+ + +// Boiling prints the boiling point of water. +package main + +import "fmt" + +const boilingF = 212.0 + +func main() { + var f = boilingF + var c = (f - 32) * 5 / 9 + fmt.Printf("boiling point = %g°F or %g°C\n", f, c) + // Output: + // boiling point = 212°F or 100°C +} + +//!- diff --git a/vendor/gopl.io/ch2/cf/main.go b/vendor/gopl.io/ch2/cf/main.go new file mode 100644 index 0000000..882101f --- /dev/null +++ b/vendor/gopl.io/ch2/cf/main.go @@ -0,0 +1,32 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 43. +//!+ + +// Cf converts its numeric argument to Celsius and Fahrenheit. +package main + +import ( + "fmt" + "os" + "strconv" + + "gopl.io/ch2/tempconv" +) + +func main() { + for _, arg := range os.Args[1:] { + t, err := strconv.ParseFloat(arg, 64) + if err != nil { + fmt.Fprintf(os.Stderr, "cf: %v\n", err) + os.Exit(1) + } + f := tempconv.Fahrenheit(t) + c := tempconv.Celsius(t) + fmt.Printf("%s = %s, %s = %s\n", + f, tempconv.FToC(f), c, tempconv.CToF(c)) + } +} + +//!- diff --git a/vendor/gopl.io/ch2/echo4/main.go b/vendor/gopl.io/ch2/echo4/main.go new file mode 100644 index 0000000..5a029fd --- /dev/null +++ b/vendor/gopl.io/ch2/echo4/main.go @@ -0,0 +1,27 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 33. +//!+ + +// Echo4 prints its command-line arguments. +package main + +import ( + "flag" + "fmt" + "strings" +) + +var n = flag.Bool("n", false, "omit trailing newline") +var sep = flag.String("s", " ", "separator") + +func main() { + flag.Parse() + fmt.Print(strings.Join(flag.Args(), *sep)) + if !*n { + fmt.Println() + } +} + +//!- diff --git a/vendor/gopl.io/ch2/ftoc/main.go b/vendor/gopl.io/ch2/ftoc/main.go new file mode 100644 index 0000000..3f3abd0 --- /dev/null +++ b/vendor/gopl.io/ch2/ftoc/main.go @@ -0,0 +1,22 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 29. +//!+ + +// Ftoc prints two Fahrenheit-to-Celsius conversions. +package main + +import "fmt" + +func main() { + const freezingF, boilingF = 32.0, 212.0 + fmt.Printf("%g°F = %g°C\n", freezingF, fToC(freezingF)) // "32°F = 0°C" + fmt.Printf("%g°F = %g°C\n", boilingF, fToC(boilingF)) // "212°F = 100°C" +} + +func fToC(f float64) float64 { + return (f - 32) * 5 / 9 +} + +//!- diff --git a/vendor/gopl.io/ch2/popcount/main.go b/vendor/gopl.io/ch2/popcount/main.go new file mode 100644 index 0000000..c994e08 --- /dev/null +++ b/vendor/gopl.io/ch2/popcount/main.go @@ -0,0 +1,31 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 45. + +// (Package doc comment intentionally malformed to demonstrate golint.) +//!+ +package popcount + +// pc[i] is the population count of i. +var pc [256]byte + +func init() { + for i := range pc { + pc[i] = pc[i/2] + byte(i&1) + } +} + +// PopCount returns the population count (number of set bits) of x. +func PopCount(x uint64) int { + return int(pc[byte(x>>(0*8))] + + pc[byte(x>>(1*8))] + + pc[byte(x>>(2*8))] + + pc[byte(x>>(3*8))] + + pc[byte(x>>(4*8))] + + pc[byte(x>>(5*8))] + + pc[byte(x>>(6*8))] + + pc[byte(x>>(7*8))]) +} + +//!- diff --git a/vendor/gopl.io/ch2/popcount/popcount_test.go b/vendor/gopl.io/ch2/popcount/popcount_test.go new file mode 100644 index 0000000..504548d --- /dev/null +++ b/vendor/gopl.io/ch2/popcount/popcount_test.go @@ -0,0 +1,83 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +package popcount_test + +import ( + "testing" + + "gopl.io/ch2/popcount" +) + +// -- Alternative implementations -- + +func BitCount(x uint64) int { + // Hacker's Delight, Figure 5-2. + x = x - ((x >> 1) & 0x5555555555555555) + x = (x & 0x3333333333333333) + ((x >> 2) & 0x3333333333333333) + x = (x + (x >> 4)) & 0x0f0f0f0f0f0f0f0f + x = x + (x >> 8) + x = x + (x >> 16) + x = x + (x >> 32) + return int(x & 0x7f) +} + +func PopCountByClearing(x uint64) int { + n := 0 + for x != 0 { + x = x & (x - 1) // clear rightmost non-zero bit + n++ + } + return n +} + +func PopCountByShifting(x uint64) int { + n := 0 + for i := uint(0); i < 64; i++ { + if x&(1< a, a.go => a, a/b/c.go => c, a/b.c.go => b.c +func basename(s string) string { + // Discard last '/' and everything before. + for i := len(s) - 1; i >= 0; i-- { + if s[i] == '/' { + s = s[i+1:] + break + } + } + // Preserve everything before last '.'. + for i := len(s) - 1; i >= 0; i-- { + if s[i] == '.' { + s = s[:i] + break + } + } + return s +} + +//!- diff --git a/vendor/gopl.io/ch3/basename2/main.go b/vendor/gopl.io/ch3/basename2/main.go new file mode 100644 index 0000000..93c471c --- /dev/null +++ b/vendor/gopl.io/ch3/basename2/main.go @@ -0,0 +1,36 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 72. + +// Basename2 reads file names from stdin and prints the base name of each one. +package main + +import ( + "bufio" + "fmt" + "os" + "strings" +) + +func main() { + input := bufio.NewScanner(os.Stdin) + for input.Scan() { + fmt.Println(basename(input.Text())) + } + // NOTE: ignoring potential errors from input.Err() +} + +// basename removes directory components and a trailing .suffix. +// e.g., a => a, a.go => a, a/b/c.go => c, a/b.c.go => b.c +//!+ +func basename(s string) string { + slash := strings.LastIndex(s, "/") // -1 if "/" not found + s = s[slash+1:] + if dot := strings.LastIndex(s, "."); dot >= 0 { + s = s[:dot] + } + return s +} + +//!- diff --git a/vendor/gopl.io/ch3/comma/main.go b/vendor/gopl.io/ch3/comma/main.go new file mode 100644 index 0000000..6f0f0e9 --- /dev/null +++ b/vendor/gopl.io/ch3/comma/main.go @@ -0,0 +1,40 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 73. + +// Comma prints its argument numbers with a comma at each power of 1000. +// +// Example: +// $ go build gopl.io/ch3/comma +// $ ./comma 1 12 123 1234 1234567890 +// 1 +// 12 +// 123 +// 1,234 +// 1,234,567,890 +// +package main + +import ( + "fmt" + "os" +) + +func main() { + for i := 1; i < len(os.Args); i++ { + fmt.Printf(" %s\n", comma(os.Args[i])) + } +} + +//!+ +// comma inserts commas in a non-negative decimal integer string. +func comma(s string) string { + n := len(s) + if n <= 3 { + return s + } + return comma(s[:n-3]) + "," + s[n-3:] +} + +//!- diff --git a/vendor/gopl.io/ch3/mandelbrot/main.go b/vendor/gopl.io/ch3/mandelbrot/main.go new file mode 100644 index 0000000..805922d --- /dev/null +++ b/vendor/gopl.io/ch3/mandelbrot/main.go @@ -0,0 +1,84 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 61. +//!+ + +// Mandelbrot emits a PNG image of the Mandelbrot fractal. +package main + +import ( + "image" + "image/color" + "image/png" + "math/cmplx" + "os" +) + +func main() { + const ( + xmin, ymin, xmax, ymax = -2, -2, +2, +2 + width, height = 1024, 1024 + ) + + img := image.NewRGBA(image.Rect(0, 0, width, height)) + for py := 0; py < height; py++ { + y := float64(py)/height*(ymax-ymin) + ymin + for px := 0; px < width; px++ { + x := float64(px)/width*(xmax-xmin) + xmin + z := complex(x, y) + // Image point (px, py) represents complex value z. + img.Set(px, py, mandelbrot(z)) + } + } + png.Encode(os.Stdout, img) // NOTE: ignoring errors +} + +func mandelbrot(z complex128) color.Color { + const iterations = 200 + const contrast = 15 + + var v complex128 + for n := uint8(0); n < iterations; n++ { + v = v*v + z + if cmplx.Abs(v) > 2 { + return color.Gray{255 - contrast*n} + } + } + return color.Black +} + +//!- + +// Some other interesting functions: + +func acos(z complex128) color.Color { + v := cmplx.Acos(z) + blue := uint8(real(v)*128) + 127 + red := uint8(imag(v)*128) + 127 + return color.YCbCr{192, blue, red} +} + +func sqrt(z complex128) color.Color { + v := cmplx.Sqrt(z) + blue := uint8(real(v)*128) + 127 + red := uint8(imag(v)*128) + 127 + return color.YCbCr{128, blue, red} +} + +// f(x) = x^4 - 1 +// +// z' = z - f(z)/f'(z) +// = z - (z^4 - 1) / (4 * z^3) +// = z - (z - 1/z^3) / 4 +func newton(z complex128) color.Color { + const iterations = 37 + const contrast = 7 + for i := uint8(0); i < iterations; i++ { + z -= (z - 1/(z*z*z)) / 4 + if cmplx.Abs(z*z*z*z-1) < 1e-6 { + return color.Gray{255 - contrast*i} + } + } + return color.Black +} diff --git a/vendor/gopl.io/ch3/netflag/netflag.go b/vendor/gopl.io/ch3/netflag/netflag.go new file mode 100644 index 0000000..dd4aaff --- /dev/null +++ b/vendor/gopl.io/ch3/netflag/netflag.go @@ -0,0 +1,30 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 77. + +// Netflag demonstrates an integer type used as a bit field. +package main + +import ( + "fmt" + . "net" +) + +//!+ +func IsUp(v Flags) bool { return v&FlagUp == FlagUp } +func TurnDown(v *Flags) { *v &^= FlagUp } +func SetBroadcast(v *Flags) { *v |= FlagBroadcast } +func IsCast(v Flags) bool { return v&(FlagBroadcast|FlagMulticast) != 0 } + +func main() { + var v Flags = FlagMulticast | FlagUp + fmt.Printf("%b %t\n", v, IsUp(v)) // "10001 true" + TurnDown(&v) + fmt.Printf("%b %t\n", v, IsUp(v)) // "10000 false" + SetBroadcast(&v) + fmt.Printf("%b %t\n", v, IsUp(v)) // "10010 false" + fmt.Printf("%b %t\n", v, IsCast(v)) // "10010 true" +} + +//!- diff --git a/vendor/gopl.io/ch3/printints/main.go b/vendor/gopl.io/ch3/printints/main.go new file mode 100644 index 0000000..1c875d8 --- /dev/null +++ b/vendor/gopl.io/ch3/printints/main.go @@ -0,0 +1,33 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 74. + +// Printints demonstrates the use of bytes.Buffer to format a string. +package main + +import ( + "bytes" + "fmt" +) + +//!+ +// intsToString is like fmt.Sprint(values) but adds commas. +func intsToString(values []int) string { + var buf bytes.Buffer + buf.WriteByte('[') + for i, v := range values { + if i > 0 { + buf.WriteString(", ") + } + fmt.Fprintf(&buf, "%d", v) + } + buf.WriteByte(']') + return buf.String() +} + +func main() { + fmt.Println(intsToString([]int{1, 2, 3})) // "[1, 2, 3]" +} + +//!- diff --git a/vendor/gopl.io/ch3/surface/main.go b/vendor/gopl.io/ch3/surface/main.go new file mode 100644 index 0000000..07b26d9 --- /dev/null +++ b/vendor/gopl.io/ch3/surface/main.go @@ -0,0 +1,62 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 58. +//!+ + +// Surface computes an SVG rendering of a 3-D surface function. +package main + +import ( + "fmt" + "math" +) + +const ( + width, height = 600, 320 // canvas size in pixels + cells = 100 // number of grid cells + xyrange = 30.0 // axis ranges (-xyrange..+xyrange) + xyscale = width / 2 / xyrange // pixels per x or y unit + zscale = height * 0.4 // pixels per z unit + angle = math.Pi / 6 // angle of x, y axes (=30°) +) + +var sin30, cos30 = math.Sin(angle), math.Cos(angle) // sin(30°), cos(30°) + +func main() { + fmt.Printf("", width, height) + for i := 0; i < cells; i++ { + for j := 0; j < cells; j++ { + ax, ay := corner(i+1, j) + bx, by := corner(i, j) + cx, cy := corner(i, j+1) + dx, dy := corner(i+1, j+1) + fmt.Printf("\n", + ax, ay, bx, by, cx, cy, dx, dy) + } + } + fmt.Println("") +} + +func corner(i, j int) (float64, float64) { + // Find point (x,y) at corner of cell (i,j). + x := xyrange * (float64(i)/cells - 0.5) + y := xyrange * (float64(j)/cells - 0.5) + + // Compute surface height z. + z := f(x, y) + + // Project (x,y,z) isometrically onto 2-D SVG canvas (sx,sy). + sx := width/2 + (x-y)*cos30*xyscale + sy := height/2 + (x+y)*sin30*xyscale - z*zscale + return sx, sy +} + +func f(x, y float64) float64 { + r := math.Hypot(x, y) // distance from (0,0) + return math.Sin(r) / r +} + +//!- diff --git a/vendor/gopl.io/ch4/append/main.go b/vendor/gopl.io/ch4/append/main.go new file mode 100644 index 0000000..ef95048 --- /dev/null +++ b/vendor/gopl.io/ch4/append/main.go @@ -0,0 +1,79 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 88. + +// Append illustrates the behavior of the built-in append function. +package main + +import "fmt" + +func appendslice(x []int, y ...int) []int { + var z []int + zlen := len(x) + len(y) + if zlen <= cap(x) { + // There is room to expand the slice. + z = x[:zlen] + } else { + // There is insufficient space. + // Grow by doubling, for amortized linear complexity. + zcap := zlen + if zcap < 2*len(x) { + zcap = 2 * len(x) + } + z = make([]int, zlen, zcap) + copy(z, x) + } + copy(z[len(x):], y) + return z +} + +//!+append +func appendInt(x []int, y int) []int { + var z []int + zlen := len(x) + 1 + if zlen <= cap(x) { + // There is room to grow. Extend the slice. + z = x[:zlen] + } else { + // There is insufficient space. Allocate a new array. + // Grow by doubling, for amortized linear complexity. + zcap := zlen + if zcap < 2*len(x) { + zcap = 2 * len(x) + } + z = make([]int, zlen, zcap) + copy(z, x) // a built-in function; see text + } + z[len(x)] = y + return z +} + +//!-append + +//!+growth +func main() { + var x, y []int + for i := 0; i < 10; i++ { + y = appendInt(x, i) + fmt.Printf("%d cap=%d\t%v\n", i, cap(y), y) + x = y + } +} + +//!-growth + +/* +//!+output +0 cap=1 [0] +1 cap=2 [0 1] +2 cap=4 [0 1 2] +3 cap=4 [0 1 2 3] +4 cap=8 [0 1 2 3 4] +5 cap=8 [0 1 2 3 4 5] +6 cap=8 [0 1 2 3 4 5 6] +7 cap=8 [0 1 2 3 4 5 6 7] +8 cap=16 [0 1 2 3 4 5 6 7 8] +9 cap=16 [0 1 2 3 4 5 6 7 8 9] +//!-output +*/ diff --git a/vendor/gopl.io/ch4/autoescape/main.go b/vendor/gopl.io/ch4/autoescape/main.go new file mode 100644 index 0000000..d39e57d --- /dev/null +++ b/vendor/gopl.io/ch4/autoescape/main.go @@ -0,0 +1,30 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 117. + +// Autoescape demonstrates automatic HTML escaping in html/template. +package main + +import ( + "html/template" + "log" + "os" +) + +//!+ +func main() { + const templ = `

A: {{.A}}

B: {{.B}}

` + t := template.Must(template.New("escape").Parse(templ)) + var data struct { + A string // untrusted plain text + B template.HTML // trusted HTML + } + data.A = "Hello!" + data.B = "Hello!" + if err := t.Execute(os.Stdout, data); err != nil { + log.Fatal(err) + } +} + +//!- diff --git a/vendor/gopl.io/ch4/charcount/main.go b/vendor/gopl.io/ch4/charcount/main.go new file mode 100644 index 0000000..ae7a4db --- /dev/null +++ b/vendor/gopl.io/ch4/charcount/main.go @@ -0,0 +1,56 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 97. +//!+ + +// Charcount computes counts of Unicode characters. +package main + +import ( + "bufio" + "fmt" + "io" + "os" + "unicode" + "unicode/utf8" +) + +func main() { + counts := make(map[rune]int) // counts of Unicode characters + var utflen [utf8.UTFMax + 1]int // count of lengths of UTF-8 encodings + invalid := 0 // count of invalid UTF-8 characters + + in := bufio.NewReader(os.Stdin) + for { + r, n, err := in.ReadRune() // returns rune, nbytes, error + if err == io.EOF { + break + } + if err != nil { + fmt.Fprintf(os.Stderr, "charcount: %v\n", err) + os.Exit(1) + } + if r == unicode.ReplacementChar && n == 1 { + invalid++ + continue + } + counts[r]++ + utflen[n]++ + } + fmt.Printf("rune\tcount\n") + for c, n := range counts { + fmt.Printf("%q\t%d\n", c, n) + } + fmt.Print("\nlen\tcount\n") + for i, n := range utflen { + if i > 0 { + fmt.Printf("%d\t%d\n", i, n) + } + } + if invalid > 0 { + fmt.Printf("\n%d invalid UTF-8 characters\n", invalid) + } +} + +//!- diff --git a/vendor/gopl.io/ch4/dedup/main.go b/vendor/gopl.io/ch4/dedup/main.go new file mode 100644 index 0000000..bf32e93 --- /dev/null +++ b/vendor/gopl.io/ch4/dedup/main.go @@ -0,0 +1,33 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 96. + +// Dedup prints only one instance of each line; duplicates are removed. +package main + +import ( + "bufio" + "fmt" + "os" +) + +//!+ +func main() { + seen := make(map[string]bool) // a set of strings + input := bufio.NewScanner(os.Stdin) + for input.Scan() { + line := input.Text() + if !seen[line] { + seen[line] = true + fmt.Println(line) + } + } + + if err := input.Err(); err != nil { + fmt.Fprintf(os.Stderr, "dedup: %v\n", err) + os.Exit(1) + } +} + +//!- diff --git a/vendor/gopl.io/ch4/embed/main.go b/vendor/gopl.io/ch4/embed/main.go new file mode 100644 index 0000000..f85cfcf --- /dev/null +++ b/vendor/gopl.io/ch4/embed/main.go @@ -0,0 +1,46 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 106. + +// Embed demonstrates basic struct embedding. +package main + +import "fmt" + +type Point struct{ X, Y int } + +type Circle struct { + Point + Radius int +} + +type Wheel struct { + Circle + Spokes int +} + +func main() { + var w Wheel + //!+ + w = Wheel{Circle{Point{8, 8}, 5}, 20} + + w = Wheel{ + Circle: Circle{ + Point: Point{X: 8, Y: 8}, + Radius: 5, + }, + Spokes: 20, // NOTE: trailing comma necessary here (and at Radius) + } + + fmt.Printf("%#v\n", w) + // Output: + // Wheel{Circle:Circle{Point:Point{X:8, Y:8}, Radius:5}, Spokes:20} + + w.X = 42 + + fmt.Printf("%#v\n", w) + // Output: + // Wheel{Circle:Circle{Point:Point{X:42, Y:8}, Radius:5}, Spokes:20} + //!- +} diff --git a/vendor/gopl.io/ch4/github/github.go b/vendor/gopl.io/ch4/github/github.go new file mode 100644 index 0000000..053b954 --- /dev/null +++ b/vendor/gopl.io/ch4/github/github.go @@ -0,0 +1,35 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 110. +//!+ + +// Package github provides a Go API for the GitHub issue tracker. +// See https://developer.github.com/v3/search/#search-issues. +package github + +import "time" + +const IssuesURL = "https://api.github.com/search/issues" + +type IssuesSearchResult struct { + TotalCount int `json:"total_count"` + Items []*Issue +} + +type Issue struct { + Number int + HTMLURL string `json:"html_url"` + Title string + State string + User *User + CreatedAt time.Time `json:"created_at"` + Body string // in Markdown format +} + +type User struct { + Login string + HTMLURL string `json:"html_url"` +} + +//!- diff --git a/vendor/gopl.io/ch4/github/search.go b/vendor/gopl.io/ch4/github/search.go new file mode 100644 index 0000000..070666c --- /dev/null +++ b/vendor/gopl.io/ch4/github/search.go @@ -0,0 +1,53 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +//!+ + +package github + +import ( + "encoding/json" + "fmt" + "net/http" + "net/url" + "strings" +) + +// SearchIssues queries the GitHub issue tracker. +func SearchIssues(terms []string) (*IssuesSearchResult, error) { + q := url.QueryEscape(strings.Join(terms, " ")) + resp, err := http.Get(IssuesURL + "?q=" + q) + if err != nil { + return nil, err + } + //!- + // For long-term stability, instead of http.Get, use the + // variant below which adds an HTTP request header indicating + // that only version 3 of the GitHub API is acceptable. + // + // req, err := http.NewRequest("GET", IssuesURL+"?q="+q, nil) + // if err != nil { + // return nil, err + // } + // req.Header.Set( + // "Accept", "application/vnd.github.v3.text-match+json") + // resp, err := http.DefaultClient.Do(req) + //!+ + + // We must close resp.Body on all execution paths. + // (Chapter 5 presents 'defer', which makes this simpler.) + if resp.StatusCode != http.StatusOK { + resp.Body.Close() + return nil, fmt.Errorf("search query failed: %s", resp.Status) + } + + var result IssuesSearchResult + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + resp.Body.Close() + return nil, err + } + resp.Body.Close() + return &result, nil +} + +//!- diff --git a/vendor/gopl.io/ch4/graph/main.go b/vendor/gopl.io/ch4/graph/main.go new file mode 100644 index 0000000..e47d90c --- /dev/null +++ b/vendor/gopl.io/ch4/graph/main.go @@ -0,0 +1,43 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 99. + +// Graph shows how to use a map of maps to represent a directed graph. +package main + +import "fmt" + +//!+ +var graph = make(map[string]map[string]bool) + +func addEdge(from, to string) { + edges := graph[from] + if edges == nil { + edges = make(map[string]bool) + graph[from] = edges + } + edges[to] = true +} + +func hasEdge(from, to string) bool { + return graph[from][to] +} + +//!- + +func main() { + addEdge("a", "b") + addEdge("c", "d") + addEdge("a", "d") + addEdge("d", "a") + fmt.Println(hasEdge("a", "b")) + fmt.Println(hasEdge("c", "d")) + fmt.Println(hasEdge("a", "d")) + fmt.Println(hasEdge("d", "a")) + fmt.Println(hasEdge("x", "b")) + fmt.Println(hasEdge("c", "d")) + fmt.Println(hasEdge("x", "d")) + fmt.Println(hasEdge("d", "x")) + +} diff --git a/vendor/gopl.io/ch4/issues/main.go b/vendor/gopl.io/ch4/issues/main.go new file mode 100644 index 0000000..b1b6dbf --- /dev/null +++ b/vendor/gopl.io/ch4/issues/main.go @@ -0,0 +1,52 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 112. +//!+ + +// Issues prints a table of GitHub issues matching the search terms. +package main + +import ( + "fmt" + "log" + "os" + + "gopl.io/ch4/github" +) + +//!+ +func main() { + result, err := github.SearchIssues(os.Args[1:]) + if err != nil { + log.Fatal(err) + } + fmt.Printf("%d issues:\n", result.TotalCount) + for _, item := range result.Items { + fmt.Printf("#%-5d %9.9s %.55s\n", + item.Number, item.User.Login, item.Title) + } +} + +//!- + +/* +//!+textoutput +$ go build gopl.io/ch4/issues +$ ./issues repo:golang/go is:open json decoder +13 issues: +#5680 eaigner encoding/json: set key converter on en/decoder +#6050 gopherbot encoding/json: provide tokenizer +#8658 gopherbot encoding/json: use bufio +#8462 kortschak encoding/json: UnmarshalText confuses json.Unmarshal +#5901 rsc encoding/json: allow override type marshaling +#9812 klauspost encoding/json: string tag not symmetric +#7872 extempora encoding/json: Encoder internally buffers full output +#9650 cespare encoding/json: Decoding gives errPhase when unmarshalin +#6716 gopherbot encoding/json: include field name in unmarshal error me +#6901 lukescott encoding/json, encoding/xml: option to treat unknown fi +#6384 joeshaw encoding/json: encode precise floating point integers u +#6647 btracey x/tools/cmd/godoc: display type kind of each named type +#4237 gjemiller encoding/base64: URLEncoding padding is optional +//!-textoutput +*/ diff --git a/vendor/gopl.io/ch4/issueshtml/main.go b/vendor/gopl.io/ch4/issueshtml/main.go new file mode 100644 index 0000000..18ad896 --- /dev/null +++ b/vendor/gopl.io/ch4/issueshtml/main.go @@ -0,0 +1,52 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 115. + +// Issueshtml prints an HTML table of issues matching the search terms. +package main + +import ( + "log" + "os" + + "gopl.io/ch4/github" +) + +//!+template +import "html/template" + +var issueList = template.Must(template.New("issuelist").Parse(` +

{{.TotalCount}} issues

+ + + + + + + +{{range .Items}} + + + + + + +{{end}} +
#StateUserTitle
{{.Number}}{{.State}}{{.User.Login}}{{.Title}}
+`)) + +//!-template + +//!+ +func main() { + result, err := github.SearchIssues(os.Args[1:]) + if err != nil { + log.Fatal(err) + } + if err := issueList.Execute(os.Stdout, result); err != nil { + log.Fatal(err) + } +} + +//!- diff --git a/vendor/gopl.io/ch4/issuesreport/main.go b/vendor/gopl.io/ch4/issuesreport/main.go new file mode 100644 index 0000000..2d41afd --- /dev/null +++ b/vendor/gopl.io/ch4/issuesreport/main.go @@ -0,0 +1,89 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 113. + +// Issuesreport prints a report of issues matching the search terms. +package main + +import ( + "log" + "os" + "text/template" + "time" + + "gopl.io/ch4/github" +) + +//!+template +const templ = `{{.TotalCount}} issues: +{{range .Items}}---------------------------------------- +Number: {{.Number}} +User: {{.User.Login}} +Title: {{.Title | printf "%.64s"}} +Age: {{.CreatedAt | daysAgo}} days +{{end}}` + +//!-template + +//!+daysAgo +func daysAgo(t time.Time) int { + return int(time.Since(t).Hours() / 24) +} + +//!-daysAgo + +//!+exec +var report = template.Must(template.New("issuelist"). + Funcs(template.FuncMap{"daysAgo": daysAgo}). + Parse(templ)) + +func main() { + result, err := github.SearchIssues(os.Args[1:]) + if err != nil { + log.Fatal(err) + } + if err := report.Execute(os.Stdout, result); err != nil { + log.Fatal(err) + } +} + +//!-exec + +func noMust() { + //!+parse + report, err := template.New("report"). + Funcs(template.FuncMap{"daysAgo": daysAgo}). + Parse(templ) + if err != nil { + log.Fatal(err) + } + //!-parse + result, err := github.SearchIssues(os.Args[1:]) + if err != nil { + log.Fatal(err) + } + if err := report.Execute(os.Stdout, result); err != nil { + log.Fatal(err) + } +} + +/* +//!+output +$ go build gopl.io/ch4/issuesreport +$ ./issuesreport repo:golang/go is:open json decoder +13 issues: +---------------------------------------- +Number: 5680 +User: eaigner +Title: encoding/json: set key converter on en/decoder +Age: 750 days +---------------------------------------- +Number: 6050 +User: gopherbot +Title: encoding/json: provide tokenizer +Age: 695 days +---------------------------------------- +... +//!-output +*/ diff --git a/vendor/gopl.io/ch4/movie/main.go b/vendor/gopl.io/ch4/movie/main.go new file mode 100644 index 0000000..6429410 --- /dev/null +++ b/vendor/gopl.io/ch4/movie/main.go @@ -0,0 +1,104 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 108. + +// Movie prints Movies as JSON. +package main + +import ( + "encoding/json" + "fmt" + "log" +) + +//!+ +type Movie struct { + Title string + Year int `json:"released"` + Color bool `json:"color,omitempty"` + Actors []string +} + +var movies = []Movie{ + {Title: "Casablanca", Year: 1942, Color: false, + Actors: []string{"Humphrey Bogart", "Ingrid Bergman"}}, + {Title: "Cool Hand Luke", Year: 1967, Color: true, + Actors: []string{"Paul Newman"}}, + {Title: "Bullitt", Year: 1968, Color: true, + Actors: []string{"Steve McQueen", "Jacqueline Bisset"}}, + // ... +} + +//!- + +func main() { + { + //!+Marshal + data, err := json.Marshal(movies) + if err != nil { + log.Fatalf("JSON marshaling failed: %s", err) + } + fmt.Printf("%s\n", data) + //!-Marshal + } + + { + //!+MarshalIndent + data, err := json.MarshalIndent(movies, "", " ") + if err != nil { + log.Fatalf("JSON marshaling failed: %s", err) + } + fmt.Printf("%s\n", data) + //!-MarshalIndent + + //!+Unmarshal + var titles []struct{ Title string } + if err := json.Unmarshal(data, &titles); err != nil { + log.Fatalf("JSON unmarshaling failed: %s", err) + } + fmt.Println(titles) // "[{Casablanca} {Cool Hand Luke} {Bullitt}]" + //!-Unmarshal + } +} + +/* +//!+output +[{"Title":"Casablanca","released":1942,"Actors":["Humphrey Bogart","Ingr +id Bergman"]},{"Title":"Cool Hand Luke","released":1967,"color":true,"Ac +tors":["Paul Newman"]},{"Title":"Bullitt","released":1968,"color":true," +Actors":["Steve McQueen","Jacqueline Bisset"]}] +//!-output +*/ + +/* +//!+indented +[ + { + "Title": "Casablanca", + "released": 1942, + "Actors": [ + "Humphrey Bogart", + "Ingrid Bergman" + ] + }, + { + "Title": "Cool Hand Luke", + "released": 1967, + "color": true, + "Actors": [ + "Paul Newman" + ] + }, + { + "Title": "Bullitt", + "released": 1968, + "color": true, + "Actors": [ + "Steve McQueen", + "Jacqueline Bisset" + ] + } +] +//!-indented +*/ diff --git a/vendor/gopl.io/ch4/nonempty/main.go b/vendor/gopl.io/ch4/nonempty/main.go new file mode 100644 index 0000000..2a20c84 --- /dev/null +++ b/vendor/gopl.io/ch4/nonempty/main.go @@ -0,0 +1,47 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 91. + +//!+nonempty + +// Nonempty is an example of an in-place slice algorithm. +package main + +import "fmt" + +// nonempty returns a slice holding only the non-empty strings. +// The underlying array is modified during the call. +func nonempty(strings []string) []string { + i := 0 + for _, s := range strings { + if s != "" { + strings[i] = s + i++ + } + } + return strings[:i] +} + +//!-nonempty + +func main() { + //!+main + data := []string{"one", "", "three"} + fmt.Printf("%q\n", nonempty(data)) // `["one" "three"]` + fmt.Printf("%q\n", data) // `["one" "three" "three"]` + //!-main +} + +//!+alt +func nonempty2(strings []string) []string { + out := strings[:0] // zero-length slice of original + for _, s := range strings { + if s != "" { + out = append(out, s) + } + } + return out +} + +//!-alt diff --git a/vendor/gopl.io/ch4/rev/main.go b/vendor/gopl.io/ch4/rev/main.go new file mode 100644 index 0000000..039b58a --- /dev/null +++ b/vendor/gopl.io/ch4/rev/main.go @@ -0,0 +1,60 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 86. + +// Rev reverses a slice. +package main + +import ( + "bufio" + "fmt" + "os" + "strconv" + "strings" +) + +func main() { + //!+array + a := [...]int{0, 1, 2, 3, 4, 5} + reverse(a[:]) + fmt.Println(a) // "[5 4 3 2 1 0]" + //!-array + + //!+slice + s := []int{0, 1, 2, 3, 4, 5} + // Rotate s left by two positions. + reverse(s[:2]) + reverse(s[2:]) + reverse(s) + fmt.Println(s) // "[2 3 4 5 0 1]" + //!-slice + + // Interactive test of reverse. + input := bufio.NewScanner(os.Stdin) +outer: + for input.Scan() { + var ints []int + for _, s := range strings.Fields(input.Text()) { + x, err := strconv.ParseInt(s, 10, 64) + if err != nil { + fmt.Fprintln(os.Stderr, err) + continue outer + } + ints = append(ints, int(x)) + } + reverse(ints) + fmt.Printf("%v\n", ints) + } + // NOTE: ignoring potential errors from input.Err() +} + +//!+rev +// reverse reverses a slice of ints in place. +func reverse(s []int) { + for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 { + s[i], s[j] = s[j], s[i] + } +} + +//!-rev diff --git a/vendor/gopl.io/ch4/sha256/main.go b/vendor/gopl.io/ch4/sha256/main.go new file mode 100644 index 0000000..51e7b24 --- /dev/null +++ b/vendor/gopl.io/ch4/sha256/main.go @@ -0,0 +1,25 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 83. + +// The sha256 command computes the SHA256 hash (an array) of a string. +package main + +import "fmt" + +//!+ +import "crypto/sha256" + +func main() { + c1 := sha256.Sum256([]byte("x")) + c2 := sha256.Sum256([]byte("X")) + fmt.Printf("%x\n%x\n%t\n%T\n", c1, c2, c1 == c2, c1) + // Output: + // 2d711642b726b04401627ca9fbac32f5c8530fb1903cc4db02258717921a4881 + // 4b68ab3847feda7d6c62c1fbcbeebfa35eab7351ed5e78f4ddadea5df64b8015 + // false + // [32]uint8 +} + +//!- diff --git a/vendor/gopl.io/ch4/treesort/sort.go b/vendor/gopl.io/ch4/treesort/sort.go new file mode 100644 index 0000000..f9ee575 --- /dev/null +++ b/vendor/gopl.io/ch4/treesort/sort.go @@ -0,0 +1,50 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 101. + +// Package treesort provides insertion sort using an unbalanced binary tree. +package treesort + +//!+ +type tree struct { + value int + left, right *tree +} + +// Sort sorts values in place. +func Sort(values []int) { + var root *tree + for _, v := range values { + root = add(root, v) + } + appendValues(values[:0], root) +} + +// appendValues appends the elements of t to values in order +// and returns the resulting slice. +func appendValues(values []int, t *tree) []int { + if t != nil { + values = appendValues(values, t.left) + values = append(values, t.value) + values = appendValues(values, t.right) + } + return values +} + +func add(t *tree, value int) *tree { + if t == nil { + // Equivalent to return &tree{value: value}. + t = new(tree) + t.value = value + return t + } + if value < t.value { + t.left = add(t.left, value) + } else { + t.right = add(t.right, value) + } + return t +} + +//!- diff --git a/vendor/gopl.io/ch4/treesort/sort_test.go b/vendor/gopl.io/ch4/treesort/sort_test.go new file mode 100644 index 0000000..bf0cb02 --- /dev/null +++ b/vendor/gopl.io/ch4/treesort/sort_test.go @@ -0,0 +1,23 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +package treesort_test + +import ( + "math/rand" + "sort" + "testing" + + "gopl.io/ch4/treesort" +) + +func TestSort(t *testing.T) { + data := make([]int, 50) + for i := range data { + data[i] = rand.Int() % 50 + } + treesort.Sort(data) + if !sort.IntsAreSorted(data) { + t.Errorf("not sorted: %v", data) + } +} diff --git a/vendor/gopl.io/ch5/defer1/defer.go b/vendor/gopl.io/ch5/defer1/defer.go new file mode 100644 index 0000000..3d2b80e --- /dev/null +++ b/vendor/gopl.io/ch5/defer1/defer.go @@ -0,0 +1,48 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 150. + +// Defer1 demonstrates a deferred call being invoked during a panic. +package main + +import "fmt" + +//!+f +func main() { + f(3) +} + +func f(x int) { + fmt.Printf("f(%d)\n", x+0/x) // panics if x == 0 + defer fmt.Printf("defer %d\n", x) + f(x - 1) +} + +//!-f + +/* +//!+stdout +f(3) +f(2) +f(1) +defer 1 +defer 2 +defer 3 +//!-stdout + +//!+stderr +panic: runtime error: integer divide by zero +main.f(0) + src/gopl.io/ch5/defer1/defer.go:14 +main.f(1) + src/gopl.io/ch5/defer1/defer.go:16 +main.f(2) + src/gopl.io/ch5/defer1/defer.go:16 + +main.f(3) + src/gopl.io/ch5/defer1/defer.go:16 +main.main() + src/gopl.io/ch5/defer1/defer.go:10 +//!-stderr +*/ diff --git a/vendor/gopl.io/ch5/defer2/defer.go b/vendor/gopl.io/ch5/defer2/defer.go new file mode 100644 index 0000000..25fa881 --- /dev/null +++ b/vendor/gopl.io/ch5/defer2/defer.go @@ -0,0 +1,51 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 151. + +// Defer2 demonstrates a deferred call to runtime.Stack during a panic. +package main + +import ( + "fmt" + "os" + "runtime" +) + +//!+ +func main() { + defer printStack() + f(3) +} + +func printStack() { + var buf [4096]byte + n := runtime.Stack(buf[:], false) + os.Stdout.Write(buf[:n]) +} + +//!- + +func f(x int) { + fmt.Printf("f(%d)\n", x+0/x) // panics if x == 0 + defer fmt.Printf("defer %d\n", x) + f(x - 1) +} + +/* +//!+printstack +goroutine 1 [running]: +main.printStack() + src/gopl.io/ch5/defer2/defer.go:20 +main.f(0) + src/gopl.io/ch5/defer2/defer.go:27 +main.f(1) + src/gopl.io/ch5/defer2/defer.go:29 +main.f(2) + src/gopl.io/ch5/defer2/defer.go:29 +main.f(3) + src/gopl.io/ch5/defer2/defer.go:29 +main.main() + src/gopl.io/ch5/defer2/defer.go:15 +//!-printstack +*/ diff --git a/vendor/gopl.io/ch5/fetch/main.go b/vendor/gopl.io/ch5/fetch/main.go new file mode 100644 index 0000000..c838195 --- /dev/null +++ b/vendor/gopl.io/ch5/fetch/main.go @@ -0,0 +1,54 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 148. + +// Fetch saves the contents of a URL into a local file. +package main + +import ( + "fmt" + "io" + "net/http" + "os" + "path" +) + +//!+ +// Fetch downloads the URL and returns the +// name and length of the local file. +func fetch(url string) (filename string, n int64, err error) { + resp, err := http.Get(url) + if err != nil { + return "", 0, err + } + defer resp.Body.Close() + + local := path.Base(resp.Request.URL.Path) + if local == "/" { + local = "index.html" + } + f, err := os.Create(local) + if err != nil { + return "", 0, err + } + n, err = io.Copy(f, resp.Body) + // Close file, but prefer error from Copy, if any. + if closeErr := f.Close(); err == nil { + err = closeErr + } + return local, n, err +} + +//!- + +func main() { + for _, url := range os.Args[1:] { + local, n, err := fetch(url) + if err != nil { + fmt.Fprintf(os.Stderr, "fetch %s: %v\n", url, err) + continue + } + fmt.Fprintf(os.Stderr, "%s => %s (%d bytes).\n", url, local, n) + } +} diff --git a/vendor/gopl.io/ch5/findlinks1/main.go b/vendor/gopl.io/ch5/findlinks1/main.go new file mode 100644 index 0000000..8996671 --- /dev/null +++ b/vendor/gopl.io/ch5/findlinks1/main.go @@ -0,0 +1,76 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 122. +//!+main + +// Findlinks1 prints the links in an HTML document read from standard input. +package main + +import ( + "fmt" + "os" + + "golang.org/x/net/html" +) + +func main() { + doc, err := html.Parse(os.Stdin) + if err != nil { + fmt.Fprintf(os.Stderr, "findlinks1: %v\n", err) + os.Exit(1) + } + for _, link := range visit(nil, doc) { + fmt.Println(link) + } +} + +//!-main + +//!+visit +// visit appends to links each link found in n and returns the result. +func visit(links []string, n *html.Node) []string { + if n.Type == html.ElementNode && n.Data == "a" { + for _, a := range n.Attr { + if a.Key == "href" { + links = append(links, a.Val) + } + } + } + for c := n.FirstChild; c != nil; c = c.NextSibling { + links = visit(links, c) + } + return links +} + +//!-visit + +/* +//!+html +package html + +type Node struct { + Type NodeType + Data string + Attr []Attribute + FirstChild, NextSibling *Node +} + +type NodeType int32 + +const ( + ErrorNode NodeType = iota + TextNode + DocumentNode + ElementNode + CommentNode + DoctypeNode +) + +type Attribute struct { + Key, Val string +} + +func Parse(r io.Reader) (*Node, error) +//!-html +*/ diff --git a/vendor/gopl.io/ch5/findlinks2/main.go b/vendor/gopl.io/ch5/findlinks2/main.go new file mode 100644 index 0000000..35ba2a5 --- /dev/null +++ b/vendor/gopl.io/ch5/findlinks2/main.go @@ -0,0 +1,69 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 125. + +// Findlinks2 does an HTTP GET on each URL, parses the +// result as HTML, and prints the links within it. +// +// Usage: +// findlinks url ... +package main + +import ( + "fmt" + "net/http" + "os" + + "golang.org/x/net/html" +) + +// visit appends to links each link found in n, and returns the result. +func visit(links []string, n *html.Node) []string { + if n.Type == html.ElementNode && n.Data == "a" { + for _, a := range n.Attr { + if a.Key == "href" { + links = append(links, a.Val) + } + } + } + for c := n.FirstChild; c != nil; c = c.NextSibling { + links = visit(links, c) + } + return links +} + +//!+ +func main() { + for _, url := range os.Args[1:] { + links, err := findLinks(url) + if err != nil { + fmt.Fprintf(os.Stderr, "findlinks2: %v\n", err) + continue + } + for _, link := range links { + fmt.Println(link) + } + } +} + +// findLinks performs an HTTP GET request for url, parses the +// response as HTML, and extracts and returns the links. +func findLinks(url string) ([]string, error) { + resp, err := http.Get(url) + if err != nil { + return nil, err + } + if resp.StatusCode != http.StatusOK { + resp.Body.Close() + return nil, fmt.Errorf("getting %s: %s", url, resp.Status) + } + doc, err := html.Parse(resp.Body) + resp.Body.Close() + if err != nil { + return nil, fmt.Errorf("parsing %s as HTML: %v", url, err) + } + return visit(nil, doc), nil +} + +//!- diff --git a/vendor/gopl.io/ch5/findlinks3/findlinks.go b/vendor/gopl.io/ch5/findlinks3/findlinks.go new file mode 100644 index 0000000..f45b39e --- /dev/null +++ b/vendor/gopl.io/ch5/findlinks3/findlinks.go @@ -0,0 +1,56 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 139. + +// Findlinks3 crawls the web, starting with the URLs on the command line. +package main + +import ( + "fmt" + "log" + "os" + + "gopl.io/ch5/links" +) + +//!+breadthFirst +// breadthFirst calls f for each item in the worklist. +// Any items returned by f are added to the worklist. +// f is called at most once for each item. +func breadthFirst(f func(item string) []string, worklist []string) { + seen := make(map[string]bool) + for len(worklist) > 0 { + items := worklist + worklist = nil + for _, item := range items { + if !seen[item] { + seen[item] = true + worklist = append(worklist, f(item)...) + } + } + } +} + +//!-breadthFirst + +//!+crawl +func crawl(url string) []string { + fmt.Println(url) + list, err := links.Extract(url) + if err != nil { + log.Print(err) + } + return list +} + +//!-crawl + +//!+main +func main() { + // Crawl the web breadth-first, + // starting from the command-line arguments. + breadthFirst(crawl, os.Args[1:]) +} + +//!-main diff --git a/vendor/gopl.io/ch5/links/links.go b/vendor/gopl.io/ch5/links/links.go new file mode 100644 index 0000000..2df9e14 --- /dev/null +++ b/vendor/gopl.io/ch5/links/links.go @@ -0,0 +1,67 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 138. +//!+Extract + +// Package links provides a link-extraction function. +package links + +import ( + "fmt" + "net/http" + + "golang.org/x/net/html" +) + +// Extract makes an HTTP GET request to the specified URL, parses +// the response as HTML, and returns the links in the HTML document. +func Extract(url string) ([]string, error) { + resp, err := http.Get(url) + if err != nil { + return nil, err + } + if resp.StatusCode != http.StatusOK { + resp.Body.Close() + return nil, fmt.Errorf("getting %s: %s", url, resp.Status) + } + + doc, err := html.Parse(resp.Body) + resp.Body.Close() + if err != nil { + return nil, fmt.Errorf("parsing %s as HTML: %v", url, err) + } + + var links []string + visitNode := func(n *html.Node) { + if n.Type == html.ElementNode && n.Data == "a" { + for _, a := range n.Attr { + if a.Key != "href" { + continue + } + link, err := resp.Request.URL.Parse(a.Val) + if err != nil { + continue // ignore bad URLs + } + links = append(links, link.String()) + } + } + } + forEachNode(doc, visitNode, nil) + return links, nil +} + +//!-Extract + +// Copied from gopl.io/ch5/outline2. +func forEachNode(n *html.Node, pre, post func(n *html.Node)) { + if pre != nil { + pre(n) + } + for c := n.FirstChild; c != nil; c = c.NextSibling { + forEachNode(c, pre, post) + } + if post != nil { + post(n) + } +} diff --git a/vendor/gopl.io/ch5/outline/main.go b/vendor/gopl.io/ch5/outline/main.go new file mode 100644 index 0000000..d3ccbf9 --- /dev/null +++ b/vendor/gopl.io/ch5/outline/main.go @@ -0,0 +1,36 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 123. + +// Outline prints the outline of an HTML document tree. +package main + +import ( + "fmt" + "os" + + "golang.org/x/net/html" +) + +//!+ +func main() { + doc, err := html.Parse(os.Stdin) + if err != nil { + fmt.Fprintf(os.Stderr, "outline: %v\n", err) + os.Exit(1) + } + outline(nil, doc) +} + +func outline(stack []string, n *html.Node) { + if n.Type == html.ElementNode { + stack = append(stack, n.Data) // push tag + fmt.Println(stack) + } + for c := n.FirstChild; c != nil; c = c.NextSibling { + outline(stack, c) + } +} + +//!- diff --git a/vendor/gopl.io/ch5/outline2/outline.go b/vendor/gopl.io/ch5/outline2/outline.go new file mode 100644 index 0000000..d3aee13 --- /dev/null +++ b/vendor/gopl.io/ch5/outline2/outline.go @@ -0,0 +1,80 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 133. + +// Outline prints the outline of an HTML document tree. +package main + +import ( + "fmt" + "net/http" + "os" + + "golang.org/x/net/html" +) + +func main() { + for _, url := range os.Args[1:] { + outline(url) + } +} + +func outline(url string) error { + resp, err := http.Get(url) + if err != nil { + return err + } + defer resp.Body.Close() + + doc, err := html.Parse(resp.Body) + if err != nil { + return err + } + + //!+call + forEachNode(doc, startElement, endElement) + //!-call + + return nil +} + +//!+forEachNode +// forEachNode calls the functions pre(x) and post(x) for each node +// x in the tree rooted at n. Both functions are optional. +// pre is called before the children are visited (preorder) and +// post is called after (postorder). +func forEachNode(n *html.Node, pre, post func(n *html.Node)) { + if pre != nil { + pre(n) + } + + for c := n.FirstChild; c != nil; c = c.NextSibling { + forEachNode(c, pre, post) + } + + if post != nil { + post(n) + } +} + +//!-forEachNode + +//!+startend +var depth int + +func startElement(n *html.Node) { + if n.Type == html.ElementNode { + fmt.Printf("%*s<%s>\n", depth*2, "", n.Data) + depth++ + } +} + +func endElement(n *html.Node) { + if n.Type == html.ElementNode { + depth-- + fmt.Printf("%*s\n", depth*2, "", n.Data) + } +} + +//!-startend diff --git a/vendor/gopl.io/ch5/squares/main.go b/vendor/gopl.io/ch5/squares/main.go new file mode 100644 index 0000000..1e10060 --- /dev/null +++ b/vendor/gopl.io/ch5/squares/main.go @@ -0,0 +1,30 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 135. + +// The squares program demonstrates a function value with state. +package main + +import "fmt" + +//!+ +// squares returns a function that returns +// the next square number each time it is called. +func squares() func() int { + var x int + return func() int { + x++ + return x * x + } +} + +func main() { + f := squares() + fmt.Println(f()) // "1" + fmt.Println(f()) // "4" + fmt.Println(f()) // "9" + fmt.Println(f()) // "16" +} + +//!- diff --git a/vendor/gopl.io/ch5/sum/main.go b/vendor/gopl.io/ch5/sum/main.go new file mode 100644 index 0000000..0a60d35 --- /dev/null +++ b/vendor/gopl.io/ch5/sum/main.go @@ -0,0 +1,33 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 142. + +// The sum program demonstrates a variadic function. +package main + +import "fmt" + +//!+ +func sum(vals ...int) int { + total := 0 + for _, val := range vals { + total += val + } + return total +} + +//!- + +func main() { + //!+main + fmt.Println(sum()) // "0" + fmt.Println(sum(3)) // "3" + fmt.Println(sum(1, 2, 3, 4)) // "10" + //!-main + + //!+slice + values := []int{1, 2, 3, 4} + fmt.Println(sum(values...)) // "10" + //!-slice +} diff --git a/vendor/gopl.io/ch5/title1/title.go b/vendor/gopl.io/ch5/title1/title.go new file mode 100644 index 0000000..65b5276 --- /dev/null +++ b/vendor/gopl.io/ch5/title1/title.go @@ -0,0 +1,82 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 144. + +// Title1 prints the title of an HTML document specified by a URL. +package main + +/* +//!+output +$ go build gopl.io/ch5/title1 +$ ./title1 http://gopl.io +The Go Programming Language +$ ./title1 https://golang.org/doc/effective_go.html +Effective Go - The Go Programming Language +$ ./title1 https://golang.org/doc/gopher/frontpage.png +title: https://golang.org/doc/gopher/frontpage.png + has type image/png, not text/html +//!-output +*/ + +import ( + "fmt" + "net/http" + "os" + "strings" + + "golang.org/x/net/html" +) + +// Copied from gopl.io/ch5/outline2. +func forEachNode(n *html.Node, pre, post func(n *html.Node)) { + if pre != nil { + pre(n) + } + for c := n.FirstChild; c != nil; c = c.NextSibling { + forEachNode(c, pre, post) + } + if post != nil { + post(n) + } +} + +//!+ +func title(url string) error { + resp, err := http.Get(url) + if err != nil { + return err + } + + // Check Content-Type is HTML (e.g., "text/html; charset=utf-8"). + ct := resp.Header.Get("Content-Type") + if ct != "text/html" && !strings.HasPrefix(ct, "text/html;") { + resp.Body.Close() + return fmt.Errorf("%s has type %s, not text/html", url, ct) + } + + doc, err := html.Parse(resp.Body) + resp.Body.Close() + if err != nil { + return fmt.Errorf("parsing %s as HTML: %v", url, err) + } + + visitNode := func(n *html.Node) { + if n.Type == html.ElementNode && n.Data == "title" && + n.FirstChild != nil { + fmt.Println(n.FirstChild.Data) + } + } + forEachNode(doc, visitNode, nil) + return nil +} + +//!- + +func main() { + for _, arg := range os.Args[1:] { + if err := title(arg); err != nil { + fmt.Fprintf(os.Stderr, "title: %v\n", err) + } + } +} diff --git a/vendor/gopl.io/ch5/title2/title.go b/vendor/gopl.io/ch5/title2/title.go new file mode 100644 index 0000000..1641739 --- /dev/null +++ b/vendor/gopl.io/ch5/title2/title.go @@ -0,0 +1,72 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 145. + +// Title2 prints the title of an HTML document specified by a URL. +// It uses defer to simplify closing the response body stream. +package main + +import ( + "fmt" + "net/http" + "os" + "strings" + + "golang.org/x/net/html" +) + +// Copied from gopl.io/ch5/outline2. +func forEachNode(n *html.Node, pre, post func(n *html.Node)) { + if pre != nil { + pre(n) + } + for c := n.FirstChild; c != nil; c = c.NextSibling { + forEachNode(c, pre, post) + } + if post != nil { + post(n) + } +} + +//!+ +func title(url string) error { + resp, err := http.Get(url) + if err != nil { + return err + } + defer resp.Body.Close() + + ct := resp.Header.Get("Content-Type") + if ct != "text/html" && !strings.HasPrefix(ct, "text/html;") { + return fmt.Errorf("%s has type %s, not text/html", url, ct) + } + + doc, err := html.Parse(resp.Body) + if err != nil { + return fmt.Errorf("parsing %s as HTML: %v", url, err) + } + + // ...print doc's title element... + //!- + visitNode := func(n *html.Node) { + if n.Type == html.ElementNode && n.Data == "title" && + n.FirstChild != nil { + fmt.Println(n.FirstChild.Data) + } + } + forEachNode(doc, visitNode, nil) + //!+ + + return nil +} + +//!- + +func main() { + for _, arg := range os.Args[1:] { + if err := title(arg); err != nil { + fmt.Fprintf(os.Stderr, "title: %v\n", err) + } + } +} diff --git a/vendor/gopl.io/ch5/title3/title.go b/vendor/gopl.io/ch5/title3/title.go new file mode 100644 index 0000000..727b209 --- /dev/null +++ b/vendor/gopl.io/ch5/title3/title.go @@ -0,0 +1,99 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 153. + +// Title3 prints the title of an HTML document specified by a URL. +package main + +import ( + "fmt" + "net/http" + "os" + "strings" + + "golang.org/x/net/html" +) + +// Copied from gopl.io/ch5/outline2. +func forEachNode(n *html.Node, pre, post func(n *html.Node)) { + if pre != nil { + pre(n) + } + for c := n.FirstChild; c != nil; c = c.NextSibling { + forEachNode(c, pre, post) + } + if post != nil { + post(n) + } +} + +//!+ +// soleTitle returns the text of the first non-empty title element +// in doc, and an error if there was not exactly one. +func soleTitle(doc *html.Node) (title string, err error) { + type bailout struct{} + + defer func() { + switch p := recover(); p { + case nil: + // no panic + case bailout{}: + // "expected" panic + err = fmt.Errorf("multiple title elements") + default: + panic(p) // unexpected panic; carry on panicking + } + }() + + // Bail out of recursion if we find more than one non-empty title. + forEachNode(doc, func(n *html.Node) { + if n.Type == html.ElementNode && n.Data == "title" && + n.FirstChild != nil { + if title != "" { + panic(bailout{}) // multiple title elements + } + title = n.FirstChild.Data + } + }, nil) + if title == "" { + return "", fmt.Errorf("no title element") + } + return title, nil +} + +//!- + +func title(url string) error { + resp, err := http.Get(url) + if err != nil { + return err + } + + // Check Content-Type is HTML (e.g., "text/html; charset=utf-8"). + ct := resp.Header.Get("Content-Type") + if ct != "text/html" && !strings.HasPrefix(ct, "text/html;") { + resp.Body.Close() + return fmt.Errorf("%s has type %s, not text/html", url, ct) + } + + doc, err := html.Parse(resp.Body) + resp.Body.Close() + if err != nil { + return fmt.Errorf("parsing %s as HTML: %v", url, err) + } + title, err := soleTitle(doc) + if err != nil { + return err + } + fmt.Println(title) + return nil +} + +func main() { + for _, arg := range os.Args[1:] { + if err := title(arg); err != nil { + fmt.Fprintf(os.Stderr, "title: %v\n", err) + } + } +} diff --git a/vendor/gopl.io/ch5/toposort/main.go b/vendor/gopl.io/ch5/toposort/main.go new file mode 100644 index 0000000..1c5e899 --- /dev/null +++ b/vendor/gopl.io/ch5/toposort/main.go @@ -0,0 +1,69 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 136. + +// The toposort program prints the nodes of a DAG in topological order. +package main + +import ( + "fmt" + "sort" +) + +//!+table +// prereqs maps computer science courses to their prerequisites. +var prereqs = map[string][]string{ + "algorithms": {"data structures"}, + "calculus": {"linear algebra"}, + + "compilers": { + "data structures", + "formal languages", + "computer organization", + }, + + "data structures": {"discrete math"}, + "databases": {"data structures"}, + "discrete math": {"intro to programming"}, + "formal languages": {"discrete math"}, + "networks": {"operating systems"}, + "operating systems": {"data structures", "computer organization"}, + "programming languages": {"data structures", "computer organization"}, +} + +//!-table + +//!+main +func main() { + for i, course := range topoSort(prereqs) { + fmt.Printf("%d:\t%s\n", i+1, course) + } +} + +func topoSort(m map[string][]string) []string { + var order []string + seen := make(map[string]bool) + var visitAll func(items []string) + + visitAll = func(items []string) { + for _, item := range items { + if !seen[item] { + seen[item] = true + visitAll(m[item]) + order = append(order, item) + } + } + } + + var keys []string + for key := range m { + keys = append(keys, key) + } + + sort.Strings(keys) + visitAll(keys) + return order +} + +//!-main diff --git a/vendor/gopl.io/ch5/trace/main.go b/vendor/gopl.io/ch5/trace/main.go new file mode 100644 index 0000000..cc1962f --- /dev/null +++ b/vendor/gopl.io/ch5/trace/main.go @@ -0,0 +1,40 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 146. + +// The trace program uses defer to add entry/exit diagnostics to a function. +package main + +import ( + "log" + "time" +) + +//!+main +func bigSlowOperation() { + defer trace("bigSlowOperation")() // don't forget the extra parentheses + // ...lots of work... + time.Sleep(10 * time.Second) // simulate slow operation by sleeping +} + +func trace(msg string) func() { + start := time.Now() + log.Printf("enter %s", msg) + return func() { log.Printf("exit %s (%s)", msg, time.Since(start)) } +} + +//!-main + +func main() { + bigSlowOperation() +} + +/* +!+output +$ go build gopl.io/ch5/trace +$ ./trace +2015/11/18 09:53:26 enter bigSlowOperation +2015/11/18 09:53:36 exit bigSlowOperation (10.000589217s) +!-output +*/ diff --git a/vendor/gopl.io/ch5/wait/wait.go b/vendor/gopl.io/ch5/wait/wait.go new file mode 100644 index 0000000..f5313e7 --- /dev/null +++ b/vendor/gopl.io/ch5/wait/wait.go @@ -0,0 +1,50 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 130. + +// The wait program waits for an HTTP server to start responding. +package main + +import ( + "fmt" + "log" + "net/http" + "os" + "time" +) + +//!+ +// WaitForServer attempts to contact the server of a URL. +// It tries for one minute using exponential back-off. +// It reports an error if all attempts fail. +func WaitForServer(url string) error { + const timeout = 1 * time.Minute + deadline := time.Now().Add(timeout) + for tries := 0; time.Now().Before(deadline); tries++ { + _, err := http.Head(url) + if err == nil { + return nil // success + } + log.Printf("server not responding (%s); retrying...", err) + time.Sleep(time.Second << uint(tries)) // exponential back-off + } + return fmt.Errorf("server %s failed to respond after %s", url, timeout) +} + +//!- + +func main() { + if len(os.Args) != 2 { + fmt.Fprintf(os.Stderr, "usage: wait url\n") + os.Exit(1) + } + url := os.Args[1] + //!+main + // (In function main.) + if err := WaitForServer(url); err != nil { + fmt.Fprintf(os.Stderr, "Site is down: %v\n", err) + os.Exit(1) + } + //!-main +} diff --git a/vendor/gopl.io/ch6/coloredpoint/main.go b/vendor/gopl.io/ch6/coloredpoint/main.go new file mode 100644 index 0000000..4b2335c --- /dev/null +++ b/vendor/gopl.io/ch6/coloredpoint/main.go @@ -0,0 +1,89 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 161. + +// Coloredpoint demonstrates struct embedding. +package main + +import ( + "fmt" + "math" +) + +//!+decl +import "image/color" + +type Point struct{ X, Y float64 } + +type ColoredPoint struct { + Point + Color color.RGBA +} + +//!-decl + +func (p Point) Distance(q Point) float64 { + dX := q.X - p.X + dY := q.Y - p.Y + return math.Sqrt(dX*dX + dY*dY) +} + +func (p *Point) ScaleBy(factor float64) { + p.X *= factor + p.Y *= factor +} + +func main() { + //!+main + red := color.RGBA{255, 0, 0, 255} + blue := color.RGBA{0, 0, 255, 255} + var p = ColoredPoint{Point{1, 1}, red} + var q = ColoredPoint{Point{5, 4}, blue} + fmt.Println(p.Distance(q.Point)) // "5" + p.ScaleBy(2) + q.ScaleBy(2) + fmt.Println(p.Distance(q.Point)) // "10" + //!-main +} + +/* +//!+error + p.Distance(q) // compile error: cannot use q (ColoredPoint) as Point +//!-error +*/ + +func init() { + //!+methodexpr + p := Point{1, 2} + q := Point{4, 6} + + distance := Point.Distance // method expression + fmt.Println(distance(p, q)) // "5" + fmt.Printf("%T\n", distance) // "func(Point, Point) float64" + + scale := (*Point).ScaleBy + scale(&p, 2) + fmt.Println(p) // "{2 4}" + fmt.Printf("%T\n", scale) // "func(*Point, float64)" + //!-methodexpr +} + +func init() { + red := color.RGBA{255, 0, 0, 255} + blue := color.RGBA{0, 0, 255, 255} + + //!+indirect + type ColoredPoint struct { + *Point + Color color.RGBA + } + + p := ColoredPoint{&Point{1, 1}, red} + q := ColoredPoint{&Point{5, 4}, blue} + fmt.Println(p.Distance(*q.Point)) // "5" + q.Point = p.Point // p and q now share the same Point + p.ScaleBy(2) + fmt.Println(*p.Point, *q.Point) // "{2 2} {2 2}" + //!-indirect +} diff --git a/vendor/gopl.io/ch6/geometry/geometry.go b/vendor/gopl.io/ch6/geometry/geometry.go new file mode 100644 index 0000000..2400e3e --- /dev/null +++ b/vendor/gopl.io/ch6/geometry/geometry.go @@ -0,0 +1,42 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 156. + +// Package geometry defines simple types for plane geometry. +//!+point +package geometry + +import "math" + +type Point struct{ X, Y float64 } + +// traditional function +func Distance(p, q Point) float64 { + return math.Hypot(q.X-p.X, q.Y-p.Y) +} + +// same thing, but as a method of the Point type +func (p Point) Distance(q Point) float64 { + return math.Hypot(q.X-p.X, q.Y-p.Y) +} + +//!-point + +//!+path + +// A Path is a journey connecting the points with straight lines. +type Path []Point + +// Distance returns the distance traveled along the path. +func (path Path) Distance() float64 { + sum := 0.0 + for i := range path { + if i > 0 { + sum += path[i-1].Distance(path[i]) + } + } + return sum +} + +//!-path diff --git a/vendor/gopl.io/ch6/intset/intset.go b/vendor/gopl.io/ch6/intset/intset.go new file mode 100644 index 0000000..78227e5 --- /dev/null +++ b/vendor/gopl.io/ch6/intset/intset.go @@ -0,0 +1,73 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 165. + +// Package intset provides a set of integers based on a bit vector. +package intset + +import ( + "bytes" + "fmt" +) + +//!+intset + +// An IntSet is a set of small non-negative integers. +// Its zero value represents the empty set. +type IntSet struct { + words []uint64 +} + +// Has reports whether the set contains the non-negative value x. +func (s *IntSet) Has(x int) bool { + word, bit := x/64, uint(x%64) + return word < len(s.words) && s.words[word]&(1<= len(s.words) { + s.words = append(s.words, 0) + } + s.words[word] |= 1 << bit +} + +// UnionWith sets s to the union of s and t. +func (s *IntSet) UnionWith(t *IntSet) { + for i, tword := range t.words { + if i < len(s.words) { + s.words[i] |= tword + } else { + s.words = append(s.words, tword) + } + } +} + +//!-intset + +//!+string + +// String returns the set as a string of the form "{1 2 3}". +func (s *IntSet) String() string { + var buf bytes.Buffer + buf.WriteByte('{') + for i, word := range s.words { + if word == 0 { + continue + } + for j := 0; j < 64; j++ { + if word&(1< len("{") { + buf.WriteByte(' ') + } + fmt.Fprintf(&buf, "%d", 64*i+j) + } + } + } + buf.WriteByte('}') + return buf.String() +} + +//!-string diff --git a/vendor/gopl.io/ch6/intset/intset_test.go b/vendor/gopl.io/ch6/intset/intset_test.go new file mode 100644 index 0000000..620a39e --- /dev/null +++ b/vendor/gopl.io/ch6/intset/intset_test.go @@ -0,0 +1,50 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +package intset + +import "fmt" + +func Example_one() { + //!+main + var x, y IntSet + x.Add(1) + x.Add(144) + x.Add(9) + fmt.Println(x.String()) // "{1 9 144}" + + y.Add(9) + y.Add(42) + fmt.Println(y.String()) // "{9 42}" + + x.UnionWith(&y) + fmt.Println(x.String()) // "{1 9 42 144}" + + fmt.Println(x.Has(9), x.Has(123)) // "true false" + //!-main + + // Output: + // {1 9 144} + // {9 42} + // {1 9 42 144} + // true false +} + +func Example_two() { + var x IntSet + x.Add(1) + x.Add(144) + x.Add(9) + x.Add(42) + + //!+note + fmt.Println(&x) // "{1 9 42 144}" + fmt.Println(x.String()) // "{1 9 42 144}" + fmt.Println(x) // "{[4398046511618 0 65536]}" + //!-note + + // Output: + // {1 9 42 144} + // {1 9 42 144} + // {[4398046511618 0 65536]} +} diff --git a/vendor/gopl.io/ch6/urlvalues/main.go b/vendor/gopl.io/ch6/urlvalues/main.go new file mode 100644 index 0000000..7e226d5 --- /dev/null +++ b/vendor/gopl.io/ch6/urlvalues/main.go @@ -0,0 +1,53 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 160. + +// The urlvalues command demonstrates a map type with methods. +package main + +/* +//!+values +package url + +// Values maps a string key to a list of values. +type Values map[string][]string + +// Get returns the first value associated with the given key, +// or "" if there are none. +func (v Values) Get(key string) string { + if vs := v[key]; len(vs) > 0 { + return vs[0] + } + return "" +} + +// Add adds the value to key. +// It appends to any existing values associated with key. +func (v Values) Add(key, value string) { + v[key] = append(v[key], value) +} +//!-values +*/ + +import ( + "fmt" + "net/url" +) + +func main() { + //!+main + m := url.Values{"lang": {"en"}} // direct construction + m.Add("item", "1") + m.Add("item", "2") + + fmt.Println(m.Get("lang")) // "en" + fmt.Println(m.Get("q")) // "" + fmt.Println(m.Get("item")) // "1" (first value) + fmt.Println(m["item"]) // "[1 2]" (direct map access) + + m = nil + fmt.Println(m.Get("item")) // "" + m.Add("item", "3") // panic: assignment to entry in nil map + //!-main +} diff --git a/vendor/gopl.io/ch7/bytecounter/main.go b/vendor/gopl.io/ch7/bytecounter/main.go new file mode 100644 index 0000000..cb6e175 --- /dev/null +++ b/vendor/gopl.io/ch7/bytecounter/main.go @@ -0,0 +1,35 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 173. + +// Bytecounter demonstrates an implementation of io.Writer that counts bytes. +package main + +import ( + "fmt" +) + +//!+bytecounter + +type ByteCounter int + +func (c *ByteCounter) Write(p []byte) (int, error) { + *c += ByteCounter(len(p)) // convert int to ByteCounter + return len(p), nil +} + +//!-bytecounter + +func main() { + //!+main + var c ByteCounter + c.Write([]byte("hello")) + fmt.Println(c) // "5", = len("hello") + + c = 0 // reset the counter + var name = "Dolly" + fmt.Fprintf(&c, "hello, %s", name) + fmt.Println(c) // "12", = len("hello, Dolly") + //!-main +} diff --git a/vendor/gopl.io/ch7/eval/ast.go b/vendor/gopl.io/ch7/eval/ast.go new file mode 100644 index 0000000..4b6f381 --- /dev/null +++ b/vendor/gopl.io/ch7/eval/ast.go @@ -0,0 +1,40 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +package eval + +// An Expr is an arithmetic expression. +type Expr interface { + // Eval returns the value of this Expr in the environment env. + Eval(env Env) float64 + // Check reports errors in this Expr and adds its Vars to the set. + Check(vars map[Var]bool) error +} + +//!+ast + +// A Var identifies a variable, e.g., x. +type Var string + +// A literal is a numeric constant, e.g., 3.141. +type literal float64 + +// A unary represents a unary operator expression, e.g., -x. +type unary struct { + op rune // one of '+', '-' + x Expr +} + +// A binary represents a binary operator expression, e.g., x+y. +type binary struct { + op rune // one of '+', '-', '*', '/' + x, y Expr +} + +// A call represents a function call expression, e.g., sin(x). +type call struct { + fn string // one of "pow", "sin", "sqrt" + args []Expr +} + +//!-ast diff --git a/vendor/gopl.io/ch7/eval/check.go b/vendor/gopl.io/ch7/eval/check.go new file mode 100644 index 0000000..7fddcbe --- /dev/null +++ b/vendor/gopl.io/ch7/eval/check.go @@ -0,0 +1,58 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +package eval + +import ( + "fmt" + "strings" +) + +//!+Check + +func (v Var) Check(vars map[Var]bool) error { + vars[v] = true + return nil +} + +func (literal) Check(vars map[Var]bool) error { + return nil +} + +func (u unary) Check(vars map[Var]bool) error { + if !strings.ContainsRune("+-", u.op) { + return fmt.Errorf("unexpected unary op %q", u.op) + } + return u.x.Check(vars) +} + +func (b binary) Check(vars map[Var]bool) error { + if !strings.ContainsRune("+-*/", b.op) { + return fmt.Errorf("unexpected binary op %q", b.op) + } + if err := b.x.Check(vars); err != nil { + return err + } + return b.y.Check(vars) +} + +func (c call) Check(vars map[Var]bool) error { + arity, ok := numParams[c.fn] + if !ok { + return fmt.Errorf("unknown function %q", c.fn) + } + if len(c.args) != arity { + return fmt.Errorf("call to %s has %d args, want %d", + c.fn, len(c.args), arity) + } + for _, arg := range c.args { + if err := arg.Check(vars); err != nil { + return err + } + } + return nil +} + +var numParams = map[string]int{"pow": 2, "sin": 1, "sqrt": 1} + +//!-Check diff --git a/vendor/gopl.io/ch7/eval/coverage_test.go b/vendor/gopl.io/ch7/eval/coverage_test.go new file mode 100644 index 0000000..190dd37 --- /dev/null +++ b/vendor/gopl.io/ch7/eval/coverage_test.go @@ -0,0 +1,48 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +package eval + +import ( + "fmt" + "math" + "testing" +) + +//!+TestCoverage +func TestCoverage(t *testing.T) { + var tests = []struct { + input string + env Env + want string // expected error from Parse/Check or result from Eval + }{ + {"x % 2", nil, "unexpected '%'"}, + {"!true", nil, "unexpected '!'"}, + {"log(10)", nil, `unknown function "log"`}, + {"sqrt(1, 2)", nil, "call to sqrt has 2 args, want 1"}, + {"sqrt(A / pi)", Env{"A": 87616, "pi": math.Pi}, "167"}, + {"pow(x, 3) + pow(y, 3)", Env{"x": 9, "y": 10}, "1729"}, + {"5 / 9 * (F - 32)", Env{"F": -40}, "-40"}, + } + + for _, test := range tests { + expr, err := Parse(test.input) + if err == nil { + err = expr.Check(map[Var]bool{}) + } + if err != nil { + if err.Error() != test.want { + t.Errorf("%s: got %q, want %q", test.input, err, test.want) + } + continue + } + + got := fmt.Sprintf("%.6g", expr.Eval(test.env)) + if got != test.want { + t.Errorf("%s: %v => %s, want %s", + test.input, test.env, got, test.want) + } + } +} + +//!-TestCoverage diff --git a/vendor/gopl.io/ch7/eval/eval.go b/vendor/gopl.io/ch7/eval/eval.go new file mode 100644 index 0000000..4ab1dd7 --- /dev/null +++ b/vendor/gopl.io/ch7/eval/eval.go @@ -0,0 +1,70 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 198. + +// Package eval provides an expression evaluator. +package eval + +import ( + "fmt" + "math" +) + +//!+env + +type Env map[Var]float64 + +//!-env + +//!+Eval1 + +func (v Var) Eval(env Env) float64 { + return env[v] +} + +func (l literal) Eval(_ Env) float64 { + return float64(l) +} + +//!-Eval1 + +//!+Eval2 + +func (u unary) Eval(env Env) float64 { + switch u.op { + case '+': + return +u.x.Eval(env) + case '-': + return -u.x.Eval(env) + } + panic(fmt.Sprintf("unsupported unary operator: %q", u.op)) +} + +func (b binary) Eval(env Env) float64 { + switch b.op { + case '+': + return b.x.Eval(env) + b.y.Eval(env) + case '-': + return b.x.Eval(env) - b.y.Eval(env) + case '*': + return b.x.Eval(env) * b.y.Eval(env) + case '/': + return b.x.Eval(env) / b.y.Eval(env) + } + panic(fmt.Sprintf("unsupported binary operator: %q", b.op)) +} + +func (c call) Eval(env Env) float64 { + switch c.fn { + case "pow": + return math.Pow(c.args[0].Eval(env), c.args[1].Eval(env)) + case "sin": + return math.Sin(c.args[0].Eval(env)) + case "sqrt": + return math.Sqrt(c.args[0].Eval(env)) + } + panic(fmt.Sprintf("unsupported function call: %s", c.fn)) +} + +//!-Eval2 diff --git a/vendor/gopl.io/ch7/eval/eval_test.go b/vendor/gopl.io/ch7/eval/eval_test.go new file mode 100644 index 0000000..88905cd --- /dev/null +++ b/vendor/gopl.io/ch7/eval/eval_test.go @@ -0,0 +1,113 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +package eval + +import ( + "fmt" + "math" + "testing" +) + +//!+Eval +func TestEval(t *testing.T) { + tests := []struct { + expr string + env Env + want string + }{ + {"sqrt(A / pi)", Env{"A": 87616, "pi": math.Pi}, "167"}, + {"pow(x, 3) + pow(y, 3)", Env{"x": 12, "y": 1}, "1729"}, + {"pow(x, 3) + pow(y, 3)", Env{"x": 9, "y": 10}, "1729"}, + {"5 / 9 * (F - 32)", Env{"F": -40}, "-40"}, + {"5 / 9 * (F - 32)", Env{"F": 32}, "0"}, + {"5 / 9 * (F - 32)", Env{"F": 212}, "100"}, + //!-Eval + // additional tests that don't appear in the book + {"-1 + -x", Env{"x": 1}, "-2"}, + {"-1 - x", Env{"x": 1}, "-2"}, + //!+Eval + } + var prevExpr string + for _, test := range tests { + // Print expr only when it changes. + if test.expr != prevExpr { + fmt.Printf("\n%s\n", test.expr) + prevExpr = test.expr + } + expr, err := Parse(test.expr) + if err != nil { + t.Error(err) // parse error + continue + } + got := fmt.Sprintf("%.6g", expr.Eval(test.env)) + fmt.Printf("\t%v => %s\n", test.env, got) + if got != test.want { + t.Errorf("%s.Eval() in %v = %q, want %q\n", + test.expr, test.env, got, test.want) + } + } +} + +//!-Eval + +/* +//!+output +sqrt(A / pi) + map[A:87616 pi:3.141592653589793] => 167 + +pow(x, 3) + pow(y, 3) + map[x:12 y:1] => 1729 + map[x:9 y:10] => 1729 + +5 / 9 * (F - 32) + map[F:-40] => -40 + map[F:32] => 0 + map[F:212] => 100 +//!-output + +// Additional outputs that don't appear in the book. + +-1 - x + map[x:1] => -2 + +-1 + -x + map[x:1] => -2 +*/ + +func TestErrors(t *testing.T) { + for _, test := range []struct{ expr, wantErr string }{ + {"x % 2", "unexpected '%'"}, + {"math.Pi", "unexpected '.'"}, + {"!true", "unexpected '!'"}, + {`"hello"`, "unexpected '\"'"}, + {"log(10)", `unknown function "log"`}, + {"sqrt(1, 2)", "call to sqrt has 2 args, want 1"}, + } { + expr, err := Parse(test.expr) + if err == nil { + vars := make(map[Var]bool) + err = expr.Check(vars) + if err == nil { + t.Errorf("unexpected success: %s", test.expr) + continue + } + } + fmt.Printf("%-20s%v\n", test.expr, err) // (for book) + if err.Error() != test.wantErr { + t.Errorf("got error %s, want %s", err, test.wantErr) + } + } +} + +/* +//!+errors +x % 2 unexpected '%' +math.Pi unexpected '.' +!true unexpected '!' +"hello" unexpected '"' + +log(10) unknown function "log" +sqrt(1, 2) call to sqrt has 2 args, want 1 +//!-errors +*/ diff --git a/vendor/gopl.io/ch7/eval/parse.go b/vendor/gopl.io/ch7/eval/parse.go new file mode 100644 index 0000000..677e9a0 --- /dev/null +++ b/vendor/gopl.io/ch7/eval/parse.go @@ -0,0 +1,160 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +package eval + +import ( + "fmt" + "strconv" + "strings" + "text/scanner" +) + +// ---- lexer ---- + +// This lexer is similar to the one described in Chapter 13. +type lexer struct { + scan scanner.Scanner + token rune // current lookahead token +} + +func (lex *lexer) next() { lex.token = lex.scan.Scan() } +func (lex *lexer) text() string { return lex.scan.TokenText() } + +type lexPanic string + +// describe returns a string describing the current token, for use in errors. +func (lex *lexer) describe() string { + switch lex.token { + case scanner.EOF: + return "end of file" + case scanner.Ident: + return fmt.Sprintf("identifier %s", lex.text()) + case scanner.Int, scanner.Float: + return fmt.Sprintf("number %s", lex.text()) + } + return fmt.Sprintf("%q", rune(lex.token)) // any other rune +} + +func precedence(op rune) int { + switch op { + case '*', '/': + return 2 + case '+', '-': + return 1 + } + return 0 +} + +// ---- parser ---- + +// Parse parses the input string as an arithmetic expression. +// +// expr = num a literal number, e.g., 3.14159 +// | id a variable name, e.g., x +// | id '(' expr ',' ... ')' a function call +// | '-' expr a unary operator (+-) +// | expr '+' expr a binary operator (+-*/) +// +func Parse(input string) (_ Expr, err error) { + defer func() { + switch x := recover().(type) { + case nil: + // no panic + case lexPanic: + err = fmt.Errorf("%s", x) + default: + // unexpected panic: resume state of panic. + panic(x) + } + }() + lex := new(lexer) + lex.scan.Init(strings.NewReader(input)) + lex.scan.Mode = scanner.ScanIdents | scanner.ScanInts | scanner.ScanFloats + lex.next() // initial lookahead + e := parseExpr(lex) + if lex.token != scanner.EOF { + return nil, fmt.Errorf("unexpected %s", lex.describe()) + } + return e, nil +} + +func parseExpr(lex *lexer) Expr { return parseBinary(lex, 1) } + +// binary = unary ('+' binary)* +// parseBinary stops when it encounters an +// operator of lower precedence than prec1. +func parseBinary(lex *lexer, prec1 int) Expr { + lhs := parseUnary(lex) + for prec := precedence(lex.token); prec >= prec1; prec-- { + for precedence(lex.token) == prec { + op := lex.token + lex.next() // consume operator + rhs := parseBinary(lex, prec+1) + lhs = binary{op, lhs, rhs} + } + } + return lhs +} + +// unary = '+' expr | primary +func parseUnary(lex *lexer) Expr { + if lex.token == '+' || lex.token == '-' { + op := lex.token + lex.next() // consume '+' or '-' + return unary{op, parseUnary(lex)} + } + return parsePrimary(lex) +} + +// primary = id +// | id '(' expr ',' ... ',' expr ')' +// | num +// | '(' expr ')' +func parsePrimary(lex *lexer) Expr { + switch lex.token { + case scanner.Ident: + id := lex.text() + lex.next() // consume Ident + if lex.token != '(' { + return Var(id) + } + lex.next() // consume '(' + var args []Expr + if lex.token != ')' { + for { + args = append(args, parseExpr(lex)) + if lex.token != ',' { + break + } + lex.next() // consume ',' + } + if lex.token != ')' { + msg := fmt.Sprintf("got %q, want ')'", lex.token) + panic(lexPanic(msg)) + } + } + lex.next() // consume ')' + return call{id, args} + + case scanner.Int, scanner.Float: + f, err := strconv.ParseFloat(lex.text(), 64) + if err != nil { + panic(lexPanic(err.Error())) + } + lex.next() // consume number + return literal(f) + + case '(': + lex.next() // consume ')' + e := parseExpr(lex) + if lex.token != ')' { + msg := fmt.Sprintf("got %s, want ')'", lex.describe()) + panic(lexPanic(msg)) + } + lex.next() // consume ')' + return e + } + msg := fmt.Sprintf("unexpected %s", lex.describe()) + panic(lexPanic(msg)) +} diff --git a/vendor/gopl.io/ch7/eval/print.go b/vendor/gopl.io/ch7/eval/print.go new file mode 100644 index 0000000..69bf2db --- /dev/null +++ b/vendor/gopl.io/ch7/eval/print.go @@ -0,0 +1,52 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +package eval + +import ( + "bytes" + "fmt" +) + +// Format formats an expression as a string. +// It does not attempt to remove unnecessary parens. +func Format(e Expr) string { + var buf bytes.Buffer + write(&buf, e) + return buf.String() +} + +func write(buf *bytes.Buffer, e Expr) { + switch e := e.(type) { + case literal: + fmt.Fprintf(buf, "%g", e) + + case Var: + fmt.Fprintf(buf, "%s", e) + + case unary: + fmt.Fprintf(buf, "(%c", e.op) + write(buf, e.x) + buf.WriteByte(')') + + case binary: + buf.WriteByte('(') + write(buf, e.x) + fmt.Fprintf(buf, " %c ", e.op) + write(buf, e.y) + buf.WriteByte(')') + + case call: + fmt.Fprintf(buf, "%s(", e.fn) + for i, arg := range e.args { + if i > 0 { + buf.WriteString(", ") + } + write(buf, arg) + } + buf.WriteByte(')') + + default: + panic(fmt.Sprintf("unknown Expr: %T", e)) + } +} diff --git a/vendor/gopl.io/ch7/http1/main.go b/vendor/gopl.io/ch7/http1/main.go new file mode 100644 index 0000000..9e7b664 --- /dev/null +++ b/vendor/gopl.io/ch7/http1/main.go @@ -0,0 +1,46 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 191. + +// Http1 is a rudimentary e-commerce server. +package main + +import ( + "fmt" + "log" + "net/http" +) + +//!+main + +func main() { + db := database{"shoes": 50, "socks": 5} + log.Fatal(http.ListenAndServe("localhost:8000", db)) +} + +type dollars float32 + +func (d dollars) String() string { return fmt.Sprintf("$%.2f", d) } + +type database map[string]dollars + +func (db database) ServeHTTP(w http.ResponseWriter, req *http.Request) { + for item, price := range db { + fmt.Fprintf(w, "%s: %s\n", item, price) + } +} + +//!-main + +/* +//!+handler +package http + +type Handler interface { + ServeHTTP(w ResponseWriter, r *Request) +} + +func ListenAndServe(address string, h Handler) error +//!-handler +*/ diff --git a/vendor/gopl.io/ch7/http2/main.go b/vendor/gopl.io/ch7/http2/main.go new file mode 100644 index 0000000..78ba8bc --- /dev/null +++ b/vendor/gopl.io/ch7/http2/main.go @@ -0,0 +1,48 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 192. + +// Http2 is an e-commerce server with /list and /price endpoints. +package main + +import ( + "fmt" + "log" + "net/http" +) + +func main() { + db := database{"shoes": 50, "socks": 5} + log.Fatal(http.ListenAndServe("localhost:8000", db)) +} + +type dollars float32 + +func (d dollars) String() string { return fmt.Sprintf("$%.2f", d) } + +type database map[string]dollars + +//!+handler +func (db database) ServeHTTP(w http.ResponseWriter, req *http.Request) { + switch req.URL.Path { + case "/list": + for item, price := range db { + fmt.Fprintf(w, "%s: %s\n", item, price) + } + case "/price": + item := req.URL.Query().Get("item") + price, ok := db[item] + if !ok { + w.WriteHeader(http.StatusNotFound) // 404 + fmt.Fprintf(w, "no such item: %q\n", item) + return + } + fmt.Fprintf(w, "%s\n", price) + default: + w.WriteHeader(http.StatusNotFound) // 404 + fmt.Fprintf(w, "no such page: %s\n", req.URL) + } +} + +//!-handler diff --git a/vendor/gopl.io/ch7/http3/main.go b/vendor/gopl.io/ch7/http3/main.go new file mode 100644 index 0000000..816e2a2 --- /dev/null +++ b/vendor/gopl.io/ch7/http3/main.go @@ -0,0 +1,61 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 194. + +// Http3 is an e-commerce server that registers the /list and /price +// endpoints by calling (*http.ServeMux).Handle. +package main + +import ( + "fmt" + "log" + "net/http" +) + +type dollars float32 + +func (d dollars) String() string { return fmt.Sprintf("$%.2f", d) } + +//!+main + +func main() { + db := database{"shoes": 50, "socks": 5} + mux := http.NewServeMux() + mux.Handle("/list", http.HandlerFunc(db.list)) + mux.Handle("/price", http.HandlerFunc(db.price)) + log.Fatal(http.ListenAndServe("localhost:8000", mux)) +} + +type database map[string]dollars + +func (db database) list(w http.ResponseWriter, req *http.Request) { + for item, price := range db { + fmt.Fprintf(w, "%s: %s\n", item, price) + } +} + +func (db database) price(w http.ResponseWriter, req *http.Request) { + item := req.URL.Query().Get("item") + price, ok := db[item] + if !ok { + w.WriteHeader(http.StatusNotFound) // 404 + fmt.Fprintf(w, "no such item: %q\n", item) + return + } + fmt.Fprintf(w, "%s\n", price) +} + +//!-main + +/* +//!+handlerfunc +package http + +type HandlerFunc func(w ResponseWriter, r *Request) + +func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) { + f(w, r) +} +//!-handlerfunc +*/ diff --git a/vendor/gopl.io/ch7/http3a/main.go b/vendor/gopl.io/ch7/http3a/main.go new file mode 100644 index 0000000..b682466 --- /dev/null +++ b/vendor/gopl.io/ch7/http3a/main.go @@ -0,0 +1,54 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 195. + +// Http3a is an e-commerce server that registers the /list and /price +// endpoints by calling (*http.ServeMux).HandleFunc. +package main + +import ( + "fmt" + "log" + "net/http" +) + +func main() { + db := database{"shoes": 50, "socks": 5} + mux := http.NewServeMux() + //!+main + mux.HandleFunc("/list", db.list) + mux.HandleFunc("/price", db.price) + //!-main + log.Fatal(http.ListenAndServe("localhost:8000", mux)) +} + +type database map[string]int + +func (db database) list(w http.ResponseWriter, req *http.Request) { + for item, price := range db { + fmt.Fprintf(w, "%s: $%d\n", item, price) + } +} + +func (db database) price(w http.ResponseWriter, req *http.Request) { + item := req.URL.Query().Get("item") + if price, ok := db[item]; ok { + fmt.Fprintf(w, "$%d\n", price) + } else { + w.WriteHeader(http.StatusNotFound) // 404 + fmt.Fprintf(w, "no such item: %q\n", item) + } +} + +/* +//!+handlerfunc +package http + +type HandlerFunc func(w ResponseWriter, r *Request) + +func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) { + f(w, r) +} +//!-handlerfunc +*/ diff --git a/vendor/gopl.io/ch7/http4/main.go b/vendor/gopl.io/ch7/http4/main.go new file mode 100644 index 0000000..b207d7a --- /dev/null +++ b/vendor/gopl.io/ch7/http4/main.go @@ -0,0 +1,47 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 195. + +// Http4 is an e-commerce server that registers the /list and /price +// endpoint by calling http.HandleFunc. +package main + +import ( + "fmt" + "log" + "net/http" +) + +//!+main + +func main() { + db := database{"shoes": 50, "socks": 5} + http.HandleFunc("/list", db.list) + http.HandleFunc("/price", db.price) + log.Fatal(http.ListenAndServe("localhost:8000", nil)) +} + +//!-main + +type dollars float32 + +func (d dollars) String() string { return fmt.Sprintf("$%.2f", d) } + +type database map[string]dollars + +func (db database) list(w http.ResponseWriter, req *http.Request) { + for item, price := range db { + fmt.Fprintf(w, "%s: %s\n", item, price) + } +} + +func (db database) price(w http.ResponseWriter, req *http.Request) { + item := req.URL.Query().Get("item") + if price, ok := db[item]; ok { + fmt.Fprintf(w, "%s\n", price) + } else { + w.WriteHeader(http.StatusNotFound) // 404 + fmt.Fprintf(w, "no such item: %q\n", item) + } +} diff --git a/vendor/gopl.io/ch7/sleep/sleep.go b/vendor/gopl.io/ch7/sleep/sleep.go new file mode 100644 index 0000000..04f247e --- /dev/null +++ b/vendor/gopl.io/ch7/sleep/sleep.go @@ -0,0 +1,25 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 179. + +// The sleep program sleeps for a specified period of time. +package main + +import ( + "flag" + "fmt" + "time" +) + +//!+sleep +var period = flag.Duration("period", 1*time.Second, "sleep period") + +func main() { + flag.Parse() + fmt.Printf("Sleeping for %v...", *period) + time.Sleep(*period) + fmt.Println() +} + +//!-sleep diff --git a/vendor/gopl.io/ch7/sorting/main.go b/vendor/gopl.io/ch7/sorting/main.go new file mode 100644 index 0000000..be598d9 --- /dev/null +++ b/vendor/gopl.io/ch7/sorting/main.go @@ -0,0 +1,167 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 187. + +// Sorting sorts a music playlist into a variety of orders. +package main + +import ( + "fmt" + "os" + "sort" + "text/tabwriter" + "time" +) + +//!+main +type Track struct { + Title string + Artist string + Album string + Year int + Length time.Duration +} + +var tracks = []*Track{ + {"Go", "Delilah", "From the Roots Up", 2012, length("3m38s")}, + {"Go", "Moby", "Moby", 1992, length("3m37s")}, + {"Go Ahead", "Alicia Keys", "As I Am", 2007, length("4m36s")}, + {"Ready 2 Go", "Martin Solveig", "Smash", 2011, length("4m24s")}, +} + +func length(s string) time.Duration { + d, err := time.ParseDuration(s) + if err != nil { + panic(s) + } + return d +} + +//!-main + +//!+printTracks +func printTracks(tracks []*Track) { + const format = "%v\t%v\t%v\t%v\t%v\t\n" + tw := new(tabwriter.Writer).Init(os.Stdout, 0, 8, 2, ' ', 0) + fmt.Fprintf(tw, format, "Title", "Artist", "Album", "Year", "Length") + fmt.Fprintf(tw, format, "-----", "------", "-----", "----", "------") + for _, t := range tracks { + fmt.Fprintf(tw, format, t.Title, t.Artist, t.Album, t.Year, t.Length) + } + tw.Flush() // calculate column widths and print table +} + +//!-printTracks + +//!+artistcode +type byArtist []*Track + +func (x byArtist) Len() int { return len(x) } +func (x byArtist) Less(i, j int) bool { return x[i].Artist < x[j].Artist } +func (x byArtist) Swap(i, j int) { x[i], x[j] = x[j], x[i] } + +//!-artistcode + +//!+yearcode +type byYear []*Track + +func (x byYear) Len() int { return len(x) } +func (x byYear) Less(i, j int) bool { return x[i].Year < x[j].Year } +func (x byYear) Swap(i, j int) { x[i], x[j] = x[j], x[i] } + +//!-yearcode + +func main() { + fmt.Println("byArtist:") + sort.Sort(byArtist(tracks)) + printTracks(tracks) + + fmt.Println("\nReverse(byArtist):") + sort.Sort(sort.Reverse(byArtist(tracks))) + printTracks(tracks) + + fmt.Println("\nbyYear:") + sort.Sort(byYear(tracks)) + printTracks(tracks) + + fmt.Println("\nCustom:") + //!+customcall + sort.Sort(customSort{tracks, func(x, y *Track) bool { + if x.Title != y.Title { + return x.Title < y.Title + } + if x.Year != y.Year { + return x.Year < y.Year + } + if x.Length != y.Length { + return x.Length < y.Length + } + return false + }}) + //!-customcall + printTracks(tracks) +} + +/* +//!+artistoutput +Title Artist Album Year Length +----- ------ ----- ---- ------ +Go Ahead Alicia Keys As I Am 2007 4m36s +Go Delilah From the Roots Up 2012 3m38s +Ready 2 Go Martin Solveig Smash 2011 4m24s +Go Moby Moby 1992 3m37s +//!-artistoutput + +//!+artistrevoutput +Title Artist Album Year Length +----- ------ ----- ---- ------ +Go Moby Moby 1992 3m37s +Ready 2 Go Martin Solveig Smash 2011 4m24s +Go Delilah From the Roots Up 2012 3m38s +Go Ahead Alicia Keys As I Am 2007 4m36s +//!-artistrevoutput + +//!+yearoutput +Title Artist Album Year Length +----- ------ ----- ---- ------ +Go Moby Moby 1992 3m37s +Go Ahead Alicia Keys As I Am 2007 4m36s +Ready 2 Go Martin Solveig Smash 2011 4m24s +Go Delilah From the Roots Up 2012 3m38s +//!-yearoutput + +//!+customout +Title Artist Album Year Length +----- ------ ----- ---- ------ +Go Moby Moby 1992 3m37s +Go Delilah From the Roots Up 2012 3m38s +Go Ahead Alicia Keys As I Am 2007 4m36s +Ready 2 Go Martin Solveig Smash 2011 4m24s +//!-customout +*/ + +//!+customcode +type customSort struct { + t []*Track + less func(x, y *Track) bool +} + +func (x customSort) Len() int { return len(x.t) } +func (x customSort) Less(i, j int) bool { return x.less(x.t[i], x.t[j]) } +func (x customSort) Swap(i, j int) { x.t[i], x.t[j] = x.t[j], x.t[i] } + +//!-customcode + +func init() { + //!+ints + values := []int{3, 1, 4, 1} + fmt.Println(sort.IntsAreSorted(values)) // "false" + sort.Ints(values) + fmt.Println(values) // "[1 1 3 4]" + fmt.Println(sort.IntsAreSorted(values)) // "true" + sort.Sort(sort.Reverse(sort.IntSlice(values))) + fmt.Println(values) // "[4 3 1 1]" + fmt.Println(sort.IntsAreSorted(values)) // "false" + //!-ints +} diff --git a/vendor/gopl.io/ch7/surface/surface.go b/vendor/gopl.io/ch7/surface/surface.go new file mode 100644 index 0000000..ed768ba --- /dev/null +++ b/vendor/gopl.io/ch7/surface/surface.go @@ -0,0 +1,112 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 203. + +// The surface program plots the 3-D surface of a user-provided function. +package main + +import ( + "fmt" + "io" + "log" + "math" + "net/http" +) + +//!+parseAndCheck +import "gopl.io/ch7/eval" + +//!-parseAndCheck + +// -- copied from gopl.io/ch3/surface -- + +const ( + width, height = 600, 320 // canvas size in pixels + cells = 100 // number of grid cells + xyrange = 30.0 // x, y axis range (-xyrange..+xyrange) + xyscale = width / 2 / xyrange // pixels per x or y unit + zscale = height * 0.4 // pixels per z unit +) + +var sin30, cos30 = 0.5, math.Sqrt(3.0 / 4.0) // sin(30°), cos(30°) + +func corner(f func(x, y float64) float64, i, j int) (float64, float64) { + // find point (x,y) at corner of cell (i,j) + x := xyrange * (float64(i)/cells - 0.5) + y := xyrange * (float64(j)/cells - 0.5) + + z := f(x, y) // compute surface height z + + // project (x,y,z) isometrically onto 2-D SVG canvas (sx,sy) + sx := width/2 + (x-y)*cos30*xyscale + sy := height/2 + (x+y)*sin30*xyscale - z*zscale + return sx, sy +} + +func surface(w io.Writer, f func(x, y float64) float64) { + fmt.Fprintf(w, "", width, height) + for i := 0; i < cells; i++ { + for j := 0; j < cells; j++ { + ax, ay := corner(f, i+1, j) + bx, by := corner(f, i, j) + cx, cy := corner(f, i, j+1) + dx, dy := corner(f, i+1, j+1) + fmt.Fprintf(w, "\n", + ax, ay, bx, by, cx, cy, dx, dy) + } + } + fmt.Fprintln(w, "") +} + +// -- main code for gopl.io/ch7/surface -- + +//!+parseAndCheck +func parseAndCheck(s string) (eval.Expr, error) { + if s == "" { + return nil, fmt.Errorf("empty expression") + } + expr, err := eval.Parse(s) + if err != nil { + return nil, err + } + vars := make(map[eval.Var]bool) + if err := expr.Check(vars); err != nil { + return nil, err + } + for v := range vars { + if v != "x" && v != "y" && v != "r" { + return nil, fmt.Errorf("undefined variable: %s", v) + } + } + return expr, nil +} + +//!-parseAndCheck + +//!+plot +func plot(w http.ResponseWriter, r *http.Request) { + r.ParseForm() + expr, err := parseAndCheck(r.Form.Get("expr")) + if err != nil { + http.Error(w, "bad expr: "+err.Error(), http.StatusBadRequest) + return + } + w.Header().Set("Content-Type", "image/svg+xml") + surface(w, func(x, y float64) float64 { + r := math.Hypot(x, y) // distance from (0,0) + return expr.Eval(eval.Env{"x": x, "y": y, "r": r}) + }) +} + +//!-plot + +//!+main +func main() { + http.HandleFunc("/plot", plot) + log.Fatal(http.ListenAndServe("localhost:8000", nil)) +} + +//!-main diff --git a/vendor/gopl.io/ch7/tempconv/tempconv.go b/vendor/gopl.io/ch7/tempconv/tempconv.go new file mode 100644 index 0000000..6c3ef41 --- /dev/null +++ b/vendor/gopl.io/ch7/tempconv/tempconv.go @@ -0,0 +1,66 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 180. + +// Package tempconv performs Celsius and Fahrenheit temperature computations. +package tempconv + +import ( + "flag" + "fmt" +) + +type Celsius float64 +type Fahrenheit float64 + +func CToF(c Celsius) Fahrenheit { return Fahrenheit(c*9.0/5.0 + 32.0) } +func FToC(f Fahrenheit) Celsius { return Celsius((f - 32.0) * 5.0 / 9.0) } + +func (c Celsius) String() string { return fmt.Sprintf("%g°C", c) } + +/* +//!+flagvalue +package flag + +// Value is the interface to the value stored in a flag. +type Value interface { + String() string + Set(string) error +} +//!-flagvalue +*/ + +//!+celsiusFlag +// *celsiusFlag satisfies the flag.Value interface. +type celsiusFlag struct{ Celsius } + +func (f *celsiusFlag) Set(s string) error { + var unit string + var value float64 + fmt.Sscanf(s, "%f%s", &value, &unit) // no error check needed + switch unit { + case "C", "°C": + f.Celsius = Celsius(value) + return nil + case "F", "°F": + f.Celsius = FToC(Fahrenheit(value)) + return nil + } + return fmt.Errorf("invalid temperature %q", s) +} + +//!-celsiusFlag + +//!+CelsiusFlag + +// CelsiusFlag defines a Celsius flag with the specified name, +// default value, and usage, and returns the address of the flag variable. +// The flag argument must have a quantity and a unit, e.g., "100C". +func CelsiusFlag(name string, value Celsius, usage string) *Celsius { + f := celsiusFlag{value} + flag.CommandLine.Var(&f, name, usage) + return &f.Celsius +} + +//!-CelsiusFlag diff --git a/vendor/gopl.io/ch7/tempconv/tempconv.go.~master~ b/vendor/gopl.io/ch7/tempconv/tempconv.go.~master~ new file mode 100644 index 0000000..5ee2de8 --- /dev/null +++ b/vendor/gopl.io/ch7/tempconv/tempconv.go.~master~ @@ -0,0 +1,64 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + + +// Package tempconv performs Celsius and Fahrenheit temperature computations. +package tempconv + +import ( + "flag" + "fmt" +) + +type Celsius float64 +type Fahrenheit float64 + +func CToF(c Celsius) Fahrenheit { return Fahrenheit(c*9.0/5.0 + 32.0) } +func FToC(f Fahrenheit) Celsius { return Celsius((f - 32.0) * 5.0 / 9.0) } + +func (c Celsius) String() string { return fmt.Sprintf("%g°C", c) } + +/* +//!+flagvalue +package flag + +// Value is the interface to the value stored in a flag. +type Value interface { + String() string + Set(string) error +} +//!-flagvalue +*/ + +//!+celsiusflag +// *celsiusFlag satisfies the flag.Value interface. +type celsiusFlag struct{ Celsius } + +func (f *celsiusFlag) Set(s string) error { + var unit string + var value float64 + fmt.Sscanf(s, "%f%s", &value, &unit) // no error check needed + switch unit { + case "C", "°C": + f.Celsius = Celsius(value) + return nil + case "F", "°F": + f.Celsius = FToC(Fahrenheit(value)) + return nil + } + return fmt.Errorf("invalid temperature %q", s) +} + +//!-celsiusflag + +//!+Celsiusflag +// CelsiusFlag defines a Celsius flag with the specified name, +// default value, and usage, and returns the address of the flag variable. +// The flag argument must have a quantity and a unit, e.g., "100C". +func CelsiusFlag(name string, value Celsius, usage string) *Celsius { + f := celsiusFlag{value} + flag.CommandLine.Var(&f, name, usage) + return &f.Celsius +} + +//!-Celsiusflag diff --git a/vendor/gopl.io/ch7/tempflag/tempflag.go b/vendor/gopl.io/ch7/tempflag/tempflag.go new file mode 100644 index 0000000..6f6a4e7 --- /dev/null +++ b/vendor/gopl.io/ch7/tempflag/tempflag.go @@ -0,0 +1,24 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 181. + +// Tempflag prints the value of its -temp (temperature) flag. +package main + +import ( + "flag" + "fmt" + + "gopl.io/ch7/tempconv" +) + +//!+ +var temp = tempconv.CelsiusFlag("temp", 20.0, "the temperature") + +func main() { + flag.Parse() + fmt.Println(*temp) +} + +//!- diff --git a/vendor/gopl.io/ch7/xmlselect/main.go b/vendor/gopl.io/ch7/xmlselect/main.go new file mode 100644 index 0000000..66cea65 --- /dev/null +++ b/vendor/gopl.io/ch7/xmlselect/main.go @@ -0,0 +1,56 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 214. +//!+ + +// Xmlselect prints the text of selected elements of an XML document. +package main + +import ( + "encoding/xml" + "fmt" + "io" + "os" + "strings" +) + +func main() { + dec := xml.NewDecoder(os.Stdin) + var stack []string // stack of element names + for { + tok, err := dec.Token() + if err == io.EOF { + break + } else if err != nil { + fmt.Fprintf(os.Stderr, "xmlselect: %v\n", err) + os.Exit(1) + } + switch tok := tok.(type) { + case xml.StartElement: + stack = append(stack, tok.Name.Local) // push + case xml.EndElement: + stack = stack[:len(stack)-1] // pop + case xml.CharData: + if containsAll(stack, os.Args[1:]) { + fmt.Printf("%s: %s\n", strings.Join(stack, " "), tok) + } + } + } +} + +// containsAll reports whether x contains the elements of y, in order. +func containsAll(x, y []string) bool { + for len(y) <= len(x) { + if len(y) == 0 { + return true + } + if x[0] == y[0] { + y = y[1:] + } + x = x[1:] + } + return false +} + +//!- diff --git a/vendor/gopl.io/ch8/cake/cake.go b/vendor/gopl.io/ch8/cake/cake.go new file mode 100644 index 0000000..8de0c3d --- /dev/null +++ b/vendor/gopl.io/ch8/cake/cake.go @@ -0,0 +1,89 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 234. + +// Package cake provides a simulation of +// a concurrent cake shop with numerous parameters. +// +// Use this command to run the benchmarks: +// $ go test -bench=. gopl.io/ch8/cake +package cake + +import ( + "fmt" + "math/rand" + "time" +) + +type Shop struct { + Verbose bool + Cakes int // number of cakes to bake + BakeTime time.Duration // time to bake one cake + BakeStdDev time.Duration // standard deviation of baking time + BakeBuf int // buffer slots between baking and icing + NumIcers int // number of cooks doing icing + IceTime time.Duration // time to ice one cake + IceStdDev time.Duration // standard deviation of icing time + IceBuf int // buffer slots between icing and inscribing + InscribeTime time.Duration // time to inscribe one cake + InscribeStdDev time.Duration // standard deviation of inscribing time +} + +type cake int + +func (s *Shop) baker(baked chan<- cake) { + for i := 0; i < s.Cakes; i++ { + c := cake(i) + if s.Verbose { + fmt.Println("baking", c) + } + work(s.BakeTime, s.BakeStdDev) + baked <- c + } + close(baked) +} + +func (s *Shop) icer(iced chan<- cake, baked <-chan cake) { + for c := range baked { + if s.Verbose { + fmt.Println("icing", c) + } + work(s.IceTime, s.IceStdDev) + iced <- c + } +} + +func (s *Shop) inscriber(iced <-chan cake) { + for i := 0; i < s.Cakes; i++ { + c := <-iced + if s.Verbose { + fmt.Println("inscribing", c) + } + work(s.InscribeTime, s.InscribeStdDev) + if s.Verbose { + fmt.Println("finished", c) + } + } +} + +// Work runs the simulation 'runs' times. +func (s *Shop) Work(runs int) { + for run := 0; run < runs; run++ { + baked := make(chan cake, s.BakeBuf) + iced := make(chan cake, s.IceBuf) + go s.baker(baked) + for i := 0; i < s.NumIcers; i++ { + go s.icer(iced, baked) + } + s.inscriber(iced) + } +} + +// work blocks the calling goroutine for a period of time +// that is normally distributed around d +// with a standard deviation of stddev. +func work(d, stddev time.Duration) { + delay := d + time.Duration(rand.NormFloat64()*float64(stddev)) + time.Sleep(delay) +} diff --git a/vendor/gopl.io/ch8/cake/cake_test.go b/vendor/gopl.io/ch8/cake/cake_test.go new file mode 100644 index 0000000..f8fea0e --- /dev/null +++ b/vendor/gopl.io/ch8/cake/cake_test.go @@ -0,0 +1,74 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +package cake_test + +import ( + "testing" + "time" + + "gopl.io/ch8/cake" +) + +var defaults = cake.Shop{ + Verbose: testing.Verbose(), + Cakes: 20, + BakeTime: 10 * time.Millisecond, + NumIcers: 1, + IceTime: 10 * time.Millisecond, + InscribeTime: 10 * time.Millisecond, +} + +func Benchmark(b *testing.B) { + // Baseline: one baker, one icer, one inscriber. + // Each step takes exactly 10ms. No buffers. + cakeshop := defaults + cakeshop.Work(b.N) // 224 ms +} + +func BenchmarkBuffers(b *testing.B) { + // Adding buffers has no effect. + cakeshop := defaults + cakeshop.BakeBuf = 10 + cakeshop.IceBuf = 10 + cakeshop.Work(b.N) // 224 ms +} + +func BenchmarkVariable(b *testing.B) { + // Adding variability to rate of each step + // increases total time due to channel delays. + cakeshop := defaults + cakeshop.BakeStdDev = cakeshop.BakeTime / 4 + cakeshop.IceStdDev = cakeshop.IceTime / 4 + cakeshop.InscribeStdDev = cakeshop.InscribeTime / 4 + cakeshop.Work(b.N) // 259 ms +} + +func BenchmarkVariableBuffers(b *testing.B) { + // Adding channel buffers reduces + // delays resulting from variability. + cakeshop := defaults + cakeshop.BakeStdDev = cakeshop.BakeTime / 4 + cakeshop.IceStdDev = cakeshop.IceTime / 4 + cakeshop.InscribeStdDev = cakeshop.InscribeTime / 4 + cakeshop.BakeBuf = 10 + cakeshop.IceBuf = 10 + cakeshop.Work(b.N) // 244 ms +} + +func BenchmarkSlowIcing(b *testing.B) { + // Making the middle stage slower + // adds directly to the critical path. + cakeshop := defaults + cakeshop.IceTime = 50 * time.Millisecond + cakeshop.Work(b.N) // 1.032 s +} + +func BenchmarkSlowIcingManyIcers(b *testing.B) { + // Adding more icing cooks reduces the cost of icing + // to its sequential component, following Amdahl's Law. + cakeshop := defaults + cakeshop.IceTime = 50 * time.Millisecond + cakeshop.NumIcers = 5 + cakeshop.Work(b.N) // 288ms +} diff --git a/vendor/gopl.io/ch8/chat/chat.go b/vendor/gopl.io/ch8/chat/chat.go new file mode 100644 index 0000000..990bcf3 --- /dev/null +++ b/vendor/gopl.io/ch8/chat/chat.go @@ -0,0 +1,96 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 254. +//!+ + +// Chat is a server that lets clients chat with each other. +package main + +import ( + "bufio" + "fmt" + "log" + "net" +) + +//!+broadcaster +type client chan<- string // an outgoing message channel + +var ( + entering = make(chan client) + leaving = make(chan client) + messages = make(chan string) // all incoming client messages +) + +func broadcaster() { + clients := make(map[client]bool) // all connected clients + for { + select { + case msg := <-messages: + // Broadcast incoming message to all + // clients' outgoing message channels. + for cli := range clients { + cli <- msg + } + + case cli := <-entering: + clients[cli] = true + + case cli := <-leaving: + delete(clients, cli) + close(cli) + } + } +} + +//!-broadcaster + +//!+handleConn +func handleConn(conn net.Conn) { + ch := make(chan string) // outgoing client messages + go clientWriter(conn, ch) + + who := conn.RemoteAddr().String() + ch <- "You are " + who + messages <- who + " has arrived" + entering <- ch + + input := bufio.NewScanner(conn) + for input.Scan() { + messages <- who + ": " + input.Text() + } + // NOTE: ignoring potential errors from input.Err() + + leaving <- ch + messages <- who + " has left" + conn.Close() +} + +func clientWriter(conn net.Conn, ch <-chan string) { + for msg := range ch { + fmt.Fprintln(conn, msg) // NOTE: ignoring network errors + } +} + +//!-handleConn + +//!+main +func main() { + listener, err := net.Listen("tcp", "localhost:8000") + if err != nil { + log.Fatal(err) + } + + go broadcaster() + for { + conn, err := listener.Accept() + if err != nil { + log.Print(err) + continue + } + go handleConn(conn) + } +} + +//!-main diff --git a/vendor/gopl.io/ch8/chat/chat.go.~master~ b/vendor/gopl.io/ch8/chat/chat.go.~master~ new file mode 100644 index 0000000..225af17 --- /dev/null +++ b/vendor/gopl.io/ch8/chat/chat.go.~master~ @@ -0,0 +1,89 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +//!+ + +// Chat is a server that lets clients chat with each other. +package main + +import ( + "bufio" + "fmt" + "log" + "net" +) + +//!+broadcaster +type client chan<- string // an outgoing message channel + +var ( + entering = make(chan client) + leaving = make(chan client) + messages = make(chan string) // all incoming client messages +) + +func broadcaster() { + clients := make(map[client]bool) // all connected clients + for { + select { + case msg := <-messages: + // Broadcast incoming message to all + // clients' outgoing message channels. + for cli := range clients { + cli <- msg + } + case cli := <-entering: + clients[cli] = true + case cli := <-leaving: + delete(clients, cli) + close(cli) + } + } +} + +//!-broadcaster + +//!+handleConn +func handleConn(conn net.Conn) { + ch := make(chan string) // outgoing client messages + go clientWriter(conn, ch) + + who := conn.RemoteAddr().String() + entering <- ch + messages <- who + " has arrived" + input := bufio.NewScanner(conn) + for input.Scan() { + messages <- who + ": " + input.Text() + } + messages <- who + " has left" + leaving <- ch + conn.Close() +} + +func clientWriter(conn net.Conn, ch <-chan string) { + for msg := range ch { + fmt.Fprintln(conn, msg) + } +} + +//!-handleConn + +//!+main +func main() { + listener, err := net.Listen("tcp", "localhost:8000") + if err != nil { + log.Fatal(err) + } + + go broadcaster() + for { + conn, err := listener.Accept() + if err != nil { + log.Print(err) + continue + } + go handleConn(conn) + } +} + +//!-main diff --git a/vendor/gopl.io/ch8/clock1/clock.go b/vendor/gopl.io/ch8/clock1/clock.go new file mode 100644 index 0000000..03fada1 --- /dev/null +++ b/vendor/gopl.io/ch8/clock1/clock.go @@ -0,0 +1,43 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 219. +//!+ + +// Clock1 is a TCP server that periodically writes the time. +package main + +import ( + "io" + "log" + "net" + "time" +) + +func main() { + listener, err := net.Listen("tcp", "localhost:8000") + if err != nil { + log.Fatal(err) + } + for { + conn, err := listener.Accept() + if err != nil { + log.Print(err) // e.g., connection aborted + continue + } + handleConn(conn) // handle one connection at a time + } +} + +func handleConn(c net.Conn) { + defer c.Close() + for { + _, err := io.WriteString(c, time.Now().Format("15:04:05\n")) + if err != nil { + return // e.g., client disconnected + } + time.Sleep(1 * time.Second) + } +} + +//!- diff --git a/vendor/gopl.io/ch8/clock2/clock.go b/vendor/gopl.io/ch8/clock2/clock.go new file mode 100644 index 0000000..55c6235 --- /dev/null +++ b/vendor/gopl.io/ch8/clock2/clock.go @@ -0,0 +1,42 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 222. + +// Clock is a TCP server that periodically writes the time. +package main + +import ( + "io" + "log" + "net" + "time" +) + +func handleConn(c net.Conn) { + defer c.Close() + for { + _, err := io.WriteString(c, time.Now().Format("15:04:05\n")) + if err != nil { + return // e.g., client disconnected + } + time.Sleep(1 * time.Second) + } +} + +func main() { + listener, err := net.Listen("tcp", "localhost:8000") + if err != nil { + log.Fatal(err) + } + //!+ + for { + conn, err := listener.Accept() + if err != nil { + log.Print(err) // e.g., connection aborted + continue + } + go handleConn(conn) // handle connections concurrently + } + //!- +} diff --git a/vendor/gopl.io/ch8/countdown1/countdown.go b/vendor/gopl.io/ch8/countdown1/countdown.go new file mode 100644 index 0000000..036ba6a --- /dev/null +++ b/vendor/gopl.io/ch8/countdown1/countdown.go @@ -0,0 +1,29 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 244. + +// Countdown implements the countdown for a rocket launch. +package main + +import ( + "fmt" + "time" +) + +//!+ +func main() { + fmt.Println("Commencing countdown.") + tick := time.Tick(1 * time.Second) + for countdown := 10; countdown > 0; countdown-- { + fmt.Println(countdown) + <-tick + } + launch() +} + +//!- + +func launch() { + fmt.Println("Lift off!") +} diff --git a/vendor/gopl.io/ch8/countdown2/countdown.go b/vendor/gopl.io/ch8/countdown2/countdown.go new file mode 100644 index 0000000..6ac6746 --- /dev/null +++ b/vendor/gopl.io/ch8/countdown2/countdown.go @@ -0,0 +1,46 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 244. + +// Countdown implements the countdown for a rocket launch. +package main + +import ( + "fmt" + "os" + "time" +) + +//!+ + +func main() { + // ...create abort channel... + + //!- + + //!+abort + abort := make(chan struct{}) + go func() { + os.Stdin.Read(make([]byte, 1)) // read a single byte + abort <- struct{}{} + }() + //!-abort + + //!+ + fmt.Println("Commencing countdown. Press return to abort.") + select { + case <-time.After(10 * time.Second): + // Do nothing. + case <-abort: + fmt.Println("Launch aborted!") + return + } + launch() +} + +//!- + +func launch() { + fmt.Println("Lift off!") +} diff --git a/vendor/gopl.io/ch8/countdown3/countdown.go b/vendor/gopl.io/ch8/countdown3/countdown.go new file mode 100644 index 0000000..ade9a55 --- /dev/null +++ b/vendor/gopl.io/ch8/countdown3/countdown.go @@ -0,0 +1,51 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 246. + +// Countdown implements the countdown for a rocket launch. +package main + +// NOTE: the ticker goroutine never terminates if the launch is aborted. +// This is a "goroutine leak". + +import ( + "fmt" + "os" + "time" +) + +//!+ + +func main() { + // ...create abort channel... + + //!- + + abort := make(chan struct{}) + go func() { + os.Stdin.Read(make([]byte, 1)) // read a single byte + abort <- struct{}{} + }() + + //!+ + fmt.Println("Commencing countdown. Press return to abort.") + tick := time.Tick(1 * time.Second) + for countdown := 10; countdown > 0; countdown-- { + fmt.Println(countdown) + select { + case <-tick: + // Do nothing. + case <-abort: + fmt.Println("Launch aborted!") + return + } + } + launch() +} + +//!- + +func launch() { + fmt.Println("Lift off!") +} diff --git a/vendor/gopl.io/ch8/crawl1/findlinks.go b/vendor/gopl.io/ch8/crawl1/findlinks.go new file mode 100644 index 0000000..df568b4 --- /dev/null +++ b/vendor/gopl.io/ch8/crawl1/findlinks.go @@ -0,0 +1,72 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 240. + +// Crawl1 crawls web links starting with the command-line arguments. +// +// This version quickly exhausts available file descriptors +// due to excessive concurrent calls to links.Extract. +// +// Also, it never terminates because the worklist is never closed. +package main + +import ( + "fmt" + "log" + "os" + + "gopl.io/ch5/links" +) + +//!+crawl +func crawl(url string) []string { + fmt.Println(url) + list, err := links.Extract(url) + if err != nil { + log.Print(err) + } + return list +} + +//!-crawl + +//!+main +func main() { + worklist := make(chan []string) + + // Start with the command-line arguments. + go func() { worklist <- os.Args[1:] }() + + // Crawl the web concurrently. + seen := make(map[string]bool) + for list := range worklist { + for _, link := range list { + if !seen[link] { + seen[link] = true + go func(link string) { + worklist <- crawl(link) + }(link) + } + } + } +} + +//!-main + +/* +//!+output +$ go build gopl.io/ch8/crawl1 +$ ./crawl1 http://gopl.io/ +http://gopl.io/ +https://golang.org/help/ + +https://golang.org/doc/ +https://golang.org/blog/ +... +2015/07/15 18:22:12 Get ...: dial tcp: lookup blog.golang.org: no such host +2015/07/15 18:22:12 Get ...: dial tcp 23.21.222.120:443: socket: + too many open files +... +//!-output +*/ diff --git a/vendor/gopl.io/ch8/crawl2/findlinks.go b/vendor/gopl.io/ch8/crawl2/findlinks.go new file mode 100644 index 0000000..aad20f8 --- /dev/null +++ b/vendor/gopl.io/ch8/crawl2/findlinks.go @@ -0,0 +1,64 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 241. + +// Crawl2 crawls web links starting with the command-line arguments. +// +// This version uses a buffered channel as a counting semaphore +// to limit the number of concurrent calls to links.Extract. +package main + +import ( + "fmt" + "log" + "os" + + "gopl.io/ch5/links" +) + +//!+sema +// tokens is a counting semaphore used to +// enforce a limit of 20 concurrent requests. +var tokens = make(chan struct{}, 20) + +func crawl(url string) []string { + fmt.Println(url) + tokens <- struct{}{} // acquire a token + list, err := links.Extract(url) + <-tokens // release the token + + if err != nil { + log.Print(err) + } + return list +} + +//!-sema + +//!+ +func main() { + worklist := make(chan []string) + var n int // number of pending sends to worklist + + // Start with the command-line arguments. + n++ + go func() { worklist <- os.Args[1:] }() + + // Crawl the web concurrently. + seen := make(map[string]bool) + for ; n > 0; n-- { + list := <-worklist + for _, link := range list { + if !seen[link] { + seen[link] = true + n++ + go func(link string) { + worklist <- crawl(link) + }(link) + } + } + } +} + +//!- diff --git a/vendor/gopl.io/ch8/crawl3/findlinks.go b/vendor/gopl.io/ch8/crawl3/findlinks.go new file mode 100644 index 0000000..8283de1 --- /dev/null +++ b/vendor/gopl.io/ch8/crawl3/findlinks.go @@ -0,0 +1,61 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 243. + +// Crawl3 crawls web links starting with the command-line arguments. +// +// This version uses bounded parallelism. +// For simplicity, it does not address the termination problem. +// +package main + +import ( + "fmt" + "log" + "os" + + "gopl.io/ch5/links" +) + +func crawl(url string) []string { + fmt.Println(url) + list, err := links.Extract(url) + if err != nil { + log.Print(err) + } + return list +} + +//!+ +func main() { + worklist := make(chan []string) // lists of URLs, may have duplicates + unseenLinks := make(chan string) // de-duplicated URLs + + // Add command-line arguments to worklist. + go func() { worklist <- os.Args[1:] }() + + // Create 20 crawler goroutines to fetch each unseen link. + for i := 0; i < 20; i++ { + go func() { + for link := range unseenLinks { + foundLinks := crawl(link) + go func() { worklist <- foundLinks }() + } + }() + } + + // The main goroutine de-duplicates worklist items + // and sends the unseen ones to the crawlers. + seen := make(map[string]bool) + for list := range worklist { + for _, link := range list { + if !seen[link] { + seen[link] = true + unseenLinks <- link + } + } + } +} + +//!- diff --git a/vendor/gopl.io/ch8/du1/main.go b/vendor/gopl.io/ch8/du1/main.go new file mode 100644 index 0000000..8aee837 --- /dev/null +++ b/vendor/gopl.io/ch8/du1/main.go @@ -0,0 +1,79 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 247. + +//!+main + +// The du1 command computes the disk usage of the files in a directory. +package main + +import ( + "flag" + "fmt" + "io/ioutil" + "os" + "path/filepath" +) + +func main() { + // Determine the initial directories. + flag.Parse() + roots := flag.Args() + if len(roots) == 0 { + roots = []string{"."} + } + + // Traverse the file tree. + fileSizes := make(chan int64) + go func() { + for _, root := range roots { + walkDir(root, fileSizes) + } + close(fileSizes) + }() + + // Print the results. + var nfiles, nbytes int64 + for size := range fileSizes { + nfiles++ + nbytes += size + } + printDiskUsage(nfiles, nbytes) +} + +func printDiskUsage(nfiles, nbytes int64) { + fmt.Printf("%d files %.1f GB\n", nfiles, float64(nbytes)/1e9) +} + +//!-main + +//!+walkDir + +// walkDir recursively walks the file tree rooted at dir +// and sends the size of each found file on fileSizes. +func walkDir(dir string, fileSizes chan<- int64) { + for _, entry := range dirents(dir) { + if entry.IsDir() { + subdir := filepath.Join(dir, entry.Name()) + walkDir(subdir, fileSizes) + } else { + fileSizes <- entry.Size() + } + } +} + +// dirents returns the entries of directory dir. +func dirents(dir string) []os.FileInfo { + entries, err := ioutil.ReadDir(dir) + if err != nil { + fmt.Fprintf(os.Stderr, "du1: %v\n", err) + return nil + } + return entries +} + +//!-walkDir + +// The du1 variant uses two goroutines and +// prints the total after every file is found. diff --git a/vendor/gopl.io/ch8/du2/main.go b/vendor/gopl.io/ch8/du2/main.go new file mode 100644 index 0000000..842944f --- /dev/null +++ b/vendor/gopl.io/ch8/du2/main.go @@ -0,0 +1,94 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 249. + +// The du2 command computes the disk usage of the files in a directory. +package main + +// The du2 variant uses select and a time.Ticker +// to print the totals periodically if -v is set. + +import ( + "flag" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "time" +) + +//!+ +var verbose = flag.Bool("v", false, "show verbose progress messages") + +func main() { + // ...start background goroutine... + + //!- + // Determine the initial directories. + flag.Parse() + roots := flag.Args() + if len(roots) == 0 { + roots = []string{"."} + } + + // Traverse the file tree. + fileSizes := make(chan int64) + go func() { + for _, root := range roots { + walkDir(root, fileSizes) + } + close(fileSizes) + }() + + //!+ + // Print the results periodically. + var tick <-chan time.Time + if *verbose { + tick = time.Tick(500 * time.Millisecond) + } + var nfiles, nbytes int64 +loop: + for { + select { + case size, ok := <-fileSizes: + if !ok { + break loop // fileSizes was closed + } + nfiles++ + nbytes += size + case <-tick: + printDiskUsage(nfiles, nbytes) + } + } + printDiskUsage(nfiles, nbytes) // final totals +} + +//!- + +func printDiskUsage(nfiles, nbytes int64) { + fmt.Printf("%d files %.1f GB\n", nfiles, float64(nbytes)/1e9) +} + +// walkDir recursively walks the file tree rooted at dir +// and sends the size of each found file on fileSizes. +func walkDir(dir string, fileSizes chan<- int64) { + for _, entry := range dirents(dir) { + if entry.IsDir() { + subdir := filepath.Join(dir, entry.Name()) + walkDir(subdir, fileSizes) + } else { + fileSizes <- entry.Size() + } + } +} + +// dirents returns the entries of directory dir. +func dirents(dir string) []os.FileInfo { + entries, err := ioutil.ReadDir(dir) + if err != nil { + fmt.Fprintf(os.Stderr, "du: %v\n", err) + return nil + } + return entries +} diff --git a/vendor/gopl.io/ch8/du3/main.go b/vendor/gopl.io/ch8/du3/main.go new file mode 100644 index 0000000..42b3214 --- /dev/null +++ b/vendor/gopl.io/ch8/du3/main.go @@ -0,0 +1,118 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 250. + +// The du3 command computes the disk usage of the files in a directory. +package main + +// The du3 variant traverses all directories in parallel. +// It uses a concurrency-limiting counting semaphore +// to avoid opening too many files at once. + +import ( + "flag" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "sync" + "time" +) + +var vFlag = flag.Bool("v", false, "show verbose progress messages") + +//!+ +func main() { + // ...determine roots... + + //!- + flag.Parse() + + // Determine the initial directories. + roots := flag.Args() + if len(roots) == 0 { + roots = []string{"."} + } + + //!+ + // Traverse each root of the file tree in parallel. + fileSizes := make(chan int64) + var n sync.WaitGroup + for _, root := range roots { + n.Add(1) + go walkDir(root, &n, fileSizes) + } + go func() { + n.Wait() + close(fileSizes) + }() + //!- + + // Print the results periodically. + var tick <-chan time.Time + if *vFlag { + tick = time.Tick(500 * time.Millisecond) + } + var nfiles, nbytes int64 +loop: + for { + select { + case size, ok := <-fileSizes: + if !ok { + break loop // fileSizes was closed + } + nfiles++ + nbytes += size + case <-tick: + printDiskUsage(nfiles, nbytes) + } + } + + printDiskUsage(nfiles, nbytes) // final totals + //!+ + // ...select loop... +} + +//!- + +func printDiskUsage(nfiles, nbytes int64) { + fmt.Printf("%d files %.1f GB\n", nfiles, float64(nbytes)/1e9) +} + +// walkDir recursively walks the file tree rooted at dir +// and sends the size of each found file on fileSizes. +//!+walkDir +func walkDir(dir string, n *sync.WaitGroup, fileSizes chan<- int64) { + defer n.Done() + for _, entry := range dirents(dir) { + if entry.IsDir() { + n.Add(1) + subdir := filepath.Join(dir, entry.Name()) + go walkDir(subdir, n, fileSizes) + } else { + fileSizes <- entry.Size() + } + } +} + +//!-walkDir + +//!+sema +// sema is a counting semaphore for limiting concurrency in dirents. +var sema = make(chan struct{}, 20) + +// dirents returns the entries of directory dir. +func dirents(dir string) []os.FileInfo { + sema <- struct{}{} // acquire token + defer func() { <-sema }() // release token + // ... + //!-sema + + entries, err := ioutil.ReadDir(dir) + if err != nil { + fmt.Fprintf(os.Stderr, "du: %v\n", err) + return nil + } + return entries +} diff --git a/vendor/gopl.io/ch8/du4/main.go b/vendor/gopl.io/ch8/du4/main.go new file mode 100644 index 0000000..bc3f764 --- /dev/null +++ b/vendor/gopl.io/ch8/du4/main.go @@ -0,0 +1,145 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 251. + +// The du4 command computes the disk usage of the files in a directory. +package main + +// The du4 variant includes cancellation: +// it terminates quickly when the user hits return. + +import ( + "fmt" + "os" + "path/filepath" + "sync" + "time" +) + +//!+1 +var done = make(chan struct{}) + +func cancelled() bool { + select { + case <-done: + return true + default: + return false + } +} + +//!-1 + +func main() { + // Determine the initial directories. + roots := os.Args[1:] + if len(roots) == 0 { + roots = []string{"."} + } + + //!+2 + // Cancel traversal when input is detected. + go func() { + os.Stdin.Read(make([]byte, 1)) // read a single byte + close(done) + }() + //!-2 + + // Traverse each root of the file tree in parallel. + fileSizes := make(chan int64) + var n sync.WaitGroup + for _, root := range roots { + n.Add(1) + go walkDir(root, &n, fileSizes) + } + go func() { + n.Wait() + close(fileSizes) + }() + + // Print the results periodically. + tick := time.Tick(500 * time.Millisecond) + var nfiles, nbytes int64 +loop: + //!+3 + for { + select { + case <-done: + // Drain fileSizes to allow existing goroutines to finish. + for range fileSizes { + // Do nothing. + } + return + case size, ok := <-fileSizes: + // ... + //!-3 + if !ok { + break loop // fileSizes was closed + } + nfiles++ + nbytes += size + case <-tick: + printDiskUsage(nfiles, nbytes) + } + } + printDiskUsage(nfiles, nbytes) // final totals +} + +func printDiskUsage(nfiles, nbytes int64) { + fmt.Printf("%d files %.1f GB\n", nfiles, float64(nbytes)/1e9) +} + +// walkDir recursively walks the file tree rooted at dir +// and sends the size of each found file on fileSizes. +//!+4 +func walkDir(dir string, n *sync.WaitGroup, fileSizes chan<- int64) { + defer n.Done() + if cancelled() { + return + } + for _, entry := range dirents(dir) { + // ... + //!-4 + if entry.IsDir() { + n.Add(1) + subdir := filepath.Join(dir, entry.Name()) + go walkDir(subdir, n, fileSizes) + } else { + fileSizes <- entry.Size() + } + //!+4 + } +} + +//!-4 + +var sema = make(chan struct{}, 20) // concurrency-limiting counting semaphore + +// dirents returns the entries of directory dir. +//!+5 +func dirents(dir string) []os.FileInfo { + select { + case sema <- struct{}{}: // acquire token + case <-done: + return nil // cancelled + } + defer func() { <-sema }() // release token + + // ...read directory... + //!-5 + + f, err := os.Open(dir) + if err != nil { + fmt.Fprintf(os.Stderr, "du: %v\n", err) + return nil + } + defer f.Close() + + entries, err := f.Readdir(0) // 0 => no limit; read all entries + if err != nil { + fmt.Fprintf(os.Stderr, "du: %v\n", err) + // Don't return: Readdir may return partial results. + } + return entries +} diff --git a/vendor/gopl.io/ch8/netcat1/netcat.go b/vendor/gopl.io/ch8/netcat1/netcat.go new file mode 100644 index 0000000..e3c7823 --- /dev/null +++ b/vendor/gopl.io/ch8/netcat1/netcat.go @@ -0,0 +1,32 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 221. +//!+ + +// Netcat1 is a read-only TCP client. +package main + +import ( + "io" + "log" + "net" + "os" +) + +func main() { + conn, err := net.Dial("tcp", "localhost:8000") + if err != nil { + log.Fatal(err) + } + defer conn.Close() + mustCopy(os.Stdout, conn) +} + +func mustCopy(dst io.Writer, src io.Reader) { + if _, err := io.Copy(dst, src); err != nil { + log.Fatal(err) + } +} + +//!- diff --git a/vendor/gopl.io/ch8/netcat2/netcat.go b/vendor/gopl.io/ch8/netcat2/netcat.go new file mode 100644 index 0000000..7a86a5c --- /dev/null +++ b/vendor/gopl.io/ch8/netcat2/netcat.go @@ -0,0 +1,33 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 223. + +// Netcat is a simple read/write client for TCP servers. +package main + +import ( + "io" + "log" + "net" + "os" +) + +//!+ +func main() { + conn, err := net.Dial("tcp", "localhost:8000") + if err != nil { + log.Fatal(err) + } + defer conn.Close() + go mustCopy(os.Stdout, conn) + mustCopy(conn, os.Stdin) +} + +//!- + +func mustCopy(dst io.Writer, src io.Reader) { + if _, err := io.Copy(dst, src); err != nil { + log.Fatal(err) + } +} diff --git a/vendor/gopl.io/ch8/netcat3/netcat.go b/vendor/gopl.io/ch8/netcat3/netcat.go new file mode 100644 index 0000000..3950998 --- /dev/null +++ b/vendor/gopl.io/ch8/netcat3/netcat.go @@ -0,0 +1,39 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 227. + +// Netcat is a simple read/write client for TCP servers. +package main + +import ( + "io" + "log" + "net" + "os" +) + +//!+ +func main() { + conn, err := net.Dial("tcp", "localhost:8000") + if err != nil { + log.Fatal(err) + } + done := make(chan struct{}) + go func() { + io.Copy(os.Stdout, conn) // NOTE: ignoring errors + log.Println("done") + done <- struct{}{} // signal the main goroutine + }() + mustCopy(conn, os.Stdin) + conn.Close() + <-done // wait for background goroutine to finish +} + +//!- + +func mustCopy(dst io.Writer, src io.Reader) { + if _, err := io.Copy(dst, src); err != nil { + log.Fatal(err) + } +} diff --git a/vendor/gopl.io/ch8/pipeline1/main.go b/vendor/gopl.io/ch8/pipeline1/main.go new file mode 100644 index 0000000..06ecd5c --- /dev/null +++ b/vendor/gopl.io/ch8/pipeline1/main.go @@ -0,0 +1,37 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 228. + +// Pipeline1 demonstrates an infinite 3-stage pipeline. +package main + +import "fmt" + +//!+ +func main() { + naturals := make(chan int) + squares := make(chan int) + + // Counter + go func() { + for x := 0; ; x++ { + naturals <- x + } + }() + + // Squarer + go func() { + for { + x := <-naturals + squares <- x * x + } + }() + + // Printer (in main goroutine) + for { + fmt.Println(<-squares) + } +} + +//!- diff --git a/vendor/gopl.io/ch8/pipeline2/main.go b/vendor/gopl.io/ch8/pipeline2/main.go new file mode 100644 index 0000000..6015c01 --- /dev/null +++ b/vendor/gopl.io/ch8/pipeline2/main.go @@ -0,0 +1,38 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 229. + +// Pipeline2 demonstrates a finite 3-stage pipeline. +package main + +import "fmt" + +//!+ +func main() { + naturals := make(chan int) + squares := make(chan int) + + // Counter + go func() { + for x := 0; x < 100; x++ { + naturals <- x + } + close(naturals) + }() + + // Squarer + go func() { + for x := range naturals { + squares <- x * x + } + close(squares) + }() + + // Printer (in main goroutine) + for x := range squares { + fmt.Println(x) + } +} + +//!- diff --git a/vendor/gopl.io/ch8/pipeline3/main.go b/vendor/gopl.io/ch8/pipeline3/main.go new file mode 100644 index 0000000..cc679ea --- /dev/null +++ b/vendor/gopl.io/ch8/pipeline3/main.go @@ -0,0 +1,42 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 231. + +// Pipeline3 demonstrates a finite 3-stage pipeline +// with range, close, and unidirectional channel types. +package main + +import "fmt" + +//!+ +func counter(out chan<- int) { + for x := 0; x < 100; x++ { + out <- x + } + close(out) +} + +func squarer(out chan<- int, in <-chan int) { + for v := range in { + out <- v * v + } + close(out) +} + +func printer(in <-chan int) { + for v := range in { + fmt.Println(v) + } +} + +func main() { + naturals := make(chan int) + squares := make(chan int) + + go counter(naturals) + go squarer(squares, naturals) + printer(squares) +} + +//!- diff --git a/vendor/gopl.io/ch8/reverb1/reverb.go b/vendor/gopl.io/ch8/reverb1/reverb.go new file mode 100644 index 0000000..c72823f --- /dev/null +++ b/vendor/gopl.io/ch8/reverb1/reverb.go @@ -0,0 +1,51 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 223. + +// Reverb1 is a TCP server that simulates an echo. +package main + +import ( + "bufio" + "fmt" + "log" + "net" + "strings" + "time" +) + +//!+ +func echo(c net.Conn, shout string, delay time.Duration) { + fmt.Fprintln(c, "\t", strings.ToUpper(shout)) + time.Sleep(delay) + fmt.Fprintln(c, "\t", shout) + time.Sleep(delay) + fmt.Fprintln(c, "\t", strings.ToLower(shout)) +} + +func handleConn(c net.Conn) { + input := bufio.NewScanner(c) + for input.Scan() { + echo(c, input.Text(), 1*time.Second) + } + // NOTE: ignoring potential errors from input.Err() + c.Close() +} + +//!- + +func main() { + l, err := net.Listen("tcp", "localhost:8000") + if err != nil { + log.Fatal(err) + } + for { + conn, err := l.Accept() + if err != nil { + log.Print(err) // e.g., connection aborted + continue + } + go handleConn(conn) + } +} diff --git a/vendor/gopl.io/ch8/reverb2/reverb.go b/vendor/gopl.io/ch8/reverb2/reverb.go new file mode 100644 index 0000000..b74487d --- /dev/null +++ b/vendor/gopl.io/ch8/reverb2/reverb.go @@ -0,0 +1,51 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 224. + +// Reverb2 is a TCP server that simulates an echo. +package main + +import ( + "bufio" + "fmt" + "log" + "net" + "strings" + "time" +) + +func echo(c net.Conn, shout string, delay time.Duration) { + fmt.Fprintln(c, "\t", strings.ToUpper(shout)) + time.Sleep(delay) + fmt.Fprintln(c, "\t", shout) + time.Sleep(delay) + fmt.Fprintln(c, "\t", strings.ToLower(shout)) +} + +//!+ +func handleConn(c net.Conn) { + input := bufio.NewScanner(c) + for input.Scan() { + go echo(c, input.Text(), 1*time.Second) + } + // NOTE: ignoring potential errors from input.Err() + c.Close() +} + +//!- + +func main() { + l, err := net.Listen("tcp", "localhost:8000") + if err != nil { + log.Fatal(err) + } + for { + conn, err := l.Accept() + if err != nil { + log.Print(err) // e.g., connection aborted + continue + } + go handleConn(conn) + } +} diff --git a/vendor/gopl.io/ch8/spinner/main.go b/vendor/gopl.io/ch8/spinner/main.go new file mode 100644 index 0000000..c6a8cb1 --- /dev/null +++ b/vendor/gopl.io/ch8/spinner/main.go @@ -0,0 +1,38 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 218. + +// Spinner displays an animation while computing the 45th Fibonacci number. +package main + +import ( + "fmt" + "time" +) + +//!+ +func main() { + go spinner(100 * time.Millisecond) + const n = 45 + fibN := fib(n) // slow + fmt.Printf("\rFibonacci(%d) = %d\n", n, fibN) +} + +func spinner(delay time.Duration) { + for { + for _, r := range `-\|/` { + fmt.Printf("\r%c", r) + time.Sleep(delay) + } + } +} + +func fib(x int) int { + if x < 2 { + return x + } + return fib(x-1) + fib(x-2) +} + +//!- diff --git a/vendor/gopl.io/ch8/thumbnail/main.go b/vendor/gopl.io/ch8/thumbnail/main.go new file mode 100644 index 0000000..a66174c --- /dev/null +++ b/vendor/gopl.io/ch8/thumbnail/main.go @@ -0,0 +1,42 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// +build ignore + +// The thumbnail command produces thumbnails of JPEG files +// whose names are provided on each line of the standard input. +// +// The "+build ignore" tag (see p.295) excludes this file from the +// thumbnail package, but it can be compiled as a command and run like +// this: +// +// Run with: +// $ go run $GOPATH/src/gopl.io/ch8/thumbnail/main.go +// foo.jpeg +// ^D +// +package main + +import ( + "bufio" + "fmt" + "log" + "os" + + "gopl.io/ch8/thumbnail" +) + +func main() { + input := bufio.NewScanner(os.Stdin) + for input.Scan() { + thumb, err := thumbnail.ImageFile(input.Text()) + if err != nil { + log.Print(err) + continue + } + fmt.Println(thumb) + } + if err := input.Err(); err != nil { + log.Fatal(err) + } +} diff --git a/vendor/gopl.io/ch8/thumbnail/thumbnail.go b/vendor/gopl.io/ch8/thumbnail/thumbnail.go new file mode 100644 index 0000000..a05f702 --- /dev/null +++ b/vendor/gopl.io/ch8/thumbnail/thumbnail.go @@ -0,0 +1,86 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 234. + +// The thumbnail package produces thumbnail-size images from +// larger images. Only JPEG images are currently supported. +package thumbnail + +import ( + "fmt" + "image" + "image/jpeg" + "io" + "os" + "path/filepath" + "strings" +) + +// Image returns a thumbnail-size version of src. +func Image(src image.Image) image.Image { + // Compute thumbnail size, preserving aspect ratio. + xs := src.Bounds().Size().X + ys := src.Bounds().Size().Y + width, height := 128, 128 + if aspect := float64(xs) / float64(ys); aspect < 1.0 { + width = int(128 * aspect) // portrait + } else { + height = int(128 / aspect) // landscape + } + xscale := float64(xs) / float64(width) + yscale := float64(ys) / float64(height) + + dst := image.NewRGBA(image.Rect(0, 0, width, height)) + + // a very crude scaling algorithm + for x := 0; x < width; x++ { + for y := 0; y < height; y++ { + srcx := int(float64(x) * xscale) + srcy := int(float64(y) * yscale) + dst.Set(x, y, src.At(srcx, srcy)) + } + } + return dst +} + +// ImageStream reads an image from r and +// writes a thumbnail-size version of it to w. +func ImageStream(w io.Writer, r io.Reader) error { + src, _, err := image.Decode(r) + if err != nil { + return err + } + dst := Image(src) + return jpeg.Encode(w, dst, nil) +} + +// ImageFile2 reads an image from infile and writes +// a thumbnail-size version of it to outfile. +func ImageFile2(outfile, infile string) (err error) { + in, err := os.Open(infile) + if err != nil { + return err + } + defer in.Close() + + out, err := os.Create(outfile) + if err != nil { + return err + } + + if err := ImageStream(out, in); err != nil { + out.Close() + return fmt.Errorf("scaling %s to %s: %s", infile, outfile, err) + } + return out.Close() +} + +// ImageFile reads an image from infile and writes +// a thumbnail-size version of it in the same directory. +// It returns the generated file name, e.g. "foo.thumb.jpeg". +func ImageFile(infile string) (string, error) { + ext := filepath.Ext(infile) // e.g., ".jpg", ".JPEG" + outfile := strings.TrimSuffix(infile, ext) + ".thumb" + ext + return outfile, ImageFile2(outfile, infile) +} diff --git a/vendor/gopl.io/ch8/thumbnail/thumbnail_test.go b/vendor/gopl.io/ch8/thumbnail/thumbnail_test.go new file mode 100644 index 0000000..be05b6c --- /dev/null +++ b/vendor/gopl.io/ch8/thumbnail/thumbnail_test.go @@ -0,0 +1,148 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// This file is just a place to put example code from the book. +// It does not actually run any code in gopl.io/ch8/thumbnail. + +package thumbnail_test + +import ( + "log" + "os" + "sync" + + "gopl.io/ch8/thumbnail" +) + +//!+1 +// makeThumbnails makes thumbnails of the specified files. +func makeThumbnails(filenames []string) { + for _, f := range filenames { + if _, err := thumbnail.ImageFile(f); err != nil { + log.Println(err) + } + } +} + +//!-1 + +//!+2 +// NOTE: incorrect! +func makeThumbnails2(filenames []string) { + for _, f := range filenames { + go thumbnail.ImageFile(f) // NOTE: ignoring errors + } +} + +//!-2 + +//!+3 +// makeThumbnails3 makes thumbnails of the specified files in parallel. +func makeThumbnails3(filenames []string) { + ch := make(chan struct{}) + for _, f := range filenames { + go func(f string) { + thumbnail.ImageFile(f) // NOTE: ignoring errors + ch <- struct{}{} + }(f) + } + + // Wait for goroutines to complete. + for range filenames { + <-ch + } +} + +//!-3 + +//!+4 +// makeThumbnails4 makes thumbnails for the specified files in parallel. +// It returns an error if any step failed. +func makeThumbnails4(filenames []string) error { + errors := make(chan error) + + for _, f := range filenames { + go func(f string) { + _, err := thumbnail.ImageFile(f) + errors <- err + }(f) + } + + for range filenames { + if err := <-errors; err != nil { + return err // NOTE: incorrect: goroutine leak! + } + } + + return nil +} + +//!-4 + +//!+5 +// makeThumbnails5 makes thumbnails for the specified files in parallel. +// It returns the generated file names in an arbitrary order, +// or an error if any step failed. +func makeThumbnails5(filenames []string) (thumbfiles []string, err error) { + type item struct { + thumbfile string + err error + } + + ch := make(chan item, len(filenames)) + for _, f := range filenames { + go func(f string) { + var it item + it.thumbfile, it.err = thumbnail.ImageFile(f) + ch <- it + }(f) + } + + for range filenames { + it := <-ch + if it.err != nil { + return nil, it.err + } + thumbfiles = append(thumbfiles, it.thumbfile) + } + + return thumbfiles, nil +} + +//!-5 + +//!+6 +// makeThumbnails6 makes thumbnails for each file received from the channel. +// It returns the number of bytes occupied by the files it creates. +func makeThumbnails6(filenames <-chan string) int64 { + sizes := make(chan int64) + var wg sync.WaitGroup // number of working goroutines + for f := range filenames { + wg.Add(1) + // worker + go func(f string) { + defer wg.Done() + thumb, err := thumbnail.ImageFile(f) + if err != nil { + log.Println(err) + return + } + info, _ := os.Stat(thumb) // OK to ignore error + sizes <- info.Size() + }(f) + } + + // closer + go func() { + wg.Wait() + close(sizes) + }() + + var total int64 + for size := range sizes { + total += size + } + return total +} + +//!-6 diff --git a/vendor/gopl.io/ch9/bank1/bank.go b/vendor/gopl.io/ch9/bank1/bank.go new file mode 100644 index 0000000..67fb41a --- /dev/null +++ b/vendor/gopl.io/ch9/bank1/bank.go @@ -0,0 +1,31 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 261. +//!+ + +// Package bank provides a concurrency-safe bank with one account. +package bank + +var deposits = make(chan int) // send amount to deposit +var balances = make(chan int) // receive balance + +func Deposit(amount int) { deposits <- amount } +func Balance() int { return <-balances } + +func teller() { + var balance int // balance is confined to teller goroutine + for { + select { + case amount := <-deposits: + balance += amount + case balances <- balance: + } + } +} + +func init() { + go teller() // start the monitor goroutine +} + +//!- diff --git a/vendor/gopl.io/ch9/bank1/bank_test.go b/vendor/gopl.io/ch9/bank1/bank_test.go new file mode 100644 index 0000000..1c36b1c --- /dev/null +++ b/vendor/gopl.io/ch9/bank1/bank_test.go @@ -0,0 +1,36 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +package bank_test + +import ( + "fmt" + "testing" + + "gopl.io/ch9/bank1" +) + +func TestBank(t *testing.T) { + done := make(chan struct{}) + + // Alice + go func() { + bank.Deposit(200) + fmt.Println("=", bank.Balance()) + done <- struct{}{} + }() + + // Bob + go func() { + bank.Deposit(100) + done <- struct{}{} + }() + + // Wait for both transactions. + <-done + <-done + + if got, want := bank.Balance(), 300; got != want { + t.Errorf("Balance = %d, want %d", got, want) + } +} diff --git a/vendor/gopl.io/ch9/bank2/bank.go b/vendor/gopl.io/ch9/bank2/bank.go new file mode 100644 index 0000000..8cc73d7 --- /dev/null +++ b/vendor/gopl.io/ch9/bank2/bank.go @@ -0,0 +1,28 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 262. + +// Package bank provides a concurrency-safe bank with one account. +package bank + +//!+ +var ( + sema = make(chan struct{}, 1) // a binary semaphore guarding balance + balance int +) + +func Deposit(amount int) { + sema <- struct{}{} // acquire token + balance = balance + amount + <-sema // release token +} + +func Balance() int { + sema <- struct{}{} // acquire token + b := balance + <-sema // release token + return b +} + +//!- diff --git a/vendor/gopl.io/ch9/bank2/bank_test.go b/vendor/gopl.io/ch9/bank2/bank_test.go new file mode 100644 index 0000000..98fed00 --- /dev/null +++ b/vendor/gopl.io/ch9/bank2/bank_test.go @@ -0,0 +1,28 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +package bank_test + +import ( + "sync" + "testing" + + "gopl.io/ch9/bank2" +) + +func TestBank(t *testing.T) { + // Deposit [1..1000] concurrently. + var n sync.WaitGroup + for i := 1; i <= 1000; i++ { + n.Add(1) + go func(amount int) { + bank.Deposit(amount) + n.Done() + }(i) + } + n.Wait() + + if got, want := bank.Balance(), (1000+1)*1000/2; got != want { + t.Errorf("Balance = %d, want %d", got, want) + } +} diff --git a/vendor/gopl.io/ch9/bank3/bank.go b/vendor/gopl.io/ch9/bank3/bank.go new file mode 100644 index 0000000..5c42d45 --- /dev/null +++ b/vendor/gopl.io/ch9/bank3/bank.go @@ -0,0 +1,30 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 263. + +// Package bank provides a concurrency-safe single-account bank. +package bank + +//!+ +import "sync" + +var ( + mu sync.Mutex // guards balance + balance int +) + +func Deposit(amount int) { + mu.Lock() + balance = balance + amount + mu.Unlock() +} + +func Balance() int { + mu.Lock() + b := balance + mu.Unlock() + return b +} + +//!- diff --git a/vendor/gopl.io/ch9/bank3/bank_test.go b/vendor/gopl.io/ch9/bank3/bank_test.go new file mode 100644 index 0000000..98fed00 --- /dev/null +++ b/vendor/gopl.io/ch9/bank3/bank_test.go @@ -0,0 +1,28 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +package bank_test + +import ( + "sync" + "testing" + + "gopl.io/ch9/bank2" +) + +func TestBank(t *testing.T) { + // Deposit [1..1000] concurrently. + var n sync.WaitGroup + for i := 1; i <= 1000; i++ { + n.Add(1) + go func(amount int) { + bank.Deposit(amount) + n.Done() + }(i) + } + n.Wait() + + if got, want := bank.Balance(), (1000+1)*1000/2; got != want { + t.Errorf("Balance = %d, want %d", got, want) + } +} diff --git a/vendor/gopl.io/ch9/memo1/memo.go b/vendor/gopl.io/ch9/memo1/memo.go new file mode 100644 index 0000000..7b9c1b6 --- /dev/null +++ b/vendor/gopl.io/ch9/memo1/memo.go @@ -0,0 +1,40 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 272. + +//!+ + +// Package memo provides a concurrency-unsafe +// memoization of a function of type Func. +package memo + +// A Memo caches the results of calling a Func. +type Memo struct { + f Func + cache map[string]result +} + +// Func is the type of the function to memoize. +type Func func(key string) (interface{}, error) + +type result struct { + value interface{} + err error +} + +func New(f Func) *Memo { + return &Memo{f: f, cache: make(map[string]result)} +} + +// NOTE: not concurrency-safe! +func (memo *Memo) Get(key string) (interface{}, error) { + res, ok := memo.cache[key] + if !ok { + res.value, res.err = memo.f(key) + memo.cache[key] = res + } + return res.value, res.err +} + +//!- diff --git a/vendor/gopl.io/ch9/memo1/memo_test.go b/vendor/gopl.io/ch9/memo1/memo_test.go new file mode 100644 index 0000000..1b1c9af --- /dev/null +++ b/vendor/gopl.io/ch9/memo1/memo_test.go @@ -0,0 +1,67 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +package memo_test + +import ( + "testing" + + "gopl.io/ch9/memo1" + "gopl.io/ch9/memotest" +) + +var httpGetBody = memotest.HTTPGetBody + +func Test(t *testing.T) { + m := memo.New(httpGetBody) + memotest.Sequential(t, m) +} + +// NOTE: not concurrency-safe! Test fails. +func TestConcurrent(t *testing.T) { + m := memo.New(httpGetBody) + memotest.Concurrent(t, m) +} + +/* +//!+output +$ go test -v gopl.io/ch9/memo1 +=== RUN Test +https://golang.org, 175.026418ms, 7537 bytes +https://godoc.org, 172.686825ms, 6878 bytes +https://play.golang.org, 115.762377ms, 5767 bytes +http://gopl.io, 749.887242ms, 2856 bytes + +https://golang.org, 721ns, 7537 bytes +https://godoc.org, 152ns, 6878 bytes +https://play.golang.org, 205ns, 5767 bytes +http://gopl.io, 326ns, 2856 bytes +--- PASS: Test (1.21s) +PASS +ok gopl.io/ch9/memo1 1.257s +//!-output +*/ + +/* +//!+race +$ go test -run=TestConcurrent -race -v gopl.io/ch9/memo1 +=== RUN TestConcurrent +... +WARNING: DATA RACE +Write by goroutine 36: + runtime.mapassign1() + ~/go/src/runtime/hashmap.go:411 +0x0 + gopl.io/ch9/memo1.(*Memo).Get() + ~/gobook2/src/gopl.io/ch9/memo1/memo.go:32 +0x205 + ... + +Previous write by goroutine 35: + runtime.mapassign1() + ~/go/src/runtime/hashmap.go:411 +0x0 + gopl.io/ch9/memo1.(*Memo).Get() + ~/gobook2/src/gopl.io/ch9/memo1/memo.go:32 +0x205 +... +Found 1 data race(s) +FAIL gopl.io/ch9/memo1 2.393s +//!-race +*/ diff --git a/vendor/gopl.io/ch9/memo2/memo.go b/vendor/gopl.io/ch9/memo2/memo.go new file mode 100644 index 0000000..6397c1a --- /dev/null +++ b/vendor/gopl.io/ch9/memo2/memo.go @@ -0,0 +1,44 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 275. + +// Package memo provides a concurrency-safe memoization a function of +// type Func. Concurrent requests are serialized by a Mutex. +package memo + +import "sync" + +// Func is the type of the function to memoize. +type Func func(string) (interface{}, error) + +type result struct { + value interface{} + err error +} + +func New(f Func) *Memo { + return &Memo{f: f, cache: make(map[string]result)} +} + +//!+ + +type Memo struct { + f Func + mu sync.Mutex // guards cache + cache map[string]result +} + +// Get is concurrency-safe. +func (memo *Memo) Get(key string) (value interface{}, err error) { + memo.mu.Lock() + res, ok := memo.cache[key] + if !ok { + res.value, res.err = memo.f(key) + memo.cache[key] = res + } + memo.mu.Unlock() + return res.value, res.err +} + +//!- diff --git a/vendor/gopl.io/ch9/memo2/memo_test.go b/vendor/gopl.io/ch9/memo2/memo_test.go new file mode 100644 index 0000000..bd4dc7b --- /dev/null +++ b/vendor/gopl.io/ch9/memo2/memo_test.go @@ -0,0 +1,23 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +package memo_test + +import ( + "testing" + + "gopl.io/ch9/memo2" + "gopl.io/ch9/memotest" +) + +var httpGetBody = memotest.HTTPGetBody + +func Test(t *testing.T) { + m := memo.New(httpGetBody) + memotest.Sequential(t, m) +} + +func TestConcurrent(t *testing.T) { + m := memo.New(httpGetBody) + memotest.Concurrent(t, m) +} diff --git a/vendor/gopl.io/ch9/memo3/memo.go b/vendor/gopl.io/ch9/memo3/memo.go new file mode 100644 index 0000000..0c21afe --- /dev/null +++ b/vendor/gopl.io/ch9/memo3/memo.go @@ -0,0 +1,48 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 276. + +// Package memo provides a concurrency-safe memoization a function of +// type Func. Requests for different keys run concurrently. +// Concurrent requests for the same key result in duplicate work. +package memo + +import "sync" + +type Memo struct { + f Func + mu sync.Mutex // guards cache + cache map[string]result +} + +type Func func(string) (interface{}, error) + +type result struct { + value interface{} + err error +} + +func New(f Func) *Memo { + return &Memo{f: f, cache: make(map[string]result)} +} + +//!+ + +func (memo *Memo) Get(key string) (value interface{}, err error) { + memo.mu.Lock() + res, ok := memo.cache[key] + memo.mu.Unlock() + if !ok { + res.value, res.err = memo.f(key) + + // Between the two critical sections, several goroutines + // may race to compute f(key) and update the map. + memo.mu.Lock() + memo.cache[key] = res + memo.mu.Unlock() + } + return res.value, res.err +} + +//!- diff --git a/vendor/gopl.io/ch9/memo3/memo_test.go b/vendor/gopl.io/ch9/memo3/memo_test.go new file mode 100644 index 0000000..19fc5f0 --- /dev/null +++ b/vendor/gopl.io/ch9/memo3/memo_test.go @@ -0,0 +1,23 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +package memo_test + +import ( + "testing" + + "gopl.io/ch9/memo3" + "gopl.io/ch9/memotest" +) + +var httpGetBody = memotest.HTTPGetBody + +func Test(t *testing.T) { + m := memo.New(httpGetBody) + memotest.Sequential(t, m) +} + +func TestConcurrent(t *testing.T) { + m := memo.New(httpGetBody) + memotest.Concurrent(t, m) +} diff --git a/vendor/gopl.io/ch9/memo4/memo.go b/vendor/gopl.io/ch9/memo4/memo.go new file mode 100644 index 0000000..fc70e2f --- /dev/null +++ b/vendor/gopl.io/ch9/memo4/memo.go @@ -0,0 +1,61 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 276. + +// Package memo provides a concurrency-safe memoization a function of +// a function. Requests for different keys proceed in parallel. +// Concurrent requests for the same key block until the first completes. +// This implementation uses a Mutex. +package memo + +import "sync" + +// Func is the type of the function to memoize. +type Func func(string) (interface{}, error) + +type result struct { + value interface{} + err error +} + +//!+ +type entry struct { + res result + ready chan struct{} // closed when res is ready +} + +func New(f Func) *Memo { + return &Memo{f: f, cache: make(map[string]*entry)} +} + +type Memo struct { + f Func + mu sync.Mutex // guards cache + cache map[string]*entry +} + +func (memo *Memo) Get(key string) (value interface{}, err error) { + memo.mu.Lock() + e := memo.cache[key] + if e == nil { + // This is the first request for this key. + // This goroutine becomes responsible for computing + // the value and broadcasting the ready condition. + e = &entry{ready: make(chan struct{})} + memo.cache[key] = e + memo.mu.Unlock() + + e.res.value, e.res.err = memo.f(key) + + close(e.ready) // broadcast ready condition + } else { + // This is a repeat request for this key. + memo.mu.Unlock() + + <-e.ready // wait for ready condition + } + return e.res.value, e.res.err +} + +//!- diff --git a/vendor/gopl.io/ch9/memo4/memo_test.go b/vendor/gopl.io/ch9/memo4/memo_test.go new file mode 100644 index 0000000..55cdc12 --- /dev/null +++ b/vendor/gopl.io/ch9/memo4/memo_test.go @@ -0,0 +1,23 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +package memo_test + +import ( + "testing" + + "gopl.io/ch9/memo4" + "gopl.io/ch9/memotest" +) + +var httpGetBody = memotest.HTTPGetBody + +func Test(t *testing.T) { + m := memo.New(httpGetBody) + memotest.Sequential(t, m) +} + +func TestConcurrent(t *testing.T) { + m := memo.New(httpGetBody) + memotest.Concurrent(t, m) +} diff --git a/vendor/gopl.io/ch9/memo5/memo.go b/vendor/gopl.io/ch9/memo5/memo.go new file mode 100644 index 0000000..4a8fd9b --- /dev/null +++ b/vendor/gopl.io/ch9/memo5/memo.go @@ -0,0 +1,88 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 278. + +// Package memo provides a concurrency-safe non-blocking memoization +// of a function. Requests for different keys proceed in parallel. +// Concurrent requests for the same key block until the first completes. +// This implementation uses a monitor goroutine. +package memo + +//!+Func + +// Func is the type of the function to memoize. +type Func func(key string) (interface{}, error) + +// A result is the result of calling a Func. +type result struct { + value interface{} + err error +} + +type entry struct { + res result + ready chan struct{} // closed when res is ready +} + +//!-Func + +//!+get + +// A request is a message requesting that the Func be applied to key. +type request struct { + key string + response chan<- result // the client wants a single result +} + +type Memo struct{ requests chan request } + +// New returns a memoization of f. Clients must subsequently call Close. +func New(f Func) *Memo { + memo := &Memo{requests: make(chan request)} + go memo.server(f) + return memo +} + +func (memo *Memo) Get(key string) (interface{}, error) { + response := make(chan result) + memo.requests <- request{key, response} + res := <-response + return res.value, res.err +} + +func (memo *Memo) Close() { close(memo.requests) } + +//!-get + +//!+monitor + +func (memo *Memo) server(f Func) { + cache := make(map[string]*entry) + for req := range memo.requests { + e := cache[req.key] + if e == nil { + // This is the first request for this key. + e = &entry{ready: make(chan struct{})} + cache[req.key] = e + go e.call(f, req.key) // call f(key) + } + go e.deliver(req.response) + } +} + +func (e *entry) call(f Func, key string) { + // Evaluate the function. + e.res.value, e.res.err = f(key) + // Broadcast the ready condition. + close(e.ready) +} + +func (e *entry) deliver(response chan<- result) { + // Wait for the ready condition. + <-e.ready + // Send the result to the client. + response <- e.res +} + +//!-monitor diff --git a/vendor/gopl.io/ch9/memo5/memo_test.go b/vendor/gopl.io/ch9/memo5/memo_test.go new file mode 100644 index 0000000..e74052e --- /dev/null +++ b/vendor/gopl.io/ch9/memo5/memo_test.go @@ -0,0 +1,25 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +package memo_test + +import ( + "testing" + + "gopl.io/ch9/memo5" + "gopl.io/ch9/memotest" +) + +var httpGetBody = memotest.HTTPGetBody + +func Test(t *testing.T) { + m := memo.New(httpGetBody) + defer m.Close() + memotest.Sequential(t, m) +} + +func TestConcurrent(t *testing.T) { + m := memo.New(httpGetBody) + defer m.Close() + memotest.Concurrent(t, m) +} diff --git a/vendor/gopl.io/ch9/memotest/memotest.go b/vendor/gopl.io/ch9/memotest/memotest.go new file mode 100644 index 0000000..aab1acf --- /dev/null +++ b/vendor/gopl.io/ch9/memotest/memotest.go @@ -0,0 +1,102 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 272. + +// Package memotest provides common functions for +// testing various designs of the memo package. +package memotest + +import ( + "fmt" + "io/ioutil" + "log" + "net/http" + "sync" + "testing" + "time" +) + +//!+httpRequestBody +func httpGetBody(url string) (interface{}, error) { + resp, err := http.Get(url) + if err != nil { + return nil, err + } + defer resp.Body.Close() + return ioutil.ReadAll(resp.Body) +} + +//!-httpRequestBody + +var HTTPGetBody = httpGetBody + +func incomingURLs() <-chan string { + ch := make(chan string) + go func() { + for _, url := range []string{ + "https://golang.org", + "https://godoc.org", + "https://play.golang.org", + "http://gopl.io", + "https://golang.org", + "https://godoc.org", + "https://play.golang.org", + "http://gopl.io", + } { + ch <- url + } + close(ch) + }() + return ch +} + +type M interface { + Get(key string) (interface{}, error) +} + +/* +//!+seq + m := memo.New(httpGetBody) +//!-seq +*/ + +func Sequential(t *testing.T, m M) { + //!+seq + for url := range incomingURLs() { + start := time.Now() + value, err := m.Get(url) + if err != nil { + log.Print(err) + } + fmt.Printf("%s, %s, %d bytes\n", + url, time.Since(start), len(value.([]byte))) + } + //!-seq +} + +/* +//!+conc + m := memo.New(httpGetBody) +//!-conc +*/ + +func Concurrent(t *testing.T, m M) { + //!+conc + var n sync.WaitGroup + for url := range incomingURLs() { + n.Add(1) + go func(url string) { + start := time.Now() + value, err := m.Get(url) + if err != nil { + log.Print(err) + } + fmt.Printf("%s, %s, %d bytes\n", + url, time.Since(start), len(value.([]byte))) + n.Done() + }(url) + } + n.Wait() + //!-conc +} diff --git a/weixin-golang-china.jpg b/weixin-golang-china.jpg new file mode 100644 index 0000000..78facdd Binary files /dev/null and b/weixin-golang-china.jpg differ diff --git a/zh2tw.go b/zh2tw.go new file mode 100644 index 0000000..0f586be --- /dev/null +++ b/zh2tw.go @@ -0,0 +1,4395 @@ +// Copyright 2013 . All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build ingore + +// +// 简体字转繁体字, 1:1转换, 支持逆向转换. +// +// Example: +// zh2tw dir +// zh2tw dir "\.go$" +// zh2tw dir "\.md$" zh2tw +// zh2tw dir "\.md$" tw2zh +// +// Help: +// zh2tw -h +// +package main + +import ( + "fmt" + "io/ioutil" + "log" + "os" + "path/filepath" + "regexp" + "sort" + "unicode/utf8" +) + +const MaxFileSize = 8 << 20 // 8MB + +const usage = ` +Usage: zh2tw dir [nameFilter] + zh2tw -h + +Example: + zh2tw dir + zh2tw dir "\.go$" + zh2tw dir "\.md$" zh2tw + zh2tw dir "\.md$" tw2zh + +Report bugs to . +` + +func main() { + if len(os.Args) < 2 || os.Args[1] == "-h" { + fmt.Fprintln(os.Stderr, usage[1:len(usage)-1]) + os.Exit(0) + } + dir, nameFilter, method := os.Args[1], ".*", "zh2tw" + if len(os.Args) > 2 { + nameFilter = os.Args[2] + } + if len(os.Args) > 3 { + method = os.Args[3] + } + + if method != "zh2tw" && method != "tw2zh" { + fmt.Fprintln(os.Stderr, usage[1:len(usage)-1]) + os.Exit(0) + } + + total := 0 + filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { + if err != nil { + log.Fatal("filepath.Walk: ", err) + return err + } + if info.IsDir() { + return nil + } + relpath, err := filepath.Rel(dir, path) + if err != nil { + log.Fatal("filepath.Rel: ", err) + return err + } + mathed, err := regexp.MatchString(nameFilter, relpath) + if err != nil { + log.Fatal("regexp.MatchString: ", err) + } + if mathed { + if changed := convertFile(path, method); changed { + fmt.Printf("%s\n", relpath) + total++ + } + } + return nil + }) + fmt.Printf("%s total %d\n", method, total) +} + +func convertFile(path, method string) (changed bool) { + abspath, err := filepath.Abs(path) + if err != nil { + log.Fatal("convertFile: filepath.Abs:", err) + } + + fi, err := os.Lstat(abspath) + if err != nil { + log.Fatal("convertFile: os.Lstat:", err) + } + if fi.Size() > MaxFileSize { + return false + } + + oldData, err := ioutil.ReadFile(abspath) + if err != nil { + log.Fatal("convertFile: ioutil.ReadFile:", err) + } + if !utf8.Valid(oldData) { + return false + } + + newData := oldData + switch { + case method == "zh2tw": + newData = []byte(zh2tw(string(oldData))) + case method == "tw2zh": + newData = []byte(tw2zh(string(oldData))) + } + + if string(newData) == string(oldData) { + return false + } + + err = ioutil.WriteFile(abspath, newData, 0666) + if err != nil { + log.Fatal("convertFile: ioutil.WriteFile:", err) + } + return true +} + +func zh2tw(s string) string { + old := []rune(s) + new := make([]rune, 0) + for _, c := range old { + if x, ok := zh2twMap[c]; ok { + new = append(new, x) + } else { + new = append(new, c) + } + } + return string(new) +} + +func tw2zh(s string) string { + old := []rune(s) + new := make([]rune, 0) + for _, c := range old { + if x, ok := tw2zhMap[c]; ok { + new = append(new, x) + } else { + new = append(new, c) + } + } + return string(new) +} + +func init() { + // 作为map键的繁体没有重复 + // 但一个繁体可能对应多个简体, 需要按照key字典顺序导入 + // 只保留根据繁体key字典顺序第一个出现的简体 + kkMap := make([]int, 0, len(_TSCharactersMap)) + for k, _ := range _TSCharactersMap { + kkMap = append(kkMap, int(k)) + } + sort.Ints(kkMap) + + // 导入初始转换表 + for _, k := range kkMap { + k := rune(k) + v := _TSCharactersMap[k] + tw2zhMap[k] = v + zh2twMap[v] = k + } + + // 修正错误的转换(仅简体到繁体) + for k, v := range zh2twMapPatch { + zh2twMap[k] = v + } +} + +var ( + zh2twMap = make(map[rune]rune) + tw2zhMap = make(map[rune]rune) +) + +// 修正错误的转换 +var zh2twMapPatch = map[rune]rune{ + '面': '面', + '发': '發', + '参': '參', + '表': '表', + '同': '同', + '向': '向', + '合': '合', + '针': '針', + '别': '别', + '个': '個', + '家': '家', + '当': '當', + '才': '才', + '出': '出', + '台': '台', + '国': '国', + '获': '獲', + '核': '核', + '糖': '糖', + '须': '須', + '晖': '晖', +} + +var _TSCharactersMap = map[rune]rune{ + '㑮': '𫝈', + '㑯': '㑔', + '㑳': '㑇', + '㑶': '㐹', + '㒓': '𠉂', + '㓄': '𪠟', + '㓨': '刾', + '㔋': '𪟎', + '㖮': '𪠵', + '㗲': '𠵾', + '㗿': '𪡛', + '㘉': '𠰱', + '㘓': '𪢌', + '㘔': '㗷', + '㘚': '㘎', + '㛝': '𫝦', + '㜄': '㚯', + '㜏': '㛣', + '㜐': '𫝧', + '㜗': '𡞋', + '㜢': '𡞱', + '㜷': '𡝠', + '㞞': '𪨊', + '㟺': '𪩇', + '㠏': '㟆', + '㢗': '𪪑', + '㢝': '𢋈', + '㥮': '㤘', + '㦎': '𢛯', + '㦛': '𢗓', + '㦞': '𪫷', + '㨻': '𪮃', + '㩋': '𪮋', + '㩜': '㨫', + '㩳': '㧐', + '㩵': '擜', + '㪎': '𪯋', + '㯤': '𣘐', + '㰙': '𣗙', + '㵗': '𣳆', + '㵾': '𪷍', + '㶆': '𫞛', + '㷍': '𤆢', + '㷿': '𤈷', + '㸇': '𤎺', + '㹽': '𫞣', + '㺏': '𤠋', + '㺜': '𪺻', + '㻶': '𪼋', + '㿖': '𪽮', + '㿗': '𤻊', + '㿧': '𤽯', + '䀉': '𥁢', + '䀹': '𥅴', + '䁪': '𥇢', + '䁻': '䀥', + '䂎': '𥎝', + '䅐': '𫀨', + '䅳': '𫀬', + '䆉': '𫁂', + '䉑': '𫁲', + '䉙': '𥬀', + '䉬': '𫂈', + '䉲': '𥮜', + '䉶': '𫁷', + '䊭': '𥺅', + '䊷': '䌶', + '䊺': '𫄚', + '䋃': '𫄜', + '䋔': '𫄞', + '䋙': '䌺', + '䋚': '䌻', + '䋦': '𫄩', + '䋹': '䌿', + '䋻': '䌾', + '䋼': '𫄮', + '䋿': '𦈓', + '䌈': '𦈖', + '䌋': '𦈘', + '䌖': '𦈜', + '䌝': '𦈟', + '䌟': '𦈞', + '䌥': '𦈠', + '䌰': '𦈙', + '䍤': '𫅅', + '䍦': '䍠', + '䍽': '𦍠', + '䎙': '𫅭', + '䎱': '䎬', + '䕤': '𫟕', + '䕳': '𦰴', + '䖅': '𫟑', + '䗅': '𫊪', + '䗿': '𧉞', + '䙔': '𫋲', + '䙡': '䙌', + '䙱': '𧜭', + '䚩': '𫌯', + '䛄': '𫍠', + '䛳': '𫍫', + '䜀': '䜧', + '䜖': '𫟢', + '䝭': '𫎧', + '䝻': '𧹕', + '䝼': '䞍', + '䞈': '𧹑', + '䞋': '𫎪', + '䞓': '𫎭', + '䟃': '𫎺', + '䟆': '𫎳', + '䟐': '𫎱', + '䠆': '𫏃', + '䠱': '𨅛', + '䡐': '𫟤', + '䡩': '𫟥', + '䡵': '𫟦', + '䢨': '𨑹', + '䤤': '𫟺', + '䥄': '𫠀', + '䥇': '䦂', + '䥗': '𫔋', + '䥩': '𨱖', + '䥯': '𫔆', + '䥱': '䥾', + '䦘': '𨸄', + '䦛': '䦶', + '䦟': '䦷', + '䦯': '𫔵', + '䦳': '𨷿', + '䧢': '𨸟', + '䪊': '𫖅', + '䪏': '𩏼', + '䪗': '𩐀', + '䪘': '𩏿', + '䪴': '𫖫', + '䪾': '𫖬', + '䫀': '𫖱', + '䫂': '𫖰', + '䫟': '𫖲', + '䫴': '𩖗', + '䫶': '𫖺', + '䫻': '𫗇', + '䫾': '𫠈', + '䬓': '𫗊', + '䬘': '𩙮', + '䬝': '𩙯', + '䬞': '𩙧', + '䬧': '𫗟', + '䭀': '𩠇', + '䭃': '𩠈', + '䭑': '𫗱', + '䭔': '𫗰', + '䭿': '𩧭', + '䮄': '𫠊', + '䮝': '𩧰', + '䮞': '𩨁', + '䮠': '𩧿', + '䮫': '𩨇', + '䮰': '𫘮', + '䮳': '𩨏', + '䮾': '𩧪', + '䯀': '䯅', + '䯤': '𩩈', + '䰾': '鲃', + '䱀': '𫚐', + '䱁': '𫚏', + '䱙': '𩾈', + '䱧': '𫚠', + '䱬': '𩾊', + '䱰': '𩾋', + '䱷': '䲣', + '䱸': '𫠑', + '䱽': '䲝', + '䲁': '鳚', + '䲅': '𫚜', + '䲖': '𩾂', + '䲘': '鳤', + '䲰': '𪉂', + '䳜': '𫛬', + '䳢': '𫛰', + '䳤': '𫛮', + '䳧': '𫛺', + '䳫': '𫛼', + '䴉': '鹮', + '䴋': '𫜅', + '䴬': '𪎈', + '䴱': '𫜒', + '䴴': '𪎋', + '䴽': '𫜔', + '䵳': '𪑅', + '䵴': '𫜙', + '䶕': '𫜨', + '䶲': '𫜳', + '万': '万', + '丑': '丑', + '丟': '丢', + '並': '并', + '丰': '丰', + '么': '么', + '乾': '干', + '亂': '乱', + '了': '了', + '于': '于', + '云': '云', + '亙': '亘', + '亞': '亚', + '仆': '仆', + '仇': '仇', + '价': '价', + '仿': '仿', + '伙': '伙', + '佇': '伫', + '佈': '布', + '佔': '占', + '余': '余', + '佛': '佛', + '佣': '佣', + '併': '并', + '來': '来', + '侖': '仑', + '侶': '侣', + '侷': '局', + '俁': '俣', + '係': '系', + '俊': '俊', + '俓': '𠇹', + '俔': '伣', + '俠': '侠', + '俥': '伡', + '俬': '私', + '修': '修', + '倀': '伥', + '倆': '俩', + '倈': '俫', + '倉': '仓', + '個': '个', + '們': '们', + '倖': '幸', + '借': '借', + '倫': '伦', + '倲': '㑈', + '偉': '伟', + '偑': '㐽', + '側': '侧', + '偵': '侦', + '偽': '伪', + '傌': '㐷', + '傑': '杰', + '傖': '伧', + '傘': '伞', + '備': '备', + '傢': '家', + '傭': '佣', + '傯': '偬', + '傳': '传', + '傴': '伛', + '債': '债', + '傷': '伤', + '傾': '倾', + '僂': '偻', + '僅': '仅', + '僉': '佥', + '僑': '侨', + '僕': '仆', + '僞': '伪', + '僥': '侥', + '僨': '偾', + '僱': '雇', + '僵': '僵', + '價': '价', + '儀': '仪', + '儁': '俊', + '儂': '侬', + '億': '亿', + '儈': '侩', + '儉': '俭', + '儎': '傤', + '儐': '傧', + '儔': '俦', + '儕': '侪', + '儘': '尽', + '償': '偿', + '儣': '𠆲', + '優': '优', + '儭': '𠋆', + '儲': '储', + '儷': '俪', + '儸': '㑩', + '儺': '傩', + '儻': '傥', + '儼': '俨', + '兇': '凶', + '克': '克', + '兌': '兑', + '兒': '儿', + '兗': '兖', + '党': '党', + '內': '内', + '兩': '两', + '冊': '册', + '冑': '胄', + '冪': '幂', + '冬': '冬', + '准': '准', + '凈': '净', + '凌': '凌', + '凍': '冻', + '凙': '𪞝', + '凜': '凛', + '几': '几', + '凱': '凯', + '凶': '凶', + '出': '出', + '划': '划', + '別': '别', + '刪': '删', + '刮': '刮', + '制': '制', + '剄': '刭', + '則': '则', + '剋': '克', + '剎': '刹', + '剗': '刬', + '剛': '刚', + '剝': '剥', + '剮': '剐', + '剴': '剀', + '創': '创', + '剷': '铲', + '剾': '𠛅', + '劃': '划', + '劇': '剧', + '劉': '刘', + '劊': '刽', + '劌': '刿', + '劍': '剑', + '劏': '㓥', + '劑': '剂', + '劚': '㔉', + '勁': '劲', + '勑': '𠡠', + '動': '动', + '務': '务', + '勛': '勋', + '勝': '胜', + '勞': '劳', + '勢': '势', + '勣': '𪟝', + '勩': '勚', + '勱': '劢', + '勳': '勋', + '勵': '励', + '勸': '劝', + '勻': '匀', + '匭': '匦', + '匯': '汇', + '匱': '匮', + '區': '区', + '千': '千', + '升': '升', + '協': '协', + '卜': '卜', + '占': '占', + '卷': '卷', + '卹': '恤', + '卻': '却', + '卽': '即', + '厂': '厂', + '厘': '厘', + '厙': '厍', + '厠': '厕', + '厤': '历', + '厭': '厌', + '厲': '厉', + '厴': '厣', + '參': '参', + '叄': '叁', + '叢': '丛', + '只': '只', + '台': '台', + '叶': '叶', + '吁': '吁', + '合': '合', + '吊': '吊', + '同': '同', + '后': '后', + '向': '向', + '吒': '咤', + '吳': '吴', + '吶': '呐', + '呂': '吕', + '周': '周', + '咨': '咨', + '咸': '咸', + '咼': '呙', + '咽': '咽', + '哄': '哄', + '員': '员', + '哯': '𠯟', + '唄': '呗', + '唓': '𪠳', + '唚': '吣', + '唸': '念', + '問': '问', + '啓': '启', + '啞': '哑', + '啟': '启', + '啢': '唡', + '喂': '喂', + '喎': '㖞', + '喚': '唤', + '喪': '丧', + '喫': '吃', + '喬': '乔', + '單': '单', + '喲': '哟', + '嗆': '呛', + '嗇': '啬', + '嗊': '唝', + '嗎': '吗', + '嗚': '呜', + '嗩': '唢', + '嗰': '𠮶', + '嗶': '哔', + '嗹': '𪡏', + '嘆': '叹', + '嘍': '喽', + '嘓': '啯', + '嘔': '呕', + '嘖': '啧', + '嘗': '尝', + '嘜': '唛', + '嘩': '哗', + '嘪': '𪡃', + '嘮': '唠', + '嘯': '啸', + '嘰': '叽', + '嘳': '𪡞', + '嘵': '哓', + '嘸': '呒', + '嘺': '𪡀', + '嘽': '啴', + '噁': '恶', + '噅': '𠯠', + '噓': '嘘', + '噚': '㖊', + '噝': '咝', + '噞': '𪡋', + '噠': '哒', + '噥': '哝', + '噦': '哕', + '噪': '噪', + '噯': '嗳', + '噲': '哙', + '噴': '喷', + '噸': '吨', + '噹': '当', + '嚀': '咛', + '嚇': '吓', + '嚌': '哜', + '嚐': '尝', + '嚕': '噜', + '嚙': '啮', + '嚛': '𪠸', + '嚥': '咽', + '嚦': '呖', + '嚧': '𠰷', + '嚨': '咙', + '嚮': '向', + '嚲': '亸', + '嚳': '喾', + '嚴': '严', + '嚶': '嘤', + '嚽': '𪢕', + '囀': '啭', + '囁': '嗫', + '囂': '嚣', + '囃': '𠱞', + '囅': '冁', + '囈': '呓', + '囉': '啰', + '囌': '苏', + '囑': '嘱', + '囒': '𪢠', + '回': '回', + '囪': '囱', + '困': '困', + '圇': '囵', + '國': '国', + '圍': '围', + '園': '园', + '圓': '圆', + '圖': '图', + '團': '团', + '圞': '𪢮', + '坐': '坐', + '垵': '埯', + '埡': '垭', + '埬': '𪣆', + '埰': '采', + '執': '执', + '堅': '坚', + '堊': '垩', + '堖': '垴', + '堚': '𪣒', + '堝': '埚', + '堯': '尧', + '報': '报', + '場': '场', + '塊': '块', + '塋': '茔', + '塏': '垲', + '塒': '埘', + '塗': '涂', + '塚': '冢', + '塢': '坞', + '塤': '埙', + '塵': '尘', + '塹': '堑', + '塿': '𪣻', + '墊': '垫', + '墜': '坠', + '墮': '堕', + '墰': '坛', + '墲': '𪢸', + '墳': '坟', + '墶': '垯', + '墻': '墙', + '墾': '垦', + '壇': '坛', + '壈': '𡒄', + '壋': '垱', + '壎': '埙', + '壓': '压', + '壗': '𡋤', + '壘': '垒', + '壙': '圹', + '壚': '垆', + '壜': '坛', + '壞': '坏', + '壟': '垄', + '壠': '垅', + '壢': '坜', + '壣': '𪤚', + '壩': '坝', + '壪': '塆', + '壯': '壮', + '壺': '壶', + '壼': '壸', + '壽': '寿', + '夠': '够', + '夢': '梦', + '夥': '伙', + '夸': '夸', + '夾': '夹', + '奐': '奂', + '奧': '奥', + '奩': '奁', + '奪': '夺', + '奬': '奖', + '奮': '奋', + '奸': '奸', + '奼': '姹', + '妝': '妆', + '姍': '姗', + '姜': '姜', + '姦': '奸', + '娘': '娘', + '娛': '娱', + '婁': '娄', + '婡': '𫝫', + '婦': '妇', + '婭': '娅', + '媈': '𫝨', + '媧': '娲', + '媯': '妫', + '媰': '㛀', + '媼': '媪', + '媽': '妈', + '嫋': '袅', + '嫗': '妪', + '嫵': '妩', + '嫺': '娴', + '嫻': '娴', + '嫿': '婳', + '嬀': '妫', + '嬃': '媭', + '嬇': '𫝬', + '嬈': '娆', + '嬋': '婵', + '嬌': '娇', + '嬙': '嫱', + '嬡': '嫒', + '嬣': '𪥰', + '嬤': '嬷', + '嬦': '𫝩', + '嬪': '嫔', + '嬰': '婴', + '嬸': '婶', + '嬻': '𪥿', + '孃': '娘', + '孄': '𫝮', + '孆': '𫝭', + '孇': '𪥫', + '孋': '㛤', + '孌': '娈', + '孎': '𡠟', + '孫': '孙', + '學': '学', + '孻': '𡥧', + '孾': '𪧀', + '孿': '孪', + '宮': '宫', + '家': '家', + '寀': '采', + '寠': '𪧘', + '寢': '寝', + '實': '实', + '寧': '宁', + '審': '审', + '寫': '写', + '寬': '宽', + '寵': '宠', + '寶': '宝', + '將': '将', + '專': '专', + '尋': '寻', + '對': '对', + '導': '导', + '尷': '尴', + '尸': '尸', + '局': '局', + '屆': '届', + '屍': '尸', + '屓': '屃', + '屜': '屉', + '屢': '屡', + '層': '层', + '屨': '屦', + '屩': '𪨗', + '屬': '属', + '岡': '冈', + '岩': '岩', + '峯': '峰', + '峴': '岘', + '島': '岛', + '峽': '峡', + '崍': '崃', + '崑': '昆', + '崗': '岗', + '崙': '仑', + '崢': '峥', + '崬': '岽', + '嵐': '岚', + '嵗': '岁', + '嵼': '𡶴', + '嵾': '㟥', + '嶁': '嵝', + '嶄': '崭', + '嶇': '岖', + '嶈': '𡺃', + '嶔': '嵚', + '嶗': '崂', + '嶘': '𡺄', + '嶠': '峤', + '嶢': '峣', + '嶧': '峄', + '嶨': '峃', + '嶮': '崄', + '嶴': '岙', + '嶸': '嵘', + '嶹': '𫝵', + '嶺': '岭', + '嶼': '屿', + '嶽': '岳', + '巊': '𪩎', + '巋': '岿', + '巒': '峦', + '巔': '巅', + '巖': '岩', + '巗': '𪨷', + '巘': '𪩘', + '巨': '巨', + '巰': '巯', + '巹': '卺', + '布': '布', + '帘': '帘', + '帥': '帅', + '師': '师', + '席': '席', + '帳': '帐', + '帶': '带', + '幀': '帧', + '幃': '帏', + '幓': '㡎', + '幗': '帼', + '幘': '帻', + '幝': '𪩷', + '幟': '帜', + '幣': '币', + '幩': '𪩸', + '幫': '帮', + '幬': '帱', + '干': '干', + '幸': '幸', + '幹': '干', + '幺': '幺', + '幾': '几', + '广': '广', + '座': '座', + '庫': '库', + '庵': '庵', + '廁': '厕', + '廂': '厢', + '廄': '厩', + '廈': '厦', + '廎': '庼', + '廕': '荫', + '廚': '厨', + '廝': '厮', + '廟': '庙', + '廠': '厂', + '廡': '庑', + '廢': '废', + '廣': '广', + '廧': '𪪞', + '廩': '廪', + '廬': '庐', + '廳': '厅', + '弒': '弑', + '弔': '吊', + '弦': '弦', + '弳': '弪', + '張': '张', + '強': '强', + '彃': '𪪼', + '彆': '别', + '彈': '弹', + '彌': '弥', + '彎': '弯', + '彔': '录', + '彙': '汇', + '彞': '彝', + '彠': '彟', + '彥': '彦', + '彩': '彩', + '彫': '雕', + '彲': '彨', + '彷': '彷', + '彿': '佛', + '征': '征', + '後': '后', + '徑': '径', + '從': '从', + '徠': '徕', + '御': '御', + '復': '复', + '徵': '征', + '徹': '彻', + '徿': '𪫌', + '志': '志', + '念': '念', + '恆': '恒', + '恥': '耻', + '悅': '悦', + '悞': '悮', + '悵': '怅', + '悶': '闷', + '悽': '凄', + '惡': '恶', + '惱': '恼', + '惲': '恽', + '惻': '恻', + '愈': '愈', + '愛': '爱', + '愜': '惬', + '愨': '悫', + '愴': '怆', + '愷': '恺', + '愻': '𢙏', + '愾': '忾', + '愿': '愿', + '慄': '栗', + '態': '态', + '慍': '愠', + '慘': '惨', + '慚': '惭', + '慟': '恸', + '慣': '惯', + '慤': '悫', + '慪': '怄', + '慫': '怂', + '慮': '虑', + '慳': '悭', + '慶': '庆', + '慺': '㥪', + '慼': '戚', + '慾': '欲', + '憂': '忧', + '憊': '惫', + '憐': '怜', + '憑': '凭', + '憒': '愦', + '憖': '慭', + '憚': '惮', + '憢': '𢙒', + '憤': '愤', + '憫': '悯', + '憮': '怃', + '憲': '宪', + '憶': '忆', + '憸': '𪫺', + '憹': '𢙐', + '懀': '𢙓', + '懇': '恳', + '應': '应', + '懌': '怿', + '懍': '懔', + '懎': '𢠁', + '懞': '蒙', + '懟': '怼', + '懣': '懑', + '懤': '㤽', + '懨': '恹', + '懲': '惩', + '懶': '懒', + '懷': '怀', + '懸': '悬', + '懺': '忏', + '懼': '惧', + '懾': '慑', + '戀': '恋', + '戇': '戆', + '戔': '戋', + '戚': '戚', + '戧': '戗', + '戩': '戬', + '戰': '战', + '戱': '戯', + '戲': '戏', + '戶': '户', + '才': '才', + '扎': '扎', + '托': '托', + '扣': '扣', + '折': '折', + '拋': '抛', + '拐': '拐', + '拚': '拚', + '挂': '挂', + '挨': '挨', + '挩': '捝', + '挱': '挲', + '挽': '挽', + '挾': '挟', + '捨': '舍', + '捫': '扪', + '据': '据', + '捱': '挨', + '捲': '卷', + '掃': '扫', + '掄': '抡', + '掆': '㧏', + '掗': '挜', + '掙': '挣', + '掚': '𪭵', + '掛': '挂', + '採': '采', + '揀': '拣', + '揚': '扬', + '換': '换', + '揮': '挥', + '揯': '搄', + '損': '损', + '搖': '摇', + '搗': '捣', + '搜': '搜', + '搵': '揾', + '搶': '抢', + '摋': '𢫬', + '摐': '𪭢', + '摑': '掴', + '摜': '掼', + '摟': '搂', + '摯': '挚', + '摳': '抠', + '摶': '抟', + '摺': '折', + '摻': '掺', + '撈': '捞', + '撊': '𪭾', + '撏': '挦', + '撐': '撑', + '撓': '挠', + '撝': '㧑', + '撟': '挢', + '撣': '掸', + '撥': '拨', + '撧': '𪮖', + '撫': '抚', + '撲': '扑', + '撳': '揿', + '撻': '挞', + '撾': '挝', + '撿': '捡', + '擁': '拥', + '擄': '掳', + '擇': '择', + '擊': '击', + '擋': '挡', + '擓': '㧟', + '擔': '担', + '據': '据', + '擟': '𪭧', + '擠': '挤', + '擡': '抬', + '擣': '捣', + '擫': '𢬍', + '擬': '拟', + '擯': '摈', + '擰': '拧', + '擱': '搁', + '擲': '掷', + '擴': '扩', + '擷': '撷', + '擺': '摆', + '擻': '擞', + '擼': '撸', + '擽': '㧰', + '擾': '扰', + '攄': '摅', + '攆': '撵', + '攋': '𪮶', + '攏': '拢', + '攔': '拦', + '攖': '撄', + '攙': '搀', + '攛': '撺', + '攜': '携', + '攝': '摄', + '攢': '攒', + '攣': '挛', + '攤': '摊', + '攪': '搅', + '攬': '揽', + '敎': '教', + '敓': '敚', + '敗': '败', + '敘': '叙', + '敵': '敌', + '數': '数', + '斂': '敛', + '斃': '毙', + '斅': '𢽾', + '斆': '敩', + '斕': '斓', + '斗': '斗', + '斬': '斩', + '斷': '断', + '斸': '𣃁', + '於': '于', + '旂': '旗', + '旣': '既', + '昆': '昆', + '昇': '升', + '時': '时', + '晉': '晋', + '晝': '昼', + '暈': '晕', + '暉': '晖', + '暗': '暗', + '暘': '旸', + '暢': '畅', + '暫': '暂', + '曄': '晔', + '曆': '历', + '曇': '昙', + '曉': '晓', + '曊': '𪰶', + '曏': '向', + '曖': '暧', + '曠': '旷', + '曥': '𣆐', + '曨': '昽', + '曬': '晒', + '曲': '曲', + '書': '书', + '會': '会', + '朥': '𦛨', + '朧': '胧', + '朮': '术', + '朱': '朱', + '朴': '朴', + '杆': '杆', + '杠': '杠', + '杯': '杯', + '杰': '杰', + '東': '东', + '杴': '锨', + '松': '松', + '板': '板', + '极': '极', + '枴': '拐', + '柜': '柜', + '柵': '栅', + '柺': '拐', + '査': '查', + '栗': '栗', + '核': '核', + '桱': '𣐕', + '桿': '杆', + '梁': '梁', + '梔': '栀', + '梖': '𪱷', + '梘': '枧', + '條': '条', + '梟': '枭', + '梲': '棁', + '棄': '弃', + '棊': '棋', + '棖': '枨', + '棗': '枣', + '棟': '栋', + '棡': '㭎', + '棧': '栈', + '棱': '棱', + '棲': '栖', + '棶': '梾', + '椏': '桠', + '椲': '㭏', + '楇': '𣒌', + '楊': '杨', + '楓': '枫', + '楨': '桢', + '業': '业', + '極': '极', + '榘': '矩', + '榦': '干', + '榪': '杩', + '榮': '荣', + '榲': '榅', + '榿': '桤', + '構': '构', + '槍': '枪', + '槓': '杠', + '槤': '梿', + '槧': '椠', + '槨': '椁', + '槫': '𣏢', + '槮': '椮', + '槳': '桨', + '槶': '椢', + '槼': '椝', + '樁': '桩', + '樂': '乐', + '樅': '枞', + '樑': '梁', + '樓': '楼', + '標': '标', + '樞': '枢', + '樠': '𣗊', + '樢': '㭤', + '樣': '样', + '樤': '𣔌', + '樧': '榝', + '樫': '㭴', + '樳': '桪', + '樸': '朴', + '樹': '树', + '樺': '桦', + '樿': '椫', + '橈': '桡', + '橋': '桥', + '機': '机', + '橢': '椭', + '橫': '横', + '橯': '𣓿', + '檁': '檩', + '檉': '柽', + '檔': '档', + '檜': '桧', + '檟': '槚', + '檢': '检', + '檣': '樯', + '檭': '𣘴', + '檮': '梼', + '檯': '台', + '檳': '槟', + '檵': '𪲛', + '檸': '柠', + '檻': '槛', + '檾': '𦼖', + '櫃': '柜', + '櫅': '𪲎', + '櫓': '橹', + '櫚': '榈', + '櫛': '栉', + '櫝': '椟', + '櫞': '橼', + '櫟': '栎', + '櫠': '𪲮', + '櫥': '橱', + '櫧': '槠', + '櫨': '栌', + '櫪': '枥', + '櫫': '橥', + '櫬': '榇', + '櫱': '蘖', + '櫳': '栊', + '櫸': '榉', + '櫺': '棂', + '櫻': '樱', + '欄': '栏', + '欅': '榉', + '欇': '𪳍', + '權': '权', + '欍': '𣐤', + '欏': '椤', + '欐': '𪲔', + '欑': '𪴙', + '欒': '栾', + '欓': '𣗋', + '欖': '榄', + '欘': '𣚚', + '欞': '棂', + '欲': '欲', + '欽': '钦', + '歎': '叹', + '歐': '欧', + '歟': '欤', + '歡': '欢', + '歲': '岁', + '歷': '历', + '歸': '归', + '歿': '殁', + '殘': '残', + '殞': '殒', + '殢': '𣨼', + '殤': '殇', + '殨': '㱮', + '殫': '殚', + '殭': '僵', + '殮': '殓', + '殯': '殡', + '殰': '㱩', + '殲': '歼', + '殺': '杀', + '殻': '壳', + '殼': '壳', + '毀': '毁', + '毆': '殴', + '毊': '𪵑', + '毿': '毵', + '氂': '牦', + '氈': '毡', + '氌': '氇', + '氣': '气', + '氫': '氢', + '氬': '氩', + '氭': '𣱝', + '氳': '氲', + '氾': '泛', + '汎': '泛', + '汙': '污', + '決': '决', + '沈': '沈', + '沒': '没', + '沖': '冲', + '況': '况', + '泛': '泛', + '泝': '溯', + '注': '注', + '洩': '泄', + '洶': '汹', + '浹': '浃', + '涂': '涂', + '涇': '泾', + '涌': '涌', + '涗': '涚', + '涼': '凉', + '淀': '淀', + '淒': '凄', + '淚': '泪', + '淥': '渌', + '淨': '净', + '淩': '凌', + '淪': '沦', + '淵': '渊', + '淶': '涞', + '淺': '浅', + '渙': '涣', + '減': '减', + '渢': '沨', + '渦': '涡', + '測': '测', + '游': '游', + '渾': '浑', + '湊': '凑', + '湋': '𣲗', + '湞': '浈', + '湧': '涌', + '湯': '汤', + '溈': '沩', + '準': '准', + '溝': '沟', + '溡': '𪶄', + '溫': '温', + '溮': '浉', + '溳': '涢', + '溼': '湿', + '滄': '沧', + '滅': '灭', + '滌': '涤', + '滎': '荥', + '滙': '汇', + '滬': '沪', + '滯': '滞', + '滲': '渗', + '滷': '卤', + '滸': '浒', + '滻': '浐', + '滾': '滚', + '滿': '满', + '漁': '渔', + '漊': '溇', + '漓': '漓', + '漚': '沤', + '漢': '汉', + '漣': '涟', + '漬': '渍', + '漲': '涨', + '漵': '溆', + '漸': '渐', + '漿': '浆', + '潁': '颍', + '潑': '泼', + '潔': '洁', + '潕': '𣲘', + '潙': '沩', + '潚': '㴋', + '潛': '潜', + '潣': '𫞗', + '潤': '润', + '潯': '浔', + '潰': '溃', + '潷': '滗', + '潿': '涠', + '澀': '涩', + '澅': '𣶩', + '澆': '浇', + '澇': '涝', + '澐': '沄', + '澗': '涧', + '澠': '渑', + '澤': '泽', + '澦': '滪', + '澩': '泶', + '澬': '𫞚', + '澮': '浍', + '澱': '淀', + '澾': '㳠', + '濁': '浊', + '濃': '浓', + '濄': '㳡', + '濆': '𣸣', + '濕': '湿', + '濘': '泞', + '濚': '溁', + '濛': '蒙', + '濜': '浕', + '濟': '济', + '濤': '涛', + '濧': '㳔', + '濫': '滥', + '濰': '潍', + '濱': '滨', + '濺': '溅', + '濼': '泺', + '濾': '滤', + '濿': '𪵱', + '瀂': '澛', + '瀃': '𣽷', + '瀅': '滢', + '瀆': '渎', + '瀇': '㲿', + '瀉': '泻', + '瀋': '沈', + '瀏': '浏', + '瀕': '濒', + '瀘': '泸', + '瀝': '沥', + '瀟': '潇', + '瀠': '潆', + '瀦': '潴', + '瀧': '泷', + '瀨': '濑', + '瀰': '弥', + '瀲': '潋', + '瀾': '澜', + '灃': '沣', + '灄': '滠', + '灍': '𫞝', + '灑': '洒', + '灒': '𪷽', + '灕': '漓', + '灘': '滩', + '灙': '𣺼', + '灝': '灏', + '灡': '㳕', + '灣': '湾', + '灤': '滦', + '灧': '滟', + '灩': '滟', + '災': '灾', + '為': '为', + '烏': '乌', + '烴': '烃', + '無': '无', + '煇': '𪸩', + '煉': '炼', + '煒': '炜', + '煙': '烟', + '煢': '茕', + '煥': '焕', + '煩': '烦', + '煬': '炀', + '煱': '㶽', + '熂': '𪸕', + '熅': '煴', + '熉': '𤈶', + '熌': '𤇄', + '熏': '熏', + '熒': '荧', + '熓': '𤆡', + '熗': '炝', + '熚': '𤇹', + '熡': '𤋏', + '熱': '热', + '熲': '颎', + '熾': '炽', + '燁': '烨', + '燈': '灯', + '燉': '炖', + '燒': '烧', + '燙': '烫', + '燜': '焖', + '營': '营', + '燦': '灿', + '燬': '毁', + '燭': '烛', + '燴': '烩', + '燶': '㶶', + '燻': '熏', + '燼': '烬', + '燾': '焘', + '爃': '𫞡', + '爄': '𤇃', + '爇': '𦶟', + '爍': '烁', + '爐': '炉', + '爖': '𤇭', + '爛': '烂', + '爥': '𪹳', + '爧': '𫞠', + '爭': '争', + '爲': '为', + '爺': '爷', + '爾': '尔', + '牀': '床', + '牆': '墙', + '牘': '牍', + '牴': '牴', + '牽': '牵', + '犖': '荦', + '犛': '牦', + '犞': '𪺭', + '犢': '犊', + '犧': '牺', + '狀': '状', + '狹': '狭', + '狽': '狈', + '猌': '𪺽', + '猙': '狰', + '猶': '犹', + '猻': '狲', + '獁': '犸', + '獃': '呆', + '獄': '狱', + '獅': '狮', + '獊': '𪺷', + '獎': '奖', + '獨': '独', + '獩': '𤞃', + '獪': '狯', + '獫': '猃', + '獮': '狝', + '獰': '狞', + '獱': '㺍', + '獲': '获', + '獵': '猎', + '獷': '犷', + '獸': '兽', + '獺': '獭', + '獻': '献', + '獼': '猕', + '玀': '猡', + '玁': '𤞤', + '珼': '𫞥', + '現': '现', + '琱': '雕', + '琺': '珐', + '琿': '珲', + '瑋': '玮', + '瑒': '玚', + '瑣': '琐', + '瑤': '瑶', + '瑩': '莹', + '瑪': '玛', + '瑲': '玱', + '瑻': '𪻲', + '瑽': '𪻐', + '璉': '琏', + '璊': '𫞩', + '璝': '𪻺', + '璡': '琎', + '璣': '玑', + '璦': '瑷', + '璫': '珰', + '璯': '㻅', + '環': '环', + '璵': '玙', + '璸': '瑸', + '璼': '𫞨', + '璽': '玺', + '璾': '𫞦', + '瓄': '𪻨', + '瓊': '琼', + '瓏': '珑', + '瓔': '璎', + '瓕': '𤦀', + '瓚': '瓒', + '瓛': '𤩽', + '瓮': '瓮', + '甌': '瓯', + '甕': '瓮', + '產': '产', + '産': '产', + '甦': '苏', + '甯': '宁', + '畝': '亩', + '畢': '毕', + '畫': '画', + '異': '异', + '畵': '画', + '當': '当', + '畼': '𪽈', + '疇': '畴', + '疊': '叠', + '症': '症', + '痙': '痉', + '痠': '酸', + '痮': '𪽪', + '痾': '疴', + '瘂': '痖', + '瘋': '疯', + '瘍': '疡', + '瘓': '痪', + '瘞': '瘗', + '瘡': '疮', + '瘧': '疟', + '瘮': '瘆', + '瘱': '𪽷', + '瘲': '疭', + '瘺': '瘘', + '瘻': '瘘', + '療': '疗', + '癆': '痨', + '癇': '痫', + '癉': '瘅', + '癐': '𤶊', + '癒': '愈', + '癘': '疠', + '癟': '瘪', + '癡': '痴', + '癢': '痒', + '癤': '疖', + '癥': '症', + '癧': '疬', + '癩': '癞', + '癬': '癣', + '癭': '瘿', + '癮': '瘾', + '癰': '痈', + '癱': '瘫', + '癲': '癫', + '發': '发', + '皁': '皂', + '皚': '皑', + '皟': '𤾀', + '皰': '疱', + '皸': '皲', + '皺': '皱', + '盃': '杯', + '盜': '盗', + '盞': '盏', + '盡': '尽', + '監': '监', + '盤': '盘', + '盧': '卢', + '盨': '𪾔', + '盪': '荡', + '眝': '𪾣', + '眞': '真', + '眥': '眦', + '眾': '众', + '睍': '𪾢', + '睏': '困', + '睜': '睁', + '睞': '睐', + '睪': '睾', + '瞘': '眍', + '瞜': '䁖', + '瞞': '瞒', + '瞤': '𥆧', + '瞭': '瞭', + '瞶': '瞆', + '瞼': '睑', + '矇': '蒙', + '矉': '𪾸', + '矑': '𪾦', + '矓': '眬', + '矚': '瞩', + '矩': '矩', + '矯': '矫', + '硃': '朱', + '硜': '硁', + '硤': '硖', + '硨': '砗', + '确': '确', + '硯': '砚', + '碕': '埼', + '碙': '𥐻', + '碩': '硕', + '碭': '砀', + '碸': '砜', + '確': '确', + '碼': '码', + '碽': '䂵', + '磑': '硙', + '磚': '砖', + '磠': '硵', + '磣': '碜', + '磧': '碛', + '磯': '矶', + '磽': '硗', + '磾': '䃅', + '礄': '硚', + '礆': '硷', + '礎': '础', + '礒': '𥐟', + '礙': '碍', + '礦': '矿', + '礪': '砺', + '礫': '砾', + '礬': '矾', + '礮': '𪿫', + '礱': '砻', + '祇': '祇', + '祕': '秘', + '祘': '祘', + '祿': '禄', + '禍': '祸', + '禎': '祯', + '禕': '祎', + '禡': '祃', + '禦': '御', + '禪': '禅', + '禮': '礼', + '禰': '祢', + '禱': '祷', + '禿': '秃', + '私': '私', + '秈': '籼', + '秋': '秋', + '种': '种', + '稅': '税', + '稈': '秆', + '稏': '䅉', + '稜': '棱', + '稟': '禀', + '種': '种', + '稱': '称', + '穀': '谷', + '穇': '䅟', + '穌': '稣', + '積': '积', + '穎': '颖', + '穗': '穗', + '穠': '秾', + '穡': '穑', + '穢': '秽', + '穩': '稳', + '穫': '获', + '穭': '穞', + '窩': '窝', + '窪': '洼', + '窮': '穷', + '窯': '窑', + '窵': '窎', + '窶': '窭', + '窺': '窥', + '竄': '窜', + '竅': '窍', + '竇': '窦', + '竈': '灶', + '竊': '窃', + '竚': '𥩟', + '竪': '竖', + '竱': '𫁟', + '競': '竞', + '筆': '笔', + '筍': '笋', + '筑': '筑', + '筧': '笕', + '筴': '䇲', + '箇': '个', + '箋': '笺', + '箏': '筝', + '節': '节', + '範': '范', + '築': '筑', + '篋': '箧', + '篔': '筼', + '篘': '𥬠', + '篠': '筿', + '篤': '笃', + '篩': '筛', + '篳': '筚', + '篸': '𥮾', + '簀': '箦', + '簂': '𫂆', + '簍': '篓', + '簑': '蓑', + '簞': '箪', + '簡': '简', + '簢': '𫂃', + '簣': '篑', + '簫': '箫', + '簹': '筜', + '簽': '签', + '簾': '帘', + '籃': '篮', + '籅': '𥫣', + '籋': '𥬞', + '籌': '筹', + '籔': '䉤', + '籙': '箓', + '籛': '篯', + '籜': '箨', + '籟': '籁', + '籠': '笼', + '籤': '签', + '籩': '笾', + '籪': '簖', + '籬': '篱', + '籮': '箩', + '籲': '吁', + '粵': '粤', + '糉': '粽', + '糝': '糁', + '糞': '粪', + '糧': '粮', + '糰': '团', + '糲': '粝', + '糴': '籴', + '糶': '粜', + '糹': '纟', + '糺': '𫄙', + '系': '系', + '糾': '纠', + '紀': '纪', + '紂': '纣', + '約': '约', + '紅': '红', + '紆': '纡', + '紇': '纥', + '紈': '纨', + '紉': '纫', + '紋': '纹', + '納': '纳', + '紐': '纽', + '紓': '纾', + '純': '纯', + '紕': '纰', + '紖': '纼', + '紗': '纱', + '紘': '纮', + '紙': '纸', + '級': '级', + '紛': '纷', + '紜': '纭', + '紝': '纴', + '紟': '𫄛', + '紡': '纺', + '紬': '䌷', + '紮': '扎', + '累': '累', + '細': '细', + '紱': '绂', + '紲': '绁', + '紳': '绅', + '紵': '纻', + '紹': '绍', + '紺': '绀', + '紼': '绋', + '紿': '绐', + '絀': '绌', + '絁': '𫄟', + '終': '终', + '絃': '弦', + '組': '组', + '絅': '䌹', + '絆': '绊', + '絍': '𫟃', + '絎': '绗', + '結': '结', + '絕': '绝', + '絙': '𫄠', + '絛': '绦', + '絝': '绔', + '絞': '绞', + '絡': '络', + '絢': '绚', + '絥': '𫄢', + '給': '给', + '絧': '𫄡', + '絨': '绒', + '絰': '绖', + '統': '统', + '絲': '丝', + '絳': '绛', + '絶': '绝', + '絹': '绢', + '絺': '𫄨', + '綀': '𦈌', + '綁': '绑', + '綃': '绡', + '綆': '绠', + '綇': '𦈋', + '綈': '绨', + '綉': '绣', + '綋': '𫟄', + '綌': '绤', + '綏': '绥', + '綐': '䌼', + '綑': '捆', + '經': '经', + '綖': '𫄧', + '綜': '综', + '綞': '缍', + '綟': '𫄫', + '綠': '绿', + '綡': '𫟅', + '綢': '绸', + '綣': '绻', + '綫': '线', + '綬': '绶', + '維': '维', + '綯': '绹', + '綰': '绾', + '綱': '纲', + '網': '网', + '綳': '绷', + '綴': '缀', + '綵': '彩', + '綸': '纶', + '綹': '绺', + '綺': '绮', + '綻': '绽', + '綽': '绰', + '綾': '绫', + '綿': '绵', + '緄': '绲', + '緇': '缁', + '緊': '紧', + '緋': '绯', + '緍': '𦈏', + '緑': '绿', + '緒': '绪', + '緓': '绬', + '緔': '绱', + '緗': '缃', + '緘': '缄', + '緙': '缂', + '線': '线', + '緝': '缉', + '緞': '缎', + '緟': '𫟆', + '締': '缔', + '緡': '缗', + '緣': '缘', + '緤': '𫄬', + '緦': '缌', + '編': '编', + '緩': '缓', + '緬': '缅', + '緮': '𫄭', + '緯': '纬', + '緰': '𦈕', + '緱': '缑', + '緲': '缈', + '練': '练', + '緶': '缏', + '緷': '𦈉', + '緸': '𦈑', + '緹': '缇', + '緻': '致', + '緼': '缊', + '縈': '萦', + '縉': '缙', + '縊': '缢', + '縋': '缒', + '縍': '𫄰', + '縎': '𦈔', + '縐': '绉', + '縑': '缣', + '縕': '缊', + '縗': '缞', + '縛': '缚', + '縝': '缜', + '縞': '缟', + '縟': '缛', + '縣': '县', + '縧': '绦', + '縫': '缝', + '縬': '𦈚', + '縭': '缡', + '縮': '缩', + '縰': '𫄳', + '縱': '纵', + '縲': '缧', + '縳': '䌸', + '縴': '纤', + '縵': '缦', + '縶': '絷', + '縷': '缕', + '縸': '𫄲', + '縹': '缥', + '縺': '𦈐', + '總': '总', + '績': '绩', + '繂': '𫄴', + '繃': '绷', + '繅': '缫', + '繆': '缪', + '繈': '𫄶', + '繏': '𦈝', + '繐': '穗', + '繒': '缯', + '繓': '𦈛', + '織': '织', + '繕': '缮', + '繚': '缭', + '繞': '绕', + '繟': '𦈎', + '繡': '绣', + '繢': '缋', + '繨': '𫄤', + '繩': '绳', + '繪': '绘', + '繫': '系', + '繬': '𫄱', + '繭': '茧', + '繮': '缰', + '繯': '缳', + '繰': '缲', + '繳': '缴', + '繶': '𫄷', + '繷': '𫄣', + '繸': '䍁', + '繹': '绎', + '繻': '𦈡', + '繼': '继', + '繽': '缤', + '繾': '缱', + '繿': '䍀', + '纁': '𫄸', + '纇': '颣', + '纈': '缬', + '纊': '纩', + '續': '续', + '纍': '累', + '纏': '缠', + '纓': '缨', + '纔': '才', + '纖': '纤', + '纗': '𫄹', + '纘': '缵', + '纚': '𫄥', + '纜': '缆', + '缽': '钵', + '罃': '䓨', + '罈': '坛', + '罌': '罂', + '罎': '坛', + '罰': '罚', + '罵': '骂', + '罷': '罢', + '羅': '罗', + '羆': '罴', + '羈': '羁', + '羋': '芈', + '羣': '群', + '羥': '羟', + '羨': '羡', + '義': '义', + '羵': '𫅗', + '羶': '膻', + '習': '习', + '翬': '翚', + '翹': '翘', + '翽': '翙', + '耬': '耧', + '耮': '耢', + '聖': '圣', + '聞': '闻', + '聯': '联', + '聰': '聪', + '聲': '声', + '聳': '耸', + '聵': '聩', + '聶': '聂', + '職': '职', + '聹': '聍', + '聻': '𫆏', + '聽': '听', + '聾': '聋', + '肅': '肃', + '肴': '肴', + '胜': '胜', + '胡': '胡', + '脅': '胁', + '脈': '脉', + '脛': '胫', + '脣': '唇', + '脥': '𣍰', + '脩': '修', + '脫': '脱', + '脹': '胀', + '腊': '腊', + '腌': '腌', + '腎': '肾', + '腖': '胨', + '腡': '脶', + '腦': '脑', + '腪': '𣍯', + '腫': '肿', + '腳': '脚', + '腸': '肠', + '膃': '腽', + '膕': '腘', + '膚': '肤', + '膞': '䏝', + '膠': '胶', + '膢': '𦝼', + '膩': '腻', + '膹': '𪱥', + '膽': '胆', + '膾': '脍', + '膿': '脓', + '臉': '脸', + '臍': '脐', + '臏': '膑', + '臗': '𣎑', + '臘': '腊', + '臚': '胪', + '臟': '脏', + '臠': '脔', + '臢': '臜', + '臥': '卧', + '臨': '临', + '致': '致', + '臺': '台', + '與': '与', + '興': '兴', + '舉': '举', + '舊': '旧', + '舍': '舍', + '舘': '馆', + '艙': '舱', + '艣': '𫇛', + '艤': '舣', + '艦': '舰', + '艫': '舻', + '艱': '艰', + '艷': '艳', + '芻': '刍', + '苧': '苎', + '苹': '苹', + '范': '范', + '茲': '兹', + '荊': '荆', + '荐': '荐', + '莊': '庄', + '莖': '茎', + '莢': '荚', + '莧': '苋', + '菕': '芲', + '華': '华', + '菴': '庵', + '菸': '烟', + '萇': '苌', + '萊': '莱', + '萬': '万', + '萴': '荝', + '萵': '莴', + '葉': '叶', + '葒': '荭', + '著': '著', + '葝': '𫈎', + '葤': '荮', + '葦': '苇', + '葯': '药', + '葷': '荤', + '蒍': '𫇭', + '蒐': '搜', + '蒓': '莼', + '蒔': '莳', + '蒕': '蒀', + '蒙': '蒙', + '蒞': '莅', + '蒭': '𫇴', + '蒼': '苍', + '蓀': '荪', + '蓆': '席', + '蓋': '盖', + '蓧': '𦰏', + '蓮': '莲', + '蓯': '苁', + '蓴': '莼', + '蓽': '荜', + '蔑': '蔑', + '蔔': '卜', + '蔘': '参', + '蔞': '蒌', + '蔣': '蒋', + '蔥': '葱', + '蔦': '茑', + '蔭': '荫', + '蔯': '𫈟', + '蔿': '𫇭', + '蕁': '荨', + '蕆': '蒇', + '蕎': '荞', + '蕒': '荬', + '蕓': '芸', + '蕕': '莸', + '蕘': '荛', + '蕝': '𫈵', + '蕢': '蒉', + '蕩': '荡', + '蕪': '芜', + '蕭': '萧', + '蕳': '𫈉', + '蕷': '蓣', + '蕽': '𫇽', + '薀': '蕰', + '薆': '𫉁', + '薈': '荟', + '薊': '蓟', + '薌': '芗', + '薑': '姜', + '薔': '蔷', + '薘': '荙', + '薟': '莶', + '薦': '荐', + '薩': '萨', + '薰': '薰', + '薳': '䓕', + '薴': '苧', + '薵': '䓓', + '薹': '苔', + '薺': '荠', + '藉': '藉', + '藍': '蓝', + '藎': '荩', + '藝': '艺', + '藥': '药', + '藪': '薮', + '藭': '䓖', + '藴': '蕴', + '藶': '苈', + '藷': '𫉄', + '藹': '蔼', + '藺': '蔺', + '蘀': '萚', + '蘄': '蕲', + '蘆': '芦', + '蘇': '苏', + '蘊': '蕴', + '蘋': '苹', + '蘚': '藓', + '蘞': '蔹', + '蘟': '𦻕', + '蘢': '茏', + '蘭': '兰', + '蘺': '蓠', + '蘿': '萝', + '虆': '蔂', + '處': '处', + '虛': '虚', + '虜': '虏', + '號': '号', + '虧': '亏', + '虫': '虫', + '虯': '虬', + '蛺': '蛱', + '蛻': '蜕', + '蜆': '蚬', + '蜡': '蜡', + '蝕': '蚀', + '蝟': '猬', + '蝦': '虾', + '蝨': '虱', + '蝸': '蜗', + '螄': '蛳', + '螞': '蚂', + '螢': '萤', + '螮': '䗖', + '螻': '蝼', + '螿': '螀', + '蟂': '𫋇', + '蟄': '蛰', + '蟈': '蝈', + '蟎': '螨', + '蟘': '𫋌', + '蟜': '𫊸', + '蟣': '虮', + '蟬': '蝉', + '蟯': '蛲', + '蟲': '虫', + '蟳': '𫊻', + '蟶': '蛏', + '蟻': '蚁', + '蠀': '𧏗', + '蠁': '蚃', + '蠅': '蝇', + '蠆': '虿', + '蠍': '蝎', + '蠐': '蛴', + '蠑': '蝾', + '蠔': '蚝', + '蠙': '𧏖', + '蠟': '蜡', + '蠣': '蛎', + '蠦': '𫊮', + '蠨': '蟏', + '蠱': '蛊', + '蠶': '蚕', + '蠻': '蛮', + '蠾': '𧑏', + '衆': '众', + '衊': '蔑', + '術': '术', + '衕': '同', + '衚': '胡', + '衛': '卫', + '衝': '冲', + '表': '表', + '衹': '衹', + '袞': '衮', + '裊': '袅', + '裏': '里', + '補': '补', + '裝': '装', + '裡': '里', + '製': '制', + '複': '复', + '褌': '裈', + '褘': '袆', + '褲': '裤', + '褳': '裢', + '褸': '褛', + '褻': '亵', + '襀': '𫌀', + '襆': '幞', + '襇': '裥', + '襉': '裥', + '襏': '袯', + '襓': '𫋹', + '襖': '袄', + '襗': '𫋷', + '襘': '𫋻', + '襝': '裣', + '襠': '裆', + '襤': '褴', + '襪': '袜', + '襬': '摆', + '襯': '衬', + '襰': '𧝝', + '襲': '袭', + '襴': '襕', + '襵': '𫌇', + '覆': '覆', + '覈': '核', + '見': '见', + '覎': '觃', + '規': '规', + '覓': '觅', + '視': '视', + '覘': '觇', + '覛': '𫌪', + '覡': '觋', + '覥': '觍', + '覦': '觎', + '親': '亲', + '覬': '觊', + '覯': '觏', + '覲': '觐', + '覷': '觑', + '覹': '𫌭', + '覺': '觉', + '覼': '𫌨', + '覽': '览', + '覿': '觌', + '觀': '观', + '觴': '觞', + '觶': '觯', + '觸': '触', + '訁': '讠', + '訂': '订', + '訃': '讣', + '計': '计', + '訊': '讯', + '訌': '讧', + '討': '讨', + '訐': '讦', + '訑': '𫍙', + '訒': '讱', + '訓': '训', + '訕': '讪', + '訖': '讫', + '託': '托', + '記': '记', + '訛': '讹', + '訜': '𫍛', + '訝': '讶', + '訞': '𫍚', + '訟': '讼', + '訢': '䜣', + '訣': '诀', + '訥': '讷', + '訨': '𫟞', + '訩': '讻', + '訪': '访', + '設': '设', + '許': '许', + '訴': '诉', + '訶': '诃', + '診': '诊', + '註': '注', + '証': '证', + '詀': '𧮪', + '詁': '诂', + '詆': '诋', + '詊': '𫟟', + '詎': '讵', + '詐': '诈', + '詑': '𫍡', + '詒': '诒', + '詓': '𫍜', + '詔': '诏', + '評': '评', + '詖': '诐', + '詗': '诇', + '詘': '诎', + '詛': '诅', + '詞': '词', + '詠': '咏', + '詡': '诩', + '詢': '询', + '詣': '诣', + '試': '试', + '詩': '诗', + '詫': '诧', + '詬': '诟', + '詭': '诡', + '詮': '诠', + '詰': '诘', + '話': '话', + '該': '该', + '詳': '详', + '詵': '诜', + '詷': '𫍣', + '詼': '诙', + '詿': '诖', + '誂': '𫍥', + '誄': '诔', + '誅': '诛', + '誆': '诓', + '誇': '夸', + '誋': '𫍪', + '誌': '志', + '認': '认', + '誑': '诳', + '誒': '诶', + '誕': '诞', + '誘': '诱', + '誚': '诮', + '語': '语', + '誠': '诚', + '誡': '诫', + '誣': '诬', + '誤': '误', + '誥': '诰', + '誦': '诵', + '誨': '诲', + '說': '说', + '誫': '𫍨', + '説': '说', + '誰': '谁', + '課': '课', + '誳': '𫍮', + '誴': '𫟡', + '誶': '谇', + '誷': '𫍬', + '誹': '诽', + '誺': '𫍧', + '誼': '谊', + '誾': '訚', + '調': '调', + '諂': '谄', + '諄': '谆', + '談': '谈', + '諉': '诿', + '請': '请', + '諍': '诤', + '諏': '诹', + '諑': '诼', + '諒': '谅', + '論': '论', + '諗': '谂', + '諛': '谀', + '諜': '谍', + '諝': '谞', + '諞': '谝', + '諡': '谥', + '諢': '诨', + '諣': '𫍩', + '諤': '谔', + '諥': '𫍳', + '諦': '谛', + '諧': '谐', + '諫': '谏', + '諭': '谕', + '諮': '咨', + '諯': '𫍱', + '諰': '𫍰', + '諱': '讳', + '諳': '谙', + '諴': '𫍯', + '諶': '谌', + '諷': '讽', + '諸': '诸', + '諺': '谚', + '諼': '谖', + '諾': '诺', + '謀': '谋', + '謁': '谒', + '謂': '谓', + '謄': '誊', + '謅': '诌', + '謆': '𫍸', + '謉': '𫍷', + '謊': '谎', + '謎': '谜', + '謏': '𫍲', + '謐': '谧', + '謔': '谑', + '謖': '谡', + '謗': '谤', + '謙': '谦', + '謚': '谥', + '講': '讲', + '謝': '谢', + '謠': '谣', + '謡': '谣', + '謨': '谟', + '謫': '谪', + '謬': '谬', + '謭': '谫', + '謯': '𫍹', + '謱': '𫍴', + '謳': '讴', + '謸': '𫍵', + '謹': '谨', + '謾': '谩', + '譁': '哗', + '譂': '𫟠', + '譅': '䜧', + '譆': '𫍻', + '證': '证', + '譊': '𫍢', + '譎': '谲', + '譏': '讥', + '譑': '𫍤', + '譖': '谮', + '識': '识', + '譙': '谯', + '譚': '谭', + '譜': '谱', + '譞': '𫍽', + '譟': '噪', + '譨': '𫍦', + '譫': '谵', + '譭': '毁', + '譯': '译', + '議': '议', + '譴': '谴', + '護': '护', + '譸': '诪', + '譽': '誉', + '譾': '谫', + '讀': '读', + '讅': '谉', + '變': '变', + '讋': '詟', + '讌': '䜩', + '讎': '雠', + '讒': '谗', + '讓': '让', + '讕': '谰', + '讖': '谶', + '讚': '赞', + '讜': '谠', + '讞': '谳', + '谷': '谷', + '豈': '岂', + '豎': '竖', + '豐': '丰', + '豔': '艳', + '豬': '猪', + '豵': '𫎆', + '豶': '豮', + '貓': '猫', + '貗': '𫎌', + '貙': '䝙', + '貝': '贝', + '貞': '贞', + '貟': '贠', + '負': '负', + '財': '财', + '貢': '贡', + '貧': '贫', + '貨': '货', + '販': '贩', + '貪': '贪', + '貫': '贯', + '責': '责', + '貯': '贮', + '貰': '贳', + '貲': '赀', + '貳': '贰', + '貴': '贵', + '貶': '贬', + '買': '买', + '貸': '贷', + '貺': '贶', + '費': '费', + '貼': '贴', + '貽': '贻', + '貿': '贸', + '賀': '贺', + '賁': '贲', + '賂': '赂', + '賃': '赁', + '賄': '贿', + '賅': '赅', + '資': '资', + '賈': '贾', + '賊': '贼', + '賑': '赈', + '賒': '赊', + '賓': '宾', + '賕': '赇', + '賙': '赒', + '賚': '赉', + '賜': '赐', + '賝': '𫎩', + '賞': '赏', + '賟': '𧹖', + '賠': '赔', + '賡': '赓', + '賢': '贤', + '賣': '卖', + '賤': '贱', + '賦': '赋', + '賧': '赕', + '質': '质', + '賫': '赍', + '賬': '账', + '賭': '赌', + '賰': '䞐', + '賴': '赖', + '賵': '赗', + '賺': '赚', + '賻': '赙', + '購': '购', + '賽': '赛', + '賾': '赜', + '贃': '𧹗', + '贄': '贽', + '贅': '赘', + '贇': '赟', + '贈': '赠', + '贉': '𫎫', + '贊': '赞', + '贋': '赝', + '贍': '赡', + '贏': '赢', + '贐': '赆', + '贑': '𫎬', + '贓': '赃', + '贔': '赑', + '贖': '赎', + '贗': '赝', + '贚': '𫎦', + '贛': '赣', + '贜': '赃', + '赬': '赪', + '趕': '赶', + '趙': '赵', + '趨': '趋', + '趲': '趱', + '跡': '迹', + '踊': '踊', + '踐': '践', + '踰': '逾', + '踴': '踊', + '蹌': '跄', + '蹔': '𫏐', + '蹕': '跸', + '蹟': '迹', + '蹣': '蹒', + '蹤': '踪', + '蹳': '𫏆', + '蹺': '跷', + '蹻': '𫏋', + '躂': '跶', + '躉': '趸', + '躊': '踌', + '躋': '跻', + '躍': '跃', + '躎': '䟢', + '躑': '踯', + '躒': '跞', + '躓': '踬', + '躕': '蹰', + '躘': '𨀁', + '躚': '跹', + '躝': '𨅬', + '躡': '蹑', + '躥': '蹿', + '躦': '躜', + '躪': '躏', + '軀': '躯', + '軉': '𨉗', + '車': '车', + '軋': '轧', + '軌': '轨', + '軍': '军', + '軏': '𫐄', + '軑': '轪', + '軒': '轩', + '軔': '轫', + '軕': '𫐅', + '軗': '𨐅', + '軛': '轭', + '軜': '𫐇', + '軟': '软', + '軤': '轷', + '軨': '𫐉', + '軫': '轸', + '軬': '𫐊', + '軲': '轱', + '軷': '𫐈', + '軸': '轴', + '軹': '轵', + '軺': '轺', + '軻': '轲', + '軼': '轶', + '軾': '轼', + '軿': '𫐌', + '較': '较', + '輄': '𨐈', + '輅': '辂', + '輇': '辁', + '輈': '辀', + '載': '载', + '輊': '轾', + '輋': '𪨶', + '輒': '辄', + '輓': '挽', + '輔': '辅', + '輕': '轻', + '輖': '𫐏', + '輗': '𫐐', + '輛': '辆', + '輜': '辎', + '輝': '辉', + '輞': '辋', + '輟': '辍', + '輢': '𫐎', + '輥': '辊', + '輦': '辇', + '輨': '𫐑', + '輩': '辈', + '輪': '轮', + '輬': '辌', + '輮': '𫐓', + '輯': '辑', + '輳': '辏', + '輷': '𫐒', + '輸': '输', + '輻': '辐', + '輼': '辒', + '輾': '辗', + '輿': '舆', + '轀': '辒', + '轂': '毂', + '轄': '辖', + '轅': '辕', + '轆': '辘', + '轇': '𫐖', + '轉': '转', + '轊': '𫐕', + '轍': '辙', + '轎': '轿', + '轐': '𫐗', + '轔': '辚', + '轗': '𫐘', + '轟': '轰', + '轠': '𫐙', + '轡': '辔', + '轢': '轹', + '轣': '𫐆', + '轤': '轳', + '辟': '辟', + '辦': '办', + '辭': '辞', + '辮': '辫', + '辯': '辩', + '農': '农', + '迴': '回', + '适': '适', + '逕': '迳', + '這': '这', + '連': '连', + '週': '周', + '進': '进', + '遊': '游', + '運': '运', + '過': '过', + '達': '达', + '違': '违', + '遙': '遥', + '遜': '逊', + '遞': '递', + '遠': '远', + '遡': '溯', + '適': '适', + '遱': '𫐷', + '遲': '迟', + '遷': '迁', + '選': '选', + '遺': '遗', + '遼': '辽', + '邁': '迈', + '還': '还', + '邇': '迩', + '邊': '边', + '邏': '逻', + '邐': '逦', + '郁': '郁', + '郟': '郏', + '郵': '邮', + '鄆': '郓', + '鄉': '乡', + '鄒': '邹', + '鄔': '邬', + '鄖': '郧', + '鄟': '𫑘', + '鄧': '邓', + '鄭': '郑', + '鄰': '邻', + '鄲': '郸', + '鄳': '𫑡', + '鄴': '邺', + '鄶': '郐', + '鄺': '邝', + '酇': '酂', + '酈': '郦', + '酸': '酸', + '醃': '腌', + '醖': '酝', + '醜': '丑', + '醞': '酝', + '醟': '蒏', + '醣': '糖', + '醫': '医', + '醬': '酱', + '醱': '酦', + '醶': '𫑷', + '釀': '酿', + '釁': '衅', + '釃': '酾', + '釅': '酽', + '采': '采', + '釋': '释', + '里': '里', + '釐': '厘', + '釒': '钅', + '釓': '钆', + '釔': '钇', + '釕': '钌', + '釗': '钊', + '釘': '钉', + '釙': '钋', + '釚': '𫟲', + '針': '针', + '釟': '𫓥', + '釣': '钓', + '釤': '钐', + '釦': '扣', + '釧': '钏', + '釨': '𫓦', + '釩': '钒', + '釲': '𫟳', + '釳': '𨰿', + '釵': '钗', + '釷': '钍', + '釹': '钕', + '釺': '钎', + '釾': '䥺', + '鈀': '钯', + '鈁': '钫', + '鈃': '钘', + '鈄': '钭', + '鈅': '钥', + '鈆': '𫓪', + '鈇': '𫓧', + '鈈': '钚', + '鈉': '钠', + '鈋': '𨱂', + '鈍': '钝', + '鈎': '钩', + '鈐': '钤', + '鈑': '钣', + '鈒': '钑', + '鈔': '钞', + '鈕': '钮', + '鈖': '𫟴', + '鈗': '𫟵', + '鈛': '𫓨', + '鈞': '钧', + '鈠': '𨱁', + '鈡': '钟', + '鈣': '钙', + '鈥': '钬', + '鈦': '钛', + '鈧': '钪', + '鈮': '铌', + '鈯': '𨱄', + '鈰': '铈', + '鈲': '𨱃', + '鈳': '钶', + '鈴': '铃', + '鈷': '钴', + '鈸': '钹', + '鈹': '铍', + '鈺': '钰', + '鈽': '钸', + '鈾': '铀', + '鈿': '钿', + '鉀': '钾', + '鉁': '𨱅', + '鉅': '巨', + '鉆': '钻', + '鉈': '铊', + '鉉': '铉', + '鉋': '铇', + '鉍': '铋', + '鉑': '铂', + '鉔': '𫓬', + '鉕': '钷', + '鉗': '钳', + '鉚': '铆', + '鉛': '铅', + '鉝': '𫟷', + '鉞': '钺', + '鉠': '𫓭', + '鉢': '钵', + '鉤': '钩', + '鉦': '钲', + '鉬': '钼', + '鉭': '钽', + '鉳': '锫', + '鉶': '铏', + '鉷': '𫟹', + '鉸': '铰', + '鉺': '铒', + '鉻': '铬', + '鉽': '𫟸', + '鉾': '𫓴', + '鉿': '铪', + '銀': '银', + '銁': '𫓲', + '銂': '𫟻', + '銃': '铳', + '銅': '铜', + '銈': '𫓯', + '銊': '𫓰', + '銍': '铚', + '銏': '𫟶', + '銑': '铣', + '銓': '铨', + '銖': '铢', + '銘': '铭', + '銚': '铫', + '銛': '铦', + '銜': '衔', + '銠': '铑', + '銣': '铷', + '銥': '铱', + '銦': '铟', + '銨': '铵', + '銩': '铥', + '銪': '铕', + '銫': '铯', + '銬': '铐', + '銱': '铞', + '銳': '锐', + '銶': '𨱇', + '銷': '销', + '銹': '锈', + '銻': '锑', + '銼': '锉', + '鋁': '铝', + '鋂': '镅', + '鋃': '锒', + '鋅': '锌', + '鋇': '钡', + '鋉': '𨱈', + '鋌': '铤', + '鋏': '铗', + '鋒': '锋', + '鋗': '𫓶', + '鋙': '铻', + '鋝': '锊', + '鋟': '锓', + '鋠': '𫓵', + '鋣': '铘', + '鋤': '锄', + '鋥': '锃', + '鋦': '锔', + '鋨': '锇', + '鋩': '铓', + '鋪': '铺', + '鋭': '锐', + '鋮': '铖', + '鋯': '锆', + '鋰': '锂', + '鋱': '铽', + '鋶': '锍', + '鋸': '锯', + '鋼': '钢', + '錁': '锞', + '錂': '𨱋', + '錄': '录', + '錆': '锖', + '錇': '锫', + '錈': '锩', + '錏': '铔', + '錐': '锥', + '錒': '锕', + '錕': '锟', + '錘': '锤', + '錙': '锱', + '錚': '铮', + '錛': '锛', + '錜': '𫓻', + '錝': '𫓽', + '錟': '锬', + '錠': '锭', + '錡': '锜', + '錢': '钱', + '錤': '𫓹', + '錥': '𫓾', + '錦': '锦', + '錨': '锚', + '錩': '锠', + '錫': '锡', + '錮': '锢', + '錯': '错', + '録': '录', + '錳': '锰', + '錶': '表', + '錸': '铼', + '錼': '镎', + '錽': '𫓸', + '鍀': '锝', + '鍁': '锨', + '鍃': '锪', + '鍄': '𨱉', + '鍅': '钫', + '鍆': '钔', + '鍇': '锴', + '鍈': '锳', + '鍉': '𫔂', + '鍊': '炼', + '鍋': '锅', + '鍍': '镀', + '鍒': '𫔄', + '鍔': '锷', + '鍘': '铡', + '鍚': '钖', + '鍛': '锻', + '鍠': '锽', + '鍤': '锸', + '鍥': '锲', + '鍩': '锘', + '鍬': '锹', + '鍮': '𨱎', + '鍰': '锾', + '鍵': '键', + '鍶': '锶', + '鍺': '锗', + '鍼': '针', + '鍾': '钟', + '鎂': '镁', + '鎄': '锿', + '鎇': '镅', + '鎈': '𫟿', + '鎊': '镑', + '鎌': '镰', + '鎍': '𫔅', + '鎔': '镕', + '鎖': '锁', + '鎘': '镉', + '鎙': '𫔈', + '鎚': '锤', + '鎛': '镈', + '鎝': '𨱏', + '鎞': '𫔇', + '鎡': '镃', + '鎢': '钨', + '鎣': '蓥', + '鎦': '镏', + '鎧': '铠', + '鎩': '铩', + '鎪': '锼', + '鎬': '镐', + '鎭': '镇', + '鎮': '镇', + '鎯': '𨱍', + '鎰': '镒', + '鎲': '镋', + '鎳': '镍', + '鎵': '镓', + '鎷': '𨰾', + '鎸': '镌', + '鎿': '镎', + '鏃': '镞', + '鏆': '𨱌', + '鏇': '镟', + '鏈': '链', + '鏉': '𨱒', + '鏌': '镆', + '鏍': '镙', + '鏐': '镠', + '鏑': '镝', + '鏗': '铿', + '鏘': '锵', + '鏚': '戚', + '鏜': '镗', + '鏝': '镘', + '鏞': '镛', + '鏟': '铲', + '鏡': '镜', + '鏢': '镖', + '鏤': '镂', + '鏥': '𫔊', + '鏦': '𫓩', + '鏨': '錾', + '鏰': '镚', + '鏵': '铧', + '鏷': '镤', + '鏹': '镪', + '鏺': '䥽', + '鏽': '锈', + '鏾': '𫔌', + '鐃': '铙', + '鐄': '𨱑', + '鐇': '𫔍', + '鐈': '𫓱', + '鐋': '铴', + '鐍': '𫔎', + '鐎': '𨱓', + '鐏': '𨱔', + '鐐': '镣', + '鐒': '铹', + '鐓': '镦', + '鐔': '镡', + '鐗': '锏', + '鐘': '钟', + '鐙': '镫', + '鐝': '镢', + '鐠': '镨', + '鐥': '䦅', + '鐦': '锎', + '鐧': '锏', + '鐨': '镄', + '鐪': '𫓺', + '鐫': '镌', + '鐮': '镰', + '鐯': '䦃', + '鐲': '镯', + '鐳': '镭', + '鐵': '铁', + '鐶': '镮', + '鐸': '铎', + '鐺': '铛', + '鐼': '𫔁', + '鐽': '𫟼', + '鐿': '镱', + '鑀': '锿', + '鑄': '铸', + '鑉': '𫠁', + '鑊': '镬', + '鑌': '镔', + '鑑': '鉴', + '鑒': '鉴', + '鑔': '镲', + '鑕': '锧', + '鑞': '镴', + '鑠': '铄', + '鑣': '镳', + '鑥': '镥', + '鑭': '镧', + '鑰': '钥', + '鑱': '镵', + '鑲': '镶', + '鑴': '𫔔', + '鑷': '镊', + '鑹': '镩', + '鑼': '锣', + '鑽': '钻', + '鑾': '銮', + '鑿': '凿', + '钁': '镢', + '钂': '镋', + '镟': '旋', + '長': '长', + '門': '门', + '閂': '闩', + '閃': '闪', + '閆': '闫', + '閈': '闬', + '閉': '闭', + '開': '开', + '閌': '闶', + '閍': '𨸂', + '閎': '闳', + '閏': '闰', + '閐': '𨸃', + '閑': '闲', + '閒': '闲', + '間': '间', + '閔': '闵', + '閗': '𫔯', + '閘': '闸', + '閝': '𫠂', + '閞': '𫔰', + '閡': '阂', + '閣': '阁', + '閤': '合', + '閥': '阀', + '閨': '闺', + '閩': '闽', + '閫': '阃', + '閬': '阆', + '閭': '闾', + '閱': '阅', + '閲': '阅', + '閵': '𫔴', + '閶': '阊', + '閹': '阉', + '閻': '阎', + '閼': '阏', + '閽': '阍', + '閾': '阈', + '閿': '阌', + '闃': '阒', + '闆': '板', + '闇': '暗', + '闈': '闱', + '闊': '阔', + '闋': '阕', + '闌': '阑', + '闍': '阇', + '闐': '阗', + '闑': '𫔶', + '闒': '阘', + '闓': '闿', + '闔': '阖', + '闕': '阙', + '闖': '闯', + '關': '关', + '闞': '阚', + '闠': '阓', + '闡': '阐', + '闢': '辟', + '闤': '阛', + '闥': '闼', + '阪': '阪', + '陘': '陉', + '陝': '陕', + '陞': '升', + '陣': '阵', + '陰': '阴', + '陳': '陈', + '陸': '陆', + '陽': '阳', + '隉': '陧', + '隊': '队', + '階': '阶', + '隕': '陨', + '際': '际', + '隨': '随', + '險': '险', + '隯': '陦', + '隱': '隐', + '隴': '陇', + '隸': '隶', + '隻': '只', + '雇': '雇', + '雋': '隽', + '雕': '雕', + '雖': '虽', + '雙': '双', + '雛': '雏', + '雜': '杂', + '雞': '鸡', + '離': '离', + '難': '难', + '雲': '云', + '電': '电', + '霢': '霡', + '霣': '𫕥', + '霧': '雾', + '霼': '𪵣', + '霽': '霁', + '靂': '雳', + '靄': '霭', + '靆': '叇', + '靈': '灵', + '靉': '叆', + '靚': '靓', + '靜': '静', + '靝': '靔', + '面': '面', + '靦': '腼', + '靧': '𫖃', + '靨': '靥', + '鞀': '鼗', + '鞏': '巩', + '鞝': '绱', + '鞦': '秋', + '鞽': '鞒', + '鞾': '𫖇', + '韁': '缰', + '韃': '鞑', + '韆': '千', + '韉': '鞯', + '韋': '韦', + '韌': '韧', + '韍': '韨', + '韓': '韩', + '韙': '韪', + '韚': '𫠅', + '韛': '𫖔', + '韜': '韬', + '韝': '鞲', + '韞': '韫', + '韠': '𫖒', + '韻': '韵', + '響': '响', + '頁': '页', + '頂': '顶', + '頃': '顷', + '項': '项', + '順': '顺', + '頇': '顸', + '須': '须', + '頊': '顼', + '頌': '颂', + '頍': '𫠆', + '頎': '颀', + '頏': '颃', + '預': '预', + '頑': '顽', + '頒': '颁', + '頓': '顿', + '頗': '颇', + '領': '领', + '頜': '颌', + '頡': '颉', + '頤': '颐', + '頦': '颏', + '頫': '𫖯', + '頭': '头', + '頮': '颒', + '頰': '颊', + '頲': '颋', + '頴': '颕', + '頵': '𫖳', + '頷': '颔', + '頸': '颈', + '頹': '颓', + '頻': '频', + '頽': '颓', + '顂': '𩓋', + '顃': '𩖖', + '顅': '𫖶', + '顆': '颗', + '題': '题', + '額': '额', + '顎': '颚', + '顏': '颜', + '顒': '颙', + '顓': '颛', + '顔': '颜', + '顗': '𫖮', + '願': '愿', + '顙': '颡', + '顛': '颠', + '類': '类', + '顢': '颟', + '顣': '𫖹', + '顥': '颢', + '顧': '顾', + '顫': '颤', + '顬': '颥', + '顯': '显', + '顰': '颦', + '顱': '颅', + '顳': '颞', + '顴': '颧', + '風': '风', + '颭': '飐', + '颮': '飑', + '颯': '飒', + '颰': '𩙥', + '颱': '台', + '颳': '刮', + '颶': '飓', + '颷': '𩙪', + '颸': '飔', + '颺': '飏', + '颻': '飖', + '颼': '飕', + '颾': '𩙫', + '飀': '飗', + '飄': '飘', + '飆': '飙', + '飈': '飚', + '飋': '𫗋', + '飛': '飞', + '飠': '饣', + '飢': '饥', + '飣': '饤', + '飥': '饦', + '飦': '𫗞', + '飩': '饨', + '飪': '饪', + '飫': '饫', + '飭': '饬', + '飯': '饭', + '飱': '飧', + '飲': '饮', + '飴': '饴', + '飵': '𫗢', + '飶': '𫗣', + '飼': '饲', + '飽': '饱', + '飾': '饰', + '飿': '饳', + '餃': '饺', + '餄': '饸', + '餅': '饼', + '餉': '饷', + '養': '养', + '餌': '饵', + '餎': '饹', + '餏': '饻', + '餑': '饽', + '餒': '馁', + '餓': '饿', + '餔': '𫗦', + '餕': '馂', + '餖': '饾', + '餗': '𫗧', + '餘': '余', + '餚': '肴', + '餛': '馄', + '餜': '馃', + '餞': '饯', + '餡': '馅', + '餦': '𫗠', + '餧': '𫗪', + '館': '馆', + '餪': '𫗬', + '餫': '𫗥', + '餬': '糊', + '餭': '𫗮', + '餱': '糇', + '餳': '饧', + '餵': '喂', + '餶': '馉', + '餷': '馇', + '餸': '𩠌', + '餺': '馎', + '餼': '饩', + '餾': '馏', + '餿': '馊', + '饁': '馌', + '饃': '馍', + '饅': '馒', + '饈': '馐', + '饉': '馑', + '饊': '馓', + '饋': '馈', + '饌': '馔', + '饑': '饥', + '饒': '饶', + '饗': '飨', + '饘': '𫗴', + '饜': '餍', + '饞': '馋', + '饟': '𫗵', + '饠': '𫗩', + '饢': '馕', + '馬': '马', + '馭': '驭', + '馮': '冯', + '馯': '𫘛', + '馱': '驮', + '馳': '驰', + '馴': '驯', + '馹': '驲', + '馼': '𫘜', + '駁': '驳', + '駃': '𫘝', + '駊': '𫘟', + '駎': '𩧨', + '駐': '驻', + '駑': '驽', + '駒': '驹', + '駔': '驵', + '駕': '驾', + '駘': '骀', + '駙': '驸', + '駚': '𩧫', + '駛': '驶', + '駝': '驼', + '駞': '𫘞', + '駟': '驷', + '駡': '骂', + '駢': '骈', + '駤': '𫘠', + '駧': '𩧲', + '駩': '𩧴', + '駫': '𫘡', + '駭': '骇', + '駰': '骃', + '駱': '骆', + '駶': '𩧺', + '駸': '骎', + '駻': '𫘣', + '駿': '骏', + '騁': '骋', + '騂': '骍', + '騃': '𫘤', + '騄': '𫘧', + '騅': '骓', + '騉': '𫘥', + '騊': '𫘦', + '騌': '骔', + '騍': '骒', + '騎': '骑', + '騏': '骐', + '騔': '𩨀', + '騖': '骛', + '騙': '骗', + '騚': '𩨊', + '騜': '𫘩', + '騝': '𩨃', + '騟': '𩨈', + '騠': '𫘨', + '騤': '骙', + '騧': '䯄', + '騪': '𩨄', + '騫': '骞', + '騭': '骘', + '騮': '骝', + '騰': '腾', + '騱': '𫘬', + '騴': '𫘫', + '騵': '𫘪', + '騶': '驺', + '騷': '骚', + '騸': '骟', + '騻': '𫘭', + '騼': '𫠋', + '騾': '骡', + '驀': '蓦', + '驁': '骜', + '驂': '骖', + '驃': '骠', + '驄': '骢', + '驅': '驱', + '驊': '骅', + '驋': '𩧯', + '驌': '骕', + '驍': '骁', + '驏': '骣', + '驓': '𫘯', + '驕': '骄', + '驗': '验', + '驙': '𫘰', + '驚': '惊', + '驛': '驿', + '驟': '骤', + '驢': '驴', + '驤': '骧', + '驥': '骥', + '驦': '骦', + '驨': '𫘱', + '驪': '骊', + '驫': '骉', + '骯': '肮', + '髏': '髅', + '髒': '脏', + '體': '体', + '髕': '髌', + '髖': '髋', + '髮': '发', + '鬆': '松', + '鬍': '胡', + '鬖': '𩭹', + '鬚': '须', + '鬠': '𫘽', + '鬢': '鬓', + '鬥': '斗', + '鬧': '闹', + '鬨': '哄', + '鬩': '阋', + '鬮': '阄', + '鬱': '郁', + '鬹': '鬶', + '魎': '魉', + '魘': '魇', + '魚': '鱼', + '魛': '鱽', + '魟': '𫚉', + '魢': '鱾', + '魥': '𩽹', + '魦': '𫚌', + '魨': '鲀', + '魯': '鲁', + '魴': '鲂', + '魵': '𫚍', + '魷': '鱿', + '魺': '鲄', + '魽': '𫠐', + '鮁': '鲅', + '鮃': '鲆', + '鮄': '𫚒', + '鮅': '𫚑', + '鮆': '𫚖', + '鮊': '鲌', + '鮋': '鲉', + '鮍': '鲏', + '鮎': '鲇', + '鮐': '鲐', + '鮑': '鲍', + '鮒': '鲋', + '鮓': '鲊', + '鮕': '𩾀', + '鮚': '鲒', + '鮜': '鲘', + '鮝': '鲞', + '鮞': '鲕', + '鮟': '𩽾', + '鮣': '䲟', + '鮤': '𫚓', + '鮦': '鲖', + '鮪': '鲔', + '鮫': '鲛', + '鮭': '鲑', + '鮮': '鲜', + '鮯': '𫚗', + '鮰': '𫚔', + '鮳': '鲓', + '鮵': '𫚛', + '鮶': '鲪', + '鮸': '𩾃', + '鮺': '鲝', + '鮿': '𫚚', + '鯀': '鲧', + '鯁': '鲠', + '鯄': '𩾁', + '鯆': '𫚙', + '鯇': '鲩', + '鯉': '鲤', + '鯊': '鲨', + '鯒': '鲬', + '鯔': '鲻', + '鯕': '鲯', + '鯖': '鲭', + '鯗': '鲞', + '鯛': '鲷', + '鯝': '鲴', + '鯞': '𫚡', + '鯡': '鲱', + '鯢': '鲵', + '鯤': '鲲', + '鯧': '鲳', + '鯨': '鲸', + '鯪': '鲮', + '鯫': '鲰', + '鯬': '𫚞', + '鯰': '鲶', + '鯱': '𩾇', + '鯴': '鲺', + '鯶': '𩽼', + '鯷': '鳀', + '鯽': '鲫', + '鯾': '𫚣', + '鯿': '鳊', + '鰁': '鳈', + '鰂': '鲗', + '鰃': '鳂', + '鰆': '䲠', + '鰈': '鲽', + '鰉': '鳇', + '鰋': '𫚢', + '鰌': '䲡', + '鰍': '鳅', + '鰏': '鲾', + '鰐': '鳄', + '鰑': '𫚊', + '鰒': '鳆', + '鰓': '鳃', + '鰕': '𫚥', + '鰛': '鳁', + '鰜': '鳒', + '鰟': '鳑', + '鰠': '鳋', + '鰣': '鲥', + '鰤': '𫚕', + '鰥': '鳏', + '鰦': '𫚤', + '鰧': '䲢', + '鰨': '鳎', + '鰩': '鳐', + '鰫': '𫚦', + '鰭': '鳍', + '鰮': '鳁', + '鰱': '鲢', + '鰲': '鳌', + '鰳': '鳓', + '鰵': '鳘', + '鰷': '鲦', + '鰹': '鲣', + '鰺': '鲹', + '鰻': '鳗', + '鰼': '鳛', + '鰽': '𫚧', + '鰾': '鳔', + '鱂': '鳉', + '鱄': '𫚋', + '鱅': '鳙', + '鱆': '𫠒', + '鱇': '𩾌', + '鱈': '鳕', + '鱉': '鳖', + '鱊': '𫚪', + '鱒': '鳟', + '鱔': '鳝', + '鱖': '鳜', + '鱗': '鳞', + '鱘': '鲟', + '鱝': '鲼', + '鱟': '鲎', + '鱠': '鲙', + '鱢': '𫚫', + '鱣': '鳣', + '鱤': '鳡', + '鱧': '鳢', + '鱨': '鲿', + '鱭': '鲚', + '鱮': '𫚈', + '鱯': '鳠', + '鱲': '𫚭', + '鱷': '鳄', + '鱸': '鲈', + '鱺': '鲡', + '鳥': '鸟', + '鳧': '凫', + '鳩': '鸠', + '鳬': '凫', + '鳲': '鸤', + '鳳': '凤', + '鳴': '鸣', + '鳶': '鸢', + '鳷': '𫛛', + '鳼': '𪉃', + '鳽': '𫛚', + '鳾': '䴓', + '鴀': '𫛜', + '鴃': '𫛞', + '鴅': '𫛝', + '鴆': '鸩', + '鴇': '鸨', + '鴉': '鸦', + '鴐': '𫛤', + '鴒': '鸰', + '鴔': '𫛡', + '鴕': '鸵', + '鴗': '𫁡', + '鴛': '鸳', + '鴜': '𪉈', + '鴝': '鸲', + '鴞': '鸮', + '鴟': '鸱', + '鴣': '鸪', + '鴥': '𫛣', + '鴦': '鸯', + '鴨': '鸭', + '鴮': '𫛦', + '鴯': '鸸', + '鴰': '鸹', + '鴲': '𪉆', + '鴳': '𫛩', + '鴴': '鸻', + '鴷': '䴕', + '鴻': '鸿', + '鴽': '𫛪', + '鴿': '鸽', + '鵁': '䴔', + '鵂': '鸺', + '鵃': '鸼', + '鵊': '𫛥', + '鵐': '鹀', + '鵑': '鹃', + '鵒': '鹆', + '鵓': '鹁', + '鵚': '𪉍', + '鵜': '鹈', + '鵝': '鹅', + '鵟': '𫛭', + '鵠': '鹄', + '鵡': '鹉', + '鵧': '𫛨', + '鵩': '𫛳', + '鵪': '鹌', + '鵫': '𫛱', + '鵬': '鹏', + '鵮': '鹐', + '鵯': '鹎', + '鵰': '雕', + '鵲': '鹊', + '鵷': '鹓', + '鵾': '鹍', + '鶄': '䴖', + '鶇': '鸫', + '鶉': '鹑', + '鶊': '鹒', + '鶌': '𫛵', + '鶒': '𫛶', + '鶓': '鹋', + '鶖': '鹙', + '鶗': '𫛸', + '鶘': '鹕', + '鶚': '鹗', + '鶡': '鹖', + '鶥': '鹛', + '鶦': '𫛷', + '鶩': '鹜', + '鶪': '䴗', + '鶬': '鸧', + '鶭': '𫛯', + '鶯': '莺', + '鶰': '𫛫', + '鶲': '鹟', + '鶴': '鹤', + '鶹': '鹠', + '鶺': '鹡', + '鶻': '鹘', + '鶼': '鹣', + '鶿': '鹚', + '鷀': '鹚', + '鷁': '鹢', + '鷂': '鹞', + '鷄': '鸡', + '鷅': '𫛽', + '鷈': '䴘', + '鷉': '䴘', + '鷊': '鹝', + '鷐': '𫜀', + '鷓': '鹧', + '鷔': '𪉑', + '鷖': '鹥', + '鷗': '鸥', + '鷙': '鸷', + '鷚': '鹨', + '鷣': '𫜃', + '鷤': '𫛴', + '鷥': '鸶', + '鷦': '鹪', + '鷨': '𪉊', + '鷩': '𫜁', + '鷫': '鹔', + '鷯': '鹩', + '鷲': '鹫', + '鷳': '鹇', + '鷴': '鹇', + '鷷': '𫜄', + '鷸': '鹬', + '鷹': '鹰', + '鷺': '鹭', + '鷽': '鸴', + '鷿': '䴙', + '鸂': '㶉', + '鸇': '鹯', + '鸊': '䴙', + '鸋': '𫛢', + '鸌': '鹱', + '鸏': '鹲', + '鸕': '鸬', + '鸗': '𫛟', + '鸘': '鹴', + '鸚': '鹦', + '鸛': '鹳', + '鸝': '鹂', + '鸞': '鸾', + '鹵': '卤', + '鹹': '咸', + '鹺': '鹾', + '鹼': '碱', + '鹽': '盐', + '麗': '丽', + '麥': '麦', + '麨': '𪎊', + '麩': '麸', + '麪': '面', + '麫': '面', + '麬': '𤿲', + '麯': '曲', + '麲': '𪎉', + '麳': '𪎌', + '麴': '曲', + '麵': '面', + '麷': '𫜑', + '麼': '么', + '麽': '么', + '黃': '黄', + '黌': '黉', + '點': '点', + '黨': '党', + '黲': '黪', + '黴': '霉', + '黶': '黡', + '黷': '黩', + '黽': '黾', + '黿': '鼋', + '鼂': '鼌', + '鼉': '鼍', + '鼕': '冬', + '鼴': '鼹', + '齇': '齄', + '齊': '齐', + '齋': '斋', + '齎': '赍', + '齏': '齑', + '齒': '齿', + '齔': '龀', + '齕': '龁', + '齗': '龂', + '齙': '龅', + '齜': '龇', + '齟': '龃', + '齠': '龆', + '齡': '龄', + '齣': '出', + '齦': '龈', + '齧': '啮', + '齩': '𫜪', + '齪': '龊', + '齬': '龉', + '齭': '𫜭', + '齯': '𫠜', + '齰': '𫜬', + '齲': '龋', + '齴': '𫜮', + '齶': '腭', + '齷': '龌', + '齾': '𫜰', + '龍': '龙', + '龎': '厐', + '龐': '庞', + '龑': '䶮', + '龓': '𫜲', + '龔': '龚', + '龕': '龛', + '龜': '龟', + '龭': '𩨎', + '龯': '𨱆', + '鿁': '䜤', + '𠁞': '𠀾', + '𠌥': '𠆿', + '𠏢': '𠉗', + '𠐊': '𫝋', + '𠗣': '㓆', + '𠞆': '𠛆', + '𠠎': '𠚳', + '𠬙': '𪠡', + '𠽃': '𪠺', + '𠿕': '𪜎', + '𡂡': '𪢒', + '𡃄': '𪡺', + '𡃕': '𠴛', + '𡃤': '𪢐', + '𡄔': '𠴢', + '𡄣': '𠵸', + '𡅏': '𠲥', + '𡅯': '𪢖', + '𡑭': '𡋗', + '𡓁': '𪤄', + '𡓾': '𡋀', + '𡔖': '𡍣', + '𡞵': '㛟', + '𡟫': '𫝪', + '𡠹': '㛿', + '𡡎': '𡞱', + '𡢃': '㛠', + '𡮉': '𡭜', + '𡮣': '𡭬', + '𡳳': '𡳃', + '𡸗': '𪨩', + '𡹬': '𪨹', + '𡻕': '岁', + '𡽗': '𡸃', + '𡾱': '㟜', + '𡿖': '𪩛', + '𢍰': '𪪴', + '𢠼': '𢙑', + '𢣐': '𪬚', + '𢣚': '𢘝', + '𢣭': '𢘞', + '𢤩': '𪫡', + '𢤱': '𢘙', + '𢤿': '𪬯', + '𢯷': '𪭝', + '𢶒': '𪭯', + '𢶫': '𢫞', + '𢷬': '𢭏', + '𢷮': '𢫊', + '𢹿': '𢬦', + '𢺳': '𪮳', + '𣈶': '暅', + '𣋋': '𣈣', + '𣍐': '𠊉', + '𣙎': '㭣', + '𣜬': '𪳗', + '𣝕': '𣘷', + '𣞻': '𣘓', + '𣠩': '𣞎', + '𣠲': '𣑶', + '𣯩': '𣯣', + '𣯴': '𣭤', + '𣯶': '毶', + '𣽏': '𪶮', + '𣾷': '㳢', + '𣿉': '𣶫', + '𤁣': '𣺽', + '𤄷': '𪶒', + '𤅶': '𣷷', + '𤑳': '𤎻', + '𤑹': '𪹀', + '𤒎': '𤊀', + '𤒻': '𪹹', + '𤓌': '𪹠', + '𤓩': '𤊰', + '𤘀': '𪺣', + '𤛮': '𤙯', + '𤛱': '𫞢', + '𤜆': '𪺪', + '𤠮': '𪺸', + '𤢟': '𤝢', + '𤢻': '𢢐', + '𤩂': '𫞧', + '𤪺': '㻘', + '𤫩': '㻏', + '𤬅': '𪼴', + '𤳷': '𪽝', + '𤳸': '𤳄', + '𤷃': '𪽭', + '𤸫': '𤶧', + '𤺔': '𪽴', + '𥊝': '𥅿', + '𥌃': '𥅘', + '𥏝': '𪿊', + '𥕥': '𥐰', + '𥖅': '𥐯', + '𥖲': '𪿞', + '𥗇': '𪿵', + '𥜐': '𫀓', + '𥜰': '𫀌', + '𥞵': '𥞦', + '𥢢': '䅪', + '𥢶': '𫞷', + '𥢷': '𫀮', + '𥨐': '𥧂', + '𥪂': '𥩺', + '𥯤': '𫁳', + '𥴨': '𫂖', + '𥴼': '𫁺', + '𥵃': '𥱔', + '𥵊': '𥭉', + '𥶽': '𫁱', + '𥸠': '𥮋', + '𥻦': '𫂿', + '𥼽': '𥹥', + '𥽖': '𥺇', + '𥾯': '𫄝', + '𥿊': '𦈈', + '𦀖': '𫄦', + '𦂅': '𦈒', + '𦃄': '𦈗', + '𦃩': '𫄯', + '𦅇': '𫄪', + '𦅈': '𫄵', + '𦆲': '𫟇', + '𦒀': '𫅥', + '𦔖': '𫅼', + '𦘧': '𡳒', + '𦟼': '𫆝', + '𦠅': '𫞅', + '𦡝': '𫆫', + '𦢈': '𣍨', + '𦣎': '𦟗', + '𦧺': '𫇘', + '𦪙': '䑽', + '𦪽': '𦨩', + '𦱌': '𫇪', + '𦾟': '𦶻', + '𧎈': '𧌥', + '𧒯': '𫊹', + '𧔥': '𧒭', + '𧕟': '𧉐', + '𧜗': '䘞', + '𧜵': '䙊', + '𧝞': '䘛', + '𧞫': '𫌋', + '𧟀': '𧝧', + '𧡴': '𫌫', + '𧢄': '𫌬', + '𧦝': '𫍞', + '𧦧': '𫍟', + '𧩕': '𫍭', + '𧩙': '䜥', + '𧩼': '𫍶', + '𧫝': '𫍺', + '𧬤': '𫍼', + '𧭈': '𫍾', + '𧭹': '𫍐', + '𧳟': '𧳕', + '𧵳': '䞌', + '𧶔': '𧹓', + '𧶧': '䞎', + '𧷎': '𪠀', + '𧸘': '𫎨', + '𧹈': '𪥠', + '𧽯': '𫎸', + '𨂐': '𫏌', + '𨄣': '𨀱', + '𨅍': '𨁴', + '𨆪': '𫏕', + '𨇁': '𧿈', + '𨇞': '𨅫', + '𨇤': '𫏨', + '𨇰': '𫏞', + '𨇽': '𫏑', + '𨈊': '𨂺', + '𨈌': '𨄄', + '𨊰': '䢀', + '𨊸': '䢁', + '𨊻': '𨐆', + '𨋢': '䢂', + '𨌈': '𫐍', + '𨍰': '𫐔', + '𨎌': '𫐋', + '𨎮': '𨐉', + '𨏠': '𨐇', + '𨏥': '𨐊', + '𨞺': '𫟫', + '𨟊': '𫟬', + '𨢿': '𨡙', + '𨣈': '𨡺', + '𨣞': '𨟳', + '𨣧': '𨠨', + '𨤻': '𨤰', + '𨥛': '𨱀', + '𨥟': '𫓫', + '𨦫': '䦀', + '𨧜': '䦁', + '𨧰': '𫟽', + '𨧱': '𨱊', + '𨨛': '𫓼', + '𨨢': '𫓿', + '𨩰': '𫟾', + '𨪕': '𫓮', + '𨫒': '𨱐', + '𨬖': '𫔏', + '𨭖': '𫔑', + '𨭸': '𫔐', + '𨮂': '𨱕', + '𨮳': '𫔒', + '𨯅': '䥿', + '𨯟': '𫔓', + '𨰃': '𫔉', + '𨰋': '𫓳', + '𨰥': '𫔕', + '𨰲': '𫔃', + '𨲳': '𫔖', + '𨳑': '𨸁', + '𨳕': '𨸀', + '𨴗': '𨸅', + '𨴹': '𫔲', + '𨵩': '𨸆', + '𨵸': '𨸇', + '𨶀': '𨸉', + '𨶏': '𨸊', + '𨶮': '𨸌', + '𨶲': '𨸋', + '𨷲': '𨸎', + '𨼳': '𫔽', + '𨽏': '𨸘', + '𩀨': '𫕚', + '𩅙': '𫕨', + '𩎖': '𫖑', + '𩎢': '𩏾', + '𩏂': '𫖓', + '𩏠': '𫖖', + '𩏪': '𩏽', + '𩏷': '𫃗', + '𩑔': '𫖪', + '𩒎': '𫖭', + '𩓣': '𩖕', + '𩓥': '𫖵', + '𩔑': '𫖷', + '𩔳': '𫖴', + '𩖰': '𫠇', + '𩗀': '𩙦', + '𩗓': '𫗈', + '𩗴': '𫗉', + '𩘀': '𩙩', + '𩘝': '𩙭', + '𩘹': '𩙨', + '𩘺': '𩙬', + '𩙈': '𩙰', + '𩚛': '𩟿', + '𩚥': '𩠀', + '𩚩': '𫗡', + '𩚵': '𩠁', + '𩛆': '𩠂', + '𩛌': '𫗤', + '𩛡': '𫗨', + '𩛩': '𩠃', + '𩜇': '𩠉', + '𩜦': '𩠆', + '𩜵': '𩠊', + '𩝔': '𩠋', + '𩝽': '𫗳', + '𩞄': '𩠎', + '𩞦': '𩠏', + '𩞯': '䭪', + '𩟐': '𩠅', + '𩟗': '𫗚', + '𩠴': '𩠠', + '𩡣': '𩡖', + '𩡺': '𩧦', + '𩢡': '𩧬', + '𩢴': '𩧵', + '𩢸': '𩧳', + '𩢾': '𩧮', + '𩣏': '𩧶', + '𩣑': '䯃', + '𩣫': '𩧸', + '𩣵': '𩧻', + '𩣺': '𩧼', + '𩤊': '𩧩', + '𩤙': '𩨆', + '𩤲': '𩨉', + '𩤸': '𩨅', + '𩥄': '𩨋', + '𩥇': '𩨍', + '𩥉': '𩧱', + '𩥑': '𩨌', + '𩦠': '𫠌', + '𩧆': '𩨐', + '𩭙': '𩬣', + '𩯁': '𫙂', + '𩯳': '𩯒', + '𩰀': '𩬤', + '𩰹': '𩰰', + '𩳤': '𩲒', + '𩴵': '𩴌', + '𩵦': '𫠏', + '𩵩': '𩽺', + '𩵹': '𩽻', + '𩶁': '𫚎', + '𩶘': '䲞', + '𩶰': '𩽿', + '𩶱': '𩽽', + '𩷰': '𩾄', + '𩸃': '𩾅', + '𩸄': '𫚝', + '𩸡': '𫚟', + '𩸦': '𩾆', + '𩻗': '𫚨', + '𩻬': '𫚩', + '𩻮': '𫚘', + '𩼶': '𫚬', + '𩽇': '𩾎', + '𩿅': '𫠖', + '𩿤': '𫛠', + '𩿪': '𪉄', + '𪀖': '𫛧', + '𪀦': '𪉅', + '𪀾': '𪉋', + '𪁈': '𪉉', + '𪁖': '𪉌', + '𪂆': '𪉎', + '𪃍': '𪉐', + '𪃏': '𪉏', + '𪃒': '𫛻', + '𪃧': '𫛹', + '𪄆': '𪉔', + '𪄕': '𪉒', + '𪅂': '𫜂', + '𪆷': '𫛾', + '𪇳': '𪉕', + '𪈼': '𪉓', + '𪉸': '𫜊', + '𪋿': '𪎍', + '𪌭': '𫜓', + '𪍠': '𫜕', + '𪓰': '𫜟', + '𪔵': '𪔭', + '𪘀': '𪚏', + '𪘯': '𪚐', + '𪙏': '𫜯', + '𪟖': '𠛾', + '𪷓': '𣶭', + '𫒡': '𫓷', + '𫜦': '𫜫', +}